首页 / 课程 / D09 深挖
Part 3: 扩展集成 · 第 18 讲

24 讲路线 · 与 S09 配对

D09: Bridge IDE 深挖 · Bridge IDE

本讲在 S09 主线之上,聚焦实现细节、边界条件与自测;导图与主线相同模块,便于对照。

建议:先读完 S09,再按下方顺序走读源码与练习。

模块导图(与 S09 同源,便于对照):WS 协议、状态同步与冲突

🔬 深挖目标

IDE Bridge 是双主状态同步:编辑器 buffer、宿主会话、文件系统三角形里任意一边更新都可能冲突;本讲抓协议消息与冲突解决策略。

📡 消息类型(抽象)

  • 文档级:打开/关闭、变更 diff、光标/选区(视实现而定)。
  • 命令级:从 IDE 触发 run、从宿主推送诊断。
  • 心跳与重连:WebSocket 断线后如何对齐版本向量?

⚔️ 冲突模型

场景典型策略
宿主写文件同时 IDE 编辑Last-write-wins / 提示重载 / OT
大补丁 apply 失败回滚到 snapshot + 通知模型
二进制文件禁止文本 merge,整文件替换

🔗 与工具链

Apply_patch / write 工具应与 bridge 共用「单一写入层」,否则会出现磁盘与 buffer 撕裂(调试极痛)。

📖 走读顺序

  1. 定位 bridge server 启动点与端口协商。
  2. 列出所有 WS 事件枚举,标注哪些是 fire-and-forget。
  3. 断网 10 秒再恢复,观察是否丢事件;若丢,缺陷在哪?

✏️ 自测 1 · 参考答案:「宿主覆盖 IDE 未保存 buffer」测试用例

目标:验证当磁盘已被宿主(工具写文件)更新,而 IDE 里同一文件仍有未保存编辑时,产品行为是否符合预期(提示 / 重载 / diff),且不会静默丢一边的修改

前置条件

  • 工作区文件 src/foo.ts,磁盘内容为版本 A
  • IDE 打开该文件,用户改成版本 B未保存(buffer dirty)。
  • Bridge 已连接;可观测 IDE 通知或 RPC 日志。

操作步骤(自动化可脚本化)

  1. 记录 IDE 侧「文档版本号」或 hash(若有);记录 buffer 文本 B
  2. 在宿主侧触发一次合法写盘:例如模拟 Write / apply_patch 工具把 foo.ts 写成版本 C(与 B 冲突)。
  3. 不向 IDE 发送保存;观察 bridge 是否推送「文件在磁盘已变」类事件。

期望结果(择一或组合,需在需求里写死)

策略断言
安全默认IDE 提示「磁盘已在外部修改」,禁止静默用 C 覆盖 buffer B;用户选「重载丢本地」或「另存为」。
Diff 流(重建树中有 openDiff 思路)打开 diff 页,左侧/右侧与 B/C 一致;用户 Accept 后 buffer 与磁盘对齐为选定版本。
宿主回读工具链在写盘后若需继续对话,read_file 读到的是 C,且日志中可见「IDE 未保存」标记(若有)。

伪代码(集成测试骨架)

// 伪代码:用假 IDE client + 真文件系统即可跑通思路
test('host write while IDE buffer dirty', async () => {
  const path = 'src/foo.ts'
  await resetFile(path, 'A')
  const ide = await openInFakeIDE(path)
  ide.setBufferContents('B', { saved: false })

  await hostToolWrite(path, 'C') // 应走与正式产品相同的写入层

  const ev = await ide.waitForEvent('external_change' /* 或等价 RPC */, 3000)
  expect(ev).toBeDefined()
  expect(ide.getBufferContents()).not.toBe('C') // 未确认前不应静默等于磁盘
  // 用户点击「Reload from disk」后:
  ide.simulateReloadFromDisk()
  expect(await readDisk(path)).toBe(ide.getBufferContents())
})

重建源码里与「在 IDE 里展示差异、等用户保存/关闭」相关的线索可见 useDiffInIDE.tscallIdeRpc('openDiff', …) 及对 save / closed / rejected 消息的分支——测试用例应对照你的产品是否具备同等显式状态机

✏️ 自测 2 · 参考答案:Bridge 为何不应直接执行任意 shell?

核心结论

Bridge / IDE 插件运行在另一信任边界(编辑器进程、扩展宿主),若在这里开「任意 shell 执行」旁路,会整体绕过终端侧已实现的 canUseTool、审计、配额与沙箱策略,形成双轨执行面——安全与可观测性都会塌缩。

分点分析

  1. 权限与同意:Bash 在 CLI 里走 D03 的 ask/auto/deny;若在 bridge 里直接 exec,用户永远看不到同一条确认链,「我明明没批准终端跑 rm」却发生删文件——责任界面断裂。
  2. 审计与合规:企业场景要回答「谁、在什么会话、对哪条命令点了允许」;旁路 shell 不会进入同一 tool_result / 日志管道,事后无法复盘。
  3. 一致的工具契约:正式路径是「模型 → tool_use → 校验 → 权限 → 执行 → tool_result → 下一轮」;bridge 直连 shell 会破坏消息顺序与重试语义(D01)。
  4. IDE 侧攻击面:扩展可能被恶意配置或供应链污染;若扩展能直接 shell,等于给攻击者多一个入口,而宿主无法统一限流与封禁。
  5. 测试与复现:所有自动化应只断言「通过 MCP/RPC 触发了等价于某工具的调用」,而不是「机器上曾 spawn 过 sh」,否则 CI 与本地行为不可比。

反模式 vs 推荐模式(示意)

// ❌ 反模式:IDE / bridge 收到消息后偷偷 exec
bridge.on('runCommand', (cmd: string) => {
  child_process.exec(cmd) // 无 canUseTool、无 tool_result、无会话归因
})

// ✅ 推荐:bridge 只传「意图」或结构化 payload,由 CLI 宿主调度工具
bridge.on('requestAction', (payload) => {
  enqueueAsUserOrSystemMessage(/* … */)
  // 最终仍进入 QueryEngine → Tool 执行 → 权限门 → 与终端路径一致
})

实务上,允许 IDE 触发有限、白名单动作(如「在终端里粘贴已生成命令」)可以讨论,但「任意 shell」应始终禁止;与 D03D01 工具失败处理 同读,可拼出完整宿主安全模型。

✏️ 自测(题干回顾)