diff --git a/src/admin-ui/admin-legacy.js b/src/admin-ui/admin-legacy.js
index 123860b..ef08da8 100644
--- a/src/admin-ui/admin-legacy.js
+++ b/src/admin-ui/admin-legacy.js
@@ -185,14 +185,53 @@ export function initAdminPage(options = {}) {
if (!Number.isFinite(seconds)) return null;
return Math.max(((seconds * 1000) - Date.now()) / 86400000, 1 / (24 * 60));
}
+ function msUntilReset(resetsAt) {
+ const seconds = Number(resetsAt);
+ if (!Number.isFinite(seconds)) return null;
+ return Math.max((seconds * 1000) - Date.now(), 60000);
+ }
function weightedWeeklyQuotaScore(remaining, refreshDays) {
if (remaining == null) return null;
return (remaining / 100) / ((refreshDays || 7) / 7);
}
+ function weightedQuotaWindowScore(remaining, limit, fallbackWindowMins) {
+ if (remaining == null) return null;
+ const windowMins = normalizeWindowDurationMins(limit?.windowDurationMins, fallbackWindowMins);
+ const resetMs = msUntilReset(limit?.resetsAt) || windowMins * 60000;
+ return (remaining / 100) / (resetMs / (windowMins * 60000));
+ }
function formatWeightedWeeklyQuotaScore(score) {
if (!Number.isFinite(Number(score))) return "0";
return Number(score).toFixed(2).replace(/\.00$/, "").replace(/(\.\d)0$/, "$1");
}
+ function normalizeWindowDurationMins(value, fallback) {
+ const mins = Number(value);
+ return Number.isFinite(mins) && mins > 0 ? mins : fallback;
+ }
+ function formatWindowDuration(windowMins) {
+ if (windowMins % 1440 === 0) return (windowMins / 1440) + "d";
+ if (windowMins % 60 === 0) return (windowMins / 60) + "h";
+ return windowMins + "m";
+ }
+ function quotaWindowDisplay(limit, fallbackWindowMins) {
+ const remaining = remainingPercent(limit?.usedPercent);
+ if (remaining == null) return null;
+ const windowMins = normalizeWindowDurationMins(limit?.windowDurationMins, fallbackWindowMins);
+ const score = weightedQuotaWindowScore(remaining, limit, windowMins);
+ return formatWindowDuration(windowMins) + " " + Math.round(remaining) + "% / " + formatWeightedWeeklyQuotaScore(score);
+ }
+ function authQuotaDisplay(rateLimits) {
+ const primary = rateLimits?.primary;
+ const primaryRemaining = remainingPercent(primary?.usedPercent);
+ const primaryScore = weightedQuotaWindowScore(primaryRemaining, primary, 300);
+ const shortLabel = Number.isFinite(Number(primaryScore)) && Number(primaryScore) < 0.5
+ ? quotaWindowDisplay(primary, 300)
+ : null;
+ return [
+ quotaWindowDisplay(rateLimits?.secondary, 10080),
+ shortLabel
+ ].filter(Boolean).join(" | ") || null;
+ }
function weeklyQuotaDisplay(limit) {
const remaining = remainingPercent(limit?.usedPercent);
if (remaining == null) return null;
@@ -219,11 +258,11 @@ export function initAdminPage(options = {}) {
if (plan === "chatgpt") return "ChatGPT";
return plan;
}
- function profileTooltip(profile, weeklyLabel, secondary) {
+ function profileTooltip(profile, quotaLabel, secondary) {
return [
profileAccountLabel(profile),
profilePlanLabel(profile),
- weeklyLabel ? "周额度 " + weeklyLabel : "",
+ quotaLabel ? "额度 " + quotaLabel : "",
secondary ? "周重置 " + formatResetTime(secondary.resetsAt) : "",
profile.name ? "内部标识 " + profile.name : ""
].filter(Boolean).join(" · ");
@@ -234,7 +273,7 @@ export function initAdminPage(options = {}) {
const rateLimits = profile.rateLimits || {};
if (rateLimits.ok === false) return null;
const secondary = rateLimits.rateLimits?.secondary;
- const label = weeklyQuotaDisplay(secondary);
+ const label = authQuotaDisplay(rateLimits.rateLimits);
if (!label) return null;
const remaining = remainingPercent(secondary?.usedPercent);
const score = weeklyQuotaScore(secondary);
@@ -504,9 +543,9 @@ export function initAdminPage(options = {}) {
if (!rateLimits || !rateLimits.ok) return '
' + esc(rateLimits?.error || "额度不可用") + '
';
const snapshot = rateLimits.rateLimits || {};
const secondary = snapshot.secondary;
- const weeklyLabel = weeklyQuotaDisplay(secondary);
+ const quotaLabel = authQuotaDisplay(snapshot);
return '' +
- '
周额度' + esc(weeklyLabel || "--") + '' + esc(secondary ? formatResetTime(secondary.resetsAt) : "不可用") + '
' +
+ '
额度' + esc(quotaLabel || "--") + '' + esc(secondary ? "周 " + formatResetTime(secondary.resetsAt) : "不可用") + '
' +
'
';
}
function renderAuthProfiles(data) {
diff --git a/src/admin-ui/admin.css b/src/admin-ui/admin.css
index 8c894f4..e36e583 100644
--- a/src/admin-ui/admin.css
+++ b/src/admin-ui/admin.css
@@ -142,7 +142,7 @@
.auth-profile-label { color: var(--muted); font-size: 10px; white-space: nowrap; }
.session-timeline-panel .mini-body { flex: 1; min-height: 0; overflow: hidden; padding: 0; }
- .timeline { height: 100%; display: grid; gap: 0; overflow: auto; border: 0; }
+ .timeline { height: 100%; display: grid; grid-auto-rows: max-content; align-content: start; gap: 0; overflow: auto; border: 0; }
.timeline-event { display: grid; grid-template-columns: 72px 90px minmax(0, 1fr); gap: 6px; align-items: start; padding: 4px 6px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 10px; }
.timeline-event:last-child { border-bottom: 0; }
.timeline-main { min-width: 0; }
diff --git a/src/admin-ui/auth-profile-display.ts b/src/admin-ui/auth-profile-display.ts
index d526b8b..20af283 100644
--- a/src/admin-ui/auth-profile-display.ts
+++ b/src/admin-ui/auth-profile-display.ts
@@ -1,4 +1,4 @@
-import { formatWeeklyQuotaDisplay } from "../auth-profile-quota.js";
+import { formatAuthQuotaDisplay } from "../auth-profile-quota.js";
type AuthProfileRecord = Record;
interface QuotaLabelOptions {
@@ -66,12 +66,12 @@ export function profileQuotaLabel(profile: AuthProfileRecord, options: QuotaLabe
}
const limits = rateLimits.rateLimits || {};
- const label = formatWeeklyQuotaDisplay({
- usedPercent: limits.secondary?.usedPercent,
- resetsAt: limits.secondary?.resetsAt,
+ const label = formatAuthQuotaDisplay({
+ primary: limits.primary,
+ secondary: limits.secondary,
now: options.now
});
- return label ?? "周额度未知";
+ return label ?? "额度未知";
}
export function profileWeeklyQuotaLabel(profile: AuthProfileRecord, options: QuotaLabelOptions = {}): string {
@@ -81,11 +81,11 @@ export function profileWeeklyQuotaLabel(profile: AuthProfileRecord, options: Quo
}
const limits = rateLimits.rateLimits || {};
- return formatWeeklyQuotaDisplay({
- usedPercent: limits.secondary?.usedPercent,
- resetsAt: limits.secondary?.resetsAt,
+ return formatAuthQuotaDisplay({
+ primary: limits.primary,
+ secondary: limits.secondary,
now: options.now
- }) ?? "周额度未知";
+ }) ?? "额度未知";
}
function readString(value: unknown): string | null {
diff --git a/src/admin-ui/session-view.tsx b/src/admin-ui/session-view.tsx
index be1e4e8..f971f0d 100644
--- a/src/admin-ui/session-view.tsx
+++ b/src/admin-ui/session-view.tsx
@@ -40,6 +40,7 @@ type TimelinePayload = {
} | TimelineEvent[];
const sessionFilters = ["ongoing", "all", "active", "inbound", "jobs", "issues", "usage"];
+const AUTO_AUTH_PROFILE_VALUE = "__auto_auth_profile__";
export function AdminSessionsView(): React.JSX.Element {
const permalinkSessionKey = readPermalinkSessionKey();
@@ -577,7 +578,7 @@ function AuthProfilePanel({ session, profiles, currentProfile: providedCurrentPr
readonly currentProfile?: SessionRecord | undefined;
}): React.JSX.Element {
const dialogRef = useRef(null);
- const [selected, setSelected] = useState(String(session.authProfileName || ""));
+ const [selected, setSelected] = useState(() => initialAuthProfileSelection(session));
const [busy, setBusy] = useState(false);
const [message, setMessage] = useState(null);
const currentProfile = providedCurrentProfile ?? profiles.find((profile) => profile.name === session.authProfileName);
@@ -588,7 +589,7 @@ function AuthProfilePanel({ session, profiles, currentProfile: providedCurrentPr
const compactLabel = currentProfile ? profileQuotaLabel(currentProfile) : (blocked ? "账号不可用" : "账号");
useEffect(() => {
- setSelected(String(session.authProfileName || ""));
+ setSelected(initialAuthProfileSelection(session));
setMessage(null);
}, [session.key, session.authProfileName, session.authBlockedAt]);
@@ -613,7 +614,8 @@ function AuthProfilePanel({ session, profiles, currentProfile: providedCurrentPr
}
async function switchProfile(): Promise {
- if (!selected || selected === session.authProfileName) {
+ const autoSelected = selected === AUTO_AUTH_PROFILE_VALUE;
+ if (!autoSelected && (!selected || selected === session.authProfileName)) {
return;
}
@@ -623,11 +625,11 @@ function AuthProfilePanel({ session, profiles, currentProfile: providedCurrentPr
await requestJson("/admin/api/sessions/" + encodeURIComponent(String(session.key || "")) + "/auth-profile", {
method: "POST",
headers: { "content-type": "application/json" },
- body: JSON.stringify({ name: selected })
+ body: JSON.stringify(autoSelected ? { mode: "auto" } : { name: selected })
});
const timelinePayload = await requestJson(sessionTimelineApiPath(String(session.key || "")));
publishTimelinePayload(String(session.key || ""), timelinePayload as TimelinePayload);
- setMessage("已切换,正在恢复待处理消息");
+ setMessage(autoSelected ? "已自动分配,正在恢复待处理消息" : "已切换,正在恢复待处理消息");
} catch (error) {
setMessage(error instanceof Error ? error.message : String(error));
} finally {
@@ -653,7 +655,7 @@ function AuthProfilePanel({ session, profiles, currentProfile: providedCurrentPr
{currentProfile ? (
- 周额度
+ 额度
{profileQuotaLabel(currentProfile)}
) : null}
@@ -671,6 +673,7 @@ function AuthProfilePanel({ session, profiles, currentProfile: providedCurrentPr
onChange={(event) => setSelected(event.target.value)}
>
+
{profiles.map((profile) => (