ai-zero-token 2.0.1 → 2.0.3

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 (55) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +13 -5
  3. package/README.zh-CN.md +13 -5
  4. package/admin-ui/dist/assets/InfoRow-0ULI9iI3.js +1 -0
  5. package/admin-ui/dist/assets/accounts-DymL4WIa.js +2 -0
  6. package/admin-ui/dist/assets/activity-D21-Xrc4.js +1 -0
  7. package/admin-ui/dist/assets/app-mark-nsRs4vo7.svg +8 -0
  8. package/admin-ui/dist/assets/circle-check-ZYtn9GqY.js +1 -0
  9. package/admin-ui/dist/assets/clock-3-BzDANsVk.js +1 -0
  10. package/admin-ui/dist/assets/docs-BRNWAMPw.css +1 -0
  11. package/admin-ui/dist/assets/docs-DtO-AOWU.js +1735 -0
  12. package/admin-ui/dist/assets/earth-DFdZaQIi.js +1 -0
  13. package/admin-ui/dist/assets/image-bed-yIVQ4dKs.js +1 -0
  14. package/admin-ui/dist/assets/index-By4r-wy3.css +1 -0
  15. package/admin-ui/dist/assets/index-DRe-tByu.js +10 -0
  16. package/admin-ui/dist/assets/jsx-runtime-DqpGtLhh.js +1 -0
  17. package/admin-ui/dist/assets/launch-CQXYrl-h.js +1 -0
  18. package/admin-ui/dist/assets/logs-awABDg1C.js +1 -0
  19. package/admin-ui/dist/assets/network-detect-sSrnwZqf.js +1 -0
  20. package/admin-ui/dist/assets/overview-BbSON0Jl.js +1 -0
  21. package/admin-ui/dist/assets/profiles-DMOjJORP.js +1 -0
  22. package/admin-ui/dist/assets/refresh-cw-CAAH2rqe.js +1 -0
  23. package/admin-ui/dist/assets/search-B2hz41D3.js +1 -0
  24. package/admin-ui/dist/assets/server-BrjJPb9D.js +1 -0
  25. package/admin-ui/dist/assets/settings-DvRiHS7i.js +1 -0
  26. package/admin-ui/dist/assets/tester-CftPgRE9.js +3 -0
  27. package/admin-ui/dist/assets/upload-CwXb7Q1b.js +1 -0
  28. package/admin-ui/dist/assets/zap-B4_oDbCp.js +1 -0
  29. package/admin-ui/dist/index.html +5 -3
  30. package/build/icon.icns +0 -0
  31. package/build/icon.ico +0 -0
  32. package/build/icon.png +0 -0
  33. package/build/icon.svg +8 -0
  34. package/build/mac-install-guide.txt +25 -0
  35. package/dist/core/context.js +3 -0
  36. package/dist/core/providers/http-client.js +11 -4
  37. package/dist/core/services/auth-service.js +88 -12
  38. package/dist/core/services/config-service.js +87 -23
  39. package/dist/core/services/github-image-bed-service.js +264 -0
  40. package/dist/core/services/network-detect-service.js +189 -74
  41. package/dist/core/services/version-service.js +1 -1
  42. package/dist/core/store/github-image-bed-history-store.js +68 -0
  43. package/dist/core/store/github-image-bed-store.js +52 -0
  44. package/dist/core/store/profile-store.js +73 -32
  45. package/dist/core/store/settings-store.js +14 -0
  46. package/dist/desktop/main.js +158 -6
  47. package/dist/server/app.js +168 -26
  48. package/dist/server/index.js +41 -15
  49. package/docs/DESKTOP_RELEASE.md +35 -3
  50. package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +2 -2
  51. package/package.json +34 -2
  52. package/admin-ui/dist/assets/app-mark-Gd2QnHMO.svg +0 -9
  53. package/admin-ui/dist/assets/index-DNzR8XR7.css +0 -1
  54. package/admin-ui/dist/assets/index-DZMegNPs.js +0 -1745
  55. 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
+ };
@@ -6,6 +6,7 @@ import {
6
6
  getStorePath
7
7
  } from "./state-paths.js";
8
8
  const PROFILE_CLAIM_PATH = "https://api.openai.com/profile";
9
+ let storeMutationQueue = Promise.resolve();
9
10
  function createEmptyStore() {
10
11
  return {
11
12
  version: 1,
@@ -73,36 +74,50 @@ async function saveStore(store) {
73
74
  await fs.writeFile(getStorePath(), `${JSON.stringify(store, null, 2)}
74
75
  `, "utf8");
75
76
  }
77
+ async function withStoreMutation(operation) {
78
+ const run = storeMutationQueue.then(operation, operation);
79
+ storeMutationQueue = run.then(
80
+ () => void 0,
81
+ () => void 0
82
+ );
83
+ return run;
84
+ }
76
85
  async function saveProfile(profile) {
77
- const store = await loadStore();
78
- store.profiles[profile.profileId] = profile;
79
- store.activeProfileId = profile.profileId;
80
- await saveStore(store);
86
+ await withStoreMutation(async () => {
87
+ const store = await loadStore();
88
+ store.profiles[profile.profileId] = profile;
89
+ store.activeProfileId = profile.profileId;
90
+ await saveStore(store);
91
+ });
81
92
  }
82
93
  async function updateProfile(profileId, updater) {
83
- const store = await loadStore();
84
- const profile = store.profiles[profileId];
85
- if (!profile) {
86
- return null;
87
- }
88
- const updated = updater(profile);
89
- store.profiles[profileId] = updated;
90
- await saveStore(store);
91
- return updated;
94
+ return withStoreMutation(async () => {
95
+ const store = await loadStore();
96
+ const profile = store.profiles[profileId];
97
+ if (!profile) {
98
+ return null;
99
+ }
100
+ const updated = updater(profile);
101
+ store.profiles[profileId] = updated;
102
+ await saveStore(store);
103
+ return updated;
104
+ });
92
105
  }
93
106
  async function listProfiles() {
94
107
  const store = await loadStore();
95
108
  return Object.values(store.profiles);
96
109
  }
97
110
  async function setActiveProfile(profileId) {
98
- const store = await loadStore();
99
- const profile = store.profiles[profileId];
100
- if (!profile) {
101
- return null;
102
- }
103
- store.activeProfileId = profileId;
104
- await saveStore(store);
105
- return profile;
111
+ return withStoreMutation(async () => {
112
+ const store = await loadStore();
113
+ const profile = store.profiles[profileId];
114
+ if (!profile) {
115
+ return null;
116
+ }
117
+ store.activeProfileId = profileId;
118
+ await saveStore(store);
119
+ return profile;
120
+ });
106
121
  }
107
122
  async function getActiveProfile() {
108
123
  const store = await loadStore();
@@ -114,19 +129,44 @@ async function getActiveProfile() {
114
129
  return first ?? null;
115
130
  }
116
131
  async function removeProfile(profileId) {
117
- const store = await loadStore();
118
- if (!store.profiles[profileId]) {
119
- return null;
120
- }
121
- delete store.profiles[profileId];
122
- if (store.activeProfileId === profileId) {
123
- store.activeProfileId = Object.keys(store.profiles)[0];
124
- }
125
- await saveStore(store);
126
- return store.activeProfileId ? store.profiles[store.activeProfileId] ?? null : null;
132
+ return withStoreMutation(async () => {
133
+ const store = await loadStore();
134
+ if (!store.profiles[profileId]) {
135
+ return null;
136
+ }
137
+ delete store.profiles[profileId];
138
+ if (store.activeProfileId === profileId) {
139
+ store.activeProfileId = Object.keys(store.profiles)[0];
140
+ }
141
+ await saveStore(store);
142
+ return store.activeProfileId ? store.profiles[store.activeProfileId] ?? null : null;
143
+ });
144
+ }
145
+ async function removeProfiles(profileIds) {
146
+ return withStoreMutation(async () => {
147
+ const idSet = new Set(profileIds.map((id) => id.trim()).filter(Boolean));
148
+ if (idSet.size === 0) {
149
+ return 0;
150
+ }
151
+ const store = await loadStore();
152
+ let removed = 0;
153
+ for (const profileId of idSet) {
154
+ if (store.profiles[profileId]) {
155
+ delete store.profiles[profileId];
156
+ removed += 1;
157
+ }
158
+ }
159
+ if (store.activeProfileId && !store.profiles[store.activeProfileId]) {
160
+ store.activeProfileId = Object.keys(store.profiles)[0];
161
+ }
162
+ await saveStore(store);
163
+ return removed;
164
+ });
127
165
  }
128
166
  async function clearStore() {
129
- await fs.rm(getStateDir(), { recursive: true, force: true });
167
+ await withStoreMutation(async () => {
168
+ await fs.rm(getStateDir(), { recursive: true, force: true });
169
+ });
130
170
  }
131
171
  export {
132
172
  clearStore,
@@ -136,6 +176,7 @@ export {
136
176
  listProfiles,
137
177
  loadStore,
138
178
  removeProfile,
179
+ removeProfiles,
139
180
  saveProfile,
140
181
  saveStore,
141
182
  setActiveProfile,
@@ -18,6 +18,9 @@ function createDefaultSettings() {
18
18
  autoSwitch: {
19
19
  enabled: false
20
20
  },
21
+ runtime: {
22
+ quotaSyncConcurrency: 16
23
+ },
21
24
  server: {
22
25
  host: "0.0.0.0",
23
26
  port: 8787
@@ -42,6 +45,9 @@ async function loadSettings() {
42
45
  autoSwitch: {
43
46
  enabled: parsed.autoSwitch?.enabled ?? defaults.autoSwitch.enabled
44
47
  },
48
+ runtime: {
49
+ quotaSyncConcurrency: normalizeQuotaSyncConcurrency(parsed.runtime?.quotaSyncConcurrency, defaults.runtime.quotaSyncConcurrency)
50
+ },
45
51
  server: {
46
52
  host: parsed.server?.host ?? defaults.server.host,
47
53
  port: parsed.server?.port ?? defaults.server.port
@@ -51,6 +57,13 @@ async function loadSettings() {
51
57
  return createDefaultSettings();
52
58
  }
53
59
  }
60
+ function normalizeQuotaSyncConcurrency(value, fallback = 16) {
61
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : fallback;
62
+ if (!Number.isFinite(parsed)) {
63
+ return fallback;
64
+ }
65
+ return Math.min(32, Math.max(1, Math.trunc(parsed)));
66
+ }
54
67
  async function saveSettings(settings) {
55
68
  await ensureStateMigrated();
56
69
  await fs.mkdir(getStateDir(), { recursive: true });
@@ -61,5 +74,6 @@ export {
61
74
  createDefaultSettings,
62
75
  getSettingsPath,
63
76
  loadSettings,
77
+ normalizeQuotaSyncConcurrency,
64
78
  saveSettings
65
79
  };
@@ -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() {