优化: 缓存命中字段解析与提示词前缀稳定化
- openai 兼容口径补解析嵌套 cached_tokens - cache 显示区分无数据与真零,估算轮不渲染 - 轨迹展开折叠空过程记录行降噪 - token 统计行恒完整可见不截断 - openai_compat 纳入 system 稳定易变拆分 - 系统警示与主题标记改尾部注入不位移前缀 - 工具结果视图压缩按入库时刻冻结 - 会话内工具集冻结仅增不减 - anthropic 缓存补消息级断点,设置页加开关 - 补 intent 测试对 Agent domain 覆盖
This commit is contained in:
@@ -131,6 +131,14 @@ function tokenInOf(m: AiMessage): number {
|
||||
function tokenCacheOf(m: AiMessage): number {
|
||||
return m.tokenUsage?.cache_hit ?? m.prompt_cache_hit_tokens ?? 0
|
||||
}
|
||||
/** cache 段是否有真实上报数据(渲染闸):hit/miss 均为 0 时「provider 没报缓存」与
|
||||
* 「真 0」不可区分(GLM 无缓存口径,估算轮更无真实用量),渲染「0 cache」会误导,不渲染。 */
|
||||
function tokenCacheVisible(m: AiMessage): boolean {
|
||||
if (tokenEstimated(m)) return false
|
||||
const hit = m.tokenUsage?.cache_hit ?? m.prompt_cache_hit_tokens ?? 0
|
||||
const miss = m.tokenUsage?.cache_miss ?? m.prompt_cache_miss_tokens ?? 0
|
||||
return hit > 0 || miss > 0
|
||||
}
|
||||
/** 取消息的 out token */
|
||||
function tokenOutOf(m: AiMessage): number {
|
||||
return m.tokenUsage?.completion ?? m.completion_tokens ?? 0
|
||||
@@ -765,15 +773,20 @@ function thinkFullText(m: AiMessage): string {
|
||||
if (!txt) return t('ai.trace.processOnly')
|
||||
return txt.length > 500 ? txt.slice(0, 500) + '…' : txt
|
||||
}
|
||||
/** 思考 pill 右端 token 小字:「30.3k in(估算) · 0 cache · 157 out」(无 token 数据返回空串隐藏) */
|
||||
/** 该轮是否有实质思考内容:空内容轮(pill 只会显示「过程记录」占位)不渲染 pill,消除占位噪音;
|
||||
* 该轮工具卡照常渲染,有实质内容的思考行不受影响。 */
|
||||
function hasThinkBody(m: AiMessage): boolean {
|
||||
const txt = thinkPlainText(m)
|
||||
return !!txt && txt !== t('ai.trace.processOnly')
|
||||
}
|
||||
/** 思考 pill 右端 token 小字:「30.3k in(估算) · 12k cache · 157 out」(无 token 数据返回空串隐藏)。
|
||||
* cache 段仅在 provider 真实上报时渲染(tokenCacheVisible),避免把「无数据」显示成「0 cache」。 */
|
||||
function roundTokenText(m: AiMessage): string {
|
||||
if (!m.tokenUsage && m.prompt_tokens == null) return ''
|
||||
const est = tokenEstimated(m) ? `(${t('aiChat.estimated')})` : ''
|
||||
const parts = [
|
||||
`${formatTokensK(tokenInOf(m))} in${est}`,
|
||||
`${formatTokensK(tokenCacheOf(m))} cache`,
|
||||
`${formatTokensK(tokenOutOf(m))} out`,
|
||||
]
|
||||
const parts = [`${formatTokensK(tokenInOf(m))} in${est}`]
|
||||
if (tokenCacheVisible(m)) parts.push(`${formatTokensK(tokenCacheOf(m))} cache`)
|
||||
parts.push(`${formatTokensK(tokenOutOf(m))} out`)
|
||||
if (tokenReasonOf(m) > 0) parts.push(`${formatTokensK(tokenReasonOf(m))} reason`)
|
||||
return parts.join(' · ')
|
||||
}
|
||||
@@ -1053,22 +1066,25 @@ defineExpose({
|
||||
<!-- 展开态 rail 面板:每轮=思考单行 pill(点击展开全文)+工具卡,图标锚定左轨连线贯通 -->
|
||||
<div v-if="item.expanded" class="ai-trace-body">
|
||||
<div v-for="m in item.traceMsgs" :key="m.id" class="ai-trace-round">
|
||||
<button
|
||||
class="ai-think-pill"
|
||||
:title="thinkFullText(m)"
|
||||
@click="toggleThink(m.id)"
|
||||
>
|
||||
<span class="ai-think-ic">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 00-4 12.7c.6.5 1 1.4 1 2.3h6c0-.9.4-1.8 1-2.3A7 7 0 0012 2z"/></svg>
|
||||
</span>
|
||||
<span class="ai-think-text">{{ thinkSnippet(m) }}</span>
|
||||
<span v-if="roundTokenText(m)" class="ai-think-tokens">{{ roundTokenText(m) }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="expandedThinkIds.has(m.id) && m.content?.trim()"
|
||||
class="ai-think-full ai-md"
|
||||
v-html="renderMd(m.content)"
|
||||
></div>
|
||||
<!-- 空内容轮(只剩「过程记录」占位)不渲染思考 pill,降噪;工具卡照常 -->
|
||||
<template v-if="hasThinkBody(m)">
|
||||
<button
|
||||
class="ai-think-pill"
|
||||
:title="thinkFullText(m)"
|
||||
@click="toggleThink(m.id)"
|
||||
>
|
||||
<span class="ai-think-ic">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 00-4 12.7c.6.5 1 1.4 1 2.3h6c0-.9.4-1.8 1-2.3A7 7 0 0012 2z"/></svg>
|
||||
</span>
|
||||
<span class="ai-think-text">{{ thinkSnippet(m) }}</span>
|
||||
<span v-if="roundTokenText(m)" class="ai-think-tokens">{{ roundTokenText(m) }}</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="expandedThinkIds.has(m.id) && m.content?.trim()"
|
||||
class="ai-think-full ai-md"
|
||||
v-html="renderMd(m.content)"
|
||||
></div>
|
||||
</template>
|
||||
<ToolCardList
|
||||
v-if="m.toolCalls && m.toolCalls.length > 0"
|
||||
ref="toolCardListRef"
|
||||
@@ -1257,8 +1273,11 @@ defineExpose({
|
||||
@click="toggleTokenPopover(item.msg, $event)"
|
||||
>
|
||||
<span>{{ formatTokensK(tokenInOf(item.msg)) }} in<template v-if="tokenEstimated(item.msg)"><span class="ai-token-est">({{ $t('aiChat.estimated') }})</span></template></span>
|
||||
<span class="ai-token-sep">·</span>
|
||||
<span>{{ formatTokensK(tokenCacheOf(item.msg)) }} cache</span>
|
||||
<!-- cache 段仅在 provider 真实上报时渲染(tokenCacheVisible),避免「无数据」显示成「0 cache」 -->
|
||||
<template v-if="tokenCacheVisible(item.msg)">
|
||||
<span class="ai-token-sep">·</span>
|
||||
<span>{{ formatTokensK(tokenCacheOf(item.msg)) }} cache</span>
|
||||
</template>
|
||||
<span class="ai-token-sep">·</span>
|
||||
<span>{{ formatTokensK(tokenOutOf(item.msg)) }} out</span>
|
||||
<template v-if="tokenReasonOf(item.msg) > 0">
|
||||
@@ -1277,7 +1296,7 @@ defineExpose({
|
||||
<span class="ai-token-popover-label">{{ $t('aiChat.tokenInLabel') }}</span>
|
||||
<span class="ai-token-popover-val">{{ formatTokensK(tokenInOf(item.msg)) }} ({{ tokenInOf(item.msg) }})</span>
|
||||
</div>
|
||||
<div class="ai-token-popover-row">
|
||||
<div v-if="tokenCacheVisible(item.msg)" class="ai-token-popover-row">
|
||||
<span class="ai-token-popover-label">{{ $t('aiChat.tokenCacheLabel') }}</span>
|
||||
<span class="ai-token-popover-val">{{ formatTokensK(tokenCacheOf(item.msg)) }} ({{ tokenCacheOf(item.msg) }})</span>
|
||||
</div>
|
||||
@@ -1289,7 +1308,8 @@ defineExpose({
|
||||
<span class="ai-token-popover-label">{{ $t('aiChat.tokenReasonLabel') }}</span>
|
||||
<span class="ai-token-popover-val">{{ formatTokensK(tokenReasonOf(item.msg)) }} ({{ tokenReasonOf(item.msg) }})</span>
|
||||
</div>
|
||||
<div v-if="cacheHitRate(item.msg) != null" class="ai-token-popover-row">
|
||||
<!-- 命中率与 cache 段同闸:无缓存口径的 provider 不显 0%,防「无数据」误读成「全未命中」 -->
|
||||
<div v-if="tokenCacheVisible(item.msg) && cacheHitRate(item.msg) != null" class="ai-token-popover-row">
|
||||
<span class="ai-token-popover-label">{{ $t('aiChat.tokenCacheRateLabel') }}</span>
|
||||
<span class="ai-token-popover-val">{{ cacheHitRate(item.msg) }}%</span>
|
||||
</div>
|
||||
@@ -1938,7 +1958,8 @@ defineExpose({
|
||||
}
|
||||
.ai-trace-round + .ai-trace-round { margin-top: 2px; }
|
||||
|
||||
/* ── 思考单行 pill:灯泡锚轨 + 单行截断 + 右端 token 小字(空间不足先吃 token 尾段) ── */
|
||||
/* ── 思考单行 pill:灯泡锚轨 + 单行截断 + 右端 token 小字
|
||||
(空间不足先压缩摘要文本段(hover title 有全文),token 尾段不再被截,恒完整可见) ── */
|
||||
.ai-think-pill {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -1978,16 +1999,13 @@ defineExpose({
|
||||
}
|
||||
.ai-think-text {
|
||||
flex: 1 1 auto;
|
||||
min-width: 80px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-think-tokens {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
font-family: var(--df-font-mono);
|
||||
font-size: 9.5px;
|
||||
|
||||
Reference in New Issue
Block a user