diff --git a/admin/api_key_concurrency.go b/admin/api_key_concurrency.go new file mode 100644 index 000000000..635dd1e41 --- /dev/null +++ b/admin/api_key_concurrency.go @@ -0,0 +1,12 @@ +package admin + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +// GetAPIKeyConcurrency 读取内存快照,避免轮询时重复聚合历史用量。 +func (h *Handler) GetAPIKeyConcurrency(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"concurrency": h.authCacheProxy.APIKeyConcurrencySnapshot()}) +} diff --git a/admin/handler.go b/admin/handler.go index a1e4b1b21..9a5277cdf 100644 --- a/admin/handler.go +++ b/admin/handler.go @@ -1265,6 +1265,7 @@ func (h *Handler) RegisterRoutes(r *gin.Engine) { api.DELETE("/usage/logs", h.ClearUsageLogs) api.GET("/setup-hints", h.GetSetupHints) api.GET("/keys", h.ListAPIKeys) + api.GET("/keys-concurrency", h.GetAPIKeyConcurrency) api.POST("/keys", h.CreateAPIKey) api.POST("/keys/reset-all-quotas", h.ResetAllAPIKeyQuotas) api.PATCH("/keys/:id", h.UpdateAPIKey) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2118db9ec..e3613d3fe 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3885,9 +3885,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" diff --git a/frontend/src/api.ts b/frontend/src/api.ts index b69f499ff..028c7019d 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -1248,6 +1248,7 @@ export const api = { return request<{ trend: AccountEventTrendPoint[] }>(`/accounts/event-trend?${sp.toString()}`) }, getAPIKeys: () => request('/keys'), + getAPIKeyConcurrency: () => request<{ concurrency: Record }>('/keys-concurrency'), createAPIKey: (data: CreateAPIKeyRequest) => request('/keys', { method: 'POST', diff --git a/frontend/src/components/APIKeyConcurrencyBadge.tsx b/frontend/src/components/APIKeyConcurrencyBadge.tsx new file mode 100644 index 000000000..4875d8ed2 --- /dev/null +++ b/frontend/src/components/APIKeyConcurrencyBadge.tsx @@ -0,0 +1,57 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { api } from "../api"; +import { Badge } from "./ui/badge"; + +const REFRESH_INTERVAL_MS = 5_000; + +export function useAPIKeyConcurrency() { + const [counts, setCounts] = useState | null>(null); + useEffect(() => { + let cancelled = false; + let pending = false; + const refresh = async () => { + if (pending || document.hidden) return; + pending = true; + try { + const response = await api.getAPIKeyConcurrency(); + if (!cancelled) setCounts(response.concurrency); + } catch { + // 暂时读取失败时保留上次快照,下一次轮询重试。 + } finally { + pending = false; + } + }; + void refresh(); + const timer = window.setInterval(() => void refresh(), REFRESH_INTERVAL_MS); + document.addEventListener("visibilitychange", refresh); + return () => { + cancelled = true; + window.clearInterval(timer); + document.removeEventListener("visibilitychange", refresh); + }; + }, []); + return counts; +} + +export default function APIKeyConcurrencyBadge({ current, limit }: { + current?: number; + limit?: number; +}) { + const { t } = useTranslation(); + if (current === undefined || current <= 0) return null; + + return ( + + + {current}/{limit && limit > 0 ? limit : "∞"} + + ); +} diff --git a/frontend/src/components/DaybreakBadge.tsx b/frontend/src/components/DaybreakBadge.tsx index 3ef3ee090..71ff5a850 100644 --- a/frontend/src/components/DaybreakBadge.tsx +++ b/frontend/src/components/DaybreakBadge.tsx @@ -24,7 +24,7 @@ export default function DaybreakBadge({ return ( {variant.label} diff --git a/frontend/src/pages/APIKeys.tsx b/frontend/src/pages/APIKeys.tsx index 63e3d0ec7..66c0710d2 100644 --- a/frontend/src/pages/APIKeys.tsx +++ b/frontend/src/pages/APIKeys.tsx @@ -10,6 +10,7 @@ import { import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { api } from "../api"; +import APIKeyConcurrencyBadge, { useAPIKeyConcurrency } from "../components/APIKeyConcurrencyBadge"; import APIKeyTokenUsagePanel from "../components/APIKeyTokenUsagePanel"; import APIKeyModelRequestLimitsEditor from "../components/APIKeyModelRequestLimitsEditor"; import APIKeyModelRequestUsageCard from "../components/APIKeyModelRequestUsage"; @@ -405,6 +406,7 @@ export default function APIKeys() { }, load: loadKeys, }); + const concurrency = useAPIKeyConcurrency(); const keys = data.keys; const groups = data.groups; const modelOptions = data.modelOptions; @@ -1548,6 +1550,10 @@ export default function APIKeys() { {keyRow.name} + {isNew ? ( {keyRow.name} + {isNew ? ( )} + {isFullCard && resetCredits > 0 && (