ai-zero-token 1.0.9 → 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 +29 -0
- package/README.zh-CN.md +29 -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/openai-codex/chat.js +23 -0
- package/dist/core/providers/openai-codex/oauth.js +24 -1
- package/dist/core/services/auth-service.js +176 -24
- package/dist/core/services/chat-service.js +2 -2
- package/dist/core/services/image-service.js +2 -2
- package/dist/desktop/main.js +127 -0
- package/dist/server/admin-page.js +85 -6
- package/dist/server/app.js +94 -5
- package/docs/DESKTOP_RELEASE.md +64 -0
- package/docs/PRODUCT_UPDATE_DESKTOP_TOOLBOX.md +429 -0
- package/package.json +70 -4
|
@@ -1898,6 +1898,7 @@ function renderAdminPage() {
|
|
|
1898
1898
|
<option value="all">\u5168\u90E8\u72B6\u6001</option>
|
|
1899
1899
|
<option value="healthy">\u5065\u5EB7</option>
|
|
1900
1900
|
<option value="warning">\u5373\u5C06\u8017\u5C3D</option>
|
|
1901
|
+
<option value="invalid">\u767B\u5F55\u5931\u6548</option>
|
|
1901
1902
|
<option value="expired">\u5DF2\u8FC7\u671F</option>
|
|
1902
1903
|
<option value="active">\u4F7F\u7528\u4E2D</option>
|
|
1903
1904
|
</select>
|
|
@@ -2302,6 +2303,13 @@ function renderAdminPage() {
|
|
|
2302
2303
|
return date.toLocaleString("zh-CN", { hour12: false });
|
|
2303
2304
|
}
|
|
2304
2305
|
|
|
2306
|
+
function timestampToMillis(value) {
|
|
2307
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
2308
|
+
return null;
|
|
2309
|
+
}
|
|
2310
|
+
return value < 1000000000000 ? value * 1000 : value;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2305
2313
|
function formatShortTime(value) {
|
|
2306
2314
|
if (!value) {
|
|
2307
2315
|
return "--:--";
|
|
@@ -2528,6 +2536,15 @@ function renderAdminPage() {
|
|
|
2528
2536
|
};
|
|
2529
2537
|
}
|
|
2530
2538
|
|
|
2539
|
+
if (profile.authStatus && (profile.authStatus.state === "token_invalidated" || profile.authStatus.state === "auth_error")) {
|
|
2540
|
+
return {
|
|
2541
|
+
supported: false,
|
|
2542
|
+
label: "\u8BA4\u8BC1\u5931\u6548",
|
|
2543
|
+
detail: "\u8D26\u53F7\u8BA4\u8BC1\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u4F7F\u7528\u56FE\u7247\u751F\u6210\u3002",
|
|
2544
|
+
badgeClass: "red",
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2531
2548
|
const planType = getPlanType(profile);
|
|
2532
2549
|
if (planType === "free") {
|
|
2533
2550
|
return {
|
|
@@ -2546,6 +2563,17 @@ function renderAdminPage() {
|
|
|
2546
2563
|
};
|
|
2547
2564
|
}
|
|
2548
2565
|
|
|
2566
|
+
function describeAuthStatus(profile) {
|
|
2567
|
+
const authStatus = profile && profile.authStatus ? profile.authStatus : null;
|
|
2568
|
+
if (!authStatus || authStatus.state === "ok") {
|
|
2569
|
+
return authStatus && authStatus.checkedAt ? "\u6B63\u5E38 \xB7 " + formatTime(authStatus.checkedAt) : "\u6B63\u5E38";
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
const prefix = authStatus.state === "token_invalidated" ? "\u767B\u5F55\u5931\u6548" : "\u8BA4\u8BC1\u5F02\u5E38";
|
|
2573
|
+
const detail = authStatus.code || authStatus.httpStatus ? " (" + (authStatus.code || authStatus.httpStatus) + ")" : "";
|
|
2574
|
+
return prefix + detail + " \xB7 " + formatTime(authStatus.checkedAt);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2549
2577
|
function maskEmail(email) {
|
|
2550
2578
|
if (typeof email !== "string" || email.indexOf("@") === -1) {
|
|
2551
2579
|
return email || "";
|
|
@@ -2664,6 +2692,22 @@ function renderAdminPage() {
|
|
|
2664
2692
|
|
|
2665
2693
|
function getProfileHealth(profile) {
|
|
2666
2694
|
const now = Date.now();
|
|
2695
|
+
if (profile && profile.authStatus && profile.authStatus.state === "token_invalidated") {
|
|
2696
|
+
return {
|
|
2697
|
+
key: "invalid",
|
|
2698
|
+
label: "\u767B\u5F55\u5931\u6548",
|
|
2699
|
+
badgeClass: "red",
|
|
2700
|
+
barClass: "red",
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
if (profile && profile.authStatus && profile.authStatus.state === "auth_error") {
|
|
2704
|
+
return {
|
|
2705
|
+
key: "invalid",
|
|
2706
|
+
label: "\u8BA4\u8BC1\u5F02\u5E38",
|
|
2707
|
+
badgeClass: "red",
|
|
2708
|
+
barClass: "red",
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2667
2711
|
if (profile && profile.expiresAt && profile.expiresAt <= now) {
|
|
2668
2712
|
return {
|
|
2669
2713
|
key: "expired",
|
|
@@ -2708,8 +2752,9 @@ function renderAdminPage() {
|
|
|
2708
2752
|
const quota = profile.quota;
|
|
2709
2753
|
const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
|
|
2710
2754
|
const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
|
|
2711
|
-
|
|
2712
|
-
|
|
2755
|
+
const resetAtMillis = timestampToMillis(resetAt);
|
|
2756
|
+
if (resetAtMillis) {
|
|
2757
|
+
return formatTime(resetAtMillis);
|
|
2713
2758
|
}
|
|
2714
2759
|
if (typeof resetAfter === "number" && resetAfter > 0) {
|
|
2715
2760
|
return formatCompactDuration(resetAfter) + "\u540E";
|
|
@@ -2739,11 +2784,13 @@ function renderAdminPage() {
|
|
|
2739
2784
|
const quota = profile.quota;
|
|
2740
2785
|
const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
|
|
2741
2786
|
const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
|
|
2742
|
-
|
|
2743
|
-
|
|
2787
|
+
const resetAtMillis = timestampToMillis(resetAt);
|
|
2788
|
+
if (resetAtMillis) {
|
|
2789
|
+
return formatCompactDateTime(resetAtMillis);
|
|
2744
2790
|
}
|
|
2745
2791
|
if (typeof resetAfter === "number" && resetAfter > 0) {
|
|
2746
|
-
|
|
2792
|
+
const capturedAt = timestampToMillis(quota.capturedAt);
|
|
2793
|
+
return capturedAt ? formatCompactDateTime(capturedAt + resetAfter * 1000) : formatCompactDuration(resetAfter) + "\u540E";
|
|
2747
2794
|
}
|
|
2748
2795
|
return "\u672A\u77E5";
|
|
2749
2796
|
}
|
|
@@ -3169,6 +3216,9 @@ function renderAdminPage() {
|
|
|
3169
3216
|
if (status === "warning" && health.key !== "warning") {
|
|
3170
3217
|
return false;
|
|
3171
3218
|
}
|
|
3219
|
+
if (status === "invalid" && health.key !== "invalid") {
|
|
3220
|
+
return false;
|
|
3221
|
+
}
|
|
3172
3222
|
if (status === "expired" && health.key !== "expired") {
|
|
3173
3223
|
return false;
|
|
3174
3224
|
}
|
|
@@ -3344,6 +3394,7 @@ function renderAdminPage() {
|
|
|
3344
3394
|
? '<div class="meta-grid">'
|
|
3345
3395
|
+ '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
|
|
3346
3396
|
+ '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
|
|
3397
|
+
+ '<div class="meta-item"><label>\u8BA4\u8BC1\u72B6\u6001</label><strong>' + escapeHtml(describeAuthStatus(profile)) + "</strong></div>"
|
|
3347
3398
|
+ '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
|
|
3348
3399
|
+ '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
|
|
3349
3400
|
+ '<div class="meta-item"><label>Account ID</label><code>' + escapeHtml(state.showEmails ? (profile.accountId || "\u672A\u63D0\u4F9B") : maskIdentifier(profile.accountId || "\u672A\u63D0\u4F9B")) + "</code></div>"
|
|
@@ -3353,6 +3404,7 @@ function renderAdminPage() {
|
|
|
3353
3404
|
+ '<div class="account-actions">'
|
|
3354
3405
|
+ actionButton
|
|
3355
3406
|
+ codexButton
|
|
3407
|
+
+ '<button class="btn-secondary" type="button" data-profile-action="sync-quota" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5237\u65B0\u989D\u5EA6</button>'
|
|
3356
3408
|
+ '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
|
|
3357
3409
|
+ '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
|
|
3358
3410
|
+ "</div>"
|
|
@@ -3862,6 +3914,28 @@ function renderAdminPage() {
|
|
|
3862
3914
|
await applyProfileToCodex(profileId, button);
|
|
3863
3915
|
return;
|
|
3864
3916
|
}
|
|
3917
|
+
if (action === "sync-quota") {
|
|
3918
|
+
setBusy(button, true);
|
|
3919
|
+
authStatus.textContent = "\u6B63\u5728\u5237\u65B0\u8D26\u53F7\u989D\u5EA6...";
|
|
3920
|
+
try {
|
|
3921
|
+
const config = await fetchJson("/_gateway/admin/profiles/sync-quota", {
|
|
3922
|
+
method: "POST",
|
|
3923
|
+
headers: {
|
|
3924
|
+
"Content-Type": "application/json",
|
|
3925
|
+
},
|
|
3926
|
+
body: formatJson({
|
|
3927
|
+
profileId: profileId,
|
|
3928
|
+
}),
|
|
3929
|
+
});
|
|
3930
|
+
renderConfig(config);
|
|
3931
|
+
authStatus.textContent = "\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65\u3002";
|
|
3932
|
+
} catch (error) {
|
|
3933
|
+
authStatus.textContent = error.message;
|
|
3934
|
+
} finally {
|
|
3935
|
+
setBusy(button, false);
|
|
3936
|
+
}
|
|
3937
|
+
return;
|
|
3938
|
+
}
|
|
3865
3939
|
|
|
3866
3940
|
setBusy(button, true);
|
|
3867
3941
|
authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
|
|
@@ -4285,7 +4359,12 @@ function renderAdminPage() {
|
|
|
4285
4359
|
authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
|
|
4286
4360
|
refreshConfig({
|
|
4287
4361
|
syncRuntime: true,
|
|
4288
|
-
}).then(function () {
|
|
4362
|
+
}).then(function (config) {
|
|
4363
|
+
if (config && config.quotaSync) {
|
|
4364
|
+
const sync = config.quotaSync;
|
|
4365
|
+
authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0: " + String(sync.synced) + "/" + String(sync.total) + " \u4E2A\u8D26\u53F7\u6210\u529F" + (sync.failed ? "\uFF0C" + String(sync.failed) + " \u4E2A\u5931\u8D25" : "") + (sync.skipped ? "\uFF0C" + String(sync.skipped) + " \u4E2A\u767B\u5F55\u5931\u6548\u5DF2\u8DF3\u8FC7" : "") + "\u3002";
|
|
4366
|
+
return;
|
|
4367
|
+
}
|
|
4289
4368
|
authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0\u3002";
|
|
4290
4369
|
}).catch(function (error) {
|
|
4291
4370
|
authStatus.textContent = error && error.message ? error.message : String(error);
|
package/dist/server/app.js
CHANGED
|
@@ -1,11 +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";
|
|
7
10
|
import { requestText } from "../core/providers/http-client.js";
|
|
8
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
|
+
}
|
|
9
56
|
const inputPartSchema = z.object({
|
|
10
57
|
type: z.string().optional(),
|
|
11
58
|
text: z.string().optional()
|
|
@@ -383,6 +430,7 @@ function serializeProfile(profile) {
|
|
|
383
430
|
accountId: profile.accountId,
|
|
384
431
|
email: profile.email,
|
|
385
432
|
quota: profile.quota,
|
|
433
|
+
authStatus: profile.authStatus,
|
|
386
434
|
expiresAt: profile.expires,
|
|
387
435
|
accessTokenPreview: maskSecret(profile.access),
|
|
388
436
|
refreshTokenPreview: maskSecret(profile.refresh)
|
|
@@ -395,6 +443,7 @@ function serializeManagedProfile(profile) {
|
|
|
395
443
|
accountId: profile.accountId,
|
|
396
444
|
email: profile.email,
|
|
397
445
|
quota: profile.quota,
|
|
446
|
+
authStatus: profile.authStatus,
|
|
398
447
|
expiresAt: profile.expiresAt,
|
|
399
448
|
accessTokenPreview: profile.accessTokenPreview,
|
|
400
449
|
refreshTokenPreview: profile.refreshTokenPreview,
|
|
@@ -416,6 +465,10 @@ function getErrorStatusCode(error) {
|
|
|
416
465
|
if (typeof normalized.statusCode === "number") {
|
|
417
466
|
return normalized.statusCode;
|
|
418
467
|
}
|
|
468
|
+
const upstreamStatus = normalized.upstreamStatus;
|
|
469
|
+
if (upstreamStatus === 401 || upstreamStatus === 403) {
|
|
470
|
+
return upstreamStatus;
|
|
471
|
+
}
|
|
419
472
|
const message = normalized.message;
|
|
420
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")) {
|
|
421
474
|
return 400;
|
|
@@ -506,9 +559,28 @@ function createApp(params) {
|
|
|
506
559
|
};
|
|
507
560
|
}
|
|
508
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
|
+
}
|
|
509
566
|
reply.header("Content-Type", "text/html; charset=utf-8");
|
|
510
567
|
return renderAdminPage();
|
|
511
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
|
+
});
|
|
512
584
|
app.get("/favicon.ico", async (_request, reply) => {
|
|
513
585
|
reply.code(204);
|
|
514
586
|
return "";
|
|
@@ -527,15 +599,18 @@ function createApp(params) {
|
|
|
527
599
|
};
|
|
528
600
|
});
|
|
529
601
|
app.post("/_gateway/admin/runtime-refresh", async (request) => {
|
|
530
|
-
await Promise.all([
|
|
531
|
-
ctx.authService.
|
|
602
|
+
const [quotaSync] = await Promise.all([
|
|
603
|
+
ctx.authService.syncAllProfileQuotas("openai-codex", {
|
|
532
604
|
suppressErrors: true
|
|
533
605
|
}),
|
|
534
606
|
ctx.versionService.getVersionStatus({
|
|
535
607
|
force: true
|
|
536
608
|
})
|
|
537
609
|
]);
|
|
538
|
-
return
|
|
610
|
+
return {
|
|
611
|
+
...await buildAdminConfig(request),
|
|
612
|
+
quotaSync
|
|
613
|
+
};
|
|
539
614
|
});
|
|
540
615
|
app.get("/_gateway/admin/config", async (request) => buildAdminConfig(request));
|
|
541
616
|
app.post("/_gateway/admin/login", async (request) => {
|
|
@@ -567,8 +642,22 @@ function createApp(params) {
|
|
|
567
642
|
});
|
|
568
643
|
return buildAdminConfig(request);
|
|
569
644
|
});
|
|
570
|
-
app.post("/_gateway/admin/profiles/sync-quota", async (request) => {
|
|
571
|
-
|
|
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
|
+
}
|
|
572
661
|
return buildAdminConfig(request);
|
|
573
662
|
});
|
|
574
663
|
app.post("/_gateway/admin/profiles/remove", async (request, reply) => {
|
|
@@ -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.
|