diff --git a/src/panels/ICHelperPanel.ts b/src/panels/ICHelperPanel.ts index a34155f..bd9e660 100644 --- a/src/panels/ICHelperPanel.ts +++ b/src/panels/ICHelperPanel.ts @@ -436,6 +436,7 @@ export async function showICHelperPanel( message.askId, message.selected, message.customInput, + message.answers ); break; // 新增:中止对话 diff --git a/src/services/dialogService.ts b/src/services/dialogService.ts index b4298c9..585e512 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; @@ -97,7 +96,7 @@ export interface DialogCallbacks { summary: string ) => void; /** 显示问题(ask_user) */ - onQuestion?: (askId: string, question: string, options: string[]) => void; + onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void; /** 实时更新段落(流式过程中) */ onSegmentUpdate?: (segments: MessageSegment[]) => void; /** 对话完成,返回所有段落 */ @@ -647,8 +646,11 @@ export class DialogSession { this.segments.push({ type: "question", askId: askId, - question: question, - options: ["确认执行", "取消"], + questions: [{ + question: question, + options: ["确认执行", "取消"], + multiSelect: false + }], }); // 实时发送段落更新 @@ -666,8 +668,11 @@ export class DialogSession { await userInteractionManager.handleAskUser( { askId: askId, - question: question, - options: ["确认执行", "取消"], + questions: [{ + question: question, + options: ["确认执行", "取消"], + multiSelect: false + }] } as AskUserEvent, this.taskId ); @@ -714,8 +719,11 @@ export class DialogSession { // 注册问题到前端(类似 askUser),以便用户回答时能找到 const planEvent = { askId: askId, - question: `请确认执行计划:${data.title}`, - options: ["确认执行", "修改计划", "取消"], + questions: [{ + question: `请确认执行计划:${data.title}`, + options: ["确认执行", "修改计划", "取消"], + multiSelect: false + }] }; try { await userInteractionManager.handleAskUser( @@ -856,13 +864,12 @@ 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); + callbacks.onQuestion?.(data.askId, data.questions); try { await userInteractionManager.handleAskUser(data, this.taskId); } catch (error) { @@ -1110,7 +1117,8 @@ export class DialogSession { async submitAnswer( askId: string, selected?: string[], - customInput?: string + customInput?: string, + answers?: { [questionIndex: string]: string[] } ): Promise { // 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId // 如果 pendingQuestions 中有问题,走正常流程 @@ -1119,6 +1127,7 @@ export class DialogSession { askId, selected, customInput, + answers, this.taskId ); } diff --git a/src/services/userInteraction.ts b/src/services/userInteraction.ts index 0885b35..3ba8d68 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,9 +44,9 @@ 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 命令,避免重复显示 @@ -57,8 +56,7 @@ export class UserInteractionManager { this.pendingQuestions.set(askId, { askId, taskId, - question, - options, + questions, resolve: (answer: string) => { this.submitUserAnswer(askId, taskId, answer) .then(() => resolve()) @@ -80,24 +78,38 @@ export class UserInteractionManager { /** * 处理用户提交的回答(从 WebView 调用) * @param askId 问题ID - * @param selected 选中的选项 - * @param customInput 自定义输入 + * @param selected 选中的选项(旧格式) + * @param customInput 自定义输入(旧格式) + * @param answers 新格式:按问题索引的答案 * @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端) */ async receiveAnswer( askId: string, selected?: string[], customInput?: string, + answers?: { [questionIndex: string]: string[] }, fallbackTaskId?: string ): Promise { const pending = this.pendingQuestions.get(askId); - const answer = customInput || selected?.join(', ') || ''; + + // 构建答案字符串 + 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); + await this.submitUserAnswer(askId, fallbackTaskId, answer, answers); } else { console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`); } @@ -119,7 +131,8 @@ export class UserInteractionManager { private async submitUserAnswer( askId: string, taskId: string, - answer: string + answer: string, + answers?: { [questionIndex: string]: string[] } ): Promise { // 检查是否是工具确认类型的问题 if (askId.startsWith('tool_confirm_')) { @@ -148,7 +161,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..41a6c97 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; + /** 新格式:按问题索引的答案 */ + answers?: { [questionIndex: string]: string[] }; } /** 用户回答响应 */ diff --git a/src/utils/messageHandler.ts b/src/utils/messageHandler.ts index 72ff7ff..6f9392e 100644 --- a/src/utils/messageHandler.ts +++ b/src/utils/messageHandler.ts @@ -469,10 +469,11 @@ async function handleUserMessageWithBackend( export async function handleUserAnswer( askId: string, selected?: string[], - customInput?: string + customInput?: string, + answers?: { [questionIndex: string]: string[] } ): Promise { if (currentSession) { - await currentSession.submitAnswer(askId, selected, customInput); + await currentSession.submitAnswer(askId, selected, customInput, answers); } } diff --git a/src/views/ICViewProvider.ts b/src/views/ICViewProvider.ts index d612458..d2b90a4 100644 --- a/src/views/ICViewProvider.ts +++ b/src/views/ICViewProvider.ts @@ -129,7 +129,8 @@ export function showICHelperPanel(context: vscode.ExtensionContext) { handleUserAnswer( message.askId, message.selected, - message.customInput + message.customInput, + message.answers ); break; // 新增:中止对话 diff --git a/src/views/messageArea.ts b/src/views/messageArea.ts index ded1e5e..5c50d2a 100644 --- a/src/views/messageArea.ts +++ b/src/views/messageArea.ts @@ -1188,64 +1188,60 @@ export function getMessageAreaScript(): string { } else if (segment.type === 'question') { segmentDiv.className += ' segment-question'; + // 兼容旧格式:如果有 segment.question,转换为 questions 数组 + const questions = segment.questions || (segment.question ? [{ + question: segment.question, + options: segment.options || [], + multiSelect: false + }] : []); + // 检查是否已回答 const isAnswered = answeredQuestions.has(segment.askId); - const selectedAnswer = answeredQuestions.get(segment.askId); + const savedAnswers = answeredQuestions.get(segment.askId) || {}; if (isAnswered) { segmentDiv.classList.add('answered'); } - // 检查是否有选项 - const hasOptions = segment.options && segment.options.length > 0; + // 渲染多个问题 + const questionsHtml = questions.map((q, qIndex) => { + const inputType = q.multiSelect ? 'checkbox' : 'radio'; + const inputName = \`q\${qIndex}\`; + const selectedAnswers = savedAnswers[qIndex] || []; - const optionsHtml = hasOptions - ? (segment.options || []).map(opt => { - const isSelected = isAnswered && opt === selectedAnswer; - return \`\`; - }).join('') - : ''; + const optionsHtml = q.options.map(opt => { + const isSelected = selectedAnswers.includes(opt); + return \`\`; + }).join(''); + + return \` +
+
\${formatText(q.question)}
+
\${optionsHtml}
+
+ \`; + }).join(''); segmentDiv.innerHTML = \` -
\${formatText(segment.question || '')}
- \${hasOptions ? \`
\${optionsHtml}
\` : ''} -
- - -
+ \${questionsHtml} + \`; // 只在未回答时添加事件监听 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, qIndex) => { + const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`); + answers[qIndex] = Array.from(inputs).map(input => input.value); + }); + handleMultiQuestionAnswer(segment.askId, answers, segmentDiv); }); } }, 0); @@ -1723,6 +1719,34 @@ export function getMessageAreaScript(): string { }); } + // 处理多问题答案提交 + function handleMultiQuestionAnswer(askId, answers, segmentDiv) { + console.log('[WebView] 多问题答案提交:', askId, answers); + + // 保存答案到 Map 中 + answeredQuestions.set(askId, answers); + + // 标记问题已回答 + segmentDiv.classList.add('answered'); + + // 禁用所有输入 + const inputs = segmentDiv.querySelectorAll('input'); + inputs.forEach(input => input.disabled = true); + + // 隐藏提交按钮 + const submitBtn = segmentDiv.querySelector('.custom-submit'); + if (submitBtn) { + submitBtn.style.display = 'none'; + } + + // 发送答案到后端 + vscode.postMessage({ + command: 'submitAnswer', + askId: askId, + answers: answers + }); + } + ${getWaveformPreviewScript()} ${getCodeHighlightScript()}