ai-zero-token 1.0.8 → 1.0.10
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/README.md +39 -0
- package/README.zh-CN.md +39 -0
- package/admin-ui/dist/assets/index-BBXWfa-w.js +11 -0
- package/admin-ui/dist/assets/index-n7rmcV5d.css +1 -0
- package/admin-ui/dist/assets/wechat-contact-Dlaib1YP.png +0 -0
- package/admin-ui/dist/index.html +13 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.png +0 -0
- package/dist/core/providers/http-client.js +1 -1
- package/dist/core/providers/openai-codex/chat.js +23 -0
- package/dist/core/providers/openai-codex/oauth.js +24 -1
- package/dist/core/services/auth-service.js +264 -21
- package/dist/core/services/chat-service.js +2 -2
- package/dist/core/services/config-service.js +15 -3
- package/dist/core/services/image-service.js +2 -2
- package/dist/core/services/version-service.js +18 -13
- package/dist/core/store/settings-store.js +6 -0
- package/dist/desktop/main.js +127 -0
- package/dist/server/admin-page.js +1094 -100
- package/dist/server/app.js +160 -6
- package/docs/DESKTOP_RELEASE.md +64 -0
- package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +429 -0
- package/package.json +70 -4
package/dist/server/app.js
CHANGED
|
@@ -1,10 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
3
6
|
import Fastify from "fastify";
|
|
4
7
|
import cors from "@fastify/cors";
|
|
5
8
|
import { z } from "zod";
|
|
6
9
|
import { createGatewayContext } from "../core/context.js";
|
|
10
|
+
import { requestText } from "../core/providers/http-client.js";
|
|
7
11
|
import { renderAdminPage } from "./admin-page.js";
|
|
12
|
+
const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
13
|
+
const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
|
|
14
|
+
const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
|
|
15
|
+
const assetContentTypes = {
|
|
16
|
+
".css": "text/css; charset=utf-8",
|
|
17
|
+
".gif": "image/gif",
|
|
18
|
+
".html": "text/html; charset=utf-8",
|
|
19
|
+
".ico": "image/x-icon",
|
|
20
|
+
".jpg": "image/jpeg",
|
|
21
|
+
".jpeg": "image/jpeg",
|
|
22
|
+
".js": "text/javascript; charset=utf-8",
|
|
23
|
+
".json": "application/json; charset=utf-8",
|
|
24
|
+
".map": "application/json; charset=utf-8",
|
|
25
|
+
".png": "image/png",
|
|
26
|
+
".svg": "image/svg+xml",
|
|
27
|
+
".webp": "image/webp"
|
|
28
|
+
};
|
|
29
|
+
async function pathExists(targetPath) {
|
|
30
|
+
try {
|
|
31
|
+
await fs.access(targetPath);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function getContentType(filePath) {
|
|
38
|
+
return assetContentTypes[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
|
|
39
|
+
}
|
|
40
|
+
async function readAdminUiAsset(assetPath) {
|
|
41
|
+
const normalized = path.normalize(assetPath).replace(/^(\.\.(\/|\\|$))+/, "");
|
|
42
|
+
const filePath = path.resolve(adminUiDistDir, normalized);
|
|
43
|
+
const root = path.resolve(adminUiDistDir);
|
|
44
|
+
if (!filePath.startsWith(`${root}${path.sep}`)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return {
|
|
49
|
+
body: await fs.readFile(filePath),
|
|
50
|
+
filePath
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
8
56
|
const inputPartSchema = z.object({
|
|
9
57
|
type: z.string().optional(),
|
|
10
58
|
text: z.string().optional()
|
|
@@ -72,8 +120,18 @@ const settingsUpdateSchema = z.object({
|
|
|
72
120
|
enabled: z.boolean(),
|
|
73
121
|
url: z.string().optional(),
|
|
74
122
|
noProxy: z.string().optional()
|
|
123
|
+
}).optional(),
|
|
124
|
+
autoSwitch: z.object({
|
|
125
|
+
enabled: z.boolean()
|
|
75
126
|
}).optional()
|
|
76
127
|
});
|
|
128
|
+
const proxyTestSchema = z.object({
|
|
129
|
+
networkProxy: z.object({
|
|
130
|
+
enabled: z.boolean(),
|
|
131
|
+
url: z.string().optional(),
|
|
132
|
+
noProxy: z.string().optional()
|
|
133
|
+
})
|
|
134
|
+
});
|
|
77
135
|
const profileActionSchema = z.object({
|
|
78
136
|
profileId: z.string().min(1)
|
|
79
137
|
});
|
|
@@ -372,6 +430,7 @@ function serializeProfile(profile) {
|
|
|
372
430
|
accountId: profile.accountId,
|
|
373
431
|
email: profile.email,
|
|
374
432
|
quota: profile.quota,
|
|
433
|
+
authStatus: profile.authStatus,
|
|
375
434
|
expiresAt: profile.expires,
|
|
376
435
|
accessTokenPreview: maskSecret(profile.access),
|
|
377
436
|
refreshTokenPreview: maskSecret(profile.refresh)
|
|
@@ -384,6 +443,7 @@ function serializeManagedProfile(profile) {
|
|
|
384
443
|
accountId: profile.accountId,
|
|
385
444
|
email: profile.email,
|
|
386
445
|
quota: profile.quota,
|
|
446
|
+
authStatus: profile.authStatus,
|
|
387
447
|
expiresAt: profile.expiresAt,
|
|
388
448
|
accessTokenPreview: profile.accessTokenPreview,
|
|
389
449
|
refreshTokenPreview: profile.refreshTokenPreview,
|
|
@@ -405,6 +465,10 @@ function getErrorStatusCode(error) {
|
|
|
405
465
|
if (typeof normalized.statusCode === "number") {
|
|
406
466
|
return normalized.statusCode;
|
|
407
467
|
}
|
|
468
|
+
const upstreamStatus = normalized.upstreamStatus;
|
|
469
|
+
if (upstreamStatus === 401 || upstreamStatus === 403) {
|
|
470
|
+
return upstreamStatus;
|
|
471
|
+
}
|
|
408
472
|
const message = normalized.message;
|
|
409
473
|
if (message.includes("\u7F3A\u5C11") || message.includes("\u683C\u5F0F\u9519\u8BEF") || message.includes("\u672A\u5185\u7F6E\u6A21\u578B") || message.includes("\u4E0D\u652F\u6301") || message.includes("\u6CA1\u6709\u63D0\u4F9B")) {
|
|
410
474
|
return 400;
|
|
@@ -495,9 +559,28 @@ function createApp(params) {
|
|
|
495
559
|
};
|
|
496
560
|
}
|
|
497
561
|
app.get("/", async (_request, reply) => {
|
|
562
|
+
if (await pathExists(adminUiIndexPath)) {
|
|
563
|
+
reply.header("Content-Type", "text/html; charset=utf-8");
|
|
564
|
+
return fs.readFile(adminUiIndexPath, "utf8");
|
|
565
|
+
}
|
|
498
566
|
reply.header("Content-Type", "text/html; charset=utf-8");
|
|
499
567
|
return renderAdminPage();
|
|
500
568
|
});
|
|
569
|
+
app.get("/assets/*", async (request, reply) => {
|
|
570
|
+
const assetPath = request.params["*"];
|
|
571
|
+
const asset = await readAdminUiAsset(path.join("assets", assetPath));
|
|
572
|
+
if (!asset) {
|
|
573
|
+
reply.code(404);
|
|
574
|
+
return {
|
|
575
|
+
error: {
|
|
576
|
+
type: "not_found",
|
|
577
|
+
message: "asset not found"
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
reply.header("Content-Type", getContentType(asset.filePath));
|
|
582
|
+
return asset.body;
|
|
583
|
+
});
|
|
501
584
|
app.get("/favicon.ico", async (_request, reply) => {
|
|
502
585
|
reply.code(204);
|
|
503
586
|
return "";
|
|
@@ -516,15 +599,18 @@ function createApp(params) {
|
|
|
516
599
|
};
|
|
517
600
|
});
|
|
518
601
|
app.post("/_gateway/admin/runtime-refresh", async (request) => {
|
|
519
|
-
await Promise.all([
|
|
520
|
-
ctx.authService.
|
|
602
|
+
const [quotaSync] = await Promise.all([
|
|
603
|
+
ctx.authService.syncAllProfileQuotas("openai-codex", {
|
|
521
604
|
suppressErrors: true
|
|
522
605
|
}),
|
|
523
606
|
ctx.versionService.getVersionStatus({
|
|
524
607
|
force: true
|
|
525
608
|
})
|
|
526
609
|
]);
|
|
527
|
-
return
|
|
610
|
+
return {
|
|
611
|
+
...await buildAdminConfig(request),
|
|
612
|
+
quotaSync
|
|
613
|
+
};
|
|
528
614
|
});
|
|
529
615
|
app.get("/_gateway/admin/config", async (request) => buildAdminConfig(request));
|
|
530
616
|
app.post("/_gateway/admin/login", async (request) => {
|
|
@@ -551,12 +637,27 @@ function createApp(params) {
|
|
|
551
637
|
}
|
|
552
638
|
await ctx.authService.activateProfile(parsed.data.profileId);
|
|
553
639
|
await ctx.authService.syncActiveProfileQuota("openai-codex", {
|
|
554
|
-
suppressErrors: true
|
|
640
|
+
suppressErrors: true,
|
|
641
|
+
skipAutoSwitch: true
|
|
555
642
|
});
|
|
556
643
|
return buildAdminConfig(request);
|
|
557
644
|
});
|
|
558
|
-
app.post("/_gateway/admin/profiles/sync-quota", async (request) => {
|
|
559
|
-
|
|
645
|
+
app.post("/_gateway/admin/profiles/sync-quota", async (request, reply) => {
|
|
646
|
+
const parsed = profileActionSchema.partial().safeParse(request.body ?? {});
|
|
647
|
+
if (!parsed.success) {
|
|
648
|
+
reply.code(400);
|
|
649
|
+
return {
|
|
650
|
+
error: {
|
|
651
|
+
type: "validation_error",
|
|
652
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
if (parsed.data.profileId) {
|
|
657
|
+
await ctx.authService.syncProfileQuota(parsed.data.profileId, "openai-codex");
|
|
658
|
+
} else {
|
|
659
|
+
await ctx.authService.syncActiveProfileQuota("openai-codex");
|
|
660
|
+
}
|
|
560
661
|
return buildAdminConfig(request);
|
|
561
662
|
});
|
|
562
663
|
app.post("/_gateway/admin/profiles/remove", async (request, reply) => {
|
|
@@ -649,8 +750,61 @@ function createApp(params) {
|
|
|
649
750
|
if (parsed.data.networkProxy) {
|
|
650
751
|
await ctx.configService.setNetworkProxy(parsed.data.networkProxy);
|
|
651
752
|
}
|
|
753
|
+
if (parsed.data.autoSwitch) {
|
|
754
|
+
await ctx.configService.setAutoSwitch(parsed.data.autoSwitch);
|
|
755
|
+
}
|
|
652
756
|
return buildAdminConfig(request);
|
|
653
757
|
});
|
|
758
|
+
app.post("/_gateway/admin/settings/proxy-test", async (request, reply) => {
|
|
759
|
+
const parsed = proxyTestSchema.safeParse(request.body);
|
|
760
|
+
if (!parsed.success) {
|
|
761
|
+
reply.code(400);
|
|
762
|
+
return {
|
|
763
|
+
error: {
|
|
764
|
+
type: "validation_error",
|
|
765
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
const proxy = {
|
|
770
|
+
enabled: parsed.data.networkProxy.enabled,
|
|
771
|
+
url: parsed.data.networkProxy.url?.trim() ?? "",
|
|
772
|
+
noProxy: parsed.data.networkProxy.noProxy?.trim() || "localhost,127.0.0.1,::1"
|
|
773
|
+
};
|
|
774
|
+
if (proxy.enabled && !proxy.url) {
|
|
775
|
+
reply.code(400);
|
|
776
|
+
return {
|
|
777
|
+
error: {
|
|
778
|
+
type: "validation_error",
|
|
779
|
+
message: "\u542F\u7528\u4EE3\u7406\u65F6\u5FC5\u987B\u586B\u5199\u4EE3\u7406\u5730\u5740\u3002"
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const startedAt = performance.now();
|
|
784
|
+
try {
|
|
785
|
+
const response = await requestText({
|
|
786
|
+
method: "GET",
|
|
787
|
+
url: "https://chatgpt.com/",
|
|
788
|
+
timeoutMs: 8e3,
|
|
789
|
+
proxyOverride: proxy
|
|
790
|
+
});
|
|
791
|
+
return {
|
|
792
|
+
ok: response.status >= 200 && response.status < 500,
|
|
793
|
+
status: response.status,
|
|
794
|
+
elapsedMs: Math.round(performance.now() - startedAt),
|
|
795
|
+
target: "https://chatgpt.com/",
|
|
796
|
+
transport: response.transport
|
|
797
|
+
};
|
|
798
|
+
} catch (error) {
|
|
799
|
+
reply.code(502);
|
|
800
|
+
return {
|
|
801
|
+
error: {
|
|
802
|
+
type: "proxy_test_failed",
|
|
803
|
+
message: error instanceof Error ? error.message : String(error)
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
});
|
|
654
808
|
app.get("/v1/models", async () => ({
|
|
655
809
|
object: "list",
|
|
656
810
|
data: (await ctx.modelService.listModels()).map((model) => ({
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# AI Zero Token Desktop Release
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Build Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm run build
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Builds:
|
|
12
|
+
|
|
13
|
+
- `admin-ui/dist`: React management UI
|
|
14
|
+
- `dist`: TypeScript gateway, CLI, and Electron main process
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm run desktop
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Runs the desktop app locally.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run dist:dir
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Creates an unpacked desktop app for the current platform in `release/`.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm run dist:mac
|
|
30
|
+
npm run dist:win
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
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
|
+
|
|
35
|
+
## Signing
|
|
36
|
+
|
|
37
|
+
Unsigned builds are suitable for internal testing only. Public commercial distribution should use platform signing:
|
|
38
|
+
|
|
39
|
+
- macOS: Apple Developer ID Application certificate and notarization.
|
|
40
|
+
- Windows: Authenticode code-signing certificate.
|
|
41
|
+
|
|
42
|
+
`electron-builder` reads the standard signing environment variables. Configure these in CI instead of committing credentials to the repository.
|
|
43
|
+
|
|
44
|
+
## Release Artifacts
|
|
45
|
+
|
|
46
|
+
The packaged output is written to:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
release/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The folder is intentionally ignored by git.
|
|
53
|
+
|
|
54
|
+
## App Resources
|
|
55
|
+
|
|
56
|
+
App icon files live in:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
build/icon.png
|
|
60
|
+
build/icon.icns
|
|
61
|
+
build/icon.ico
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
They are included in Electron packaging and npm packing.
|