ai-zero-token 1.0.10 → 2.0.1

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.
@@ -3,9 +3,10 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <link rel="icon" type="image/svg+xml" href="/assets/app-mark-Gd2QnHMO.svg" />
6
7
  <title>AI Zero Token</title>
7
- <script type="module" crossorigin src="/assets/index-BBXWfa-w.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-n7rmcV5d.css">
8
+ <script type="module" crossorigin src="/assets/index-DZMegNPs.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DNzR8XR7.css">
9
10
  </head>
10
11
  <body>
11
12
  <div id="root"></div>
@@ -4,12 +4,14 @@ import { AuthService } from "./services/auth-service.js";
4
4
  import { ChatService } from "./services/chat-service.js";
5
5
  import { ImageService } from "./services/image-service.js";
6
6
  import { ModelService } from "./services/model-service.js";
7
+ import { NetworkDetectService } from "./services/network-detect-service.js";
7
8
  import { VersionService } from "./services/version-service.js";
8
9
  function createGatewayContext() {
9
10
  const configService = new ConfigService();
10
11
  const authService = new AuthService(configService);
11
12
  const modelService = new ModelService(configService);
12
13
  const versionService = new VersionService();
14
+ const networkDetectService = new NetworkDetectService();
13
15
  const chatService = new ChatService({
14
16
  authService,
15
17
  modelService
@@ -23,6 +25,7 @@ function createGatewayContext() {
23
25
  authService,
24
26
  modelService,
25
27
  versionService,
28
+ networkDetectService,
26
29
  chatService,
27
30
  imageService
28
31
  };
@@ -19,8 +19,14 @@ function finalizeTiming(startedAt, phases) {
19
19
  totalMs: roundMs(performance.now() - startedAt)
20
20
  };
21
21
  }
22
+ function safeConsole(method, message, meta) {
23
+ try {
24
+ console[method](message, meta);
25
+ } catch {
26
+ }
27
+ }
22
28
  function logHttpTiming(params) {
23
- console.info("[http] request timing", {
29
+ safeConsole("info", "[http] request timing", {
24
30
  requestId: params.requestId,
25
31
  method: params.method,
26
32
  url: params.url,
@@ -127,7 +133,7 @@ async function runCurlRequest(init, params) {
127
133
  try {
128
134
  headers = normalizeCurlHeaders(JSON.parse(headersText));
129
135
  } catch (error) {
130
- console.warn("[http] failed to parse curl response headers", {
136
+ safeConsole("warn", "[http] failed to parse curl response headers", {
131
137
  requestId,
132
138
  url: init.url,
133
139
  error: error instanceof Error ? error.message : String(error)
@@ -160,7 +166,7 @@ async function loadNetworkProxySettings() {
160
166
  const settings = await loadSettings();
161
167
  return settings.networkProxy;
162
168
  } catch (error) {
163
- console.warn("[http] failed to load network proxy settings", {
169
+ safeConsole("warn", "[http] failed to load network proxy settings", {
164
170
  error: error instanceof Error ? error.message : String(error)
165
171
  });
166
172
  return void 0;
@@ -208,7 +214,7 @@ async function requestText(init) {
208
214
  };
209
215
  } catch (error) {
210
216
  const message = error instanceof Error ? error.message : String(error);
211
- console.warn("[http] fetch attempt failed", {
217
+ safeConsole("warn", "[http] fetch attempt failed", {
212
218
  requestId,
213
219
  method: init.method,
214
220
  url: init.url,
@@ -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
+ };
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { app as electronApp, BrowserWindow, dialog, shell } from "electron";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { startServer } from "../server/index.js";
4
6
  let gatewayServer = null;
5
7
  let mainWindow = null;
6
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");
7
12
  function createBrowserUrl(host, port) {
8
13
  if (host === "0.0.0.0" || host === "::") {
9
14
  return `http://127.0.0.1:${port}`;
@@ -45,6 +50,7 @@ async function createMainWindow() {
45
50
  minWidth: 1100,
46
51
  minHeight: 720,
47
52
  title: "AI Zero Token",
53
+ icon: appIconPath,
48
54
  backgroundColor: "#f8fafc",
49
55
  webPreferences: {
50
56
  contextIsolation: true,
@@ -101,7 +107,17 @@ if (!hasSingleInstanceLock) {
101
107
  electronApp.on("second-instance", () => {
102
108
  focusMainWindow();
103
109
  });
104
- electronApp.whenReady().then(createMainWindow).catch(handleStartupError);
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);
105
121
  electronApp.on("activate", () => {
106
122
  if (BrowserWindow.getAllWindows().length === 0) {
107
123
  focusMainWindow();
@@ -2648,6 +2648,10 @@ function renderAdminPage() {
2648
2648
  return Math.max(0, Math.min(100, value));
2649
2649
  }
2650
2650
 
2651
+ function getPrimaryRemaining(profile) {
2652
+ return 100 - getPrimaryUsage(profile);
2653
+ }
2654
+
2651
2655
  function getSecondaryUsage(profile) {
2652
2656
  const value = profile && profile.quota && typeof profile.quota.secondaryUsedPercent === "number"
2653
2657
  ? profile.quota.secondaryUsedPercent
@@ -2744,6 +2748,22 @@ function renderAdminPage() {
2744
2748
  };
2745
2749
  }
2746
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
+
2747
2767
  function describeReset(profile, slot) {
2748
2768
  if (!profile || !profile.quota) {
2749
2769
  return "\u6682\u65E0\u6570\u636E";
@@ -3226,24 +3246,18 @@ function renderAdminPage() {
3226
3246
  });
3227
3247
 
3228
3248
  filtered.sort(function (a, b) {
3229
- const aCodexActive = Boolean(codexAccountId && a.accountId === codexAccountId);
3230
- const bCodexActive = Boolean(codexAccountId && b.accountId === codexAccountId);
3231
- const activeDiff = Number(b.isActive || bCodexActive) - Number(a.isActive || aCodexActive);
3232
- if (activeDiff !== 0) {
3233
- return activeDiff;
3234
- }
3235
- const gatewayDiff = Number(b.isActive) - Number(a.isActive);
3236
- if (gatewayDiff !== 0) {
3237
- return gatewayDiff;
3238
- }
3239
- const codexDiff = Number(bCodexActive) - Number(aCodexActive);
3240
- if (codexDiff !== 0) {
3241
- return codexDiff;
3249
+ const groupDiff = getProfileSortGroup(a, codexAccountId) - getProfileSortGroup(b, codexAccountId);
3250
+ if (groupDiff !== 0) {
3251
+ return groupDiff;
3242
3252
  }
3243
3253
  const planDiff = getPlanRank(b) - getPlanRank(a);
3244
3254
  if (planDiff !== 0) {
3245
3255
  return planDiff;
3246
3256
  }
3257
+ const primaryRemainingDiff = getPrimaryRemaining(b) - getPrimaryRemaining(a);
3258
+ if (primaryRemainingDiff !== 0) {
3259
+ return primaryRemainingDiff;
3260
+ }
3247
3261
  if (sort === "latency-asc") {
3248
3262
  const aCapturedAt = getQuotaSnapshotTime(a) || 0;
3249
3263
  const bCapturedAt = getQuotaSnapshotTime(b) || 0;
@@ -805,6 +805,10 @@ function createApp(params) {
805
805
  };
806
806
  }
807
807
  });
808
+ app.get("/_gateway/admin/network-detect", async () => {
809
+ const settings = await ctx.configService.getSettings();
810
+ return ctx.networkDetectService.collectReport(settings.networkProxy);
811
+ });
808
812
  app.get("/v1/models", async () => ({
809
813
  object: "list",
810
814
  data: (await ctx.modelService.listModels()).map((model) => ({
@@ -2,6 +2,15 @@
2
2
 
3
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
4
 
5
+ ## 2.0.0 Release Notes
6
+
7
+ Version `2.0.0` is the first desktop-focused major release. It includes:
8
+
9
+ - Electron desktop packaging for macOS and Windows.
10
+ - The embedded React management UI under `admin-ui/`.
11
+ - Desktop launch, overview, account, tester, docs, network, logs, and settings pages.
12
+ - Release links in the app shell and README that point to GitHub Releases.
13
+
5
14
  ## Build Commands
6
15
 
7
16
  ```bash
@@ -32,6 +41,21 @@ npm run dist:win
32
41
 
33
42
  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
43
 
44
+ ## UI Engineering Standards
45
+
46
+ Before building release artifacts, the desktop React UI should follow:
47
+
48
+ - [Frontend Architecture Guide](FRONTEND_ARCHITECTURE.md)
49
+ - [Desktop Design System](DESIGN_SYSTEM.md)
50
+
51
+ At minimum, verify:
52
+
53
+ - `App.tsx` only composes the application root.
54
+ - Page modules live under `admin-ui/src/pages`.
55
+ - Shared components and helpers live under `admin-ui/src/shared`.
56
+ - Desktop routes are registered through `admin-ui/src/routes/routes.tsx`.
57
+ - The app renders cleanly at desktop sizes around `1180px x 760px` and above.
58
+
35
59
  ## Signing
36
60
 
37
61
  Unsigned builds are suitable for internal testing only. Public commercial distribution should use platform signing:
@@ -51,6 +75,22 @@ release/
51
75
 
52
76
  The folder is intentionally ignored by git.
53
77
 
78
+ ### Publish Flow
79
+
80
+ 1. Build the desktop package:
81
+
82
+ ```bash
83
+ npm run dist:dir
84
+ ```
85
+
86
+ 2. Upload the generated files from `release/` to the matching GitHub Release tag.
87
+
88
+ 3. Publish the npm package after confirming `package.json` and `package-lock.json` both point at the new version:
89
+
90
+ ```bash
91
+ npm publish
92
+ ```
93
+
54
94
  ## App Resources
55
95
 
56
96
  App icon files live in:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-zero-token",
3
- "version": "1.0.10",
3
+ "version": "2.0.1",
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",
@@ -17,6 +17,7 @@
17
17
  "azt": "dist/cli.js",
18
18
  "ai-zero-token": "dist/cli.js"
19
19
  },
20
+ "main": "dist/desktop/main.js",
20
21
  "keywords": [
21
22
  "ai",
22
23
  "cli",
@@ -45,7 +46,7 @@
45
46
  "build": "npm run build:ui && npm run build:server",
46
47
  "build:server": "tsup",
47
48
  "build:ui": "vite build --config admin-ui/vite.config.ts",
48
- "desktop": "npm run build && electron dist/desktop/main.js",
49
+ "desktop": "npm run build && electron .",
49
50
  "desktop:dev": "node scripts/dev.mjs desktop",
50
51
  "dist": "npm run build && electron-builder",
51
52
  "dist:dir": "npm run build && electron-builder --dir",