From 99382aa013e00a0423eb3a3c4511349993f9f482 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 22 Jan 2026 16:48:22 +0300 Subject: [PATCH 01/13] fix(my-usage): UI improvements for My Usage page - Remove bold font from model name in usage logs table - Rename "Quota Usage" to "Quota User Usage" - Rename "Statistics Summary" to "Key Statistics" Co-Authored-By: Claude Opus 4.5 --- messages/en/myUsage.json | 4 ++-- messages/ja/myUsage.json | 4 ++-- messages/ru/myUsage.json | 4 ++-- messages/zh-CN/myUsage.json | 4 ++-- messages/zh-TW/myUsage.json | 4 ++-- .../my-usage/_components/statistics-summary-card.tsx | 7 ++++--- src/app/[locale]/my-usage/_components/usage-logs-table.tsx | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) diff --git a/messages/en/myUsage.json b/messages/en/myUsage.json index 2d6aaab68..5cdf39672 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": "Quota User Usage", "daily": "Daily", "monthly": "Monthly", "total": "Total" 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/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/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-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/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx index a17af0415..38becb346 100644 --- a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx +++ b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx @@ -119,9 +119,10 @@ export function StatisticsSummaryCard({ {t("title")} -

- {t("autoRefresh", { seconds: autoRefreshSeconds })} -

+
+

{t("autoRefresh", { seconds: autoRefreshSeconds })}

+

{t("serverTime")}

+
-
{log.model ?? t("unknownModel")}
+
{log.model ?? t("unknownModel")}
{log.modelRedirect ? (
{log.modelRedirect}
) : null} From ff336f6c499ad2e4d9f14334283f68ec7a172451 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 22 Jan 2026 17:22:29 +0300 Subject: [PATCH 02/13] fix(i18n): replace hardcoded zh-CN locale with dynamic locale in dashboard charts - Use useLocale() from next-intl to get current locale - Replace hardcoded "zh-CN" in toLocaleTimeString/toLocaleDateString/toLocaleString - Move chartConfig inside component to use translated label - Add locale to useMemo dependencies where needed Co-Authored-By: Claude Opus 4.5 --- .../bento/statistics-chart-card.tsx | 11 +++++----- .../_components/rate-limit-events-chart.tsx | 21 ++++++++++--------- .../_components/rate-limit-top-users.tsx | 7 ++++--- .../_components/statistics/chart.tsx | 11 +++++----- .../_components/statistics-summary-card.tsx | 7 +++---- 5 files changed, 30 insertions(+), 27 deletions(-) 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..240573748 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,25 @@ 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 = { + count: { + label: t("events"), + color: "hsl(var(--chart-1))", + }, + } satisfies ChartConfig; // 格式化小时显示 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 +40,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]/my-usage/_components/statistics-summary-card.tsx b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx index 38becb346..a17af0415 100644 --- a/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx +++ b/src/app/[locale]/my-usage/_components/statistics-summary-card.tsx @@ -119,10 +119,9 @@ export function StatisticsSummaryCard({ {t("title")} -
-

{t("autoRefresh", { seconds: autoRefreshSeconds })}

-

{t("serverTime")}

-
+

+ {t("autoRefresh", { seconds: autoRefreshSeconds })} +

Date: Thu, 22 Jan 2026 17:31:11 +0300 Subject: [PATCH 03/13] fix(i18n): translate database connection unavailable error message - Add connectionUnavailable key to all 5 language files - Use translated message in database-status.tsx when isAvailable is false - Handle both HTTP 503 errors and status.error from API response Co-Authored-By: Claude Opus 4.5 --- messages/en/settings/data.json | 1 + messages/ja/settings/data.json | 1 + messages/ru/settings/data.json | 1 + messages/zh-CN/settings/data.json | 1 + messages/zh-TW/settings/data.json | 1 + .../[locale]/settings/data/_components/database-status.tsx | 6 +++++- 6 files changed, 10 insertions(+), 1 deletion(-) 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/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/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/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-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/src/app/[locale]/settings/data/_components/database-status.tsx b/src/app/[locale]/settings/data/_components/database-status.tsx index 9942a5618..5dcab93e9 100644 --- a/src/app/[locale]/settings/data/_components/database-status.tsx +++ b/src/app/[locale]/settings/data/_components/database-status.tsx @@ -24,6 +24,10 @@ export function DatabaseStatusDisplay() { if (!response.ok) { const errorData = await response.json(); + // Use translated message for connection unavailable error + if (response.status === 503) { + throw new Error(t("connectionUnavailable")); + } 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}
)}
From 09e9ac1a78e612a354e52d09189f878036462b29 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 22 Jan 2026 23:37:00 +0300 Subject: [PATCH 04/13] fix(provider-test): use configured proxy for model testing Previously, the provider model test ignored the configured proxy URL and always made direct connections. Now the test respects proxyUrl setting by creating a dispatcher via createProxyAgentForProvider. Co-Authored-By: Claude Opus 4.5 --- src/lib/provider-testing/test-service.ts | 28 ++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/lib/provider-testing/test-service.ts b/src/lib/provider-testing/test-service.ts index f8dc3a263..f6d961015 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,6 +78,23 @@ export async function executeProviderTest(config: ProviderTestConfig): Promise

Date: Thu, 22 Jan 2026 23:38:59 +0300 Subject: [PATCH 05/13] feat(i18n): add batchEdit translations and update group field description - Add providersBatchEdit import to all 5 locale index.ts files - Update provider group field description to mention select/create behavior Co-Authored-By: Claude Opus 4.5 --- messages/en/settings/index.ts | 2 ++ messages/en/settings/providers/form/sections.json | 2 +- messages/ja/settings/index.ts | 2 ++ messages/ja/settings/providers/form/sections.json | 2 +- messages/ru/settings/index.ts | 2 ++ messages/ru/settings/providers/form/sections.json | 4 ++-- messages/zh-CN/settings/index.ts | 2 ++ messages/zh-CN/settings/providers/form/sections.json | 4 ++-- messages/zh-TW/settings/index.ts | 2 ++ messages/zh-TW/settings/providers/form/sections.json | 4 ++-- 10 files changed, 18 insertions(+), 8 deletions(-) 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/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/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/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/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", From d7da89ae03e5f97414eb0113e8e4ed166a0a3189 Mon Sep 17 00:00:00 2001 From: John Doe Date: Thu, 22 Jan 2026 23:39:01 +0300 Subject: [PATCH 06/13] fix(ui): improve MCP passthrough select and raw response display - Fix MCP passthrough select to show translated label instead of value - Fix raw response overflow with break-all and overflow-x-hidden Co-Authored-By: Claude Opus 4.5 --- .../forms/provider-form/sections/testing-section.tsx | 4 +++- .../settings/providers/_components/forms/test-result-card.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) 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}
             
From befa1e4d981a12f92a921ea00cc835044eccec00 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 23 Jan 2026 00:33:40 +0300 Subject: [PATCH 07/13] fix(ui): adjust logs table column widths for better user visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shrink Time column (flex 0.8→0.6, min-width 80→56px) and expand User column (flex 0.6→0.8) to show more of the username. Co-Authored-By: Claude Opus 4.5 --- .../dashboard/logs/_components/virtualized-logs-table.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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}
From cc8ff0cf066d68c7b302ae6da8965578bdaea472 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 23 Jan 2026 01:03:30 +0300 Subject: [PATCH 08/13] feat(ui): add color indicators for cache hit rate in leaderboard Apply color coding to cache hit rate column: green (>=85%), yellow (60-84%), orange (<60%) Co-Authored-By: Claude Opus 4.5 --- .../leaderboard/_components/leaderboard-view.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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, }, From ee13c76e07718ebd53e817f9a8736d46ae65247d Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 23 Jan 2026 02:05:06 +0300 Subject: [PATCH 09/13] feat(ui): enhance user key statistics modal with token details - Add token statistics (input, output, cache creation, cache read) to SQL queries - Redesign key stats dialog with summary cards and compact model rows - Add cache hit rate indicator per model with color coding - Fix user limit refresh by clearing usage cache on refresh button click - Add i18n translations for new modal fields (5 languages) Co-Authored-By: Claude Opus 4.5 --- messages/en/dashboard.json | 15 ++ messages/ja/dashboard.json | 15 ++ messages/ru/dashboard.json | 15 ++ messages/zh-CN/dashboard.json | 15 ++ messages/zh-TW/dashboard.json | 15 ++ .../_components/user/key-row-item.tsx | 4 + .../_components/user/key-stats-dialog.tsx | 212 ++++++++++++++---- .../_components/user/user-limit-badge.tsx | 8 + .../dashboard/users/users-page-client.tsx | 6 +- src/repository/key.ts | 30 ++- src/types/user.ts | 4 + 11 files changed, 294 insertions(+), 45 deletions(-) diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json index 2cf6b7930..0a5ee8a68 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" diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json index 52c6e2a09..0c105b6ba 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": "総消費" diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json index 9a93d4299..87dcb8e5f 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": "Общий расход" diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json index 9b04c2c95..e9f5e089c 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": "总消费" diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json index 10ae12463..8a321a559 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": "今日總消費" 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..6887dd6d2 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). */ 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-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/users/users-page-client.tsx b/src/app/[locale]/dashboard/users/users-page-client.tsx index faa78d1f9..545524651 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({ @@ -704,7 +705,10 @@ function UsersPageContent({ currentUser }: UsersPageClientProps) { onSelectKey={handleSelectKey} onOpenBatchEdit={handleOpenBatchEdit} translations={tableTranslations} - onRefresh={() => refetch()} + onRefresh={() => { + clearUsageCache(); + refetch(); + }} isRefreshing={isRefreshing} />
diff --git a/src/repository/key.ts b/src/repository/key.ts index ed7d78666..4b3f1031d 100644 --- a/src/repository/key.ts +++ b/src/repository/key.ts @@ -554,6 +554,10 @@ export interface KeyStatistics { model: string; callCount: number; totalCost: number; + inputTokens: number; + outputTokens: number; + cacheCreationTokens: number; + cacheReadTokens: number; }>; } @@ -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; // 格式化后的具体时间 From 0adf7fb3d3a756be4874ed446d2cd4f980f122bf Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 23 Jan 2026 16:55:48 +0300 Subject: [PATCH 10/13] fix(ui): improve group tooltip formatting in dashboard users Add header and bullet list styling to group tooltips for consistency with request-filters tooltip design. Co-Authored-By: Claude Opus 4.5 --- .../dashboard/_components/user/key-row-item.tsx | 13 ++++++++----- .../_components/user/user-key-table-row.tsx | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) 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 6887dd6d2..2c7a7fd97 100644 --- a/src/app/[locale]/dashboard/_components/user/key-row-item.tsx +++ b/src/app/[locale]/dashboard/_components/user/key-row-item.tsx @@ -433,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/user-key-table-row.tsx b/src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx index 34330864b..bc901a0fa 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 @@ -332,11 +332,14 @@ export function UserKeyTableRow({
-
    - {userGroups.map((group) => ( -
  • {group}
  • - ))} -
+
+

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

+
    + {userGroups.map((group) => ( +
  • {group}
  • + ))} +
+
) : null} From a64c98538ac4a93ba12b7a9245cfe49ee254fbbe Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 23 Jan 2026 20:06:18 +0300 Subject: [PATCH 11/13] feat(ui): add expiry countdown badge for user mode in dashboard - Show compact badge with clock icon and days remaining when <= 7 days - Display tooltip with full localized text on hover - Rename column header from "Edit" to "Status" in user mode - Add i18n support for all 5 languages (en, zh-CN, zh-TW, ja, ru) Co-Authored-By: Claude Opus 4.5 --- messages/en/common.json | 1 + messages/en/dashboard.json | 3 +- messages/ja/common.json | 1 + messages/ja/dashboard.json | 3 +- messages/ru/common.json | 1 + messages/ru/dashboard.json | 3 +- messages/zh-CN/common.json | 1 + messages/zh-CN/dashboard.json | 3 +- messages/zh-TW/common.json | 1 + messages/zh-TW/dashboard.json | 3 +- .../_components/user/user-key-table-row.tsx | 36 ++++++++++++++++++- .../user/user-management-table.tsx | 8 +++-- .../dashboard/users/users-page-client.tsx | 1 + 13 files changed, 57 insertions(+), 8 deletions(-) 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 0a5ee8a68..1fff423b0 100644 --- a/messages/en/dashboard.json +++ b/messages/en/dashboard.json @@ -1592,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/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 0c105b6ba..f3e9654e0 100644 --- a/messages/ja/dashboard.json +++ b/messages/ja/dashboard.json @@ -1527,7 +1527,8 @@ "clickToEnableUser": "クリックしてユーザーを有効化", "operationFailed": "操作に失敗しました", "deleteFailed": "削除に失敗しました", - "deleteSuccess": "削除しました" + "deleteSuccess": "削除しました", + "daysLeft": "{days, plural, =0 {本日期限} =1 {残り1日} other {残り#日}}" }, "userEditSection": { "sections": { 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 87dcb8e5f..3625e6466 100644 --- a/messages/ru/dashboard.json +++ b/messages/ru/dashboard.json @@ -1580,7 +1580,8 @@ "clickToEnableUser": "Нажмите, чтобы включить пользователя", "operationFailed": "Операция не удалась", "deleteFailed": "Не удалось удалить", - "deleteSuccess": "Удаление успешно" + "deleteSuccess": "Удаление успешно", + "daysLeft": "{days, plural, =0 {Истекает сегодня} =1 {Остался 1 день} few {Осталось # дня} many {Осталось # дней} other {Осталось # дней}}" }, "userEditSection": { "sections": { 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 e9f5e089c..fc0ae7829 100644 --- a/messages/zh-CN/dashboard.json +++ b/messages/zh-CN/dashboard.json @@ -1551,7 +1551,8 @@ "clickToEnableUser": "点击启用用户", "operationFailed": "操作失败", "deleteFailed": "删除失败", - "deleteSuccess": "删除成功" + "deleteSuccess": "删除成功", + "daysLeft": "{days, plural, =0 {今天到期} =1 {剩余1天} other {剩余#天}}" }, "userEditSection": { "sections": { 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 8a321a559..8d5ef0137 100644 --- a/messages/zh-TW/dashboard.json +++ b/messages/zh-TW/dashboard.json @@ -1536,7 +1536,8 @@ "clickToEnableUser": "點擊啟用使用者", "operationFailed": "操作失敗", "deleteFailed": "刪除失敗", - "deleteSuccess": "刪除成功" + "deleteSuccess": "刪除成功", + "daysLeft": "{days, plural, =0 {今天到期} =1 {剩餘1天} other {剩餘#天}}" }, "userEditSection": { "sections": { 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 bc901a0fa..b4ce59049 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 @@ -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)) return null; + const msLeft = expTs - now; + return Math.max(0, 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 = getDaysLeft(localExpiresAt ?? null); + const showExpiryBadge = !isAdmin && daysLeft !== null && daysLeft <= 7; + // 处理 Provider Group:拆分成数组 const userGroups = splitGroups(user.providerGroup); const visibleGroups = userGroups.slice(0, MAX_VISIBLE_GROUPS); @@ -485,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-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/users/users-page-client.tsx b/src/app/[locale]/dashboard/users/users-page-client.tsx index 545524651..cd7141754 100644 --- a/src/app/[locale]/dashboard/users/users-page-client.tsx +++ b/src/app/[locale]/dashboard/users/users-page-client.tsx @@ -470,6 +470,7 @@ function UsersPageContent({ currentUser }: UsersPageClientProps) { editDialog: {}, actions: { edit: tCommon("edit"), + status: tCommon("status"), details: tKeyList("detailsButton"), logs: tKeyList("logsButton"), delete: tCommon("delete"), From 469276cffd9fba1e56d2466a6f6011caaddc04fb Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 23 Jan 2026 21:46:58 +0300 Subject: [PATCH 12/13] perf(ui): memoize chartConfig and daysLeft calculations - Wrap chartConfig in useMemo to prevent recreation on every render - Wrap daysLeft calculation in useMemo with localExpiresAt dependency Co-Authored-By: Claude Opus 4.5 --- .../_components/rate-limit-events-chart.tsx | 16 ++++++++++------ .../_components/user/user-key-table-row.tsx | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) 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 240573748..485458fd8 100644 --- a/src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx +++ b/src/app/[locale]/dashboard/_components/rate-limit-events-chart.tsx @@ -19,12 +19,16 @@ export function RateLimitEventsChart({ data }: RateLimitEventsChartProps) { const t = useTranslations("dashboard.rateLimits.chart"); const locale = useLocale(); - const chartConfig = { - count: { - label: t("events"), - color: "hsl(var(--chart-1))", - }, - } satisfies ChartConfig; + const chartConfig = React.useMemo( + () => + ({ + count: { + label: t("events"), + color: "hsl(var(--chart-1))", + }, + }) satisfies ChartConfig, + [t] + ); // 格式化小时显示 const formatHour = (hourStr: string) => { 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 b4ce59049..162cbb9d7 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"; @@ -186,7 +186,7 @@ export function UserKeyTableRow({ const expiryStatus = getExpiryStatus(localIsEnabled, localExpiresAt ?? null); // 计算剩余天数(仅用于 user mode 显示) - const daysLeft = getDaysLeft(localExpiresAt ?? null); + const daysLeft = useMemo(() => getDaysLeft(localExpiresAt ?? null), [localExpiresAt]); const showExpiryBadge = !isAdmin && daysLeft !== null && daysLeft <= 7; // 处理 Provider Group:拆分成数组 From adb1d1753b7bbe915c7690341a1decb8a1734b46 Mon Sep 17 00:00:00 2001 From: John Doe Date: Fri, 23 Jan 2026 22:23:01 +0300 Subject: [PATCH 13/13] fix: address PR review feedback - Move proxy creation into try/catch block (test-service.ts) - Check 503 status before parsing JSON (database-status.tsx) - Fix English grammar "User Quota Usage" (myUsage.json) - Return null for expired keys in getDaysLeft (user-key-table-row.tsx) Co-Authored-By: Claude Opus 4.5 --- messages/en/myUsage.json | 2 +- .../_components/user/user-key-table-row.tsx | 4 +-- .../data/_components/database-status.tsx | 4 +-- src/lib/provider-testing/test-service.ts | 32 ++++++++++--------- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/messages/en/myUsage.json b/messages/en/myUsage.json index 5cdf39672..0ebe076a2 100644 --- a/messages/en/myUsage.json +++ b/messages/en/myUsage.json @@ -116,7 +116,7 @@ "noRestrictions": "No restrictions" }, "quotaCollapsible": { - "title": "Quota User Usage", + "title": "User Quota Usage", "daily": "Daily", "monthly": "Monthly", "total": "Total" 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 162cbb9d7..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 @@ -111,9 +111,9 @@ function getDaysLeft(expiresAt: Date | null | undefined): number | null { if (!expiresAt) return null; const now = Date.now(); const expTs = expiresAt.getTime(); - if (!Number.isFinite(expTs)) return null; + if (!Number.isFinite(expTs) || expTs <= now) return null; const msLeft = expTs - now; - return Math.max(0, Math.ceil(msLeft / (1000 * 60 * 60 * 24))); + return Math.ceil(msLeft / (1000 * 60 * 60 * 24)); } function normalizeLimitValue(value: unknown): number | null { diff --git a/src/app/[locale]/settings/data/_components/database-status.tsx b/src/app/[locale]/settings/data/_components/database-status.tsx index 5dcab93e9..843fbc844 100644 --- a/src/app/[locale]/settings/data/_components/database-status.tsx +++ b/src/app/[locale]/settings/data/_components/database-status.tsx @@ -23,11 +23,11 @@ export function DatabaseStatusDisplay() { }); if (!response.ok) { - const errorData = await response.json(); - // Use translated message for connection unavailable error + // 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")); } diff --git a/src/lib/provider-testing/test-service.ts b/src/lib/provider-testing/test-service.ts index f6d961015..e2a07c1be 100644 --- a/src/lib/provider-testing/test-service.ts +++ b/src/lib/provider-testing/test-service.ts @@ -78,24 +78,26 @@ export async function executeProviderTest(config: ProviderTestConfig): Promise

controller.abort(), timeoutMs);