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.
- package/CHANGELOG.md +13 -0
- package/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/admin-ui/dist/assets/app-mark-Gd2QnHMO.svg +9 -0
- package/admin-ui/dist/assets/index-DNzR8XR7.css +1 -0
- package/admin-ui/dist/assets/index-DZMegNPs.js +1745 -0
- package/admin-ui/dist/index.html +3 -2
- package/dist/core/context.js +3 -0
- package/dist/core/providers/http-client.js +10 -4
- package/dist/core/services/network-detect-service.js +239 -0
- package/dist/desktop/main.js +17 -1
- package/dist/server/admin-page.js +27 -13
- package/dist/server/app.js +4 -0
- package/docs/DESKTOP_RELEASE.md +40 -0
- package/package.json +3 -2
- package/admin-ui/dist/assets/index-BBXWfa-w.js +0 -11
- package/admin-ui/dist/assets/index-n7rmcV5d.css +0 -1
package/admin-ui/dist/index.html
CHANGED
|
@@ -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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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>
|
package/dist/core/context.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/desktop/main.js
CHANGED
|
@@ -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(
|
|
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
|
|
3230
|
-
|
|
3231
|
-
|
|
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;
|
package/dist/server/app.js
CHANGED
|
@@ -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) => ({
|
package/docs/DESKTOP_RELEASE.md
CHANGED
|
@@ -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": "
|
|
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
|
|
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",
|