修复: 对话轨迹生成锚点回归与耗时口径

生成中锚点改真实末条,修第二轮打字机与工具卡不渲染回归
轨迹总耗时计入末轮时长,单卡耗时剔除审批等待区间
聚合文案与截断常量下沉共用,时长进位边界钳制
This commit is contained in:
lxy
2026-10-01 08:28:07 +08:00
parent 7b5a2c83b0
commit bff5ae9d7a
6 changed files with 57 additions and 29 deletions
+4 -1
View File
@@ -95,8 +95,11 @@ export function handleToolEvent(event: AiChatEvent): boolean {
clearToolSlowTimer(event.id)
clearApprovalTimer(event.id)
} else {
const tc = findToolCall(event.id)
// 后端对整批工具先批量发 Started 再走审批,审批等待不算执行耗时:通过时重置开始时刻
if (tc) tc.startedAt = Date.now()
// 审批通过 → 工具重回执行态,重启慢执行计时器;对称清审批超时计时器防到点误拒
startToolSlowTimer(event.id, findToolCall(event.id)?.name || '')
startToolSlowTimer(event.id, tc?.name || '')
clearApprovalTimer(event.id)
}
// 路径授权挂起的工具收到 ApprovalResult → 按 id 移除该条(数组化后只清这一条)
+4 -1
View File
@@ -122,6 +122,9 @@ export const fileIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="n
/** grep 工具放大镜图标(跨文件内容搜索,F-260621) */
export const searchIcon = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>'
/** list_directory 条目截断上限(与后端截断行为对齐,截断标注文案取此值) */
export const DIR_TRUNCATE_LIMIT = 1000
/** 默认保持展开:执行中/待审批/已拒绝/write_file——折叠无收益 */
export function shouldKeepOpen(tc: AiToolCallInfo): boolean {
return tc.status === 'running'
@@ -660,7 +663,7 @@ function summaryListDir(r: ToolResult): string {
const n = Array.isArray(r.entries) ? r.entries.length : 0
if (!n) return ''
return r.truncated
? t('aiTool.dirTruncated', { n: 1000 })
? t('aiTool.dirTruncated', { n: DIR_TRUNCATE_LIMIT })
: `${n} ${t('aiTool.items')}`
}
+22 -2
View File
@@ -91,6 +91,21 @@ export function statsTail(s: TurnCallStats): string {
return parts.join(' · ')
}
/**
* 聚合行整句:「N 次调用 · 成功 x 失败 y · 总耗时 T」。
* 头部 total=0 显「过程记录」;零值段省略;durationMs 可选(轨迹卡传起止差,分组行不传)。
* 供 MessageList 轨迹卡与 ToolCardList 聚合分组行共用,拼装语义单一出口。
*/
export function aggText(s: TurnCallStats, durationMs?: number): string {
const parts: string[] = []
parts.push(s.total > 0 ? t('ai.trace.calls', { n: s.total }) : t('ai.trace.processOnly'))
const tail = statsTail(s)
if (tail) parts.push(tail)
const dur = formatDuration(durationMs)
if (dur) parts.push(t('ai.trace.duration', { time: dur }))
return parts.join(' · ')
}
/**
* 时间跨度格式化。precise=true(单卡耗时)亚秒显毫秒;false(轨迹总耗时)亚秒归 <1s。
* 超时跨度进位到分/时。负值/非有限值返回空串(调用方隐藏该段)。
@@ -100,8 +115,13 @@ export function formatDuration(ms: number | undefined | null, precise = false):
if (ms < 1000) return precise ? `${Math.round(ms)}ms` : '<1s'
const sec = ms / 1000
if (sec < 60) return `${sec.toFixed(sec < 10 ? 1 : 0)}s`
const min = Math.floor(sec / 60)
const remainSec = Math.round(sec % 60)
let min = Math.floor(sec / 60)
let remainSec = Math.round(sec % 60)
// 秒四舍五入进位(sec∈[119.5,120) 得 60s):并入分,避免显「1m 60s」
if (remainSec === 60) {
min++
remainSec = 0
}
if (min < 60) return `${min}m ${remainSec}s`
const hour = Math.floor(min / 60)
return `${hour}h ${min % 60}m`