ai-zero-token 1.0.8 → 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.
@@ -1,10 +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";
10
+ import { requestText } from "../core/providers/http-client.js";
7
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
+ }
8
56
  const inputPartSchema = z.object({
9
57
  type: z.string().optional(),
10
58
  text: z.string().optional()
@@ -72,8 +120,18 @@ const settingsUpdateSchema = z.object({
72
120
  enabled: z.boolean(),
73
121
  url: z.string().optional(),
74
122
  noProxy: z.string().optional()
123
+ }).optional(),
124
+ autoSwitch: z.object({
125
+ enabled: z.boolean()
75
126
  }).optional()
76
127
  });
128
+ const proxyTestSchema = z.object({
129
+ networkProxy: z.object({
130
+ enabled: z.boolean(),
131
+ url: z.string().optional(),
132
+ noProxy: z.string().optional()
133
+ })
134
+ });
77
135
  const profileActionSchema = z.object({
78
136
  profileId: z.string().min(1)
79
137
  });
@@ -372,6 +430,7 @@ function serializeProfile(profile) {
372
430
  accountId: profile.accountId,
373
431
  email: profile.email,
374
432
  quota: profile.quota,
433
+ authStatus: profile.authStatus,
375
434
  expiresAt: profile.expires,
376
435
  accessTokenPreview: maskSecret(profile.access),
377
436
  refreshTokenPreview: maskSecret(profile.refresh)
@@ -384,6 +443,7 @@ function serializeManagedProfile(profile) {
384
443
  accountId: profile.accountId,
385
444
  email: profile.email,
386
445
  quota: profile.quota,
446
+ authStatus: profile.authStatus,
387
447
  expiresAt: profile.expiresAt,
388
448
  accessTokenPreview: profile.accessTokenPreview,
389
449
  refreshTokenPreview: profile.refreshTokenPreview,
@@ -405,6 +465,10 @@ function getErrorStatusCode(error) {
405
465
  if (typeof normalized.statusCode === "number") {
406
466
  return normalized.statusCode;
407
467
  }
468
+ const upstreamStatus = normalized.upstreamStatus;
469
+ if (upstreamStatus === 401 || upstreamStatus === 403) {
470
+ return upstreamStatus;
471
+ }
408
472
  const message = normalized.message;
409
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")) {
410
474
  return 400;
@@ -495,9 +559,28 @@ function createApp(params) {
495
559
  };
496
560
  }
497
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
+ }
498
566
  reply.header("Content-Type", "text/html; charset=utf-8");
499
567
  return renderAdminPage();
500
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
+ });
501
584
  app.get("/favicon.ico", async (_request, reply) => {
502
585
  reply.code(204);
503
586
  return "";
@@ -516,15 +599,18 @@ function createApp(params) {
516
599
  };
517
600
  });
518
601
  app.post("/_gateway/admin/runtime-refresh", async (request) => {
519
- await Promise.all([
520
- ctx.authService.syncActiveProfileQuota("openai-codex", {
602
+ const [quotaSync] = await Promise.all([
603
+ ctx.authService.syncAllProfileQuotas("openai-codex", {
521
604
  suppressErrors: true
522
605
  }),
523
606
  ctx.versionService.getVersionStatus({
524
607
  force: true
525
608
  })
526
609
  ]);
527
- return buildAdminConfig(request);
610
+ return {
611
+ ...await buildAdminConfig(request),
612
+ quotaSync
613
+ };
528
614
  });
529
615
  app.get("/_gateway/admin/config", async (request) => buildAdminConfig(request));
530
616
  app.post("/_gateway/admin/login", async (request) => {
@@ -551,12 +637,27 @@ function createApp(params) {
551
637
  }
552
638
  await ctx.authService.activateProfile(parsed.data.profileId);
553
639
  await ctx.authService.syncActiveProfileQuota("openai-codex", {
554
- suppressErrors: true
640
+ suppressErrors: true,
641
+ skipAutoSwitch: true
555
642
  });
556
643
  return buildAdminConfig(request);
557
644
  });
558
- app.post("/_gateway/admin/profiles/sync-quota", async (request) => {
559
- 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
+ }
560
661
  return buildAdminConfig(request);
561
662
  });
562
663
  app.post("/_gateway/admin/profiles/remove", async (request, reply) => {
@@ -649,8 +750,61 @@ function createApp(params) {
649
750
  if (parsed.data.networkProxy) {
650
751
  await ctx.configService.setNetworkProxy(parsed.data.networkProxy);
651
752
  }
753
+ if (parsed.data.autoSwitch) {
754
+ await ctx.configService.setAutoSwitch(parsed.data.autoSwitch);
755
+ }
652
756
  return buildAdminConfig(request);
653
757
  });
758
+ app.post("/_gateway/admin/settings/proxy-test", async (request, reply) => {
759
+ const parsed = proxyTestSchema.safeParse(request.body);
760
+ if (!parsed.success) {
761
+ reply.code(400);
762
+ return {
763
+ error: {
764
+ type: "validation_error",
765
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
766
+ }
767
+ };
768
+ }
769
+ const proxy = {
770
+ enabled: parsed.data.networkProxy.enabled,
771
+ url: parsed.data.networkProxy.url?.trim() ?? "",
772
+ noProxy: parsed.data.networkProxy.noProxy?.trim() || "localhost,127.0.0.1,::1"
773
+ };
774
+ if (proxy.enabled && !proxy.url) {
775
+ reply.code(400);
776
+ return {
777
+ error: {
778
+ type: "validation_error",
779
+ message: "\u542F\u7528\u4EE3\u7406\u65F6\u5FC5\u987B\u586B\u5199\u4EE3\u7406\u5730\u5740\u3002"
780
+ }
781
+ };
782
+ }
783
+ const startedAt = performance.now();
784
+ try {
785
+ const response = await requestText({
786
+ method: "GET",
787
+ url: "https://chatgpt.com/",
788
+ timeoutMs: 8e3,
789
+ proxyOverride: proxy
790
+ });
791
+ return {
792
+ ok: response.status >= 200 && response.status < 500,
793
+ status: response.status,
794
+ elapsedMs: Math.round(performance.now() - startedAt),
795
+ target: "https://chatgpt.com/",
796
+ transport: response.transport
797
+ };
798
+ } catch (error) {
799
+ reply.code(502);
800
+ return {
801
+ error: {
802
+ type: "proxy_test_failed",
803
+ message: error instanceof Error ? error.message : String(error)
804
+ }
805
+ };
806
+ }
807
+ });
654
808
  app.get("/v1/models", async () => ({
655
809
  object: "list",
656
810
  data: (await ctx.modelService.listModels()).map((model) => ({
@@ -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.