Files
Ra3ModXmlExt/docs/ai-agent-integration-plan.md
T
2026-09-10 17:03:10 +02:00

774 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RA3 Mod XML AI Agent 接入计划与进度追踪
> 状态:实施中
> 创建时间:2026-09-09
> 目标:让 AI Agent / 其他工具能够方便、可靠地使用 RA3 Mod XML 扩展构建的语义索引。
---
## 进度追踪
| 阶段 | 内容 | 状态 |
|---|---|---|
| Phase 0 | 计划与进度文件 | ✅ 完成 |
| Phase 1 | 外部快照格式与导出命令 | ✅ 完成 |
| Phase 2 | 查询核心与 CLI | ✅ 完成 |
| Phase 3 | MCP Server | ✅ 完成(基础 stdio MCP |
| Phase 4 | MCP 配置助手 / 稳定 launcher | ✅ 完成(launcher + 启用命令 + Claude/Cursor 配置写入 + 复制配置) |
| Phase 5 | Agent Skill 安装器 | ✅ 完成(默认安装 + 多位置选择 + 安装记录 + 卸载/同步函数) |
| Phase 6 | Live 查询与快照合并策略 | ✅ 完成(本地只读 HTTP live server + endpoint 文件 + MCP 在线优先/离线兜底 + final 索引静默 5 秒自动快照) |
| Phase 7 | 测试、文档、发布 | 🟡 部分完成(README + 计划文档已更新;全部 31 个 test/*.test.mjs 逐个直接运行通过;已用直接 esbuild CLI 验证 dist 构建与 MCP smoke test`node --test` 受沙箱限制,尚未 VSIX 打包/发布) |
### 二期进度(2026-09-10
| 阶段 | 内容 | 状态 |
|---|---|---|
| 二期 A1 | live 响应携带 `projectDir`MCP 侧校验 | ✅ 完成 |
| 二期 A2 | endpoint 按项目分文件(保留全局兜底) | ✅ 完成 |
| 二期 A3 | live server 支持 `?project=` 路由(不再只看活动编辑器) | ✅ 完成 |
| 二期 A4 | 关闭窗口只清理自己写的 endpoint | ✅ 完成 |
| 二期 A5 | `processId` 存活校验 | ✅ 完成 |
| 二期 A6 | live 失败负缓存 | ✅ 完成 |
| 二期 A7 | `snapshotBaseName` 改为 sha1 前缀 + 可读 slug | ✅ 完成 |
| 二期 B1 | Skill 适用范围段落(正/负信号 + `get_status` 探针) | ✅ 完成 |
| 二期 C1 | `get_asset_references`:有界、带 provenance 的正向引用查询 | ✅ 完成 |
| 二期 C2 | 本地 HTTP `/get_asset_references` 与 MCP 工具接入 | ✅ 完成 |
| 二期 C3 | 二期测试与文档 | ✅ 完成(264/264 测试通过) |
---
## 1. 背景与目标
当前 RA3 Mod XML 扩展已经能构建大型项目的语义索引,包括:
- asset 定义:`type / id / file / line / origin / stream`
- `$DEFINE`
- `<Include>` / `xi:include` 关系
- 语义引用和反向引用
- manifest 资产
- `.w3x` art 资产
这些能力目前主要服务于 VS Code 编辑器内部。
本计划的目标是:
1. 让 AI Agent、脚本和其他工具能够方便、可靠地使用该索引;
2. 不需要用户注册全局 CLI / 修改 PATH;
3. 安装/更新扩展后能通过一次性设置完成接入;
4. 在用户持续编辑代码时,索引查询仍然足够实时且不会造成磁盘写放大;
5. 为 AI Agent 提供工具 + 引导 Skill,让它知道何时、如何调用索引。
---
## 2. 核心设计决策
### 2.1 不采用“全局 CLI + PATH”作为主入口
CLI 可以作为高级/调试工具保留,但不是默认路径。
### 2.2 以 MCP Server 作为 AI Agent 主接口
MCP Server 使用本地 stdio 启动,通过绝对路径配置即可,不需要 PATH。
### 2.3 内存索引为主,磁盘快照为辅
- 实时查询尽量走 VS Code 扩展内存中的 `ModIndex`
- 磁盘只保存低频、合并后的“当前稳定快照”作为离线兜底;
- 避免用户连续编辑保存时产生大量磁盘 IO。
### 2.4 Agent Skill 是可选增强层
MCP 注册解决“工具可用”,Skill 解决“Agent 知道怎么用好工具”。
Skill 不作为启用 MCP 的强制条件。
### 2.5 所有查询结果都携带索引状态
外部接口必须返回:索引是否存在、是否正在构建、是否完整、是否 stale、数据版本/时间。
---
## 3. 目标架构
```text
VS Code 扩展
├── 内存 ModIndex
│ │
│ ├── 本地只读查询服务(可选,热数据)
│ └── 低频快照导出(冷数据)
├── MCP 配置助手
│ └── 写入 AI 客户端配置 + 稳定 launcher
└── Agent Skill 安装器
└── 安装到 ~/.agents/skills 等位置
AI Agent / 外部工具
├── MCP Server(推荐)
│ ├── 扩展在线 → 查询内存/本地服务
│ └── 扩展离线 → 读取最近稳定快照
├── CLI(可选)
└── HTTP/JSON-RPC(可选)
```
---
## 4. 组件设计
### 4.1 外部快照格式
定义一份稳定、公开的快照 schema,不直接暴露内部 workspaceStorage 缓存。
```json
{
"schemaVersion": 1,
"project": "D:/Mods/CoronaMod/mods/mods/corona",
"generatedAt": "...",
"buildId": 128,
"phase": "art",
"complete": true,
"stale": false,
"stats": {
"assetCount": 38304,
"referenceCount": 97862,
"defineCount": 240,
"streamCount": 12
},
"assets": [
{
"type": "GameObject",
"id": "...",
"file": "Data/...",
"line": 10,
"origin": "project",
"stream": "static",
"viaInstance": false
}
],
"defines": [],
"references": [],
"includeGraph": [],
"sourceCandidates": []
}
```
生成方式:
-`ModIndex` 导出;
- 使用数组而不是 JS Map
- 原子写入:`temp + rename`
- 同一项目只维护 `current` 快照,不保留每次历史版本;
- 可选维护 `previous` 用于构建期间回退。
### 4.2 MCP Server
MCP Server 不直接持有完整索引,而是作为查询代理:
1. 扩展在线时,转发到 VS Code 扩展的本地只读查询服务;
2. 扩展离线时,读取最近一次稳定快照;
3. 所有工具返回结果时附带索引状态。
建议 MCP 工具集:
```text
get_status()
list_projects()
find_asset(id, type?)
find_references(id, type?)
list_assets_by_type(type, prefix?, limit?)
get_definition(type, id)
resolve_include(source)
is_file_active(path)
find_define(name)
get_usage_guide()
```
### 4.3 稳定 launcher 与 MCP 配置助手
扩展安装路径会随版本变化,所以不能直接写死在 MCP 配置里。
稳定入口:
```text
~/.ra3modxml/
ra3-mod-xml-mcp.cmd
projects.json
snapshots/
current.json.gz
meta.json
skill-install.json
```
MCP 配置示例:
```json
{
"mcpServers": {
"ra3-mod-xml": {
"command": "C:\\Users\\lanyi\\.ra3modxml\\ra3-mod-xml-mcp.cmd",
"args": ["--project", "D:\\Mods\\CoronaMod\\mods\\mods\\corona"]
}
}
}
```
扩展每次激活/升级时刷新 launcher 内容,指向当前扩展安装目录。
这样 MCP 配置只需要写一次,升级不断。
### 4.4 Agent Skill
默认安装:
```text
~/.agents/skills/ra3-mod-xml/
├── SKILL.md
└── references/
└── query-guide.md
```
Skill 内容原则:
- 只描述功能本身;
- 说明“什么情况下该用这个 Skill”以及“怎么用”;
- 不引用各个工作区里的具体文档,例如 `docs/codebase-navigation-guide.md`,避免注意力噪音;
- 保持通用,不绑定某个 mod。
Skill 内容建议包括:
- 索引覆盖范围和状态含义;
- MCP 工具用途;
- 推荐查询工作流:
- 从 ID 找定义 → `find_asset`
- 找所有引用 → `find_references`
- 确认文件是否 active → `is_file_active`
- 追继承链 → `find_references` + `find_asset`
- 找未引用资产 → `unreferenced`
- 使用规则:
- 优先查询索引,而不是全项目 `rg`
- 查询尽量带 type,避免同名不同类型混淆;
- 不要把整个索引快照读进上下文;
- 如果索引状态不是 ready/stale,应查询状态或重试。
### 4.5 索引状态与一致性
定义索引状态:
| 状态 | 含义 |
|---|---|
| `no_index` | 从未构建 |
| `building` | 正在构建/重建 |
| `ready_xml` | XML phase 已发布,art 未完成 |
| `ready` | 完整 final 索引 |
| `stale` | 构建期间有文件变化 |
| `error` | 构建失败但保留 last good |
查询接口支持两种模式:
- 默认 / eventual:有可用快照就返回,同时附带 `stale/incomplete` 标记;
- 严格 / consistent:调用方传 `wait_for_index=true` / `allow_stale=false`,等待新 final 索引或超时。
### 4.6 持久化策略
- 热数据:内存 `ModIndex`
- 实时查询:本地查询服务 / MCP 转发;
- 冷数据:低频快照,例如:
- 用户手动导出;
- 索引稳定一段时间后自动写一次;
- 扩展退出前写一次(尽力而为);
- 不每次 rebuild 都写全量快照。
### 4.7 用户交互流程
安装/更新扩展后:
1. 首次索引完成;
2. 如果尚未启用且未被用户忽略,弹出提示:
```text
RA3 Mod XML 索引已就绪。
是否让 AI Agent 使用索引查询能力?
```
3. 用户点击“启用”;
4. 扩展默认注册 MCP Server
5. 同时询问/默认勾选:
```text
[✓] 同时安装 RA3 Mod XML Agent Skill(推荐)
```
6. 写入 MCP 配置;
7. 安装 Skill 到 `~/.agents/skills/ra3-mod-xml/`
8. 提供“安装到其他位置”或“手动操作指南”。
### 4.8 安全设计
- 本地查询服务只监听 `127.0.0.1`
- 使用随机 token
- MCP Server 默认只读,不提供文件修改/命令执行工具;
- 写 MCP 配置前必须用户同意;
- Skill 安装目录记录 managed marker,避免覆盖用户自定义内容。
---
## 5. 实施阶段
### Phase 1:外部快照格式与导出命令
- 新增 `src/agent/snapshot.ts`
- 新增命令:`ra3modxml.exportIndexSnapshot`
- 定义 `schemaVersion`
-`ModIndex` 生成可序列化快照
- 原子写入 `~/.ra3modxml/snapshots/current.json.gz`
- 输出状态元数据
### Phase 2:查询核心与 CLI
- 复用纯 TS `indexer` 模块;
- 新增 `src/agent/query.ts`
- `findAsset`
- `findReferences`
- `isFileActive`
- `listAssetsByType`
- `getStatus`
- 新增 CLI 入口(可选):
- 不注册 PATH
- 通过绝对路径调用;
- 供脚本/调试使用。
### Phase 3MCP Server
- 新增 `src/agent/mcpServer.ts`
- 支持 stdio 协议;
- 启动时读取连接信息/快照;
- 提供前述 MCP 工具;
- 每个响应带索引状态。
### Phase 4MCP 配置助手
- 新增 `src/agent/setup.ts`
- 创建稳定 launcher
- 检测/写入常见客户端配置:
- Claude Desktop
- Cursor
- Codex
- 通用 JSON
- 提供“复制 MCP 配置”;
- 提供手动操作指南。
### Phase 5Agent Skill 安装器
- 新增 `src/agent/skill.ts`
- 生成 `SKILL.md` + `references/query-guide.md`
- 默认安装到 `~/.agents/skills/ra3-mod-xml/`
- 可选安装到:
- `~/.claude/skills/ra3-mod-xml/`
- 项目 `.agents/skills/`
- 项目 `.claude/skills/`
- 自定义位置
- 记录已安装位置,扩展升级时自动同步;
- 不覆盖用户自定义内容。
### Phase 6Live 查询与快照合并
- 在扩展内启动本地只读查询服务;
- MCP Server 在线时优先转发到扩展;
- 离线时读取 `current.json.gz`
- 实现快照 quiet-period 合并写入,避免频繁全量落盘;
- 实现状态机:`no_index / building / ready_xml / ready / stale / error`
### Phase 7:测试、文档、发布
- 测试快照导出/导入一致性;
- 测试 MCP 工具查询;
- 测试 Skill 安装/更新/卸载;
- 测试持续编辑场景下的状态与 stale 行为;
- 更新 README / docs
- 发布新版本。
---
## 6. 预估文件改动
### 新增文件
```text
src/agent/
types.ts
snapshot.ts
query.ts
mcpServer.ts
setup.ts
skill.ts
localServer.ts
launcher.ts
docs/
ai-agent-integration-plan.md
```
### 修改文件
```text
package.json # commands / configuration
src/extension.ts # 注册命令、设置、初始化
src/workspace.ts # 暴露 index status / snapshot 发布钩子
src/features/agentSetup.ts # 用户引导 UI(或并入 agent/setup.ts
```
---
## 7. 验收标准
1. 用户安装/更新扩展并完成一次索引后,可以看到“启用 AI Agent 访问”的引导。
2. 用户点击启用后,不需要手动配置 PATH,不需要手动查找 workspaceStorage。
3. 常见 AI 客户端能通过 MCP 调用索引工具。
4. Agent 能正确回答:
- “某 asset 定义在哪个文件?”
- “谁引用了这个 asset?”
- “这个 XML 文件是否真的被 Include?”
5. 查询接口在无索引/索引更新中不会返回误导性空结果。
6. 用户连续编辑保存时,不会因为频繁快照写入而产生明显卡顿或磁盘膨胀。
7. Skill 安装后,Agent 知道优先使用 MCP 查询,而不是全文 grep。
8. 扩展升级后,MCP 配置仍然有效,Skill 能自动同步到已安装位置。
---
---
# 第二部分:二期计划(讨论结论 + 实施)
> 讨论时间:2026-09-10
> 主题:Skill 的项目识别边界、正向/间接引用查询的取舍、MCP 与 localhost 架构现状
---
## 8. 讨论结论一:谁来判定"这是不是一个红警 3 模组项目"
### 问题
Skill 需要说明"何时应该被使用"和"适用范围"。这要求 Agent 能理解什么是
"红警 3 模组项目里的 XML",同时避免把无关项目误判为 RA3 模组。
问题在于:这个判断应该由扩展/Skill 自己解释(甚至给出推断规则),还是应该
由项目开发者在自己的项目文档 / `AGENTS.md` 里声明?
### 结论:分层,且 Skill 不做复杂推断
| 层 | 职责 | 理由 |
|---|---|---|
| **Skill** | 说明能力覆盖范围 + 少量正/负信号 + 便宜的探测方式 | Skill 是用户全局的,会在**所有**对话里加载,必须简短且能自我限制 |
| **项目开发者**`AGENTS.md` / README / 项目标记) | 声明"这个仓库是什么项目、要不要用这些工具" | 只有项目自己知道自己是什么,这是每个仓库的局部事实 |
| **MCP 工具自身** | 用响应证明自己是否可用 | `get_status` 是最便宜的判定,不应靠读文档猜 |
### 为什么不把判断规则写进 Skill
- Skill 在 `~/.agents/skills/` 下全局生效;如果写一堆判定规则,会在与 RA3
无关的项目里制造"要不要用这个工具"的注意力噪音。
- 规则永远不完备。真实反例:
- 目录里有 `Schemas/xsd/CnC3Types.xsd` → 可能只是 SDK 本身,不是 mod;
- 只有一堆从 SDK 抄来的 `.xsd` → 是"RA3 相关",但没有 mod 数据;
- `CnC3Types.xsd` 里的 `CnC3`**C&C3**(命令与征服 3 / 凯恩之怒),
所以"存在 CnC3Types.xsd" **不等于** "是 RA3 模组"
-`Data/Mod.xml` / `*.babproj` / `Data/additionalmaps/mapmetadata_*.xml`
→ 基本可确认是 **SAGE / BinaryAssetBuilder** 系项目(RA3 只是其中之一)。
- 一旦误判,Agent 会拿 RA3 的索引数据去回答另一个项目的问题,比不使用更糟。
### 落地方案
**Skill 侧**`src/agent/skill.ts``SKILL_MD`)新增 applicability 段落:
- 正向信号(任一命中即可尝试):
`Data/Mod.xml``Data/additionalmaps/mapmetadata_*.xml``*.babproj`
XML 根为 `<AssetDeclaration>`;使用 `<Includes><Include source="DATA:…"/></Includes>`
- 明确否定:无关仓库、仅含拷贝 `.xsd` 的项目、通用 XML 配置、构建脚本、
非 SAGE 游戏项目。
- 便宜的探针:不确定时先调 `get_status`;它返回索引归属的 `projectDir`
如果与当前工作区根不一致,或状态是 `no_index`,就**停止使用索引工具**
改为直接读文件。
这样即使误触发,成本也只是一次 `get_status`,不会污染后续推理。
**项目开发者侧**:在项目根 `AGENTS.md` 写一句即可,例如:
```markdown
## RA3 Mod tooling
This repository is a Red Alert 3 mod (SAGE XML). When locating asset
definitions or references, prefer the `ra3-mod-xml` MCP tools
(`find_asset`, `find_references`, `is_file_active`) over full-text search.
```
扩展**不自动**往用户仓库写标记文件(如 `.agents/ra3-mod-xml-project.json`
作为二期内容;若将来提供,必须显式确认。
---
## 9. 讨论结论二:要不要提供"某个 Asset 直接/间接引用的所有 Asset"
### 场景
用户要求 Agent"查阅或修改雅典娜炮的武器"。Agent 需要:
1. 找到雅典娜炮的 GameObject
2. 读源码,发现 `<FireWeaponUpdate />` / `<WeaponSetUpdate />` 等子元素,
在这些子元素里找到武器引用;
3.`<CreateObjectDie />` 引用了临时物体,还要去读临时物体的 GameObject
在它的 `FireWeaponUpdate` 里找武器;
4. 若雅典娜炮只有 `FireWeaponUpdate``WeaponSetUpdate`
`inheritFrom``BaseCannon` 上,还要读 `BaseCannon` 并综合判断。
评估问题:
- 是否有必要提供"某个 Asset 直接或间接引用的所有 Asset"
- 只给精炼 asset 列表,Agent 不知道是哪个子元素导致的引用,有用吗?
- 若同时提供上下文,磁盘/内存/性能允许吗?内容长度会不会反而比直接读源码更贵?
- 相对直接读源码未必有显著提升,反而增加注意力噪音?
- 间接引用会不会多项式/指数爆炸?
### 先指出当前实现的真实缺口
现在的索引只有**反向**引用(谁引用了我),没有**正向**查询(我引用了谁)。
`find_references(AthenaCannon)` 返回的是"谁引用了雅典娜炮",而不是"雅典娜炮
用了哪些武器"。所以上述 Agent 工作流**目前完全无法用工具完成**,只能读源码。
因此问题不是"要不要锦上添花",而是这里确实缺一个基础能力。
### 结论:不提供"所有可达 Asset 的扁平闭包",改提供"有界 + 带 provenance 的边列表"
**不采用扁平静态闭包的理由:**
1. **丢失 provenanceAgent 仍然要读源码。** 到底是 `FireWeaponUpdate` 的武器、
`WeaponSetUpdate` 的武器、`CreateObjectDie` 临时物体的武器,还是
`inheritFrom` 继承来的武器?这决定了 Agent 改哪个文件、加不加
`xai:joinAction`。丢掉上下文,列表基本没用。
2. **尺寸会失控。** 一个 GameObject 的可达闭包可能是
`Weapon → Projectile → Warhead → DamageNugget → FX → Particle → Texture`
`Model → Mesh → Material → Texture``AttributeModifier``Upgrade``Command`
`Button`……Corona 规模下单个单位几百到上千节点完全可能。200 条边就是几万
token**比直接读 3 个小 XML 文件贵得多**。
3. **注意力噪音。** 列表里 80% 是与当前问题无关的 Texture / FX 时,推理质量下降。
4. **"间接"的诱惑会持续膨胀。** 一旦提供无界闭包,Agent 会习惯性调用它,
然后被淹没。
**但以下特性确实值得工具化:**
- 多跳遍历是**纯机械**的,正是工具该做的事;
- `inheritFrom` 链 + `xai:joinAction` 语义很微妙,Agent 手推容易错;
- 每跳都 read 一个文件,Corona 下成本和 token 都很高;
- 工具可以做到**确定性、不遗漏**(按 XSD 判定"哪些属性是引用"
而不是靠 Agent 逐个注意)。
**设计要点(二期实现遵循):**
- 返回**边列表**而不是节点集合。每条边携带:
- `from`effective asset)、`to`(解析出的定义位置)、
- `via.kind``attribute` / `content` / `inheritFrom`)、
- `via.element``via.parent``via.attribute`
- `source.file` / `source.line`XML 实际所在位置),
- `definedIn`(当该边来自 `inheritFrom` 祖先的 XML 时,标明真正写它的资产)。
- **默认 `depth: 1`,硬上限 3。** Agent 拿到直接引用后自己决定是否对某个具体
节点递归,比一次性 dump 闭包更省 token、更有针对性。
- **`targetTypes` 过滤**是抑制噪音最有效的手段。雅典娜炮场景就是
`targetTypes: ["WeaponTemplate"]`(可选再带 `GameObject``CreateObjectDie`),
返回可能只有 310 条边。
- **`inheritFrom` 不消耗 depth**:继承是资产自身定义的一部分,不是运行时引用。
因此在同一 depth 内递归遍历继承链(链长上限 8),并把 `definedIn` 标出来。
- **截断时返回 `omittedByTargetType` 摘要**,让 Agent 知道被砍掉了什么形状的数据,
可以精确下钻,而不是无脑重试或放弃。
- **不提供"合并后的有效值"**`joinAction` 的 Replace/Remove/merge 语义算错的风险
太高,一期不做。
**关于爆炸和成本:**
- 算法上不是指数:带 visited-set 的 BFS 是 `O(V+E)`。真正的风险是**语义宽度**
不是复杂度,因此用 `depth` + `targetTypes` + `maxEdges` 三重限制。
- **只做 live 查询**(VS Code 打开时可用)。要拿元素上下文,最干净的做法是按需
解析定义所在文件的 DOM,而不是往 records 缓存里塞元素名/属性名 —— Corona 有
~98k 条引用记录,再塞 5 个字符串字段会让常驻内存明显膨胀,为一个默认 `depth=1`
的功能付出这个成本不划算。
- 离线快照路径明确返回"需要 live index",而不是静默返回空结果。
---
## 10. 讨论结论三:MCP / localhost 的实际架构与多窗口缺陷
### 当前架构(事实)
```text
AI 客户端
│ stdio (JSON-RPC, MCP)
dist/agent/mcpServer.js ← 由 AI 客户端启动,参数 --project <dir>
│ HTTP (127.0.0.1:临时端口, Bearer token)
VS Code 扩展进程内的 localServer ← 仅在"启用 AI Agent 访问"后启动
├─ 在线 → ws.activeIndex() 实时内存索引
└─ 离线 → ~/.ra3modxml/snapshots/project-<hash>.json.gz
```
- **AI 客户端不需要知道端口。** MCP Server 自己读
`~/.ra3modxml/endpoint.json``url` + `token``fetch`。localhost 层是
纯实现细节。
- **端口**`server.listen(port ?? 0, "127.0.0.1")`,即由操作系统分配临时端口,
不会冲突;当前未传 `port`,所以永远是临时端口。
- **鉴权**`Authorization: Bearer <token>`token 为启动时随机生成;绑定
`127.0.0.1`,局域网访问不到。
- **降级**live 查询失败(endpoint 不存在 / 连接被拒 / 超时 1.5s)时回退到快照。
### 多项目 / 多 VSCode 窗口的真实缺陷
`endpoint.json` 是**全局唯一**的一个文件,所有 VSCode 窗口都往它写入:
1. **最后启用的窗口覆盖前面所有窗口。** 两个窗口都启用了 Agent 访问时,
只有后启动的那个能被 MCP Server 找到。
2. **可能串项目(严重)。** 假设窗口 B 覆盖了 endpoint,而某个 MCP Server 是用
`--project 窗口A` 启动的,它的 `tryLiveQuery` 会打到**窗口 B** 的 server
拿到窗口 B 的索引数据,而返回值里没有任何东西能暴露这个不一致。
这比 `no_index` 危险得多 —— 会静默给出错误答案。
3. **关闭任意窗口都会 `clearEndpoint()`**,把还开着的其他窗口的 live 通道一起断掉。
4. **同一窗口内多项目也不稳。** live server 用
`getIndex: () => ws.activeIndex()`,而 `activeIndex()` 跟着**当前活动编辑器**走。
用户切到另一个项目的文件,live 查询就会静默换项目;而 `endpoint.json` 里的
`projectDir` 只在启动时写一次,之后不更新。
5. `processId` 虽然写进了 endpoint,但**从未被校验**VSCode 崩溃后残留的 endpoint
只能靠连接失败兜底。
6. 离线快照路径是**正确**的 —— `snapshotPathForProject()` 按项目路径做 hash
多项目互不干扰。问题只在 live 层。
### 修复方案(二期 A,按性价比排序)
1. **live 响应必须带 `projectDir`MCP 侧必须校验它和 `--project` 一致**
不一致就直接走快照。这是最小的正确性补丁,最先做。
2. **endpoint 按项目存**`~/.ra3modxml/endpoints/<project-hash>.json`
MCP Server 按 `--project` 找对应的那个;同时保留 `endpoint.json` 作为
"最近"的兼容入口。
3. **live server 支持 `?project=` 路由**,而不是只认 `activeIndex()`
扩展侧加 `indexForProject(projectDir)`,一个窗口的一个 server 就能服务它
所有已索引项目,也就不怕用户切编辑器。
4. **关闭时只清自己的那一条**,不无条件删全局 endpoint。
5. **校验 `processId` 是否存活**,不存活就当 endpoint 失效。
6. `tryLiveQuery` 失败后做几秒的"live 不可用"负缓存,避免每次调用都试一次。
7. `snapshotBaseName` 换成 sha1 前缀 + 可读 slug(与 `diskCacheKey` 一致的做法),
降低碰撞概率。
---
## 11. 二期实施清单
### 二期 A:多窗口 / 多项目正确性
- `src/agent/snapshot.ts`:新增 `projectHash()``snapshotBaseName()` 改为
`<slug>-<sha1-12>`
- `src/agent/endpoint.ts`:新增
`endpointPathForProject()` / `writeEndpointForProject()` /
`readEndpointForProject()` / `clearEndpointForProject()` / `isProcessAlive()`
- `src/workspace.ts`:新增 `indexForProject(projectDir)`
- `src/agent/liveQuery.ts``liveStatus()` 在没有索引时回落到请求的 `projectDir`
- `src/agent/localServer.ts``getIndex(projectDir?)` 路由、异步 handler、
`/projects` 端点、所有响应携带 `index.projectDir`
- `src/agent/mcpServer.ts`:按项目读 endpoint、校验 `projectDir``processId`
`?project=` 透传、live 失败负缓存、响应 `projectDir` 二次校验。
- `src/extension.ts`:启动时按项目写 endpoint、关闭时只清自己写过的那些。
### 二期 BSkill 适用范围
- `src/agent/skill.ts``SKILL_MD` 增加 applicability 段落(正向信号、明确否定、
`get_status` 探针);保持"不引用工作区文档"的原则。
### 二期 C`get_asset_references`
- `src/agent/forwardRefs.ts`(新增):DOM-based、有界、带 provenance 的正向引用遍历。
- `src/agent/localServer.ts`:新增 `GET /get_asset_references`
- `src/agent/mcpServer.ts`:新增 `get_asset_references` 工具(live-only
离线时返回明确的"需要 live index"说明)。
- `src/extension.ts`:为 live server 提供 `loadFile()`(读文本 + 解析 + LineMap)。
### 二期 D:测试
- `test/agentEndpoint.test.mjs`:按项目 endpoint、进程存活、清理隔离。
- `test/agentLocalServer.test.mjs``?project=` 路由、`projectDir` 校验、
`/get_asset_references`
- `test/agentForwardRefs.test.mjs`:直接引用、继承链、`CreateObjectDie` 内容引用、
`targetTypes` 过滤、`maxEdges` 截断、`depth` 上限。
- `test/agentSkill.test.mjs`Skill 文本包含 applicability 段落且不含工作区文档引用。
- `test/agentMcpRouting.test.mjs`live URL 固定 `project`、跨项目响应拒绝。
---
## 12. 二期实施结果
全部完成,`npx tsc --noEmit` 通过,`test/*.test.mjs`**264/264** 通过。
### 新增 / 修改文件
```text
新增:
src/agent/forwardRefs.ts 有界、带 provenance 的正向引用遍历
test/agentForwardRefs.test.mjs
test/agentMcpRouting.test.mjs
修改:
src/agent/endpoint.ts 按项目 endpoint + isProcessAlive()
src/agent/snapshot.ts projectHash() / projectSlug() / snapshotBaseName()
src/agent/liveQuery.ts liveStatus() 回落请求的 projectDir
src/agent/localServer.ts 异步 handler、?project= 路由、/projects、/get_asset_references
src/agent/mcpServer.ts 按项目 endpoint、projectDir 校验、负缓存、get_asset_references
src/agent/skill.ts applicability 段落 + get_asset_references 用法
src/workspace.ts 新增 indexForProject()
src/extension.ts 按项目写/清 endpoint、loadFile、endpoint 刷新
test/agentEndpoint.test.mjs
test/agentLocalServer.test.mjs
test/agentSkill.test.mjs
```
### 关键行为
**多窗口 / 多项目**
- `~/.ra3modxml/endpoints/<slug>-<sha1-12>.json` 一项目一文件;MCP Server 先读自己项目的
文件,读不到才退回全局 `endpoint.json`,且仅在 `projectDir` 匹配时使用。
- 每个 live 请求带 `?project=`server 用 `ModWorkspace.indexForProject()` 路由,
不再依赖"当前活动编辑器"。
- MCP 侧对响应里的 `index.projectDir` 做二次校验,不匹配就拒答并提示重新启用,
而不是给一个看起来合理但属于别的项目的答案。
- 关闭窗口只删除本窗口写过的 endpoint 文件。
- `processId` 不存活时 endpoint 视为失效。
- live 连接失败后有 5 秒负缓存,避免每次工具调用都探一次。
**Skill**
- 新增 `When this skill applies` 段落:正向信号(`Data/Mod.xml`
`mapmetadata_*.xml``*.babproj``<AssetDeclaration>`
`uri:ea.com:eala:asset`)、明确否定规则(无关仓库、仅含拷贝 `.xsd` 的项目)、
以及 `CnC3Types.xsd` 属于 C&C3 而非 RA3 的反例。
- 规定用 `get_status` 做便宜探针:状态为 `no_index``projectDir` 不匹配时
停止使用索引工具,改为直接读文件。
- 仍然不引用任何工作区文档(有测试断言)。
**`get_asset_references`(正向引用)**
- 返回**边**`from` / `to` / `via{kind,element,parent,attribute}` /
`source{file,line,character}` / `definedIn` / `value`
- `depth` 默认 1、上限 3`inheritFrom` 不消耗 depth,祖先 XML 在同层遍历,
`definedIn` 标出真正写这条边的资产(解决"雅典娜炮自己没有 WeaponSetUpdate
但 BaseCannon 有"的场景)。
- `targetTypes` 按 XSD 可赋值性过滤,但 `inheritFrom` 边始终保留,因为
它解释了其余边写在哪里。
- `maxEdges` 截断时返回 `truncated` + `omittedByTargetType`(只统计因上限被丢弃的边,
已被 `targetTypes` 过滤的不计入)。
- 只走 live 路径;不可用时返回明确的错误说明,而不是空结果。
### 已知限制
- `get_asset_references` 不计算 `xai:joinAction``Replace`/`Remove`)合并后的
有效值,需要调用方自行确认。
- 只有 live 路径支持正向引用;离线快照路径明确返回"需要 live index"。
- `snapshotBaseName` 改为 `<slug>-<sha1-12>`,旧的 `project-<hash>.json.gz`
快照文件不会被自动清理(该功能尚未发布,无兼容负担)。
- 本沙箱禁止 spawn 子进程,因此 `node --test``npm run build` 无法直接运行;
验证方式为:逐个 `node test/*.test.mjs`、直接调用 esbuild CLI 构建 dist、
用管道驱动 `dist/agent/mcpServer.js` 做端到端 smoke test。