This commit is contained in:
2026-09-10 19:18:15 +02:00
parent 3a3d70efeb
commit 5993da4ce6
21 changed files with 2916 additions and 340 deletions
+349
View File
@@ -35,6 +35,18 @@
| 二期 C2 | 本地 HTTP `/get_asset_references` 与 MCP 工具接入 | ✅ 完成 |
| 二期 C3 | 二期测试与文档 | ✅ 完成(264/264 测试通过) |
### 三期进度(2026-09-10
| 阶段 | 内容 | 状态 |
|---|---|---|
| 三期 0 | 讨论结论与进度落档 | ✅ 完成 |
| 三期 1 | 运行时解析:无 Node 时用 VS Code Electron`ELECTRON_RUN_AS_NODE` | ✅ 完成 |
| 三期 2 | 抽出 `src/agent/liveClient.ts`,MCP 与 CLI 共用转发/校验/负缓存 | ✅ 完成 |
| 三期 3 | `instances/` 注册表 + 跨实例剪枝 + 只读 `index.json` 发现面 | ✅ 完成 |
| 三期 4 | CLI 瘦身:live 优先、快照兜底、cwd 项目识别、补 `projects`/`get_asset_references` | ✅ 完成 |
| 三期 5 | 三期测试与文档 | ✅ 完成(302 用例,299 通过 / 3 因沙箱跳过) |
| 三期 6 | (可选)TCP → 命名管道 / UDS 传输 | ⬜ 未开始 |
---
## 1. 背景与目标
@@ -770,4 +782,341 @@ VS Code 扩展进程内的 localServer ← 仅在"启用 AI Agent 访问"后启
验证方式为:逐个 `node test/*.test.mjs`、直接调用 esbuild CLI 构建 dist、
用管道驱动 `dist/agent/mcpServer.js` 做端到端 smoke test。
---
---
# 第三部分:三期计划(讨论结论 + 实施)
> 讨论时间:2026-09-10
> 主题:Agent 自发现 MCP 的现实边界、崩溃残留清理、CLI 定位、无 Node 环境
---
## 13. 讨论结论:自操作 Agent 能否自行发现并配置 MCP
### 现状(核实过代码)
`SKILL_MD` 全文不含 `~/.ra3modxml`、launcher 路径、CLI 或任何"工具不存在时怎么办"
的说明。它的第一条指令是"调用 `get_status`"。因此当 skill 已加载但 MCP 工具
不在工具列表里时,agent 只能得到 unknown tool,然后没有下一步。
三个独立缺口:
1. **没有发现面。** 磁盘上的文件名带 `sha1-12` 哈希与 slug
`endpoints/<slug>-<sha1-12>.json``snapshots/<slug>-<sha1-12>.json.gz`),
只有读过本仓库源码的人才知道这套命名。agent 没有"读一个文件就知道全貌"的入口。
2. **skill 与 MCP 是独立安装的。** `installAgentSkill` 装的是
`~/.agents/skills/ra3-mod-xml/`,与 MCP 配置无关,所以"skill 在、MCP 不在"
是常见状态,而这恰恰是最需要 fallback 的场景。
3. **多数 harness 不允许 agent 给自己挂 MCP。** Claude Code 需要重启才能加载新的
MCP server(相关 issue 与 SIGHUP workaround 见调研),Reddit 上也有"每次装
MCP 都要手动重启"的反馈;反例是 LibreChat 的设置面板明确"take effect without
a restart"。所以这是 **harness 相关**:agent 可以**写**配置,但通常不能让自己
**当前会话**用上,只能为下一次会话准备好。
### 结论
- **skill 不应假设 MCP 已就绪**,而要给出"如何触达索引"的阶梯:
1. MCP 工具已在工具列表 → 直接用;
2. 否则读发现面清单;不存在 → 告诉用户去 VS Code 跑一次 Enable**不要瞎猜**
存在 → 用 shell 走 stdio,或用 CLI
3. 若能写自己的 MCP 配置,可以提议代写,但必须告知**下个会话才生效**。
- **"需要重启才生效"这类细节不必写进 skill。** 新一代自操作 agent 本来就该自己
探测能力边界;skill 只需提示它"检查自己的能力范围并告知用户"。
- **不要把各 harness 的配置路径写进 skill 文本。** 那些路径会变,写进 skill 等于
把易变知识固化进 agent 上下文。逻辑留在 `setup.ts`
(已有 `addMcpServerToConfigFile` / `commonMcpConfigTargets`),通过 CLI 暴露成
`configure --harness <name>`skill 只说"在用户同意后可以运行这条命令"。
---
## 14. 讨论结论:崩溃残留与 `instances/` 注册表
### 问题确认
核实结果:全仓只有两处 `rm`,都在 `stopAgentLocalServer`(即优雅 dispose)中。
`endpoints/` **没有任何剪枝逻辑**。因此:
- **崩溃后文件永久残留**,只有下次打开**同一个工作区**才会覆盖同名文件;
删掉项目、移动目录、改 SDK 路径后,旧文件永远躺着。
- **PID 复用是真实风险。** Windows PID 会被回收;若残留 PID 恰好被无关进程占用,
`isProcessAlive` 返回 true → 连接失败 → 5 秒负缓存 → 反复重试。不会给出错误
答案(`responseProjectMismatch` 仍在),但会持续做无用功。
### 结论:不要"共享一个 JSON + 加锁合并"
共享单文件需要锁文件(Windows 上 `wx` + 退避 + 陈旧锁窃取)、需要
read-modify-write 合并(否则并发写互相丢条目),而锁本身也会被崩溃留下
——**为了修 A 的崩溃问题引入了 B 的崩溃问题**。
改用**写入方互不重叠**的结构:
```text
~/.ra3modxml/
instances/
vscode-<pid>-<rand>.json ← 每个 VS Code 窗口一个,只有属主会写
index.json ← 只读合并视图,供 agent/人发现
snapshots/<slug>-<sha1-12>.json.gz
skill-install.json
```
- **无锁、无合并**:每实例只写自己那一个文件,天然无竞争。
- **崩溃自愈**:任何实例激活时扫一遍 `instances/``isProcessAlive(pid) === false`
的直接 unlink,不需要等"下次打开同一个工作区"。
- **读方仍要校验**liveness = PID 存活 **且** 能连上;PID 复用场景下连接失败即
视为死,并顺带触发剪枝。
- `index.json` 是纯只读派生物(由 `instances/` 生成),机器协调文件与人类/agent
发现面分离。
`endpoints/` 保留为兼容读取路径,不再主动写入。
---
## 15. 讨论结论:CLI 定位与"稳定 IPC"
### 澄清:端口从未写进 harness 永久设置
写进配置的是 launcher
```json
{ "mcpServers": { "ra3-mod-xml": { "command": "~/.ra3modxml/ra3-mod-xml-mcp.cmd", "args": ["--project", "..."] } } }
```
端口由 `instances/*.json` 桥接,每次开新会话时由 MCP server 现读。所以
"临时端口会变"对 harness 完全透明。换命名管道/UDS 的收益不是"稳定性",而是:
- Windows 防火墙对监听 TCP socket 可能弹窗,命名管道不会;
- 不占端口;
- 权限模型更自然(管道可设 ACL;TCP loopback 任何本机进程都能连,现在靠 token)。
代价:Node 的 `fetch` 不支持 socketPath,客户端要从 `fetch` 换成
`http.request({ socketPath })`(服务端 `listen(pipePath)` 即可,路由不用改)。
### CLI 定位:薄
CLI 只做两件事:**读磁盘快照** + **转发给扩展(live**。不要在 CLI 里重新实现
"高级功能",那等于写两遍。
**但要注意**:转发逻辑现在锁死在 `mcpServer.ts`
`tryLiveQuery` / `liveUrlForTool` / `responseProjectMismatch` / 负缓存)。
若 CLI 再写一遍就是重复。因此必须先抽 `src/agent/liveClient.ts`,让
`mcpServer.ts``cli.ts` **同时**依赖它:
- 换传输层时只改一个文件;
- `projectDir` 校验、负缓存等安全逻辑不会在两个入口之间漂移。
---
## 16. 讨论结论:无 Node 环境(已验证)
### 问题
- 不能假定用户装了 Node。
- `writeLauncher` 生成的是裸 `node "<server>" ...`,所以**没装 Node 的用户
点完 Enable 之后 MCP 起不来**,而且不会报"缺 node"。这是**已存在的缺口**
不是未来风险。
- 打包自包含或原生二进制是否必要?
### 结论:不需要打包,VS Code 自带运行时,只是还没用它
VS Code 的 Electron 二进制可以当 Node 用:
```bat
@echo off
set ELECTRON_RUN_AS_NODE=1
"C:\...\Microsoft VS Code\Code.exe" "<ext>\dist\agent\mcpServer.js" --project "D:\..."
```
**本机实测(2026-09-10VS Code 1.135.0 / Electron 42.8.1**
| 项目 | 结果 |
|---|---|
| `ELECTRON_RUN_AS_NODE=1` 生效 | ✅ `process.versions.electron = 42.8.1` |
| Node 版本 | `24.18.1`(系统 Node 为 24.16.0 |
| 必需模块(`fs`/`fs/promises`/`path`/`os`/`http`/`readline`/`zlib`/`crypto`/`util`/`net`/`child_process` | ✅ 全部可载入 |
| `node:test` | ✅ 可载入 |
| `forwardRefs.parseLoadedXml` | ✅ `elements=2` |
| `localServer.startLocalServer` + HTTP 请求 | ✅ 正常返回 |
| `zlib` gzip/gunzip 往返 | ✅ |
| **打包后的 `dist/agent/mcpServer.js` 完整 stdio 会话** | ✅ `initialize` / `tools/list`(9 个工具) / `tools/call get_status` 全部正确 |
因此:
- **零外部依赖**,只装 VS Code 就够;
- 路径来自 `process.execPath`,自动适配 Code / Insiders / VSCodium / 各平台;
- VS Code 升级后路径变了也没关系——扩展每次激活重写 launcher,而 launcher 本身
`~/.ra3modxml/` 下路径稳定;
- 需要 fallbackElectron 二进制被移动/卸载时回退到 PATH 上的 `node`
已知代价(实测未测,需留意的边界):
- Electron 以 Node 模式启动比真 Node 慢(握手延迟增加,对 MCP 应无碍);
- 远程/WSL/容器场景下 `process.execPath` 是 server 端二进制,MCP 必须跑在远端
—— 独立边界问题,本期不解决。
### 如何在"本机有 Node"的前提下测试"无 Node"
不需要真的卸载 Node。要做的是**证明 Electron 路径可以独立工作**,即整条链路
不依赖"PATH 上能解析到 `node`"
1. 直接只用 Electron 二进制驱动 `dist/agent/mcpServer.js`(已做,见上表);
2. 让解析出的 launcher **只用** Electron,不回落;
3. 断言 launcher 文本中不出现裸 `node`(当 Electron 可用时)。
这三点都在本环境可做,无需移除 Node。
---
## 17. 三期实施清单
| 优先级 | 内容 | 理由 |
|---|---|---|
| 1 | 运行时解析(`src/agent/runtime.ts`+ launcher 优先 Electron | 现在是"配好了但可能起不来",最伤 |
| 2 | 抽 `src/agent/liveClient.ts` | 后续所有传输层改动的前提;避免转发逻辑写两遍 |
| 3 | `instances/` + 跨实例剪枝 + 只读 `index.json` | 修崩溃残留,兼做 agent 发现面 |
| 4 | CLI 瘦身(live 优先 / 快照兜底 / cwd 识别 / `projects` | 让"没有 MCP 的 agent"也能用 |
| 5 | 传输层 TCP → 命名管道 / UDS | 收益中等,需 2 完成,可缓 |
---
## 18. 三期实施结果
全部完成。
### 新增 / 修改文件
```text
新增:
src/agent/runtime.ts 运行时解析 + launcher 脚本生成(纯函数,可测)
src/agent/liveClient.ts 共享 live 转发层(MCP 与 CLI 共用)
src/agent/instances.ts instances/ 注册表、剪枝、只读 index.json
test/agentRuntime.test.mjs
test/agentLiveClient.test.mjs
test/agentInstances.test.mjs
test/agentCli.test.mjs
test/agentLiveE2E.test.mjs
tools/probe-electron-node.cjs Electron-as-Node 能力探针(保留为证据)
tools/serve-fake-index.cjs 假 live 索引服务器(手工 smoke 用)
修改:
src/agent/setup.ts writeLauncher 改用 runtime + launcherScript
返回 { path, runtime, nodeFree }
src/agent/mcpServer.ts 改用 LiveClient;新增 list_projects
启动时剪枝崩溃实例;require.main 守卫
src/agent/cli.ts 重写为薄客户端:live 优先 / 快照兜底 /
cwd 项目识别 / projects / outgoing
src/extension.ts instances/ 写入与剪枝、index.json 刷新、
launcher 激活时刷新、nodeFree 警告
```
### 关键行为
**无 Node 运行(三期 1**
- launcher 优先用 VS Code 的 Electron 二进制(`ELECTRON_RUN_AS_NODE=1`),
Node 只作为"Electron 二进制不存在"时的兜底:
`if not exist "%RA3_RUNTIME%" goto :ra3_node`
-`goto` 而不是 `if (...)` 块:批处理里块内的 `%errorlevel%` 在**解析时**展开,
会导致退出码错误。
- 扩展激活时会重写 launcher(不只是 Enable 时),所以 VS Code 升级换了路径
也能自动跟上。
- 解析不到 Electron 时会警告用户"launcher 将依赖 PATH 上的 Node"。
**`instances/` 注册表(三期 3**
```text
~/.ra3modxml/
instances/vscode-<pid>-<rand>.json 每窗口一个,只有属主写
index.json 只读合并视图(不含 token)
endpoints/<slug>-<sha1-12>.json 兼容读取,不再主动写
snapshots/…
skill-install.json
```
- **无锁无合并**:写入方互不重叠,没有 read-modify-write 竞争。
- **崩溃自愈**:任何实例启动时剪掉 PID 已死的条目,不需要等同一工作区重开。
- **只剪确定的死 PID**`isProcessAlive` 对未知/缺失 PID 返回 true
所以老格式文件不会被误删。
- `index.json` 是派生物,只用于发现;断言过其中不含 token。
**共享 live 层(三期 2**
`liveClient.ts` 现在同时被 MCP 与 CLI 使用,集中了:
- 请求永远带 `?project=`(否则会被活动编辑器的项目回答);
- `index.projectDir` 不匹配就拒答;
- 死 PID 视为失效;
- 失败后 5 秒负缓存(可注入时钟,测试可确定性验证);
- 端点发现顺序:按项目文件 → 任何 `projects` 含该项目的实例 → 全局兜底指针。
**薄 CLI(三期 4**
- `outgoing` / `projects` 是 live-only:不可用时返回**明确原因**,
而不是空结果(空结果会被 agent 读成"没有引用")。
- 项目解析顺序:`--project``--snapshot` → 从 cwd 向上找 mod 项目根。
- 退出码:0 成功 / 2 用法错误 / 3 不可用。
### 已知限制
- `get_asset_references` 不计算 `xai:joinAction``Replace`/`Remove`)合并后的
有效值,需要调用方自行确认。
- 传输层仍是 loopback HTTP(临时端口 + token)。换命名管道/UDS 的收益是
免防火墙弹窗与更自然的权限模型,代价是客户端要从 `fetch` 改为
`http.request({ socketPath })`;因为转发逻辑已集中,改动面只剩
`liveClient.ts`
- 远程 / WSL / 容器场景下 `process.execPath` 是服务端二进制,MCP 必须跑在
远端;本期未处理。
- CLI 在"无 Node 且无 VS Code"的环境下无法运行(这是逻辑必然:它需要一个
运行时)。有 VS Code 时用 Electron 二进制即可。
---
## 19. 三期测试与验证
```
npx tsc --noEmit 通过
test/*.test.mjs 37 个文件 / 302 个用例,299 通过,3 跳过
```
跳过的 3 个都是同一原因:本沙箱禁止从 Node 进程 spawn shell`EPERM`),
所以"真正拉起 launcher / CLI 子进程"的集成用例会优雅跳过并给出原因。
它们在本机正常运行时会执行。
### Electron-as-Node 实测证据
`tools/probe-electron-node.cjs`VS Code 1.135.0 / Electron 42.8.1Windows):
| 项目 | 结果 |
|---|---|
| `ELECTRON_RUN_AS_NODE=1` 生效 | ✅ `process.versions.electron = 42.8.1` |
| Node 版本 | `24.18.1`(系统 Node 24.16.0 |
| 必需模块 | ✅ `fs`/`fs/promises`/`path`/`os`/`http`/`readline`/`zlib`/`crypto`/`util`/`net`/`child_process` |
| `node:test` | ✅ 可载入 |
| `parseLoadedXml` | ✅ |
| `startLocalServer` + HTTP 请求 | ✅ |
| `zlib` gzip/gunzip 往返 | ✅ |
| **打包后 `dist/agent/mcpServer.js` 完整 stdio 会话** | ✅ initialize / tools/list(10 个) / tools/call |
### 无 Node 运行的证明方式
不是卸载 Node,而是证明整条链路不依赖"PATH 上能解析到 node"
1.`cmd /c` + `ELECTRON_RUN_AS_NODE=1` 直接驱动 `dist/agent/mcpServer.js`
→ 完整 MCP 会话成功;
2. 生成的 launcher 里把 **Node 兜底指向一个不存在的路径**,会话仍然成功
→ 证明走的是 Electron 分支;
3. 断言 launcher 文本:Windows 下 Node 分支只能通过
`if not exist "%RA3_RUNTIME%" goto :ra3_node` 到达。
### 其他已验证行为
- **CLI 会自然退出**:实测一次 live 查询后进程 59ms 内自然退出,
`fetch` 的 keep-alive 不会吊住 CLI(否则对 agent 是致命的可用性问题)。
- **live 端到端**`test/agentLiveE2E.test.mjs` 启动真实 `startLocalServer`
按扩展的方式写 instance/endpoint/manifest,然后用**同一个 `LiveClient`**
跑通全部工具,包括 `get_asset_references``definedIn` 继承溯源。
- **跨项目隔离**:为第三个项目创建的客户端在只有 A/B 实例时返回 null,
不会回落到 A 的数据。
- **发现降级**:删掉 `endpoints/` 后仍能通过 `instances/` 找到实例。
- **崩溃剪枝**:插入一个 PID 必死的实例,剪枝后另一个实例仍正常工作;
清理本窗口实例不影响其他窗口。