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[] = []; const stats: string[] = [];
if (msg.timestamp) stats.push(formatTime(msg.timestamp)); if (msg.timestamp) stats.push(formatTime(msg.timestamp));
if (msg.eval_count) stats.push(`${msg.eval_count} tokens`); if (msg.eval_count != null && msg.eval_count > 0) stats.push(`${msg.eval_count} tokens`);
if (msg.total_duration) stats.push(`${(msg.total_duration / 1e9).toFixed(1)}s`); if (msg.total_duration != null && msg.total_duration > 0) stats.push(`${(msg.total_duration / 1e9).toFixed(1)}s`);
if (stats.length > 0) { if (stats.length > 0) {
contentHtml += `<div class="msg-stats">${stats.map(s => `<span>${s}</span>`).join(' · ')}</div>`; 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); lastMsg.querySelector('.msg-body')!.appendChild(statsDiv);
} }
const parts: string[] = []; const parts: string[] = [];
if (stats.eval_count) parts.push(`${stats.eval_count} tokens`); if (stats.eval_count != null && stats.eval_count > 0) parts.push(`${stats.eval_count} tokens`);
if (stats.total_duration) parts.push(`${(stats.total_duration / 1e9).toFixed(1)}s`); 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(' · '); 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 (think && typeof think === 'string') thinkContent += think;
} }
if (chunk.model) modelName = chunk.model; 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); updateLastAssistantMessage(assistantContent, thinkContent || null, null, modelName || undefined);
if (chunk.done) { 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); }, abortController);
@@ -706,7 +717,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
onConfirmTool: async (call) => { onConfirmTool: async (call) => {
return showToolConfirm(call); return showToolConfirm(call);
}, },
onDone: async (finalContent, toolRecords) => { onDone: async (finalContent, toolRecords, loopStats) => {
assistantContent = finalContent; assistantContent = finalContent;
const assistantMsg: ChatMessage = { const assistantMsg: ChatMessage = {
@@ -714,7 +725,9 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
content: assistantContent || '', content: assistantContent || '',
timestamp: Date.now(), timestamp: Date.now(),
...(thinkContent && { think: thinkContent }), ...(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) => ({ state.update(KEYS.CURRENT_SESSION, (session: any) => ({
...session, ...session,
@@ -722,7 +735,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
updatedAt: Date.now() updatedAt: Date.now()
})); }));
updateLastAssistantMessage(assistantContent, thinkContent || null, null); updateLastAssistantMessage(assistantContent, thinkContent || null, loopStats || null);
renderMessages(); renderMessages();
await saveCurrentSession(); await saveCurrentSession();
} }
+16 -9
View File
@@ -32,7 +32,7 @@ export interface AgentCallbacks {
onToolCallStart: (call: ToolCall) => void; onToolCallStart: (call: ToolCall) => void;
onToolCallResult: (name: string, result: ToolResult, call: ToolCall) => void; onToolCallResult: (name: string, result: ToolResult, call: ToolCall) => void;
onToolCallError: (name: string, error: string, 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>; onConfirmTool: (call: ToolCall) => Promise<boolean>;
} }
@@ -101,6 +101,12 @@ export async function runAgentLoop(
const allToolRecords: ToolCallRecord[] = []; const allToolRecords: ToolCallRecord[] = [];
const loopStartTime = Date.now(); const loopStartTime = Date.now();
let content = ''; 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) { while (loopCount < MAX_LOOPS) {
loopCount++; loopCount++;
@@ -108,14 +114,14 @@ export async function runAgentLoop(
// 全局超时检查 // 全局超时检查
if (Date.now() - loopStartTime > MAX_LOOP_TIME) { if (Date.now() - loopStartTime > MAX_LOOP_TIME) {
logWarn('Agent Loop 达到最大运行时间(5分钟),自动停止'); logWarn('Agent Loop 达到最大运行时间(5分钟),自动停止');
callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords); callbacks.onDone(content || '(达到最大运行时间限制)', allToolRecords, makeStats());
return; return;
} }
// 检查是否已中止 // 检查是否已中止
if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) { if (state.get<AbortController | null>(KEYS.ABORT_CONTROLLER)?.signal.aborted) {
logInfo('Agent Loop 已中止'); logInfo('Agent Loop 已中止');
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined); callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
@@ -156,6 +162,7 @@ export async function runAgentLoop(
content += chunk.message.content; content += chunk.message.content;
callbacks.onContent(content); callbacks.onContent(content);
} }
if (chunk.eval_count) totalEvalCount = chunk.eval_count;
if (chunk.message?.tool_calls?.length) { if (chunk.message?.tool_calls?.length) {
for (const tc of chunk.message.tool_calls) { for (const tc of chunk.message.tool_calls) {
if (tc.function?.name) { if (tc.function?.name) {
@@ -193,7 +200,7 @@ export async function runAgentLoop(
...(thinking && { thinking }) ...(thinking && { thinking })
}); });
} }
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined); callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
// 超时错误 — 保留已生成的内容 // 超时错误 — 保留已生成的内容
@@ -206,7 +213,7 @@ export async function runAgentLoop(
...(thinking && { thinking }) ...(thinking && { thinking })
}); });
} }
callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined); callbacks.onDone(content || '(模型响应超时,已自动中断)', allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
throw err; throw err;
@@ -228,14 +235,14 @@ export async function runAgentLoop(
if (toolCalls.length === 0) { if (toolCalls.length === 0) {
logInfo('无工具调用,Agent Loop 结束'); logInfo('无工具调用,Agent Loop 结束');
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined); callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
for (const call of toolCalls) { for (const call of toolCalls) {
// 检查是否已中止 // 检查是否已中止
if (abortController.signal.aborted) { if (abortController.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined); callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
@@ -246,7 +253,7 @@ export async function runAgentLoop(
const confirmed = await callbacks.onConfirmTool(call); const confirmed = await callbacks.onConfirmTool(call);
// 确认后再次检查是否已中止 // 确认后再次检查是否已中止
if (abortController.signal.aborted) { if (abortController.signal.aborted) {
callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined); callbacks.onDone(content, allToolRecords.length > 0 ? allToolRecords : undefined, makeStats());
return; return;
} }
if (!confirmed) { if (!confirmed) {
@@ -320,5 +327,5 @@ export async function runAgentLoop(
} }
logWarn('Agent Loop 达到最大工具调用次数限制'); logWarn('Agent Loop 达到最大工具调用次数限制');
callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords); callbacks.onDone(content || '(达到最大工具调用次数限制)', allToolRecords, makeStats());
} }