docs/developers/daemon/16-vscode-ide-adapter.md
packages/vscode-ide-companion/src/services/daemonIdeConnection.ts 是 VSCode 扩展的 daemon 适配器。它让 IDE companion 通过 HTTP + SSE 跟在跑的 qwen serve daemon 通话,而不是启动一个进程内 qwen --acp stdio 子进程(老 AcpConnectionState 路径)。它是 VSCode 宿主侧 14-cli-tui-adapter.md 的同级传输等价物。
IDE 的 chat webview 通过本适配器消费 daemon 事件;权限请求以 VSCode 原生 quick-pick 弹窗呈现。
baseUrl 构造 DaemonClient + DaemonSessionClient。onSessionUpdate、onPermissionRequest、onAskUserQuestion、onEndTurn、onDisconnected)。connect(options) 时强制 loopback only(IDE 应当只与同主机 daemon 通话)。postMessage,chat 面板保持同步。connect() 串行化,避免宿主快速 double-call 时 race。class DaemonIdeConnection {
connect(options: DaemonIdeConnectionOptions): Promise<void>;
disconnect(): Promise<void>;
sendPrompt(prompt: string | ContentBlock[]): Promise<DaemonIdePromptResult>;
cancelSession(): Promise<void>;
setModel(modelId: string): Promise<DaemonIdeSetModelResult>;
onSessionUpdate: (data: SessionNotification) => void;
onPermissionRequest: (
data: RequestPermissionRequest,
) => Promise<{ optionId?: string }>;
onAskUserQuestion: (data: AskUserQuestionRequest) => Promise<{
optionId: string;
answers?: Record<string, string>;
}>;
onEndTurn: (reason?: string) => void;
onDisconnected: (code: number | null, signal: string | null) => void;
}
interface DaemonIdeConnectionOptions {
baseUrl: string; // 必须 loopback(127.0.0.1 / localhost / [::1])
token?: string;
workspaceCwd?: string;
modelServiceId?: string;
lastEventId?: number;
sessionFactory?: DaemonIdeSessionFactory;
}
connect(options) 时(daemonIdeConnection.ts 的 connectInternal()):
const baseUrl = validateDaemonBaseUrl(options.baseUrl);
这是 客户端硬约束,与 daemon 自己的 hostAllowlist(见 12-auth-security.md)不同。IDE companion 永远不连远程 daemon —— 即便 operator 配了远程。理由:VSCode 的威胁模型假设 workspace 与 daemon 共享同一宿主(文件系统信任等)。
createSdkDaemonSessionFactory()daemonIdeConnection.ts 的 createSdkDaemonSessionFactory():从 @qwen-code/sdk 构造 DaemonClient 并调 DaemonSessionClient.createOrAttach()。connection 类持有工厂而不是直接实例化,方便测试注入 fake。
connection 跑一个 SSE 消费者(for await over session.events()),按 type 路由:
| daemon event / source | IDE 回调 / 动作 |
|---|---|
session_update | onSessionUpdate |
普通 permission_request | onPermissionRequest,随后 respondToPermission() |
permission_request 且 toolCall.kind === 'ask_user_question'、rawInput.questions 是数组 | onAskUserQuestion,随后把 answers 透传给 daemon |
session_died,且 payload 的 sessionId 匹配当前 session | onDisconnected(null, reason) |
SSE 自然结束 / stream 失败 / 手动 disconnect() | onDisconnected(null, 'stream_ended' / 'daemon_error' / 'disconnected') |
| 其他 daemon event | debug 级日志,当前不触发 IDE 回调 |
onEndTurn 不是 SSE 事件分发结果;sendPrompt() 等待 daemon HTTP prompt 响应后用 response.stopReason 调它,非 abort 异常路径调 onEndTurn('error')。
connection 类只做传输。真正的 VSCode 集成住在 packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts 等。Provider 订阅 connection 的回调并翻成 webview 的 postMessage。webview 自身用 packages/webui/ 组件库渲染 —— 见 01-architecture.md 的适配器矩阵。
connect() 内部用队列,宿主快速 double-call(用户在握手中打开 panel 两次)不会 race。第二次 await 第一次;connection 最终落在一个确定状态。
sequenceDiagram
autonumber
participant H as VSCode host
participant C as DaemonIdeConnection
participant F as createSdkDaemonSessionFactory
participant SDK as DaemonSessionClient
participant D as Daemon
H->>C: new DaemonIdeConnection()
H->>C: connect({baseUrl, token, workspaceCwd, lastEventId})
C->>C: validate loopback host
C->>F: factory({baseUrl, token, workspaceCwd, lastEventId})
F->>SDK: DaemonClient + DaemonSessionClient.createOrAttach
SDK->>D: POST /session
D-->>SDK: DaemonSession
F-->>C: DaemonSessionClient
C->>SDK: session.events()
par event pump
SDK->>D: GET /session/:id/events
loop per frame
D-->>SDK: DaemonEvent
SDK-->>C: DaemonEvent
C->>C: dispatch by type
C->>H: onSessionUpdate / onPermissionRequest / ...
end
end
sequenceDiagram
autonumber
participant D as Daemon
participant SDK as DaemonSessionClient
participant C as DaemonIdeConnection
participant P as Webview/QuickPick provider
participant U as User
D-->>SDK: permission_request event
SDK-->>C: DaemonEvent
C-->>P: onPermissionRequest(req)
P->>U: vscode.window.showQuickPick(options)
U->>P: choose option
P->>C: respondToPermission({optionId})
C->>SDK: session.respondToPermission(...)
SDK->>D: POST /permission/:requestId
D-->>SDK: 200 (or 409 already_resolved)
sequenceDiagram
autonumber
participant D as Daemon
participant SDK as DaemonSessionClient
participant C as DaemonIdeConnection
participant H as Host
D-->>SDK: session_died (or other terminal)
SDK-->>C: DaemonEvent
C->>C: shut down pump
C-->>H: onDisconnected(null, reason)
H->>C: connect({baseUrl, token, workspaceCwd, lastEventId})
connect(options)。connect() 通过内部队列幂等;二次调串行化。disconnect() 通过 AbortController 中止 SSE iterator 并清回调。lastEventId 在 disconnect 时从 SDK DaemonSessionClient 抓出来,下次 connect() 可再传以重放。packages/sdk-typescript/src/daemon/ —— DaemonClient、DaemonSessionClient(真正的传输)。vscode.*)—— 宿主 API、quick-pick、webview。packages/webui/src/adapters/ACPAdapter.ts —— webview 通过 postMessage 拿到 ACP 形态消息后渲染。| 旋钮 | 位置 | 效果 |
|---|---|---|
baseUrl | connect(options) | daemon URL;必须 loopback |
token | connect(options) | Bearer token(通过 SDK 盖) |
workspaceCwd | connect(options) | POST /session 用;需与 daemon 绑定的 workspace 一致 |
modelServiceId | connect(options) / setModel() | 初始 model |
lastEventId | connect(options) | 恢复游标(一般从宿主状态恢复) |
VSCode 设置 qwen.ide.daemonUrl(或等价键) | 工作区设置 | operator 配的 daemon URL |
connect(options) 时硬拒。想让 IDE 指向远程 daemon 的 operator 需要 SSH port-forward / 本地代理;适配器永远不连非 loopback URL。AcpConnectionState 路径仍是 IDE companion 的主路径(stdio child)。本适配器是 Mode-B 迁移的同级传输;迁移阻塞项与计划中的 BridgeFileSystem 一致工作见 ../daemon-client-adapters/ide.md。DaemonIdeConnection。workspaceCwd 与 daemon 绑定不一致 → 400 workspace_mismatch,应当作清晰的配置错误暴露,不要重试。packages/vscode-ide-companion/src/services/daemonIdeConnection.tspackages/vscode-ide-companion/src/services/daemonIdeConnection.ts(createSdkDaemonSessionFactory)packages/vscode-ide-companion/src/types/connectionTypes.ts(老 AcpConnectionState)packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts(webview bridge)packages/webui/src/adapters/ACPAdapter.ts(webview ACP-message 适配器)../daemon-client-adapters/ide.md13-sdk-daemon-client.md