令特定 Copilot Chat 会话暴露一个命名管道,允许任何外部进程通过管道向会话注入消息
项目定位
解决什么问题
VS Code Copilot Chat 的模型是逐轮调用的无状态函数:一轮对话结束、工具调用 resolve 之后,没有任何带内通道能让外部进程把新消息送进模型的上下文。MCP 服务器后台跑完一条远端命令、CI 监控脚本发现构建失败、任意本地守护进程产生了值得 agent 知道的事件——这些消息只能躺在进程里,等用户下一次手动开口。
VS Code 内部其实已有两条"宿主替外部主体唤醒模型"的先例:RunInTerminalTool._registerCompletionNotification 在后台终端命令完成时以 chatService.sendRequest(..., { isSystemInitiated: true, queue: Steering }) 伪造一轮系统署名的对话;agent host 的 send_message 会话编排工具允许 agent 间跨会话投递消息(忙时排队、回合间隙送达、MessageKind.Agent 结构化出处)。但两者都是内置特权代码,不向 MCP 服务器和第三方扩展开放。
本扩展把这个能力做成一个指向 VS Code 的会话级注入口:用户在扩展内显式开放某个 chat 会话,该会话获得一条专属 named pipe(自动或自定义命名)+ 独立随机 token;持有凭证的本地进程经标准 IPC 输入消息,下游经 CDP 向该会话注入带机器署名的通知消息。未开放的会话在协议层不存在——没有"任意会话可达"这回事,只有"用户点名开放的会话可达"。
为什么是独立扩展
- 生态位为空:marketplace 与 GitHub 上不存在"MCP/外部进程异步结果注入 chat 会话"的已发布扩展;最近邻 Apc Customize UI++ 只做 UI 定制且走磁盘补丁路线,目标与风险类别均不同,不适合寄生
- 微软官方的同类语义(agent-queued messages、Automations)只覆盖 agent host 会话,经典 sidebar 会话的带外唤醒仍是空白;且官方实现为将来收编提供了明确的语义规范(排队、机器出处、忙时不打断)
设计目标
核心目标
- 会话级白名单开放:由用户在扩展中显式配置哪些会话对外开放,未开放的会话不存在任何管道入口、在协议层不可达。每个开放的会话自动分配一条专属 named pipe(管道名自动生成,用户亦可自定义),管道名 + 随机 token 共同构成该会话的注入凭证。token 安全等级由用户掌握:默认随机 64 位 hex,亦可自定义任意字符串,或故意设为空 token(免鉴权:任何能连上该管道的本机进程都能注入,风险用户自担)——服务端不对 token 做字符集/长度校验。
- 极简上游协议:上游进程只能按名称接入管道然后发送消息——动词仅
initialize(token 鉴权)与 inject(注入文本)两个;无会话枚举、无跨会话寻址、无任何对本扩展的控制原语,不透传 eval 类内部机制
- 机器署名:注入消息渲染为
isSystemInitiated 紧凑行而非用户气泡,文本前缀 [<label> notification: ...],语义对齐官方 MessageKind taxonomy;绝不伪装用户
- 忙时排队:目标会话回合进行中时不强插,利用平台内建排队语义等回合结束送达(
sendRequest 的 queue 参数是前提:不传则忙会话被直接拒绝 Request already in progress);mode:'steering' 打断模式作为高危可选项单独审批
- 可收编:注入后端是可替换薄层——一旦官方 API(AHP send_message 开放 / MCP Tasks 接线)可用,整体替换而后端协议不变
非目标
- 不注入 agent host 托管的会话(Agents 窗口 / Copilot CLI / Cloud Agent,其 sendRequest 路由不经本窗口 renderer——官方 skill buttons 用同路径的事实是强佐证但未实测,见"已知边界")
- 不做远程窗口(WSL/SSH remote 的 renderer 在远端,CDP 附着的是本地 renderer)
- 不承诺跨 VS Code 版本零维护:bundle 锚点随月度更新漂移是已接受的成本
- 不做消息之外的任何会话操控(读取 transcript、取消回合等均不在协议面)
- 不向上游提供任何会话可见性:会话枚举 / conversationId 解析等发现类能力是扩展内部实现细节(仅供用户在 VS Code 内选择要开放哪个会话),一律不出现在管道协议面上——上游无从得知本机存在哪些会话
- 不接受上游对扩展本体的任何控制:启停通道、改配置、查状态、吊销授权均只能由用户在 VS Code 内操作
技术路线
总体架构
flowchart LR
subgraph 上游本地进程
A[MCP server 后台任务]
B[CI watcher]
C[任意脚本]
end
subgraph Extension Host Node 进程
D[会话开放注册表\n每开放会话一条 named pipe\n协议面仅 initialize + inject]
E[CDP 客户端\nWebSocket → localhost:port\n按窗口标题复核 target]
F[argv.json 引导器\n+ WMI 派发自动重启助手]
I[磁盘会话枚举器\n直读 state.vscdb 索引\n零用户操作零 renderer 依赖]
end
subgraph Renderer workbench
G[断点钥匙交接\n主动:invokeFunction DI锚点+noop心跳撞击\n被动:acquireOrLoadSession/sendRequest/invokeTool]
H[注入器 acquireOrLoadSession\n+ sendRequest queue:'queued'\n+ isSystemInitiated]
end
A & B & C -->|管道名 + token| D
I -.零操作候选列表.-> D
D --> E --> H
G -.钥匙.-> H
F -.一次性写入 remote-debugging-port 并重启.-> E
六个组件的职责与依据:
argv.json 引导器:首次激活时检测 ~/.vscode-insiders/argv.json(dataFolderName 按 Stable/Insiders 区分)是否已含 "remote-debugging-port",缺失则以 JSONC 注释保留方式写入高位固定端口(如 17890),并提议自动重启一次。依据:该键在官方 SUPPORTED_ELECTRON_SWITCHES 白名单内(src/main.ts),IArgvConfig 类型声明与 JSON schema 注册齐备——是微软官方支持的持久启动参数面,不动安装目录、无"安装损坏"告警。写入必须防并发(VS Code 自身会维护 crash-reporter-id 字段),操作幂等。⚠️ 值必须是字符串("17890" 而非 17890):main.js 的开关处理只有 c===true||c==="true" 与 typeof c=="string"&&c 两个分支,JSON 数字两分支都不命中而被静默忽略(端口不开、零告警),官方 argv.json schema 亦声明该键 type:'string'
CDP 客户端:扩展宿主内用零依赖的最小 WebSocket 客户端连 http://127.0.0.1:<port>/json/list,取 type:'page' target。多窗口共端口是真实风险(两个窗口同开时,首取 page target 会附着到错误窗口),故附着前必须用 target 标题复核:标题须含本窗口 workspace 文件夹名,否则轮询等待,避免把注入打到别的窗口
renderer 钩子:经 Debugger.setBreakpointByUrl 在 bundle 锚点下断点,命中后在该帧词法作用域内用 evaluateOnCallFrame 完成钥匙交接(接收者自适应)。主动锚点(首选):DI 容器的 invokeFunction(InstantiationService 分发函数,workbench 一切命令派发都经它),配条件断点 + 扩展周期性主动执行内置 noop 命令(官方注册的零副作用空 handler,经 _instantiationService.invokeFunction 派发)——每次派发必撞锚点,空闲窗口也能秒级完成交接,无需任何用户 chat 交互;帧内沿 _parent 容器链找 chatService(已是实例直用;仅懒描述符时经 _getOrCreateServiceInstance 强制实例化),URI 构造器从容器链里的 workspaceContextService 候选提取(不依赖任何会话加载过)。被动锚点(兜底):ChatService.acquireOrLoadSession / ChatService.sendRequest / LanguageModelToolsService.invokeTool(bundle 实证定义各自唯一或首命中即目标),用户自然的 chat 交互命中时同样完成交接。主动撞击的零感知三重门:①条件断点 !globalThis['__vsciBridge.installed'] 由 V8 页内求值,条件不成立直接放行(零暂停、零 CDP 往返),装桥后永久失效——invokeFunction 是每秒被调上百次的热函数,不装条件断点会冻 renderer;②暂停收集器先于布点订阅(否则布点与订阅之间的命中丢帧,renderer 永久冻结无人 resume);③非目标暂停事件立即放行。交接后包装原型方法做被动映射积累(增强字段)。选断点而非纯 Runtime.evaluate 的原因:minified bundle 里 CancellationToken.None 等导入绑定已被改名(如 ye.None),只有帧作用域能拿到真实对象
磁盘会话枚举器(零操作候选):扩展宿主 Node 进程直读磁盘列出本工作区全部可注入会话,不依赖 renderer、不依赖钥匙交接、用户零操作:
- 数据源:
workspaceStorage/<hash>/state.vscdb(SQLite ItemTable)键 chat.ChatSessionStore.index 存完整会话索引(bundle 实证 ChatSessionStore.getIndex 读的就是这个键),与 renderer 侧 getHistorySessionItems() 同源
- 读取策略:零依赖二进制扫描(搜键名→括号平衡提取 JSON→解析校验,主库+WAL),刻意不用
node:sqlite(宿主 Node 版本随 VS Code 而定、参数支持不一、可能对宿主正持有的库加锁);chatSessions/<id>.jsonl 正档文件存在性交叉校验——SQLite 空闲页可能残留已删除会话的陈旧索引副本,正档随删除同步移除是最可靠的"真实存活"判据
- sessionId → sessionResource URI:
vscode-chat-session://local/<base64url(sessionId)>(官方 LocalChatSessionUri.forSession 源码与本机 DB 记录一致,Node 侧 Buffer.toString('base64') 后 -/_ 替换去 padding 逐字符一致)
- 过滤与官方
getHistorySessionItems 同条件:非外部、非空、initialLocation==='panel'(本机 bundle 实证该值就是字符串 "panel");作用域对齐官方 getIndexStorageScope:带文件夹工作区→WORKSPACE 作用域,仅当工作区级无数据时兜底 PROFILE 级 globalStorage(防空窗口的其它上下文会话混入候选)
- 渲染器桥就绪时,候选与 renderer 实时枚举合并增强(busy 实时状态/toolId→MCP server 名/未落盘新会话);桥未就绪也照常开放(注入执行时才需桥)
注入器:照抄 agentHostSkillButtons.ts 与 runInTerminalTool.ts 范文——acquireOrLoadSession(uri, location, token, owner) → sendRequest(uri, text, { queue:'queued', isSystemInitiated: true, systemInitiatedLabel, agentIdSilent, modeInfo, userSelectedModelId }) → kind==='queued' 时立即返回不阻塞等 deferred → finally { ref.dispose() }。agent 上下文四元组从 sessionRef.object.lastRequest 复制(官方注释明言弱模型接不住整轮会"left the agent silent")
会话开放注册表(IPC 层):Windows named pipe(按用户 ACL,天然免疫浏览器 localhost 探测与 DNS rebinding),JSON-RPC 2.0 换行帧。一条管道对应一个被用户开放的会话;凭证(管道名 + token)全部只存于扩展宿主进程内存,零落盘(不写 token 文件、不写 endpoint 发现文件;激活时清除 %LOCALAPPDATA%\vscode-chat-bridge 目录,保证磁盘上没有任何凭证残留):两者都默认自动分配、都允许用户随时修改(重命名管道 / 轮换 token),上游接入只能由用户亲手从凭证卡片复制交接
会话开放与管道分配
授权粒度 = 会话:
- 白名单开放:用户在扩展 UI 中选择一个或多个会话点击开放;未开放的会话不存在管道入口,即使攻击者拿到了别的会话的管道名也无从触及——会话之间在协议层互相不可见
- 每会话一条管道:开放动作产生专属 named pipe,管道名与 token 都默认自动分配(名形如
vsci-<会话短id>-<随机>,token 为 64 位随机 hex);开放时两者都可改(InputBox 预填默认值),开放后也可随时改(管理菜单里的重命名/轮换);两者均支持任意字符含中文/emoji,只保留经证实的真实约束:管道名禁反斜杠与 NUL(Windows named pipe 官方约束 + 内部分隔符),长 1~247(全串 \\.\pipe\<名> ≤256),大小写不敏感查重(OS 语义:Foo 与 foo 是同一条管道);token 无任何字符集/长度限制,安全等级由用户掌握(默认自动生成 64 hex 强随机值,亦可自定义任意字符串、或置空=免鉴权;真实威胁:管道名可被同用户进程枚举不是秘密、initialize 无失败限速,故非空 token 的熵是对在线爆破的唯一防线——置空或过短即自行承担)
- 管道名 + token = 凭证,且凭证纯内存:两者只存于扩展宿主进程内存,不写任何文件;唯一的“落地点”是用户眼前的凭证卡片(可一键复制管道名/token/接入参数对),交接完全经用户之手;上游必须两者齐备才能通过
initialize;窗口 reload/重启后凭证即消失,需重新开放(重新开放仍需用户亲手授权,不自动恢复)
- 上游能力封顶:接入管道后上游只能
inject(发一条带署名的消息),不能枚举会话、不能寻址其它会话、不能控制扩展本体
扩展内部的会话候选列表(供开放选择器使用)来自磁盘直读(磁盘会话枚举器,见组件 4):扩展激活即从 state.vscdb 索引 + chatSessions/ 正档交叉校验列出本工作区全部非空 panel 会话——用户无需先发过任何消息、无需打开过 chat 面板、无需任何工具调用,甚至无需 renderer 桥就绪。桥就绪后再合并 renderer 实时枚举以增强忙闲/MCP server 名。这份候选列表只服务于 VS Code 内的用户 UI,不对外暴露。
备注:MCP _meta['vscode.conversationId'](mcpServer.ts 源码实证)仍可用作用户侧的便捷识别——例如用户在 UI 里看到「最近调用过 ssh 工具的会话」这样的描述。但把 conversationId 换成 sessionResource 的反查也只在扩展内部发生,上游协议面不提供该动词。
- 抑制门四个:用户手动停止会话 → 静默;会话被删除/checkpoint 回滚(磁盘枚举中消失,扩展周期轮询检测;正档文件随会话删除同步移除,是最权威的存活判据)→ 自动吊销该会话的管道开放(防止悬挂凭证;两道防误删护栏:空枚举结果跳过、连续两轮都消失才吊销);窗口关闭 → 所有会话管道销毁、内存凭证随之消失(无文件可清);同一输出重复到达 → 按内容+时间窗去重节流
- 映射表与上游任务均带 TTL;CDP 断线(窗口 reload)后自动重连并重装钩子,期间到达的消息进本地重发缓冲
- 每会话独立管道:开放会话时创建、吊销时销毁(内存 token 即时作废、断开已接入 client、清空该通道 client 授权);重命名管道同样断开全部现有连接并清空旧名授权;管道随扩展宿主生命周期存在
- renderer 钩子必须幂等(重复 evaluate 不重复包裹),以定义式正则(方法名 minify 后不变)+ 容器内形状匹配定位目标,锚点集中管理在一处
使用方法
前置条件
- VS Code Desktop(Stable 或 Insiders);本窗口使用经典 sidebar/chat 会话
- 首次使用需接受一次性重启(写入 argv.json 后生效),扩展会提议经 WMI 自动重启
- 用户必须先在扩展内开放目标会话,未开放的会话没有管道入口、不可被注入
- 安全确认弹窗:被开放管道上首次注入的上游 client 仍需用户批准(client 审批)
目录结构与文件命名说明
src/ TypeScript 源码(extension.ts 入口;common/protocolContract.ts 协议契约)
dist/ tsc 编译产物(package.json main → dist/extension.js,不入库)
examples/ upstreamClient.ps1 上游客户端示例
icon.png marketplace 图标
为什么文件名全用 ASCII 英文:VS Code Marketplace 服务端不支持扩展包内的非 ASCII 文件名——上传时服务端解包会把文件名中的每个非 ASCII 字节逐一转成 ?,同目录下同字数的不同文件在字典键上直接碰撞,导致上传被拒(报 Item has already been added. Key in dictionary: 'extension/??/???????.js')。本地打包(VSIX 内文件名正确带 UTF-8 flag)完全合规,问题纯在服务端。因此本项目的文件名/目录名一律 ASCII;代码内部的中文标识符、注释与 UI 文案不受影响、照常保留。
安装与引导
- 安装扩展(marketplace 搜索安装,或本地
vsix 文件安装)
- 激活后扩展检测 argv.json,缺失则写入
"remote-debugging-port": "17890"(字符串值)并提议自动重启:同意后扩展经 WMI 脱离 Job Object 派发重启助手,保存→退出→自动重新拉起(仅 reload window 不会重启主进程、端口不开);重启后状态栏图标显示 CDP 连接与钩子装载状态
- 钥匙交接自动完成,无需任何用户操作:断点布设后扩展主动执行内置
noop 命令(零副作用空函数)周期性撞击 DI 容器锚点(invokeFunction,实测活跃窗口 162~480ms 内必撞,空闲窗口靠心跳约 1.5s),条件断点保证装桥前后零暂停零感知;即使主动锚点漂移,被动断点(acquireOrLoadSession/sendRequest/invokeTool)也覆盖你正常使用 VS Code 的几乎全部 chat 交互(打开面板/切换会话/发任何消息/任何工具调用)自然命中即交接。状态栏自动从「交接中」变「就绪」。「开放会话给外部注入」命令内保留一次 chat.open 兜底预热(handshaking 状态下才触发,面板已开着时只是聚焦),但通常用不到——主动撞击早已完成交接
- 命令面板提供:开放会话给外部注入 / 管理开放会话 / Enable / Disable 注入通道(参照 Apc 的一键复原设计)/ 查看审计日志(Output channel)/ 彻底重启 VS Code 以开启调试端口(端口未监听或运行中途失能时的一键修复,走 WMI 派发自动重启;状态栏点击后菜单也会按端口连通性把该项置顶并标注推荐)
开放会话(用户侧,授权的唯一入口)
- 命令面板 → 「Chat 注入器: 开放会话给外部注入」
- 选择器列出磁盘直读的全部非空 panel 会话(标题、最近活动;桥就绪时额外显示忙闲与来源 MCP server)——无需先发过消息或碰过 chat 面板,用户选定目标
- 确认凭证(两步 InputBox,都是“默认自动分配 + 可直接修改”):管道名预填自动分配值(形如
vsci-<会话短id>-<随机>),token 预填 64 位随机 hex;直接回车即用默认,也可改成自己的值(管道名仅禁反斜杠/NUL、长 1~247、大小写不敏感全局唯一;token 无字符集/长度限制,安全等级用户掌握——清空回车即空 token=免鉴权)
- 开放成功后弹出凭证卡片:管道全名
\\\\.\\pipe\\<名> + token 值(纯内存,无任何文件),可一键复制管道名/token/--管道名 <名> --令牌 <值> 参数对;凭证只在用户眼前落地,由用户自行交接给目标上游
- 随时可在「管理开放会话」中:补看/复制凭证、重命名管道、轮换 token(重新随机/自定义/设为空 token 免鉴权;旧值即时作废,已接入 client 需用新凭证重连)、吊销单个会话的开放(立即销毁对应管道)
未开放的会话没有任何管道入口;吊销/重命名/轮换后旧凭证立即失效;窗口 reload/重启后内存凭证消失,需重新开放。
上游客户端接入(协议面全部:2 个动词)
示例客户端为 PowerShell 脚本(无需 Node 运行时,.NET NamedPipeClientStream 直连管道)。握手成功后进入交互注入模式:每输入一行文本回车即注入一条,可连续发任意多条;/quit 或 Ctrl+C 退出,/mode queued|steering、/label <新署名> 可运行中切换。带了 --正文 会先自动注入那一条再接替循环(管道化 stdin 投喂时输入耗尽即自动退出,便于脚本化):
powershell -ExecutionPolicy Bypass -File examples/upstreamClient.ps1 --管道名 <名> --令牌 <值> [-客户端名 my-watcher] [-标签 我的监视器] [-正文 "消息正文"] [-模式 queued|steering]
# ——客户端名 既作 initialize 的身份(审批授权键,支持任意字符,仅禁 NUL/空/纯空白/超长),也是会话内缺省署名;-标签 只缺省时才需单独给(允许任意文本、改了不重新审批)
// 连接: \\.\pipe\<用户开放时分配或自定义的管道名>
// 1. 握手: 携带该会话的 token
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "clientName": "my-watcher", "token": "..." } }
// 2. 注入(无需也无法指定 target:管道即会话;mode 默认 queued)
{ "jsonrpc": "2.0", "id": 2, "method": "inject", "params": {
"label": "SSH task a1b2c3", // 渲染为 systemInitiatedLabel 与消息前缀
"text": "[SSH task a1b2c3 notification: command completed (exit 0)]\nOutput:\n...",
"mode": "queued" } }
// 目标忙 → 立即返回 { "queued": true }(不阻塞等送达),回合结束后平台自动作为新一轮送达
// 目标闲 → 立即返回 { "queued": false }
// 其它一律不存在:无 listSessions、无 resolveConversationId、无任何控制动词。
// 未知方法返回 -32601。
凭证不写任何文件(扩展激活时会清除 %LOCALAPPDATA%\vscode-chat-bridge 目录,磁盘凭证残留面归零),本机不存在任何可供上游自动发现的入口——没有用户亲手从凭证卡片复制交接,任何进程都无法接入。凭证卡片「复制接入参数对」产出的 --管道名 <名> --令牌 <值> 可直接粘贴到示例命令尾部(脚本自行解析参数,--/- 前缀在直接调用与 powershell -File 两种模式下都可用;PowerShell 直接调用时内建绑定不认 --,不能依赖它)。
安全模型
威胁模型
本扩展实质是"接受被授权本地进程的文本、以新一轮对话形式喂给持有文件与终端权限的 agent"的守护进程——若设防不当即是 prompt-injection-to-RCE 放大器。信任锚必须自建:本扩展没有 mcp.json 那种"用户亲手配置了服务器"的天然锚,只有"用户亲手开放了会话"这一动作可作锚点。
sendRequest 产生的请求在会话历史中构建为 role: user(chatServiceImpl 的 history 映射无 isSystemInitiated 分支,源码实证),模型收到的结构与用户手打的消息完全相同。isSystemInitiated/systemInitiatedLabel 是纯 UI 层字段(chatListRenderer 消费),模型看不到;[<label> notification: ...] 文本前缀只是软约定。官方 run_in_terminal 通知能安全工作,是因为 VS Code 系统提示词教过模型解释该格式;本扩展的注入没有这层教育。因此安全完全依赖事前准入门与事后审计,不存在任何事中防线:一旦不可信进程跨过门注入指令性文本,模型可能当成正经用户请求执行。这也是准入门必须做到会话级、而非"反正只是通知"心态的根本原因。
五道门(会话白名单为第零道)
- 会话级白名单(信任锚):只有用户在 VS Code 内显式开放的会话才存在管道;未开放会话在协议层不可达、不可见。每个开放是一次用户亲手授权动作(等价于 mcp.json 手动添加一条服务器的信任强度)
- 传输鉴权:每会话独立 named pipe + 独立 token(默认 32 字节 CSPRNG;凭证纯内存零落盘,磁盘上无 token 文件可窃、无发现文件可供自动枚举);不用 TCP localhost,免疫浏览器侧探测与 DNS rebinding;管道名即使泄露,非空 token 通道无 token 也接不进。⚠️ token 安全等级由用户掌握:用户可将 token 设为空(免鉴权)——此时本道门对该会话失效,任何能连上管道的同用户进程都能 initialize 成功(inject 仍受门2 client 审批拦截),是用户在 UI 上知情选择的结果
- 首见审批:新 client identity 在已开放管道上首次 inject 仍弹窗批准(Allow in this session / Always / Not now / Never 四按钮语义抄 McpSamplingService.allowButtons),决定持久化——管道白名单管"哪个会话可被注入",client 审批管"哪个进程可向该会话注入"
- 窄协议:仅
initialize + inject 两个动词;无发现、无枚举、无控制;CDP/Runtime.evaluate/钩子映射表均为内部实现细节,协议面绝不透传;文本长度/label 长度/频率均设硬上限
- 高危模式单独设卡:
mode:'steering'(打断飞行中回合)不随 always 授权自动放行,每次或按 client 单独审批;全部注入写审计日志(时间、client、管道/会话、label、文本摘要;日志永不记 token 值);吊销/重命名/轮换即时生效(销毁或重建管道、作废旧凭证、清空对应通道 client 授权)
已知边界与风险
- agent host 会话不保证:官方 skill buttons 经同一 chatService.sendRequest 路径向 agent host 会话投递成功,说明该路由对 host 会话可达(
_isServerManagedQueue 时队列转 host 侧管理)——此为源码推断,待实测,失败则该场景在开放选择器里直接标记 not-supported
- bundle 锚点漂移:断点锚点依赖 minified bundle 特征(主动锚
(?<![.\w])invokeFunction\s*\([^)]*\)\s*\{ 定位 InstantiationService 类自身方法定义,下划线字段名 _throwIfDisposed/_services/_parent 是定位佐证,负向后顾排除两百多个 .invokeFunction( 调用点;被动锚 async\s+acquireOrLoadSession\s*\(、async\s+sendRequest\s*\( 均实测全局唯一;注意 trace 文案锚点可能命中同 bundle 里的其它辅助函数,必须用定义式正则);VS Code 月度更新可能破坏;对策是锚点集中管理 + 帧形状校验重试 + DI 容器内按形状(而非装饰器键名)匹配 chatService + 找不到锚点时状态栏示警而非崩溃
- 磁盘枚举的范围:索引只收录
initialLocation==='panel'、非空、非外部的会话(与官方 getHistorySessionItems 同源同条件),故 inline chat / 编辑器内会话 / agent host 会话不在候选列表(与非目标一致);全新工作区尚无任何非空会话时开放选择器会提示先新建会话发一条消息。索引写入有 flush 延迟:刚发出的首条消息可能尚未落盘,桥就绪时会经实时枚举补入;磁盘扫描属版本逆向(存储键与编码规则已双重实证,但 VS Code 未来改版需同步更新 src/diskSessionEnum.ts 的键名/编码常量)
- CDP 端口全应用共享:多个 VS Code 窗口共用一个 remote-debugging-port(两窗口时首取 page target 会附着到错误窗口),必须按 target 标题含本窗口 workspace 名复核后才附着
- argv.json 值类型陷阱:
remote-debugging-port 必须是字符串值,JSON 数字被 main.js 静默忽略;SUPPORTED_ELECTRON_SWITCHES 的字符串分支要求 typeof c=="string"&&c
- 自动重启依赖 WMI/schtasks:spawn 的子进程逃不出 Electron Job Object(KILL_ON_JOB_CLOSE 连坐,表现为「关了没拉起」),必须经 WMI Win32_Process.Create(新进程父级 WmiPrvSE,在 Job 外)派发重启助手;两者都不可用时回退手动指引、绝不退出应用
- 开放会话被用户删除/回滚:磁盘枚举中消失时自动吊销对应管道开放,防止悬挂凭证;实现为周期轮询(带两道防误删护栏:空枚举结果跳过、连续两轮都消失才吊销)。不用事件监听是因为 renderer 侧 onDidChange 钩子接入成本高于轮询收益
- 凭证不落盘的代价:扩展宿主进程崩溃/窗口 reload/重启后全部开放状态与凭证即蒸发,上游需重新获得凭证重接;这是有意取舍(磁盘残留泄密面 vs 重新交接成本,选择前者归零)。同理,凭证卡片弹出后若未复制就关闭,只能从「管理开放会话 → 查看/复制凭证」补看,无处可找文件副本
- VS Code 1.137+ 官方收编风险(也是机遇):agent-queued messages / Automations 表明微软正在填充同一语义空间;一旦官方路径开放给 MCP/第三方,注入后端整体替换,本协议面不变——这是"可收编"设计目标的由来
未来方向
- 加固:renderer 侧会话删除事件监听替代轮询联动、断线重放缓冲、锚点失效的自动重装与通知、协议版本协商、steering 模式实测、开放会话跨窗口重启恢复(重新开放需用户再次确认,不自动恢复凭证)、
startNewLocalSession 被动锚点补充(新建本地会话绕开 acquireOrLoadSession,现为锚点盲区);磁盘扫描属版本逆向,升级后需回归验证存储键/编码常量
- 注入消息携带上下文:
inject 扩展 attachedContext 参数,让消息可附文件/目录引用、图像(base64→Uint8Array)、粘贴文本、终端输出等附件(官方 sendRequest 的 options.attachedContext 直通 variableData,UI 自动渲染附件 chip;纯附件+空文本亦合法)
- 退役条件:官方提供等价带外注入 API 之时,本扩展降级为薄适配层或归档