fix: delay tool blocks until text fully streamed

This commit is contained in:
JOJO 2025-12-14 14:50:20 +08:00
parent 6d330b1388
commit 55af5b52c6

View File

@ -37,6 +37,32 @@ export async function initializeLegacySocket(ctx: any) {
ignoreThinking: false ignoreThinking: false
}; };
const pendingToolEvents: Array<{ event: string; data: any; handler: () => void }> = [];
const hasPendingStreamingText = () =>
!!streamingState.buffer.length ||
!!streamingState.pendingCompleteContent ||
streamingState.timer !== null ||
streamingState.completionTimer !== null;
const resetPendingToolEvents = () => {
pendingToolEvents.length = 0;
};
const flushPendingToolEvents = () => {
if (!pendingToolEvents.length) {
return;
}
const queue = pendingToolEvents.splice(0);
queue.forEach((item) => {
try {
item.handler();
} catch (error) {
console.warn('延后工具事件处理失败:', item.event, error);
}
});
};
const snapshotStreamingState = () => ({ const snapshotStreamingState = () => ({
bufferLength: streamingState.buffer.length, bufferLength: streamingState.buffer.length,
timerActive: streamingState.timer !== null, timerActive: streamingState.timer !== null,
@ -297,6 +323,7 @@ export async function initializeLegacySocket(ctx: any) {
streamingState.activeMessageIndex = null; streamingState.activeMessageIndex = null;
streamingState.activeTextAction = null; streamingState.activeTextAction = null;
markStreamingIdleIfPossible('finalizeStreamingText'); markStreamingIdleIfPossible('finalizeStreamingText');
flushPendingToolEvents();
return true; return true;
}; };
@ -326,6 +353,7 @@ export async function initializeLegacySocket(ctx: any) {
streamingState.renderedText = ''; streamingState.renderedText = '';
streamingState.activeMessageIndex = null; streamingState.activeMessageIndex = null;
streamingState.activeTextAction = null; streamingState.activeTextAction = null;
resetPendingToolEvents();
logStreamingDebug('resetStreamingBuffer', snapshotStreamingState()); logStreamingDebug('resetStreamingBuffer', snapshotStreamingState());
}; };
@ -658,6 +686,7 @@ export async function initializeLegacySocket(ctx: any) {
socketLog('AI消息开始'); socketLog('AI消息开始');
logStreamingDebug('socket:ai_message_start'); logStreamingDebug('socket:ai_message_start');
finalizeStreamingText({ force: true }); finalizeStreamingText({ force: true });
flushPendingToolEvents();
resetStreamingBuffer(); resetStreamingBuffer();
ctx.monitorResetSpeech(); ctx.monitorResetSpeech();
ctx.cleanupStaleToolActions(); ctx.cleanupStaleToolActions();
@ -807,34 +836,41 @@ export async function initializeLegacySocket(ctx: any) {
socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id); socketLog('跳过tool_preparing(对话不匹配)', data.conversation_id);
return; return;
} }
const msg = ctx.chatEnsureAssistantMessage(); const handler = () => {
if (!msg) { const msg = ctx.chatEnsureAssistantMessage();
return; if (!msg) {
} return;
const action = { }
id: data.id, const action = {
type: 'tool',
tool: {
id: data.id, id: data.id,
name: data.name, type: 'tool',
arguments: {}, tool: {
argumentSnapshot: null, id: data.id,
argumentLabel: '', name: data.name,
status: 'preparing', arguments: {},
result: null, argumentSnapshot: null,
message: data.message || `准备调用 ${data.name}...` argumentLabel: '',
}, status: 'preparing',
timestamp: Date.now() result: null,
message: data.message || `准备调用 ${data.name}...`
},
timestamp: Date.now()
};
msg.actions.push(action);
ctx.preparingTools.set(data.id, action);
ctx.toolRegisterAction(action, data.id);
ctx.toolTrackAction(data.name, action);
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
if (ctx.monitorPreviewTool) {
ctx.monitorPreviewTool(data);
}
}; };
msg.actions.push(action);
ctx.preparingTools.set(data.id, action); if (hasPendingStreamingText()) {
ctx.toolRegisterAction(action, data.id); pendingToolEvents.push({ event: 'tool_preparing', data, handler });
ctx.toolTrackAction(data.name, action); } else {
ctx.$forceUpdate(); handler();
ctx.conditionalScrollToBottom();
// 虚拟显示器在模型检测到工具时立即展示“正在XX”预览
if (ctx.monitorPreviewTool) {
ctx.monitorPreviewTool(data);
} }
}); });
@ -868,46 +904,54 @@ export async function initializeLegacySocket(ctx: any) {
socketLog('跳过tool_start(对话不匹配)', data.conversation_id); socketLog('跳过tool_start(对话不匹配)', data.conversation_id);
return; return;
} }
let action = null; const handler = () => {
if (data.preparing_id && ctx.preparingTools.has(data.preparing_id)) { let action = null;
action = ctx.preparingTools.get(data.preparing_id); if (data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
ctx.preparingTools.delete(data.preparing_id); action = ctx.preparingTools.get(data.preparing_id);
} else { ctx.preparingTools.delete(data.preparing_id);
action = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id); } else {
} action = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
if (!action) {
const msg = ctx.chatEnsureAssistantMessage();
if (!msg) {
return;
} }
action = { if (!action) {
id: data.id, const msg = ctx.chatEnsureAssistantMessage();
type: 'tool', if (!msg) {
tool: { return;
id: data.id, }
name: data.name, action = {
arguments: {}, id: data.id,
argumentSnapshot: null, type: 'tool',
argumentLabel: '', tool: {
status: 'running', id: data.id,
result: null name: data.name,
}, arguments: {},
timestamp: Date.now() argumentSnapshot: null,
}; argumentLabel: '',
msg.actions.push(action); status: 'running',
result: null
},
timestamp: Date.now()
};
msg.actions.push(action);
}
action.tool.status = 'running';
action.tool.arguments = data.arguments;
action.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
action.tool.argumentLabel = ctx.buildToolLabel(action.tool.argumentSnapshot);
action.tool.message = null;
action.tool.id = data.id;
action.tool.executionId = data.id;
ctx.toolRegisterAction(action, data.id);
ctx.toolTrackAction(data.name, action);
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
ctx.monitorQueueTool(data);
};
if (hasPendingStreamingText()) {
pendingToolEvents.push({ event: 'tool_start', data, handler });
} else {
handler();
} }
action.tool.status = 'running';
action.tool.arguments = data.arguments;
action.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
action.tool.argumentLabel = ctx.buildToolLabel(action.tool.argumentSnapshot);
action.tool.message = null;
action.tool.id = data.id;
action.tool.executionId = data.id;
ctx.toolRegisterAction(action, data.id);
ctx.toolTrackAction(data.name, action);
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
ctx.monitorQueueTool(data);
}); });
// 更新action工具完成 // 更新action工具完成
@ -917,82 +961,89 @@ export async function initializeLegacySocket(ctx: any) {
socketLog('跳过update_action(对话不匹配)', data.conversation_id); socketLog('跳过update_action(对话不匹配)', data.conversation_id);
return; return;
} }
let targetAction = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id); const handler = () => {
if (!targetAction && data.preparing_id && ctx.preparingTools.has(data.preparing_id)) { let targetAction = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id);
targetAction = ctx.preparingTools.get(data.preparing_id); if (!targetAction && data.preparing_id && ctx.preparingTools.has(data.preparing_id)) {
} targetAction = ctx.preparingTools.get(data.preparing_id);
if (!targetAction) { }
outer: for (const message of ctx.messages) { if (!targetAction) {
if (!message.actions) continue; outer: for (const message of ctx.messages) {
for (const action of message.actions) { if (!message.actions) continue;
if (action.type !== 'tool') continue; for (const action of message.actions) {
const matchByExecution = action.tool.executionId && action.tool.executionId === data.id; if (action.type !== 'tool') continue;
const matchByToolId = action.tool.id === data.id; const matchByExecution = action.tool.executionId && action.tool.executionId === data.id;
const matchByPreparingId = action.id === data.preparing_id; const matchByToolId = action.tool.id === data.id;
if (matchByExecution || matchByToolId || matchByPreparingId) { const matchByPreparingId = action.id === data.preparing_id;
targetAction = action; if (matchByExecution || matchByToolId || matchByPreparingId) {
break outer; targetAction = action;
break outer;
}
} }
} }
} }
} if (targetAction) {
if (targetAction) { if (data.status) {
if (data.status) { targetAction.tool.status = data.status;
targetAction.tool.status = data.status;
}
if (data.result !== undefined) {
targetAction.tool.result = data.result;
}
if (targetAction.tool && targetAction.tool.name === 'trigger_easter_egg' && data.result !== undefined) {
const eggPromise = ctx.handleEasterEggPayload(data.result);
if (eggPromise && typeof eggPromise.catch === 'function') {
eggPromise.catch((error) => {
console.warn('彩蛋处理异常:', error);
});
} }
} if (data.result !== undefined) {
if (data.message !== undefined) { targetAction.tool.result = data.result;
targetAction.tool.message = data.message;
}
if (data.awaiting_content) {
targetAction.tool.awaiting_content = true;
} else if (data.status === 'completed') {
targetAction.tool.awaiting_content = false;
}
if (!targetAction.tool.executionId && (data.execution_id || data.id)) {
targetAction.tool.executionId = data.execution_id || data.id;
}
if (data.arguments) {
targetAction.tool.arguments = data.arguments;
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot);
}
ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
ctx.toolUnregisterAction(targetAction);
if (data.id) {
ctx.preparingTools.delete(data.id);
} }
if (data.preparing_id) { if (targetAction.tool && targetAction.tool.name === 'trigger_easter_egg' && data.result !== undefined) {
ctx.preparingTools.delete(data.preparing_id); const eggPromise = ctx.handleEasterEggPayload(data.result);
if (eggPromise && typeof eggPromise.catch === 'function') {
eggPromise.catch((error) => {
console.warn('彩蛋处理异常:', error);
});
}
} }
if (data.message !== undefined) {
targetAction.tool.message = data.message;
}
if (data.awaiting_content) {
targetAction.tool.awaiting_content = true;
} else if (data.status === 'completed') {
targetAction.tool.awaiting_content = false;
}
if (!targetAction.tool.executionId && (data.execution_id || data.id)) {
targetAction.tool.executionId = data.execution_id || data.id;
}
if (data.arguments) {
targetAction.tool.arguments = data.arguments;
targetAction.tool.argumentSnapshot = ctx.cloneToolArguments(data.arguments);
targetAction.tool.argumentLabel = ctx.buildToolLabel(targetAction.tool.argumentSnapshot);
}
ctx.toolRegisterAction(targetAction, data.execution_id || data.id);
if (data.status && ["completed", "failed", "error"].includes(data.status) && !data.awaiting_content) {
ctx.toolUnregisterAction(targetAction);
if (data.id) {
ctx.preparingTools.delete(data.id);
}
if (data.preparing_id) {
ctx.preparingTools.delete(data.preparing_id);
}
}
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
} }
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
}
// 关键修复每个工具完成后都更新当前上下文Token if (data.status === 'completed') {
if (data.status === 'completed') { setTimeout(() => {
setTimeout(() => { ctx.updateCurrentContextTokens();
ctx.updateCurrentContextTokens(); }, 500);
}, 500); try {
try { ctx.fileFetchTree();
ctx.fileFetchTree(); } catch (error) {
} catch (error) { console.warn('刷新文件树失败', error);
console.warn('刷新文件树失败', error); }
} }
ctx.monitorResolveTool(data);
};
if (hasPendingStreamingText()) {
pendingToolEvents.push({ event: 'update_action', data, handler });
} else {
handler();
} }
ctx.monitorResolveTool(data);
}); });
ctx.socket.on('append_payload', (data) => { ctx.socket.on('append_payload', (data) => {
@ -1046,6 +1097,8 @@ export async function initializeLegacySocket(ctx: any) {
socketLog('任务完成', data); socketLog('任务完成', data);
ctx.scheduleResetAfterTask('socket:task_complete', { preserveMonitorWindows: true }); ctx.scheduleResetAfterTask('socket:task_complete', { preserveMonitorWindows: true });
resetPendingToolEvents();
// 任务完成后立即更新Token统计关键修复 // 任务完成后立即更新Token统计关键修复
if (ctx.currentConversationId) { if (ctx.currentConversationId) {
ctx.updateCurrentContextTokens(); ctx.updateCurrentContextTokens();