ai-zero-token 1.0.9 → 1.0.10

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.
@@ -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 || "";
@@ -2664,6 +2692,22 @@ function renderAdminPage() {
2664
2692
 
2665
2693
  function getProfileHealth(profile) {
2666
2694
  const now = Date.now();
2695
+ if (profile && profile.authStatus && profile.authStatus.state === "token_invalidated") {
2696
+ return {
2697
+ key: "invalid",
2698
+ label: "\u767B\u5F55\u5931\u6548",
2699
+ badgeClass: "red",
2700
+ barClass: "red",
2701
+ };
2702
+ }
2703
+ if (profile && profile.authStatus && profile.authStatus.state === "auth_error") {
2704
+ return {
2705
+ key: "invalid",
2706
+ label: "\u8BA4\u8BC1\u5F02\u5E38",
2707
+ badgeClass: "red",
2708
+ barClass: "red",
2709
+ };
2710
+ }
2667
2711
  if (profile && profile.expiresAt && profile.expiresAt <= now) {
2668
2712
  return {
2669
2713
  key: "expired",
@@ -2708,8 +2752,9 @@ function renderAdminPage() {
2708
2752
  const quota = profile.quota;
2709
2753
  const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
2710
2754
  const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
2711
- if (typeof resetAt === "number" && resetAt > 0) {
2712
- return formatTime(resetAt * 1000);
2755
+ const resetAtMillis = timestampToMillis(resetAt);
2756
+ if (resetAtMillis) {
2757
+ return formatTime(resetAtMillis);
2713
2758
  }
2714
2759
  if (typeof resetAfter === "number" && resetAfter > 0) {
2715
2760
  return formatCompactDuration(resetAfter) + "\u540E";
@@ -2739,11 +2784,13 @@ function renderAdminPage() {
2739
2784
  const quota = profile.quota;
2740
2785
  const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
2741
2786
  const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
2742
- if (typeof resetAt === "number" && resetAt > 0) {
2743
- return formatCompactDateTime(resetAt * 1000);
2787
+ const resetAtMillis = timestampToMillis(resetAt);
2788
+ if (resetAtMillis) {
2789
+ return formatCompactDateTime(resetAtMillis);
2744
2790
  }
2745
2791
  if (typeof resetAfter === "number" && resetAfter > 0) {
2746
- return formatCompactDuration(resetAfter) + "\u540E";
2792
+ const capturedAt = timestampToMillis(quota.capturedAt);
2793
+ return capturedAt ? formatCompactDateTime(capturedAt + resetAfter * 1000) : formatCompactDuration(resetAfter) + "\u540E";
2747
2794
  }
2748
2795
  return "\u672A\u77E5";
2749
2796
  }
@@ -3169,6 +3216,9 @@ function renderAdminPage() {
3169
3216
  if (status === "warning" && health.key !== "warning") {
3170
3217
  return false;
3171
3218
  }
3219
+ if (status === "invalid" && health.key !== "invalid") {
3220
+ return false;
3221
+ }
3172
3222
  if (status === "expired" && health.key !== "expired") {
3173
3223
  return false;
3174
3224
  }
@@ -3344,6 +3394,7 @@ function renderAdminPage() {
3344
3394
  ? '<div class="meta-grid">'
3345
3395
  + '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
3346
3396
  + '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
3397
+ + '<div class="meta-item"><label>\u8BA4\u8BC1\u72B6\u6001</label><strong>' + escapeHtml(describeAuthStatus(profile)) + "</strong></div>"
3347
3398
  + '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
3348
3399
  + '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
3349
3400
  + '<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 +3404,7 @@ function renderAdminPage() {
3353
3404
  + '<div class="account-actions">'
3354
3405
  + actionButton
3355
3406
  + codexButton
3407
+ + '<button class="btn-secondary" type="button" data-profile-action="sync-quota" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5237\u65B0\u989D\u5EA6</button>'
3356
3408
  + '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
3357
3409
  + '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
3358
3410
  + "</div>"
@@ -3862,6 +3914,28 @@ function renderAdminPage() {
3862
3914
  await applyProfileToCodex(profileId, button);
3863
3915
  return;
3864
3916
  }
3917
+ if (action === "sync-quota") {
3918
+ setBusy(button, true);
3919
+ authStatus.textContent = "\u6B63\u5728\u5237\u65B0\u8D26\u53F7\u989D\u5EA6...";
3920
+ try {
3921
+ const config = await fetchJson("/_gateway/admin/profiles/sync-quota", {
3922
+ method: "POST",
3923
+ headers: {
3924
+ "Content-Type": "application/json",
3925
+ },
3926
+ body: formatJson({
3927
+ profileId: profileId,
3928
+ }),
3929
+ });
3930
+ renderConfig(config);
3931
+ authStatus.textContent = "\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65\u3002";
3932
+ } catch (error) {
3933
+ authStatus.textContent = error.message;
3934
+ } finally {
3935
+ setBusy(button, false);
3936
+ }
3937
+ return;
3938
+ }
3865
3939
 
3866
3940
  setBusy(button, true);
3867
3941
  authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
@@ -4285,7 +4359,12 @@ function renderAdminPage() {
4285
4359
  authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
4286
4360
  refreshConfig({
4287
4361
  syncRuntime: true,
4288
- }).then(function () {
4362
+ }).then(function (config) {
4363
+ if (config && config.quotaSync) {
4364
+ const sync = config.quotaSync;
4365
+ 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";
4366
+ return;
4367
+ }
4289
4368
  authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0\u3002";
4290
4369
  }).catch(function (error) {
4291
4370
  authStatus.textContent = error && error.message ? error.message : String(error);
@@ -1,11 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
3
6
  import Fastify from "fastify";
4
7
  import cors from "@fastify/cors";
5
8
  import { z } from "zod";
6
9
  import { createGatewayContext } from "../core/context.js";
7
10
  import { requestText } from "../core/providers/http-client.js";
8
11
  import { renderAdminPage } from "./admin-page.js";
12
+ const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
13
+ const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
14
+ const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
15
+ const assetContentTypes = {
16
+ ".css": "text/css; charset=utf-8",
17
+ ".gif": "image/gif",
18
+ ".html": "text/html; charset=utf-8",
19
+ ".ico": "image/x-icon",
20
+ ".jpg": "image/jpeg",
21
+ ".jpeg": "image/jpeg",
22
+ ".js": "text/javascript; charset=utf-8",
23
+ ".json": "application/json; charset=utf-8",
24
+ ".map": "application/json; charset=utf-8",
25
+ ".png": "image/png",
26
+ ".svg": "image/svg+xml",
27
+ ".webp": "image/webp"
28
+ };
29
+ async function pathExists(targetPath) {
30
+ try {
31
+ await fs.access(targetPath);
32
+ return true;
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+ function getContentType(filePath) {
38
+ return assetContentTypes[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
39
+ }
40
+ async function readAdminUiAsset(assetPath) {
41
+ const normalized = path.normalize(assetPath).replace(/^(\.\.(\/|\\|$))+/, "");
42
+ const filePath = path.resolve(adminUiDistDir, normalized);
43
+ const root = path.resolve(adminUiDistDir);
44
+ if (!filePath.startsWith(`${root}${path.sep}`)) {
45
+ return null;
46
+ }
47
+ try {
48
+ return {
49
+ body: await fs.readFile(filePath),
50
+ filePath
51
+ };
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
9
56
  const inputPartSchema = z.object({
10
57
  type: z.string().optional(),
11
58
  text: z.string().optional()
@@ -383,6 +430,7 @@ function serializeProfile(profile) {
383
430
  accountId: profile.accountId,
384
431
  email: profile.email,
385
432
  quota: profile.quota,
433
+ authStatus: profile.authStatus,
386
434
  expiresAt: profile.expires,
387
435
  accessTokenPreview: maskSecret(profile.access),
388
436
  refreshTokenPreview: maskSecret(profile.refresh)
@@ -395,6 +443,7 @@ function serializeManagedProfile(profile) {
395
443
  accountId: profile.accountId,
396
444
  email: profile.email,
397
445
  quota: profile.quota,
446
+ authStatus: profile.authStatus,
398
447
  expiresAt: profile.expiresAt,
399
448
  accessTokenPreview: profile.accessTokenPreview,
400
449
  refreshTokenPreview: profile.refreshTokenPreview,
@@ -416,6 +465,10 @@ function getErrorStatusCode(error) {
416
465
  if (typeof normalized.statusCode === "number") {
417
466
  return normalized.statusCode;
418
467
  }
468
+ const upstreamStatus = normalized.upstreamStatus;
469
+ if (upstreamStatus === 401 || upstreamStatus === 403) {
470
+ return upstreamStatus;
471
+ }
419
472
  const message = normalized.message;
420
473
  if (message.includes("\u7F3A\u5C11") || message.includes("\u683C\u5F0F\u9519\u8BEF") || message.includes("\u672A\u5185\u7F6E\u6A21\u578B") || message.includes("\u4E0D\u652F\u6301") || message.includes("\u6CA1\u6709\u63D0\u4F9B")) {
421
474
  return 400;
@@ -506,9 +559,28 @@ function createApp(params) {
506
559
  };
507
560
  }
508
561
  app.get("/", async (_request, reply) => {
562
+ if (await pathExists(adminUiIndexPath)) {
563
+ reply.header("Content-Type", "text/html; charset=utf-8");
564
+ return fs.readFile(adminUiIndexPath, "utf8");
565
+ }
509
566
  reply.header("Content-Type", "text/html; charset=utf-8");
510
567
  return renderAdminPage();
511
568
  });
569
+ app.get("/assets/*", async (request, reply) => {
570
+ const assetPath = request.params["*"];
571
+ const asset = await readAdminUiAsset(path.join("assets", assetPath));
572
+ if (!asset) {
573
+ reply.code(404);
574
+ return {
575
+ error: {
576
+ type: "not_found",
577
+ message: "asset not found"
578
+ }
579
+ };
580
+ }
581
+ reply.header("Content-Type", getContentType(asset.filePath));
582
+ return asset.body;
583
+ });
512
584
  app.get("/favicon.ico", async (_request, reply) => {
513
585
  reply.code(204);
514
586
  return "";
@@ -527,15 +599,18 @@ function createApp(params) {
527
599
  };
528
600
  });
529
601
  app.post("/_gateway/admin/runtime-refresh", async (request) => {
530
- await Promise.all([
531
- ctx.authService.syncActiveProfileQuota("openai-codex", {
602
+ const [quotaSync] = await Promise.all([
603
+ ctx.authService.syncAllProfileQuotas("openai-codex", {
532
604
  suppressErrors: true
533
605
  }),
534
606
  ctx.versionService.getVersionStatus({
535
607
  force: true
536
608
  })
537
609
  ]);
538
- return buildAdminConfig(request);
610
+ return {
611
+ ...await buildAdminConfig(request),
612
+ quotaSync
613
+ };
539
614
  });
540
615
  app.get("/_gateway/admin/config", async (request) => buildAdminConfig(request));
541
616
  app.post("/_gateway/admin/login", async (request) => {
@@ -567,8 +642,22 @@ function createApp(params) {
567
642
  });
568
643
  return buildAdminConfig(request);
569
644
  });
570
- app.post("/_gateway/admin/profiles/sync-quota", async (request) => {
571
- await ctx.authService.syncActiveProfileQuota("openai-codex");
645
+ app.post("/_gateway/admin/profiles/sync-quota", async (request, reply) => {
646
+ const parsed = profileActionSchema.partial().safeParse(request.body ?? {});
647
+ if (!parsed.success) {
648
+ reply.code(400);
649
+ return {
650
+ error: {
651
+ type: "validation_error",
652
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
653
+ }
654
+ };
655
+ }
656
+ if (parsed.data.profileId) {
657
+ await ctx.authService.syncProfileQuota(parsed.data.profileId, "openai-codex");
658
+ } else {
659
+ await ctx.authService.syncActiveProfileQuota("openai-codex");
660
+ }
572
661
  return buildAdminConfig(request);
573
662
  });
574
663
  app.post("/_gateway/admin/profiles/remove", async (request, reply) => {
@@ -0,0 +1,64 @@
1
+ # AI Zero Token Desktop Release
2
+
3
+ This project ships the desktop app with Electron. The desktop main process starts the existing local Fastify gateway and loads the React management UI served by that gateway.
4
+
5
+ ## Build Commands
6
+
7
+ ```bash
8
+ npm run build
9
+ ```
10
+
11
+ Builds:
12
+
13
+ - `admin-ui/dist`: React management UI
14
+ - `dist`: TypeScript gateway, CLI, and Electron main process
15
+
16
+ ```bash
17
+ npm run desktop
18
+ ```
19
+
20
+ Runs the desktop app locally.
21
+
22
+ ```bash
23
+ npm run dist:dir
24
+ ```
25
+
26
+ Creates an unpacked desktop app for the current platform in `release/`.
27
+
28
+ ```bash
29
+ npm run dist:mac
30
+ npm run dist:win
31
+ ```
32
+
33
+ Creates macOS and Windows distributables. macOS builds should be produced on macOS. Windows builds are best produced on Windows CI or a runner with a complete Windows packaging environment.
34
+
35
+ ## Signing
36
+
37
+ Unsigned builds are suitable for internal testing only. Public commercial distribution should use platform signing:
38
+
39
+ - macOS: Apple Developer ID Application certificate and notarization.
40
+ - Windows: Authenticode code-signing certificate.
41
+
42
+ `electron-builder` reads the standard signing environment variables. Configure these in CI instead of committing credentials to the repository.
43
+
44
+ ## Release Artifacts
45
+
46
+ The packaged output is written to:
47
+
48
+ ```text
49
+ release/
50
+ ```
51
+
52
+ The folder is intentionally ignored by git.
53
+
54
+ ## App Resources
55
+
56
+ App icon files live in:
57
+
58
+ ```text
59
+ build/icon.png
60
+ build/icon.icns
61
+ build/icon.ico
62
+ ```
63
+
64
+ They are included in Electron packaging and npm packing.