mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
- 将 WebView 调整到编辑器右侧 - 添加 ChatHeader 组件,实现会话下拉菜单 - 替换模态框为紧凑型下拉菜单 - 更新会话切换逻辑,显示当前标题 - 清理旧的会话选择器样式 基于 Claude Code v2.0.43 UI 分析实现。
1027 lines
30 KiB
Markdown
1027 lines
30 KiB
Markdown
> 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 (
|
||
<div>
|
||
<input value={input} onChange={e => setInput(e.target.value)} />
|
||
<button onClick={handleSend}>Send</button>
|
||
</div>
|
||
);
|
||
|
||
};
|
||
|
||
---
|
||
|
||
第 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<void> {
|
||
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||
const config = vscode.workspace.getConfiguration('aionui');
|
||
const qwenPath = config.get<string>('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<void> {
|
||
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<void> {
|
||
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<number, {
|
||
resolve: (value: any) => 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<AcpResponse> {
|
||
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<void> {
|
||
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<any[]>([]);
|
||
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 (
|
||
<div>
|
||
<MessageList messages={messages} />
|
||
<ChatInput />
|
||
</div>
|
||
);
|
||
|
||
};
|
||
|
||
---
|
||
|
||
📊 完整的消息流示意图
|
||
|
||
// 用户输入 "帮我分析代码"
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ [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 也是一样的道理!
|