ai-zero-token 2.0.4 → 2.0.5
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 +10 -0
- package/README.md +17 -2
- package/README.zh-CN.md +18 -3
- package/admin-ui/dist/assets/{accounts-CTjk9c4F.js → accounts-ABMyXo4H.js} +1 -1
- package/admin-ui/dist/assets/{docs-oNIugCIL.js → docs-Dh0aFha_.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-CQtIhjg_.js → image-bed-C1M7-0q1.js} +1 -1
- package/admin-ui/dist/assets/{index-rgcJgVAu.js → index--rNjdmzf.js} +2 -2
- package/admin-ui/dist/assets/{index-By4r-wy3.css → index-DjtN30PC.css} +1 -1
- package/admin-ui/dist/assets/{launch-B-2Zdz9m.js → launch-pB7YlWFI.js} +1 -1
- package/admin-ui/dist/assets/{logs-JFuSf56b.js → logs-B7McijSi.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-SfvK6uhx.js → network-detect-Bx3XmXPk.js} +1 -1
- package/admin-ui/dist/assets/{overview-X_WodIqE.js → overview-CV0H2Nsq.js} +1 -1
- package/admin-ui/dist/assets/settings-ynCIdUvZ.js +7 -0
- package/admin-ui/dist/assets/{tester-ocpF053C.js → tester-BG-up8qP.js} +1 -1
- package/admin-ui/dist/index.html +2 -2
- package/dist/core/providers/http-client.js +228 -3
- package/dist/core/providers/openai-codex/chat.js +83 -23
- package/dist/core/services/auth-service.js +14 -5
- package/dist/core/services/config-service.js +15 -5
- package/dist/core/store/codex-auth-store.js +295 -4
- package/dist/core/store/settings-store.js +54 -24
- package/dist/server/app.js +410 -49
- package/docs/API_USAGE.md +18 -1
- package/docs/DESKTOP_RELEASE.md +12 -1
- package/package.json +1 -1
- package/admin-ui/dist/assets/settings-0eXUAvcm.js +0 -1
package/dist/server/app.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import Fastify from "fastify";
|
|
7
8
|
import cors from "@fastify/cors";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
import { createGatewayContext } from "../core/context.js";
|
|
10
11
|
import { requestText } from "../core/providers/http-client.js";
|
|
12
|
+
import { streamOpenAICodex } from "../core/providers/openai-codex/chat.js";
|
|
11
13
|
const packageRoot = path.dirname(fileURLToPath(new URL("../../package.json", import.meta.url)));
|
|
12
14
|
const adminUiDistDir = path.join(packageRoot, "admin-ui", "dist");
|
|
13
15
|
const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
|
|
@@ -45,17 +47,9 @@ async function readAdminUiAsset(assetPath) {
|
|
|
45
47
|
return null;
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
|
-
const inputPartSchema = z.object({
|
|
49
|
-
type: z.string().optional(),
|
|
50
|
-
text: z.string().optional()
|
|
51
|
-
}).passthrough();
|
|
52
|
-
const inputMessageSchema = z.object({
|
|
53
|
-
role: z.string().optional(),
|
|
54
|
-
content: z.array(inputPartSchema).optional()
|
|
55
|
-
}).passthrough();
|
|
56
50
|
const responsesBodySchema = z.object({
|
|
57
51
|
model: z.string().optional(),
|
|
58
|
-
input: z.
|
|
52
|
+
input: z.unknown().optional(),
|
|
59
53
|
instructions: z.string().optional(),
|
|
60
54
|
stream: z.boolean().optional(),
|
|
61
55
|
tools: z.array(z.unknown()).optional(),
|
|
@@ -69,7 +63,7 @@ const responsesBodySchema = z.object({
|
|
|
69
63
|
allow_unknown_model: z.boolean().optional(),
|
|
70
64
|
include_raw: z.boolean().optional()
|
|
71
65
|
}).passthrough().optional()
|
|
72
|
-
});
|
|
66
|
+
}).passthrough();
|
|
73
67
|
const chatCompletionContentPartSchema = z.object({
|
|
74
68
|
type: z.string().optional(),
|
|
75
69
|
text: z.string().optional(),
|
|
@@ -114,7 +108,8 @@ const settingsUpdateSchema = z.object({
|
|
|
114
108
|
noProxy: z.string().optional()
|
|
115
109
|
}).optional(),
|
|
116
110
|
autoSwitch: z.object({
|
|
117
|
-
enabled: z.boolean()
|
|
111
|
+
enabled: z.boolean().optional(),
|
|
112
|
+
excludedProfileIds: z.array(z.string()).optional()
|
|
118
113
|
}).optional(),
|
|
119
114
|
runtime: z.object({
|
|
120
115
|
quotaSyncConcurrency: z.number().int().min(1).max(32).optional()
|
|
@@ -150,6 +145,10 @@ const profileExportSchema = z.object({
|
|
|
150
145
|
const codexApplySchema = z.object({
|
|
151
146
|
profileId: z.string().min(1)
|
|
152
147
|
});
|
|
148
|
+
const codexProviderConfigSchema = z.object({
|
|
149
|
+
baseUrl: z.string().min(1).optional(),
|
|
150
|
+
providerId: z.string().min(1).optional()
|
|
151
|
+
});
|
|
153
152
|
const githubImageBedConfigSchema = z.object({
|
|
154
153
|
token: z.string().min(1)
|
|
155
154
|
});
|
|
@@ -199,6 +198,21 @@ const imageEditsBodySchema = z.object({
|
|
|
199
198
|
response_format: z.enum(["b64_json", "url"]).optional(),
|
|
200
199
|
user: z.string().optional()
|
|
201
200
|
}).passthrough();
|
|
201
|
+
function extractTextFromInputContent(content) {
|
|
202
|
+
if (typeof content === "string" && content.trim()) {
|
|
203
|
+
return [content.trim()];
|
|
204
|
+
}
|
|
205
|
+
if (!Array.isArray(content)) {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
return content.flatMap((part) => {
|
|
209
|
+
if (!part || typeof part !== "object") {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const record = part;
|
|
213
|
+
return typeof record.text === "string" && record.text.trim() ? [record.text.trim()] : [];
|
|
214
|
+
});
|
|
215
|
+
}
|
|
202
216
|
function extractTextInput(input) {
|
|
203
217
|
if (typeof input === "undefined") {
|
|
204
218
|
return "";
|
|
@@ -207,12 +221,14 @@ function extractTextInput(input) {
|
|
|
207
221
|
return input;
|
|
208
222
|
}
|
|
209
223
|
const chunks = [];
|
|
224
|
+
if (!Array.isArray(input)) {
|
|
225
|
+
return "";
|
|
226
|
+
}
|
|
210
227
|
for (const item of input) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
chunks.push(part.text.trim());
|
|
214
|
-
}
|
|
228
|
+
if (!item || typeof item !== "object") {
|
|
229
|
+
continue;
|
|
215
230
|
}
|
|
231
|
+
chunks.push(...extractTextFromInputContent(item.content));
|
|
216
232
|
}
|
|
217
233
|
return chunks.join("\n").trim();
|
|
218
234
|
}
|
|
@@ -415,6 +431,46 @@ function summarizeToolNames(tools) {
|
|
|
415
431
|
return typeof fn?.name === "string" ? fn.name : typeof record.name === "string" ? record.name : "";
|
|
416
432
|
}).filter(Boolean);
|
|
417
433
|
}
|
|
434
|
+
function summarizeResponsesRequest(data) {
|
|
435
|
+
const input = data.input;
|
|
436
|
+
const toolNames = summarizeToolNames(Array.isArray(data.tools) ? data.tools : void 0);
|
|
437
|
+
return {
|
|
438
|
+
endpoint: "/v1/responses",
|
|
439
|
+
model: data.model ?? "default",
|
|
440
|
+
stream: data.stream ?? false,
|
|
441
|
+
inputKind: typeof input === "string" ? "string" : Array.isArray(input) ? "array" : "override",
|
|
442
|
+
inputItems: Array.isArray(input) ? input.length : void 0,
|
|
443
|
+
inputTextPreview: typeof input === "string" ? truncateForLog(input) : "",
|
|
444
|
+
instructionsLength: typeof data.instructions === "string" ? data.instructions.length : void 0,
|
|
445
|
+
toolCount: Array.isArray(data.tools) ? data.tools.length : 0,
|
|
446
|
+
toolNames: toolNames.slice(0, 50),
|
|
447
|
+
toolNamesTruncated: toolNames.length > 50,
|
|
448
|
+
toolChoice: typeof data.tool_choice === "undefined" ? "default" : typeof data.tool_choice,
|
|
449
|
+
parallelToolCalls: data.parallel_tool_calls,
|
|
450
|
+
hasReasoning: Boolean(data.reasoning)
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function createResponsesCodexBody(data) {
|
|
454
|
+
const experimentalBody = data.experimental_codex?.body ?? {};
|
|
455
|
+
const body = {
|
|
456
|
+
...experimentalBody,
|
|
457
|
+
...data
|
|
458
|
+
};
|
|
459
|
+
delete body.experimental_codex;
|
|
460
|
+
const normalizedInput = normalizeResponseInput(data.input);
|
|
461
|
+
if (typeof normalizedInput !== "undefined") {
|
|
462
|
+
body.input = normalizedInput;
|
|
463
|
+
}
|
|
464
|
+
return body;
|
|
465
|
+
}
|
|
466
|
+
function createCodexPassthroughBody(data, model) {
|
|
467
|
+
const body = {
|
|
468
|
+
...data,
|
|
469
|
+
model
|
|
470
|
+
};
|
|
471
|
+
delete body.experimental_codex;
|
|
472
|
+
return body;
|
|
473
|
+
}
|
|
418
474
|
function summarizeChatCompletionsRequest(data) {
|
|
419
475
|
const lastUserMessage = [...data.messages].reverse().find((message) => (message.role ?? "user") === "user");
|
|
420
476
|
const toolNames = summarizeToolNames(data.tools);
|
|
@@ -455,7 +511,17 @@ function profileLogLabel(profile) {
|
|
|
455
511
|
return profile?.email || profile?.accountId || profile?.profileId || "-";
|
|
456
512
|
}
|
|
457
513
|
function requestSourceFromUserAgent(userAgent) {
|
|
458
|
-
|
|
514
|
+
if (typeof userAgent !== "string") {
|
|
515
|
+
return "API";
|
|
516
|
+
}
|
|
517
|
+
const normalized = userAgent.toLowerCase();
|
|
518
|
+
if (normalized.includes("codex")) {
|
|
519
|
+
return "Codex";
|
|
520
|
+
}
|
|
521
|
+
if (normalized.includes("openclaw")) {
|
|
522
|
+
return "OpenClaw";
|
|
523
|
+
}
|
|
524
|
+
return "API";
|
|
459
525
|
}
|
|
460
526
|
function createChatCompletionsCodexBody(data) {
|
|
461
527
|
const body = {
|
|
@@ -749,7 +815,7 @@ function getErrorStatusCode(error) {
|
|
|
749
815
|
return normalized.statusCode;
|
|
750
816
|
}
|
|
751
817
|
const upstreamStatus = normalized.upstreamStatus;
|
|
752
|
-
if (upstreamStatus ===
|
|
818
|
+
if (typeof upstreamStatus === "number" && upstreamStatus >= 400 && upstreamStatus < 600) {
|
|
753
819
|
return upstreamStatus;
|
|
754
820
|
}
|
|
755
821
|
const message = normalized.message;
|
|
@@ -761,6 +827,50 @@ function getErrorStatusCode(error) {
|
|
|
761
827
|
}
|
|
762
828
|
return 500;
|
|
763
829
|
}
|
|
830
|
+
function isQuotaLimitError(error) {
|
|
831
|
+
const normalized = normalizeError(error);
|
|
832
|
+
const marker = `${normalized.upstreamErrorCode ?? ""} ${normalized.upstreamErrorType ?? ""} ${normalized.message}`.toLowerCase();
|
|
833
|
+
return normalized.upstreamStatus === 429 || marker.includes("usage_limit_reached");
|
|
834
|
+
}
|
|
835
|
+
function createSseStreamStats() {
|
|
836
|
+
return {
|
|
837
|
+
buffer: "",
|
|
838
|
+
bytes: 0,
|
|
839
|
+
completed: false
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
function trackSseChunk(stats, chunk) {
|
|
843
|
+
const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : String(chunk);
|
|
844
|
+
stats.bytes += Buffer.byteLength(text);
|
|
845
|
+
stats.buffer += text.replace(/\r\n/g, "\n");
|
|
846
|
+
let separatorIndex = stats.buffer.indexOf("\n\n");
|
|
847
|
+
while (separatorIndex !== -1) {
|
|
848
|
+
const block = stats.buffer.slice(0, separatorIndex);
|
|
849
|
+
stats.buffer = stats.buffer.slice(separatorIndex + 2);
|
|
850
|
+
const eventName = block.split("\n").find((line) => line.startsWith("event:"))?.slice("event:".length).trim();
|
|
851
|
+
const data = block.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).trim()).join("\n");
|
|
852
|
+
let eventType = eventName;
|
|
853
|
+
if (data && data !== "[DONE]") {
|
|
854
|
+
try {
|
|
855
|
+
const parsed = JSON.parse(data);
|
|
856
|
+
if (typeof parsed.type === "string") {
|
|
857
|
+
eventType = parsed.type;
|
|
858
|
+
}
|
|
859
|
+
} catch {
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (eventType === "response.completed") {
|
|
863
|
+
stats.completed = true;
|
|
864
|
+
stats.terminalEvent = eventType;
|
|
865
|
+
} else if (eventType === "response.failed" || eventType === "response.incomplete") {
|
|
866
|
+
stats.terminalEvent = eventType;
|
|
867
|
+
}
|
|
868
|
+
separatorIndex = stats.buffer.indexOf("\n\n");
|
|
869
|
+
}
|
|
870
|
+
if (stats.buffer.length > 65536) {
|
|
871
|
+
stats.buffer = stats.buffer.slice(-65536);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
764
874
|
function createApp(params) {
|
|
765
875
|
const app = Fastify({
|
|
766
876
|
logger: false,
|
|
@@ -833,6 +943,7 @@ function createApp(params) {
|
|
|
833
943
|
codex: codexStatus,
|
|
834
944
|
adminUrl: `${origin}/`,
|
|
835
945
|
baseUrl: `${origin}/v1`,
|
|
946
|
+
codexBaseUrl: `${origin}/codex/v1`,
|
|
836
947
|
restartSupported: Boolean(params?.onRestart),
|
|
837
948
|
codexRestartSupported: Boolean(params?.onRestartCodex),
|
|
838
949
|
supportedEndpoints: [
|
|
@@ -846,6 +957,11 @@ function createApp(params) {
|
|
|
846
957
|
path: "/v1/responses",
|
|
847
958
|
description: "OpenAI responses \u517C\u5BB9\u63A5\u53E3\u3002"
|
|
848
959
|
},
|
|
960
|
+
{
|
|
961
|
+
method: "POST",
|
|
962
|
+
path: "/codex/v1/responses",
|
|
963
|
+
description: "Codex custom provider \u4E13\u7528 Responses SSE \u900F\u4F20\u63A5\u53E3\u3002"
|
|
964
|
+
},
|
|
849
965
|
{
|
|
850
966
|
method: "POST",
|
|
851
967
|
path: "/v1/chat/completions",
|
|
@@ -1092,6 +1208,45 @@ function createApp(params) {
|
|
|
1092
1208
|
config: await buildAdminConfig(request)
|
|
1093
1209
|
};
|
|
1094
1210
|
});
|
|
1211
|
+
app.post("/_gateway/admin/codex/configure-provider", async (request, reply) => {
|
|
1212
|
+
const parsed = codexProviderConfigSchema.safeParse(request.body ?? {});
|
|
1213
|
+
if (!parsed.success) {
|
|
1214
|
+
reply.code(400);
|
|
1215
|
+
return {
|
|
1216
|
+
error: {
|
|
1217
|
+
type: "validation_error",
|
|
1218
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
const origin = resolveOrigin(request);
|
|
1223
|
+
const baseUrl = parsed.data.baseUrl ?? `${origin}/codex/v1`;
|
|
1224
|
+
return {
|
|
1225
|
+
codexProvider: await ctx.authService.applyGatewayToCodexProvider({
|
|
1226
|
+
baseUrl,
|
|
1227
|
+
providerId: parsed.data.providerId
|
|
1228
|
+
}),
|
|
1229
|
+
config: await buildAdminConfig(request)
|
|
1230
|
+
};
|
|
1231
|
+
});
|
|
1232
|
+
app.post("/_gateway/admin/codex/remove-provider", async (request, reply) => {
|
|
1233
|
+
const parsed = codexProviderConfigSchema.safeParse(request.body ?? {});
|
|
1234
|
+
if (!parsed.success) {
|
|
1235
|
+
reply.code(400);
|
|
1236
|
+
return {
|
|
1237
|
+
error: {
|
|
1238
|
+
type: "validation_error",
|
|
1239
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
codexProvider: await ctx.authService.removeGatewayFromCodexProvider({
|
|
1245
|
+
providerId: parsed.data.providerId
|
|
1246
|
+
}),
|
|
1247
|
+
config: await buildAdminConfig(request)
|
|
1248
|
+
};
|
|
1249
|
+
});
|
|
1095
1250
|
app.put("/_gateway/admin/settings", async (request, reply) => {
|
|
1096
1251
|
const parsed = settingsUpdateSchema.safeParse(request.body);
|
|
1097
1252
|
if (!parsed.success) {
|
|
@@ -1262,9 +1417,171 @@ function createApp(params) {
|
|
|
1262
1417
|
owned_by: model.provider
|
|
1263
1418
|
}))
|
|
1264
1419
|
}));
|
|
1265
|
-
|
|
1420
|
+
async function handleCodexResponsesPassthrough(request, reply, data, startedAt) {
|
|
1421
|
+
const abortController = new AbortController();
|
|
1422
|
+
let streamFinished = false;
|
|
1423
|
+
let headersCommitted = false;
|
|
1424
|
+
let profile = null;
|
|
1425
|
+
let retryCount = 0;
|
|
1426
|
+
let failureRecorded = false;
|
|
1427
|
+
reply.raw.on("close", () => {
|
|
1428
|
+
if (!streamFinished) {
|
|
1429
|
+
abortController.abort();
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1432
|
+
try {
|
|
1433
|
+
const model = await ctx.modelService.resolveModel("openai-codex", data.model, {
|
|
1434
|
+
allowUnknown: data.experimental_codex?.allow_unknown_model
|
|
1435
|
+
});
|
|
1436
|
+
const codexBody = createCodexPassthroughBody(data, model);
|
|
1437
|
+
let upstream = null;
|
|
1438
|
+
const maxProfileAttempts = 5;
|
|
1439
|
+
for (let attempt = 0; attempt < maxProfileAttempts; attempt += 1) {
|
|
1440
|
+
profile = await ctx.authService.requireUsableProfile("openai-codex");
|
|
1441
|
+
try {
|
|
1442
|
+
upstream = await streamOpenAICodex({
|
|
1443
|
+
profile,
|
|
1444
|
+
model,
|
|
1445
|
+
bodyOverride: codexBody,
|
|
1446
|
+
passthroughBody: true,
|
|
1447
|
+
signal: abortController.signal
|
|
1448
|
+
});
|
|
1449
|
+
break;
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
const quota = error.quota;
|
|
1452
|
+
const switchedProfile = await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex");
|
|
1453
|
+
failureRecorded = true;
|
|
1454
|
+
if (attempt < maxProfileAttempts - 1 && isQuotaLimitError(error) && switchedProfile && switchedProfile.profileId !== profile.profileId && !abortController.signal.aborted) {
|
|
1455
|
+
retryCount += 1;
|
|
1456
|
+
failureRecorded = false;
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
throw error;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
if (!upstream || !profile) {
|
|
1463
|
+
throw new Error("Codex stream \u672A\u80FD\u5EFA\u7ACB\u3002");
|
|
1464
|
+
}
|
|
1465
|
+
await ctx.authService.recordProfileRequestSuccess(profile.profileId, upstream.quota, "openai-codex");
|
|
1466
|
+
const headers = {
|
|
1467
|
+
"Content-Type": upstream.headers["content-type"] ?? "text/event-stream; charset=utf-8",
|
|
1468
|
+
"Cache-Control": "no-cache, no-transform",
|
|
1469
|
+
Connection: "keep-alive",
|
|
1470
|
+
"X-Accel-Buffering": "no"
|
|
1471
|
+
};
|
|
1472
|
+
for (const [key, value] of Object.entries(upstream.headers)) {
|
|
1473
|
+
if (key.startsWith("x-codex-") || key === "x-request-id") {
|
|
1474
|
+
headers[key] = value;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
reply.raw.writeHead(upstream.status, headers);
|
|
1478
|
+
headersCommitted = true;
|
|
1479
|
+
reply.raw.flushHeaders?.();
|
|
1480
|
+
const streamStats = createSseStreamStats();
|
|
1481
|
+
for await (const chunk of Readable.fromWeb(upstream.body)) {
|
|
1482
|
+
trackSseChunk(streamStats, chunk);
|
|
1483
|
+
if (!reply.raw.write(chunk)) {
|
|
1484
|
+
await new Promise((resolve) => reply.raw.once("drain", resolve));
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
streamFinished = true;
|
|
1488
|
+
reply.raw.end();
|
|
1489
|
+
if (!streamStats.completed) {
|
|
1490
|
+
console.warn("[gateway:codex:stream] upstream stream ended without response.completed", {
|
|
1491
|
+
requestId: request.id,
|
|
1492
|
+
upstreamRequestId: upstream.requestId,
|
|
1493
|
+
account: profileLogLabel(profile),
|
|
1494
|
+
model,
|
|
1495
|
+
bytes: streamStats.bytes,
|
|
1496
|
+
terminalEvent: streamStats.terminalEvent
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
pushGatewayRequestLog({
|
|
1500
|
+
method: request.method,
|
|
1501
|
+
endpoint: request.url,
|
|
1502
|
+
account: profileLogLabel(profile),
|
|
1503
|
+
model,
|
|
1504
|
+
statusCode: upstream.status,
|
|
1505
|
+
durationMs: performance.now() - startedAt,
|
|
1506
|
+
source: "Codex",
|
|
1507
|
+
details: {
|
|
1508
|
+
requestId: request.id,
|
|
1509
|
+
upstreamRequestId: upstream.requestId,
|
|
1510
|
+
remoteAddress: request.ip,
|
|
1511
|
+
userAgent: request.headers["user-agent"],
|
|
1512
|
+
request: summarizeResponsesRequest(data),
|
|
1513
|
+
response: {
|
|
1514
|
+
stream: true,
|
|
1515
|
+
passthrough: true,
|
|
1516
|
+
retryCount,
|
|
1517
|
+
completed: streamStats.completed,
|
|
1518
|
+
terminalEvent: streamStats.terminalEvent,
|
|
1519
|
+
bytes: streamStats.bytes
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
return reply;
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
const quota = error.quota;
|
|
1526
|
+
if (profile && !failureRecorded) {
|
|
1527
|
+
await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex");
|
|
1528
|
+
}
|
|
1529
|
+
const normalized = normalizeError(error);
|
|
1530
|
+
const statusCode = getErrorStatusCode(normalized);
|
|
1531
|
+
pushGatewayRequestLog({
|
|
1532
|
+
method: request.method,
|
|
1533
|
+
endpoint: request.url,
|
|
1534
|
+
account: profileLogLabel(profile),
|
|
1535
|
+
model: data.model ?? "default",
|
|
1536
|
+
statusCode,
|
|
1537
|
+
durationMs: performance.now() - startedAt,
|
|
1538
|
+
source: "Codex",
|
|
1539
|
+
details: {
|
|
1540
|
+
requestId: request.id,
|
|
1541
|
+
remoteAddress: request.ip,
|
|
1542
|
+
userAgent: request.headers["user-agent"],
|
|
1543
|
+
request: summarizeResponsesRequest(data),
|
|
1544
|
+
response: {
|
|
1545
|
+
retryCount
|
|
1546
|
+
},
|
|
1547
|
+
error: {
|
|
1548
|
+
message: normalized.message,
|
|
1549
|
+
upstreamStatus: normalized.upstreamStatus,
|
|
1550
|
+
upstreamErrorCode: normalized.upstreamErrorCode,
|
|
1551
|
+
upstreamErrorMessage: normalized.upstreamErrorMessage
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
if (headersCommitted) {
|
|
1556
|
+
streamFinished = true;
|
|
1557
|
+
reply.raw.end();
|
|
1558
|
+
return reply;
|
|
1559
|
+
}
|
|
1560
|
+
throw error;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
app.post("/codex/v1/responses", async (request, reply) => {
|
|
1564
|
+
const startedAt = performance.now();
|
|
1266
1565
|
const parsed = responsesBodySchema.safeParse(request.body);
|
|
1267
1566
|
if (!parsed.success) {
|
|
1567
|
+
pushGatewayRequestLog({
|
|
1568
|
+
method: request.method,
|
|
1569
|
+
endpoint: request.url,
|
|
1570
|
+
account: "-",
|
|
1571
|
+
model: "-",
|
|
1572
|
+
statusCode: 400,
|
|
1573
|
+
durationMs: performance.now() - startedAt,
|
|
1574
|
+
source: "Codex",
|
|
1575
|
+
details: {
|
|
1576
|
+
requestId: request.id,
|
|
1577
|
+
remoteAddress: request.ip,
|
|
1578
|
+
userAgent: request.headers["user-agent"],
|
|
1579
|
+
error: {
|
|
1580
|
+
type: "validation_error",
|
|
1581
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1268
1585
|
reply.code(400);
|
|
1269
1586
|
return {
|
|
1270
1587
|
error: {
|
|
@@ -1273,18 +1590,61 @@ function createApp(params) {
|
|
|
1273
1590
|
}
|
|
1274
1591
|
};
|
|
1275
1592
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1593
|
+
return handleCodexResponsesPassthrough(request, reply, parsed.data, startedAt);
|
|
1594
|
+
});
|
|
1595
|
+
app.post("/v1/responses", async (request, reply) => {
|
|
1596
|
+
const startedAt = performance.now();
|
|
1597
|
+
const parsed = responsesBodySchema.safeParse(request.body);
|
|
1598
|
+
if (!parsed.success) {
|
|
1599
|
+
pushGatewayRequestLog({
|
|
1600
|
+
method: request.method,
|
|
1601
|
+
endpoint: request.url,
|
|
1602
|
+
account: "-",
|
|
1603
|
+
model: "-",
|
|
1604
|
+
statusCode: 400,
|
|
1605
|
+
durationMs: performance.now() - startedAt,
|
|
1606
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1607
|
+
details: {
|
|
1608
|
+
requestId: request.id,
|
|
1609
|
+
remoteAddress: request.ip,
|
|
1610
|
+
userAgent: request.headers["user-agent"],
|
|
1611
|
+
error: {
|
|
1612
|
+
type: "validation_error",
|
|
1613
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
reply.code(400);
|
|
1278
1618
|
return {
|
|
1279
1619
|
error: {
|
|
1280
|
-
type: "
|
|
1281
|
-
message: "\
|
|
1620
|
+
type: "validation_error",
|
|
1621
|
+
message: parsed.error.issues[0]?.message ?? "\u8BF7\u6C42\u4F53\u683C\u5F0F\u9519\u8BEF"
|
|
1282
1622
|
}
|
|
1283
1623
|
};
|
|
1284
1624
|
}
|
|
1625
|
+
const wantsEventStream = typeof request.headers.accept === "string" && request.headers.accept.toLowerCase().includes("text/event-stream");
|
|
1285
1626
|
const input = extractTextInput(parsed.data.input);
|
|
1286
1627
|
const hasInput = typeof parsed.data.input !== "undefined" || typeof parsed.data.experimental_codex?.body?.input !== "undefined";
|
|
1287
1628
|
if (!hasInput) {
|
|
1629
|
+
pushGatewayRequestLog({
|
|
1630
|
+
method: request.method,
|
|
1631
|
+
endpoint: request.url,
|
|
1632
|
+
account: "-",
|
|
1633
|
+
model: parsed.data.model ?? "default",
|
|
1634
|
+
statusCode: 400,
|
|
1635
|
+
durationMs: performance.now() - startedAt,
|
|
1636
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1637
|
+
details: {
|
|
1638
|
+
requestId: request.id,
|
|
1639
|
+
remoteAddress: request.ip,
|
|
1640
|
+
userAgent: request.headers["user-agent"],
|
|
1641
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
1642
|
+
error: {
|
|
1643
|
+
type: "validation_error",
|
|
1644
|
+
message: "\u6CA1\u6709\u63D0\u4F9B input\uFF0C\u4E5F\u6CA1\u6709\u5728 experimental_codex.body \u91CC\u900F\u4F20 input"
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1288
1648
|
reply.code(400);
|
|
1289
1649
|
return {
|
|
1290
1650
|
error: {
|
|
@@ -1293,34 +1653,35 @@ function createApp(params) {
|
|
|
1293
1653
|
}
|
|
1294
1654
|
};
|
|
1295
1655
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1656
|
+
if (parsed.data.stream || wantsEventStream) {
|
|
1657
|
+
pushGatewayRequestLog({
|
|
1658
|
+
method: request.method,
|
|
1659
|
+
endpoint: request.url,
|
|
1660
|
+
account: "-",
|
|
1661
|
+
model: parsed.data.model ?? "default",
|
|
1662
|
+
statusCode: 501,
|
|
1663
|
+
durationMs: performance.now() - startedAt,
|
|
1664
|
+
source: requestSourceFromUserAgent(request.headers["user-agent"]),
|
|
1665
|
+
details: {
|
|
1666
|
+
requestId: request.id,
|
|
1667
|
+
remoteAddress: request.ip,
|
|
1668
|
+
userAgent: request.headers["user-agent"],
|
|
1669
|
+
request: summarizeResponsesRequest(parsed.data),
|
|
1670
|
+
error: {
|
|
1671
|
+
type: "not_supported",
|
|
1672
|
+
message: "\u666E\u901A Responses stream \u5C1A\u672A\u5B9E\u73B0\uFF1BCodex custom provider \u8BF7\u6C42\u4F1A\u8D70\u4E13\u7528\u900F\u4F20\u8DEF\u5F84\u3002"
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
reply.code(501);
|
|
1677
|
+
return {
|
|
1678
|
+
error: {
|
|
1679
|
+
type: "not_supported",
|
|
1680
|
+
message: "\u666E\u901A Responses stream \u5C1A\u672A\u5B9E\u73B0\uFF1BCodex custom provider \u8BF7\u6C42\u4F1A\u8D70\u4E13\u7528\u900F\u4F20\u8DEF\u5F84\u3002"
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1323
1683
|
}
|
|
1684
|
+
const codexBody = createResponsesCodexBody(parsed.data);
|
|
1324
1685
|
const result = await ctx.chatService.chat({
|
|
1325
1686
|
model: parsed.data.model,
|
|
1326
1687
|
input: input || void 0,
|
package/docs/API_USAGE.md
CHANGED
|
@@ -59,6 +59,23 @@ The gateway accepts OpenClaw-style `chat.completions` requests with `tools`, `to
|
|
|
59
59
|
|
|
60
60
|
OpenClaw requests are visible in the management console request log when the client sends an OpenClaw user agent. The log keeps safe summaries only; it does not store full access tokens.
|
|
61
61
|
|
|
62
|
+
## Codex Custom Provider
|
|
63
|
+
|
|
64
|
+
Codex CLI/Desktop can route model traffic through AI Zero Token by using a custom Responses provider in `~/.codex/config.toml`. The management console Settings page can write this automatically with "接管 Codex 请求" and remove it with "解除接管":
|
|
65
|
+
|
|
66
|
+
```toml
|
|
67
|
+
model = "gpt-5.4"
|
|
68
|
+
model_provider = "ai-zero-token"
|
|
69
|
+
|
|
70
|
+
[model_providers.ai-zero-token]
|
|
71
|
+
name = "AI Zero Token"
|
|
72
|
+
base_url = "http://127.0.0.1:8787/codex/v1"
|
|
73
|
+
wire_api = "responses"
|
|
74
|
+
supports_websockets = false
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Codex sends `POST /codex/v1/responses` with `Accept: text/event-stream`; the gateway forwards that request to the active Codex OAuth account and streams upstream Responses SSE events back to Codex. The regular `/v1/*` routes remain OpenAI-compatible API routes for non-Codex clients.
|
|
78
|
+
|
|
62
79
|
## Models
|
|
63
80
|
|
|
64
81
|
```bash
|
|
@@ -196,7 +213,7 @@ console.log(response.choices[0]?.message?.content);
|
|
|
196
213
|
|
|
197
214
|
- Login first through the management page or `azt login`.
|
|
198
215
|
- A model appearing in `/v1/models` means the local Codex cache lists it. Final availability still depends on the active account.
|
|
199
|
-
- `stream=true` is supported for `/v1/chat/completions` through OpenAI-style SSE chunks.
|
|
216
|
+
- `stream=true` is supported for `/v1/chat/completions` through OpenAI-style SSE chunks. Codex passthrough streaming is isolated under `/codex/v1/responses`.
|
|
200
217
|
- `n > 1` is not supported for `/v1/chat/completions`.
|
|
201
218
|
- Tool/function calling is supported for common OpenAI-compatible clients, including OpenClaw, but exact upstream behavior still depends on the active Codex model and account.
|
|
202
219
|
- The default listener is `0.0.0.0:8787`, so local-network clients can call the gateway by using the machine IP.
|
package/docs/DESKTOP_RELEASE.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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.5 Release Notes
|
|
6
|
+
|
|
7
|
+
Version `2.0.5` adds Codex custom provider routing and finer account rotation controls:
|
|
8
|
+
|
|
9
|
+
- Settings-page Codex provider setup for writing or removing the AI Zero Token managed `~/.codex/config.toml` provider.
|
|
10
|
+
- Local and remote Codex gateway URL modes, including automatic normalization to `/codex/v1`.
|
|
11
|
+
- Dedicated `POST /codex/v1/responses` passthrough route for Codex CLI/Desktop Responses SSE traffic.
|
|
12
|
+
- Provider status reporting in the management console, including active provider, base URL, and config path.
|
|
13
|
+
- Auto-switch exclusion list for accounts that should not participate in automatic quota rotation.
|
|
14
|
+
- Safer settings persistence through normalized loads, deduplicated profile IDs, queued saves, and atomic writes.
|
|
15
|
+
|
|
5
16
|
## 2.0.4 Release Notes
|
|
6
17
|
|
|
7
18
|
Version `2.0.4` adds the macOS menu-bar account panel and OpenClaw compatibility work:
|
|
@@ -103,7 +114,7 @@ AI Zero Token Setup {version}.exe
|
|
|
103
114
|
AI Zero Token-{version}-win.zip
|
|
104
115
|
```
|
|
105
116
|
|
|
106
|
-
For `2.0.
|
|
117
|
+
For `2.0.5`, replace `{version}` with `2.0.5`.
|
|
107
118
|
|
|
108
119
|
Artifact purpose:
|
|
109
120
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-zero-token",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.5",
|
|
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",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as e,n as t,r as n,t as r}from"./jsx-runtime-DqpGtLhh.js";import{t as i}from"./refresh-cw-CAAH2rqe.js";import{w as a}from"./profiles-DMOjJORP.js";import{_ as o,p as s,r as c}from"./index-rgcJgVAu.js";var l=t(`settings-2`,[[`path`,{d:`M14 17H5`,key:`gfn3mx`}],[`path`,{d:`M19 7h-9`,key:`6i9tg`}],[`circle`,{cx:`17`,cy:`17`,r:`3`,key:`18b49y`}],[`circle`,{cx:`7`,cy:`7`,r:`3`,key:`dfmy0x`}]]),u=e(n(),1),d=r();function f(e){return{defaultModel:e.settings.defaultModel,proxyEnabled:e.settings.networkProxy.enabled,proxyUrl:e.settings.networkProxy.url,proxyNoProxy:e.settings.networkProxy.noProxy||`localhost,127.0.0.1,::1`,autoSwitchEnabled:e.settings.autoSwitch.enabled,quotaSyncConcurrency:String(e.settings.runtime?.quotaSyncConcurrency||16),serverPort:String(e.settings.server.port||8787)}}function p(e){let[t,n]=(0,u.useState)({defaultModel:``,proxyEnabled:!1,proxyUrl:``,proxyNoProxy:`localhost,127.0.0.1,::1`,autoSwitchEnabled:!1,quotaSyncConcurrency:`16`,serverPort:`8787`}),[r,p]=(0,u.useState)(!1);(0,u.useEffect)(()=>{!e.config||r||n(f(e.config))},[e.config,r]);function m(e){n(t=>({...t,...e})),p(!0)}async function h(n){let r=Number.parseInt(t.serverPort,10);if(!Number.isInteger(r)||r<1||r>65535){e.setStatus(`端口必须是 1 到 65535 之间的整数。`);return}let i=Number.parseInt(t.quotaSyncConcurrency,10);if(!Number.isInteger(i)||i<1||i>32){e.setStatus(`全局额度刷新并发数必须是 1 到 32 之间的整数。`);return}let o=n?.restart?`restart`:`settings`;e.setBusy(o);try{let o=await s(`/_gateway/admin/settings`,{method:`PUT`,headers:{"Content-Type":`application/json`},body:a({defaultModel:t.defaultModel,networkProxy:{enabled:t.proxyEnabled,url:t.proxyUrl,noProxy:t.proxyNoProxy},autoSwitch:{enabled:t.autoSwitchEnabled},runtime:{quotaSyncConcurrency:i},server:{port:r}})});e.setConfig(o),p(!1),n?.restart?(e.setStatus(`设置已保存,正在重启本地网关...`),await s(`/_gateway/admin/restart`,{method:`POST`}),e.setStatus(`本地网关正在重启,页面会自动恢复。`)):e.setStatus(`设置已保存。`)}catch(t){e.setStatus(c(t))}finally{e.setBusy(null)}}async function g(){e.setBusy(`proxy`);try{let n=await s(`/_gateway/admin/settings/proxy-test`,{method:`POST`,headers:{"Content-Type":`application/json`},body:a({networkProxy:{enabled:t.proxyEnabled,url:t.proxyUrl,noProxy:t.proxyNoProxy}})});e.setStatus(`代理测试通过: HTTP ${n.status},耗时 ${n.elapsedMs} ms。`)}catch(t){e.setStatus(`代理测试失败: ${c(t)}`)}finally{e.setBusy(null)}}async function _(){e.setBusy(`models`);try{await s(`/_gateway/models/refresh`,{method:`POST`}),await e.refreshConfig({silent:!0}),e.setStatus(`Codex 模型列表已同步。`)}catch(t){e.setStatus(c(t))}finally{e.setBusy(null)}}return(0,d.jsxs)(`section`,{className:`settings-page`,children:[(0,d.jsxs)(`div`,{className:`settings-page-head`,children:[(0,d.jsxs)(`div`,{children:[(0,d.jsxs)(`div`,{className:`settings-page-kicker`,children:[(0,d.jsx)(l,{size:14}),`系统设置`]}),(0,d.jsx)(`h2`,{children:`本地网关和运行策略`}),(0,d.jsx)(`p`,{children:`设置会保存到本地状态目录,CLI、桌面端和本地服务共享。`})]}),(0,d.jsx)(`div`,{className:`settings-page-actions`,children:(0,d.jsxs)(`button`,{className:`btn-secondary`,type:`button`,onClick:_,disabled:e.busy===`models`,children:[e.busy===`models`?(0,d.jsx)(o,{className:`spin`,size:16}):(0,d.jsx)(i,{size:16}),`同步 Codex 模型`]})})]}),(0,d.jsxs)(`div`,{className:`settings-grid`,children:[(0,d.jsxs)(`section`,{className:`settings-section`,children:[(0,d.jsx)(`h4`,{children:`模型`}),(0,d.jsxs)(`label`,{className:`field`,children:[(0,d.jsx)(`span`,{children:`默认文本模型`}),(0,d.jsx)(`select`,{className:`control`,value:t.defaultModel,onChange:e=>m({defaultModel:e.target.value}),children:(e.config?.models||[]).map(e=>(0,d.jsx)(`option`,{value:e.id,children:e.id},e.id))})]}),(0,d.jsxs)(`p`,{className:`hint`,children:[`模型列表来源:`,e.config?.modelCatalog.source||`-`,`,共 `,e.config?.modelCatalog.modelCount||0,` 个。`]})]}),(0,d.jsxs)(`section`,{className:`settings-section`,children:[(0,d.jsx)(`h4`,{children:`上游代理`}),(0,d.jsxs)(`label`,{className:`switch-line`,children:[(0,d.jsx)(`input`,{type:`checkbox`,checked:t.proxyEnabled,onChange:e=>m({proxyEnabled:e.target.checked})}),(0,d.jsx)(`span`,{children:`启用 OAuth、模型刷新和接口转发代理`})]}),(0,d.jsxs)(`label`,{className:`field`,children:[(0,d.jsx)(`span`,{children:`代理地址`}),(0,d.jsx)(`input`,{className:`input`,value:t.proxyUrl,onChange:e=>m({proxyUrl:e.target.value}),placeholder:`http://127.0.0.1:7890`})]}),(0,d.jsxs)(`label`,{className:`field`,children:[(0,d.jsx)(`span`,{children:`No Proxy`}),(0,d.jsx)(`input`,{className:`input`,value:t.proxyNoProxy,onChange:e=>m({proxyNoProxy:e.target.value})})]}),(0,d.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:g,disabled:e.busy===`proxy`,children:`测试代理`})]}),(0,d.jsxs)(`section`,{className:`settings-section`,children:[(0,d.jsx)(`h4`,{children:`端口`}),(0,d.jsxs)(`label`,{className:`field`,children:[(0,d.jsx)(`span`,{children:`网关端口`}),(0,d.jsx)(`input`,{className:`input`,inputMode:`numeric`,type:`number`,min:1,max:65535,value:t.serverPort,onChange:e=>m({serverPort:e.target.value})})]}),(0,d.jsx)(`p`,{className:`hint`,children:`修改后重启本地网关生效,桌面窗口不会退出。若端口被占用,启动时会自动顺延到下一个可用端口。`})]}),(0,d.jsxs)(`section`,{className:`settings-section`,children:[(0,d.jsx)(`h4`,{children:`账号运行策略`}),(0,d.jsxs)(`label`,{className:`switch-line`,children:[(0,d.jsx)(`input`,{type:`checkbox`,checked:t.autoSwitchEnabled,onChange:e=>m({autoSwitchEnabled:e.target.checked})}),(0,d.jsx)(`span`,{children:`当前 API 账号额度耗尽后自动切换到下一个仍有额度的账号`})]}),(0,d.jsxs)(`label`,{className:`field`,children:[(0,d.jsx)(`span`,{children:`全局额度刷新并发数`}),(0,d.jsx)(`input`,{className:`input`,inputMode:`numeric`,max:32,min:1,type:`number`,value:t.quotaSyncConcurrency,onChange:e=>m({quotaSyncConcurrency:e.target.value})})]}),(0,d.jsx)(`p`,{className:`hint`,children:`手动刷新全部账号额度时使用,默认 16。账号很多可以调高,遇到限流或失败增多时调低。`}),(0,d.jsx)(`p`,{className:`hint`,children:e.status})]}),(0,d.jsxs)(`section`,{className:`settings-section`,children:[(0,d.jsx)(`h4`,{children:`显示`}),(0,d.jsxs)(`label`,{className:`switch-line`,children:[(0,d.jsx)(`input`,{type:`checkbox`,checked:e.showEmails,onChange:t=>e.setShowEmails(t.target.checked)}),(0,d.jsx)(`span`,{children:`脱敏模式`})]}),(0,d.jsx)(`p`,{className:`hint`,children:`开启后账号邮箱将以脱敏形式展示。`})]})]}),(0,d.jsxs)(`div`,{className:`settings-page-actions settings-page-footer-actions`,children:[(0,d.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>void h(),disabled:e.busy===`settings`||e.busy===`restart`||!r,children:`保存设置`}),(0,d.jsx)(`button`,{className:`btn-primary`,type:`button`,onClick:()=>void h({restart:!0}),disabled:e.busy===`settings`||e.busy===`restart`||!r||!e.config?.restartSupported,children:`保存并重启网关`})]})]})}export{p as SettingsPage};
|