feat: 缓存命中率有来源了(assistant/chunk 的 usage 事件)+ DSH 启动自动拉起浮窗
挖到真源:事件流里 assistant/chunk 带 chunk.type='usage'
{inputTokens,outputTokens,cacheReadTokens,reasoningTokens}
从此算出真实指标(不再依赖 tokenMeter 的估算):
缓存命中率 = cacheRead/(input+cacheRead) 实测 0.9976(与 App 状态栏 99% 一致)
总量 = output + input + cacheRead 实测 88.99M
tok/s = outputTokens/该步耗时 实测 37.3
上下文占用 = 最后一步的 input+cacheRead / 窗口(不是历史累加) 实测 56.3%
插件与镜像桥两侧都接上了;镜像桥改为跟随插件认定的当前会话。
另:autoOpen(默认开)——DSH 启动后 1.5s 若浮窗没在跑就自动拉起(用户选定)。
This commit is contained in:
+35
-6
@@ -87,6 +87,13 @@ function rowsFromLog(text) {
|
||||
let streamStart = 0
|
||||
let streamChars = 0
|
||||
let lastChunkAt = 0
|
||||
let outTokens = 0
|
||||
let inTokens = 0
|
||||
let cacheRead = 0
|
||||
let stepStart = 0
|
||||
let decodeMs = 0
|
||||
let lastTps = null
|
||||
let lastPromptTokens = 0 // 最后一步的输入(=未缓存+命中缓存):这才是"当前上下文占用"
|
||||
const est = (chars) => Math.round(chars / 3.2) // 中英混排的粗略 token 估算
|
||||
|
||||
for (const line of text.split('\n')) {
|
||||
@@ -97,7 +104,7 @@ function rowsFromLog(text) {
|
||||
const d = e?.data || e || {}
|
||||
const t = e?.time || 0
|
||||
if (type === 'turn/start') turns++
|
||||
else if (type === 'step/start') steps++
|
||||
else if (type === 'step/start') { steps++; stepStart = t || Date.now() }
|
||||
else if (type === 'request/context' && d.contextWindow) contextWindow = d.contextWindow
|
||||
else if (type === 'session/title' && d.title) titles.set('title', String(d.title))
|
||||
else if (type === 'user/message') {
|
||||
@@ -110,6 +117,17 @@ function rowsFromLog(text) {
|
||||
} else if (type === 'assistant/chunk') {
|
||||
const c = d.chunk || {}
|
||||
const kind = String(c.type || '')
|
||||
if (kind === 'usage' && c.usage) {
|
||||
// 真实用量:{inputTokens,outputTokens,cacheReadTokens,reasoningTokens}
|
||||
const u = c.usage
|
||||
inTokens += u.inputTokens || 0
|
||||
cacheRead += u.cacheReadTokens || 0
|
||||
outTokens += u.outputTokens || 0
|
||||
lastPromptTokens = (u.inputTokens || 0) + (u.cacheReadTokens || 0)
|
||||
const ms = stepStart ? (t || Date.now()) - stepStart : 0
|
||||
if (u.outputTokens && ms > 0) { decodeMs += ms; lastTps = u.outputTokens / (ms / 1000) }
|
||||
continue
|
||||
}
|
||||
const piece = c.text ?? c.delta ?? (kind.includes('text') ? c.content : '')
|
||||
if (piece) {
|
||||
const now = t || Date.now()
|
||||
@@ -129,16 +147,20 @@ function rowsFromLog(text) {
|
||||
}
|
||||
for (const r of rows) delete r.streaming
|
||||
const streaming = lastChunkAt && Date.now() - lastChunkAt < 8000
|
||||
const tps = streaming && streamStart ? est(streamChars) / Math.max(0.5, (lastChunkAt - streamStart) / 1000) : null
|
||||
// tok/s:优先用真实 outputTokens/步耗时;否则退化成"流式字符估算"
|
||||
const tps = lastTps ?? (streaming && streamStart ? est(streamChars) / Math.max(0.5, (lastChunkAt - streamStart) / 1000) : null)
|
||||
const promptTokens = inTokens + cacheRead
|
||||
const realUsage = promptTokens + outTokens > 0
|
||||
return {
|
||||
rows, titles, turns, steps, streaming,
|
||||
metrics: {
|
||||
turns,
|
||||
steps,
|
||||
tps,
|
||||
tokens: outChars ? est(outChars + userChars) : null,
|
||||
cacheHit: null,
|
||||
contextUsed: contextWindow ? Math.min(1, est(userChars + outChars) / contextWindow) : null,
|
||||
// 有真实用量就用真实值;否则退化成字符估算(界面会带 ≈)
|
||||
tokens: realUsage ? outTokens + promptTokens : (outChars ? est(outChars + userChars) : null),
|
||||
cacheHit: promptTokens > 0 ? cacheRead / promptTokens : null,
|
||||
contextUsed: contextWindow ? Math.min(1, (realUsage ? lastPromptTokens : est(userChars + outChars)) / contextWindow) : null,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -164,9 +186,16 @@ function buildSnapshot() {
|
||||
})
|
||||
}
|
||||
|
||||
// 当前会话:watch.json 指定优先,否则"日志最新"(真正在跑的那个)
|
||||
// 当前会话:watch.json 指定 > 插件自己认定的当前会话 > 日志最新
|
||||
let active = null
|
||||
if (watch?.sessionId) active = entries.find((e) => e.id === watch.sessionId || e.id.replace(/^session-/, '') === watch.sessionId) || null
|
||||
if (!active) {
|
||||
// 插件写的 snapshot.json 带 sessionId:跟它走,指标缺口才能对上同一个会话
|
||||
const plug = readJson(SNAPSHOT)
|
||||
if (plug?.sessionId && !plug.source) {
|
||||
active = entries.find((e) => e.id === plug.sessionId || e.id.replace(/^session-/, '') === plug.sessionId) || null
|
||||
}
|
||||
}
|
||||
if (!active) active = entries.sort((a, b) => b.mtime - a.mtime)[0] || null
|
||||
if (!active) return null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user