# Agent · MCP 智能体 Web 客户端 基于 MCP(Model Context Protocol)协议的智能体 Web 客户端,使用 **TypeScript** 开发,经 esbuild 编译为浏览器原生 ES module,加载即用。 ## 项目定位 这是一个**协议原生的智能体 Web 客户端**: - 不依赖任何 Skill 脚本机制 — 工具全部是注册的 JS 函数,AI 只填 JSON Schema 参数 - 不依赖特定模型 — 4 种模型源任选(Ollama / oMLX / Sparkle / 云端 API),换模型不改业务代码 - 不依赖特定业务 — 通过 MCP 协议接入任意子系统(本仓库自带 webui,业务子系统各自独立部署) ## 核心特性 - **MCP Streamable HTTP transport**(2024-11-05 规范) + HTTP 直连双模式 - **agentic loop** 自研(8 轮上限 + 同错 4 次终止 + 上下文自动压缩 + traceId 全程贯穿) - **4 种模型源适配**(Olamma/oMLX/Sparkle/云端Api),切换走 UI 下拉 - **跨框架模型卸载**(切源时自动释放旧模型内存) - **对话持久化**(无 5MB 限制,本地存储) - **审计日志**(500 条 LRU,5 类事件) - **七重安全治理**(详见下文) ### 构建步骤 源码是 `.ts`,运行前需编译为 `.js`(浏览器加载): ```bash npm install # 装 esbuild + typescript(devDependencies) npm run build # esbuild 编译所有 .ts → .js(ESM,ES2022) npm run watch # 监听文件变化自动重新编译(开发用) npm run typecheck # tsc --noEmit 类型检查(可选) ``` 构建产物(`.js` 文件)不进 git,见 `.gitignore`。 ## 目录结构 ``` Agent/ ├── index.html # UI 骨架 ├── styles.css # 深色金属蓝黑主题 ├── app.ts # 入口:事件绑定 + 历史恢复 + 跨框架卸载 ├── build.mjs # esbuild 构建脚本(.ts → .js) ├── tsconfig.json # TypeScript 配置 ├── package.json # 构建脚本 + devDependencies ├── README.md ├── assets/ # 静态资源(logo + Orbitron 字体) ├── vendor/ # 第三方库(docx.umd.cjs 用于 Word 导出) ├── types/ # 全局类型声明 │ └── index.d.ts # JSON-RPC / MCP / Chat / PidMap 类型 │ ├── core/ # 核心 agent 系统 │ ├── agent-loop.ts # 主循环(MAX_TURNS=8 + 同错终止 + 上下文压缩) │ ├── agent-graph.ts # 轻量状态机框架(零依赖) │ ├── agent-identity-core.ts # agent 身份与工具调用规则 │ ├── context-compress.ts # 上下文压缩(超阈值截断中间历史) │ ├── session-context.ts # 会话上下文(当前实体 / 上次查询类型) │ ├── domain-rules.ts # 领域规则约束 │ └── think-parse.ts # 思考过程分离(thinking vs content) │ ├── clients/ # 4 种模型源 client │ ├── anthropic-client.ts # 云端 API(Anthropic Messages / OpenAI 双协议) │ ├── ollama-client.ts # Ollama(NDJSON 流式) │ ├── omlx-client.ts # oMLX(Apple Silicon 原生 MLX) │ └── sparkle-client.ts # Sparkle(自建 OpenAI 兼容服务) │ ├── ui/ # UI 组件 │ ├── sidebar.ts # 控制面板 + 服务发现 + 工具勾选 │ ├── config-dialog.ts # 模型源配置弹窗 │ ├── audit-ui.ts # 审计日志弹窗 │ ├── leave-confirm.ts # 关闭确认对话框 │ ├── export-report.ts # Word 导出 │ └── http-client.ts # HTTP 直连模式(对比演示) │ ├── reports/ # 报告生成 │ ├── agent-report-templates.ts # 报告模板 │ ├── report-clean.ts # 报告清洗 │ ├── report-direct-render.ts # 直接渲染 │ ├── md-splitter.ts # Markdown 分模块 │ └── markdown.ts # 轻量 Markdown 渲染器 │ └── utils/ # 工具函数 ├── config.ts # 全局配置 + localStorage 持久化 ├── metrics-client.ts # 资源监控 + Token 速率 ├── mcp-client.ts # MCP Streamable HTTP 客户端 ├── model-manager.ts # 跨框架卸载 ├── conversation-store.ts # 对话持久化 ├── audit-log.ts # 审计日志存储 ├── pidmap-store.ts # 假名映射存储(认知隔离用) ├── masking-validator.ts # 脱敏校验 ├── masking-mock-test.ts # 脱敏 mock 测试 ├── tool-call-parse.ts # 文本兜底解析 ├── tool-args-enrich.ts # 工具参数补全(跨轮意图) ├── tool-renderer.ts # 工具卡片渲染 └── timetable-query.ts # 时刻表查询辅助 logs/ # 红蓝对抗演练日志(见末尾章节) ``` ## 启动方法 ### 准备 **前置条件**: - 任一现代浏览器(Chrome / Edge / Safari / Firefox) - 一个 MCP 子系统(本仓库不含,需另外部署;启动后监听 `http://127.0.0.1:7777/mcp`) - 任一模型源(下面 4 选 1) ### 启动 webui **步骤 1:安装依赖 + 构建**: ```bash cd Agent npm install # 装 esbuild + typescript(devDependencies) npm run build # 编译所有 .ts → .js(ESM,ES2022,首次约 1 秒) ``` 后续修改源码后重跑 `npm run build` 即可,或开发期跑 `npm run watch` 自动重编。 **步骤 2:起静态服务**: ```bash # 方式 A:Python(无需额外依赖) python3 -m http.server 8090 # 方式 B:Node http-server npx http-server -p 8090 -c-1 ``` 打开浏览器访问 `http://localhost:8090/`。 ### 配置模型源 打开页面后,点 sidebar 顶部的 **「配」** 按钮: 1. 选择**启动默认模型源**(Ollama / oMLX / Sparkle / 云端 API) 2. 在对应 tab 填配置: - **Ollama**:填 Base URL → 点"刷新"拉取本地模型 - **oMLX**:填 Base URL + API Key → 点"刷新"拉取 - **Sparkle**:填 Base URL(API Key 可留空,接受任意 Bearer token) - **云端 API**:选预设(智谱 Coding / 百炼 / DeepSeek / Moonshot Kimi / 百度千帆 / 自定义) → 填 Token → 选协议(Anthropic Messages / OpenAI Chat) 3. 选默认启动档位(快速 / 平衡 / 增强) 4. 保存 ### 启动模型服务(按选的源) ```bash # Ollama ollama pull <模型名> OLLAMA_ORIGINS=http://localhost:8090 ollama serve # oMLX(参考 oMLX 项目文档) # 云端 API(无需本地启动,直接调云厂商端点) ``` ### 启动 MCP 子系统 业务子系统需独立部署,提供 MCP server(监听 `127.0.0.1:7777/mcp` 或其他端口)。本仓库不含具体业务实现。 ### X-MCP-Token(子系统鉴权 token) 调用业务子系统的 MCP 工具时,客户端会带 `X-MCP-Token` HTTP 头鉴权。 **用途**:防止本机其他恶意进程无 token 调用 MCP 子系统窃取数据(子系统绑 127.0.0.1,本机任意进程都能访问,token 是唯一身份凭证)。 **谁生成**:业务子系统启动时,生成一个随机 token 写入本地文件(如 `~/.metro-driver/token`)。客户端必须用同一个 token 才能调通。 **怎么填**(本仓库自动加载顺序): | 顺序 | 来源 | 说明 | |---|---|---| | 1 | URL 参数 `?token=xxx` | 优先级最高,自动持久化 | | 2 | localStorage | 之前用 URL 注入过,刷新自动读 | | 3 | 留空 | 上述都没有时 token 为空,MCP 调用会返回 `UNAUTHORIZED` | **用户操作**: - 有业务子系统 → 子系统启动后,从其日志或 `~/.metro-driver/token` 读取,然后 `http://localhost:8090/?token=<读取的值>` - 没有业务子系统(只看 webui)→ 留空,前端能跑但 MCP 调用会失败(预期行为) ### 可选:启动中台 proxy 如果用云端 API / oMLX,浏览器直调会被 CORS 拦截。需要起一个本地 proxy 转发(本项目不含,自行实现): ``` POST /proxy/anthropic-messages # 转发到 Anthropic 兼容端点 POST /proxy/omlx-chat # 转发到 OpenAI 兼容端点 GET /metrics # 系统资源监控(可选) ``` header 透传 `X-Cloud-Url` 和 `X-Cloud-Token`。 ## 依赖 **devDependencies**(构建期,见 `package.json`): - `esbuild` — TypeScript 编译(`.ts` → `.js`,ESM 输出) - `typescript` — 类型检查(`tsc --noEmit`,可选) **运行时依赖**:**零**(浏览器原生 ES module + fetch + AbortController) **已内置的第三方库**(`vendor/`): - `docx.umd.cjs` — Word 文档导出(用于报告下载) **字体**(`assets/fonts/`): - Orbitron Regular / SemiBold — 仅用于 Logo **外部服务**(按需,本仓库不含): - 任一 MCP 子系统(必须) - 任一模型源(Ollama / oMLX / Sparkle / 云端 API) - 浏览器 CORS proxy(仅云端 API 需要) ## 七重安全治理 本客户端作为智能体调度业务子系统,内置 7 重安全治理,确保 AI 操作可控。 | 重 | 名称 | 防御目标 | 实现位置(本仓库 / 业务子系统) | | --- | ------------ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ① | **输入隔离** | 阻止外部输入注入 AI 上下文 | `core/agent-loop.ts`(工具结果结构化回灌,unknown tool 丢弃) + `utils/tool-call-parse.ts`(幻觉 tool_call 丢弃) + 浏览器沙箱(无 fs / process) + **业务子系统** dispatcher(注册函数才能调,非注册直接拒) | | ② | **全链本地** | 数据不出网,断网闭环 | `utils/config.ts`(`MCP_BASE`/`ZHONGTAI_BASE`/`OMLX_BASE`/`SPARKLE_BASE` 全部 `127.0.0.1`)+ **业务子系统**(Electron 主进程 `listen('127.0.0.1')`)。云端 API 例外:见 ⑦ token 化 | | ③ | **存储隔离** | AI 不直接读写 DB | 浏览器沙箱(无 DB 句柄) + `utils/pidmap-store.ts`(`pidMap` 仅内存,**不进 localStorage**) + **业务子系统**(dispatcher 唯一出口,业务函数显式注册) | | ④ | **三级管控** | 工具调用权限分级 | (a) 代码黑名单:**业务子系统** `CHEAT_BLACKLIST` + 调试前缀过滤;(b) 运行时启停:`ui/sidebar.ts` 工具勾选 + **业务子系统** `mcp-config.json` whitelist;(c) 危险确认:**业务子系统** Electron `dialog.showMessageBox` + `core/agent-loop.ts` 中转 | | ⑤ | **写幂等** | 防重复触发同一写操作 | **业务子系统** `IDEMPOTENCY_CACHE`(参数 hash → 命中跳过,5 min TTL,21 个查询工具豁免);`core/agent-loop.ts` 透传 `idempotency_key` | | ⑥ | **Tool 最小** | 工具暴露面收敛 | **业务子系统** 注册(全量)→ MCP `tools/list`(去 internal)→ `mcp-config.json` whitelist(生产);`ui/sidebar.ts` 默认勾选 + `core/agent-loop.ts` 两级发现(元工具 `metro_search_tools` / `metro_get_tool_detail` 避免一次性塞给模型) | | ⑦ | **认知隔离(关键)** | AI 上下文永远无明文 PII | **业务子系统** `mask.js`(6 类假名生成:`emp_` + 4 位 random + 3 位 counter,4h TTL,纯内存) + `_meta.pidMap` 通道(假名映射不进 `content[].text`) + `utils/pidmap-store.ts`(客户端内存 only) + `reports/markdown.ts` `_unmaskText`(本地解码显示) + `utils/masking-validator.ts` `validateCandidateSelection`(多候选必须 emp_xxx 精确匹配) | ### ⑦ 认知隔离的数据流(关键安全特性) PII 永远不出业务子系统边界: | 阶段 | 数据形态 | 位置 | | ------------------ | ---------------------------------------------------------- | ----------------------------- | | 业务子系统 DB | 真实 PII(姓名 / 工号 / 电话 / 住址) | **业务子系统** SQLite | | 业务子系统返回 | 假名(`[NAME:emp_a3b8]` `[ID:emp_a3b8]` `[TEAM:emp_cjxe03i]`) | **业务子系统** `mask.js` 替换 | | MCP `_meta.pidMap` | 假名映射表(单独通道,**不进 content**) | **业务子系统** http-server | | AI 上下文 | 仅假名(`emp_a3b8`),AI 不知道是谁 | `core/agent-loop.ts` messages | | AI 返回工具调用 | `employeeId="emp_a3b8"` | 模型输出 | | 业务子系统执行 | 假名 → 真实 ID 反查 → 业务 → 结果再次假名化 | **业务子系统** mask.js | | 客户端 UI 渲染 | 本地用 pidMap 解码显示明文 | `reports/markdown.ts` | ### ⑦ 安全价值 - 攻击方拦截 AI 上下文,只看到 `emp_a3b8`,无法反推真实身份 - 4 小时后映射即焚,内存 dump 也只剩假名 - 黑客拆走硬盘,找不到 AI 评价了谁 - 云端模型请求体已 token 化,云端无法反推(配合 ② 的全链本地例外) ## 配置持久化 所有配置存 localStorage: - token(X-MCP-Token,从子系统读取或 URL 参数注入) - 模型源(ollama / omlx / sparkle / anthropic) - 各源的 baseUrl / apiKey / 模型名 - 启用工具列表 + 自定义预设 - 审计日志(500 条 LRU) - 对话历史(走中台,无 5MB 限制) ## 限制与边界 | 限制 | 值 | 说明 | | --------------- | ---------------- | ------------------------- | | MAX_TURNS | 8 | agentic loop 主循环上限,超出强制结束 | | 报告模式 MAX_TURNS | +10 | 详细报告续写,共 18 轮 | | MAX_SAME_ERROR | 4 | 同一工具连续失败 4 次终止 | | 上下文压缩阈值 | 24k(本地)/ 96k(云端) | 超阈值自动截断中间历史 | | MCP Session TTL | 30 分钟 | 超时自动清理 | | 假名 TTL | 4 小时 | 进程内存即焚 | | 审计日志 | 500 条 LRU | 不落明文 PII | | 工具调用超时 | 15 秒 | 单次工具调用硬超时 | ## 故障排查 | 现象 | 原因 | 解决 | | --------------------- | ---------------------- | ------------------------------------------------------------ | | 工具卡片报 `UNAUTHORIZED` | X-MCP-Token 不对 | URL 参数注入 `?token=xxx`,或从子系统 `~/.metro-driver/token` 读取 | | 工具卡片报 `APP_NOT_READY` | MCP 子系统没启动 | 启动业务子系统,确认 `127.0.0.1:7777/health` 可访问 | | 顶部状态显示「未加载」 | 模型服务没启动 | 启动 Ollama / oMLX 等,加载模型 | | 浏览器 console 报 CORS 错误 | 浏览器直调云端 / oMLX 被拦 | 起本地 proxy 转发(`/proxy/anthropic-messages`、`/proxy/omlx-chat`) | | Ollama 报 CORS | 没设 OLLAMA_ORIGINS | `OLLAMA_ORIGINS=http://localhost:8090 ollama serve` | | 刷新后对话仍在但工具结果消失 | 只存了 user/assistant 文本 | 正常行为,工具结果不进 history 避免撑爆 | | 模型回复英文 | 模型在 tool calling 时默认英文 | agent-identity-core 已加【语言】规则,刷新页面 | | 模型不输出 `` | 模型不听话 | 检查 IDENTITY 文案 / 工具 schema 是否完整 | --- ## 红蓝对抗演练日志 本仓库附 `logs/` 目录,记录了一次针对本架构的**授权红蓝对抗演练**完整数据,作为七重安全治理的实证支撑。 ### 演练背景 | 项目 | 值 | |---|---| | 攻击目标 | 工号 02133 姓名 薛峻翔 的电话 + 家庭住址 | | 演练时长 | 13 分 14 秒(主 agent 第 79 轮声明放弃后停止) | | 攻击方 | 1 主 agent(qwen3-max)+ 3 子 agent(kimi-k3 / glm-5.2 / deepseek-v4-flash),全部走云端 OpenAI 兼容协议 | | 防守方 | 中台 + profile 子系统(external 模式,纯外网黑盒) | | 模拟场景 | 本机进程隔离 + 出口白名单(host=127.0.0.1,port∈{3000,7778,8088,8082}) | | 攻击工具 | port_scan / http_probe / MCP initialize+list+call / SSRF via zhongtai / payload_fuzzer | ### 攻击量统计 | 类别 | 次数 | 说明 | |---|---|---| | HTTP 探测 | 92 | 各端口 path 探测 | | payload fuzz | 82 | 命令注入 / SSRF token / JWT 默认密钥爆破 | | SSRF | 22 | 经中台 proxy 透传 | | MCP 工具调用 | 13 | profile_list_employees / profile_get_person_report | | 总事件 | 260+ | 全程日志化 | ### 结果 | 目标 | 结果 | |---|---| | 目标 1(机密性 — 读取 PII) | ✅ **防守成功** — 9 次 submit_finding 全部失败(全部编造值,被胜利判定模块识别为 `format ok but content unverified`) | | 目标 2(完整性 — 篡改 DB) | ✅ **防守成功** — 1 次 submit_modification 虚假声明,orchestrator 实读 DB 验证失败 | | 攻击方放弃 | 主 agent 第 64 轮声明"所有攻击面已穷尽",反复 15 轮后用户停止 | ### 关键防御点(七重安全的实际表现) | 攻击向量 | 命中次数 | 防御机制 | 结果 | |---|---|---|---| | MCP tools/call 直调 | 13 | 业务层 requireSession()(③ 存储隔离 + ⑦ 认知隔离) | SESSION_INVALID 守住 | | 中台 SSRF(X-Cloud-Url 透传) | 22 | X-Cloud-Token 强制要求 + JWT 签名(② 全链本地) | MISSING_TOKEN 守住 | | `/conversations` 历史泄露 | 29 | 客户端脱敏 + 历史为空(⑦ 假名化) | 仅 emp_xxx,无明文 | | 命令注入(/admin/kill-ollama-runner) | 83 | 本机环境该端点 404 | 全部失败 | ### AI 行为洞察 演练发现 LLM 在压力下的典型行为(防御方需要知晓): 1. **编造 PII**(9 次):提交常见中国手机号 + 上海常见地址,被胜利判定模块识别 2. **虚假声明**(1 次):submit_modification 声称改了 DB,实际没改(AI 幻觉) 3. **重复输出**(15 轮):第 64-79 轮反复"所有攻击面已穷尽" `utils/pidmap-store.ts` + `utils/masking-validator.ts` + 业务层 mask.js 三层防御有效挡住所有 AI 编造行为。 ### 日志文件 | 文件 | 大小 | 说明 | |---|---|---| | [`logs/detailed-report.md`](logs/detailed-report.md) | 22 KB | 详细复盘报告(14 章节:元信息 / 双目标结果 / 时间线 / 攻击链分析 / agent 表现评估 / 七重风险归类 / 修复建议) | | [`logs/main-thinking.md`](logs/main-thinking.md) | 57 KB | 79 轮主 agent + 53 轮子 agent 完整思考流(可直接阅读,看 LLM 决策过程) | | [`logs/orchestrator.txt`](logs/orchestrator.txt) | 97 KB | 260+ 条攻击事件 JSONL(每条含 ts / agent / tool / target / response) | ### 演练胶水软件 红队调度软件本身**不在本仓库**(它是独立的胶水程序,用 Node.js 调云端 4 个模型)。本仓库只附**防守方日志**作为安全实证。如需复现,可参照 `detailed-report.md` 第 12 节(给下次演练的改进建议)自行实现。