feat: 支持多问题交互格式

- 重构 AskUserEvent 支持多问题数组格式
   - 更新 AnswerRequest 支持新的 answers 字段
   - 优化前端问题渲染,支持单选/多选
   - 保持向后兼容旧的 selected/customInput 格式
This commit is contained in:
Roe-xin
2026-03-04 18:29:16 +08:00
parent 19cdf47bed
commit c9e6b383c6
7 changed files with 921 additions and 101 deletions

View File

@ -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 `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Webview</title>
</head>
<body>
<h1>Hello from Webview!</h1>
<button onclick="sendMessage()">Send Message</button>
<script>
const vscode = acquireVsCodeApi();
function sendMessage() {
vscode.postMessage({
command: 'alert',
text: 'Hello from Webview!'
});
}
// 接收来自 Extension 的消息
window.addEventListener('message', event => {
const message = event.data;
console.log('Received:', message);
});
</script>
</body>
</html>`;
}
```
### 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 = `
<link href="${styleUri}" rel="stylesheet">
<script src="${scriptUri}"></script>
`;
```
### 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<TreeItem> {
private _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined>();
readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
refresh(): void {
this._onDidChangeTreeData.fire(undefined);
}
getTreeItem(element: TreeItem): vscode.TreeItem {
return element;
}
getChildren(element?: TreeItem): Thenable<TreeItem[]> {
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<string>('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)

View File

@ -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,
questions: [{
question: question,
options: ["确认执行", "取消"],
multiSelect: false
}],
});
// 实时发送段落更新
@ -666,9 +668,12 @@ export class DialogSession {
await userInteractionManager.handleAskUser(
{
askId: askId,
questions: [{
question: question,
options: ["确认执行", "取消"],
} as AskUserEvent,
multiSelect: false
}]
},
this.taskId
);
@ -714,12 +719,15 @@ export class DialogSession {
// 注册问题到前端(类似 askUser以便用户回答时能找到
const planEvent = {
askId: askId,
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<void> {
// 直接调用 receiveAnswer传递 taskId 作为 fallbackTaskId
// 如果 pendingQuestions 中有问题,走正常流程
// 如果没有receiveAnswer 会使用 fallbackTaskId 直接发送到后端
await userInteractionManager.receiveAnswer(
askId,
answers,
selected,
customInput,
this.taskId

View File

@ -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<void> {
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<void> {
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<void> {
// 检查是否是工具确认类型的问题
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 {

View File

@ -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[] };
}
/** 用户回答响应 */

View File

@ -468,11 +468,12 @@ async function handleUserMessageWithBackend(
*/
export async function handleUserAnswer(
askId: string,
answers?: { [key: string]: string[] },
selected?: string[],
customInput?: string
): Promise<void> {
if (currentSession) {
await currentSession.submitAnswer(askId, selected, customInput);
await currentSession.submitAnswer(askId, answers, selected, customInput);
}
}

View File

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

View File

@ -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, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return '<label class="question-option">' +
'<input type="' + inputType + '" name="' + inputName + '" value="' + escapedOpt + '" data-q-idx="' + qIdx + '" data-opt-idx="' + optIdx + '" ' + (isAnswered ? 'disabled' : '') + ' />' +
'<span>' + opt + '</span>' +
'</label>';
}).join('');
return '<div class="question-item">' +
'<div class="question-text">' + formatText(q.question) + '</div>' +
'<div class="question-options">' + optionsHtml + '</div>' +
'</div>';
}).join('');
const optionsHtml = hasOptions
? (segment.options || []).map(opt => {
const isSelected = isAnswered && opt === selectedAnswer;
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
}).join('')
: '';
segmentDiv.innerHTML = questionsHtml +
'<div class="custom-input-container" style="display: ' + (isAnswered ? 'none' : 'flex') + ';">' +
'<button class="custom-submit">提交答案</button>' +
'</div>';
segmentDiv.innerHTML = \`
<div class="question-text">\${formatText(segment.question || '')}</div>
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
<div class="custom-input-container" style="display: \${isAnswered ? 'none' : 'flex'};">
<input type="text" class="custom-input" placeholder="\${hasOptions ? '输入其他答案...' : '请输入您的答案...'}" />
<button class="custom-submit">提交</button>
</div>
\`;
// 只在未回答时添加事件监听
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);
}
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);
});
// 支持回车提交
customInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
const customValue = customInput.value.trim();
if (customValue) {
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
}
}
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 = \`
<div class="question-segment">
<div class="question-text">\${formatText(segment.question || '')}</div>
<div class="question-options">
\${(segment.options || []).map(opt => \`<span class="question-opt">\${opt}</span>\`).join('')}
</div>
</div>
\`;
const questions = segment.questions || [];
const questionsHtml = questions.map(q => {
const optionsHtml = q.options.map(opt => '<span class="question-opt">' + opt + '</span>').join('');
return '<div class="question-item">' +
'<div class="question-text">' + formatText(q.question) + '</div>' +
'<div class="question-options">' + optionsHtml + '</div>' +
'</div>';
}).join('');
segmentDiv.innerHTML = '<div class="question-segment">' + questionsHtml + '</div>';
} else if (segment.type === 'agent') {
// 智能体卡片渲染
renderAgentCard(segment, segmentDiv);