- 新增 QuestionItem 类型支持单个问题配置(question/options/multiSelect)
- AskUserEvent 改为 questions 数组支持多问题
- AnswerRequest 新增 answers 字段支持多问题答案提交
- 前端渲染支持单选按钮(radio)和多选复选框(checkbox)
- 答案格式:{\"0\": [\"选项1\"], \"1\": [\"选项A\", \"选项B\"]}
- 保持向后兼容旧的单问题格式
208 lines
5.9 KiB
TypeScript
208 lines
5.9 KiB
TypeScript
/**
|
||
* 用户交互处理器
|
||
* 处理 ask_user 事件,通过 WebView 显示问题并收集用户回答
|
||
*/
|
||
import * as vscode from 'vscode';
|
||
import { submitAnswer, submitToolConfirm } from './apiClient';
|
||
import type { AskUserEvent, AnswerRequest, QuestionItem } from '../types/api';
|
||
|
||
/**
|
||
* 待处理的用户问题
|
||
*/
|
||
interface PendingQuestion {
|
||
askId: string;
|
||
taskId: string;
|
||
questions: QuestionItem[];
|
||
resolve: (answer: string) => void;
|
||
reject: (error: Error) => void;
|
||
}
|
||
|
||
/**
|
||
* 用户交互管理器
|
||
*/
|
||
export class UserInteractionManager {
|
||
private pendingQuestions = new Map<string, PendingQuestion>();
|
||
private webviewPanel: vscode.WebviewPanel | null = null;
|
||
|
||
/**
|
||
* 设置 WebView 面板(用于发送消息)
|
||
*/
|
||
setWebviewPanel(panel: vscode.WebviewPanel): void {
|
||
this.webviewPanel = panel;
|
||
}
|
||
|
||
/**
|
||
* 获取 WebView 面板
|
||
*/
|
||
getWebviewPanel(): vscode.WebviewPanel | null {
|
||
return this.webviewPanel;
|
||
}
|
||
|
||
/**
|
||
* 处理 ask_user 事件
|
||
* @param event ask_user 事件数据
|
||
* @param taskId 当前任务ID
|
||
*/
|
||
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
|
||
const { askId, questions } = event;
|
||
|
||
console.log(`[UserInteraction] 收到问题: askId=${askId}, count=${questions.length}`);
|
||
|
||
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
|
||
// 这里不再单独发送 showQuestion 命令,避免重复显示
|
||
|
||
// 创建 Promise 等待用户回答
|
||
return new Promise((resolve, reject) => {
|
||
this.pendingQuestions.set(askId, {
|
||
askId,
|
||
taskId,
|
||
questions,
|
||
resolve: (answer: string) => {
|
||
this.submitUserAnswer(askId, taskId, answer)
|
||
.then(() => resolve())
|
||
.catch(reject);
|
||
},
|
||
reject
|
||
});
|
||
|
||
// 设置超时(2小时)
|
||
setTimeout(() => {
|
||
if (this.pendingQuestions.has(askId)) {
|
||
this.pendingQuestions.delete(askId);
|
||
reject(new Error('用户回答超时'));
|
||
}
|
||
}, 7200000);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理用户提交的回答(从 WebView 调用)
|
||
* @param askId 问题ID
|
||
* @param selected 选中的选项(旧格式)
|
||
* @param customInput 自定义输入(旧格式)
|
||
* @param answers 新格式:按问题索引的答案
|
||
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||
*/
|
||
async receiveAnswer(
|
||
askId: string,
|
||
selected?: string[],
|
||
customInput?: string,
|
||
answers?: { [questionIndex: string]: string[] },
|
||
fallbackTaskId?: string
|
||
): Promise<void> {
|
||
const pending = this.pendingQuestions.get(askId);
|
||
|
||
// 构建答案字符串
|
||
let answer = '';
|
||
if (answers && Object.keys(answers).length > 0) {
|
||
// 新格式:多问题答案
|
||
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, answers);
|
||
} else {
|
||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log(`[UserInteraction] 收到用户回答: askId=${askId}, answer=${answer}`);
|
||
|
||
// 移除待处理问题
|
||
this.pendingQuestions.delete(askId);
|
||
|
||
// 触发 resolve
|
||
pending.resolve(answer);
|
||
}
|
||
|
||
/**
|
||
* 提交用户回答到后端
|
||
*/
|
||
private async submitUserAnswer(
|
||
askId: string,
|
||
taskId: string,
|
||
answer: string,
|
||
answers?: { [questionIndex: string]: string[] }
|
||
): Promise<void> {
|
||
// 检查是否是工具确认类型的问题
|
||
if (askId.startsWith('tool_confirm_')) {
|
||
// 提取 confirmId
|
||
const confirmId = parseInt(askId.replace('tool_confirm_', ''));
|
||
const approved = answer === '确认执行';
|
||
|
||
console.log(`[UserInteraction] 提交工具确认: confirmId=${confirmId}, approved=${approved}`);
|
||
|
||
try {
|
||
const response = await submitToolConfirm({
|
||
confirmId,
|
||
taskId,
|
||
approved
|
||
});
|
||
if (!response.success) {
|
||
throw new Error(response.error || '提交工具确认失败');
|
||
}
|
||
console.log(`[UserInteraction] 工具确认已提交: confirmId=${confirmId}`);
|
||
} catch (error) {
|
||
console.error(`[UserInteraction] 提交工具确认失败: confirmId=${confirmId}`, error);
|
||
throw error;
|
||
}
|
||
} else {
|
||
// 普通问题回答
|
||
const request: AnswerRequest = {
|
||
askId,
|
||
taskId,
|
||
answers: answers,
|
||
customInput: answers ? undefined : answer
|
||
};
|
||
|
||
try {
|
||
const response = await submitAnswer(request);
|
||
if (!response.success) {
|
||
throw new Error(response.error || '提交回答失败');
|
||
}
|
||
console.log(`[UserInteraction] 回答已提交: askId=${askId}`);
|
||
} catch (error) {
|
||
console.error(`[UserInteraction] 提交回答失败: askId=${askId}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 取消所有待处理的问题
|
||
*/
|
||
cancelAll(): void {
|
||
for (const [askId, pending] of this.pendingQuestions) {
|
||
pending.reject(new Error('用户交互已取消'));
|
||
}
|
||
this.pendingQuestions.clear();
|
||
}
|
||
|
||
/**
|
||
* 检查是否有待处理的问题
|
||
*/
|
||
hasPendingQuestions(): boolean {
|
||
return this.pendingQuestions.size > 0;
|
||
}
|
||
|
||
/**
|
||
* 检查特定问题是否存在
|
||
*/
|
||
hasPendingQuestion(askId: string): boolean {
|
||
return this.pendingQuestions.has(askId);
|
||
}
|
||
}
|
||
|
||
// 全局实例
|
||
export const userInteractionManager = new UserInteractionManager();
|