|
{{ t('components.logs.tokenLabels.input') }}
- {{ formatNumber(item.input_tokens) }}
+ {{ formatTokenNumber(item.input_tokens) }}
{{ t('components.logs.tokenLabels.output') }}
- {{ formatNumber(item.output_tokens) }}
+ {{ formatTokenNumber(item.output_tokens) }}
{{ t('components.logs.tokenLabels.reasoning') }}
- {{ formatNumber(item.reasoning_tokens) }}
+ {{ formatTokenNumber(item.reasoning_tokens) }}
{{ t('components.logs.tokenLabels.cacheWrite') }}
- {{ formatNumber(item.cache_create_tokens) }}
+ {{ formatTokenNumber(item.cache_create_tokens) }}
{{ t('components.logs.tokenLabels.cacheRead') }}
- {{ formatNumber(item.cache_read_tokens) }}
+ {{ formatTokenNumber(item.cache_read_tokens) }}
|
@@ -512,6 +512,26 @@ const formatNumber = (value?: number) => {
return value.toLocaleString()
}
+/**
+ * 格式化 token 数值,支持 k/M/B 单位换算
+ * @author sm
+ */
+const formatTokenNumber = (value?: number) => {
+ if (value === undefined || value === null) return '—'
+
+ if (value >= 1_000_000_000) {
+ return `${(value / 1_000_000_000).toFixed(2)}B`
+ }
+ if (value >= 1_000_000) {
+ return `${(value / 1_000_000).toFixed(2)}M`
+ }
+ if (value >= 1_000) {
+ return `${(value / 1_000).toFixed(2)}k`
+ }
+
+ return value.toLocaleString()
+}
+
const formatCurrency = (value?: number) => {
if (value === undefined || value === null || Number.isNaN(value)) {
return '$0.0000'
@@ -547,13 +567,13 @@ const statsCards = computed(() => {
key: 'tokens',
label: t('components.logs.summary.tokens'),
hint: t('components.logs.summary.tokenHint'),
- value: data ? formatNumber(totalTokens) : '—',
+ value: data ? formatTokenNumber(totalTokens) : '—',
},
{
key: 'cacheReads',
label: t('components.logs.summary.cache'),
hint: t('components.logs.summary.cacheHint'),
- value: data ? formatNumber(data.cache_read_tokens) : '—',
+ value: data ? formatTokenNumber(data.cache_read_tokens) : '—',
},
{
key: 'cost',
diff --git a/version_service.go b/version_service.go
index 4bd44f3..a57d86a 100644
--- a/version_service.go
+++ b/version_service.go
@@ -1,6 +1,6 @@
package main
-const AppVersion = "v2.6.14"
+const AppVersion = "v2.6.15"
type VersionService struct {
version string
From 739e73bbc6695ae8979f61bea3c9a477d5aa6f12 Mon Sep 17 00:00:00 2001
From: S <1287773673@qq.com>
Date: Wed, 21 Jan 2026 11:25:15 +0800
Subject: [PATCH 6/9] =?UTF-8?q?=E2=9C=A8=20feat(main):=20=E9=A6=96?=
=?UTF-8?q?=E9=A1=B5=20token=20=E6=95=B0=E5=80=BC=E6=94=AF=E6=8C=81=20k/M/?=
=?UTF-8?q?B=20=E5=8D=95=E4=BD=8D=E6=8D=A2=E7=AE=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 formatTokenNumber 函数到首页组件
- 供应商卡片 Tokens 显示支持单位换算
- 热力图 Tooltip 的 token 指标支持单位换算
- bump version to v2.6.16
---
frontend/src/components/Main/Index.vue | 25 +++++++++++++++++++++----
version_service.go | 2 +-
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/frontend/src/components/Main/Index.vue b/frontend/src/components/Main/Index.vue
index 8014ae1..8e6ef38 100644
--- a/frontend/src/components/Main/Index.vue
+++ b/frontend/src/components/Main/Index.vue
@@ -1279,6 +1279,23 @@ const usageTooltip = reactive({
const formatMetric = (value: number) => value.toLocaleString()
+/**
+ * 格式化 token 数值,支持 k/M/B 单位换算
+ * @author sm
+ */
+const formatTokenNumber = (value: number) => {
+ if (value >= 1_000_000_000) {
+ return `${(value / 1_000_000_000).toFixed(2)}B`
+ }
+ if (value >= 1_000_000) {
+ return `${(value / 1_000_000).toFixed(2)}M`
+ }
+ if (value >= 1_000) {
+ return `${(value / 1_000).toFixed(2)}k`
+ }
+ return value.toLocaleString()
+}
+
const tooltipDateFormatter = computed(() =>
new Intl.DateTimeFormat(locale.value || 'en', {
month: 'short',
@@ -1324,17 +1341,17 @@ const usageTooltipMetrics = computed(() => [
{
key: 'inputTokens',
label: t('components.main.heatmap.metrics.inputTokens'),
- value: formatMetric(usageTooltip.inputTokens),
+ value: formatTokenNumber(usageTooltip.inputTokens),
},
{
key: 'outputTokens',
label: t('components.main.heatmap.metrics.outputTokens'),
- value: formatMetric(usageTooltip.outputTokens),
+ value: formatTokenNumber(usageTooltip.outputTokens),
},
{
key: 'reasoningTokens',
label: t('components.main.heatmap.metrics.reasoningTokens'),
- value: formatMetric(usageTooltip.reasoningTokens),
+ value: formatTokenNumber(usageTooltip.reasoningTokens),
},
])
@@ -2096,7 +2113,7 @@ const providerStatDisplay = (providerName: string): ProviderStatDisplay => {
return {
state: 'ready',
requests: `${t('components.main.providers.requests')}: ${formatMetric(stat.total_requests)}`,
- tokens: `${t('components.main.providers.tokens')}: ${formatMetric(totalTokens)}`,
+ tokens: `${t('components.main.providers.tokens')}: ${formatTokenNumber(totalTokens)}`,
cost: `${t('components.main.providers.cost')}: ${currencyFormatter.value.format(Math.max(stat.cost_total, 0))}`,
successRateLabel,
successRateClass,
diff --git a/version_service.go b/version_service.go
index a57d86a..1872d13 100644
--- a/version_service.go
+++ b/version_service.go
@@ -1,6 +1,6 @@
package main
-const AppVersion = "v2.6.15"
+const AppVersion = "v2.6.16"
type VersionService struct {
version string
From 307753fe16dec8fccc557ebc95fcd45b93a5ee5a Mon Sep 17 00:00:00 2001
From: S <1287773673@qq.com>
Date: Wed, 21 Jan 2026 12:35:03 +0800
Subject: [PATCH 7/9] =?UTF-8?q?=E2=9C=A8=20feat(logs):=20=E5=A2=9E?=
=?UTF-8?q?=E5=BC=BA=E6=97=A5=E5=BF=97=E7=95=8C=E9=9D=A2=E7=BB=9F=E8=AE=A1?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加 TOKEN 流量卡片点击弹窗,展示输入/输出 token 明细
- 缓存卡片显示缓存命中率(格式:1.23M (45.6%))
- 日志列表新增金额列,直观展示每条请求的费用
- 补充中英文国际化文案
---
frontend/src/components/Logs/Index.vue | 126 ++++++++++++++++++++++++-
frontend/src/locales/en.json | 4 +
frontend/src/locales/zh.json | 4 +
3 files changed, 130 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/Logs/Index.vue b/frontend/src/components/Logs/Index.vue
index 51ced83..6a45b8e 100644
--- a/frontend/src/components/Logs/Index.vue
+++ b/frontend/src/components/Logs/Index.vue
@@ -16,8 +16,8 @@
{{ card.label }}
{{ card.value }}
@@ -68,6 +68,7 @@
{{ t('components.logs.table.httpCode') }} |
{{ t('components.logs.table.stream') }} |
{{ t('components.logs.table.duration') }} |
+ {{ t('components.logs.table.cost') }} |
{{ t('components.logs.table.tokens') }} |
@@ -80,6 +81,7 @@
{{ item.http_code }} |
{{ formatStream(item.is_stream) }} |
{{ formatDuration(item.duration_sec) }} |
+ {{ formatCurrency(item.total_cost) }} |
{{ t('components.logs.tokenLabels.input') }}
@@ -104,7 +106,7 @@
|
- | {{ t('components.logs.empty') }} |
+ {{ t('components.logs.empty') }} |
@@ -144,6 +146,26 @@
+
+
+
+
+
+
+ {{ t('components.logs.tokenLabels.input') }}
+ {{ formatTokenNumber(stats?.input_tokens) }}
+
+
+ {{ t('components.logs.tokenLabels.output') }}
+ {{ formatTokenNumber(stats?.output_tokens) }}
+
+
+
+
@@ -201,6 +223,13 @@ const costDetailModal = reactive<{
data: [],
})
+// Token 明细弹窗状态
+const tokenDetailModal = reactive<{
+ open: boolean
+}>({
+ open: false,
+})
+
// 打开金额明细弹窗
const openCostDetailModal = async () => {
costDetailModal.open = true
@@ -225,6 +254,25 @@ const closeCostDetailModal = () => {
costDetailModal.open = false
}
+// 处理卡片点击
+const handleCardClick = (key: string) => {
+ if (key === 'cost') {
+ openCostDetailModal()
+ } else if (key === 'tokens') {
+ openTokenDetailModal()
+ }
+}
+
+// 打开 Token 明细弹窗
+const openTokenDetailModal = () => {
+ tokenDetailModal.open = true
+}
+
+// 关闭 Token 明细弹窗
+const closeTokenDetailModal = () => {
+ tokenDetailModal.open = false
+}
+
const parseLogDate = (value?: string) => {
if (!value) return null
const normalize = value.replace(' ', 'T')
@@ -532,6 +580,24 @@ const formatTokenNumber = (value?: number) => {
return value.toLocaleString()
}
+/**
+ * 计算缓存命中率
+ * @param cacheRead 缓存读取 token 数
+ * @param inputTokens 输入 token 数
+ * @returns 命中率百分比字符串
+ * @author sm
+ */
+const formatCacheHitRate = (cacheRead?: number, inputTokens?: number) => {
+ const read = cacheRead ?? 0
+ const input = inputTokens ?? 0
+ const total = read + input
+
+ if (total === 0) return '0%'
+
+ const rate = (read / total) * 100
+ return `${rate.toFixed(1)}%`
+}
+
const formatCurrency = (value?: number) => {
if (value === undefined || value === null || Number.isNaN(value)) {
return '$0.0000'
@@ -573,7 +639,9 @@ const statsCards = computed(() => {
key: 'cacheReads',
label: t('components.logs.summary.cache'),
hint: t('components.logs.summary.cacheHint'),
- value: data ? formatTokenNumber(data.cache_read_tokens) : '—',
+ value: data
+ ? `${formatTokenNumber(data.cache_read_tokens)} (${formatCacheHitRate(data.cache_read_tokens, data.input_tokens)})`
+ : '—',
},
{
key: 'cost',
@@ -757,4 +825,54 @@ html.dark .cost-detail-item__name {
color: #f97316;
font-variant-numeric: tabular-nums;
}
+
+/* Token 弹窗 */
+.token-detail-modal {
+ min-height: 80px;
+}
+.token-detail-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+.token-detail-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.75rem 1rem;
+ background: rgba(148, 163, 184, 0.08);
+ border-radius: 8px;
+ transition: background 0.15s ease;
+}
+.token-detail-item:hover {
+ background: rgba(148, 163, 184, 0.12);
+}
+html.dark .token-detail-item {
+ background: rgba(148, 163, 184, 0.12);
+}
+html.dark .token-detail-item:hover {
+ background: rgba(148, 163, 184, 0.18);
+}
+.token-detail-item__name {
+ font-weight: 500;
+ color: #1e293b;
+}
+html.dark .token-detail-item__name {
+ color: #f1f5f9;
+}
+.token-detail-item__value {
+ font-weight: 600;
+ color: #34d399;
+ font-variant-numeric: tabular-nums;
+}
+
+/* 金额列 */
+.col-cost {
+ width: 80px;
+}
+.cost-cell {
+ color: #f97316;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+}
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 6dec570..7968765 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -495,6 +495,7 @@
"httpCode": "HTTP",
"stream": "Stream",
"duration": "Duration",
+ "cost": "Cost",
"tokens": "Tokens"
},
"tokenLabels": {
@@ -513,6 +514,9 @@
"costDetail": {
"title": "Today's Cost Breakdown",
"empty": "No cost records today"
+ },
+ "tokenDetail": {
+ "title": "Today's Token Breakdown"
}
},
"general": {
diff --git a/frontend/src/locales/zh.json b/frontend/src/locales/zh.json
index 3fcac5e..e377758 100644
--- a/frontend/src/locales/zh.json
+++ b/frontend/src/locales/zh.json
@@ -496,6 +496,7 @@
"httpCode": "HTTP",
"stream": "传输",
"duration": "耗时",
+ "cost": "金额",
"tokens": "Token 汇总"
},
"tokenLabels": {
@@ -514,6 +515,9 @@
"costDetail": {
"title": "今日消费明细",
"empty": "今日暂无消费记录"
+ },
+ "tokenDetail": {
+ "title": "今日 Token 明细"
}
},
"general": {
From cef3feafd8f08a7bab3a1091dabed12e002c6113 Mon Sep 17 00:00:00 2001
From: S <1287773673@qq.com>
Date: Wed, 21 Jan 2026 12:50:10 +0800
Subject: [PATCH 8/9] =?UTF-8?q?=F0=9F=92=84=20style(logs):=20=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E7=BC=93=E5=AD=98=E5=91=BD=E4=B8=AD=E7=8E=87=E6=98=BE?=
=?UTF-8?q?=E7=A4=BA=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 将命中率与缓存数字分开显示
- 命中率使用较小字体和浅色
- 支持亮色/暗色主题
---
frontend/src/components/Logs/Index.vue | 21 +++++++++++++++++----
1 file changed, 17 insertions(+), 4 deletions(-)
diff --git a/frontend/src/components/Logs/Index.vue b/frontend/src/components/Logs/Index.vue
index 6a45b8e..a265155 100644
--- a/frontend/src/components/Logs/Index.vue
+++ b/frontend/src/components/Logs/Index.vue
@@ -20,7 +20,10 @@
@click="handleCardClick(card.key)"
>
{{ card.label }}
- {{ card.value }}
+
+ {{ card.value }}
+ ({{ card.subValue }})
+
{{ card.hint }}
@@ -639,9 +642,8 @@ const statsCards = computed(() => {
key: 'cacheReads',
label: t('components.logs.summary.cache'),
hint: t('components.logs.summary.cacheHint'),
- value: data
- ? `${formatTokenNumber(data.cache_read_tokens)} (${formatCacheHitRate(data.cache_read_tokens, data.input_tokens)})`
- : '—',
+ value: data ? formatTokenNumber(data.cache_read_tokens) : '—',
+ subValue: data ? formatCacheHitRate(data.cache_read_tokens, data.input_tokens) : '',
},
{
key: 'cost',
@@ -734,6 +736,13 @@ onUnmounted(() => {
color: #94a3b8;
}
+.summary-card__sub-value {
+ font-size: 0.65em;
+ font-weight: 400;
+ color: #64748b;
+ margin-left: 0.25rem;
+}
+
html.dark .summary-card {
border-color: rgba(255, 255, 255, 0.12);
background: radial-gradient(circle at top, rgba(148, 163, 184, 0.2), rgba(15, 23, 42, 0.35));
@@ -751,6 +760,10 @@ html.dark .summary-card__hint {
color: rgba(186, 194, 210, 0.8);
}
+html.dark .summary-card__sub-value {
+ color: #94a3b8;
+}
+
@media (max-width: 768px) {
.logs-summary {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
From aa48cc60b061a26f95b041dde76ba81107f067ea Mon Sep 17 00:00:00 2001
From: S <1287773673@qq.com>
Date: Thu, 22 Jan 2026 12:40:17 +0800
Subject: [PATCH 9/9] =?UTF-8?q?=E2=9C=A8=20feat(tray):=20=E6=B7=BB?=
=?UTF-8?q?=E5=8A=A0=E6=89=98=E7=9B=98=E8=8F=9C=E5=8D=95=E9=A2=84=E7=AE=97?=
=?UTF-8?q?=E8=BF=9B=E5=BA=A6=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
新增托盘菜单显示今日用量与预算比例的功能
- 添加 budget_total 应用设置项,支持在通用设置中配置预算总额(USD)
- 托盘菜单显示今日已用金额和预算总额
- 使用 ASCII 进度条直观展示用量占比(28 字符宽度)
- 点击托盘图标时自动刷新用量数据
- 前端增加预算输入框及国际化支持
此功能帮助用户直观监控每日 API 消费情况,便于成本控制
---
.claude/settings.local.json | 4 +-
.gitignore | 1 +
frontend/src/components/General/Index.vue | 68 ++++++++++++++
frontend/src/locales/en.json | 2 +
frontend/src/locales/zh.json | 2 +
frontend/src/services/appSettings.ts | 2 +
main.go | 109 ++++++++++++++++++----
services/appsettings.go | 2 +
version_service.go | 2 +-
9 files changed, 174 insertions(+), 18 deletions(-)
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index bc967eb..c260414 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -2,7 +2,9 @@
"permissions": {
"allow": [
"WebSearch",
- "Bash(find:*)"
+ "Bash(find:*)",
+ "mcp__exa__web_search_exa",
+ "mcp__ace-tool__search_context"
],
"deny": [],
"ask": []
diff --git a/.gitignore b/.gitignore
index 3444dd2..040cb4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ frontend/dist
.task
bin
CLAUDE.md
+.ace-tool/
diff --git a/frontend/src/components/General/Index.vue b/frontend/src/components/General/Index.vue
index 98deea3..a2a11de 100644
--- a/frontend/src/components/General/Index.vue
+++ b/frontend/src/components/General/Index.vue
@@ -22,6 +22,12 @@ const getCachedValue = (key: string, defaultValue: boolean): boolean => {
const cached = localStorage.getItem(`app-settings-${key}`)
return cached !== null ? cached === 'true' : defaultValue
}
+const getCachedNumber = (key: string, defaultValue: number): number => {
+ const cached = localStorage.getItem(`app-settings-${key}`)
+ if (cached === null) return defaultValue
+ const parsed = Number(cached)
+ return Number.isFinite(parsed) ? parsed : defaultValue
+}
const heatmapEnabled = ref(getCachedValue('heatmap', true))
const homeTitleVisible = ref(getCachedValue('homeTitle', true))
const autoStartEnabled = ref(getCachedValue('autoStart', false))
@@ -29,6 +35,7 @@ const autoUpdateEnabled = ref(getCachedValue('autoUpdate', true))
const autoConnectivityTestEnabled = ref(getCachedValue('autoConnectivityTest', false))
const switchNotifyEnabled = ref(getCachedValue('switchNotify', true)) // 切换通知开关
const roundRobinEnabled = ref(getCachedValue('roundRobin', false)) // 同 Level 轮询开关
+const budgetTotal = ref(getCachedNumber('budgetTotal', 0))
const settingsLoading = ref(true)
const saveBusy = ref(false)
@@ -62,6 +69,7 @@ const loadAppSettings = async () => {
const data = await fetchAppSettings()
heatmapEnabled.value = data?.show_heatmap ?? true
homeTitleVisible.value = data?.show_home_title ?? true
+ budgetTotal.value = Number(data?.budget_total ?? 0)
autoStartEnabled.value = data?.auto_start ?? false
autoUpdateEnabled.value = data?.auto_update ?? true
autoConnectivityTestEnabled.value = data?.auto_connectivity_test ?? false
@@ -71,6 +79,7 @@ const loadAppSettings = async () => {
// 缓存到 localStorage,下次打开时直接显示正确状态
localStorage.setItem('app-settings-heatmap', String(heatmapEnabled.value))
localStorage.setItem('app-settings-homeTitle', String(homeTitleVisible.value))
+ localStorage.setItem('app-settings-budgetTotal', String(budgetTotal.value))
localStorage.setItem('app-settings-autoStart', String(autoStartEnabled.value))
localStorage.setItem('app-settings-autoUpdate', String(autoUpdateEnabled.value))
localStorage.setItem('app-settings-autoConnectivityTest', String(autoConnectivityTestEnabled.value))
@@ -80,6 +89,7 @@ const loadAppSettings = async () => {
console.error('failed to load app settings', error)
heatmapEnabled.value = true
homeTitleVisible.value = true
+ budgetTotal.value = 0
autoStartEnabled.value = false
autoUpdateEnabled.value = true
autoConnectivityTestEnabled.value = false
@@ -94,9 +104,12 @@ const persistAppSettings = async () => {
if (settingsLoading.value || saveBusy.value) return
saveBusy.value = true
try {
+ const normalizedBudgetTotal = Number.isFinite(budgetTotal.value) ? Math.max(0, budgetTotal.value) : 0
+ budgetTotal.value = normalizedBudgetTotal
const payload: AppSettings = {
show_heatmap: heatmapEnabled.value,
show_home_title: homeTitleVisible.value,
+ budget_total: normalizedBudgetTotal,
auto_start: autoStartEnabled.value,
auto_update: autoUpdateEnabled.value,
auto_connectivity_test: autoConnectivityTestEnabled.value,
@@ -117,6 +130,7 @@ const persistAppSettings = async () => {
// 更新缓存
localStorage.setItem('app-settings-heatmap', String(heatmapEnabled.value))
localStorage.setItem('app-settings-homeTitle', String(homeTitleVisible.value))
+ localStorage.setItem('app-settings-budgetTotal', String(budgetTotal.value))
localStorage.setItem('app-settings-autoStart', String(autoStartEnabled.value))
localStorage.setItem('app-settings-autoUpdate', String(autoUpdateEnabled.value))
localStorage.setItem('app-settings-autoConnectivityTest', String(autoConnectivityTestEnabled.value))
@@ -416,6 +430,24 @@ onMounted(async () => {