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.
- package/CHANGELOG.md +9 -0
- package/admin-ui/dist/assets/InfoRow-0ULI9iI3.js +1 -0
- package/admin-ui/dist/assets/accounts-Ddq82u6R.js +1 -0
- package/admin-ui/dist/assets/activity-D21-Xrc4.js +1 -0
- package/admin-ui/dist/assets/app-mark-nsRs4vo7.svg +8 -0
- package/admin-ui/dist/assets/circle-check-ZYtn9GqY.js +1 -0
- package/admin-ui/dist/assets/clock-3-BzDANsVk.js +1 -0
- package/admin-ui/dist/assets/docs-BRNWAMPw.css +1 -0
- package/admin-ui/dist/assets/docs-CihX3Xsm.js +1735 -0
- package/admin-ui/dist/assets/earth-DFdZaQIi.js +1 -0
- package/admin-ui/dist/assets/image-bed-BGjlDLks.js +1 -0
- package/admin-ui/dist/assets/index-CX8e0-BB.js +10 -0
- package/admin-ui/dist/assets/index-ywn2Jwpu.css +1 -0
- package/admin-ui/dist/assets/jsx-runtime-DqpGtLhh.js +1 -0
- package/admin-ui/dist/assets/launch-BWw7Odq7.js +1 -0
- package/admin-ui/dist/assets/logs-DDdgDVwo.js +1 -0
- package/admin-ui/dist/assets/network-detect-cUdjg4zk.js +1 -0
- package/admin-ui/dist/assets/overview-CsjVVcvi.js +1 -0
- package/admin-ui/dist/assets/profiles-DMOjJORP.js +1 -0
- package/admin-ui/dist/assets/refresh-cw-CAAH2rqe.js +1 -0
- package/admin-ui/dist/assets/search-B2hz41D3.js +1 -0
- package/admin-ui/dist/assets/server-BrjJPb9D.js +1 -0
- package/admin-ui/dist/assets/settings-Be99HpDD.js +1 -0
- package/admin-ui/dist/assets/tester-BIvH_8DY.js +3 -0
- package/admin-ui/dist/assets/upload-CwXb7Q1b.js +1 -0
- package/admin-ui/dist/assets/zap-B4_oDbCp.js +1 -0
- package/admin-ui/dist/index.html +5 -3
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/build/icon.svg +8 -0
- package/dist/core/context.js +3 -0
- package/dist/core/providers/http-client.js +11 -4
- package/dist/core/services/github-image-bed-service.js +264 -0
- package/dist/core/services/network-detect-service.js +189 -74
- package/dist/core/store/github-image-bed-history-store.js +68 -0
- package/dist/core/store/github-image-bed-store.js +52 -0
- package/dist/desktop/main.js +158 -6
- package/dist/server/app.js +108 -13
- package/dist/server/index.js +41 -15
- package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +2 -2
- package/package.json +5 -2
- package/admin-ui/dist/assets/app-mark-Gd2QnHMO.svg +0 -9
- package/admin-ui/dist/assets/index-DNzR8XR7.css +0 -1
- package/admin-ui/dist/assets/index-DZMegNPs.js +0 -1745
- 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
|
+
};
|
package/dist/desktop/main.js
CHANGED
|
@@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
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: "#
|
|
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 (
|
|
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() {
|
package/dist/server/app.js
CHANGED
|
@@ -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
|
-
|
|
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) => ({
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|
|
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>
|