Agent/README.md

21 KiB

Agent · 城轨智能中台(L1 Web 客户端 + L2 中台引擎)

基于 MCP(Model Context Protocol)协议的智能体系统:仓库根目录为 L1 AI 客户端(Web UI,TypeScript,经 esbuild 编译为浏览器原生 ES module,加载即用);server/L2 中台引擎(Express API,仅绑定 127.0.0.1)。

项目定位

这是一个协议原生的智能体系统:

  • 不依赖任何 Skill 脚本机制 — 工具全部是注册的 JS 函数,AI 只填 JSON Schema 参数
  • 不依赖特定模型 — 4 种模型源任选(Ollama / oMLX / Sparkle / 云端 API),换模型不改业务代码
  • 不依赖特定业务 — 通过 MCP 协议接入任意子系统(本仓库自带 L1 webui + L2 中台引擎,业务子系统各自独立部署)

核心特性

  • 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(浏览器加载):

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 渲染器
│
├── server/                 # L2 中台引擎(Express API,:8088,仅 127.0.0.1)
│   ├── src/main.js         # 入口(本地回环监听)
│   ├── src/auth/           # JWT Ed25519 鉴权 + 密钥管理
│   ├── src/audit/          # 审计日志(链式哈希防篡改)
│   ├── src/rbac/           # 角色权限
│   ├── src/import/ upload/ # 数据导入(batch_uuid 幂等防重)
│   ├── src/precompute/     # 业务预计算(AI 不算账,业务系统算好)
│   ├── src/export/         # 报表导出工作队列
│   └── README.md           # 中台引擎详细说明
│
├── packages/shared/        # 中台公共包(模块注册表 / 常量 / 备份表定义)
│
└── 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:安装依赖 + 构建:

cd Agent
npm install      # 装 esbuild + typescript(devDependencies)
npm run build    # 编译所有 .ts → .js(ESM,ES2022,首次约 1 秒)

后续修改源码后重跑 npm run build 即可,或开发期跑 npm run watch 自动重编。

步骤 2:起静态服务:

# 方式 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)
  1. 选默认启动档位(快速 / 平衡 / 增强)
  2. 保存

启动模型服务(按选的源)

# 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 调用会失败(预期行为)

可选:启动中台引擎(L2,:8088)

仓库已含 L2 中台引擎源码(server/,运行说明见 server/README.md):

cd server
cp .env.example .env
npm install
npm start    # 监听 http://127.0.0.1:8088/api/v1

职责:请求验证与路由转发、JWT Ed25519 鉴权、链式哈希审计、RBAC、数据导入/导出与预计算。业务子系统(MCP Server,:7777/:7778)不在本仓库。

如用云端 API / oMLX,浏览器直调仍会被 CORS 拦截,需本地 proxy 转发(本项目不含,自行实现):

POST /proxy/anthropic-messages    # 转发到 Anthropic 兼容端点
POST /proxy/omlx-chat             # 转发到 OpenAI 兼容端点
GET  /metrics                     # 系统资源监控(可选)

header 透传 X-Cloud-UrlX-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 22 KB 详细复盘报告(14 章节:元信息 / 双目标结果 / 时间线 / 攻击链分析 / agent 表现评估 / 七重风险归类 / 修复建议)
logs/main-thinking.md 57 KB 79 轮主 agent + 53 轮子 agent 完整思考流(可直接阅读,看 LLM 决策过程)
logs/orchestrator.txt 97 KB 260+ 条攻击事件 JSONL(每条含 ts / agent / tool / target / response)

演练胶水软件

红队调度软件本身不在本仓库(它是独立的胶水程序,用 Node.js 调云端 4 个模型)。本仓库只附防守方日志作为安全实证。如需复现,可参照 detailed-report.md 第 12 节(给下次演练的改进建议)自行实现。