From d34d9bd2e6027d89cd1aadf6c8da3d9ef34eaf2f Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Fri, 30 Oct 2026 13:53:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=94=A8=E9=87=8F=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E5=B1=95=E7=A4=BA=E7=BC=93=E5=AD=98=E8=AF=BB/?= =?UTF-8?q?=E5=86=99=E4=B8=8E=E4=B8=8A=E6=B8=B8=E5=AE=9E=E9=99=85=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=EF=BC=8C=E8=A1=A5=201h=20=E7=BC=93=E5=AD=98=E5=86=99?= =?UTF-8?q?=E4=B8=8E=E6=80=9D=E8=80=83=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API:/api/v1/usage/logs 与 /api/v1/admin/usage 返回 cache_creation_tokens、 reasoning_tokens、cache_write_1h_tokens、served_model(types.ts 同步可选字段)。 前端: - 模型列在被模型映射/聚合商改写时显示「→ 上游实际模型」,长名截断并可悬浮看全; - 表格里 Token 列第二行显示缓存读/写(与成本直接相关),1h 缓存写与思考 token 放悬浮提示——两者直接上表格会把 Token 列撑到 255px,1280 宽下把「时间/原始」 挤出视野(实测 1146px vs 容器 1006px);移动卡两行都显示; - 用量表协议列由 chat/completions 简化为 chat(表头已标明「API 格式」), 时间列改月-日 时:分、完整时间进 title;调整后桌面端表格宽度 1006px, 与容器一致、无横向滚动; - 顺带修复控制台用量页 Button 未导入(上一页/下一页此前渲染不出来)。 验证:本地 mock 上游 + 临时 sqlite 实例与真实数据实测;1280 桌面与 390 窄屏 截图核对(表格 tableW == 容器宽度);vue-tsc、vitest、eslint、vite build 与 go vet、go test ./... 全绿。 --- server/internal/api/admin_stats.go | 4 +- server/internal/api/usage.go | 4 +- web/src/types.ts | 7 +++ web/src/views/admin/UsageView.vue | 77 +++++++++++++++++++++++++---- web/src/views/console/UsageView.vue | 61 ++++++++++++++++++++--- 5 files changed, 135 insertions(+), 18 deletions(-) diff --git a/server/internal/api/admin_stats.go b/server/internal/api/admin_stats.go index 98714c0..264c10c 100644 --- a/server/internal/api/admin_stats.go +++ b/server/internal/api/admin_stats.go @@ -257,7 +257,9 @@ func (h *Handler) AdminUsage(c *gin.Context) { "id": l.ID, "user": userNames[l.UserID], "user_id": l.UserID, "model": l.ModelName, "protocol": l.Protocol, "channel_id": l.ChannelID, "channel": chNames[l.ChannelID], "input_tokens": l.InputTokens, "output_tokens": l.OutputTokens, - "cache_read_tokens": l.CacheReadTokens, "cost": l.Cost, + "cache_read_tokens": l.CacheReadTokens, "cache_creation_tokens": l.CacheCreationTokens, + "reasoning_tokens": l.ReasoningTokens, "cache_write_1h_tokens": l.CacheWrite1hTokens, + "served_model": l.ServedModel, "cost": l.Cost, "latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode, "raw_request": l.RawRequest, "raw_response": l.RawResponse, "created_at": l.CreatedAt, diff --git a/server/internal/api/usage.go b/server/internal/api/usage.go index cc2615d..5de908f 100644 --- a/server/internal/api/usage.go +++ b/server/internal/api/usage.go @@ -105,7 +105,9 @@ func (h *Handler) UsageLogs(c *gin.Context) { out = append(out, gin.H{ "id": l.ID, "request_id": l.RequestID, "model": l.ModelName, "protocol": l.Protocol, "input_tokens": l.InputTokens, "output_tokens": l.OutputTokens, - "cache_read_tokens": l.CacheReadTokens, "cost": l.Cost, + "cache_read_tokens": l.CacheReadTokens, "cache_creation_tokens": l.CacheCreationTokens, + "reasoning_tokens": l.ReasoningTokens, "cache_write_1h_tokens": l.CacheWrite1hTokens, + "served_model": l.ServedModel, "cost": l.Cost, "latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode, "created_at": l.CreatedAt, }) diff --git a/web/src/types.ts b/web/src/types.ts index 68b91e9..14891de 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -112,6 +112,13 @@ export interface UsageLog { input_tokens: number output_tokens: number cache_read_tokens: number + cache_creation_tokens?: number + /** 思考 token:上游已计入 output_tokens,仅展示口径 */ + reasoning_tokens?: number + /** Anthropic 1h 缓存写(计价 2× 输入);含在 cache_creation_tokens 内 */ + cache_write_1h_tokens?: number + /** 上游实际应答的模型名;模型映射/聚合商场景下与 model 不同 */ + served_model?: string cost: number latency_ms: number status: string diff --git a/web/src/views/admin/UsageView.vue b/web/src/views/admin/UsageView.vue index 47f077b..c0f2f10 100644 --- a/web/src/views/admin/UsageView.vue +++ b/web/src/views/admin/UsageView.vue @@ -2,8 +2,7 @@ import { onMounted, ref } from 'vue' import { http, errMsg } from '@/api/client' import { useToastStore } from '@/stores/toast' -import { protocolShort } from '@/lib/protocol' -import { fmtCost, fmtTime } from '@/lib/format' +import { fmtCost, fmtNum, fmtTime } from '@/lib/format' import Badge from '@/components/ui/Badge.vue' import Button from '@/components/ui/Button.vue' import Modal from '@/components/ui/Modal.vue' @@ -22,6 +21,50 @@ const pageSize = 15 const viewing = ref(null) const viewTab = ref<'request' | 'response'>('request') +// cacheLine 缓存读/写的紧凑摘要(换行会碎成多行,只放两个数);无缓存流量返回空串。 +function cacheLine(l: UsageLog): string { + const read = l.cache_read_tokens || 0 + const write = l.cache_creation_tokens || 0 + const parts: string[] = [] + if (read) parts.push(`读${fmtNum(read)}`) + if (write) parts.push(`写${fmtNum(write)}`) + return parts.join(' ') +} + +// cacheExtra 第二行:1h 缓存写(计费 2× 输入)与思考 token,两者都没有则返回空串。 +function cacheExtra(l: UsageLog): string { + const oneHour = l.cache_write_1h_tokens || 0 + const thinking = l.reasoning_tokens || 0 + const parts: string[] = [] + if (oneHour) parts.push(`1h${fmtNum(oneHour)}`) + if (thinking) parts.push(`思考${fmtNum(thinking)}`) + return parts.join(' · ') +} + +// fmtClock 表格里的紧凑时间(月-日 时:分):完整时间在悬浮提示里, +// 否则这一列在 1280 宽下会把「原始」列挤出视野。 +function fmtClock(s?: string | null): string { + if (!s) return '-' + const d = new Date(s) + if (Number.isNaN(d.getTime())) return '-' + return d.toLocaleString('zh-CN', { + month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, + }) +} + +// tokenTitle Token 列的悬浮提示:表格里只放缓存读/写(与成本直接相关), +// 1h 缓存写与思考 token 放提示里,避免把列撑宽挤出其它列。 +function tokenTitle(l: UsageLog): string { + const extra = cacheExtra(l) + const base = `输入 ${l.input_tokens} / 输出 ${l.output_tokens}` + return extra ? `${base} · ${extra}` : base +} + +// servedModel 上游实际应答的模型:仅在模型映射/聚合商改写时与请求模型不同才展示。 +function servedModel(l: UsageLog): string { + return l.served_model && l.served_model !== l.model ? l.served_model : '' +} + function openRaw(l: UsageLog) { viewing.value = l viewTab.value = 'request' @@ -140,6 +183,7 @@ onMounted(load)

{{ l.model }}

+

→ {{ servedModel(l) }}

{{ l.channel }} · {{ l.user }}

@@ -148,8 +192,10 @@ onMounted(load)
- {{ protocolShort(l.protocol) }} + {{ l.protocol }} {{ l.input_tokens }}/{{ l.output_tokens }} tok + 缓存 {{ cacheLine(l) }} + {{ cacheExtra(l) }} {{ l.latency_ms }}ms {{ fmtTime(l.created_at) }} @@ -160,9 +206,9 @@ onMounted(load)