fix: 修复 AI 回复 token 统计信息显示

- 直接 API 路径:增强 finalStats 捕获,从任意包含统计的 chunk 兜底收集
- Agent Loop 路径:新增 total_duration / eval_count 统计回传
- chat-area.ts: 修复 falsy 判断(eval_count=0 也应显示)
- agent-engine.ts: onDone 回调新增 stats 参数
This commit is contained in:
thzxx
2026-04-07 01:56:17 +08:00
parent 9735a1ed38
commit a80baf363c
3 changed files with 37 additions and 17 deletions
+4 -4
View File
@@ -159,8 +159,8 @@ export function appendMessageDOM(msg: ChatMessage, index: number): void {
const stats: string[] = [];
if (msg.timestamp) stats.push(formatTime(msg.timestamp));
if (msg.eval_count) stats.push(`${msg.eval_count} tokens`);
if (msg.total_duration) stats.push(`${(msg.total_duration / 1e9).toFixed(1)}s`);
if (msg.eval_count != null && msg.eval_count > 0) stats.push(`${msg.eval_count} tokens`);
if (msg.total_duration != null && msg.total_duration > 0) stats.push(`${(msg.total_duration / 1e9).toFixed(1)}s`);
if (stats.length > 0) {
contentHtml += `<div class="msg-stats">${stats.map(s => `<span>${s}</span>`).join(' · ')}</div>`;
}
@@ -337,8 +337,8 @@ export function updateLastAssistantMessage(content: string, think: string | null
lastMsg.querySelector('.msg-body')!.appendChild(statsDiv);
}
const parts: string[] = [];
if (stats.eval_count) parts.push(`${stats.eval_count} tokens`);
if (stats.total_duration) parts.push(`${(stats.total_duration / 1e9).toFixed(1)}s`);
if (stats.eval_count != null && stats.eval_count > 0) parts.push(`${stats.eval_count} tokens`);
if (stats.total_duration != null && stats.total_duration > 0) parts.push(`${(stats.total_duration / 1e9).toFixed(1)}s`);
if (parts.length) statsDiv.innerHTML = parts.join(' · ');
}
+17 -4
View File
@@ -463,9 +463,20 @@ export async function sendMessage(): Promise<void> {
if (think && typeof think === 'string') thinkContent += think;
}
if (chunk.model) modelName = chunk.model;
// 捕获任何包含统计的 chunk(不限于 done)
if (chunk.eval_count || chunk.total_duration) {
finalStats = {
eval_count: chunk.eval_count ?? finalStats?.eval_count,
total_duration: chunk.total_duration ?? finalStats?.total_duration,
};
}
updateLastAssistantMessage(assistantContent, thinkContent || null, null, modelName || undefined);
if (chunk.done) {
finalStats = { eval_count: chunk.eval_count, total_duration: chunk.total_duration };
// done chunk 优先使用其统计,保留之前的兜底
finalStats = {
eval_count: chunk.eval_count ?? finalStats?.eval_count,
total_duration: chunk.total_duration ?? finalStats?.total_duration,
};
}
}, abortController);
@@ -706,7 +717,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
onConfirmTool: async (call) => {
return showToolConfirm(call);
},
onDone: async (finalContent, toolRecords) => {
onDone: async (finalContent, toolRecords, loopStats) => {
assistantContent = finalContent;
const assistantMsg: ChatMessage = {
@@ -714,7 +725,9 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
content: assistantContent || '',
timestamp: Date.now(),
...(thinkContent && { think: thinkContent }),
...(toolRecords?.length && { toolCalls: toolRecords })
...(toolRecords?.length && { toolCalls: toolRecords }),
...(loopStats?.eval_count && { eval_count: loopStats.eval_count }),
...(loopStats?.total_duration && { total_duration: loopStats.total_duration }),
};
state.update(KEYS.CURRENT_SESSION, (session: any) => ({
...session,
@@ -722,7 +735,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
updatedAt: Date.now()
}));
updateLastAssistantMessage(assistantContent, thinkContent || null, null);
updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null);
renderMessages();
await saveCurrentSession();
}
+16 -9
View File
@@ -32,7 +32,7 @@ export interface AgentCallbacks {
onToolCallStart: (call: ToolCall) => void;
onToolCallResult: (name: string, result: ToolResult, call: ToolCall) => void;
onToolCallError: (name: string, error: string, call: ToolCall) => void;
onDone: (finalContent: string, toolRecords?: ToolCallRecord[]) => void;
onDone: (finalContent: string, toolRecords?: ToolCallRecord[], stats?: { eval_count?: number; total_duration?: number }) => void;
onConfirmTool: (call: ToolCall) => Promise<boolean>;
}
@@ -101,6 +101,12 @@ export async function runAgentLoop(
const allToolRecords: ToolCallRecord[] = [];
const loopStartTime = Date.now();
let content = '';
let totalEvalCount = 0;
const makeStats = () => {
const totalDuration = (Date.now() - loopStartTime) * 1e6; // 转为纳秒
return { eval_count: totalEvalCount || undefined, total_duration: totalDuration };
};
while (loopCount < MAX_LOOPS) {
loopCount++;
@@ -108,14 +114,14 @@ export async function runAgentLoop(
// 全局超时检查
if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
logWarn('Agent Loop 达到最大运行时间(5分钟),自动停止');
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords);
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats());
return;
}
// 检查是否已中止
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
logInfo('Agent Loop 已中止');
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
@@ -156,6 +162,7 @@ export async function runAgentLoop(
content += chunk.message.content;
callbacks.onContent(content);
}
if (chunk.eval_count) totalEvalCount = chunk.eval_count;
if (chunk.message?.tool_calls?.length) {
for (const tc of chunk.message.tool_calls) {
if (tc.function?.name) {
@@ -193,7 +200,7 @@ export async function runAgentLoop(
...(thinking && { thinking })
});
}
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
// 超时错误 — 保留已生成的内容
@@ -206,7 +213,7 @@ export async function runAgentLoop(
...(thinking && { thinking })
});
}
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined);
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
throw err;
@@ -228,14 +235,14 @@ export async function runAgentLoop(
if (toolCalls.length === 0) {
logInfo('无工具调用,Agent Loop 结束');
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
for (const call of toolCalls) {
// 检查是否已中止
if (abortController.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
@@ -246,7 +253,7 @@ export async function runAgentLoop(
const confirmed = await callbacks.onConfirmTool(call);
// 确认后再次检查是否已中止
if (abortController.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined);
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return;
}
if (!confirmed) {
@@ -320,5 +327,5 @@ export async function runAgentLoop(
}
logWarn('Agent Loop 达到最大工具调用次数限制');
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords);
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords, makeStats());
}