feat(core): 实现 ask_user_question 工具的用户输入等待机制

- 创建 UserInputWaiter 管理用户输入等待状态
- 修改 agent-tool-executor 在 requiresUserInput 时等待用户回答
- 添加 onWaitingForInput 回调通知前端显示问题
- Server 端处理 waiting_for_input 广播和 user_input_response 消息
- 前端处理问题显示和用户回答提交
- 修复问题选项在流式输出时被禁用的问题
This commit is contained in:
2025-12-17 00:44:25 +08:00
parent a4e8037108
commit 8c46635dc7
13 changed files with 351 additions and 53 deletions
+12
View File
@@ -954,6 +954,18 @@ export interface ToolEndPayload {
duration?: number;
}
/** 等待用户输入事件 Payload */
export interface WaitingForInputPayload {
/** 工具调用 ID,用于匹配用户回答 */
id: string;
/** 工具名称 */
toolName: string;
/** 问题列表 */
questions: Question[];
/** 工具参数 */
arguments: Record<string, unknown>;
}
// ============ 子 Agent 事件 Payload ============
/** 子 Agent 开始事件 Payload */
+6 -3
View File
@@ -84,19 +84,22 @@ export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
);
case 'tool':
return <ToolPartItem key={part.id} part={part} />;
case 'question':
case 'question': {
// 问题组件:即使在流式输出时也允许用户回答(除非已回答)
const questionPart = part as QuestionMessagePart;
return (
<AskUserQuestion
key={part.id}
part={part as QuestionMessagePart}
part={questionPart}
onAnswer={
onAnswerQuestion
? (answers) => onAnswerQuestion(part.id, answers)
: undefined
}
disabled={isStreaming}
disabled={questionPart.answered}
/>
);
}
case 'reasoning':
return (
<div key={part.id} className="text-fg-muted italic border-l-2 border-line pl-3">
+56 -32
View File
@@ -11,6 +11,7 @@ import type {
ConfigErrorPayload,
ToolStartPayload,
ToolEndPayload,
WaitingForInputPayload,
MessagePart,
ToolMessagePart,
QuestionMessagePart,
@@ -320,6 +321,46 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
break;
}
case 'waiting_for_input': {
// 工具需要用户输入(如 ask_user_question
const payload = message.payload as WaitingForInputPayload;
setState((prev) => {
if (!prev.streamingMessage) return prev;
// 创建 QuestionMessagePart
const questionPart: QuestionMessagePart = {
type: 'question',
id: payload.id,
questions: payload.questions,
answered: false,
};
// 查找并替换对应的工具 part(如果存在),或添加新的
let found = false;
const parts = prev.streamingMessage.parts.map((part) => {
if (part.type === 'tool' && part.id === payload.id) {
found = true;
return questionPart;
}
return part;
});
// 如果没找到对应的工具 part,添加到末尾
if (!found) {
parts.push(questionPart);
}
return {
...prev,
streamingMessage: {
...prev.streamingMessage,
parts,
},
};
});
break;
}
case 'done':
setState((prev) => {
// 使用流式消息或创建新消息
@@ -655,6 +696,21 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
// 回答问题(ask_user_question 工具)
const answerQuestion = useCallback(
(questionPartId: string, answers: string[]) => {
// 发送用户输入响应
const answerText = answers.filter((a) => a).join('\n');
if (answerText && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: 'user_input_response',
sessionId,
payload: {
toolCallId: questionPartId,
answer: answerText,
},
})
);
}
// 更新问题状态为已回答
setState((prev) => {
// 更新流式消息中的问题
@@ -670,22 +726,6 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
return part;
});
// 发送回答作为用户消息
const answerText = answers.filter((a) => a).join('\n');
if (answerText && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: 'message',
sessionId,
payload: {
content: answerText,
agentMode: state.agentMode,
autoApprove: state.autoApprove,
},
})
);
}
return {
...prev,
streamingMessage: {
@@ -713,22 +753,6 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
return msg;
});
// 发送回答作为用户消息
const answerText = answers.filter((a) => a).join('\n');
if (answerText && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: 'message',
sessionId,
payload: {
content: answerText,
agentMode: state.agentMode,
autoApprove: state.autoApprove,
},
})
);
}
return { ...prev, messages };
});
},