> VSCode WebView postMessage 如何与 qwen codex gemini 之类的 cli 进行交互呢 ⏺ 非常好的问题!这是整个方案的核心技术细节。让我详细解释 VSCode WebView 如何通过 postMessage 与 CLI 进程交互。 🔄 完整的通信链路 ┌─────────────────────────────────────────────────────────────────┐ │ VSCode 窗口 │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ WebView (React UI) │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ 用户输入: "帮我分析这个文件" │ │ │ │ │ │ [Send Button] │ │ │ │ │ └──────────────┬──────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ ▼ (1) webview.postMessage() │ │ │ └─────────────────┼──────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────▼──────────────────────────────────────────┐ │ │ │ Extension Host (Node.js) │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ │ │ (2) onDidReceiveMessage() 接收 │ │ │ │ │ │ ↓ │ │ │ │ │ │ (3) AcpAgentManager.sendMessage() │ │ │ │ │ │ ↓ │ │ │ │ │ │ (4) spawn('qwen', ['--experimental-acp']) │ │ │ │ │ └──────────────┬───────────────────────────────────────┘ │ │ │ └─────────────────┼──────────────────────────────────────────┘ │ └────────────────────┼─────────────────────────────────────────────┘ │ ▼ (5) JSON-RPC via stdio/WebSocket ┌─────────────────────────────┐ │ Qwen CLI 独立进程 │ │ (--experimental-acp) │ │ │ │ 接收: {"method": "send"} │ │ 执行: AI 推理 + 工具调用 │ │ 返回: {"result": "..."} │ └─────────────┬───────────────┘ │ ▼ (6) 响应返回 ┌─────────────────────────────┐ │ Extension Host │ │ 解析响应 → 格式化 │ └─────────────┬───────────────┘ │ ▼ (7) webview.postMessage() ┌─────────────────────────────┐ │ WebView │ │ 显示 AI 的回复 │ └─────────────────────────────┘ --- 💻 详细代码实现 第 1 步:WebView 发送消息到 Extension // webview/components/ChatInput.tsx import { useState } from 'react'; const ChatInput = () => { const [input, setInput] = useState(''); // 获取 VSCode API const vscode = acquireVsCodeApi(); const handleSend = () => { // (1) 通过 postMessage 发送到 Extension Host vscode.postMessage({ type: 'send_message', // 消息类型 agentType: 'qwen', // 使用哪个 Agent content: input, // 用户输入 files: [], // 附带文件 timestamp: Date.now() }); setInput(''); }; return (
setInput(e.target.value)} />
); }; --- 第 2 步:Extension Host 接收消息 // extension.ts import \* as vscode from 'vscode'; import { spawn, ChildProcess } from 'child_process'; class AionUiViewProvider implements vscode.WebviewViewProvider { private webviewView?: vscode.WebviewView; private qwenProcess?: ChildProcess; // Qwen CLI 进程 private codexProcess?: ChildProcess; // Codex CLI 进程 private activeAgent: string = 'qwen'; resolveWebviewView(webviewView: vscode.WebviewView) { this.webviewView = webviewView; webviewView.webview.options = { enableScripts: true, localResourceRoots: [this.extensionUri] }; // (2) 监听来自 WebView 的消息 webviewView.webview.onDidReceiveMessage(async (message) => { console.log('[Extension] Received from WebView:', message); switch (message.type) { case 'send_message': await this.handleSendMessage(message); break; case 'switch_agent': await this.handleSwitchAgent(message.agentType); break; case 'approve_permission': await this.handleApprovePermission(message); break; } }); // 加载 HTML webviewView.webview.html = this.getHtmlForWebview(webviewView.webview); } // (3) 处理发送消息逻辑 private async handleSendMessage(message: any) { const { agentType, content, files } = message; // 确保 CLI 进程已启动 if (!this.qwenProcess && agentType === 'qwen') { await this.startQwenCLI(); } // 发送到对应的 CLI await this.sendToCLI(agentType, { method: 'session/send_prompt', params: { sessionId: this.currentSessionId, content: content } }); } } --- 第 3 步:启动 CLI 进程并建立 ACP 连接 // extension.ts (continued) class AionUiViewProvider { private acpConnection?: AcpConnection; private currentSessionId?: string; // (4) 启动 Qwen CLI 进程 private async startQwenCLI(): Promise { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const config = vscode.workspace.getConfiguration('aionui'); const qwenPath = config.get('qwen.cliPath', 'npx @qwen-code/qwen-code'); // 解析命令和参数 let command: string; let args: string[]; if (qwenPath.startsWith('npx ')) { command = process.platform === 'win32' ? 'npx.cmd' : 'npx'; args = [...qwenPath.split(' ').slice(1), '--experimental-acp']; } else { command = qwenPath; args = ['--experimental-acp']; } console.log(`[Extension] Starting Qwen CLI: ${command} ${args.join(' ')}`); // 启动进程 this.qwenProcess = spawn(command, args, { cwd: workspaceRoot, stdio: ['pipe', 'pipe', 'pipe'], // stdin, stdout, stderr env: { ...process.env, // 传递必要的环境变量 } }); // 监听进程输出 this.qwenProcess.stdout?.on('data', (data) => { console.log('[Qwen CLI] stdout:', data.toString()); this.handleCLIOutput(data); }); this.qwenProcess.stderr?.on('data', (data) => { console.error('[Qwen CLI] stderr:', data.toString()); }); this.qwenProcess.on('error', (error) => { console.error('[Qwen CLI] Process error:', error); this.notifyWebView({ type: 'error', message: `Failed to start Qwen CLI: ${error.message}` }); }); this.qwenProcess.on('exit', (code) => { console.log('[Qwen CLI] Process exited with code:', code); }); // 创建 ACP 连接 this.acpConnection = new AcpConnection(this.qwenProcess); // 初始化 ACP 协议 await this.initializeACP(); // 创建会话 await this.createSession(workspaceRoot); } // (5) 初始化 ACP 协议(JSON-RPC 2.0) private async initializeACP(): Promise { console.log('[Extension] Initializing ACP protocol...'); // 发送 initialize 请求 const response = await this.acpConnection!.sendRequest({ jsonrpc: '2.0', id: this.nextRequestId++, method: 'initialize', params: { protocolVersion: '0.1.0', capabilities: { /* 客户端能力 */ } } }); console.log('[Extension] ACP initialized:', response); } // (6) 创建会话 private async createSession(workingDir?: string): Promise { console.log('[Extension] Creating new session...'); const response = await this.acpConnection!.sendRequest({ jsonrpc: '2.0', id: this.nextRequestId++, method: 'session/new_session', params: { workingDirectory: workingDir || process.cwd() } }); this.currentSessionId = response.result.sessionId; console.log('[Extension] Session created:', this.currentSessionId); // 通知 WebView 会话已就绪 this.notifyWebView({ type: 'session_ready', sessionId: this.currentSessionId }); } } --- 第 4 步:ACP 连接实现(JSON-RPC 2.0) // backend/acp/AcpConnection.ts import { ChildProcess } from 'child_process'; import { EventEmitter } from 'events'; interface AcpRequest { jsonrpc: '2.0'; id: number; method: string; params?: any; } interface AcpResponse { jsonrpc: '2.0'; id: number; result?: any; error?: { code: number; message: string; }; } export class AcpConnection extends EventEmitter { private process: ChildProcess; private pendingRequests = new Map void; reject: (error: any) => void; }>(); private buffer = ''; constructor(process: ChildProcess) { super(); this.process = process; // 监听 CLI 的输出 this.process.stdout?.on('data', (data: Buffer) => { this.handleData(data); }); } // 处理来自 CLI 的数据 private handleData(data: Buffer) { this.buffer += data.toString(); // 按行分割(JSON-RPC 消息通常是一行一个) const lines = this.buffer.split('\n'); this.buffer = lines.pop() || ''; // 保留最后一个不完整的行 for (const line of lines) { if (!line.trim()) continue; try { const message = JSON.parse(line); this.handleMessage(message); } catch (error) { console.error('[ACP] Failed to parse message:', line, error); } } } // 处理 JSON-RPC 消息 private handleMessage(message: any) { console.log('[ACP] Received:', message); if (message.id !== undefined) { // 这是一个响应 const pending = this.pendingRequests.get(message.id); if (pending) { this.pendingRequests.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message)); } else { pending.resolve(message); } } } else if (message.method) { // 这是一个通知(Notification) this.handleNotification(message); } } // 处理 CLI 发来的通知 private handleNotification(message: any) { switch (message.method) { case 'session/update': // AI 正在输出内容 this.emit('session_update', message.params); break; case 'permission/request': // AI 请求执行操作的权限 this.emit('permission_request', message.params); break; case 'session/end_turn': // AI 完成了一轮对话 this.emit('end_turn', message.params); break; } } // 发送 JSON-RPC 请求 sendRequest(request: AcpRequest): Promise { return new Promise((resolve, reject) => { // 保存待处理的请求 this.pendingRequests.set(request.id, { resolve, reject }); // 发送到 CLI 的 stdin const json = JSON.stringify(request) + '\n'; this.process.stdin?.write(json); console.log('[ACP] Sent request:', request); // 设置超时 setTimeout(() => { if (this.pendingRequests.has(request.id)) { this.pendingRequests.delete(request.id); reject(new Error('Request timeout')); } }, 30000); // 30 秒超时 }); } // 发送提示词 async sendPrompt(content: string): Promise { await this.sendRequest({ jsonrpc: '2.0', id: Date.now(), method: 'session/send_prompt', params: { sessionId: this.sessionId, content: content } }); } } --- 第 5 步:Extension 监听 CLI 的响应并转发给 WebView // extension.ts (continued) class AionUiViewProvider { private async sendToCLI(agentType: string, request: any) { // 监听 CLI 的各种事件 this.acpConnection!.on('session_update', (data) => { // (7) 将 AI 的响应发送回 WebView this.notifyWebView({ type: 'agent_message', data: data }); }); this.acpConnection!.on('permission_request', (data) => { // 将权限请求发送到 WebView this.notifyWebView({ type: 'permission_request', data: data }); }); this.acpConnection!.on('end_turn', () => { // AI 完成回复 this.notifyWebView({ type: 'finish' }); }); // 发送请求 await this.acpConnection!.sendRequest(request); } // 发送消息到 WebView private notifyWebView(message: any) { if (!this.webviewView) { console.warn('[Extension] WebView not ready, dropping message:', message); return; } console.log('[Extension] Sending to WebView:', message); // (7) 通过 postMessage 发送到 WebView this.webviewView.webview.postMessage(message); } // 处理权限批准 private async handleApprovePermission(message: any) { const { permissionId, approved } = message; // 发送权限响应到 CLI await this.acpConnection!.sendRequest({ jsonrpc: '2.0', id: Date.now(), method: 'permission/respond', params: { permissionId: permissionId, approved: approved } }); } } --- 第 6 步:WebView 接收并显示 AI 响应 // webview/index.tsx import { useEffect, useState } from 'react'; const ChatView = () => { const [messages, setMessages] = useState([]); const vscode = acquireVsCodeApi(); useEffect(() => { // (8) 监听来自 Extension 的消息 window.addEventListener('message', (event) => { const message = event.data; console.log('[WebView] Received from Extension:', message); switch (message.type) { case 'session_ready': console.log('Session ready:', message.sessionId); break; case 'agent_message': // 显示 AI 的回复 setMessages(prev => [...prev, { type: 'assistant', content: message.data.content }]); break; case 'permission_request': // 显示权限请求对话框 showPermissionDialog(message.data); break; case 'finish': // AI 完成回复 console.log('AI finished'); break; case 'error': // 显示错误 alert(message.message); break; } }); }, []); const showPermissionDialog = (data: any) => { const approved = confirm( `Qwen 想要执行: ${data.toolCall.title}\n是否允许?` ); // 发送批准/拒绝到 Extension vscode.postMessage({ type: 'approve_permission', permissionId: data.permissionId, approved: approved }); }; return (
); }; --- 📊 完整的消息流示意图 // 用户输入 "帮我分析代码" ┌─────────────────────────────────────────────────────────────┐ │ [WebView] 用户点击发送 │ └─────────────┬───────────────────────────────────────────────┘ │ ▼ vscode.postMessage({type: 'send_message', ...}) ┌─────────────────────────────────────────────────────────────┐ │ [Extension] onDidReceiveMessage() 接收 │ │ → handleSendMessage() │ │ → acpConnection.sendRequest() │ └─────────────┬───────────────────────────────────────────────┘ │ ▼ process.stdin.write(JSON-RPC 请求) ┌─────────────────────────────────────────────────────────────┐ │ [Qwen CLI] --experimental-acp │ │ stdin 接收: {"method": "session/send_prompt", ...} │ │ ↓ │ │ AI 处理: 分析代码、调用工具 │ │ ↓ │ │ stdout 输出: {"method": "session/update", ...} │ └─────────────┬───────────────────────────────────────────────┘ │ ▼ process.stdout.on('data', ...) ┌─────────────────────────────────────────────────────────────┐ │ [Extension] AcpConnection.handleData() │ │ → handleNotification('session_update') │ │ → emit('session_update', data) │ │ → notifyWebView({type: 'agent_message', ...}) │ └─────────────┬───────────────────────────────────────────────┘ │ ▼ webview.postMessage({type: 'agent_message', ...}) ┌─────────────────────────────────────────────────────────────┐ │ [WebView] window.addEventListener('message', ...) │ │ → setMessages([...messages, newMessage]) │ │ → React 重新渲染,显示 AI 回复 │ └─────────────────────────────────────────────────────────────┘ --- 🎯 关键技术点总结 1. VSCode WebView ↔ Extension 通信 // WebView → Extension vscode.postMessage({ type: 'action', data: {...} }); // Extension → WebView webview.postMessage({ type: 'response', data: {...} }); 2. Extension ↔ CLI 通信 // Extension → CLI (stdin) process.stdin.write(JSON.stringify(request) + '\n'); // CLI → Extension (stdout) process.stdout.on('data', (data) => { const message = JSON.parse(data.toString()); // 处理消息 }); 3. JSON-RPC 2.0 协议 // 请求 {"jsonrpc": "2.0", "id": 1, "method": "send_prompt", "params": {...}} // 响应 {"jsonrpc": "2.0", "id": 1, "result": {...}} // 通知 {"jsonrpc": "2.0", "method": "session/update", "params": {...}} --- ✅ 最终效果 用户在 VSCode 中输入消息 ↓ WebView UI 收集输入 ↓ postMessage 发送到 Extension ↓ Extension 启动 Qwen CLI 进程 ↓ 通过 JSON-RPC 发送到 CLI ↓ Qwen 处理并返回结果 ↓ Extension 解析响应 ↓ postMessage 返回到 WebView ↓ UI 显示 AI 的回复 完全不需要中间服务器,直接在 VSCode 中完成所有通信! 需要我提供完整的可运行示例代码吗? > 这样的话 是不是就跟 acp 没有什么关系了? ⏺ 🤔 非常好的观察!但其实恰恰相反 - 这个方案的核心就是 ACP 协议! 让我澄清一个重要的概念误区: 🔑 关键理解:ACP 是什么? ACP (Agent Communication Protocol) 不是一个独立的服务或中间件,而是一个通信协议标准(基于 JSON-RPC 2.0)。 就像: - HTTP 是一个协议,不是软件 - WebSocket 是一个协议,不是服务 - ACP 也是一个协议,不是程序 --- 📋 对比说明 误解的架构(你可能以为的): VSCode Extension ↓ 需要连接到一个 "ACP 服务器" ❌ ↓ ACP 服务器再去调用 Qwen CLI ❌ 实际的架构(真实情况): VSCode Extension ↓ 直接使用 ACP 协议 ✅ ↓ 与 Qwen CLI 通信(Qwen CLI 支持 ACP 协议)✅ --- 🎯 ACP 在整个流程中的作用 让我用更清晰的方式展示 ACP 的角色: 完整的技术栈: ┌─────────────────────────────────────────────────────────┐ │ 应用层:VSCode WebView (React UI) │ │ - 用户界面 │ │ - 消息展示 │ └────────────────┬────────────────────────────────────────┘ │ postMessage (VSCode API) ┌────────────────▼────────────────────────────────────────┐ │ 桥接层:VSCode Extension Host │ │ - 接收 UI 事件 │ │ - 管理 CLI 进程生命周期 │ │ - 转换 UI 消息 → ACP 协议消息 │ └────────────────┬────────────────────────────────────────┘ │ │ 🔴 这里使用 ACP 协议! │ (JSON-RPC 2.0 via stdio) │ ┌────────────────▼────────────────────────────────────────┐ │ 协议层:ACP 协议 (JSON-RPC 2.0) │ │ - initialize │ │ - session/new_session │ │ - session/send_prompt │ │ - session/update (notification) │ │ - permission/request (notification) │ └────────────────┬────────────────────────────────────────┘ │ stdin/stdout (pipe) ┌────────────────▼────────────────────────────────────────┐ │ CLI 层:Qwen CLI (--experimental-acp) │ │ - 实现了 ACP 协议规范 │ │ - 接收 ACP 消息,执行对应操作 │ │ - 发送 ACP 通知回传结果 │ └────────────────┬────────────────────────────────────────┘ │ HTTPS ┌────────────────▼────────────────────────────────────────┐ │ AI 服务:Qwen API │ │ - 实际的 AI 模型 │ └─────────────────────────────────────────────────────────┘ --- 💡 ACP 协议的实际作用 没有 ACP 的情况(假设): // ❌ 如果没有 ACP 协议,你需要自己定义所有通信格式 // 启动 CLI const qwen = spawn('qwen'); // 自己发明一个消息格式? qwen.stdin.write('SEND: 帮我分析代码\n'); // ??? 什么格式? // 怎么接收响应? qwen.stdout.on('data', (data) => { // ??? 如何解析? // ??? 如何区分是 AI 回复还是错误信息? // ??? 如何处理权限请求? }); 有 ACP 协议的情况(实际): // ✅ 使用 ACP 协议,所有格式都是标准化的 // 启动支持 ACP 的 CLI const qwen = spawn('qwen', ['--experimental-acp']); // 使用标准的 JSON-RPC 2.0 格式 qwen.stdin.write(JSON.stringify({ jsonrpc: '2.0', // ✅ 标准协议版本 id: 1, // ✅ 请求 ID method: 'session/send_prompt', // ✅ 标准方法名 params: { sessionId: 'xxx', content: '帮我分析代码' } }) + '\n'); // 接收标准格式的响应 qwen.stdout.on('data', (data) => { const message = JSON.parse(data); // ✅ 标准的响应格式 if (message.id === 1) { // 这是对请求 1 的响应 console.log(message.result); } // ✅ 标准的通知格式 if (message.method === 'session/update') { // AI 正在输出内容 console.log(message.params.content); } // ✅ 标准的权限请求 if (message.method === 'permission/request') { // AI 需要权限 console.log(message.params.toolCall); } }); --- 📖 ACP 协议的具体内容 ACP 定义的标准方法: // 1. 初始化 { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "0.1.0", "capabilities": {} } } // 2. 创建会话 { "jsonrpc": "2.0", "id": 2, "method": "session/new_session", "params": { "workingDirectory": "/path/to/project" } } // 3. 发送提示词 { "jsonrpc": "2.0", "id": 3, "method": "session/send_prompt", "params": { "sessionId": "session-123", "content": "帮我分析代码" } } // 4. 会话更新(CLI → Extension 的通知) { "jsonrpc": "2.0", "method": "session/update", "params": { "sessionId": "session-123", "update": { "sessionUpdate": "agent_message_chunk", "content": { "type": "text", "text": "好的,我来分析..." } } } } // 5. 权限请求(CLI → Extension 的通知) { "jsonrpc": "2.0", "method": "permission/request", "params": { "sessionId": "session-123", "permissionId": "perm-456", "toolCall": { "toolCallId": "tool-789", "title": "Read file: config.json", "kind": "read" }, "options": [ {"optionId": "allow", "name": "Allow", "kind": "allow_once"}, {"optionId": "reject", "name": "Reject", "kind": "reject_once"} ] } } 这些都是 ACP 协议规范的一部分! --- 🔍 AionUi 中 ACP 的使用证据 让我展示 AionUi 代码中实际使用 ACP 的部分: 1. ACP 协议类型定义 (src/types/acpTypes.ts) // ACP JSON-RPC Protocol Types export const JSONRPC_VERSION = '2.0' as const; export interface AcpRequest { jsonrpc: typeof JSONRPC_VERSION; // ← ACP 协议标准 id: number; method: string; // ← ACP 定义的方法 params?: any; } export interface AcpNotification { jsonrpc: typeof JSONRPC_VERSION; // ← ACP 协议标准 method: string; // ← ACP 定义的通知类型 params?: any; } 2. ACP 连接实现 (src/agent/acp/AcpConnection.ts) // 启动 ACP CLI async connect(backend: AcpBackend, cliPath?: string, workspace?: string) { // 启动支持 ACP 的 CLI this.process = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] }); // 发送 ACP 初始化请求 const initResponse = await this.sendRequest({ jsonrpc: '2.0', // ← ACP 协议 id: this.nextId++, method: 'initialize', // ← ACP 方法 params: { /* ... */ } }); } // 创建 ACP 会话 async newSession(workingDirectory?: string) { const response = await this.sendRequest({ jsonrpc: '2.0', // ← ACP 协议 id: this.nextId++, method: 'session/new_session', // ← ACP 方法 params: { workingDirectory } }); } // 发送 ACP 提示词 async sendPrompt(content: string) { await this.sendRequest({ jsonrpc: '2.0', // ← ACP 协议 id: this.nextId++, method: 'session/send_prompt', // ← ACP 方法 params: { sessionId: this.sessionId, content: content } }); } 3. 处理 ACP 通知 (src/agent/acp/AcpConnection.ts) private handleMessage(message: string) { const data = JSON.parse(message); // 根据 ACP 协议处理不同类型的消息 if (data.method === 'session/update') { // ← ACP 定义的通知类型 this.onSessionUpdate?.(data.params); } if (data.method === 'permission/request') { // ← ACP 定义的通知类型 this.onPermissionRequest?.(data.params); } if (data.method === 'session/end_turn') { // ← ACP 定义的通知类型 this.onEndTurn?.(); } } --- 🎯 总结:ACP 的核心价值 ACP 协议 = 标准化的通信规范 就像: - HTTP 定义了浏览器和服务器如何通信 - WebSocket 定义了双向实时通信的格式 - ACP 定义了 UI 和 AI CLI 如何通信 没有 ACP 会怎样? 每个 CLI 都会有自己的格式: # Qwen 自己的格式(假设) qwen send "帮我分析代码" > RESPONSE: 好的... # Claude 自己的格式(假设) claude --prompt="帮我分析代码" > OUTPUT: 好的... # Codex 自己的格式(假设) codex --input "帮我分析代码" > RESULT: 好的... 你需要为每个 CLI 写不同的对接代码! ❌ 有了 ACP 之后? 所有支持 ACP 的 CLI 都使用相同的格式: // 统一的发送格式 {"jsonrpc": "2.0", "method": "session/send_prompt", "params": {...}} // 统一的接收格式 {"jsonrpc": "2.0", "method": "session/update", "params": {...}} 你只需要写一套代码就能对接所有 CLI! ✅ --- ✅ 最终答案 问:这样的话是不是就跟 ACP 没有什么关系了? 答:完全相反!整个通信的核心就是 ACP 协议! - ✅ VSCode Extension 使用 ACP 协议与 Qwen CLI 通信 - ✅ 所有的消息格式都遵循 ACP 规范(JSON-RPC 2.0) - ✅ initialize、new_session、send_prompt 都是 ACP 定义的方法 - ✅ session/update、permission/request 都是 ACP 定义的通知 ACP 不是一个中间件或服务,而是一套通信协议标准,让不同的 UI(AionUi、VSCode 插件)可以用统一的方式与不同的 CLI(Qwen、Claude、Codex)对话。 这就像你用 HTTP 协议访问网站,不需要"连接到 HTTP 服务器",HTTP 只是规定了浏览器和服务器怎么说话的格式。ACP 也是一样的道理!