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);