feat: 支持 AskUserQuestion 多问题和多选功能
- 新增 QuestionItem 类型支持单个问题配置(question/options/multiSelect)
- AskUserEvent 改为 questions 数组支持多问题
- AnswerRequest 新增 answers 字段支持多问题答案提交
- 前端渲染支持单选按钮(radio)和多选复选框(checkbox)
- 答案格式:{\"0\": [\"选项1\"], \"1\": [\"选项A\", \"选项B\"]}
- 保持向后兼容旧的单问题格式
This commit is contained in:
@ -436,6 +436,7 @@ export async function showICHelperPanel(
|
|||||||
message.askId,
|
message.askId,
|
||||||
message.selected,
|
message.selected,
|
||||||
message.customInput,
|
message.customInput,
|
||||||
|
message.answers
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
// 新增:中止对话
|
// 新增:中止对话
|
||||||
|
|||||||
@ -43,8 +43,7 @@ export interface MessageSegment {
|
|||||||
toolResult?: string;
|
toolResult?: string;
|
||||||
toolDescription?: string;
|
toolDescription?: string;
|
||||||
askId?: string;
|
askId?: string;
|
||||||
question?: string;
|
questions?: import("../types/api").QuestionItem[];
|
||||||
options?: string[];
|
|
||||||
// 智能体相关字段
|
// 智能体相关字段
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
agentName?: string;
|
agentName?: string;
|
||||||
@ -97,7 +96,7 @@ export interface DialogCallbacks {
|
|||||||
summary: string
|
summary: string
|
||||||
) => void;
|
) => void;
|
||||||
/** 显示问题(ask_user) */
|
/** 显示问题(ask_user) */
|
||||||
onQuestion?: (askId: string, question: string, options: string[]) => void;
|
onQuestion?: (askId: string, questions: import("../types/api").QuestionItem[]) => void;
|
||||||
/** 实时更新段落(流式过程中) */
|
/** 实时更新段落(流式过程中) */
|
||||||
onSegmentUpdate?: (segments: MessageSegment[]) => void;
|
onSegmentUpdate?: (segments: MessageSegment[]) => void;
|
||||||
/** 对话完成,返回所有段落 */
|
/** 对话完成,返回所有段落 */
|
||||||
@ -647,8 +646,11 @@ export class DialogSession {
|
|||||||
this.segments.push({
|
this.segments.push({
|
||||||
type: "question",
|
type: "question",
|
||||||
askId: askId,
|
askId: askId,
|
||||||
question: question,
|
questions: [{
|
||||||
options: ["确认执行", "取消"],
|
question: question,
|
||||||
|
options: ["确认执行", "取消"],
|
||||||
|
multiSelect: false
|
||||||
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 实时发送段落更新
|
// 实时发送段落更新
|
||||||
@ -666,8 +668,11 @@ export class DialogSession {
|
|||||||
await userInteractionManager.handleAskUser(
|
await userInteractionManager.handleAskUser(
|
||||||
{
|
{
|
||||||
askId: askId,
|
askId: askId,
|
||||||
question: question,
|
questions: [{
|
||||||
options: ["确认执行", "取消"],
|
question: question,
|
||||||
|
options: ["确认执行", "取消"],
|
||||||
|
multiSelect: false
|
||||||
|
}]
|
||||||
} as AskUserEvent,
|
} as AskUserEvent,
|
||||||
this.taskId
|
this.taskId
|
||||||
);
|
);
|
||||||
@ -714,8 +719,11 @@ export class DialogSession {
|
|||||||
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
// 注册问题到前端(类似 askUser),以便用户回答时能找到
|
||||||
const planEvent = {
|
const planEvent = {
|
||||||
askId: askId,
|
askId: askId,
|
||||||
question: `请确认执行计划:${data.title}`,
|
questions: [{
|
||||||
options: ["确认执行", "修改计划", "取消"],
|
question: `请确认执行计划:${data.title}`,
|
||||||
|
options: ["确认执行", "修改计划", "取消"],
|
||||||
|
multiSelect: false
|
||||||
|
}]
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await userInteractionManager.handleAskUser(
|
await userInteractionManager.handleAskUser(
|
||||||
@ -856,13 +864,12 @@ export class DialogSession {
|
|||||||
this.segments.push({
|
this.segments.push({
|
||||||
type: "question",
|
type: "question",
|
||||||
askId: data.askId,
|
askId: data.askId,
|
||||||
question: data.question,
|
questions: data.questions,
|
||||||
options: data.options,
|
|
||||||
});
|
});
|
||||||
// 实时发送段落更新(包含问题)
|
// 实时发送段落更新(包含问题)
|
||||||
callbacks.onSegmentUpdate?.(this.segments);
|
callbacks.onSegmentUpdate?.(this.segments);
|
||||||
// 同时调用 onQuestion 用于更新状态栏等
|
// 同时调用 onQuestion 用于更新状态栏等
|
||||||
callbacks.onQuestion?.(data.askId, data.question, data.options);
|
callbacks.onQuestion?.(data.askId, data.questions);
|
||||||
try {
|
try {
|
||||||
await userInteractionManager.handleAskUser(data, this.taskId);
|
await userInteractionManager.handleAskUser(data, this.taskId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -1110,7 +1117,8 @@ export class DialogSession {
|
|||||||
async submitAnswer(
|
async submitAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string
|
customInput?: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
// 直接调用 receiveAnswer,传递 taskId 作为 fallbackTaskId
|
||||||
// 如果 pendingQuestions 中有问题,走正常流程
|
// 如果 pendingQuestions 中有问题,走正常流程
|
||||||
@ -1119,6 +1127,7 @@ export class DialogSession {
|
|||||||
askId,
|
askId,
|
||||||
selected,
|
selected,
|
||||||
customInput,
|
customInput,
|
||||||
|
answers,
|
||||||
this.taskId
|
this.taskId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import { submitAnswer, submitToolConfirm } from './apiClient';
|
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 {
|
interface PendingQuestion {
|
||||||
askId: string;
|
askId: string;
|
||||||
taskId: string;
|
taskId: string;
|
||||||
question: string;
|
questions: QuestionItem[];
|
||||||
options: string[];
|
|
||||||
resolve: (answer: string) => void;
|
resolve: (answer: string) => void;
|
||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
}
|
}
|
||||||
@ -45,9 +44,9 @@ export class UserInteractionManager {
|
|||||||
* @param taskId 当前任务ID
|
* @param taskId 当前任务ID
|
||||||
*/
|
*/
|
||||||
async handleAskUser(event: AskUserEvent, taskId: string): Promise<void> {
|
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 统一处理
|
// 注意:问题显示已经通过 dialogService 的 onSegmentUpdate 统一处理
|
||||||
// 这里不再单独发送 showQuestion 命令,避免重复显示
|
// 这里不再单独发送 showQuestion 命令,避免重复显示
|
||||||
@ -57,8 +56,7 @@ export class UserInteractionManager {
|
|||||||
this.pendingQuestions.set(askId, {
|
this.pendingQuestions.set(askId, {
|
||||||
askId,
|
askId,
|
||||||
taskId,
|
taskId,
|
||||||
question,
|
questions,
|
||||||
options,
|
|
||||||
resolve: (answer: string) => {
|
resolve: (answer: string) => {
|
||||||
this.submitUserAnswer(askId, taskId, answer)
|
this.submitUserAnswer(askId, taskId, answer)
|
||||||
.then(() => resolve())
|
.then(() => resolve())
|
||||||
@ -80,24 +78,38 @@ export class UserInteractionManager {
|
|||||||
/**
|
/**
|
||||||
* 处理用户提交的回答(从 WebView 调用)
|
* 处理用户提交的回答(从 WebView 调用)
|
||||||
* @param askId 问题ID
|
* @param askId 问题ID
|
||||||
* @param selected 选中的选项
|
* @param selected 选中的选项(旧格式)
|
||||||
* @param customInput 自定义输入
|
* @param customInput 自定义输入(旧格式)
|
||||||
|
* @param answers 新格式:按问题索引的答案
|
||||||
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
* @param fallbackTaskId 当问题不存在时使用的 taskId(用于直接发送到后端)
|
||||||
*/
|
*/
|
||||||
async receiveAnswer(
|
async receiveAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string,
|
customInput?: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] },
|
||||||
fallbackTaskId?: string
|
fallbackTaskId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pending = this.pendingQuestions.get(askId);
|
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 (!pending) {
|
||||||
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
// 问题不存在(可能是页面刷新或会话切换后),尝试直接发送到后端
|
||||||
if (fallbackTaskId) {
|
if (fallbackTaskId) {
|
||||||
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
console.log(`[UserInteraction] 问题不在 pendingQuestions 中,直接发送到后端: askId=${askId}, taskId=${fallbackTaskId}`);
|
||||||
await this.submitUserAnswer(askId, fallbackTaskId, answer);
|
await this.submitUserAnswer(askId, fallbackTaskId, answer, answers);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
console.warn(`[UserInteraction] 问题不存在且无 fallbackTaskId: askId=${askId}`);
|
||||||
}
|
}
|
||||||
@ -119,7 +131,8 @@ export class UserInteractionManager {
|
|||||||
private async submitUserAnswer(
|
private async submitUserAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
answer: string
|
answer: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// 检查是否是工具确认类型的问题
|
// 检查是否是工具确认类型的问题
|
||||||
if (askId.startsWith('tool_confirm_')) {
|
if (askId.startsWith('tool_confirm_')) {
|
||||||
@ -148,7 +161,8 @@ export class UserInteractionManager {
|
|||||||
const request: AnswerRequest = {
|
const request: AnswerRequest = {
|
||||||
askId,
|
askId,
|
||||||
taskId,
|
taskId,
|
||||||
customInput: answer
|
answers: answers,
|
||||||
|
customInput: answers ? undefined : answer
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -194,11 +194,17 @@ export interface PlanSummaryUpdateEvent {
|
|||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 单个问题项 */
|
||||||
|
export interface QuestionItem {
|
||||||
|
question: string;
|
||||||
|
options: string[];
|
||||||
|
multiSelect?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** ask_user 事件数据 */
|
/** ask_user 事件数据 */
|
||||||
export interface AskUserEvent {
|
export interface AskUserEvent {
|
||||||
askId: string;
|
askId: string;
|
||||||
question: string;
|
questions: QuestionItem[];
|
||||||
options: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** complete 事件数据 */
|
/** complete 事件数据 */
|
||||||
@ -351,10 +357,12 @@ export interface AnswerRequest {
|
|||||||
askId: string;
|
askId: string;
|
||||||
/** 任务ID */
|
/** 任务ID */
|
||||||
taskId: string;
|
taskId: string;
|
||||||
/** 选中的选项列表 */
|
/** 选中的选项列表(旧格式,兼容) */
|
||||||
selected?: string[];
|
selected?: string[];
|
||||||
/** 自定义输入内容 */
|
/** 自定义输入内容(旧格式,兼容) */
|
||||||
customInput?: string;
|
customInput?: string;
|
||||||
|
/** 新格式:按问题索引的答案 */
|
||||||
|
answers?: { [questionIndex: string]: string[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 用户回答响应 */
|
/** 用户回答响应 */
|
||||||
|
|||||||
@ -469,10 +469,11 @@ async function handleUserMessageWithBackend(
|
|||||||
export async function handleUserAnswer(
|
export async function handleUserAnswer(
|
||||||
askId: string,
|
askId: string,
|
||||||
selected?: string[],
|
selected?: string[],
|
||||||
customInput?: string
|
customInput?: string,
|
||||||
|
answers?: { [questionIndex: string]: string[] }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (currentSession) {
|
if (currentSession) {
|
||||||
await currentSession.submitAnswer(askId, selected, customInput);
|
await currentSession.submitAnswer(askId, selected, customInput, answers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -129,7 +129,8 @@ export function showICHelperPanel(context: vscode.ExtensionContext) {
|
|||||||
handleUserAnswer(
|
handleUserAnswer(
|
||||||
message.askId,
|
message.askId,
|
||||||
message.selected,
|
message.selected,
|
||||||
message.customInput
|
message.customInput,
|
||||||
|
message.answers
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
// 新增:中止对话
|
// 新增:中止对话
|
||||||
|
|||||||
@ -1188,64 +1188,60 @@ export function getMessageAreaScript(): string {
|
|||||||
} else if (segment.type === 'question') {
|
} else if (segment.type === 'question') {
|
||||||
segmentDiv.className += ' segment-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 isAnswered = answeredQuestions.has(segment.askId);
|
||||||
const selectedAnswer = answeredQuestions.get(segment.askId);
|
const savedAnswers = answeredQuestions.get(segment.askId) || {};
|
||||||
|
|
||||||
if (isAnswered) {
|
if (isAnswered) {
|
||||||
segmentDiv.classList.add('answered');
|
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
|
const optionsHtml = q.options.map(opt => {
|
||||||
? (segment.options || []).map(opt => {
|
const isSelected = selectedAnswers.includes(opt);
|
||||||
const isSelected = isAnswered && opt === selectedAnswer;
|
return \`<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:4px 0;">
|
||||||
return \`<button class="question-option\${isSelected ? ' selected' : ''}" data-option="\${opt}">\${opt}</button>\`;
|
<input type="\${inputType}" name="\${inputName}" value="\${opt}" \${isSelected ? 'checked' : ''} \${isAnswered ? 'disabled' : ''}>
|
||||||
}).join('')
|
<span>\${opt}</span>
|
||||||
: '';
|
</label>\`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return \`
|
||||||
|
<div class="question-item" data-question-index="\${qIndex}" style="margin-bottom:12px;">
|
||||||
|
<div class="question-text" style="margin-bottom:8px;">\${formatText(q.question)}</div>
|
||||||
|
<div class="question-options">\${optionsHtml}</div>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
segmentDiv.innerHTML = \`
|
segmentDiv.innerHTML = \`
|
||||||
<div class="question-text">\${formatText(segment.question || '')}</div>
|
\${questionsHtml}
|
||||||
\${hasOptions ? \`<div class="question-options" data-ask-id="\${segment.askId}">\${optionsHtml}</div>\` : ''}
|
<button class="custom-submit" style="display:\${isAnswered ? 'none' : 'block'};margin-top:8px;padding:8px 16px;background:var(--vscode-button-background);color:var(--vscode-button-foreground);border:none;border-radius:6px;cursor:pointer;">提交答案</button>
|
||||||
<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) {
|
if (!isAnswered) {
|
||||||
setTimeout(() => {
|
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 submitBtn = segmentDiv.querySelector('.custom-submit');
|
||||||
const customInput = segmentDiv.querySelector('.custom-input');
|
if (submitBtn) {
|
||||||
if (submitBtn && customInput) {
|
|
||||||
submitBtn.addEventListener('click', function() {
|
submitBtn.addEventListener('click', function() {
|
||||||
const customValue = customInput.value.trim();
|
const answers = {};
|
||||||
if (customValue) {
|
questions.forEach((q, qIndex) => {
|
||||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
const inputs = segmentDiv.querySelectorAll(\`input[name="q\${qIndex}"]:checked\`);
|
||||||
}
|
answers[qIndex] = Array.from(inputs).map(input => input.value);
|
||||||
});
|
});
|
||||||
|
handleMultiQuestionAnswer(segment.askId, answers, segmentDiv);
|
||||||
// 支持回车提交
|
|
||||||
customInput.addEventListener('keypress', function(e) {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
const customValue = customInput.value.trim();
|
|
||||||
if (customValue) {
|
|
||||||
handleQuestionAnswerInSegment(segment.askId, customValue, segmentDiv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 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()}
|
${getWaveformPreviewScript()}
|
||||||
|
|
||||||
${getCodeHighlightScript()}
|
${getCodeHighlightScript()}
|
||||||
|
|||||||
Reference in New Issue
Block a user