ai-zero-token 2.0.1 → 2.0.2

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/admin-ui/dist/assets/InfoRow-0ULI9iI3.js +1 -0
  3. package/admin-ui/dist/assets/accounts-Ddq82u6R.js +1 -0
  4. package/admin-ui/dist/assets/activity-D21-Xrc4.js +1 -0
  5. package/admin-ui/dist/assets/app-mark-nsRs4vo7.svg +8 -0
  6. package/admin-ui/dist/assets/circle-check-ZYtn9GqY.js +1 -0
  7. package/admin-ui/dist/assets/clock-3-BzDANsVk.js +1 -0
  8. package/admin-ui/dist/assets/docs-BRNWAMPw.css +1 -0
  9. package/admin-ui/dist/assets/docs-CihX3Xsm.js +1735 -0
  10. package/admin-ui/dist/assets/earth-DFdZaQIi.js +1 -0
  11. package/admin-ui/dist/assets/image-bed-BGjlDLks.js +1 -0
  12. package/admin-ui/dist/assets/index-CX8e0-BB.js +10 -0
  13. package/admin-ui/dist/assets/index-ywn2Jwpu.css +1 -0
  14. package/admin-ui/dist/assets/jsx-runtime-DqpGtLhh.js +1 -0
  15. package/admin-ui/dist/assets/launch-BWw7Odq7.js +1 -0
  16. package/admin-ui/dist/assets/logs-DDdgDVwo.js +1 -0
  17. package/admin-ui/dist/assets/network-detect-cUdjg4zk.js +1 -0
  18. package/admin-ui/dist/assets/overview-CsjVVcvi.js +1 -0
  19. package/admin-ui/dist/assets/profiles-DMOjJORP.js +1 -0
  20. package/admin-ui/dist/assets/refresh-cw-CAAH2rqe.js +1 -0
  21. package/admin-ui/dist/assets/search-B2hz41D3.js +1 -0
  22. package/admin-ui/dist/assets/server-BrjJPb9D.js +1 -0
  23. package/admin-ui/dist/assets/settings-Be99HpDD.js +1 -0
  24. package/admin-ui/dist/assets/tester-BIvH_8DY.js +3 -0
  25. package/admin-ui/dist/assets/upload-CwXb7Q1b.js +1 -0
  26. package/admin-ui/dist/assets/zap-B4_oDbCp.js +1 -0
  27. package/admin-ui/dist/index.html +5 -3
  28. package/build/icon.icns +0 -0
  29. package/build/icon.ico +0 -0
  30. package/build/icon.png +0 -0
  31. package/build/icon.svg +8 -0
  32. package/dist/core/context.js +3 -0
  33. package/dist/core/providers/http-client.js +11 -4
  34. package/dist/core/services/github-image-bed-service.js +264 -0
  35. package/dist/core/services/network-detect-service.js +189 -74
  36. package/dist/core/store/github-image-bed-history-store.js +68 -0
  37. package/dist/core/store/github-image-bed-store.js +52 -0
  38. package/dist/desktop/main.js +158 -6
  39. package/dist/server/app.js +108 -13
  40. package/dist/server/index.js +41 -15
  41. package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +2 -2
  42. package/package.json +5 -2
  43. package/admin-ui/dist/assets/app-mark-Gd2QnHMO.svg +0 -9
  44. package/admin-ui/dist/assets/index-DNzR8XR7.css +0 -1
  45. package/admin-ui/dist/assets/index-DZMegNPs.js +0 -1745
  46. package/dist/server/admin-page.js +0 -4586
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { ensureStateMigrated, getStateDir } from "./state-paths.js";
5
+ function createEmptyStore() {
6
+ return {
7
+ version: 1,
8
+ github: {
9
+ token: ""
10
+ }
11
+ };
12
+ }
13
+ async function loadGithubImageBedStore() {
14
+ try {
15
+ await ensureStateMigrated();
16
+ const raw = await fs.readFile(getGithubImageBedStorePath(), "utf8");
17
+ const parsed = JSON.parse(raw);
18
+ return {
19
+ version: 1,
20
+ github: {
21
+ token: typeof parsed.github?.token === "string" ? parsed.github.token.trim() : ""
22
+ }
23
+ };
24
+ } catch {
25
+ return createEmptyStore();
26
+ }
27
+ }
28
+ async function saveGithubImageBedStore(store) {
29
+ await ensureStateMigrated();
30
+ await fs.mkdir(getStateDir(), { recursive: true });
31
+ await fs.writeFile(getGithubImageBedStorePath(), `${JSON.stringify(store, null, 2)}
32
+ `, "utf8");
33
+ }
34
+ async function updateGithubImageBedToken(token) {
35
+ const store = await loadGithubImageBedStore();
36
+ store.github.token = token.trim();
37
+ await saveGithubImageBedStore(store);
38
+ return store;
39
+ }
40
+ async function clearGithubImageBedStore() {
41
+ await saveGithubImageBedStore(createEmptyStore());
42
+ }
43
+ function getGithubImageBedStorePath() {
44
+ return path.join(getStateDir(), "github-image-bed.json");
45
+ }
46
+ export {
47
+ clearGithubImageBedStore,
48
+ getGithubImageBedStorePath,
49
+ loadGithubImageBedStore,
50
+ saveGithubImageBedStore,
51
+ updateGithubImageBedToken
52
+ };
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { app as electronApp, BrowserWindow, dialog, shell } from "electron";
3
+ import { readFileSync } from "node:fs";
3
4
  import path from "node:path";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { startServer } from "../server/index.js";
6
7
  let gatewayServer = null;
7
8
  let mainWindow = null;
8
9
  let isQuitting = false;
10
+ let isRestarting = false;
11
+ let currentGatewayUrl = null;
12
+ let currentAdminUrl = null;
9
13
  const desktopDir = path.dirname(fileURLToPath(import.meta.url));
10
14
  const appIconPath = path.resolve(desktopDir, "../../build/icon.png");
15
+ const startupPageUrl = buildStartupPageUrl("\u6B63\u5728\u542F\u52A8\u672C\u5730\u7F51\u5173");
11
16
  electronApp.setName("AI Zero Token");
12
17
  function createBrowserUrl(host, port) {
13
18
  if (host === "0.0.0.0" || host === "::") {
@@ -28,11 +33,143 @@ function resolveAdminUrl(gatewayUrl) {
28
33
  const devUrl = process.env.AZT_ADMIN_UI_DEV_URL?.trim();
29
34
  return devUrl || gatewayUrl;
30
35
  }
36
+ function resolvePreferredGatewayParams() {
37
+ const devUrl = process.env.AZT_DEV_GATEWAY_URL?.trim();
38
+ if (!devUrl) {
39
+ return void 0;
40
+ }
41
+ try {
42
+ const parsed = new URL(devUrl);
43
+ const host = parsed.hostname || void 0;
44
+ const port = Number.parseInt(parsed.port, 10);
45
+ return {
46
+ host,
47
+ port: Number.isFinite(port) ? port : void 0
48
+ };
49
+ } catch {
50
+ return void 0;
51
+ }
52
+ }
53
+ function isAllowedAppUrl(targetUrl) {
54
+ return Boolean(currentAdminUrl && isGatewayUrl(targetUrl, currentAdminUrl) || currentGatewayUrl && isGatewayUrl(targetUrl, currentGatewayUrl));
55
+ }
56
+ async function restartGateway() {
57
+ if (isRestarting) {
58
+ return;
59
+ }
60
+ isRestarting = true;
61
+ try {
62
+ if (mainWindow && !mainWindow.isDestroyed()) {
63
+ await mainWindow.loadURL(buildStartupPageUrl("\u6B63\u5728\u91CD\u542F\u672C\u5730\u7F51\u5173"));
64
+ }
65
+ await closeGatewayServer().catch((error) => {
66
+ console.error("[desktop:gateway:restart-close]", error);
67
+ });
68
+ const server = await ensureGatewayServer();
69
+ const gatewayUrl = createBrowserUrl(server.host, server.port);
70
+ const adminUrl = resolveAdminUrl(gatewayUrl);
71
+ currentGatewayUrl = gatewayUrl;
72
+ currentAdminUrl = adminUrl;
73
+ if (mainWindow && !mainWindow.isDestroyed()) {
74
+ await mainWindow.loadURL(adminUrl);
75
+ }
76
+ } catch (error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ console.error("[desktop:gateway:restart]", error);
79
+ if (mainWindow && !mainWindow.isDestroyed()) {
80
+ await mainWindow.loadURL(buildStartupPageUrl(`\u7F51\u5173\u91CD\u542F\u5931\u8D25\uFF1A${message}`)).catch(() => void 0);
81
+ }
82
+ dialog.showErrorBox("AI Zero Token \u7F51\u5173\u91CD\u542F\u5931\u8D25", message);
83
+ } finally {
84
+ isRestarting = false;
85
+ }
86
+ }
87
+ function buildStartupPageUrl(subtitle) {
88
+ const iconUrl = `data:image/png;base64,${readFileSync(appIconPath).toString("base64")}`;
89
+ const html = `<!doctype html>
90
+ <html lang="zh-CN">
91
+ <head>
92
+ <meta charset="UTF-8" />
93
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
94
+ <style>
95
+ :root { color-scheme: dark; }
96
+ html, body {
97
+ width: 100%;
98
+ height: 100%;
99
+ margin: 0;
100
+ background: #050816;
101
+ color: #f8fafc;
102
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
103
+ }
104
+ body {
105
+ display: grid;
106
+ place-items: center;
107
+ }
108
+ .wrap {
109
+ display: grid;
110
+ gap: 18px;
111
+ justify-items: center;
112
+ }
113
+ .mark {
114
+ width: 96px;
115
+ height: 96px;
116
+ border-radius: 24px;
117
+ box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
118
+ }
119
+ .title {
120
+ font-size: 22px;
121
+ font-weight: 700;
122
+ letter-spacing: 0;
123
+ }
124
+ .sub {
125
+ font-size: 14px;
126
+ color: rgba(248, 250, 252, 0.66);
127
+ }
128
+ .bar {
129
+ width: 220px;
130
+ height: 4px;
131
+ border-radius: 999px;
132
+ background: rgba(255,255,255,0.08);
133
+ overflow: hidden;
134
+ }
135
+ .bar::after {
136
+ content: "";
137
+ display: block;
138
+ width: 45%;
139
+ height: 100%;
140
+ border-radius: inherit;
141
+ background: linear-gradient(90deg, #93c5fd 0%, #66f0c0 55%, #f97316 100%);
142
+ animation: load 1.2s ease-in-out infinite;
143
+ }
144
+ @keyframes load {
145
+ 0% { transform: translateX(-30%); }
146
+ 50% { transform: translateX(80%); }
147
+ 100% { transform: translateX(-30%); }
148
+ }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <div class="wrap">
153
+ <img class="mark" src="${iconUrl}" alt="" />
154
+ <div class="title">AI Zero Token</div>
155
+ <div class="sub">${escapeHtml(subtitle)}</div>
156
+ <div class="bar" aria-hidden="true"></div>
157
+ </div>
158
+ </body>
159
+ </html>`;
160
+ return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`;
161
+ }
162
+ function escapeHtml(value) {
163
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
164
+ }
31
165
  async function ensureGatewayServer() {
32
166
  if (gatewayServer) {
33
167
  return gatewayServer;
34
168
  }
35
- gatewayServer = await startServer();
169
+ gatewayServer = await startServer({
170
+ ...resolvePreferredGatewayParams(),
171
+ onRestart: restartGateway
172
+ });
36
173
  const adminUrl = createBrowserUrl(gatewayServer.host, gatewayServer.port);
37
174
  console.log("AI Zero Token desktop gateway started.");
38
175
  console.log(`admin: ${adminUrl}`);
@@ -41,9 +178,6 @@ async function ensureGatewayServer() {
41
178
  return gatewayServer;
42
179
  }
43
180
  async function createMainWindow() {
44
- const server = await ensureGatewayServer();
45
- const gatewayUrl = createBrowserUrl(server.host, server.port);
46
- const adminUrl = resolveAdminUrl(gatewayUrl);
47
181
  mainWindow = new BrowserWindow({
48
182
  width: 1440,
49
183
  height: 960,
@@ -51,7 +185,8 @@ async function createMainWindow() {
51
185
  minHeight: 720,
52
186
  title: "AI Zero Token",
53
187
  icon: appIconPath,
54
- backgroundColor: "#f8fafc",
188
+ backgroundColor: "#050816",
189
+ show: false,
55
190
  webPreferences: {
56
191
  contextIsolation: true,
57
192
  nodeIntegration: false,
@@ -63,7 +198,7 @@ async function createMainWindow() {
63
198
  return { action: "deny" };
64
199
  });
65
200
  mainWindow.webContents.on("will-navigate", (event, url) => {
66
- if (isGatewayUrl(url, adminUrl) || isGatewayUrl(url, gatewayUrl)) {
201
+ if (isAllowedAppUrl(url)) {
67
202
  return;
68
203
  }
69
204
  event.preventDefault();
@@ -72,6 +207,23 @@ async function createMainWindow() {
72
207
  mainWindow.on("closed", () => {
73
208
  mainWindow = null;
74
209
  });
210
+ mainWindow.once("ready-to-show", () => {
211
+ if (mainWindow) {
212
+ mainWindow.show();
213
+ }
214
+ });
215
+ await mainWindow.loadURL(startupPageUrl);
216
+ if (mainWindow && !mainWindow.isVisible()) {
217
+ mainWindow.show();
218
+ }
219
+ const server = await ensureGatewayServer();
220
+ if (!mainWindow) {
221
+ return;
222
+ }
223
+ const gatewayUrl = createBrowserUrl(server.host, server.port);
224
+ const adminUrl = resolveAdminUrl(gatewayUrl);
225
+ currentGatewayUrl = gatewayUrl;
226
+ currentAdminUrl = adminUrl;
75
227
  await mainWindow.loadURL(adminUrl);
76
228
  }
77
229
  function focusMainWindow() {
@@ -8,7 +8,6 @@ import cors from "@fastify/cors";
8
8
  import { z } from "zod";
9
9
  import { createGatewayContext } from "../core/context.js";
10
10
  import { requestText } from "../core/providers/http-client.js";
11
- import { renderAdminPage } from "./admin-page.js";
12
11
  const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
13
12
  const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
14
13
  const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
@@ -26,14 +25,6 @@ const assetContentTypes = {
26
25
  ".svg": "image/svg+xml",
27
26
  ".webp": "image/webp"
28
27
  };
29
- async function pathExists(targetPath) {
30
- try {
31
- await fs.access(targetPath);
32
- return true;
33
- } catch {
34
- return false;
35
- }
36
- }
37
28
  function getContentType(filePath) {
38
29
  return assetContentTypes[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
39
30
  }
@@ -123,6 +114,9 @@ const settingsUpdateSchema = z.object({
123
114
  }).optional(),
124
115
  autoSwitch: z.object({
125
116
  enabled: z.boolean()
117
+ }).optional(),
118
+ server: z.object({
119
+ port: z.number().int().min(1).max(65535)
126
120
  }).optional()
127
121
  });
128
122
  const proxyTestSchema = z.object({
@@ -146,6 +140,19 @@ const profileExportSchema = z.object({
146
140
  const codexApplySchema = z.object({
147
141
  profileId: z.string().min(1)
148
142
  });
143
+ const githubImageBedConfigSchema = z.object({
144
+ token: z.string().min(1)
145
+ });
146
+ const githubImageBedUploadSchema = z.object({
147
+ filename: z.string().min(1),
148
+ dataUrl: z.string().min(1)
149
+ });
150
+ const githubImageBedHistoryQuerySchema = z.object({
151
+ limit: z.coerce.number().int().min(1).max(100).optional()
152
+ });
153
+ const githubImageBedHistoryParamsSchema = z.object({
154
+ id: z.string().min(1)
155
+ });
149
156
  const imageGenerationsBodySchema = z.object({
150
157
  prompt: z.string().min(1),
151
158
  model: z.string().optional(),
@@ -486,7 +493,7 @@ function createApp(params) {
486
493
  const ctx = createGatewayContext();
487
494
  void app.register(cors, {
488
495
  origin: params?.corsOrigin ?? true,
489
- methods: ["GET", "POST", "PUT", "OPTIONS"]
496
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
490
497
  });
491
498
  app.setErrorHandler((error, request, reply) => {
492
499
  const normalized = normalizeError(error);
@@ -529,6 +536,7 @@ function createApp(params) {
529
536
  codex: codexStatus,
530
537
  adminUrl: `${origin}/`,
531
538
  baseUrl: `${origin}/v1`,
539
+ restartSupported: Boolean(params?.onRestart),
532
540
  supportedEndpoints: [
533
541
  {
534
542
  method: "GET",
@@ -559,12 +567,18 @@ function createApp(params) {
559
567
  };
560
568
  }
561
569
  app.get("/", async (_request, reply) => {
562
- if (await pathExists(adminUiIndexPath)) {
570
+ try {
563
571
  reply.header("Content-Type", "text/html; charset=utf-8");
564
572
  return fs.readFile(adminUiIndexPath, "utf8");
573
+ } catch {
574
+ reply.code(503);
575
+ return {
576
+ error: {
577
+ type: "admin_ui_missing",
578
+ message: "React \u7BA1\u7406\u9875\u672A\u6784\u5EFA\uFF0C\u8BF7\u5148\u8FD0\u884C npm run build:ui\u3002"
579
+ }
580
+ };
565
581
  }
566
- reply.header("Content-Type", "text/html; charset=utf-8");
567
- return renderAdminPage();
568
582
  });
569
583
  app.get("/assets/*", async (request, reply) => {
570
584
  const assetPath = request.params["*"];
@@ -753,8 +767,31 @@ function createApp(params) {
753
767
  if (parsed.data.autoSwitch) {
754
768
  await ctx.configService.setAutoSwitch(parsed.data.autoSwitch);
755
769
  }
770
+ if (parsed.data.server) {
771
+ await ctx.configService.setServerConfig({ port: parsed.data.server.port });
772
+ }
756
773
  return buildAdminConfig(request);
757
774
  });
775
+ app.post("/_gateway/admin/restart", async (_request, reply) => {
776
+ if (!params?.onRestart) {
777
+ reply.code(501);
778
+ return {
779
+ error: {
780
+ type: "not_supported",
781
+ message: "\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u91CD\u542F\u3002"
782
+ }
783
+ };
784
+ }
785
+ setTimeout(() => {
786
+ void Promise.resolve(params.onRestart?.()).catch((error) => {
787
+ console.error("[gateway:restart]", error);
788
+ });
789
+ }, 100);
790
+ return {
791
+ ok: true,
792
+ restarting: true
793
+ };
794
+ });
758
795
  app.post("/_gateway/admin/settings/proxy-test", async (request, reply) => {
759
796
  const parsed = proxyTestSchema.safeParse(request.body);
760
797
  if (!parsed.success) {
@@ -809,6 +846,64 @@ function createApp(params) {
809
846
  const settings = await ctx.configService.getSettings();
810
847
  return ctx.networkDetectService.collectReport(settings.networkProxy);
811
848
  });
849
+ app.get("/_gateway/image-bed/config", async () => ctx.githubImageBedService.getConfig());
850
+ app.post("/_gateway/image-bed/validate", async () => ctx.githubImageBedService.testConnection());
851
+ app.get("/_gateway/image-bed/history", async (request, reply) => {
852
+ const parsed = githubImageBedHistoryQuerySchema.safeParse(request.query ?? {});
853
+ if (!parsed.success) {
854
+ reply.code(400);
855
+ return {
856
+ error: {
857
+ type: "validation_error",
858
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u53C2\u6570\u683C\u5F0F\u9519\u8BEF"
859
+ }
860
+ };
861
+ }
862
+ return ctx.githubImageBedService.listHistory(parsed.data.limit ?? 50);
863
+ });
864
+ app.put("/_gateway/image-bed/config", async (request, reply) => {
865
+ const parsed = githubImageBedConfigSchema.safeParse(request.body);
866
+ if (!parsed.success) {
867
+ reply.code(400);
868
+ return {
869
+ error: {
870
+ type: "validation_error",
871
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
872
+ }
873
+ };
874
+ }
875
+ return ctx.githubImageBedService.saveToken(parsed.data.token);
876
+ });
877
+ app.delete("/_gateway/image-bed/config", async () => ctx.githubImageBedService.clearToken());
878
+ app.post("/_gateway/image-bed/upload", async (request, reply) => {
879
+ const parsed = githubImageBedUploadSchema.safeParse(request.body);
880
+ if (!parsed.success) {
881
+ reply.code(400);
882
+ return {
883
+ error: {
884
+ type: "validation_error",
885
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
886
+ }
887
+ };
888
+ }
889
+ const uploaded = await ctx.githubImageBedService.uploadImage(parsed.data);
890
+ await ctx.githubImageBedService.rememberUpload(uploaded);
891
+ return uploaded;
892
+ });
893
+ app.delete("/_gateway/image-bed/history/:id", async (request, reply) => {
894
+ const parsed = githubImageBedHistoryParamsSchema.safeParse(request.params);
895
+ if (!parsed.success) {
896
+ reply.code(400);
897
+ return {
898
+ error: {
899
+ type: "validation_error",
900
+ message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u53C2\u6570\u683C\u5F0F\u9519\u8BEF"
901
+ }
902
+ };
903
+ }
904
+ return ctx.githubImageBedService.deleteHistoryItem(parsed.data.id);
905
+ });
906
+ app.delete("/_gateway/image-bed/history", async () => ctx.githubImageBedService.clearHistory());
812
907
  app.get("/v1/models", async () => ({
813
908
  object: "list",
814
909
  data: (await ctx.modelService.listModels()).map((model) => ({
@@ -21,27 +21,53 @@ function resolveBodyLimitBytes() {
21
21
  }
22
22
  return Math.floor(value * 1024 * 1024);
23
23
  }
24
+ function isPortInUseError(error) {
25
+ const normalized = error;
26
+ if (normalized.code === "EADDRINUSE") {
27
+ return true;
28
+ }
29
+ return typeof normalized.message === "string" && normalized.message.includes("EADDRINUSE");
30
+ }
24
31
  async function startServer(params) {
25
32
  const bodyLimit = resolveBodyLimitBytes();
26
- const app = createApp({
27
- corsOrigin: resolveCorsOrigin(),
28
- bodyLimit
29
- });
30
33
  const configService = new ConfigService();
31
34
  const defaults = await configService.getServerConfig();
32
35
  const host = params?.host ?? defaults.host;
33
36
  const port = params?.port ?? defaults.port;
34
- await app.listen({
35
- host,
36
- port
37
- });
38
- return {
39
- app,
40
- host,
41
- port,
42
- corsOrigin: process.env.AZT_CORS_ORIGIN?.trim() || "*",
43
- bodyLimit
44
- };
37
+ const maxPortAttempts = 20;
38
+ let lastError;
39
+ for (let offset = 0; offset <= maxPortAttempts; offset += 1) {
40
+ const listenPort = port + offset;
41
+ const app = createApp({
42
+ corsOrigin: resolveCorsOrigin(),
43
+ bodyLimit,
44
+ onRestart: params?.onRestart
45
+ });
46
+ try {
47
+ await app.listen({
48
+ host,
49
+ port: listenPort
50
+ });
51
+ if (offset > 0) {
52
+ console.warn(`[server] port ${port} was busy, using ${listenPort} instead.`);
53
+ await configService.setServerConfig({ port: listenPort });
54
+ }
55
+ return {
56
+ app,
57
+ host,
58
+ port: listenPort,
59
+ corsOrigin: process.env.AZT_CORS_ORIGIN?.trim() || "*",
60
+ bodyLimit
61
+ };
62
+ } catch (error) {
63
+ await app.close().catch(() => void 0);
64
+ if (!isPortInUseError(error) || offset === maxPortAttempts) {
65
+ throw error;
66
+ }
67
+ lastError = error;
68
+ }
69
+ }
70
+ throw lastError ?? new Error("\u65E0\u6CD5\u542F\u52A8\u672C\u5730\u670D\u52A1\u3002");
45
71
  }
46
72
  export {
47
73
  startServer
@@ -38,7 +38,7 @@
38
38
 
39
39
  ### 2.2 第二阶段:桌面端 React 管理台
40
40
 
41
- 当前管理页是服务端字符串 HTML,适合验证功能,但不适合长期维护桌面端和工具箱。
41
+ 管理页已迁移为 React/Vite 应用,网关生产环境只托管 `admin-ui/dist` 静态构建产物。
42
42
 
43
43
  第二阶段将管理界面迁移为 React/Vite 应用,但迁移范围只限 UI 层。
44
44
 
@@ -49,7 +49,7 @@
49
49
  - 封装 `/_gateway/*` 管理接口客户端
50
50
  - 网关生产环境托管前端静态构建产物
51
51
  - 桌面端加载同一套管理台 UI
52
- - 移除或降级当前 `admin-page.ts` 的职责
52
+ - 删除服务端内嵌管理页,保留一套 React 管理台
53
53
 
54
54
  这一阶段结束后,项目形态应变为:
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-zero-token",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Local-first OpenAI-compatible AI CLI and gateway with Codex OAuth, multi-account management, and gpt-image-2 image generation/editing.",
5
5
  "license": "MIT",
6
6
  "author": "AI Zero Token Contributors",
@@ -50,7 +50,9 @@
50
50
  "desktop:dev": "node scripts/dev.mjs desktop",
51
51
  "dist": "npm run build && electron-builder",
52
52
  "dist:dir": "npm run build && electron-builder --dir",
53
- "dist:mac": "npm run build && electron-builder --mac",
53
+ "dist:mac": "npm run build && electron-builder --mac --universal",
54
+ "dist:mac:arm64": "npm run build && electron-builder --mac --arm64",
55
+ "dist:mac:x64": "npm run build && electron-builder --mac --x64",
54
56
  "dist:win": "npm run build && electron-builder --win",
55
57
  "typecheck": "npm run typecheck:server && npm run typecheck:ui",
56
58
  "typecheck:server": "bunx tsc -p tsconfig.json --noEmit",
@@ -119,6 +121,7 @@
119
121
  "files": [
120
122
  "dist",
121
123
  "admin-ui/dist",
124
+ "build/icon.svg",
122
125
  "build/icon.png",
123
126
  "build/icon.icns",
124
127
  "build/icon.ico",
@@ -1,9 +0,0 @@
1
- <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <rect x="10" y="10" width="108" height="108" rx="26" fill="#111827"/>
3
- <rect x="18" y="18" width="92" height="92" rx="20" fill="#FFFFFF" fill-opacity=".08"/>
4
- <path d="M39 36h50c6.627 0 12 5.373 12 12v32c0 6.627-5.373 12-12 12H39c-6.627 0-12-5.373-12-12V48c0-6.627 5.373-12 12-12Z" fill="#F8FAFC"/>
5
- <path d="M42 54h20M42 66h34M42 78h25" stroke="#111827" stroke-width="7" stroke-linecap="round"/>
6
- <path d="M83 53l8 11-8 11-8-11 8-11Z" fill="#2563EB"/>
7
- <path d="M71 36c3.2-9.2 11-14 23-14 0 12-4.9 19.9-14.7 23.7" stroke="#22C55E" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
8
- <path d="M48 92c-3.2 9.2-11 14-23 14 0-12 4.9-19.9 14.7-23.7" stroke="#F59E0B" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
9
- </svg>