Files
IC-Coder-Plugin/src/services/userInteraction.ts
Roe-xin fa55e32153 feat: 支持 AskUserQuestion 多问题和多选功能
- 新增 QuestionItem 类型支持单个问题配置(question/options/multiSelect)
   - AskUserEvent 改为 questions 数组支持多问题
   - AnswerRequest 新增 answers 字段支持多问题答案提交
   - 前端渲染支持单选按钮(radio)和多选复选框(checkbox)
   - 答案格式:{\"0\": [\"选项1\"], \"1\": [\"选项A\", \"选项B\"]}
   - 保持向后兼容旧的单问题格式
2026-03-05 16:58:59 +08:00

208 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 用户交互处理器
* 处理 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();