首页
/ Qwen Code 的 VS Code 扩展如何接入 qwen serve:IDE Daemon Adapter 设计与 DaemonIdeConnection 实现

Qwen Code 的 VS Code 扩展如何接入 qwen serve:IDE Daemon Adapter 设计与 DaemonIdeConnection 实现

2026-09-12 13:05:31作者:羿妍玫Ivan

在 Qwen Code(qwen-code)中,VS Code companion 扩展默认通过 ACP stdio 子进程(qwen --acp)与 Agent 通信。为了让 IDE 也能消费守护进程模式(Mode B)——即直接连接本地运行的 qwen serve HTTP/SSE 服务——仓库中定义了 IDE Daemon Adapter 的完整设计契约,并已在扩展侧落地了可本地验证的 DaemonIdeConnection 适配器骨架。读完本篇,你将掌握:扩展宿主(extension host)如何持有 daemon URL、token、session id 与 SSE 重放状态,并只把"净化后"的应用事件转发给 webview;daemon 事件到既有 webview 回调的映射契约;以及该适配器在循环地址校验、权限应答、断线恢复上的源码级实现细节。

设计目标:扩展宿主独占 daemon 连接,webview 不直连

设计文档 ide.md 开宗明义地提出了目标:让 VS Code companion 扩展通过 DaemonSessionClient 从扩展宿主连接到 qwen serve,完成 Mode B 的 dogfood(内部自测)。其中最关键的一条架构约束是:webview 绝不能直接调用 daemon。扩展宿主独占 daemon 的 URL、token、session id 和 SSE 重放(replay)状态,只把经过净化的应用事件转发给 webview。这样做的直接收益是 bearer token 永远不会出现在 webview 的 JavaScript 运行环境里——这一条在文档的 Merge Safety 一节中被再次强调为硬性合并安全条件("Daemon token never crosses into webview JavaScript")。

这一职责切分在实际代码中得到了印证。daemonIdeConnection.ts 的文件头注释说明它是一个 "Daemon-backed IDE connection spike",其目标是"镜像 ACP 进程连接的外在形状(shape),同时把本地子进程替换为 qwen serve 会话"。文件内注释也明确:它只做传输层(transport-only),真正的 webview 桥接逻辑放在 ChatWebviewViewProvider.ts 等 provider 里,provider 订阅连接类的回调并转换为 webview 的 postMessage 调用。

入口:实验性开关与连接参数

文档给出了两类入口配置。第一类是 VS Code 设置(文档中的提案形态):

{
  "qwen-code.experimentalDaemon.enabled": true,
  "qwen-code.experimentalDaemon.url": "http://127.0.0.1:4170",
  "qwen-code.experimentalDaemon.token": ""
}

第二类是本地 dogfood 用的环境变量回退:

QWEN_IDE_DAEMON_URL=http://127.0.0.1:4170 code .

需要注意当前仓库的实际落地状态:DaemonIdeConnection 已经实现并通过单测验证,但尚未接线到默认的 QwenAgentManager 路径,因此现有 VS Code 行为仍然是 ACP 子进程模式。从源码结构看,上述 experimentalDaemon.* 设置项与 QWEN_IDE_DAEMON_URL 环境变量目前仍是设计文档中的提案形态,尚未在扩展代码中检索到对应的读取实现;当前扩展侧可编程入口是 DaemonIdeConnection.connect() 的选项对象,包含 baseUrltokenworkspaceCwdmodelServiceIdlastEventId 五个参数(外加可注入的 sessionFactory,用于测试时替换真实工厂)。

daemon 侧默认监听 http://127.0.0.1:4170,这正对应文档示例中的 URL。关于 daemon 的启动与安全模型,可参考用户文档 qwen-serve.md:默认的 loopback 部署是"trusted loopback mode",本地调用方无需 bearer 即可访问全部 API(包括以 daemon 用户执行代码),在共享主机上应使用 --require-auth 配合 QWEN_SERVER_TOKEN

最小连接流程与 SDK 分层

文档定义的 8 步最小流程是:

  1. 扩展宿主创建 DaemonClient
  2. 拉取 /capabilities 并校验工作区兼容性;
  3. 通过 DaemonSessionClient.createOrAttach() 创建或附着会话;
  4. 在扩展宿主中订阅 session.events()
  5. 把 daemon 事件翻译成既有的 webview 消息;
  6. 通过 session.prompt() 发送用户提示;
  7. 把取消 / 切换模型路由到 session.cancel()session.setModel()
  8. 把权限决定路由到 session.respondToPermission()

这条流程的 SDK 支撑在 packages/sdk-typescript 中。DaemonClient 镜像原始 HTTP API,每个方法都需要显式传入 sessionIdDaemonSessionClient 则是面向适配器的上层——它的类注释明确写道,它是 "adapter-facing layer for TUI, channel, IDE, and web backends":绑定一个 daemon 会话、转发 Stage 1 路由、并保存 SSE 重放状态,同时刻意不解释事件载荷(类型化事件归约属于协议 schema 层)。

createOrAttach() 中有一个值得注意的细节:当会话是新创建的(session.attached === false),或者附着时携带了 modelServiceId / approvalMode 变更时,首次事件订阅会用 Last-Event-ID: 0 从 daemon 重放环(bounded ring)的起点开始消费。源码注释解释了原因:新会话创建窗口内子进程的 MCP 发现会同步执行,期间触发的 mcp_budget_warning 等推送事件会先落入 per-session 事件环;如果首个订阅不从这个窗口回溯,这些事件就永远到不了实时流。这也正是文档验证计划中"SSE reconnect uses tracked Last-Event-ID"一条的底层机制。

DaemonIdeConnection:连接、事件泵与断线恢复

扩展侧的核心类 DaemonIdeConnectiondaemonIdeConnection.ts)对外暴露 connect()disconnect()sendPrompt()cancelSession()setModel() 五个方法,以及五个可赋值回调:onSessionUpdateonPermissionRequestonAskUserQuestiononEndTurnonDisconnected。它的设计要点包括:

循环地址硬校验。 connect() 内部先调用 validateDaemonBaseUrl()(见 L108-L122),强制要求:协议必须是 http:https:;URL 中不得内嵌用户名/密码;主机名必须是 loopback(localhost::1[::1]127.0.0.0/8,包括 ::ffff: 映射形式)。这是一条客户端侧硬约束,与 daemon 自身的 hostAllowlist 是两回事——即使运营者配置了远端 daemon,IDE 伴生扩展也绝不连接。其威胁模型假设是:工作区与 daemon 共享同一台主机,包括文件系统信任等前提。若坚持要连远端,正确姿势是 SSH 端口转发或本地代理。

连接串行化。 connect() 用内部 connectPromise 队列保证并发调用串行化:第二次调用会先等待(并容忍)第一次的结果再执行,避免"用户在手握中反复开关面板"导致的竞态,最终收敛到单一确定状态。

SSE 事件泵。 connectInternal() 建立会话后启动 pumpEvents(),对 session.events({ signal, lastEventId: resumeId, resume: true })for await 消费。resumeId 取"IDE 自己的权威重放游标 lastSeenEventId,否则取 SDK 侧游标"。每个事件处理后推进游标;若处理器抛错,游标推进(shouldAdvanceLastSeenEventId 保持 false),使该事件在下次重连重放时能被重新处理——这在权限应答失败的场景下尤为关键。事件泵用 pumpGeneration 代数计数器防止旧泵在 finally 中误清掉新一代泵的状态。

终止事件的三路收敛。 会话终止有三种来源:daemon 推送 session_died(且载荷 sessionId 必须与当前会话匹配,否则忽略)、SSE 流自然结束(触发 onDisconnected(null, 'stream_ended'))、流异常(触发 onDisconnected(null, 'daemon_error'));手动 disconnect() 则触发 'disconnected'。收到 session_died 时,扩展宿主可携带上一次的 lastEventId 重新 connect() 完成恢复。

事件映射契约:daemon 事件到 IDE 回调

文档给出了如下事件映射契约表:

Daemon 事件 IDE 处理
session_update / agent_message_chunk 既有的助手流式回调
session_update / agent_thought_chunk 既有的思考流式回调
session_update / tool_call 既有的工具调用更新回调
permission_request 既有的审批 UI 回调
permission_resolved 关闭/更新审批 UI
model_switched 尽可能映射到既有模型状态回调
session_died 断连 UI + 重连入口

并规定:未知事件必须被忽略,或仅记录为 debug 元数据

实际实现中的 handleEvent()L373-L394)正是按这个契约落地的:session_update 转发给 onSessionUpdatepermission_request 进入权限应答流程;session_died 走断连处理;其余事件类型一律落 debug 日志后放行。session_updatedata 载荷是 ACP 的 SessionNotification,其中 update.sessionUpdate 字段(agent_message_chunkagent_thought_chunktool_call 等)由上层的 webview provider 进一步翻译成消息面板里的助手流、思考块与工具卡片——与 ACP 子进程路径喂给 webview 的回调保持同构,这正是文档"两条路径应尽可能喂给同一组高层 webview 回调"要求的落点。对于暂时无法忠实映射的事件,设计原则是显式暴露"不支持状态"的告警,而不是假装等价。

onEndTurn 有一个容易误读的点:它不是 SSE 分发产生的。sendPrompt() 会等待 daemon 的 HTTP prompt 响应,成功后以 response.stopReason 调用 onEndTurn;非 Abort 的异常路径则调用 onEndTurn('error') 后再抛出。

权限应答:从 quick-pick 到 respondToPermission

权限请求的处理链比映射表更复杂。handlePermissionRequest() 收到 permission_request 后:

  1. 先做载荷形状校验(必须有字符串 requestId、对象型 toolCall、数组型 options),畸形数据记录告警并放行,不推进会出错;
  2. 根据 toolCall.kind 分流:若是 ask_user_questionrawInput.questions 为数组,走 onAskUserQuestion 回调,用户回答后把 answers 放进 RequestPermissionResponse 的顶层透传字段(daemon 的 HTTP 权限路由保留顶层透传字段,ACP 会话从该位置读取 answers);否则走普通 onPermissionRequest(在 VS Code 中表现为原生 quick-pick 对话框);
  3. 用户选择 cancel / reject / reject_* 类选项时,统一转换为 { outcome: { outcome: 'cancelled' } }
  4. 最终通过 session.respondToPermission(requestId, response) 回传 daemon;如果应答在回传前会话已切换、或 daemon 拒绝(返回 false),只记录告警,并且保留重放游标——注释明确说明这是刻意为之的:权限应答失败时让事件可被重放,而不是静默吞掉。

单测 daemonIdeConnection.test.tsEventQueue 假事件源 + 假会话工厂覆盖了这条链:连接后推送 session_update 验证转发、权限请求到 respondToPermission 的转发、session_died 的会话清空、以及 lastEventId 断点续传的游标行为。

与既有 ACP 连接的关系:兄弟路径,不是替换

文档用一张结构图说明了两条连接路径的关系:

QwenAgentManager
  current default -> AcpConnection -> qwen --acp child
  experimental    -> DaemonIdeConnection -> qwen serve HTTP/SSE

第一版实现引入的是兄弟连接路径,不替换 AcpConnection。两条路径应尽可能汇入同一组高层 webview 回调;当前 daemon 路径尚未接入默认 QwenAgentManager,现有 VS Code 行为保持 ACP 子进程模式不变。

文档同时列出了明确的非目标(Explicit Non-Goals),值得完整保留,因为它划清了这条实验路径的边界:

  • 不默认迁移离开 AcpConnection
  • 不允许 webview 到 daemon 的直连传输;
  • 在文件服务边界落地之前,不支持经 IDE 的 daemon 侧文件 CRUD;
  • 暂不提供反向 RPC(编辑器/浏览器/剪贴板);
  • 不做完整的远程控制集成。

合并安全(Merge Safety)的四条纪律是:默认关闭(受设置/环境变量控制)、纯增量的兄弟连接路径、既有 ACP 子进程路径不变、daemon token 绝不进入 webview JavaScript。

运行时局部性:daemon 在哪里,资源就在哪里

文档特别要求扩展必须让用户看得见 daemon 的局部性,这四点要如实呈现:

  • 工作区/文件是 daemon 主机上的路径;
  • MCP 服务器运行在 daemon 主机上;
  • skills 从 daemon 的文件系统加载;
  • 模型提供方凭据在 daemon 进程的环境中解析。

反过来,不能暗示本地的 VS Code 扩展、本地浏览器 profile、本地 localhost 服务、本地 SSH/kube 凭据会自动被 daemon 看到。这与 workspaceCwd 的语义直接相关:POST /session 时传入的 workspaceCwd 必须与 daemon 的主工作区或已注册的多工作区之一匹配,否则 daemon 返回 400 workspace_mismatch,扩展侧应把它呈现为清晰的配置错误而不是盲目重试。

验证计划与默认迁移前的阻塞项

文档的验证计划与当前测试布局一一对应:

  • 单测 daemon 会话工厂连接与 SSE 事件消费 —— 已见 daemonIdeConnection.test.tsDaemonSessionClient.test.ts
  • 单测 daemon 事件到既有扩展宿主回调的映射;
  • 单测 prompt / cancel / 切换模型 / 权限应答的转发;
  • 设置/环境变量在功能旗标接线时的解析单测(对应前文所述的待落地部分);
  • 针对本地扩展宿主 + qwen serve 的冒烟测试:提示能流式进入聊天、取消可用、权限 UI 能解决请求、SSE 重连使用受跟踪的 Last-Event-ID

而在默认迁移之前,文档列出六个阻塞项(Blockers Before Default Migration):类型化 daemon 事件 schema、daemon 端加盖的客户端身份、会话级权限路由、只读运行时诊断、FileSystemService 边界与安全文件读取路由、面向 CLI/TUI 对等的输出汇聚层重构。

延伸阅读:同一适配体系的其他端

IDE 适配器只是 Qwen Code "daemon 客户端适配层"矩阵中的一格,同目录还有 web-shell.md(浏览器端消费 @qwen-code/sdk/daemonnormalizeDaemonEvent() / createDaemonTranscriptStore(),并区分同源本地 POC、BFF 远端部署、本地浏览器跨域三种部署形态)、tui.mdchannel-web.md。更完整的实现参考在 16-vscode-ide-adapter.md,其中给出了 DaemonIdeConnection 的公共接口签名、事件分发表、以及初始化连接 / quick-pick 权限 / 断线恢复三张时序图;daemon 本身的 API 与部署说明见 qwen-serve.mdsdk-daemon-client

小结

IDE Daemon Adapter 的核心可以浓缩为三句话:连接归宿主(扩展宿主独占 URL/token/session/SSE 状态,webview 只见净化事件)、传输可替换DaemonIdeConnection 以与 ACP 子进程同形的回调表面,把 HTTP/SSE 包装成既有的 webview 消息流)、演进不破坏(默认关闭、兄弟路径、loopback 硬校验、事件映射契约 + 未知事件静默忽略)。当前它处于"已实现、已单测、未接线默认路径"的实验状态,等待文档所列的阻塞项(类型化事件 schema、会话级权限路由、文件服务边界等)落地后,才谈得上从 ACP 子进程路径的默认迁移。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
34
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.16 K
2.78 K
docsdocs
暂无描述
Markdown
904
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
936
1.86 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
862
1.36 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
535
606
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.38 K
1.47 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
4.02 K
1.03 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
549
400
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.07 K
538