From c9e6b383c6cc35f4a6d77b04ad3d18fdb1fcac9a Mon Sep 17 00:00:00 2001 From: Roe-xin Date: Wed, 4 Mar 2026 18:29:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=A4=9A=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E4=BA=A4=E4=BA=92=E6=A0=BC=E5=BC=8F=20=20=20=20-=20?= =?UTF-8?q?=E9=87=8D=E6=9E=84=20AskUserEvent=20=E6=94=AF=E6=8C=81=E5=A4=9A?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E6=95=B0=E7=BB=84=E6=A0=BC=E5=BC=8F=20=20=20?= =?UTF-8?q?=20-=20=E6=9B=B4=E6=96=B0=20AnswerRequest=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=96=B0=E7=9A=84=20answers=20=E5=AD=97=E6=AE=B5=20=20=20=20-?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=EF=BC=8C=E6=94=AF=E6=8C=81=E5=8D=95=E9=80=89?= =?UTF-8?q?/=E5=A4=9A=E9=80=89=20=20=20=20-=20=E4=BF=9D=E6=8C=81=E5=90=91?= =?UTF-8?q?=E5=90=8E=E5=85=BC=E5=AE=B9=E6=97=A7=E7=9A=84=20selected/custom?= =?UTF-8?q?Input=20=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/VSCode-Extension-API-Guide.md | 804 +++++++++++++++++++++++++++++ src/services/dialogService.ts | 39 +- src/services/userInteraction.ts | 52 +- src/types/api.ts | 16 +- src/utils/messageHandler.ts | 3 +- src/views/ICViewProvider.ts | 9 + src/views/messageArea.ts | 99 ++-- 7 files changed, 921 insertions(+), 101 deletions(-) create mode 100644 docs/VSCode-Extension-API-Guide.md diff --git a/docs/VSCode-Extension-API-Guide.md b/docs/VSCode-Extension-API-Guide.md new file mode 100644 index 0000000..636d3a4 --- /dev/null +++ b/docs/VSCode-Extension-API-Guide.md @@ -0,0 +1,804 @@ +# VS Code Extension API 核心知识点 + +## 目录 +- [1. Extension 生命周期](#1-extension-生命周期) ⭐⭐⭐ +- [2. 激活事件 (Activation Events)](#2-激活事件-activation-events) ⭐⭐ +- [3. 命令系统 (Commands)](#3-命令系统-commands) ⭐⭐ +- [4. Webview API](#4-webview-api) ⭐⭐⭐⭐⭐ **面试重点** +- [5. TreeView 和自定义视图](#5-treeview-和自定义视图) ⭐⭐ +- [6. 文件系统操作](#6-文件系统操作) ⭐⭐⭐ +- [7. 配置和存储](#7-配置和存储) ⭐⭐⭐⭐ **面试重点** +- [8. 消息通知](#8-消息通知) ⭐ +- [9. 语言特性支持](#9-语言特性支持) ⭐ +- [10. 调试和诊断](#10-调试和诊断) ⭐ + +--- + +## 1. Extension 生命周期 ⭐⭐⭐ + +### 1.1 核心函数 🔥必考 + +```typescript +// extension.ts +import * as vscode from 'vscode'; + +// 插件激活时调用(只调用一次) +export function activate(context: vscode.ExtensionContext) { + console.log('Extension is now active!'); + + // 注册命令、视图、事件监听等 + // 使用 context.subscriptions 管理资源 +} + +// 插件停用时调用(清理资源) +export function deactivate() { + console.log('Extension is deactivated'); + // 清理资源、关闭连接等 +} +``` + +### 1.2 ExtensionContext 重要属性 🔥必考 + +```typescript +interface ExtensionContext { + // 插件订阅管理(自动清理) + subscriptions: { dispose(): any }[]; + + // 工作区存储路径 + storageUri: vscode.Uri | undefined; + globalStorageUri: vscode.Uri; + + // 插件路径 + extensionUri: vscode.Uri; + extensionPath: string; + + // 状态存储 + workspaceState: Memento; // 工作区级别 + globalState: Memento; // 全局级别 + secrets: SecretStorage; // 敏感信息存储 + + // 环境变量 + environmentVariableCollection: EnvironmentVariableCollection; +} +``` + +### 1.3 资源管理最佳实践 🔥必考 + +```typescript +export function activate(context: vscode.ExtensionContext) { + // ✅ 推荐:使用 context.subscriptions 自动管理 + context.subscriptions.push( + vscode.commands.registerCommand('extension.command', () => {}) + ); + + // ❌ 不推荐:手动管理容易忘记清理 + const disposable = vscode.commands.registerCommand('extension.command', () => {}); + // 需要在 deactivate 中手动调用 disposable.dispose() +} +``` + +--- + +## 2. 激活事件 (Activation Events) ⭐⭐ + +### 2.1 常用激活事件 📌重要 + +```json +// package.json +{ + "activationEvents": [ + // 启动时激活 + "onStartupFinished", + + // 执行命令时激活 + "onCommand:extension.helloWorld", + + // 打开特定语言文件时激活 + "onLanguage:javascript", + "onLanguage:verilog", + + // 打开特定文件类型时激活 + "onFileSystem:sftp", + + // 打开特定视图时激活 + "onView:myCustomView", + + // 调试时激活 + "onDebug", + + // 打开特定 URI 时激活 + "onUri", + + // Webview 恢复时激活 + "onWebviewPanel:myWebview", + + // 任务执行时激活 + "onTaskType:npm" + ] +} +``` + +### 2.2 延迟激活策略 🔥必考 + +```typescript +// ✅ 推荐:使用 onStartupFinished 延迟激活 +"activationEvents": ["onStartupFinished"] + +// ❌ 不推荐:使用 * 会拖慢启动速度 +"activationEvents": ["*"] +``` + +--- + +## 3. 命令系统 (Commands) + +### 3.1 注册命令 + +```typescript +// 注册简单命令 +const disposable = vscode.commands.registerCommand( + 'extension.helloWorld', + () => { + vscode.window.showInformationMessage('Hello World!'); + } +); +context.subscriptions.push(disposable); + +// 注册带参数的命令 +vscode.commands.registerCommand( + 'extension.openFile', + (filePath: string) => { + vscode.workspace.openTextDocument(filePath).then(doc => { + vscode.window.showTextDocument(doc); + }); + } +); +``` + +### 3.2 执行命令 + +```typescript +// 执行内置命令 +await vscode.commands.executeCommand('workbench.action.files.save'); + +// 执行自定义命令 +await vscode.commands.executeCommand('extension.openFile', '/path/to/file'); + +// 获取所有可用命令 +const commands = await vscode.commands.getCommands(); +``` + +### 3.3 常用内置命令 + +```typescript +// 文件操作 +'workbench.action.files.save' +'workbench.action.files.saveAll' +'workbench.action.closeActiveEditor' + +// 编辑器操作 +'editor.action.formatDocument' +'editor.action.commentLine' +'editor.action.selectAll' + +// 窗口操作 +'workbench.action.toggleSidebarVisibility' +'workbench.action.terminal.new' +'workbench.action.quickOpen' + +// Git 操作 +'git.commit' +'git.push' +'git.pull' +``` + +--- + +## 4. Webview API ⭐⭐⭐⭐⭐ **面试重点** + +### 4.1 创建 Webview Panel 🔥必考 + +```typescript +const panel = vscode.window.createWebviewPanel( + 'myWebview', // viewType(唯一标识) + 'My Webview', // 标题 + vscode.ViewColumn.One, // 显示位置 + { + enableScripts: true, // 启用 JavaScript + retainContextWhenHidden: true, // 隐藏时保留状态 + localResourceRoots: [ // 允许访问的本地资源路径 + vscode.Uri.joinPath(context.extensionUri, 'media') + ] + } +); +``` + +### 4.2 设置 Webview 内容 + +```typescript +panel.webview.html = getWebviewContent(); + +function getWebviewContent() { + return ` + + + + + My Webview + + +

Hello from Webview!

+ + + + + `; +} +``` + +### 4.3 Webview 消息通信 🔥必考(项目核心) + +```typescript +// Extension → Webview +panel.webview.postMessage({ + command: 'update', + data: 'some data' +}); + +// Webview → Extension +panel.webview.onDidReceiveMessage( + message => { + switch (message.command) { + case 'alert': + vscode.window.showInformationMessage(message.text); + break; + case 'getData': + // 处理数据请求 + panel.webview.postMessage({ + command: 'dataResponse', + data: fetchData() + }); + break; + } + }, + undefined, + context.subscriptions +); +``` + +### 4.4 Webview 生命周期管理 📌重要 + +```typescript +// 监听 Webview 关闭事件 +panel.onDidDispose( + () => { + // 清理资源 + console.log('Webview disposed'); + }, + null, + context.subscriptions +); + +// 监听 Webview 可见性变化 +panel.onDidChangeViewState( + e => { + if (e.webviewPanel.visible) { + console.log('Webview is now visible'); + } + }, + null, + context.subscriptions +); +``` + +### 4.5 加载本地资源 📌重要 + +```typescript +// 获取本地资源 URI +const scriptUri = panel.webview.asWebviewUri( + vscode.Uri.joinPath(context.extensionUri, 'media', 'script.js') +); + +const styleUri = panel.webview.asWebviewUri( + vscode.Uri.joinPath(context.extensionUri, 'media', 'style.css') +); + +// 在 HTML 中使用 +const html = ` + + +`; +``` + +### 4.6 Webview 状态持久化 📌重要 + +```typescript +// Webview 中保存状态 +const vscode = acquireVsCodeApi(); +const state = vscode.getState() || { count: 0 }; + +// 更新状态 +state.count++; +vscode.setState(state); + +// Extension 中序列化状态 +panel.webview.options = { + enableScripts: true, + retainContextWhenHidden: true +}; + +// 恢复 Webview +vscode.window.registerWebviewPanelSerializer('myWebview', { + async deserializeWebviewPanel(webviewPanel, state) { + webviewPanel.webview.html = getWebviewContent(); + // 恢复状态 + webviewPanel.webview.postMessage({ command: 'restore', state }); + } +}); +``` + +--- + +## 5. TreeView 和自定义视图 + +### 5.1 创建 TreeView Provider + +```typescript +class MyTreeDataProvider implements vscode.TreeDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData = this._onDidChangeTreeData.event; + + refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + getTreeItem(element: TreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: TreeItem): Thenable { + if (!element) { + // 返回根节点 + return Promise.resolve([ + new TreeItem('Item 1', vscode.TreeItemCollapsibleState.None), + new TreeItem('Item 2', vscode.TreeItemCollapsibleState.Collapsed) + ]); + } + // 返回子节点 + return Promise.resolve([]); + } +} + +class TreeItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState + ) { + super(label, collapsibleState); + this.tooltip = `Tooltip for ${label}`; + this.command = { + command: 'extension.itemClicked', + title: 'Click Item', + arguments: [this] + }; + } +} +``` + +### 5.2 注册 TreeView + +```typescript +const treeDataProvider = new MyTreeDataProvider(); +const treeView = vscode.window.createTreeView('myTreeView', { + treeDataProvider, + showCollapseAll: true +}); + +context.subscriptions.push(treeView); + +// 刷新视图 +treeDataProvider.refresh(); +``` + +### 5.3 WebviewView Provider(侧边栏 Webview) + +```typescript +class MyWebviewViewProvider implements vscode.WebviewViewProvider { + resolveWebviewView( + webviewView: vscode.WebviewView, + context: vscode.WebviewViewResolveContext, + token: vscode.CancellationToken + ) { + webviewView.webview.options = { + enableScripts: true + }; + + webviewView.webview.html = getWebviewContent(); + + webviewView.webview.onDidReceiveMessage(message => { + // 处理消息 + }); + } +} + +// 注册 +vscode.window.registerWebviewViewProvider( + 'myWebviewView', + new MyWebviewViewProvider() +); +``` + +--- + +## 6. 文件系统操作 ⭐⭐⭐ + +### 6.1 读取文件 📌重要 + +```typescript +// 读取文本文件 +const uri = vscode.Uri.file('/path/to/file.txt'); +const content = await vscode.workspace.fs.readFile(uri); +const text = Buffer.from(content).toString('utf8'); + +// 使用 TextDocument API +const document = await vscode.workspace.openTextDocument(uri); +const text = document.getText(); +``` + +### 6.2 写入文件 + +```typescript +// 写入文件 +const uri = vscode.Uri.file('/path/to/file.txt'); +const content = Buffer.from('Hello World', 'utf8'); +await vscode.workspace.fs.writeFile(uri, content); + +// 使用 WorkspaceEdit +const edit = new vscode.WorkspaceEdit(); +edit.createFile(uri, { overwrite: true }); +edit.insert(uri, new vscode.Position(0, 0), 'Hello World'); +await vscode.workspace.applyEdit(edit); +``` + +### 6.3 文件监听 + +```typescript +// 监听文件变化 +const watcher = vscode.workspace.createFileSystemWatcher('**/*.js'); + +watcher.onDidCreate(uri => { + console.log('File created:', uri.fsPath); +}); + +watcher.onDidChange(uri => { + console.log('File changed:', uri.fsPath); +}); + +watcher.onDidDelete(uri => { + console.log('File deleted:', uri.fsPath); +}); + +context.subscriptions.push(watcher); +``` + +### 6.4 工作区操作 + +```typescript +// 获取工作区文件夹 +const workspaceFolders = vscode.workspace.workspaceFolders; +if (workspaceFolders) { + const rootPath = workspaceFolders[0].uri.fsPath; +} + +// 查找文件 +const files = await vscode.workspace.findFiles( + '**/*.ts', // include pattern + '**/node_modules/**' // exclude pattern +); + +// 打开文件 +const document = await vscode.workspace.openTextDocument(uri); +await vscode.window.showTextDocument(document); +``` + +--- + +## 7. 配置和存储 ⭐⭐⭐⭐ **面试重点** + +### 7.1 读取配置 📌重要 + +```typescript +// 读取配置 +const config = vscode.workspace.getConfiguration('myExtension'); +const value = config.get('settingName', 'defaultValue'); + +// 监听配置变化 +vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('myExtension.settingName')) { + console.log('Configuration changed'); + } +}); +``` + +### 7.2 更新配置 + +```typescript +const config = vscode.workspace.getConfiguration('myExtension'); + +// 更新用户配置(全局) +await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Global); + +// 更新工作区配置 +await config.update('settingName', 'newValue', vscode.ConfigurationTarget.Workspace); +``` + +### 7.3 状态存储 🔥必考 + +```typescript +// 工作区状态(仅当前工作区) +await context.workspaceState.update('key', 'value'); +const value = context.workspaceState.get('key'); + +// 全局状态(跨工作区) +await context.globalState.update('key', 'value'); +const value = context.globalState.get('key'); + +// 存储对象 +await context.globalState.update('userData', { name: 'John', age: 30 }); +``` + +### 7.4 敏感信息存储 🔥必考(Token 管理) + +```typescript +// 存储密码、Token 等敏感信息 +await context.secrets.store('apiToken', 'secret-token-value'); + +// 读取 +const token = await context.secrets.get('apiToken'); + +// 删除 +await context.secrets.delete('apiToken'); + +// 监听变化 +context.secrets.onDidChange(e => { + console.log('Secret changed:', e.key); +}); +``` + +--- + +## 8. 消息通知 + +### 8.1 信息提示 + +```typescript +// 普通信息 +vscode.window.showInformationMessage('Operation completed!'); + +// 警告 +vscode.window.showWarningMessage('This action may cause issues'); + +// 错误 +vscode.window.showErrorMessage('Operation failed!'); +``` + +### 8.2 带按钮的提示 + +```typescript +const result = await vscode.window.showInformationMessage( + 'Do you want to continue?', + 'Yes', + 'No', + 'Cancel' +); + +if (result === 'Yes') { + // 用户点击了 Yes +} +``` + +### 8.3 输入框 + +```typescript +// 简单输入 +const input = await vscode.window.showInputBox({ + prompt: 'Enter your name', + placeHolder: 'John Doe', + validateInput: (value) => { + return value.length < 3 ? 'Name too short' : null; + } +}); + +// 快速选择 +const selected = await vscode.window.showQuickPick( + ['Option 1', 'Option 2', 'Option 3'], + { + placeHolder: 'Select an option', + canPickMany: false + } +); +``` + +### 8.4 进度提示 + +```typescript +await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Processing...', + cancellable: true + }, + async (progress, token) => { + token.onCancellationRequested(() => { + console.log('User canceled'); + }); + + progress.report({ increment: 0, message: 'Starting...' }); + await doWork1(); + + progress.report({ increment: 50, message: 'Half done...' }); + await doWork2(); + + progress.report({ increment: 100, message: 'Complete!' }); + } +); +``` + +--- + +## 9. 语言特性支持 + +### 9.1 代码补全 + +```typescript +const provider = vscode.languages.registerCompletionItemProvider( + 'javascript', + { + provideCompletionItems(document, position) { + const item = new vscode.CompletionItem('myFunction'); + item.kind = vscode.CompletionItemKind.Function; + item.detail = 'My custom function'; + item.documentation = 'This is a custom function'; + item.insertText = new vscode.SnippetString('myFunction($1)$0'); + + return [item]; + } + }, + '.' // 触发字符 +); + +context.subscriptions.push(provider); +``` + +### 9.2 悬停提示 + +```typescript +const provider = vscode.languages.registerHoverProvider('javascript', { + provideHover(document, position) { + const range = document.getWordRangeAtPosition(position); + const word = document.getText(range); + + return new vscode.Hover([ + `**${word}**`, + 'This is a hover tooltip' + ]); + } +}); +``` + +### 9.3 诊断(错误提示) + +```typescript +const diagnosticCollection = vscode.languages.createDiagnosticCollection('myExtension'); +context.subscriptions.push(diagnosticCollection); + +function updateDiagnostics(document: vscode.TextDocument) { + const diagnostics: vscode.Diagnostic[] = []; + + const text = document.getText(); + const regex = /TODO/g; + let match; + + while ((match = regex.exec(text))) { + const range = new vscode.Range( + document.positionAt(match.index), + document.positionAt(match.index + match[0].length) + ); + + const diagnostic = new vscode.Diagnostic( + range, + 'TODO found', + vscode.DiagnosticSeverity.Warning + ); + + diagnostics.push(diagnostic); + } + + diagnosticCollection.set(document.uri, diagnostics); +} +``` + +--- + +## 10. 调试和诊断 + +### 10.1 输出通道 + +```typescript +const outputChannel = vscode.window.createOutputChannel('My Extension'); +context.subscriptions.push(outputChannel); + +outputChannel.appendLine('Extension activated'); +outputChannel.show(); // 显示输出面板 +``` + +### 10.2 日志记录 + +```typescript +// 使用 LogOutputChannel(带时间戳) +const logger = vscode.window.createOutputChannel('My Extension', { log: true }); + +logger.trace('Trace message'); +logger.debug('Debug message'); +logger.info('Info message'); +logger.warn('Warning message'); +logger.error('Error message'); +``` + +### 10.3 错误处理 + +```typescript +try { + await riskyOperation(); +} catch (error) { + if (error instanceof Error) { + vscode.window.showErrorMessage(`Error: ${error.message}`); + logger.error(error.stack || error.message); + } +} +``` + +--- + +## 最佳实践总结 + +### ✅ 推荐做法 + +1. **资源管理**:所有 disposable 对象都放入 `context.subscriptions` +2. **延迟激活**:使用 `onStartupFinished` 而不是 `*` +3. **异步操作**:使用 `async/await` 处理异步操作 +4. **错误处理**:捕获异常并给用户友好提示 +5. **类型安全**:充分利用 TypeScript 类型系统 +6. **状态持久化**:使用 `globalState`/`workspaceState` 保存状态 +7. **敏感信息**:使用 `secrets` API 存储 Token、密码等 + +### ❌ 避免做法 + +1. 不要在 `activate` 中执行耗时操作 +2. 不要忘记清理资源(监听器、Webview 等) +3. 不要在 Webview 中直接访问文件系统 +4. 不要在配置中存储敏感信息 +5. 不要阻塞主线程(使用 Worker 或异步操作) + +--- + +## 参考资源 + +- [VS Code Extension API 官方文档](https://code.visualstudio.com/api) +- [Extension Samples](https://github.com/microsoft/vscode-extension-samples) +- [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines) diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index b4298c9..b9d3045 100644 --- a/src/services/dialogService.ts +++ b/src/services/dialogService.ts @@ -43,8 +43,7 @@ export interface MessageSegment { toolResult?: string; toolDescription?: string; askId?: string; - question?: string; - options?: string[]; + questions?: import("../types/api").QuestionItem[]; // 智能体相关字段 agentId?: string; agentName?: string; @@ -647,8 +646,11 @@ export class DialogSession { this.segments.push({ type: "question", askId: askId, - question: question, - options: ["确认执行", "取消"], + questions: [{ + question: question, + options: ["确认执行", "取消"], + multiSelect: false + }], }); // 实时发送段落更新 @@ -666,9 +668,12 @@ export class DialogSession { await userInteractionManager.handleAskUser( { askId: askId, - question: question, - options: ["确认执行", "取消"], - } as AskUserEvent, + questions: [{ + question: question, + options: ["确认执行", "取消"], + multiSelect: false + }] + }, this.taskId ); @@ -714,12 +719,15 @@ export class DialogSession { // 注册问题到前端(类似 askUser),以便用户回答时能找到 const planEvent = { askId: askId, - question: `请确认执行计划:${data.title}`, - options: ["确认执行", "修改计划", "取消"], + questions: [{ + question: `请确认执行计划:${data.title}`, + options: ["确认执行", "修改计划", "取消"], + multiSelect: false + }] }; try { await userInteractionManager.handleAskUser( - planEvent as AskUserEvent, + planEvent, this.taskId ); } catch (error) { @@ -856,13 +864,9 @@ export class DialogSession { this.segments.push({ type: "question", askId: data.askId, - question: data.question, - options: data.options, + questions: data.questions, }); - // 实时发送段落更新(包含问题) callbacks.onSegmentUpdate?.(this.segments); - // 同时调用 onQuestion 用于更新状态栏等 - callbacks.onQuestion?.(data.askId, data.question, data.options); try { await userInteractionManager.handleAskUser(data, this.taskId); } catch (error) { @@ -1109,14 +1113,13 @@ export class DialogSession { */ async submitAnswer( askId: string, + answers?: { [key: string]: string[] }, selected?: string[], customInput?: string ): Promise { - // 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId - // 如果 pendingQuestions 中有问题,走正常流程 - // 如果没有,receiveAnswer 会使用 fallbackTaskId 直接发送到后端 await userInteractionManager.receiveAnswer( askId, + answers, selected, customInput, this.taskId diff --git a/src/services/userInteraction.ts b/src/services/userInteraction.ts index 0885b35..7593314 100644 --- a/src/services/userInteraction.ts +++ b/src/services/userInteraction.ts @@ -4,7 +4,7 @@ */ import * as vscode from 'vscode'; import { submitAnswer, submitToolConfirm } from './apiClient'; -import type { AskUserEvent, AnswerRequest } from '../types/api'; +import type { AskUserEvent, AnswerRequest, QuestionItem } from '../types/api'; /** * 待处理的用户问题 @@ -12,8 +12,7 @@ import type { AskUserEvent, AnswerRequest } from '../types/api'; interface PendingQuestion { askId: string; taskId: string; - question: string; - options: string[]; + questions: QuestionItem[]; resolve: (answer: string) => void; reject: (error: Error) => void; } @@ -45,20 +44,15 @@ export class UserInteractionManager { * @param taskId 当前任务ID */ async handleAskUser(event: AskUserEvent, taskId: string): Promise { - const { askId, question, options } = event; + const { askId, questions } = event; - console.log(`[UserInteraction] 收到问题: askId=${askId}, question=${question}`); + console.log(`[UserInteraction] 收到问题: askId=${askId}, count=${questions.length}`); - // 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理 - // 这里不再单独发送 showQuestion 命令,避免重复显示 - - // 创建 Promise 等待用户回答 return new Promise((resolve, reject) => { this.pendingQuestions.set(askId, { askId, taskId, - question, - options, + questions, resolve: (answer: string) => { this.submitUserAnswer(askId, taskId, answer) .then(() => resolve()) @@ -67,7 +61,6 @@ export class UserInteractionManager { reject }); - // 设置超时(2小时) setTimeout(() => { if (this.pendingQuestions.has(askId)) { this.pendingQuestions.delete(askId); @@ -80,24 +73,35 @@ export class UserInteractionManager { /** * 处理用户提交的回答(从 WebView 调用) * @param askId 问题ID - * @param selected 选中的选项 - * @param customInput 自定义输入 - * @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端) + * @param answers 新格式:按问题索引的答案 + * @param selected 旧格式:选中的选项 + * @param customInput 旧格式:自定义输入 + * @param fallbackTaskId 当问题不存在时使用的 taskId */ async receiveAnswer( askId: string, + answers?: { [key: string]: string[] }, selected?: string[], customInput?: string, fallbackTaskId?: string ): Promise { const pending = this.pendingQuestions.get(askId); - const answer = customInput || selected?.join(', ') || ''; + + // 构建答案字符串 + let answer = ''; + if (answers) { + answer = Object.entries(answers) + .sort(([a], [b]) => parseInt(a) - parseInt(b)) + .map(([_, vals]) => vals.join('; ')) + .join(' | '); + } else { + answer = customInput || selected?.join(', ') || ''; + } if (!pending) { - // 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端 if (fallbackTaskId) { - console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`); - await this.submitUserAnswer(askId, fallbackTaskId, answer); + console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}`); + await this.submitUserAnswer(askId, fallbackTaskId, answer, answers); } else { console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`); } @@ -105,11 +109,7 @@ export class UserInteractionManager { } console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`); - - // 移除待处理问题 this.pendingQuestions.delete(askId); - - // 触发 resolve pending.resolve(answer); } @@ -119,7 +119,8 @@ export class UserInteractionManager { private async submitUserAnswer( askId: string, taskId: string, - answer: string + answer: string, + answers?: { [key: string]: string[] } ): Promise { // 检查是否是工具确认类型的问题 if (askId.startsWith('tool_confirm_')) { @@ -148,7 +149,8 @@ export class UserInteractionManager { const request: AnswerRequest = { askId, taskId, - customInput: answer + answers: answers, + customInput: answers ? undefined : answer }; try { diff --git a/src/types/api.ts b/src/types/api.ts index ecb2adc..2e91665 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -194,11 +194,17 @@ export interface PlanSummaryUpdateEvent { timestamp: number; } +/** 单个问题项 */ +export interface QuestionItem { + question: string; + options: string[]; + multiSelect?: boolean; +} + /** ask_user 事件数据 */ export interface AskUserEvent { askId: string; - question: string; - options: string[]; + questions: QuestionItem[]; } /** complete 事件数据 */ @@ -351,10 +357,12 @@ export interface AnswerRequest { askId: string; /** 任务ID */ taskId: string; - /** 选中的选项列表 */ + /** 选中的选项列表(旧格式,兼容) */ selected?: string[]; - /** 自定义输入内容 */ + /** 自定义输入内容(旧格式,兼容) */ customInput?: string; + /** 新格式:按问题索引的答案,key 为问题下标 "0","1",value 为选中项列表 */ + answers?: { [key: string]: string[] }; } /** 用户回答响应 */ diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 72ff7ff..64f6f5b 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -468,11 +468,12 @@ async function handleUserMessageWithBackend( */ export async function handleUserAnswer( askId: string, + answers?: { [key: string]: string[] }, selected?: string[], customInput?: string ): Promise { if (currentSession) { - await currentSession.submitAnswer(askId, selected, customInput); + await currentSession.submitAnswer(askId, answers, selected, customInput); } } diff --git a/src/views/ICViewProvider.ts b/src/views/ICViewProvider.ts index 0015a74..ce7c260 100644 --- a/src/views/ICViewProvider.ts +++ b/src/views/ICViewProvider.ts @@ -128,6 +128,15 @@ export function showICHelperPanel(context: vscode.ExtensionContext) { case "submitAnswer": handleUserAnswer( message.askId, + message.answers, + message.selected, + message.customInput + ); + break; + case "userAnswer": + handleUserAnswer( + message.askId, + message.answers, message.selected, message.customInput ); diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index ded1e5e..d86d500 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -1188,7 +1188,6 @@ export function getMessageAreaScript(): string { } else if (segment.type === 'question') { segmentDiv.className += ' segment-question'; - // 检查是否已回答 const isAnswered = answeredQuestions.has(segment.askId); const selectedAnswer = answeredQuestions.get(segment.askId); @@ -1196,56 +1195,49 @@ export function getMessageAreaScript(): string { segmentDiv.classList.add('answered'); } - // 检查是否有选项 - const hasOptions = segment.options && segment.options.length > 0; + const questions = segment.questions || []; + const questionsHtml = questions.map((q, qIdx) => { + const inputType = q.multiSelect ? 'checkbox' : 'radio'; + const inputName = 'q_' + segment.askId + '_' + qIdx; + const optionsHtml = q.options.map((opt, optIdx) => { + const escapedOpt = opt.replace(/"/g, '"').replace(//g, '>'); + return ''; + }).join(''); + return '
' + + '
' + formatText(q.question) + '
' + + '
' + optionsHtml + '
' + + '
'; + }).join(''); - const optionsHtml = hasOptions - ? (segment.options || []).map(opt => { - const isSelected = isAnswered && opt === selectedAnswer; - return \`\`; - }).join('') - : ''; + segmentDiv.innerHTML = questionsHtml + + '
' + + '' + + '
'; - segmentDiv.innerHTML = \` -
\${formatText(segment.question || '')}
- \${hasOptions ? \`
\${optionsHtml}
\` : ''} -
- - -
- \`; - - // 只在未回答时添加事件监听 if (!isAnswered) { setTimeout(() => { - if (hasOptions) { - const optionButtons = segmentDiv.querySelectorAll('.question-option'); - optionButtons.forEach(btn => { - btn.addEventListener('click', function() { - const option = this.getAttribute('data-option'); - handleQuestionAnswerInSegment(segment.askId, option, segmentDiv); - }); - }); - } - const submitBtn = segmentDiv.querySelector('.custom-submit'); - const customInput = segmentDiv.querySelector('.custom-input'); - if (submitBtn && customInput) { + if (submitBtn) { submitBtn.addEventListener('click', function() { - const customValue = customInput.value.trim(); - if (customValue) { - handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); - } - }); - - // 支持回车提交 - customInput.addEventListener('keypress', function(e) { - if (e.key === 'Enter') { - const customValue = customInput.value.trim(); - if (customValue) { - handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv); - } - } + const answers = {}; + questions.forEach((q, qIdx) => { + const inputs = segmentDiv.querySelectorAll('input[data-q-idx="' + qIdx + '"]:checked'); + answers[qIdx] = Array.from(inputs).map(inp => (inp as HTMLInputElement).value); + }); + vscode.postMessage({ + command: 'userAnswer', + askId: segment.askId, + answers: answers, + taskId: currentTaskId + }); + answeredQuestions.set(segment.askId, JSON.stringify(answers)); + segmentDiv.classList.add('answered'); + segmentDiv.querySelectorAll('input').forEach(inp => (inp as HTMLInputElement).disabled = true); + const container = segmentDiv.querySelector('.custom-input-container') as HTMLElement; + if (container) container.style.display = 'none'; }); } }, 0); @@ -1446,14 +1438,15 @@ export function getMessageAreaScript(): string { }, 0); } } else if (segment.type === 'question') { - segmentDiv.innerHTML = \` -
-
\${formatText(segment.question || '')}
-
- \${(segment.options || []).map(opt => \`\${opt}\`).join('')} -
-
- \`; + const questions = segment.questions || []; + const questionsHtml = questions.map(q => { + const optionsHtml = q.options.map(opt => '' + opt + '').join(''); + return '
' + + '
' + formatText(q.question) + '
' + + '
' + optionsHtml + '
' + + '
'; + }).join(''); + segmentDiv.innerHTML = '
' + questionsHtml + '
'; } else if (segment.type === 'agent') { // 智能体卡片渲染 renderAgentCard(segment, segmentDiv);