feat(core): 实现 ask_user_question 工具的用户输入等待机制
- 创建 UserInputWaiter 管理用户输入等待状态 - 修改 agent-tool-executor 在 requiresUserInput 时等待用户回答 - 添加 onWaitingForInput 回调通知前端显示问题 - Server 端处理 waiting_for_input 广播和 user_input_response 消息 - 前端处理问题显示和用户回答提交 - 修复问题选项在流式输出时被禁用的问题
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user