diff --git a/messages/en/common.json b/messages/en/common.json index c12dd0b66..82af21d1e 100644 --- a/messages/en/common.json +++ b/messages/en/common.json @@ -4,6 +4,7 @@ "delete": "Delete", "confirm": "Confirm", "edit": "Edit", + "status": "Status", "create": "Create", "close": "Close", "back": "Back", diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 2cf6b7930..1fff423b0 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -1225,8 +1225,23 @@ "columns": { "model": "Model", "calls": "Calls", + "tokens": "Tokens", "cost": "Cost" }, + "modal": { + "requests": "Requests", + "totalTokens": "Total Tokens", + "cost": "Cost", + "inputTokens": "Input Tokens", + "outputTokens": "Output Tokens", + "cacheWrite": "Cache Write", + "cacheRead": "Cache Read", + "cacheHitRate": "Cache Hit Rate", + "cacheTokens": "Cache Tokens", + "performanceHigh": "High", + "performanceMedium": "Medium", + "performanceLow": "Low" + }, "noData": "No usage records today", "totalCalls": "Total Calls", "totalCost": "Total Cost" @@ -1577,7 +1592,8 @@ "clickToEnableUser": "Click to enable user", "operationFailed": "Operation failed", "deleteFailed": "Delete failed", - "deleteSuccess": "Delete successful" + "deleteSuccess": "Delete successful", + "daysLeft": "{days, plural, =0 {Expires today} =1 {1 day left} other {# days left}}" }, "userEditSection": { "sections": { diff --git a/messages/en/myUsage.json b/messages/en/myUsage.json index 2d6aaab68..0ebe076a2 100644 --- a/messages/en/myUsage.json +++ b/messages/en/myUsage.json @@ -78,7 +78,7 @@ "inheritedFromUser": "Inherited from User" }, "stats": { - "title": "Statistics Summary", + "title": "Key Statistics", "autoRefresh": "Auto refresh every {seconds}s", "totalRequests": "Total Requests", "totalCost": "Total Cost", @@ -116,7 +116,7 @@ "noRestrictions": "No restrictions" }, "quotaCollapsible": { - "title": "Quota Usage", + "title": "User Quota Usage", "daily": "Daily", "monthly": "Monthly", "total": "Total" diff --git a/messages/en/settings/data.json b/messages/en/settings/data.json index 61bbf7b55..45dc7b584 100644 --- a/messages/en/settings/data.json +++ b/messages/en/settings/data.json @@ -123,6 +123,7 @@ }, "status": { "connected": "Database connected", + "connectionUnavailable": "Database connection unavailable, please check database service status", "error": "Failed to get database status", "loading": "Loading...", "retry": "Retry", diff --git a/messages/en/settings/index.ts b/messages/en/settings/index.ts index db8bf1a03..47a6b5424 100644 --- a/messages/en/settings/index.ts +++ b/messages/en/settings/index.ts @@ -13,6 +13,7 @@ import sensitiveWords from "./sensitiveWords.json"; import strings from "./strings.json"; import providersAutoSort from "./providers/autoSort.json"; +import providersBatchEdit from "./providers/batchEdit.json"; import providersFilter from "./providers/filter.json"; import providersGuide from "./providers/guide.json"; import providersInlineEdit from "./providers/inlineEdit.json"; @@ -74,6 +75,7 @@ const providersForm = { const providers = { ...providersStrings, autoSort: providersAutoSort, + batchEdit: providersBatchEdit, filter: providersFilter, form: providersForm, guide: providersGuide, diff --git a/messages/en/settings/providers/form/sections.json b/messages/en/settings/providers/form/sections.json index 62dfad529..36b9c930c 100644 --- a/messages/en/settings/providers/form/sections.json +++ b/messages/en/settings/providers/form/sections.json @@ -284,7 +284,7 @@ "placeholder": "1.0" }, "group": { - "desc": "Group tag. Only users whose providerGroup matches can use this provider. Example: set to \"premium\" to serve users with providerGroup=\"premium\" only", + "desc": "Group tag. Select from list or type a new name and press Enter to create (max 50 chars). Only users whose providerGroup matches can use this provider.", "label": "Provider Group", "placeholder": "e.g. premium, economy" }, diff --git a/messages/ja/common.json b/messages/ja/common.json index c3442e2db..aa8a5bf1a 100644 --- a/messages/ja/common.json +++ b/messages/ja/common.json @@ -4,6 +4,7 @@ "delete": "削除", "confirm": "確認", "edit": "編集", + "status": "ステータス", "create": "作成", "close": "閉じる", "back": "戻る", diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 52c6e2a09..f3e9654e0 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -1206,8 +1206,23 @@ "columns": { "model": "モデル", "calls": "呼び出し回数", + "tokens": "トークン", "cost": "消費金額" }, + "modal": { + "requests": "リクエスト", + "totalTokens": "トークン合計", + "cost": "コスト", + "inputTokens": "入力トークン", + "outputTokens": "出力トークン", + "cacheWrite": "キャッシュ書込", + "cacheRead": "キャッシュ読取", + "cacheHitRate": "キャッシュヒット率", + "cacheTokens": "キャッシュトークン", + "performanceHigh": "高", + "performanceMedium": "中", + "performanceLow": "低" + }, "noData": "本日の使用記録はありません", "totalCalls": "総呼び出し数", "totalCost": "総消費" @@ -1512,7 +1527,8 @@ "clickToEnableUser": "クリックしてユーザーを有効化", "operationFailed": "操作に失敗しました", "deleteFailed": "削除に失敗しました", - "deleteSuccess": "削除しました" + "deleteSuccess": "削除しました", + "daysLeft": "{days, plural, =0 {本日期限} =1 {残り1日} other {残り#日}}" }, "userEditSection": { "sections": { diff --git a/messages/ja/myUsage.json b/messages/ja/myUsage.json index 66091789a..4d0b1bb7e 100644 --- a/messages/ja/myUsage.json +++ b/messages/ja/myUsage.json @@ -78,7 +78,7 @@ "inheritedFromUser": "ユーザーから継承" }, "stats": { - "title": "統計サマリー", + "title": "キー統計", "autoRefresh": "{seconds}秒ごとに自動更新", "totalRequests": "リクエスト総数", "totalCost": "総コスト", @@ -116,7 +116,7 @@ "noRestrictions": "制限なし" }, "quotaCollapsible": { - "title": "クォータ使用状況", + "title": "ユーザークォータ使用状況", "daily": "日次", "monthly": "月次", "total": "合計" diff --git a/messages/ja/settings/data.json b/messages/ja/settings/data.json index de55c4a7a..5bce34914 100644 --- a/messages/ja/settings/data.json +++ b/messages/ja/settings/data.json @@ -123,6 +123,7 @@ }, "status": { "connected": "データベース接続正常", + "connectionUnavailable": "データベース接続が利用できません。データベースサービスの状態を確認してください", "error": "データベースステータスの取得に失敗しました", "loading": "読み込み中...", "retry": "再試行", diff --git a/messages/ja/settings/index.ts b/messages/ja/settings/index.ts index db8bf1a03..47a6b5424 100644 --- a/messages/ja/settings/index.ts +++ b/messages/ja/settings/index.ts @@ -13,6 +13,7 @@ import sensitiveWords from "./sensitiveWords.json"; import strings from "./strings.json"; import providersAutoSort from "./providers/autoSort.json"; +import providersBatchEdit from "./providers/batchEdit.json"; import providersFilter from "./providers/filter.json"; import providersGuide from "./providers/guide.json"; import providersInlineEdit from "./providers/inlineEdit.json"; @@ -74,6 +75,7 @@ const providersForm = { const providers = { ...providersStrings, autoSort: providersAutoSort, + batchEdit: providersBatchEdit, filter: providersFilter, form: providersForm, guide: providersGuide, diff --git a/messages/ja/settings/providers/form/sections.json b/messages/ja/settings/providers/form/sections.json index 84c69e2bf..8256c811c 100644 --- a/messages/ja/settings/providers/form/sections.json +++ b/messages/ja/settings/providers/form/sections.json @@ -285,7 +285,7 @@ "placeholder": "1.0" }, "group": { - "desc": "グループタグ。ユーザーの providerGroup が一致する場合のみ利用可能。例: \"premium\" に設定すると providerGroup=\"premium\" のユーザーのみ対象", + "desc": "グループタグ。リストから選択するか、新しい名前を入力して Enter で作成(最大50文字)。providerGroup が一致するユーザーのみがこのプロバイダーを使用できます。", "label": "プロバイダーグループ", "placeholder": "例: premium, economy" }, diff --git a/messages/ru/common.json b/messages/ru/common.json index 86c097d5c..06661c31f 100644 --- a/messages/ru/common.json +++ b/messages/ru/common.json @@ -4,6 +4,7 @@ "delete": "Удалить", "confirm": "Подтвердить", "edit": "Редактировать", + "status": "Статус", "create": "Создать", "close": "Закрыть", "back": "Назад", diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 9a93d4299..3625e6466 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -1213,8 +1213,23 @@ "columns": { "model": "Модель", "calls": "Вызовы", + "tokens": "Токены", "cost": "Стоимость" }, + "modal": { + "requests": "Запросов", + "totalTokens": "Всего токенов", + "cost": "Стоимость", + "inputTokens": "Входные токены", + "outputTokens": "Выходные токены", + "cacheWrite": "Запись кэша", + "cacheRead": "Чтение кэша", + "cacheHitRate": "Попадание кэша", + "cacheTokens": "Токены кэша", + "performanceHigh": "Высокий", + "performanceMedium": "Средний", + "performanceLow": "Низкий" + }, "noData": "Нет записей использования за сегодня", "totalCalls": "Всего вызовов", "totalCost": "Общий расход" @@ -1565,7 +1580,8 @@ "clickToEnableUser": "Нажмите, чтобы включить пользователя", "operationFailed": "Операция не удалась", "deleteFailed": "Не удалось удалить", - "deleteSuccess": "Удаление успешно" + "deleteSuccess": "Удаление успешно", + "daysLeft": "{days, plural, =0 {Истекает сегодня} =1 {Остался 1 день} few {Осталось # дня} many {Осталось # дней} other {Осталось # дней}}" }, "userEditSection": { "sections": { diff --git a/messages/ru/myUsage.json b/messages/ru/myUsage.json index 886d1eeb6..bb3b61bd7 100644 --- a/messages/ru/myUsage.json +++ b/messages/ru/myUsage.json @@ -78,7 +78,7 @@ "inheritedFromUser": "Наследовано от пользователя" }, "stats": { - "title": "Сводка статистики", + "title": "Статистика ключа", "autoRefresh": "Автообновление каждые {seconds}с", "totalRequests": "Всего запросов", "totalCost": "Общая стоимость", @@ -116,7 +116,7 @@ "noRestrictions": "Без ограничений" }, "quotaCollapsible": { - "title": "Использование квоты", + "title": "Квота пользователя", "daily": "День", "monthly": "Месяц", "total": "Всего" diff --git a/messages/ru/settings/data.json b/messages/ru/settings/data.json index 08d92f68a..1a2b10505 100644 --- a/messages/ru/settings/data.json +++ b/messages/ru/settings/data.json @@ -123,6 +123,7 @@ }, "status": { "connected": "База данных подключена", + "connectionUnavailable": "Подключение к базе данных недоступно, проверьте состояние сервиса базы данных", "error": "Не удалось получить статус базы данных", "loading": "Загрузка...", "retry": "Повторить", diff --git a/messages/ru/settings/index.ts b/messages/ru/settings/index.ts index db8bf1a03..47a6b5424 100644 --- a/messages/ru/settings/index.ts +++ b/messages/ru/settings/index.ts @@ -13,6 +13,7 @@ import sensitiveWords from "./sensitiveWords.json"; import strings from "./strings.json"; import providersAutoSort from "./providers/autoSort.json"; +import providersBatchEdit from "./providers/batchEdit.json"; import providersFilter from "./providers/filter.json"; import providersGuide from "./providers/guide.json"; import providersInlineEdit from "./providers/inlineEdit.json"; @@ -74,6 +75,7 @@ const providersForm = { const providers = { ...providersStrings, autoSort: providersAutoSort, + batchEdit: providersBatchEdit, filter: providersFilter, form: providersForm, guide: providersGuide, diff --git a/messages/ru/settings/providers/form/sections.json b/messages/ru/settings/providers/form/sections.json index afea91597..ff0744a8e 100644 --- a/messages/ru/settings/providers/form/sections.json +++ b/messages/ru/settings/providers/form/sections.json @@ -285,9 +285,9 @@ "placeholder": "1.0" }, "group": { - "desc": "Метка группы. Пользователь может использовать провайдера только если его providerGroup совпадает. Пример: значение \"premium\" — только для пользователей с providerGroup=\"premium\"", + "desc": "Тег группы. Выберите из списка или введите новое имя и нажмите Enter для создания (макс. 50 символов). Только пользователи с соответствующим providerGroup могут использовать этого провайдера.", "label": "Группа провайдера", - "placeholder": "например: premium, economy" + "placeholder": "напр. premium, economy" }, "priority": { "desc": "Меньше — выше приоритет (0 — наивысший). Система выбирает только из провайдеров с максимальным приоритетом. Рекомендации: основной=0, резерв=1, аварийный=2", diff --git a/messages/zh-CN/common.json b/messages/zh-CN/common.json index 75c7c9abd..dc04ab262 100644 --- a/messages/zh-CN/common.json +++ b/messages/zh-CN/common.json @@ -4,6 +4,7 @@ "delete": "删除", "confirm": "确认", "edit": "编辑", + "status": "状态", "create": "创建", "close": "关闭", "back": "返回", diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 9b04c2c95..fc0ae7829 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -1226,8 +1226,23 @@ "columns": { "model": "模型", "calls": "调用次数", + "tokens": "Token数", "cost": "消费金额" }, + "modal": { + "requests": "请求", + "totalTokens": "总Token", + "cost": "费用", + "inputTokens": "输入Token", + "outputTokens": "输出Token", + "cacheWrite": "缓存写入", + "cacheRead": "缓存读取", + "cacheHitRate": "缓存命中率", + "cacheTokens": "缓存Token", + "performanceHigh": "高", + "performanceMedium": "中", + "performanceLow": "低" + }, "noData": "今日暂无使用记录", "totalCalls": "总调用", "totalCost": "总消费" @@ -1536,7 +1551,8 @@ "clickToEnableUser": "点击启用用户", "operationFailed": "操作失败", "deleteFailed": "删除失败", - "deleteSuccess": "删除成功" + "deleteSuccess": "删除成功", + "daysLeft": "{days, plural, =0 {今天到期} =1 {剩余1天} other {剩余#天}}" }, "userEditSection": { "sections": { diff --git a/messages/zh-CN/myUsage.json b/messages/zh-CN/myUsage.json index 40740dbe1..9eaaf7925 100644 --- a/messages/zh-CN/myUsage.json +++ b/messages/zh-CN/myUsage.json @@ -78,7 +78,7 @@ "inheritedFromUser": "继承自用户" }, "stats": { - "title": "统计摘要", + "title": "密钥统计", "autoRefresh": "每{seconds}秒自动刷新", "totalRequests": "总请求数", "totalCost": "总费用", @@ -116,7 +116,7 @@ "noRestrictions": "无限制" }, "quotaCollapsible": { - "title": "配额使用", + "title": "用户配额使用", "daily": "日", "monthly": "月", "total": "总计" diff --git a/messages/zh-CN/settings/data.json b/messages/zh-CN/settings/data.json index 147ce7927..ef8dcc158 100644 --- a/messages/zh-CN/settings/data.json +++ b/messages/zh-CN/settings/data.json @@ -4,6 +4,7 @@ "status": { "loading": "加载中...", "error": "获取数据库状态失败", + "connectionUnavailable": "数据库连接不可用,请检查数据库服务状态", "retry": "重试", "connected": "数据库连接正常", "unavailable": "数据库不可用", diff --git a/messages/zh-CN/settings/index.ts b/messages/zh-CN/settings/index.ts index db8bf1a03..47a6b5424 100644 --- a/messages/zh-CN/settings/index.ts +++ b/messages/zh-CN/settings/index.ts @@ -13,6 +13,7 @@ import sensitiveWords from "./sensitiveWords.json"; import strings from "./strings.json"; import providersAutoSort from "./providers/autoSort.json"; +import providersBatchEdit from "./providers/batchEdit.json"; import providersFilter from "./providers/filter.json"; import providersGuide from "./providers/guide.json"; import providersInlineEdit from "./providers/inlineEdit.json"; @@ -74,6 +75,7 @@ const providersForm = { const providers = { ...providersStrings, autoSort: providersAutoSort, + batchEdit: providersBatchEdit, filter: providersFilter, form: providersForm, guide: providersGuide, diff --git a/messages/zh-CN/settings/providers/form/sections.json b/messages/zh-CN/settings/providers/form/sections.json index 831e8463a..4912e7367 100644 --- a/messages/zh-CN/settings/providers/form/sections.json +++ b/messages/zh-CN/settings/providers/form/sections.json @@ -69,8 +69,8 @@ }, "group": { "label": "供应商分组", - "placeholder": "输入分组标签", - "desc": "供应商分组标签(支持多个,逗号分隔)。只有用户的 providerGroup 与此值匹配时,该用户才能使用此供应商。留空=对所有用户开放" + "placeholder": "例如 premium, economy", + "desc": "分组标签。从列表选择或输入新名称后按 Enter 创建(最多50字符)。只有 providerGroup 匹配的用户才能使用此供应商。" } }, "cacheTtl": { diff --git a/messages/zh-TW/common.json b/messages/zh-TW/common.json index 63f549c18..27aa34de4 100644 --- a/messages/zh-TW/common.json +++ b/messages/zh-TW/common.json @@ -4,6 +4,7 @@ "delete": "刪除", "confirm": "確認", "edit": "編輯", + "status": "狀態", "create": "建立", "close": "關閉", "back": "返回", diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index 10ae12463..8d5ef0137 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -1211,8 +1211,23 @@ "columns": { "model": "Model", "calls": "呼叫次數", + "tokens": "Token", "cost": "消費金額" }, + "modal": { + "requests": "請求", + "totalTokens": "總Token", + "cost": "費用", + "inputTokens": "輸入Token", + "outputTokens": "輸出Token", + "cacheWrite": "快取寫入", + "cacheRead": "快取讀取", + "cacheHitRate": "快取命中率", + "cacheTokens": "快取Token", + "performanceHigh": "高", + "performanceMedium": "中", + "performanceLow": "低" + }, "noData": "今日暫無使用記錄", "totalCalls": "今日總呼叫", "totalCost": "今日總消費" @@ -1521,7 +1536,8 @@ "clickToEnableUser": "點擊啟用使用者", "operationFailed": "操作失敗", "deleteFailed": "刪除失敗", - "deleteSuccess": "刪除成功" + "deleteSuccess": "刪除成功", + "daysLeft": "{days, plural, =0 {今天到期} =1 {剩餘1天} other {剩餘#天}}" }, "userEditSection": { "sections": { diff --git a/messages/zh-TW/myUsage.json b/messages/zh-TW/myUsage.json index 4c473efa0..b5247a160 100644 --- a/messages/zh-TW/myUsage.json +++ b/messages/zh-TW/myUsage.json @@ -78,7 +78,7 @@ "inheritedFromUser": "繼承自使用者" }, "stats": { - "title": "統計摘要", + "title": "金鑰統計", "autoRefresh": "每{seconds}秒自動刷新", "totalRequests": "總請求數", "totalCost": "總費用", @@ -116,7 +116,7 @@ "noRestrictions": "無限制" }, "quotaCollapsible": { - "title": "配額使用", + "title": "用戶配額使用", "daily": "每日", "monthly": "每月", "total": "總計" diff --git a/messages/zh-TW/settings/data.json b/messages/zh-TW/settings/data.json index 11fdc3110..992039f0c 100644 --- a/messages/zh-TW/settings/data.json +++ b/messages/zh-TW/settings/data.json @@ -123,6 +123,7 @@ }, "status": { "connected": "資料庫連線正常", + "connectionUnavailable": "資料庫連線不可用,請檢查資料庫服務狀態", "error": "取得資料庫狀態失敗", "loading": "載入中...", "retry": "重試", diff --git a/messages/zh-TW/settings/index.ts b/messages/zh-TW/settings/index.ts index db8bf1a03..47a6b5424 100644 --- a/messages/zh-TW/settings/index.ts +++ b/messages/zh-TW/settings/index.ts @@ -13,6 +13,7 @@ import sensitiveWords from "./sensitiveWords.json"; import strings from "./strings.json"; import providersAutoSort from "./providers/autoSort.json"; +import providersBatchEdit from "./providers/batchEdit.json"; import providersFilter from "./providers/filter.json"; import providersGuide from "./providers/guide.json"; import providersInlineEdit from "./providers/inlineEdit.json"; @@ -74,6 +75,7 @@ const providersForm = { const providers = { ...providersStrings, autoSort: providersAutoSort, + batchEdit: providersBatchEdit, filter: providersFilter, form: providersForm, guide: providersGuide, diff --git a/messages/zh-TW/settings/providers/form/sections.json b/messages/zh-TW/settings/providers/form/sections.json index e5cb7ba6a..a6e7a1e38 100644 --- a/messages/zh-TW/settings/providers/form/sections.json +++ b/messages/zh-TW/settings/providers/form/sections.json @@ -285,9 +285,9 @@ "placeholder": "1.0" }, "group": { - "desc": "分組標籤。僅供 providerGroup 與此值相符的用戶使用。例:設為「premium」表示僅供 providerGroup=\"premium\" 的用戶使用", + "desc": "分組標籤。從列表選擇或輸入新名稱後按 Enter 創建(最多50字符)。只有 providerGroup 匹配的用戶才能使用此供應商。", "label": "供應商分組", - "placeholder": "例如:premium, economy" + "placeholder": "例如 premium, economy" }, "priority": { "desc": "數值越小,優先級越高(0 最高)。系統只會從最高優先級的供應商中選擇。建議:主力=0,備用=1,緊急備援=2", diff --git a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx index 30317ba3e..6f6554f57 100644 --- a/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx +++ b/src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import * as React from "react"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { type ChartConfig, ChartContainer, ChartTooltip } from "@/components/ui/chart"; @@ -41,6 +41,7 @@ export function StatisticsChartCard({ className, }: StatisticsChartCardProps) { const t = useTranslations("dashboard.statistics"); + const locale = useLocale(); const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost"); const [chartMode, setChartMode] = React.useState<"stacked" | "overlay">("overlay"); @@ -151,22 +152,22 @@ export function StatisticsChartCard({ const formatDate = (dateStr: string) => { const date = new Date(dateStr); if (data.resolution === "hour") { - return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); + return date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }); } - return date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" }); + return date.toLocaleDateString(locale, { month: "numeric", day: "numeric" }); }; const formatTooltipDate = (dateStr: string) => { const date = new Date(dateStr); if (data.resolution === "hour") { - return date.toLocaleString("zh-CN", { + return date.toLocaleString(locale, { month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", }); } - return date.toLocaleDateString("zh-CN", { + return date.toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", diff --git a/src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx b/src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx index 13da7ea0e..485458fd8 100644 --- a/src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx +++ b/src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import * as React from "react"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -11,24 +11,29 @@ export interface RateLimitEventsChartProps { data: EventTimeline[]; } -const chartConfig = { - count: { - label: "限流事件数", - color: "hsl(var(--chart-1))", - }, -} satisfies ChartConfig; - /** * 限流事件时间线图表 * 使用 Recharts AreaChart 显示小时级别的限流事件趋势 */ export function RateLimitEventsChart({ data }: RateLimitEventsChartProps) { const t = useTranslations("dashboard.rateLimits.chart"); + const locale = useLocale(); + + const chartConfig = React.useMemo( + () => + ({ + count: { + label: t("events"), + color: "hsl(var(--chart-1))", + }, + }) satisfies ChartConfig, + [t] + ); // 格式化小时显示 const formatHour = (hourStr: string) => { const date = new Date(hourStr); - return date.toLocaleTimeString("zh-CN", { + return date.toLocaleTimeString(locale, { month: "numeric", day: "numeric", hour: "2-digit", @@ -39,7 +44,7 @@ export function RateLimitEventsChart({ data }: RateLimitEventsChartProps) { // 格式化 tooltip 显示 const formatTooltipHour = (hourStr: string) => { const date = new Date(hourStr); - return date.toLocaleString("zh-CN", { + return date.toLocaleString(locale, { year: "numeric", month: "long", day: "numeric", diff --git a/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx b/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx index 654f9e334..b7f04554a 100644 --- a/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx +++ b/src/app/[locale]/dashboard/_components/rate-limit-top-users.tsx @@ -1,7 +1,7 @@ "use client"; import { ArrowUpDown } from "lucide-react"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import * as React from "react"; import { getUsers } from "@/actions/users"; import { Button } from "@/components/ui/button"; @@ -28,6 +28,7 @@ type SortDirection = "asc" | "desc"; */ export function RateLimitTopUsers({ data }: RateLimitTopUsersProps) { const t = useTranslations("dashboard.rateLimits.topUsers"); + const locale = useLocale(); const [users, setUsers] = React.useState>([]); const [loading, setLoading] = React.useState(true); const [sortField, setSortField] = React.useState("count"); @@ -53,14 +54,14 @@ export function RateLimitTopUsers({ data }: RateLimitTopUsersProps) { })) .sort((a, b) => { if (sortField === "name") { - const comparison = a.username.localeCompare(b.username, "zh-CN"); + const comparison = a.username.localeCompare(b.username, locale); return sortDirection === "asc" ? comparison : -comparison; } else { const comparison = a.eventCount - b.eventCount; return sortDirection === "asc" ? comparison : -comparison; } }); - }, [users, data, sortField, sortDirection]); + }, [users, data, sortField, sortDirection, locale]); // 切换排序 const toggleSort = (field: SortField) => { diff --git a/src/app/[locale]/dashboard/_components/statistics/chart.tsx b/src/app/[locale]/dashboard/_components/statistics/chart.tsx index 001be1832..9e510e5ec 100644 --- a/src/app/[locale]/dashboard/_components/statistics/chart.tsx +++ b/src/app/[locale]/dashboard/_components/statistics/chart.tsx @@ -1,6 +1,6 @@ "use client"; -import { useTranslations } from "next-intl"; +import { useLocale, useTranslations } from "next-intl"; import * as React from "react"; import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -57,6 +57,7 @@ export function UserStatisticsChart({ currencyCode = "USD", }: UserStatisticsChartProps) { const t = useTranslations("dashboard.statistics"); + const locale = useLocale(); const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost"); const [chartMode, setChartMode] = React.useState<"stacked" | "overlay">("overlay"); @@ -229,12 +230,12 @@ export function UserStatisticsChart({ const formatDate = (dateStr: string) => { const date = new Date(dateStr); if (data.resolution === "hour") { - return date.toLocaleTimeString("zh-CN", { + return date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit", }); } else { - return date.toLocaleDateString("zh-CN", { + return date.toLocaleDateString(locale, { month: "numeric", day: "numeric", }); @@ -245,14 +246,14 @@ export function UserStatisticsChart({ const formatTooltipDate = (dateStr: string) => { const date = new Date(dateStr); if (data.resolution === "hour") { - return date.toLocaleString("zh-CN", { + return date.toLocaleString(locale, { month: "long", day: "numeric", hour: "2-digit", minute: "2-digit", }); } else { - return date.toLocaleDateString("zh-CN", { + return date.toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", diff --git a/src/app/[locale]/dashboard/_components/user/key-row-item.tsx b/src/app/[locale]/dashboard/_components/user/key-row-item.tsx index 24f752bc8..2c7a7fd97 100644 --- a/src/app/[locale]/dashboard/_components/user/key-row-item.tsx +++ b/src/app/[locale]/dashboard/_components/user/key-row-item.tsx @@ -59,6 +59,10 @@ export interface KeyRowItemProps { model: string; callCount: number; totalCost: number; + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; }>; }; /** User-level provider groups (used when key inherits providerGroup). */ @@ -429,11 +433,14 @@ export function KeyRowItem({ -
    - {effectiveGroups.map((group) => ( -
  • {group}
  • - ))} -
+
+

{translations.fields.group}:

+
    + {effectiveGroups.map((group) => ( +
  • {group}
  • + ))} +
+
diff --git a/src/app/[locale]/dashboard/_components/user/key-stats-dialog.tsx b/src/app/[locale]/dashboard/_components/user/key-stats-dialog.tsx index 18b760d18..60de0bb27 100644 --- a/src/app/[locale]/dashboard/_components/user/key-stats-dialog.tsx +++ b/src/app/[locale]/dashboard/_components/user/key-stats-dialog.tsx @@ -1,7 +1,15 @@ "use client"; +import { + Activity, + ArrowDownRight, + ArrowUpRight, + Coins, + Database, + Hash, + Target, +} from "lucide-react"; import { useTranslations } from "next-intl"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -11,20 +19,30 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { Separator } from "@/components/ui/separator"; import { CURRENCY_CONFIG, type CurrencyCode, formatCurrency } from "@/lib/utils/currency"; export interface ModelStat { model: string; callCount: number; totalCost: number; + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; +} + +function formatTokenAmount(tokens: number): string { + if (tokens >= 1_000_000_000) { + return `${(tokens / 1_000_000_000).toFixed(1)}B`; + } + if (tokens >= 1_000_000) { + return `${(tokens / 1_000_000).toFixed(1)}M`; + } + if (tokens >= 1_000) { + return `${(tokens / 1_000).toFixed(1)}K`; + } + return tokens.toLocaleString(); } export interface KeyStatsDialogProps { @@ -50,6 +68,11 @@ export function KeyStatsDialog({ const totalCalls = modelStats.reduce((sum, stat) => sum + stat.callCount, 0); const totalCost = modelStats.reduce((sum, stat) => sum + stat.totalCost, 0); + const totalInput = modelStats.reduce((sum, stat) => sum + stat.inputTokens, 0); + const totalOutput = modelStats.reduce((sum, stat) => sum + stat.outputTokens, 0); + const totalCacheCreation = modelStats.reduce((sum, stat) => sum + stat.cacheCreationTokens, 0); + const totalCacheRead = modelStats.reduce((sum, stat) => sum + stat.cacheReadTokens, 0); + const totalTokens = totalInput + totalOutput + totalCacheCreation + totalCacheRead; const handleClose = () => { onOpenChange(false); @@ -66,45 +89,148 @@ export function KeyStatsDialog({
{modelStats.length > 0 ? ( <> -
- - - - {t("columns.model")} - {t("columns.calls")} - {t("columns.cost")} - - - - {modelStats.map((stat) => ( - - {stat.model} - - {stat.callCount.toLocaleString()} - - - {formatCurrency(stat.totalCost, resolvedCurrencyCode)} - - - ))} - -
-
- -
-
- {t("totalCalls")}: - +
+
+
+ + {t("modal.requests")} +
+
{totalCalls.toLocaleString()} - +
-
- {t("totalCost")}: - + +
+
+ + {t("modal.totalTokens")} +
+
+ {formatTokenAmount(totalTokens)} +
+
+ +
+
+ + {t("modal.cost")} +
+
{formatCurrency(totalCost, resolvedCurrencyCode)} - +
+
+
+ + + +
+
+
+ + {t("modal.inputTokens")} +
+
+ {formatTokenAmount(totalInput)} +
+
+ +
+
+ + {t("modal.outputTokens")} +
+
+ {formatTokenAmount(totalOutput)} +
+
+
+ + + +
+

+ + {t("modal.cacheTokens")} +

+
+
+
+ + {t("modal.cacheWrite")} +
+
+ {formatTokenAmount(totalCacheCreation)} +
+
+ +
+
+ + {t("modal.cacheRead")} +
+
+ {formatTokenAmount(totalCacheRead)} +
+
+ + + +
+ {modelStats.map((stat) => { + const statTotalTokens = + stat.inputTokens + + stat.outputTokens + + stat.cacheCreationTokens + + stat.cacheReadTokens; + const statTotalInput = + stat.inputTokens + stat.cacheCreationTokens + stat.cacheReadTokens; + const statCacheHitRate = + statTotalInput > 0 ? (stat.cacheReadTokens / statTotalInput) * 100 : 0; + const statCacheHitColor = + statCacheHitRate >= 85 + ? "text-green-600 dark:text-green-400" + : statCacheHitRate >= 60 + ? "text-yellow-600 dark:text-yellow-400" + : "text-orange-600 dark:text-orange-400"; + const costPercentage = + totalCost > 0 ? ((stat.totalCost / totalCost) * 100).toFixed(1) : "0.0"; + + return ( +
+
+ + {stat.model} + +
+ + + {stat.callCount.toLocaleString()} + + + + {formatTokenAmount(statTotalTokens)} + + + + {statCacheHitRate.toFixed(1)}% + +
+
+
+
{formatCurrency(stat.totalCost, resolvedCurrencyCode)}
+
+ ({costPercentage}%) +
+
+
+ ); + })} +
) : (
{t("noData")}
diff --git a/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx b/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx index 34330864b..5146ff28b 100644 --- a/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx +++ b/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx @@ -12,7 +12,7 @@ import { XCircle, } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; -import { useEffect, useState, useTransition } from "react"; +import { useEffect, useMemo, useState, useTransition } from "react"; import { toast } from "sonner"; import { removeKey } from "@/actions/keys"; import { toggleUserEnabled } from "@/actions/users"; @@ -106,6 +106,16 @@ function getExpiryStatus( return { label: "active", variant: "default" }; } +// Calculate days left until expiry (for user mode badge) +function getDaysLeft(expiresAt: Date | null | undefined): number | null { + if (!expiresAt) return null; + const now = Date.now(); + const expTs = expiresAt.getTime(); + if (!Number.isFinite(expTs) || expTs <= now) return null; + const msLeft = expTs - now; + return Math.ceil(msLeft / (1000 * 60 * 60 * 24)); +} + function normalizeLimitValue(value: unknown): number | null { const raw = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; if (!Number.isFinite(raw) || raw <= 0) return null; @@ -175,6 +185,10 @@ export function UserKeyTableRow({ // 计算用户过期状态 const expiryStatus = getExpiryStatus(localIsEnabled, localExpiresAt ?? null); + // 计算剩余天数(仅用于 user mode 显示) + const daysLeft = useMemo(() => getDaysLeft(localExpiresAt ?? null), [localExpiresAt]); + const showExpiryBadge = !isAdmin && daysLeft !== null && daysLeft <= 7; + // 处理 Provider Group:拆分成数组 const userGroups = splitGroups(user.providerGroup); const visibleGroups = userGroups.slice(0, MAX_VISIBLE_GROUPS); @@ -332,11 +346,14 @@ export function UserKeyTableRow({
-
    - {userGroups.map((group) => ( -
  • {group}
  • - ))} -
+
+

{translations.keyRow?.fields?.group}:

+
    + {userGroups.map((group) => ( +
  • {group}
  • + ))} +
+
) : null} @@ -482,7 +499,27 @@ export function UserKeyTableRow({ - ) : null} + ) : ( + showExpiryBadge && ( + + + 0 && + daysLeft <= 7 && + "border-amber-500 text-amber-600 dark:text-amber-400" + )} + > + + {daysLeft} + + + {tUserStatus("daysLeft", { days: daysLeft })} + + ) + )}
diff --git a/src/app/[locale]/dashboard/_components/user/user-limit-badge.tsx b/src/app/[locale]/dashboard/_components/user/user-limit-badge.tsx index 3063ac32d..81d3e475b 100644 --- a/src/app/[locale]/dashboard/_components/user/user-limit-badge.tsx +++ b/src/app/[locale]/dashboard/_components/user/user-limit-badge.tsx @@ -28,6 +28,14 @@ interface LimitUsageData { const usageCache = new Map(); const CACHE_TTL = 60 * 1000; // 1 minute +export function clearUsageCache(userId?: number): void { + if (userId !== undefined) { + usageCache.delete(userId); + } else { + usageCache.clear(); + } +} + function formatPercentage(usage: number, limit: number): string { const percentage = Math.min(Math.round((usage / limit) * 100), 999); return `${percentage}%`; diff --git a/src/app/[locale]/dashboard/_components/user/user-management-table.tsx b/src/app/[locale]/dashboard/_components/user/user-management-table.tsx index 0c6601941..31df9dc7a 100644 --- a/src/app/[locale]/dashboard/_components/user/user-management-table.tsx +++ b/src/app/[locale]/dashboard/_components/user/user-management-table.tsx @@ -62,6 +62,7 @@ export interface UserManagementTableProps { editDialog: any; actions: { edit: string; + status: string; details: string; logs: string; delete: string; @@ -501,8 +502,11 @@ export function UserManagementTable({
- - {translations.actions.edit} + + {isAdmin ? translations.actions.edit : translations.actions.status}
diff --git a/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx b/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx index 6fd4121b6..5b4c0463e 100644 --- a/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx +++ b/src/app/[locale]/dashboard/leaderboard/_components/leaderboard-view.tsx @@ -284,8 +284,16 @@ export function LeaderboardView({ isAdmin }: LeaderboardViewProps) { { header: t("columns.cacheHitRate"), className: "text-right", - cell: (row) => - `${(Number((row as ProviderCacheHitRateEntry).cacheHitRate || 0) * 100).toFixed(1)}%`, + cell: (row) => { + const rate = Number((row as ProviderCacheHitRateEntry).cacheHitRate || 0) * 100; + const colorClass = + rate >= 85 + ? "text-green-600 dark:text-green-400" + : rate >= 60 + ? "text-yellow-600 dark:text-yellow-400" + : "text-orange-600 dark:text-orange-400"; + return {rate.toFixed(1)}%; + }, sortKey: "cacheHitRate", getValue: (row) => (row as ProviderCacheHitRateEntry).cacheHitRate, }, diff --git a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx index b1ab7813c..ce24a7303 100644 --- a/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx +++ b/src/app/[locale]/dashboard/logs/_components/virtualized-logs-table.tsx @@ -199,10 +199,10 @@ export function VirtualizedLogsTable({ {/* Fixed header */}
-
+
{t("logs.columns.time")}
-
+
{t("logs.columns.user")}
@@ -310,12 +310,12 @@ export function VirtualizedLogsTable({ )} > {/* Time */} -
+
{/* User */} -
+
{log.userName}
diff --git a/src/app/[locale]/dashboard/users/users-page-client.tsx b/src/app/[locale]/dashboard/users/users-page-client.tsx index faa78d1f9..cd7141754 100644 --- a/src/app/[locale]/dashboard/users/users-page-client.tsx +++ b/src/app/[locale]/dashboard/users/users-page-client.tsx @@ -27,6 +27,7 @@ import type { User, UserDisplay } from "@/types/user"; import { AddKeyDialog } from "../_components/user/add-key-dialog"; import { BatchEditDialog } from "../_components/user/batch-edit/batch-edit-dialog"; import { CreateUserDialog } from "../_components/user/create-user-dialog"; +import { clearUsageCache } from "../_components/user/user-limit-badge"; import { UserManagementTable } from "../_components/user/user-management-table"; const queryClient = new QueryClient({ @@ -469,6 +470,7 @@ function UsersPageContent({ currentUser }: UsersPageClientProps) { editDialog: {}, actions: { edit: tCommon("edit"), + status: tCommon("status"), details: tKeyList("detailsButton"), logs: tKeyList("logsButton"), delete: tCommon("delete"), @@ -704,7 +706,10 @@ function UsersPageContent({ currentUser }: UsersPageClientProps) { onSelectKey={handleSelectKey} onOpenBatchEdit={handleOpenBatchEdit} translations={tableTranslations} - onRefresh={() => refetch()} + onRefresh={() => { + clearUsageCache(); + refetch(); + }} isRefreshing={isRefreshing} />
diff --git a/src/app/[locale]/my-usage/_components/usage-logs-table.tsx b/src/app/[locale]/my-usage/_components/usage-logs-table.tsx index e9a7434b7..8464e08f9 100644 --- a/src/app/[locale]/my-usage/_components/usage-logs-table.tsx +++ b/src/app/[locale]/my-usage/_components/usage-logs-table.tsx @@ -83,7 +83,7 @@ export function UsageLogsTable({ {log.createdAt ? new Date(log.createdAt).toLocaleString() : "-"} -
{log.model ?? t("unknownModel")}
+
{log.model ?? t("unknownModel")}
{log.modelRedirect ? (
{log.modelRedirect}
) : null} diff --git a/src/app/[locale]/settings/data/_components/database-status.tsx b/src/app/[locale]/settings/data/_components/database-status.tsx index 9942a5618..843fbc844 100644 --- a/src/app/[locale]/settings/data/_components/database-status.tsx +++ b/src/app/[locale]/settings/data/_components/database-status.tsx @@ -23,6 +23,10 @@ export function DatabaseStatusDisplay() { }); if (!response.ok) { + // Check 503 before parsing JSON (response may not have JSON body) + if (response.status === 503) { + throw new Error(t("connectionUnavailable")); + } const errorData = await response.json(); throw new Error(errorData.error || t("error")); } @@ -110,7 +114,7 @@ export function DatabaseStatusDisplay() { {/* Error message */} {status.error && (
- {status.error} + {status.isAvailable === false ? t("connectionUnavailable") : status.error}
)}
diff --git a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/testing-section.tsx b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/testing-section.tsx index 0185e47c6..914d79adb 100644 --- a/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/testing-section.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/provider-form/sections/testing-section.tsx @@ -91,7 +91,9 @@ export function TestingSection() { disabled={state.ui.isPending} > - + + {t(`sections.mcpPassthrough.select.${state.mcp.mcpPassthroughType}.label`)} + diff --git a/src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx b/src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx index 44ee77b1b..b871654c3 100644 --- a/src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx +++ b/src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx @@ -418,8 +418,8 @@ function TestResultDetails({ {(result.rawResponse || result.content) && (

{t("resultCard.rawResponse.title")}

-
-
+          
+
               {result.rawResponse || result.content}
             
diff --git a/src/lib/provider-testing/test-service.ts b/src/lib/provider-testing/test-service.ts index f8dc3a263..e2a07c1be 100644 --- a/src/lib/provider-testing/test-service.ts +++ b/src/lib/provider-testing/test-service.ts @@ -8,6 +8,7 @@ * 3. Content validation (success_contains) */ +import { createProxyAgentForProvider, type ProviderProxyConfig } from "@/lib/proxy-agent"; import { getPreset, getPresetPayload } from "./presets"; import type { ProviderTestConfig, @@ -77,19 +78,42 @@ export async function executeProviderTest(config: ProviderTestConfig): Promise

controller.abort(), timeoutMs); try { // Execute request - const response = await fetch(url, { + const fetchOptions: RequestInit & { dispatcher?: unknown } = { method: "POST", headers, body: JSON.stringify(body), signal: controller.signal, - }); + }; + if (dispatcher) { + fetchOptions.dispatcher = dispatcher; + } + const response = await fetch(url, fetchOptions); firstByteMs = Date.now() - startTime; @@ -145,6 +169,7 @@ export async function executeProviderTest(config: ProviderTestConfig): Promise

; } @@ -606,6 +610,10 @@ export async function findKeysWithStatistics(userId: number): Promise`count(*)::int`, totalCost: sum(messageRequest.costUsd), + inputTokens: sql`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`, + outputTokens: sql`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`, + cacheCreationTokens: sql`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::int`, + cacheReadTokens: sql`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::int`, }) .from(messageRequest) .where( @@ -628,6 +636,10 @@ export async function findKeysWithStatistics(userId: number): Promise`count(*)::int`, totalCost: sum(messageRequest.costUsd), + inputTokens: sql`COALESCE(sum(${messageRequest.inputTokens}), 0)::int`, + outputTokens: sql`COALESCE(sum(${messageRequest.outputTokens}), 0)::int`, + cacheCreationTokens: sql`COALESCE(sum(${messageRequest.cacheCreationInputTokens}), 0)::int`, + cacheReadTokens: sql`COALESCE(sum(${messageRequest.cacheReadInputTokens}), 0)::int`, }) .from(messageRequest) .where( @@ -765,7 +781,15 @@ export async function findKeysWithStatisticsBatch( // Group model stats by key const modelStatsMap = new Map< string, - Array<{ model: string; callCount: number; totalCost: number }> + Array<{ + model: string; + callCount: number; + totalCost: number; + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; + }> >(); for (const row of modelStatsRows) { if (row.key) { @@ -779,6 +803,10 @@ export async function findKeysWithStatisticsBatch( const costDecimal = toCostDecimal(row.totalCost) ?? new Decimal(0); return costDecimal.toDecimalPlaces(6).toNumber(); })(), + inputTokens: row.inputTokens, + outputTokens: row.outputTokens, + cacheCreationTokens: row.cacheCreationTokens, + cacheReadTokens: row.cacheReadTokens, }); } } diff --git a/src/types/user.ts b/src/types/user.ts index 8c95d885c..1efcad8e3 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -107,6 +107,10 @@ export interface UserKeyDisplay { model: string; callCount: number; totalCost: number; + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; }>; // 各模型统计(当天) createdAt: Date; // 创建时间 createdAtFormatted: string; // 格式化后的具体时间