This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git


The following commit(s) were added to refs/heads/master by this push:
     new 81a9651  feat(web): show each agent's outbound uploads on its page
81a9651 is described below

commit 81a96514dcfb2d3fa5245821e4f22a306221a594
Author: Yang Luo <[email protected]>
AuthorDate: Thu Sep 24 23:38:46 2026 +0800

    feat(web): show each agent's outbound uploads on its page
---
 web/src/components/AgentEgressCard.tsx | 227 +++++++++++++++++++++++++++++++++
 web/src/components/AgentGridCard.tsx   |   9 ++
 web/src/components/AgentIcon.tsx       |   1 +
 web/src/lib/egress.ts                  |  73 +++++++++++
 web/src/locales/en/data.json           |  26 ++++
 web/src/locales/zh/data.json           |  26 ++++
 web/src/pages/AgentDetailPage.tsx      |   3 +
 7 files changed, 365 insertions(+)

diff --git a/web/src/components/AgentEgressCard.tsx 
b/web/src/components/AgentEgressCard.tsx
new file mode 100644
index 0000000..c5ac7de
--- /dev/null
+++ b/web/src/components/AgentEgressCard.tsx
@@ -0,0 +1,227 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import i18next from "i18next";
+
+import {DataTable, type Column} from "@/components/shared/data-table";
+import {Fold} from "@/components/shared/fold";
+import {MessageAlert} from "@/components/ui/alert";
+import {Badge, type BadgeVariant} from "@/components/ui/badge";
+import {SimpleTooltip} from "@/components/ui/tooltip";
+import {formatBytes} from "@/lib/agent-configs";
+import {egressDetailOf, hasEgress, useAgentEgress} from "@/lib/egress";
+import type {Agent, AgentRecord, EgressDestination, EgressSnapshot} from 
"@/types";
+
+const kinds: Record<string, {label: string; variant: BadgeVariant}> = {
+  model: {label: "Model API", variant: "muted"},
+  storage: {label: "Object storage", variant: "danger"},
+  telemetry: {label: "Telemetry", variant: "warning"},
+  local: {label: "Local process", variant: "info"},
+  other: {label: "Other", variant: "outline"},
+};
+
+function t(key: string, values: Record<string, string | number> = {}) {
+  let text = i18next.t(`agent:${key}`);
+  for (const name of Object.keys(values)) {
+    text = text.replace(`{${name}}`, String(values[name]));
+  }
+  return text;
+}
+
+function KindBadge({kind}: {kind: string}) {
+  const entry = kinds[kind] ?? kinds.other;
+  return <Badge variant={entry.variant}>{t(entry.label)}</Badge>;
+}
+
+function bytes(value: number | undefined, counted: boolean) {
+  return counted ? formatBytes(value) || "0 B" : "-";
+}
+
+/** A destination by name when the resolver had one; a local proxy by the
+ *  process behind it, since that is all this host can see of where it went. */
+function DestinationName({host, address, kind, via}: {host?: string; address: 
string; kind: string; via?: string}) {
+  const primary = kind === "local" ? via || address : host || address;
+  const secondary = primary === address ? "" : address;
+  return (
+    <div className="flex min-w-0 flex-col">
+      <span className="truncate">{primary}</span>
+      {secondary ? <code className="text-muted-foreground truncate 
text-xs">{secondary}</code> : null}
+    </div>
+  );
+}
+
+function SnapshotAlert({agent, snapshot}: {agent: Agent; snapshot: 
EgressSnapshot}) {
+  const title = snapshot.accepted
+    ? t("Snapshot uploaded", {agent: agent.name})
+    : t("Snapshot packed", {agent: agent.name});
+  const facts = [t("Snapshot size", {size: 
formatBytes(snapshot.encryptedBytes) || "-"})];
+  if (snapshot.files) {
+    facts.push(t("Snapshot files", {files: snapshot.files, gitFiles: 
snapshot.gitFiles ?? 0}));
+  }
+  if (snapshot.failures) {
+    facts.push(t("Snapshot retries", {count: snapshot.failures}));
+  }
+
+  return (
+    <MessageAlert
+      variant={snapshot.accepted ? "destructive" : "warning"}
+      title={title}
+      description={
+        <div className="space-y-1">
+          <code className="block break-all text-xs">{snapshot.workspace}</code>
+          <p>{facts.join(" · ")}</p>
+          {snapshot.gitFiles ? <p>{t("Snapshot rotate secrets")}</p> : null}
+        </div>
+      }
+    />
+  );
+}
+
+function FindingsTable({findings, counted}: {findings: AgentRecord[]; counted: 
boolean}) {
+  const columns: Column<AgentRecord>[] = [
+    {
+      title: t("Time"),
+      key: "createdTime",
+      dataIndex: "createdTime",
+      width: "180px",
+      render: (value: string) => new Date(value).toLocaleString(),
+    },
+    {
+      title: t("Finding"),
+      key: "action",
+      width: "140px",
+      render: (_value, record) => (
+        <Badge variant="danger">
+          {t(record.action === "large-upload" ? "Large upload" : "Object 
storage")}
+        </Badge>
+      ),
+    },
+    {
+      title: t("Destination"),
+      key: "destination",
+      render: (_value, record) => {
+        const detail = egressDetailOf(record);
+        return detail ? <DestinationName {...detail} /> : 
<span>{String(record.object ?? "-")}</span>;
+      },
+    },
+    {
+      title: t("Sent"),
+      key: "sent",
+      width: "110px",
+      render: (_value, record) => {
+        const detail = egressDetailOf(record);
+        return <span className="tabular-nums">{bytes(detail?.bytesOut, 
detail?.counted ?? counted)}</span>;
+      },
+    },
+  ];
+
+  return (
+    <DataTable
+      columns={columns}
+      dataSource={findings}
+      rowKey={record => String(record.id)}
+      pageSize={0}
+      emptyText={t("No suspicious uploads")}
+    />
+  );
+}
+
+function DestinationsTable({destinations, counted}: {destinations: 
EgressDestination[]; counted: boolean}) {
+  const columns: Column<EgressDestination>[] = [
+    {
+      title: t("Destination"),
+      key: "destination",
+      render: (_value, dest) => <DestinationName {...dest} />,
+    },
+    {
+      title: t("Kind"),
+      key: "kind",
+      width: "150px",
+      render: (_value, dest) => (
+        <span className="flex items-center gap-1">
+          <KindBadge kind={dest.kind} />
+          {dest.flagged ? <span className="bg-destructive size-1.5 
rounded-full" /> : null}
+        </span>
+      ),
+    },
+    {
+      title: t("Sent"),
+      key: "bytesOut",
+      width: "100px",
+      render: (_value, dest) => <span 
className="tabular-nums">{bytes(dest.bytesOut, counted)}</span>,
+    },
+    {
+      title: t("Received"),
+      key: "bytesIn",
+      width: "100px",
+      render: (_value, dest) => <span 
className="tabular-nums">{bytes(dest.bytesIn, counted)}</span>,
+    },
+    {
+      title: t("Last seen"),
+      key: "lastSeen",
+      dataIndex: "lastSeen",
+      width: "180px",
+      render: (value: string) => new Date(value).toLocaleString(),
+    },
+  ];
+
+  return (
+    <DataTable
+      columns={columns}
+      dataSource={destinations}
+      rowKey={dest => `${dest.kind}|${dest.host ?? ""}|${dest.via ?? 
""}|${dest.address}`}
+      pageSize={0}
+      emptyText={t("No connections yet")}
+    />
+  );
+}
+
+/** Where this agent's own processes send data. The proxy only sees what goes
+ *  to the model; a repository posted straight to cloud storage shows here. */
+export function AgentEgressCard({agent, enabled = true}: {agent: Agent; 
enabled?: boolean}) {
+  const egress = useAgentEgress(agent.agentId, agent.owner, enabled);
+  if (!hasEgress(egress)) {
+    return null;
+  }
+
+  return (
+    <section className="space-y-3">
+      <div className="flex flex-wrap items-center gap-2">
+        <h2 className="text-base font-semibold">{t("Outbound traffic")}</h2>
+        {egress.supported && !egress.counted ? (
+          <SimpleTooltip title={t("Bytes not counted hint")}>
+            <span>
+              <Badge variant="muted">{t("Bytes not counted")}</Badge>
+            </span>
+          </SimpleTooltip>
+        ) : null}
+      </div>
+      <p className="text-muted-foreground text-sm">{t("Outbound traffic 
hint")}</p>
+
+      {egress.snapshots.map(snapshot => (
+        <SnapshotAlert key={snapshot.workspace} agent={agent} 
snapshot={snapshot} />
+      ))}
+
+      {egress.supported || egress.findings.length > 0 ? (
+        <FindingsTable findings={egress.findings} counted={egress.counted} />
+      ) : null}
+
+      {egress.supported ? (
+        <Fold title={t("Destinations ({count})", {count: 
egress.destinations.length})}>
+          <DestinationsTable destinations={egress.destinations} 
counted={egress.counted} />
+        </Fold>
+      ) : null}
+    </section>
+  );
+}
diff --git a/web/src/components/AgentGridCard.tsx 
b/web/src/components/AgentGridCard.tsx
index 670e091..91f7790 100644
--- a/web/src/components/AgentGridCard.tsx
+++ b/web/src/components/AgentGridCard.tsx
@@ -137,6 +137,15 @@ export function AgentGridCard({
               <Link to={detail} className="truncate text-sm font-medium 
hover:underline">
                 {agent.name}
               </Link>
+              {agent.egressFindings ? (
+                <SimpleTooltip
+                  title={i18next
+                    .t("agent:Suspicious uploads recorded")
+                    .replace("{count}", String(agent.egressFindings))}
+                >
+                  <span className="bg-destructive size-1.5 shrink-0 
rounded-full" />
+                </SimpleTooltip>
+              ) : null}
               {/* A waiting release is a dot rather than a line of its own: it 
is
                   worth noticing and not worth a sentence on every card. */}
               {update?.available ? (
diff --git a/web/src/components/AgentIcon.tsx b/web/src/components/AgentIcon.tsx
index d9a32bf..b21a04c 100644
--- a/web/src/components/AgentIcon.tsx
+++ b/web/src/components/AgentIcon.tsx
@@ -45,6 +45,7 @@ const agentSites: Record<string, string> = {
   Pi: "pi.dev",
   "Kimi Code CLI": "kimi.com",
   "CodeBuddy Code": "codebuddy.ai",
+  ZCode: "z.ai",
 };
 
 // Records name an agent by id, and a few ids do not spell out the name above.
diff --git a/web/src/lib/egress.ts b/web/src/lib/egress.ts
new file mode 100644
index 0000000..090d347
--- /dev/null
+++ b/web/src/lib/egress.ts
@@ -0,0 +1,73 @@
+// Copyright 2026 The casbin Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as React from "react";
+
+import * as AgentBackend from "@/backend/AgentBackend";
+import type {AgentEgress, AgentRecord, EgressDetail} from "@/types";
+
+const refreshMs = 10000;
+
+/** What the egress watch has on one agent, read again while the page is open
+ *  since the destinations change as the agent runs. */
+export function useAgentEgress(agentId: string, owner: string, enabled: 
boolean) {
+  const [egress, setEgress] = React.useState<AgentEgress | null>(null);
+
+  React.useEffect(() => {
+    if (!enabled || agentId === "") {
+      setEgress(null);
+      return;
+    }
+
+    let cancelled = false;
+    const load = () => {
+      AgentBackend.getAgentEgress(agentId, owner)
+        .then(res => {
+          if (!cancelled && res.status === "ok") {
+            setEgress(res.data ?? null);
+          }
+        })
+        .catch(() => undefined);
+    };
+    load();
+    const timer = window.setInterval(load, refreshMs);
+    return () => {
+      cancelled = true;
+      window.clearInterval(timer);
+    };
+  }, [agentId, owner, enabled]);
+
+  return egress;
+}
+
+/** The body of an egress record, null for one this page cannot read. */
+export function egressDetailOf(record: AgentRecord): EgressDetail | null {
+  if (!record.detail) {
+    return null;
+  }
+  try {
+    return JSON.parse(record.detail) as EgressDetail;
+  } catch {
+    return null;
+  }
+}
+
+/** Whether there is anything to show at all: a platform the watch does not run
+ *  on still reports what an agent left on disk. */
+export function hasEgress(egress: AgentEgress | null): egress is AgentEgress {
+  return (
+    egress !== null &&
+    (egress.supported || egress.snapshots.length > 0 || egress.findings.length 
> 0)
+  );
+}
diff --git a/web/src/locales/en/data.json b/web/src/locales/en/data.json
index 949f710..d842470 100644
--- a/web/src/locales/en/data.json
+++ b/web/src/locales/en/data.json
@@ -73,6 +73,8 @@
     "Built-in hint": "This agent is on the model it comes with. Gateway 
forwards nothing for it and only watches what it does.",
     "Built-in model": "Built-in model",
     "Built-in model restored": "Put back on its own model",
+    "Bytes not counted": "Bytes not counted",
+    "Bytes not counted hint": "On Windows, counting bytes per connection needs 
Gateway to run as administrator. Destinations are still watched.",
     "Capture sign-in": "Capture sign-in",
     "Capture sign-in hint": "Open the next sign-in link the browser hands back 
with this copy. Without it the link opens whichever copy the agent registered 
itself for, and this one can never be signed in.",
     "Casbin configuration": "Casbin configuration",
@@ -98,6 +100,8 @@
     "Default instance": "Default instance",
     "Default instance hint": "The agent's own copy, on the state directory it 
uses when nothing hands it another one. Gateway lists it beside the extra 
copies, but neither names nor removes it.",
     "Delete a file": "Delete a file",
+    "Destination": "Destination",
+    "Destinations ({count})": "Destinations ({count})",
     "Detail": "Detail",
     "Details": "Details",
     "Direct": "Direct",
@@ -141,6 +145,7 @@
     "Fetch a URL": "Fetch a URL",
     "Filter": "Filter",
     "Find files by name": "Find files by name",
+    "Finding": "Finding",
     "Finish the sign-in in the browser": "Finish the sign-in in the browser. 
It runs against a directory of its own, so the account the agent is using now 
is left alone.",
     "Finished": "Finished",
     "First activity": "First activity",
@@ -176,12 +181,16 @@
     "Interactive hint": "This opens a window on the machine and waits for an 
answer there.",
     "Keep a task list": "Keep a task list",
     "Kimi Code config hint": "Kimi Code keeps the endpoint in 
~/.kimi-code/config.toml: a [providers.casbin-gateway] table with type 
\"openai\", base_url and api_key, and one [models.*] alias per model, picked by 
default_model at the root of the file. It reads no credential from the 
environment, and KIMI_CODE_HOME moves the whole directory.",
+    "Kind": "Kind",
+    "Large upload": "Large upload",
     "Last activity": "Last activity",
+    "Last seen": "Last seen",
     "Last {count} records": "Last {count} records",
     "Latest": "Latest",
     "Latest version": "Latest version",
     "Leave plan mode": "Leave plan mode",
     "List a directory": "List a directory",
+    "Local process": "Local process",
     "Locate": "Locate",
     "Locate hint": "Pick the agent's program on this machine. Gateway lists it 
as an installation of its own, without moving or changing anything.",
     "Locate {agent}": "Locate {agent}",
@@ -190,6 +199,7 @@
     "MCP servers": "MCP servers",
     "MCP target": "MCP target",
     "Model": "Model",
+    "Model API": "Model API",
     "Model list hint": "A name may end in \"*\", so \"claude-opus-*\" covers 
the whole family.",
     "Model placeholder": "Type a model name and press Enter",
     "Monitored": "Monitored",
@@ -204,6 +214,7 @@
     "No agent records yet - patch an agent to start collecting them": "No 
activity yet - turn on monitoring for an agent to start collecting it",
     "No agent sessions yet": "No agent sessions yet",
     "No agent sessions yet - patch an agent to start collecting them": "No 
sessions yet - turn on monitoring for an agent to start collecting them",
+    "No connections yet": "No connections seen yet",
     "No instances yet": "No instances yet - add one to sign in with a second 
account",
     "No other provider speaks this API": "No other provider speaks this API",
     "No output yet": "No output yet",
@@ -211,6 +222,7 @@
     "No provider yet": "No provider has been added yet.",
     "No rule yet": "no rule yet",
     "No supported agents found": "No supported agents found",
+    "No suspicious uploads": "No suspicious uploads recorded",
     "No usage yet": "No requests have been relayed for this agent yet",
     "No versions to list": "No versions to list",
     "Not enforced": "Not enforced",
@@ -223,6 +235,7 @@
     "Not signed in hint": "Start this instance and sign in; the account shows 
up here afterwards.",
     "Not supported": "Not supported yet",
     "Not written": "Not written",
+    "Object storage": "Object storage",
     "Onboarding add provider": "Add a provider",
     "Onboarding add provider detail": "Point Gateway at a model vendor with a 
base URL and API key.",
     "Onboarding connect agent": "Connect an agent",
@@ -231,6 +244,9 @@
     "Onboarding see it work detail": "Watch requests land in LLM Records once 
traffic flows through.",
     "Only the ones I pick": "Only the ones I pick",
     "Open the sign-in page": "Open the sign-in page",
+    "Other": "Other",
+    "Outbound traffic": "Outbound traffic",
+    "Outbound traffic hint": "Gateway watches where this agent's own processes 
send data. The proxy only sees what goes to the model; a repository uploaded 
straight to cloud storage shows up here.",
     "Patch": "Turn on monitoring",
     "Patch Status": "Monitoring",
     "Patched": "Monitoring",
@@ -259,6 +275,7 @@
     "Read from the agent's own logs": "Read from the agent's own logs",
     "Read many files at once": "Read many files at once",
     "Reading the project": "Reading the project",
+    "Received": "Received",
     "Record": "Record",
     "Records": "Records",
     "Records are stored in the database and survive a Gateway restart": 
"Records are stored in the database and survive a Gateway restart",
@@ -289,6 +306,7 @@
     "Search inside files": "Search inside files",
     "Search the codebase by meaning": "Search the codebase by meaning",
     "Search the web": "Search the web",
+    "Sent": "Sent",
     "Session": "Session",
     "Session title": "Session title",
     "Set in a PowerShell profile": "Set in a PowerShell profile",
@@ -300,6 +318,12 @@
     "Set in the user environment variables": "Set in the user environment 
variables",
     "Shared config permission hint": "This agent shares its configuration file 
with another agent, and that file routes through Gateway under the other id, so 
none of these rules are ever reached. Its requests are held to the rules of:",
     "Sign in": "Sign in",
+    "Snapshot files": "{files} files, {gitFiles} of them from .git",
+    "Snapshot packed": "{agent} packed this whole workspace to upload",
+    "Snapshot retries": "{count} failed upload attempts",
+    "Snapshot rotate secrets": "The package holds the full commit history. 
Rotate any secret that ever appeared in it, even ones since deleted.",
+    "Snapshot size": "{size} encrypted",
+    "Snapshot uploaded": "{agent} uploaded this whole workspace",
     "Source": "Source",
     "Start": "Start",
     "Start a sub-agent": "Start a sub-agent",
@@ -313,10 +337,12 @@
     "Stop hint": "Ends every process of this installation, losing whatever it 
is working on.",
     "Stopped": "Stopped",
     "Subscription": "Subscription",
+    "Suspicious uploads recorded": "{count} suspicious uploads recorded",
     "Switch takes effect at once": "Picking another provider above rewrites 
this file at once, so switching takes one click.",
     "Switch version": "Switch version",
     "Switching version": "Switching version",
     "Target / Model": "Target / Model",
+    "Telemetry": "Telemetry",
     "Terminal": "Terminal",
     "The internet": "The internet",
     "The sign-in did not finish": "The sign-in did not finish",
diff --git a/web/src/locales/zh/data.json b/web/src/locales/zh/data.json
index beb9427..571fe0b 100644
--- a/web/src/locales/zh/data.json
+++ b/web/src/locales/zh/data.json
@@ -73,6 +73,8 @@
     "Built-in hint": "该Agent正在使用自带的模型,网关不为它转发请求,只做观测。",
     "Built-in model": "自带模型",
     "Built-in model restored": "已恢复为自带模型",
+    "Bytes not counted": "未统计流量",
+    "Bytes not counted hint": "在Windows上按连接统计流量需要以管理员身份运行Gateway,目的地照常监测。",
     "Capture sign-in": "接管登录",
     "Capture sign-in hint": 
"让浏览器返回的下一个登录链接打开这个副本。否则链接只会打开Agent自己注册的那个副本,这个副本永远登录不上。",
     "Casbin configuration": "Casbin配置",
@@ -98,6 +100,8 @@
     "Default instance": "默认实例",
     "Default instance hint": 
"Agent自身的副本,运行在它默认使用的状态目录上。Gateway只把它和其他副本一起列出,不会重命名或移除它。",
     "Delete a file": "删除文件",
+    "Destination": "目的地",
+    "Destinations ({count})": "连接过的地址({count})",
     "Detail": "详情",
     "Details": "详情",
     "Direct": "直连",
@@ -141,6 +145,7 @@
     "Fetch a URL": "抓取网址",
     "Filter": "筛选",
     "Find files by name": "按文件名查找",
+    "Finding": "发现",
     "Finish the sign-in in the browser": 
"请在浏览器里完成登录。登录过程使用独立的目录,因此Agent当前正在用的账号不受影响。",
     "Finished": "已完成",
     "First activity": "首次活动",
@@ -176,12 +181,16 @@
     "Interactive hint": "这会在本机弹出窗口,需要在本机上确认。",
     "Keep a task list": "维护任务清单",
     "Kimi Code config hint": "Kimi 
Code的端点写在~/.kimi-code/config.toml里:一个[providers.casbin-gateway]表,含type 
\"openai\"、base_url和api_key,每个模型再写一个[models.*]别名,由文件根部的default_model选中。它不从环境变量读取任何凭据,KIMI_CODE_HOME会把整个目录挪到别处。",
+    "Kind": "类型",
+    "Large upload": "大量上传",
     "Last activity": "最后活动",
+    "Last seen": "最近连接",
     "Last {count} records": "最近{count}条记录",
     "Latest": "最新",
     "Latest version": "最新版本",
     "Leave plan mode": "退出计划模式",
     "List a directory": "列出目录",
+    "Local process": "本机进程",
     "Locate": "选择程序",
     "Locate hint": "在本机选择该Agent客户端的可执行程序。Gateway会把它作为一个安装列出,不会移动或修改任何文件。",
     "Locate {agent}": "选择{agent}的程序",
@@ -190,6 +199,7 @@
     "MCP servers": "MCP服务",
     "MCP target": "MCP目标",
     "Model": "模型",
+    "Model API": "模型API",
     "Model list hint": "名称可以用\"*\"结尾,例如\"claude-opus-*\"覆盖整个系列。",
     "Model placeholder": "输入模型名后按回车",
     "Monitored": "监控采集",
@@ -204,6 +214,7 @@
     "No agent records yet - patch an agent to start collecting them": 
"还没有活动记录,开启某个Agent客户端的监控后才会开始收集",
     "No agent sessions yet": "还没有会话",
     "No agent sessions yet - patch an agent to start collecting them": 
"还没有会话,开启某个Agent客户端的监控后才会开始收集",
+    "No connections yet": "还没有看到连接",
     "No instances yet": "暂无实例-添加一个即可用第二个账号登录",
     "No other provider speaks this API": "没有其他供应商使用相同的API协议",
     "No output yet": "暂无输出",
@@ -211,6 +222,7 @@
     "No provider yet": "还没有添加任何供应商。",
     "No rule yet": "还没有任何规则",
     "No supported agents found": "没有发现本机安装的Agent客户端",
+    "No suspicious uploads": "没有记录到可疑外发",
     "No usage yet": "该Agent还没有经过网关的请求",
     "No versions to list": "没有可列出的版本",
     "Not enforced": "未生效",
@@ -223,6 +235,7 @@
     "Not signed in hint": "启动该实例并登录,之后账号会显示在这里。",
     "Not supported": "暂不支持",
     "Not written": "未写入",
+    "Object storage": "对象存储",
     "Onboarding add provider": "添加一个Provider",
     "Onboarding add provider detail": "填入模型厂商的地址和API Key,让Gateway接管它。",
     "Onboarding connect agent": "绑定一个Agent",
@@ -231,6 +244,9 @@
     "Onboarding see it work detail": "流量接入后,可在LLM Records中看到请求记录。",
     "Only the ones I pick": "只允许勾选的",
     "Open the sign-in page": "打开登录页面",
+    "Other": "其他",
+    "Outbound traffic": "外发监测",
+    "Outbound traffic hint": 
"Gateway监测这个Agent自己的进程往哪里发数据。代理只能看到发给模型的请求,直接传到云存储的代码仓库会记在这里。",
     "Patch": "开启监控",
     "Patch Status": "监控状态",
     "Patched": "监控中",
@@ -259,6 +275,7 @@
     "Read from the agent's own logs": "读自agent自己的日志",
     "Read many files at once": "一次读取多个文件",
     "Reading the project": "读取项目",
+    "Received": "接收",
     "Record": "记录数",
     "Records": "记录数",
     "Records are stored in the database and survive a Gateway restart": 
"记录保存在数据库中,Gateway重启后依然存在",
@@ -289,6 +306,7 @@
     "Search inside files": "在文件内容里搜索",
     "Search the codebase by meaning": "按语义搜索代码库",
     "Search the web": "搜索网页",
+    "Sent": "发送",
     "Session": "会话",
     "Session title": "会话标题",
     "Set in a PowerShell profile": "设置在PowerShell配置文件中",
@@ -300,6 +318,12 @@
     "Set in the user environment variables": "设置在用户环境变量中",
     "Shared config permission hint": 
"该Agent客户端与另一个Agent客户端共用同一个配置文件,而文件里写的是那个Agent客户端的网关地址,因此这里的规则永远不会被用到。它的请求按下面这个Agent客户端的规则判定:",
     "Sign in": "登录",
+    "Snapshot files": "{files}个文件,其中{gitFiles}个来自.git",
+    "Snapshot packed": "{agent}已把整个工作区打包待传",
+    "Snapshot retries": "上传失败{count}次",
+    "Snapshot rotate secrets": "包里有完整的提交历史,历史中出现过的密钥都要轮换,已删除的也一样。",
+    "Snapshot size": "加密后{size}",
+    "Snapshot uploaded": "{agent}已上传整个工作区",
     "Source": "来源",
     "Start": "启动",
     "Start a sub-agent": "启动子Agent",
@@ -313,10 +337,12 @@
     "Stop hint": "结束该安装的所有进程,正在进行的工作会丢失。",
     "Stopped": "未运行",
     "Subscription": "订阅账号",
+    "Suspicious uploads recorded": "记录到{count}次可疑外发",
     "Switch takes effect at once": "在上面换一个供应商会立即改写这个文件,所以切换只需要点一下。",
     "Switch version": "切换版本",
     "Switching version": "切换版本中",
     "Target / Model": "目标/模型",
+    "Telemetry": "遥测",
     "Terminal": "终端",
     "The internet": "访问网络",
     "The sign-in did not finish": "登录未完成",
diff --git a/web/src/pages/AgentDetailPage.tsx 
b/web/src/pages/AgentDetailPage.tsx
index 7d9a2af..30c5be0 100644
--- a/web/src/pages/AgentDetailPage.tsx
+++ b/web/src/pages/AgentDetailPage.tsx
@@ -24,6 +24,7 @@ import * as LlmRecordBackend from 
"@/backend/LlmRecordBackend";
 import * as Setting from "@/Setting";
 import {AgentIcon} from "@/components/AgentIcon";
 import {AgentAccounts} from "@/components/AgentAccounts";
+import {AgentEgressCard} from "@/components/AgentEgressCard";
 import {AgentInstances} from "@/components/AgentInstances";
 import {AgentUsageTrend, hasUsageTrend} from "@/components/AgentUsageTrend";
 import {accountLabel} from "@/components/AgentGridCard";
@@ -609,6 +610,8 @@ export default function AgentDetailPage({account}: 
{account: Account}) {
 
       <PermissionCard agent={agent} providers={providers} />
 
+      <AgentEgressCard agent={agent} enabled={isAdmin} />
+
       <AgentAccounts agent={agent} enabled={isAdmin} />
 
       <AgentInstances agent={agent} enabled={isAdmin} />


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to