ai-zero-token 1.0.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { requestText } from "../providers/http-client.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const PLATFORM_PROBE_HEADERS = {
7
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8
+ "accept-language": "zh-CN,zh;q=0.9,en;q=0.8",
9
+ "cache-control": "no-cache",
10
+ "pragma": "no-cache",
11
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
12
+ };
13
+ function parseKeyValueText(input) {
14
+ return Object.fromEntries(
15
+ input.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => {
16
+ const index = line.indexOf("=");
17
+ if (index <= 0) {
18
+ return ["", ""];
19
+ }
20
+ return [line.slice(0, index), line.slice(index + 1)];
21
+ }).filter(([key]) => key)
22
+ );
23
+ }
24
+ function parseDnsServers(text) {
25
+ const servers = /* @__PURE__ */ new Set();
26
+ for (const match of text.matchAll(/nameserver\[\d+\]\s*:\s*([0-9a-fA-F:.]+)/g)) {
27
+ servers.add(match[1]);
28
+ }
29
+ return [...servers];
30
+ }
31
+ function isPrivateOrReservedIp(ip) {
32
+ return /^(10|127|169\.254|172\.(1[6-9]|2\d|3[0-1])|192\.168)\./.test(ip) || /^0\./.test(ip) || /^100\.(6[4-9]|[7-9]\d|1\d\d|2[0-3]\d|24[0-7])\./.test(ip) || /^198\.(18|19)\./.test(ip) || /^203\.0\.113\./.test(ip) || /^192\.0\.2\./.test(ip) || /^198\.51\.100\./.test(ip) || /^fc00:/i.test(ip) || /^fd00:/i.test(ip) || /^fe80:/i.test(ip) || /^::1$/.test(ip);
33
+ }
34
+ function toCountryName(code) {
35
+ if (!code) {
36
+ return void 0;
37
+ }
38
+ try {
39
+ return new Intl.DisplayNames(["zh-CN"], { type: "region" }).of(code) ?? code;
40
+ } catch {
41
+ return code;
42
+ }
43
+ }
44
+ async function runCommand(command, args, timeoutMs = 2500) {
45
+ const result = await execFileAsync(command, args, {
46
+ timeout: timeoutMs,
47
+ maxBuffer: 1024 * 128
48
+ });
49
+ return String(result.stdout || "");
50
+ }
51
+ async function readDnsServers() {
52
+ const servers = /* @__PURE__ */ new Set();
53
+ let source = "\u7CFB\u7EDF DNS";
54
+ try {
55
+ const scutil = await runCommand("scutil", ["--dns"], 2500);
56
+ parseDnsServers(scutil).forEach((item) => servers.add(item));
57
+ source = "scutil --dns";
58
+ } catch {
59
+ }
60
+ try {
61
+ const wifiDns = await runCommand("networksetup", ["-getdnsservers", "Wi-Fi"], 2500);
62
+ for (const line of wifiDns.split(/\r?\n/)) {
63
+ const value = line.trim();
64
+ if (value && /^[0-9a-fA-F:.]+$/.test(value)) {
65
+ servers.add(value);
66
+ }
67
+ }
68
+ if (servers.size > 0) {
69
+ source = `${source} + networksetup`;
70
+ }
71
+ } catch {
72
+ }
73
+ return {
74
+ servers: [...servers],
75
+ source
76
+ };
77
+ }
78
+ async function probePublicIpv4(proxy) {
79
+ const startedAt = performance.now();
80
+ const response = await requestText({
81
+ method: "GET",
82
+ url: "https://ifconfig.me/ip",
83
+ timeoutMs: 8e3,
84
+ proxyOverride: proxy
85
+ });
86
+ if (response.status < 200 || response.status >= 500) {
87
+ throw new Error(`\u516C\u7F51 IPv4 \u63A2\u6D4B\u5931\u8D25: HTTP ${response.status}`);
88
+ }
89
+ const ip = response.body.trim();
90
+ let trace = {};
91
+ try {
92
+ const traceResponse = await requestText({
93
+ method: "GET",
94
+ url: "https://www.cloudflare.com/cdn-cgi/trace",
95
+ timeoutMs: 8e3,
96
+ proxyOverride: proxy
97
+ });
98
+ trace = parseKeyValueText(traceResponse.body);
99
+ } catch {
100
+ trace = {};
101
+ }
102
+ const countryCode = trace.loc;
103
+ return {
104
+ ip,
105
+ countryCode,
106
+ countryName: toCountryName(countryCode),
107
+ colo: trace.colo,
108
+ source: "ifconfig.me + cloudflare trace",
109
+ elapsedMs: Math.round(performance.now() - startedAt)
110
+ };
111
+ }
112
+ async function probePublicIpv6(proxy) {
113
+ const startedAt = performance.now();
114
+ const ipv6Candidates = [
115
+ "https://ifconfig.me/ipv6",
116
+ "https://ipv6.test-ipv6.com/"
117
+ ];
118
+ for (const url of ipv6Candidates) {
119
+ try {
120
+ const response = await requestText({
121
+ method: "GET",
122
+ url,
123
+ timeoutMs: 6e3,
124
+ proxyOverride: proxy
125
+ });
126
+ const body = response.body.trim();
127
+ if (!body) {
128
+ continue;
129
+ }
130
+ if (/^[0-9a-fA-F:]+$/.test(body) && body.includes(":")) {
131
+ return {
132
+ available: true,
133
+ ip: body,
134
+ source: url,
135
+ detail: "\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\u3002",
136
+ elapsedMs: Math.round(performance.now() - startedAt)
137
+ };
138
+ }
139
+ } catch {
140
+ }
141
+ }
142
+ return {
143
+ available: false,
144
+ source: "ifconfig.me / test-ipv6.com",
145
+ detail: "\u672A\u68C0\u6D4B\u5230\u72EC\u7ACB IPv6 \u51FA\u53E3\uFF0C\u5F53\u524D\u8FDE\u63A5\u53EF\u80FD\u53EA\u8D70 IPv4\u3002"
146
+ };
147
+ }
148
+ async function probePlatform(url, label, proxy) {
149
+ const startedAt = performance.now();
150
+ try {
151
+ const response = await requestText({
152
+ method: "GET",
153
+ url,
154
+ headers: PLATFORM_PROBE_HEADERS,
155
+ timeoutMs: 8e3,
156
+ proxyOverride: proxy
157
+ });
158
+ const elapsedMs = Math.round(performance.now() - startedAt);
159
+ if (response.status >= 200 && response.status < 400) {
160
+ return {
161
+ key: label,
162
+ label,
163
+ url,
164
+ status: "\u53EF\u8FBE",
165
+ detail: `HTTP ${response.status} \u53EF\u6B63\u5E38\u8BBF\u95EE`,
166
+ tone: "green",
167
+ httpStatus: response.status,
168
+ elapsedMs
169
+ };
170
+ }
171
+ if (response.status >= 400 && response.status < 500) {
172
+ return {
173
+ key: label,
174
+ label,
175
+ url,
176
+ status: "\u53D7\u9650",
177
+ detail: `HTTP ${response.status}\uFF0C\u7F51\u7EDC\u53EF\u8FBE\u4F46\u7AD9\u70B9\u8FD4\u56DE\u9650\u5236`,
178
+ tone: "orange",
179
+ httpStatus: response.status,
180
+ elapsedMs
181
+ };
182
+ }
183
+ return {
184
+ key: label,
185
+ label,
186
+ url,
187
+ status: "\u4E0D\u53EF\u7528",
188
+ detail: `HTTP ${response.status}\uFF0C\u7AD9\u70B9\u4E0D\u53EF\u7528`,
189
+ tone: "red",
190
+ httpStatus: response.status,
191
+ elapsedMs
192
+ };
193
+ } catch (error) {
194
+ return {
195
+ key: label,
196
+ label,
197
+ url,
198
+ status: "\u4E0D\u53EF\u7528",
199
+ detail: error instanceof Error ? error.message : String(error),
200
+ tone: "red",
201
+ elapsedMs: Math.round(performance.now() - startedAt)
202
+ };
203
+ }
204
+ }
205
+ class NetworkDetectService {
206
+ async collectReport(proxy) {
207
+ const [publicIpv4, publicIpv6, dns, platforms] = await Promise.all([
208
+ probePublicIpv4(proxy),
209
+ probePublicIpv6(proxy),
210
+ readDnsServers(),
211
+ Promise.all([
212
+ probePlatform("https://www.google.com/generate_204", "Google", proxy),
213
+ probePlatform("https://chatgpt.com/", "ChatGPT", proxy),
214
+ probePlatform("https://claude.ai/", "Claude", proxy),
215
+ probePlatform("https://www.youtube.com/", "YouTube", proxy),
216
+ probePlatform("https://x.com/", "X", proxy)
217
+ ])
218
+ ]);
219
+ const dnsDetail = dns.servers.length > 0 ? `\u7CFB\u7EDF DNS: ${dns.servers.join(" / ")}` : "\u672A\u8BFB\u53D6\u5230\u7CFB\u7EDF DNS\u3002";
220
+ return {
221
+ checkedAt: Date.now(),
222
+ publicIpv4,
223
+ publicIpv6,
224
+ dns: {
225
+ servers: dns.servers,
226
+ source: dns.source,
227
+ detail: dnsDetail
228
+ },
229
+ proxy: {
230
+ enabled: Boolean(proxy?.enabled && proxy.url.trim()),
231
+ url: proxy?.url?.trim() || void 0
232
+ },
233
+ platforms
234
+ };
235
+ }
236
+ }
237
+ export {
238
+ NetworkDetectService
239
+ };
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import { app as electronApp, BrowserWindow, dialog, shell } from "electron";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { startServer } from "../server/index.js";
6
+ let gatewayServer = null;
7
+ let mainWindow = null;
8
+ let isQuitting = false;
9
+ const desktopDir = path.dirname(fileURLToPath(import.meta.url));
10
+ const appIconPath = path.resolve(desktopDir, "../../build/icon.png");
11
+ electronApp.setName("AI Zero Token");
12
+ function createBrowserUrl(host, port) {
13
+ if (host === "0.0.0.0" || host === "::") {
14
+ return `http://127.0.0.1:${port}`;
15
+ }
16
+ return `http://${host}:${port}`;
17
+ }
18
+ function isGatewayUrl(targetUrl, gatewayUrl) {
19
+ try {
20
+ const target = new URL(targetUrl);
21
+ const gateway = new URL(gatewayUrl);
22
+ return target.origin === gateway.origin;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+ function resolveAdminUrl(gatewayUrl) {
28
+ const devUrl = process.env.AZT_ADMIN_UI_DEV_URL?.trim();
29
+ return devUrl || gatewayUrl;
30
+ }
31
+ async function ensureGatewayServer() {
32
+ if (gatewayServer) {
33
+ return gatewayServer;
34
+ }
35
+ gatewayServer = await startServer();
36
+ const adminUrl = createBrowserUrl(gatewayServer.host, gatewayServer.port);
37
+ console.log("AI Zero Token desktop gateway started.");
38
+ console.log(`admin: ${adminUrl}`);
39
+ console.log(`apiBase: ${adminUrl}/v1`);
40
+ console.log(`listen: http://${gatewayServer.host}:${gatewayServer.port}`);
41
+ return gatewayServer;
42
+ }
43
+ async function createMainWindow() {
44
+ const server = await ensureGatewayServer();
45
+ const gatewayUrl = createBrowserUrl(server.host, server.port);
46
+ const adminUrl = resolveAdminUrl(gatewayUrl);
47
+ mainWindow = new BrowserWindow({
48
+ width: 1440,
49
+ height: 960,
50
+ minWidth: 1100,
51
+ minHeight: 720,
52
+ title: "AI Zero Token",
53
+ icon: appIconPath,
54
+ backgroundColor: "#f8fafc",
55
+ webPreferences: {
56
+ contextIsolation: true,
57
+ nodeIntegration: false,
58
+ sandbox: true
59
+ }
60
+ });
61
+ mainWindow.webContents.setWindowOpenHandler(({ url }) => {
62
+ void shell.openExternal(url);
63
+ return { action: "deny" };
64
+ });
65
+ mainWindow.webContents.on("will-navigate", (event, url) => {
66
+ if (isGatewayUrl(url, adminUrl) || isGatewayUrl(url, gatewayUrl)) {
67
+ return;
68
+ }
69
+ event.preventDefault();
70
+ void shell.openExternal(url);
71
+ });
72
+ mainWindow.on("closed", () => {
73
+ mainWindow = null;
74
+ });
75
+ await mainWindow.loadURL(adminUrl);
76
+ }
77
+ function focusMainWindow() {
78
+ if (!mainWindow) {
79
+ void createMainWindow().catch(handleStartupError);
80
+ return;
81
+ }
82
+ if (mainWindow.isMinimized()) {
83
+ mainWindow.restore();
84
+ }
85
+ mainWindow.focus();
86
+ }
87
+ async function closeGatewayServer() {
88
+ if (!gatewayServer) {
89
+ return;
90
+ }
91
+ const server = gatewayServer;
92
+ gatewayServer = null;
93
+ await server.app.close();
94
+ }
95
+ function handleStartupError(error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ console.error("[desktop:error]", error);
98
+ if (electronApp.isReady()) {
99
+ dialog.showErrorBox("AI Zero Token \u542F\u52A8\u5931\u8D25", message);
100
+ }
101
+ electronApp.quit();
102
+ }
103
+ const hasSingleInstanceLock = electronApp.requestSingleInstanceLock();
104
+ if (!hasSingleInstanceLock) {
105
+ electronApp.quit();
106
+ } else {
107
+ electronApp.on("second-instance", () => {
108
+ focusMainWindow();
109
+ });
110
+ electronApp.whenReady().then(() => {
111
+ if (process.platform === "darwin") {
112
+ electronApp.dock?.setIcon(appIconPath);
113
+ }
114
+ electronApp.setAboutPanelOptions({
115
+ applicationName: "AI Zero Token",
116
+ applicationVersion: electronApp.getVersion(),
117
+ iconPath: appIconPath
118
+ });
119
+ return createMainWindow();
120
+ }).catch(handleStartupError);
121
+ electronApp.on("activate", () => {
122
+ if (BrowserWindow.getAllWindows().length === 0) {
123
+ focusMainWindow();
124
+ }
125
+ });
126
+ electronApp.on("window-all-closed", () => {
127
+ if (process.platform !== "darwin") {
128
+ electronApp.quit();
129
+ }
130
+ });
131
+ electronApp.on("before-quit", (event) => {
132
+ if (!gatewayServer || isQuitting) {
133
+ return;
134
+ }
135
+ event.preventDefault();
136
+ isQuitting = true;
137
+ void closeGatewayServer().catch((error) => {
138
+ console.error("[desktop:gateway:close]", error);
139
+ }).finally(() => {
140
+ electronApp.quit();
141
+ });
142
+ });
143
+ }
@@ -1898,6 +1898,7 @@ function renderAdminPage() {
1898
1898
  <option value="all">\u5168\u90E8\u72B6\u6001</option>
1899
1899
  <option value="healthy">\u5065\u5EB7</option>
1900
1900
  <option value="warning">\u5373\u5C06\u8017\u5C3D</option>
1901
+ <option value="invalid">\u767B\u5F55\u5931\u6548</option>
1901
1902
  <option value="expired">\u5DF2\u8FC7\u671F</option>
1902
1903
  <option value="active">\u4F7F\u7528\u4E2D</option>
1903
1904
  </select>
@@ -2302,6 +2303,13 @@ function renderAdminPage() {
2302
2303
  return date.toLocaleString("zh-CN", { hour12: false });
2303
2304
  }
2304
2305
 
2306
+ function timestampToMillis(value) {
2307
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2308
+ return null;
2309
+ }
2310
+ return value < 1000000000000 ? value * 1000 : value;
2311
+ }
2312
+
2305
2313
  function formatShortTime(value) {
2306
2314
  if (!value) {
2307
2315
  return "--:--";
@@ -2528,6 +2536,15 @@ function renderAdminPage() {
2528
2536
  };
2529
2537
  }
2530
2538
 
2539
+ if (profile.authStatus && (profile.authStatus.state === "token_invalidated" || profile.authStatus.state === "auth_error")) {
2540
+ return {
2541
+ supported: false,
2542
+ label: "\u8BA4\u8BC1\u5931\u6548",
2543
+ detail: "\u8D26\u53F7\u8BA4\u8BC1\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u4F7F\u7528\u56FE\u7247\u751F\u6210\u3002",
2544
+ badgeClass: "red",
2545
+ };
2546
+ }
2547
+
2531
2548
  const planType = getPlanType(profile);
2532
2549
  if (planType === "free") {
2533
2550
  return {
@@ -2546,6 +2563,17 @@ function renderAdminPage() {
2546
2563
  };
2547
2564
  }
2548
2565
 
2566
+ function describeAuthStatus(profile) {
2567
+ const authStatus = profile && profile.authStatus ? profile.authStatus : null;
2568
+ if (!authStatus || authStatus.state === "ok") {
2569
+ return authStatus && authStatus.checkedAt ? "\u6B63\u5E38 \xB7 " + formatTime(authStatus.checkedAt) : "\u6B63\u5E38";
2570
+ }
2571
+
2572
+ const prefix = authStatus.state === "token_invalidated" ? "\u767B\u5F55\u5931\u6548" : "\u8BA4\u8BC1\u5F02\u5E38";
2573
+ const detail = authStatus.code || authStatus.httpStatus ? " (" + (authStatus.code || authStatus.httpStatus) + ")" : "";
2574
+ return prefix + detail + " \xB7 " + formatTime(authStatus.checkedAt);
2575
+ }
2576
+
2549
2577
  function maskEmail(email) {
2550
2578
  if (typeof email !== "string" || email.indexOf("@") === -1) {
2551
2579
  return email || "";
@@ -2620,6 +2648,10 @@ function renderAdminPage() {
2620
2648
  return Math.max(0, Math.min(100, value));
2621
2649
  }
2622
2650
 
2651
+ function getPrimaryRemaining(profile) {
2652
+ return 100 - getPrimaryUsage(profile);
2653
+ }
2654
+
2623
2655
  function getSecondaryUsage(profile) {
2624
2656
  const value = profile && profile.quota && typeof profile.quota.secondaryUsedPercent === "number"
2625
2657
  ? profile.quota.secondaryUsedPercent
@@ -2664,6 +2696,22 @@ function renderAdminPage() {
2664
2696
 
2665
2697
  function getProfileHealth(profile) {
2666
2698
  const now = Date.now();
2699
+ if (profile && profile.authStatus && profile.authStatus.state === "token_invalidated") {
2700
+ return {
2701
+ key: "invalid",
2702
+ label: "\u767B\u5F55\u5931\u6548",
2703
+ badgeClass: "red",
2704
+ barClass: "red",
2705
+ };
2706
+ }
2707
+ if (profile && profile.authStatus && profile.authStatus.state === "auth_error") {
2708
+ return {
2709
+ key: "invalid",
2710
+ label: "\u8BA4\u8BC1\u5F02\u5E38",
2711
+ badgeClass: "red",
2712
+ barClass: "red",
2713
+ };
2714
+ }
2667
2715
  if (profile && profile.expiresAt && profile.expiresAt <= now) {
2668
2716
  return {
2669
2717
  key: "expired",
@@ -2700,6 +2748,22 @@ function renderAdminPage() {
2700
2748
  };
2701
2749
  }
2702
2750
 
2751
+ function isProfileUnavailable(profile) {
2752
+ const health = getProfileHealth(profile);
2753
+ return health.key === "invalid" || health.key === "expired";
2754
+ }
2755
+
2756
+ function getProfileSortGroup(profile, codexAccountId) {
2757
+ const isCodexActive = Boolean(codexAccountId && profile.accountId === codexAccountId);
2758
+ if (profile.isActive || isCodexActive) {
2759
+ return 0;
2760
+ }
2761
+ if (isProfileUnavailable(profile)) {
2762
+ return 2;
2763
+ }
2764
+ return 1;
2765
+ }
2766
+
2703
2767
  function describeReset(profile, slot) {
2704
2768
  if (!profile || !profile.quota) {
2705
2769
  return "\u6682\u65E0\u6570\u636E";
@@ -2708,8 +2772,9 @@ function renderAdminPage() {
2708
2772
  const quota = profile.quota;
2709
2773
  const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
2710
2774
  const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
2711
- if (typeof resetAt === "number" && resetAt > 0) {
2712
- return formatTime(resetAt * 1000);
2775
+ const resetAtMillis = timestampToMillis(resetAt);
2776
+ if (resetAtMillis) {
2777
+ return formatTime(resetAtMillis);
2713
2778
  }
2714
2779
  if (typeof resetAfter === "number" && resetAfter > 0) {
2715
2780
  return formatCompactDuration(resetAfter) + "\u540E";
@@ -2739,11 +2804,13 @@ function renderAdminPage() {
2739
2804
  const quota = profile.quota;
2740
2805
  const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
2741
2806
  const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
2742
- if (typeof resetAt === "number" && resetAt > 0) {
2743
- return formatCompactDateTime(resetAt * 1000);
2807
+ const resetAtMillis = timestampToMillis(resetAt);
2808
+ if (resetAtMillis) {
2809
+ return formatCompactDateTime(resetAtMillis);
2744
2810
  }
2745
2811
  if (typeof resetAfter === "number" && resetAfter > 0) {
2746
- return formatCompactDuration(resetAfter) + "\u540E";
2812
+ const capturedAt = timestampToMillis(quota.capturedAt);
2813
+ return capturedAt ? formatCompactDateTime(capturedAt + resetAfter * 1000) : formatCompactDuration(resetAfter) + "\u540E";
2747
2814
  }
2748
2815
  return "\u672A\u77E5";
2749
2816
  }
@@ -3169,6 +3236,9 @@ function renderAdminPage() {
3169
3236
  if (status === "warning" && health.key !== "warning") {
3170
3237
  return false;
3171
3238
  }
3239
+ if (status === "invalid" && health.key !== "invalid") {
3240
+ return false;
3241
+ }
3172
3242
  if (status === "expired" && health.key !== "expired") {
3173
3243
  return false;
3174
3244
  }
@@ -3176,24 +3246,18 @@ function renderAdminPage() {
3176
3246
  });
3177
3247
 
3178
3248
  filtered.sort(function (a, b) {
3179
- const aCodexActive = Boolean(codexAccountId && a.accountId === codexAccountId);
3180
- const bCodexActive = Boolean(codexAccountId && b.accountId === codexAccountId);
3181
- const activeDiff = Number(b.isActive || bCodexActive) - Number(a.isActive || aCodexActive);
3182
- if (activeDiff !== 0) {
3183
- return activeDiff;
3184
- }
3185
- const gatewayDiff = Number(b.isActive) - Number(a.isActive);
3186
- if (gatewayDiff !== 0) {
3187
- return gatewayDiff;
3188
- }
3189
- const codexDiff = Number(bCodexActive) - Number(aCodexActive);
3190
- if (codexDiff !== 0) {
3191
- return codexDiff;
3249
+ const groupDiff = getProfileSortGroup(a, codexAccountId) - getProfileSortGroup(b, codexAccountId);
3250
+ if (groupDiff !== 0) {
3251
+ return groupDiff;
3192
3252
  }
3193
3253
  const planDiff = getPlanRank(b) - getPlanRank(a);
3194
3254
  if (planDiff !== 0) {
3195
3255
  return planDiff;
3196
3256
  }
3257
+ const primaryRemainingDiff = getPrimaryRemaining(b) - getPrimaryRemaining(a);
3258
+ if (primaryRemainingDiff !== 0) {
3259
+ return primaryRemainingDiff;
3260
+ }
3197
3261
  if (sort === "latency-asc") {
3198
3262
  const aCapturedAt = getQuotaSnapshotTime(a) || 0;
3199
3263
  const bCapturedAt = getQuotaSnapshotTime(b) || 0;
@@ -3344,6 +3408,7 @@ function renderAdminPage() {
3344
3408
  ? '<div class="meta-grid">'
3345
3409
  + '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
3346
3410
  + '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
3411
+ + '<div class="meta-item"><label>\u8BA4\u8BC1\u72B6\u6001</label><strong>' + escapeHtml(describeAuthStatus(profile)) + "</strong></div>"
3347
3412
  + '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
3348
3413
  + '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
3349
3414
  + '<div class="meta-item"><label>Account ID</label><code>' + escapeHtml(state.showEmails ? (profile.accountId || "\u672A\u63D0\u4F9B") : maskIdentifier(profile.accountId || "\u672A\u63D0\u4F9B")) + "</code></div>"
@@ -3353,6 +3418,7 @@ function renderAdminPage() {
3353
3418
  + '<div class="account-actions">'
3354
3419
  + actionButton
3355
3420
  + codexButton
3421
+ + '<button class="btn-secondary" type="button" data-profile-action="sync-quota" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5237\u65B0\u989D\u5EA6</button>'
3356
3422
  + '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
3357
3423
  + '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
3358
3424
  + "</div>"
@@ -3862,6 +3928,28 @@ function renderAdminPage() {
3862
3928
  await applyProfileToCodex(profileId, button);
3863
3929
  return;
3864
3930
  }
3931
+ if (action === "sync-quota") {
3932
+ setBusy(button, true);
3933
+ authStatus.textContent = "\u6B63\u5728\u5237\u65B0\u8D26\u53F7\u989D\u5EA6...";
3934
+ try {
3935
+ const config = await fetchJson("/_gateway/admin/profiles/sync-quota", {
3936
+ method: "POST",
3937
+ headers: {
3938
+ "Content-Type": "application/json",
3939
+ },
3940
+ body: formatJson({
3941
+ profileId: profileId,
3942
+ }),
3943
+ });
3944
+ renderConfig(config);
3945
+ authStatus.textContent = "\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65\u3002";
3946
+ } catch (error) {
3947
+ authStatus.textContent = error.message;
3948
+ } finally {
3949
+ setBusy(button, false);
3950
+ }
3951
+ return;
3952
+ }
3865
3953
 
3866
3954
  setBusy(button, true);
3867
3955
  authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
@@ -4285,7 +4373,12 @@ function renderAdminPage() {
4285
4373
  authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
4286
4374
  refreshConfig({
4287
4375
  syncRuntime: true,
4288
- }).then(function () {
4376
+ }).then(function (config) {
4377
+ if (config && config.quotaSync) {
4378
+ const sync = config.quotaSync;
4379
+ authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0: " + String(sync.synced) + "/" + String(sync.total) + " \u4E2A\u8D26\u53F7\u6210\u529F" + (sync.failed ? "\uFF0C" + String(sync.failed) + " \u4E2A\u5931\u8D25" : "") + (sync.skipped ? "\uFF0C" + String(sync.skipped) + " \u4E2A\u767B\u5F55\u5931\u6548\u5DF2\u8DF3\u8FC7" : "") + "\u3002";
4380
+ return;
4381
+ }
4289
4382
  authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0\u3002";
4290
4383
  }).catch(function (error) {
4291
4384
  authStatus.textContent = error && error.message ? error.message : String(error);