diff --git a/messages/en/dashboard.json b/messages/en/dashboard.json
index 0a5955259..23fbedb1f 100644
--- a/messages/en/dashboard.json
+++ b/messages/en/dashboard.json
@@ -1385,6 +1385,9 @@
"keyStatus": {
"enabled": "Enabled",
"disabled": "Disabled",
+ "active": "Active",
+ "expired": "Expired",
+ "expiringSoon": "Expiring Soon",
"keyEnabled": "Key enabled",
"keyDisabled": "Key disabled",
"toggleKeyStatus": "Toggle key status",
@@ -1396,6 +1399,9 @@
"userStatus": {
"enabled": "Enabled",
"disabled": "Disabled",
+ "active": "Active",
+ "expired": "Expired",
+ "expiringSoon": "Expiring Soon",
"userEnabled": "User enabled",
"userDisabled": "User disabled",
"toggleUserStatus": "Toggle user status",
diff --git a/messages/ja/dashboard.json b/messages/ja/dashboard.json
index 1e5863b68..b7a46d054 100644
--- a/messages/ja/dashboard.json
+++ b/messages/ja/dashboard.json
@@ -1344,11 +1344,24 @@
},
"keyStatus": {
"enabled": "有効",
- "disabled": "無効"
+ "disabled": "無効",
+ "active": "正常",
+ "expired": "期限切れ",
+ "expiringSoon": "まもなく期限切れ",
+ "keyEnabled": "キーが有効になりました",
+ "keyDisabled": "キーが無効になりました",
+ "toggleKeyStatus": "キー状態を切り替える",
+ "clickToDisableKey": "クリックしてキーを無効化",
+ "clickToEnableKey": "クリックしてキーを有効化",
+ "operationFailed": "操作に失敗しました",
+ "clickToQuickRenew": "クリックして更新"
},
"userStatus": {
"enabled": "有効",
"disabled": "無効",
+ "active": "正常",
+ "expired": "期限切れ",
+ "expiringSoon": "まもなく期限切れ",
"userEnabled": "ユーザーが有効になりました",
"userDisabled": "ユーザーが無効になりました",
"toggleUserStatus": "ユーザー状態を切り替える",
diff --git a/messages/ru/dashboard.json b/messages/ru/dashboard.json
index 04f40b578..e66ae1a38 100644
--- a/messages/ru/dashboard.json
+++ b/messages/ru/dashboard.json
@@ -1357,11 +1357,24 @@
},
"keyStatus": {
"enabled": "Включён",
- "disabled": "Отключён"
+ "disabled": "Отключён",
+ "active": "Активен",
+ "expired": "Истёк",
+ "expiringSoon": "Скоро истечёт",
+ "keyEnabled": "Ключ включён",
+ "keyDisabled": "Ключ отключён",
+ "toggleKeyStatus": "Переключить статус ключа",
+ "clickToDisableKey": "Нажмите, чтобы отключить ключ",
+ "clickToEnableKey": "Нажмите, чтобы включить ключ",
+ "operationFailed": "Операция не удалась",
+ "clickToQuickRenew": "Нажмите для быстрого продления"
},
"userStatus": {
"enabled": "Включён",
"disabled": "Отключён",
+ "active": "Активен",
+ "expired": "Истёк",
+ "expiringSoon": "Скоро истечёт",
"userEnabled": "Пользователь включён",
"userDisabled": "Пользователь отключён",
"toggleUserStatus": "Переключить статус пользователя",
diff --git a/messages/zh-CN/dashboard.json b/messages/zh-CN/dashboard.json
index 199d27f6a..dce1512ac 100644
--- a/messages/zh-CN/dashboard.json
+++ b/messages/zh-CN/dashboard.json
@@ -1384,6 +1384,9 @@
"keyStatus": {
"enabled": "启用",
"disabled": "禁用",
+ "active": "正常",
+ "expired": "已过期",
+ "expiringSoon": "即将过期",
"keyEnabled": "密钥已启用",
"keyDisabled": "密钥已禁用",
"toggleKeyStatus": "切换密钥启用状态",
@@ -1395,6 +1398,9 @@
"userStatus": {
"enabled": "启用",
"disabled": "禁用",
+ "active": "正常",
+ "expired": "已过期",
+ "expiringSoon": "即将过期",
"userEnabled": "用户已启用",
"userDisabled": "用户已禁用",
"toggleUserStatus": "切换用户启用状态",
diff --git a/messages/zh-TW/dashboard.json b/messages/zh-TW/dashboard.json
index c0d3de757..8ae5cd69d 100644
--- a/messages/zh-TW/dashboard.json
+++ b/messages/zh-TW/dashboard.json
@@ -1356,11 +1356,24 @@
},
"keyStatus": {
"enabled": "啟用",
- "disabled": "停用"
+ "disabled": "停用",
+ "active": "正常",
+ "expired": "已過期",
+ "expiringSoon": "即將過期",
+ "keyEnabled": "密鑰已啟用",
+ "keyDisabled": "密鑰已停用",
+ "toggleKeyStatus": "切換密鑰狀態",
+ "clickToDisableKey": "點擊停用密鑰",
+ "clickToEnableKey": "點擊啟用密鑰",
+ "operationFailed": "操作失敗",
+ "clickToQuickRenew": "點擊快速續期"
},
"userStatus": {
"enabled": "啟用",
"disabled": "停用",
+ "active": "正常",
+ "expired": "已過期",
+ "expiringSoon": "即將過期",
"userEnabled": "使用者已啟用",
"userDisabled": "使用者已停用",
"toggleUserStatus": "切換使用者狀態",
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 24a438877..e2ed9e18b 100644
--- a/src/app/[locale]/dashboard/_components/user/key-row-item.tsx
+++ b/src/app/[locale]/dashboard/_components/user/key-row-item.tsx
@@ -92,6 +92,8 @@ export interface KeyRowItemProps {
};
}
+const EXPIRING_SOON_MS = 72 * 60 * 60 * 1000; // 72小时
+
function splitGroups(value?: string | null): string[] {
return (value ?? "")
.split(",")
@@ -99,6 +101,32 @@ function splitGroups(value?: string | null): string[] {
.filter(Boolean);
}
+function formatExpiry(expiresAt: string | null | undefined, locale: string): string {
+ if (!expiresAt) return "-";
+ const date = new Date(expiresAt);
+ // 如果解析失败(如"永不过期"等翻译文本),直接返回原文本
+ if (Number.isNaN(date.getTime())) return expiresAt;
+ return formatDate(date, "yyyy-MM-dd", locale);
+}
+
+function getKeyExpiryStatus(
+ status: "enabled" | "disabled",
+ expiresAt: string | null | undefined
+): { label: string; variant: "default" | "secondary" | "destructive" | "outline" } {
+ if (status === "disabled") return { label: "disabled", variant: "secondary" };
+ if (!expiresAt) return { label: "active", variant: "default" };
+
+ const date = new Date(expiresAt);
+ if (Number.isNaN(date.getTime())) return { label: "active", variant: "default" };
+
+ const now = Date.now();
+ const expTs = date.getTime();
+
+ if (expTs <= now) return { label: "expired", variant: "destructive" };
+ if (expTs - now <= EXPIRING_SOON_MS) return { label: "expiringSoon", variant: "outline" };
+ return { label: "active", variant: "default" };
+}
+
export function KeyRowItem({
keyData,
userProviderGroup: _userProviderGroup,
@@ -148,6 +176,9 @@ export function KeyRowItem({
const keyGroups = splitGroups(keyData.providerGroup);
const effectiveGroups = keyGroups.length > 0 ? keyGroups : [translations.defaultGroup];
const visibleGroups = effectiveGroups.slice(0, 1);
+
+ // 计算 key 过期状态
+ const keyExpiryStatus = getKeyExpiryStatus(localStatus, localExpiresAt);
const remainingGroups = Math.max(0, effectiveGroups.length - visibleGroups.length);
const effectiveGroupText = effectiveGroups.join(", ");
@@ -194,13 +225,6 @@ export function KeyRowItem({
}
};
- const formatExpiry = (expiresAt: string | null | undefined): string => {
- if (!expiresAt) return "-";
- const date = new Date(expiresAt);
- if (Number.isNaN(date.getTime())) return "-";
- return formatDate(date, "yyyy-MM-dd", locale);
- };
-
const handleQuickRenewConfirm = async (
_keyId: number,
expiresAt: Date,
@@ -297,11 +321,8 @@ export function KeyRowItem({
{keyData.name}
-
- {localStatus === "enabled" ? translations.status.enabled : translations.status.disabled}
+
+ {tKeyStatus(keyExpiryStatus.label)}
@@ -444,7 +465,7 @@ export function KeyRowItem({
setQuickRenewOpen(true);
}}
>
- {formatExpiry(localExpiresAt)}
+ {formatExpiry(localExpiresAt, locale)}
{/* 操作 */}
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 fb4e3423f..722e642b6 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
@@ -13,6 +13,7 @@ import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useRouter } from "@/i18n/routing";
import { cn } from "@/lib/utils";
+import { getContrastTextColor, getGroupColor } from "@/lib/utils/color";
import { formatDate } from "@/lib/utils/date-format";
import type { UserDisplay } from "@/types/user";
import { KeyRowItem } from "./key-row-item";
@@ -66,6 +67,30 @@ export interface UserKeyTableRowProps {
}
const DEFAULT_GRID_COLUMNS_CLASS = "grid-cols-[minmax(260px,1fr)_120px_repeat(6,90px)_80px]";
+const EXPIRING_SOON_MS = 72 * 60 * 60 * 1000; // 72小时
+const MAX_VISIBLE_GROUPS = 2; // 最多显示的分组数量
+
+function splitGroups(value?: string | null): string[] {
+ return (value ?? "")
+ .split(",")
+ .map((g) => g.trim())
+ .filter(Boolean);
+}
+
+function getExpiryStatus(
+ isEnabled: boolean,
+ expiresAt: Date | null | undefined
+): { label: string; variant: "default" | "secondary" | "destructive" | "outline" } {
+ const now = Date.now();
+ const expTs = expiresAt?.getTime();
+ const hasExpiry = typeof expTs === "number" && Number.isFinite(expTs);
+
+ if (!isEnabled) return { label: "disabled", variant: "secondary" };
+ if (hasExpiry && expTs <= now) return { label: "expired", variant: "destructive" };
+ if (hasExpiry && expTs - now <= EXPIRING_SOON_MS)
+ return { label: "expiringSoon", variant: "outline" };
+ return { label: "active", variant: "default" };
+}
function normalizeLimitValue(value: unknown): number | null {
const raw = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
@@ -129,6 +154,14 @@ export function UserKeyTableRow({
const expiresText = formatExpiry(localExpiresAt ?? null, locale);
+ // 计算用户过期状态
+ const expiryStatus = getExpiryStatus(localIsEnabled, localExpiresAt ?? null);
+
+ // 处理 Provider Group:拆分成数组
+ const userGroups = splitGroups(user.providerGroup);
+ const visibleGroups = userGroups.slice(0, MAX_VISIBLE_GROUPS);
+ const remainingGroupsCount = Math.max(0, userGroups.length - MAX_VISIBLE_GROUPS);
+
const limit5h = normalizeLimitValue(user.limit5hUsd);
const limitDaily = normalizeLimitValue(user.dailyQuota);
const limitWeekly = normalizeLimitValue(user.limitWeeklyUsd);
@@ -222,11 +255,34 @@ export function UserKeyTableRow({
{isExpanded ? translations.collapse : translations.expand}
{user.name}
- {!localIsEnabled && (
+
+ {tUserStatus(expiryStatus.label)}
+
+ {visibleGroups.map((group) => {
+ const bgColor = getGroupColor(group);
+ return (
+
+ {group}
+
+ );
+ })}
+ {remainingGroupsCount > 0 && (
- {translations.userStatus?.disabled || "Disabled"}
+ +{remainingGroupsCount}
)}
+ {user.tags && user.tags.length > 0 && (
+
+ [{user.tags.join(", ")}]
+
+ )}
{user.note ? (
{user.note}
) : null}
diff --git a/src/app/v1/_lib/proxy/auth-guard.ts b/src/app/v1/_lib/proxy/auth-guard.ts
index dd5ebf28c..4f0645a03 100644
--- a/src/app/v1/_lib/proxy/auth-guard.ts
+++ b/src/app/v1/_lib/proxy/auth-guard.ts
@@ -25,7 +25,8 @@ export class ProxyAuthenticator {
return null;
}
- return ProxyResponses.buildError(401, "令牌已过期或验证不正确");
+ // 返回详细的错误信息,帮助用户快速定位问题
+ return authState.errorResponse ?? ProxyResponses.buildError(401, "认证失败");
}
private static async validate(headers: {
@@ -52,7 +53,17 @@ export class ProxyAuthenticator {
hasGeminiApiKeyHeader: !!headers.geminiApiKeyHeader,
hasGeminiApiKeyQuery: !!headers.geminiApiKeyQuery,
});
- return { user: null, key: null, apiKey: null, success: false };
+ return {
+ user: null,
+ key: null,
+ apiKey: null,
+ success: false,
+ errorResponse: ProxyResponses.buildError(
+ 401,
+ "未提供认证凭据。请在 Authorization 头部、x-api-key 头部或 x-goog-api-key 头部中包含 API 密钥。",
+ "authentication_error"
+ ),
+ };
}
const [firstKey] = providedKeys;
@@ -62,7 +73,17 @@ export class ProxyAuthenticator {
logger.warn("[ProxyAuthenticator] Multiple conflicting API keys provided", {
keyCount: providedKeys.length,
});
- return { user: null, key: null, apiKey: null, success: false };
+ return {
+ user: null,
+ key: null,
+ apiKey: null,
+ success: false,
+ errorResponse: ProxyResponses.buildError(
+ 401,
+ "提供了多个冲突的 API 密钥。请仅使用一种认证方式。",
+ "authentication_error"
+ ),
+ };
}
const apiKey = firstKey;
@@ -74,7 +95,17 @@ export class ProxyAuthenticator {
fromHeader: !!headers.authHeader || !!headers.apiKeyHeader || !!headers.geminiApiKeyHeader,
fromQuery: !!headers.geminiApiKeyQuery,
});
- return { user: null, key: null, apiKey, success: false };
+ return {
+ user: null,
+ key: null,
+ apiKey,
+ success: false,
+ errorResponse: ProxyResponses.buildError(
+ 401,
+ "API 密钥无效。提供的密钥不存在或已被删除。",
+ "invalid_api_key"
+ ),
+ };
}
// Check user status and expiration
@@ -86,7 +117,17 @@ export class ProxyAuthenticator {
userId: user.id,
userName: user.name,
});
- return { user: null, key: null, apiKey, success: false };
+ return {
+ user: null,
+ key: null,
+ apiKey,
+ success: false,
+ errorResponse: ProxyResponses.buildError(
+ 401,
+ "用户账户已被禁用。请联系管理员。",
+ "user_disabled"
+ ),
+ };
}
// 2. Check if user is expired (lazy expiration check)
@@ -103,7 +144,17 @@ export class ProxyAuthenticator {
error: error instanceof Error ? error.message : String(error),
});
});
- return { user: null, key: null, apiKey, success: false };
+ return {
+ user: null,
+ key: null,
+ apiKey,
+ success: false,
+ errorResponse: ProxyResponses.buildError(
+ 401,
+ `用户账户已于 ${user.expiresAt.toISOString().split("T")[0]} 过期。请续费订阅。`,
+ "user_expired"
+ ),
+ };
}
logger.debug("[ProxyAuthenticator] Authentication successful", {
diff --git a/src/app/v1/_lib/proxy/session.ts b/src/app/v1/_lib/proxy/session.ts
index e4d69d910..b4349047a 100644
--- a/src/app/v1/_lib/proxy/session.ts
+++ b/src/app/v1/_lib/proxy/session.ts
@@ -17,6 +17,7 @@ export interface AuthState {
key: Key | null;
apiKey: string | null;
success: boolean;
+ errorResponse?: Response; // 认证失败时的详细错误响应
}
export interface MessageContext {