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:
@@ -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(' · ');
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user