388 lines
20 KiB
Markdown
388 lines
20 KiB
Markdown
# 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_call> 文本兜底解析
|
|
├── 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 已加【语言】规则,刷新页面 |
|
|
| 模型不输出 `<tool_call>` | 模型不听话 | 检查 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 节(给下次演练的改进建议)自行实现。
|