ai-zero-token 2.0.7 → 2.0.9
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 +17 -0
- package/admin-ui/dist/assets/accounts-D0XoMUO2.js +4 -0
- package/admin-ui/dist/assets/{docs-BO-aSEzh.js → docs-Ctyhx0QT.js} +1 -1
- package/admin-ui/dist/assets/{image-bed-Dql7Vqd9.js → image-bed-BZF7fike.js} +1 -1
- package/admin-ui/dist/assets/{index-CCiBaGwU.js → index-BM5N4YUY.js} +3 -3
- package/admin-ui/dist/assets/index-BgT1IdcO.css +1 -0
- package/admin-ui/dist/assets/{launch-DXLo-NIM.js → launch-DMZlZ2Eq.js} +1 -1
- package/admin-ui/dist/assets/{logs-Cwn8-rDu.js → logs-Cyn5SyNG.js} +1 -1
- package/admin-ui/dist/assets/{network-detect-vzWfL-Tz.js → network-detect-D_SP0lTT.js} +1 -1
- package/admin-ui/dist/assets/overview-CqmN2aqg.js +1 -0
- package/admin-ui/dist/assets/{profiles-C5SmQvju.js → profiles-iNTmJFRe.js} +1 -1
- package/admin-ui/dist/assets/settings-BFKavypz.js +8 -0
- package/admin-ui/dist/assets/{tester-BKoMSoCz.js → tester-9eNSYAOK.js} +2 -2
- package/admin-ui/dist/assets/usage-Bsdlw9XG.js +1 -0
- package/admin-ui/dist/index.html +3 -3
- package/dist/core/providers/openai-codex/chat.js +139 -8
- package/dist/core/providers/openai-codex/oauth.js +8 -4
- package/dist/core/services/auth-service.js +25 -3
- package/dist/core/services/usage-service.js +402 -31
- package/dist/core/store/codex-auth-store.js +82 -7
- package/dist/server/app.js +234 -14
- package/docs/DESKTOP_RELEASE.md +9 -0
- package/package.json +1 -1
- package/admin-ui/dist/assets/accounts-D3tsDc3k.js +0 -4
- package/admin-ui/dist/assets/index-C22_3Mxq.css +0 -1
- package/admin-ui/dist/assets/overview-B_yad8ge.js +0 -1
- package/admin-ui/dist/assets/settings-BdRWcKJb.js +0 -5
- package/admin-ui/dist/assets/usage-B-qQxXzQ.js +0 -1
package/dist/server/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
|
+
import { networkInterfaces } from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { Readable } from "node:stream";
|
|
6
7
|
import { promisify } from "node:util";
|
|
@@ -24,6 +25,7 @@ const adminUiIndexPath = path.join(adminUiDistDir, "index.html");
|
|
|
24
25
|
const BYTES_PER_MIB = 1024 * 1024;
|
|
25
26
|
const MAX_GATEWAY_REQUEST_LOGS = 100;
|
|
26
27
|
const MAX_CODEX_RESPONSE_PROFILE_BINDINGS = 5e3;
|
|
28
|
+
const CODEX_STREAM_DRAIN_AFTER_CLIENT_CLOSE_MS = 3e4;
|
|
27
29
|
const DEFAULT_ROUTE_BODY_LIMIT_BYTES = 128 * BYTES_PER_MIB;
|
|
28
30
|
const CODEX_COMPACT_BODY_LIMIT_BYTES = 256 * BYTES_PER_MIB;
|
|
29
31
|
const gunzipAsync = promisify(gunzip);
|
|
@@ -252,20 +254,47 @@ function isObjectRecord(value) {
|
|
|
252
254
|
function tokenNumber(value) {
|
|
253
255
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.trunc(value) : null;
|
|
254
256
|
}
|
|
257
|
+
function sumTokenNumbers(value, keys) {
|
|
258
|
+
if (!value) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
let total = 0;
|
|
262
|
+
let seen = false;
|
|
263
|
+
for (const key of keys) {
|
|
264
|
+
const item = tokenNumber(value[key]);
|
|
265
|
+
if (item !== null) {
|
|
266
|
+
total += item;
|
|
267
|
+
seen = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return seen ? total : null;
|
|
271
|
+
}
|
|
255
272
|
function normalizeTokenUsage(value) {
|
|
256
273
|
if (!isObjectRecord(value)) {
|
|
257
274
|
return null;
|
|
258
275
|
}
|
|
259
276
|
const inputTokens = tokenNumber(value.input_tokens ?? value.prompt_tokens);
|
|
260
277
|
const outputTokens = tokenNumber(value.output_tokens ?? value.completion_tokens);
|
|
261
|
-
const
|
|
262
|
-
|
|
278
|
+
const inputDetails = isObjectRecord(value.input_tokens_details) ? value.input_tokens_details : null;
|
|
279
|
+
const promptDetails = isObjectRecord(value.prompt_tokens_details) ? value.prompt_tokens_details : null;
|
|
280
|
+
const cacheCreation = isObjectRecord(value.cache_creation) ? value.cache_creation : null;
|
|
281
|
+
const openAiCachedTokens = tokenNumber(inputDetails?.cached_tokens ?? promptDetails?.cached_tokens);
|
|
282
|
+
const cacheReadTokens = openAiCachedTokens ?? tokenNumber(value.cache_read_input_tokens ?? value.cached_tokens);
|
|
283
|
+
const cacheCreationTokens = tokenNumber(value.cache_creation_input_tokens ?? value.cache_creation_tokens) ?? tokenNumber(inputDetails?.cache_creation_tokens ?? promptDetails?.cache_creation_tokens) ?? sumTokenNumbers(cacheCreation, ["ephemeral_5m_input_tokens", "ephemeral_1h_input_tokens"]);
|
|
284
|
+
const inputIncludesCacheRead = openAiCachedTokens !== null;
|
|
285
|
+
const inferredTotalTokens = inputTokens !== null || outputTokens !== null || cacheReadTokens !== null || cacheCreationTokens !== null ? (inputTokens ?? 0) + (outputTokens ?? 0) + (inputIncludesCacheRead ? 0 : cacheReadTokens ?? 0) + (cacheCreationTokens ?? 0) : null;
|
|
286
|
+
const totalTokens = tokenNumber(value.total_tokens) ?? inferredTotalTokens;
|
|
287
|
+
const uncachedInputTokens = inputTokens !== null ? inputIncludesCacheRead ? Math.max(0, inputTokens - (cacheReadTokens ?? 0)) : inputTokens : null;
|
|
288
|
+
if (inputTokens === null && outputTokens === null && totalTokens === null && cacheReadTokens === null && cacheCreationTokens === null) {
|
|
263
289
|
return null;
|
|
264
290
|
}
|
|
265
291
|
return {
|
|
266
292
|
inputTokens,
|
|
293
|
+
uncachedInputTokens,
|
|
267
294
|
outputTokens,
|
|
268
|
-
totalTokens
|
|
295
|
+
totalTokens,
|
|
296
|
+
cacheCreationTokens,
|
|
297
|
+
cacheReadTokens
|
|
269
298
|
};
|
|
270
299
|
}
|
|
271
300
|
function extractTokenUsage(value, depth = 0) {
|
|
@@ -306,6 +335,40 @@ function imageUsageToTokenUsage(usage) {
|
|
|
306
335
|
totalTokens: usage.total_tokens
|
|
307
336
|
};
|
|
308
337
|
}
|
|
338
|
+
function buildResponsesUsagePayload(usage) {
|
|
339
|
+
if (!usage) {
|
|
340
|
+
return void 0;
|
|
341
|
+
}
|
|
342
|
+
const inputTokens = tokenNumber(usage.inputTokens) ?? 0;
|
|
343
|
+
const outputTokens = tokenNumber(usage.outputTokens) ?? 0;
|
|
344
|
+
const totalTokens = tokenNumber(usage.totalTokens) ?? inputTokens + outputTokens;
|
|
345
|
+
const cacheReadTokens = tokenNumber(usage.cacheReadTokens);
|
|
346
|
+
const cacheCreationTokens = tokenNumber(usage.cacheCreationTokens);
|
|
347
|
+
return {
|
|
348
|
+
input_tokens: inputTokens,
|
|
349
|
+
output_tokens: outputTokens,
|
|
350
|
+
total_tokens: totalTokens,
|
|
351
|
+
...cacheReadTokens !== null ? { input_tokens_details: { cached_tokens: cacheReadTokens } } : {},
|
|
352
|
+
...cacheCreationTokens !== null ? { cache_creation_input_tokens: cacheCreationTokens } : {}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function buildChatCompletionsUsagePayload(usage) {
|
|
356
|
+
if (!usage) {
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
const promptTokens = tokenNumber(usage.inputTokens) ?? 0;
|
|
360
|
+
const completionTokens = tokenNumber(usage.outputTokens) ?? 0;
|
|
361
|
+
const totalTokens = tokenNumber(usage.totalTokens) ?? promptTokens + completionTokens;
|
|
362
|
+
const cacheReadTokens = tokenNumber(usage.cacheReadTokens);
|
|
363
|
+
const cacheCreationTokens = tokenNumber(usage.cacheCreationTokens);
|
|
364
|
+
return {
|
|
365
|
+
prompt_tokens: promptTokens,
|
|
366
|
+
completion_tokens: completionTokens,
|
|
367
|
+
total_tokens: totalTokens,
|
|
368
|
+
...cacheReadTokens !== null ? { prompt_tokens_details: { cached_tokens: cacheReadTokens } } : {},
|
|
369
|
+
...cacheCreationTokens !== null ? { cache_creation_input_tokens: cacheCreationTokens } : {}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
309
372
|
function extractUsageErrorType(details, statusCode) {
|
|
310
373
|
const error = isObjectRecord(details?.error) ? details.error : null;
|
|
311
374
|
const upstreamErrorCode = error?.upstreamErrorCode;
|
|
@@ -792,6 +855,7 @@ function summarizeCodexChatBody(body) {
|
|
|
792
855
|
model: body.model ?? "default",
|
|
793
856
|
stream: body.stream,
|
|
794
857
|
store: body.store,
|
|
858
|
+
hasPromptCacheKey: typeof body.prompt_cache_key === "string" && body.prompt_cache_key.trim().length > 0,
|
|
795
859
|
inputItems: Array.isArray(body.input) ? body.input.length : void 0,
|
|
796
860
|
tools: Array.isArray(body.tools) ? body.tools.length : void 0,
|
|
797
861
|
toolNames: toolNames.slice(0, 50),
|
|
@@ -932,10 +996,12 @@ function summarizeImageEditRequestForLog(body) {
|
|
|
932
996
|
};
|
|
933
997
|
}
|
|
934
998
|
function buildResponseApiBody(result, includeRaw) {
|
|
999
|
+
const usage = buildResponsesUsagePayload(extractTokenUsage(result.raw));
|
|
935
1000
|
const responseBody = {
|
|
936
1001
|
object: "response",
|
|
937
1002
|
provider: result.provider,
|
|
938
1003
|
model: result.model,
|
|
1004
|
+
...usage ? { usage } : {},
|
|
939
1005
|
output_text: result.text,
|
|
940
1006
|
output: [
|
|
941
1007
|
{
|
|
@@ -960,11 +1026,13 @@ function buildResponseApiBody(result, includeRaw) {
|
|
|
960
1026
|
}
|
|
961
1027
|
function buildChatCompletionsBody(result) {
|
|
962
1028
|
const hasToolCalls = result.toolCalls.length > 0;
|
|
1029
|
+
const usage = buildChatCompletionsUsagePayload(extractTokenUsage(result.raw));
|
|
963
1030
|
const body = {
|
|
964
1031
|
id: `chatcmpl_${randomUUID().replace(/-/g, "")}`,
|
|
965
1032
|
object: "chat.completion",
|
|
966
1033
|
created: Math.floor(Date.now() / 1e3),
|
|
967
1034
|
model: result.model,
|
|
1035
|
+
...usage ? { usage } : {},
|
|
968
1036
|
choices: [
|
|
969
1037
|
{
|
|
970
1038
|
index: 0,
|
|
@@ -1002,7 +1070,7 @@ function buildChatCompletionChunk(params) {
|
|
|
1002
1070
|
]
|
|
1003
1071
|
};
|
|
1004
1072
|
}
|
|
1005
|
-
function sendChatCompletionsStream(reply, result) {
|
|
1073
|
+
function sendChatCompletionsStream(reply, result, includeUsage = false) {
|
|
1006
1074
|
const id = `chatcmpl_${randomUUID().replace(/-/g, "")}`;
|
|
1007
1075
|
const created = Math.floor(Date.now() / 1e3);
|
|
1008
1076
|
reply.raw.writeHead(200, {
|
|
@@ -1052,6 +1120,17 @@ function sendChatCompletionsStream(reply, result) {
|
|
|
1052
1120
|
delta: {},
|
|
1053
1121
|
finishReason: result.toolCalls.length > 0 ? "tool_calls" : "stop"
|
|
1054
1122
|
}));
|
|
1123
|
+
const usage = includeUsage ? buildChatCompletionsUsagePayload(extractTokenUsage(result.raw)) : void 0;
|
|
1124
|
+
if (usage) {
|
|
1125
|
+
writeChatCompletionsSseEvent(reply, {
|
|
1126
|
+
id,
|
|
1127
|
+
object: "chat.completion.chunk",
|
|
1128
|
+
created,
|
|
1129
|
+
model: result.model,
|
|
1130
|
+
choices: [],
|
|
1131
|
+
usage
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1055
1134
|
reply.raw.write("data: [DONE]\n\n");
|
|
1056
1135
|
reply.raw.end();
|
|
1057
1136
|
}
|
|
@@ -1136,6 +1215,57 @@ function resolveOrigin(request) {
|
|
|
1136
1215
|
}
|
|
1137
1216
|
return "http://127.0.0.1:8787";
|
|
1138
1217
|
}
|
|
1218
|
+
function isLoopbackHost(host) {
|
|
1219
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
1220
|
+
}
|
|
1221
|
+
function isPrivateIpv4(address) {
|
|
1222
|
+
if (address.startsWith("10.")) {
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1225
|
+
if (address.startsWith("192.168.")) {
|
|
1226
|
+
return true;
|
|
1227
|
+
}
|
|
1228
|
+
const match = address.match(/^172\.(\d+)\./);
|
|
1229
|
+
if (!match) {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
const second = Number.parseInt(match[1] ?? "", 10);
|
|
1233
|
+
return second >= 16 && second <= 31;
|
|
1234
|
+
}
|
|
1235
|
+
function getLanIpv4Addresses() {
|
|
1236
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1237
|
+
const addresses = [];
|
|
1238
|
+
const interfaces = networkInterfaces();
|
|
1239
|
+
for (const [name, details] of Object.entries(interfaces)) {
|
|
1240
|
+
for (const detail of details ?? []) {
|
|
1241
|
+
const family = String(detail.family);
|
|
1242
|
+
const isIpv4 = family === "IPv4" || family === "4";
|
|
1243
|
+
if (!isIpv4 || detail.internal || seen.has(detail.address)) {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
if (detail.address === "0.0.0.0" || detail.address.startsWith("127.") || detail.address.startsWith("169.254.")) {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
seen.add(detail.address);
|
|
1250
|
+
addresses.push({
|
|
1251
|
+
address: detail.address,
|
|
1252
|
+
label: name,
|
|
1253
|
+
private: isPrivateIpv4(detail.address)
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return addresses.sort((left, right) => Number(right.private) - Number(left.private) || left.address.localeCompare(right.address, "en")).map(({ address, label }) => ({ address, label }));
|
|
1258
|
+
}
|
|
1259
|
+
function createShareAddress(protocol, host, port, label) {
|
|
1260
|
+
const origin = `${protocol}://${host}:${port}`;
|
|
1261
|
+
return {
|
|
1262
|
+
host,
|
|
1263
|
+
label,
|
|
1264
|
+
adminUrl: `${origin}/`,
|
|
1265
|
+
baseUrl: `${origin}/v1`,
|
|
1266
|
+
codexBaseUrl: `${origin}/codex/v1`
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1139
1269
|
function normalizeError(error) {
|
|
1140
1270
|
return error instanceof Error ? error : new Error(String(error));
|
|
1141
1271
|
}
|
|
@@ -1169,7 +1299,8 @@ function createSseStreamStats() {
|
|
|
1169
1299
|
bytes: 0,
|
|
1170
1300
|
completed: false,
|
|
1171
1301
|
responseIds: /* @__PURE__ */ new Set(),
|
|
1172
|
-
tokenUsage: null
|
|
1302
|
+
tokenUsage: null,
|
|
1303
|
+
parseErrorCount: 0
|
|
1173
1304
|
};
|
|
1174
1305
|
}
|
|
1175
1306
|
function extractSseResponseId(value) {
|
|
@@ -1186,6 +1317,9 @@ function extractSseResponseId(value) {
|
|
|
1186
1317
|
}
|
|
1187
1318
|
return void 0;
|
|
1188
1319
|
}
|
|
1320
|
+
function isSseTerminalUsageEvent(eventType) {
|
|
1321
|
+
return eventType === "response.completed" || eventType === "response.done" || eventType === "response.failed" || eventType === "response.incomplete";
|
|
1322
|
+
}
|
|
1189
1323
|
function trackSseChunk(stats, chunk) {
|
|
1190
1324
|
const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? Buffer.from(chunk).toString("utf8") : String(chunk);
|
|
1191
1325
|
stats.bytes += Buffer.byteLength(text);
|
|
@@ -1207,14 +1341,15 @@ function trackSseChunk(stats, chunk) {
|
|
|
1207
1341
|
if (responseId) {
|
|
1208
1342
|
stats.responseIds.add(responseId);
|
|
1209
1343
|
}
|
|
1210
|
-
const tokenUsage = extractTokenUsage(parsed);
|
|
1344
|
+
const tokenUsage = isSseTerminalUsageEvent(eventType) ? extractTokenUsage(parsed) : null;
|
|
1211
1345
|
if (tokenUsage) {
|
|
1212
1346
|
stats.tokenUsage = tokenUsage;
|
|
1213
1347
|
}
|
|
1214
1348
|
} catch {
|
|
1349
|
+
stats.parseErrorCount += 1;
|
|
1215
1350
|
}
|
|
1216
1351
|
}
|
|
1217
|
-
if (eventType === "response.completed") {
|
|
1352
|
+
if (eventType === "response.completed" || eventType === "response.done") {
|
|
1218
1353
|
stats.completed = true;
|
|
1219
1354
|
stats.terminalEvent = eventType;
|
|
1220
1355
|
} else if (eventType === "response.failed" || eventType === "response.incomplete") {
|
|
@@ -1226,6 +1361,24 @@ function trackSseChunk(stats, chunk) {
|
|
|
1226
1361
|
stats.buffer = stats.buffer.slice(-65536);
|
|
1227
1362
|
}
|
|
1228
1363
|
}
|
|
1364
|
+
function sseTokenUsageStatus(stats, statusCode) {
|
|
1365
|
+
if (stats.tokenUsage) {
|
|
1366
|
+
return "captured";
|
|
1367
|
+
}
|
|
1368
|
+
if (statusCode < 200 || statusCode >= 400) {
|
|
1369
|
+
return "upstream_error";
|
|
1370
|
+
}
|
|
1371
|
+
if (stats.parseErrorCount > 0 && !stats.terminalEvent) {
|
|
1372
|
+
return "parse_failed";
|
|
1373
|
+
}
|
|
1374
|
+
if (!stats.terminalEvent) {
|
|
1375
|
+
return "missing_terminal";
|
|
1376
|
+
}
|
|
1377
|
+
if (isSseTerminalUsageEvent(stats.terminalEvent)) {
|
|
1378
|
+
return "terminal_without_usage";
|
|
1379
|
+
}
|
|
1380
|
+
return "not_returned";
|
|
1381
|
+
}
|
|
1229
1382
|
function createApp(params) {
|
|
1230
1383
|
const defaultBodyLimit = params?.bodyLimit ?? DEFAULT_ROUTE_BODY_LIMIT_BYTES;
|
|
1231
1384
|
const codexCompactBodyLimit = Math.max(defaultBodyLimit, CODEX_COMPACT_BODY_LIMIT_BYTES);
|
|
@@ -1292,6 +1445,7 @@ function createApp(params) {
|
|
|
1292
1445
|
accountLabel: entry.account,
|
|
1293
1446
|
planType: profile?.quota?.planType,
|
|
1294
1447
|
tokenUsage: log.usage?.tokenUsage,
|
|
1448
|
+
tokenUsageStatus: log.usage?.tokenUsageStatus,
|
|
1295
1449
|
imageCount: log.usage?.imageCount,
|
|
1296
1450
|
imageRoute: log.usage?.imageRoute ?? "none",
|
|
1297
1451
|
errorType: log.usage?.errorType ?? extractUsageErrorType(log.details, entry.statusCode)
|
|
@@ -1330,6 +1484,7 @@ function createApp(params) {
|
|
|
1330
1484
|
data: gatewayRequestLogs
|
|
1331
1485
|
}));
|
|
1332
1486
|
app.get("/_gateway/admin/usage", async () => ctx.usageService.getSummary());
|
|
1487
|
+
app.post("/_gateway/admin/usage/reset", async () => ctx.usageService.backupAndReset());
|
|
1333
1488
|
async function buildAdminConfig(request) {
|
|
1334
1489
|
const [status, models, modelCatalog, versionStatus, settings, profile, profiles, codexStatus, usage] = await Promise.all([
|
|
1335
1490
|
ctx.authService.getStatus(),
|
|
@@ -1469,6 +1624,26 @@ function createApp(params) {
|
|
|
1469
1624
|
};
|
|
1470
1625
|
});
|
|
1471
1626
|
app.get("/_gateway/admin/config", async (request) => buildAdminConfig(request));
|
|
1627
|
+
app.get("/_gateway/admin/share", async (request) => {
|
|
1628
|
+
const status = await ctx.authService.getStatus();
|
|
1629
|
+
const protocol = request.protocol === "https" ? "https" : "http";
|
|
1630
|
+
const port = request.raw.socket.localPort || status.serverPort;
|
|
1631
|
+
const serverHost = status.serverHost || "0.0.0.0";
|
|
1632
|
+
const lanReachable = serverHost === "0.0.0.0" || serverHost === "::" || !isLoopbackHost(serverHost);
|
|
1633
|
+
const addresses = getLanIpv4Addresses().map((item) => createShareAddress(protocol, item.address, port, item.label));
|
|
1634
|
+
const requestHost = request.headers.host?.replace(/:\d+$/u, "");
|
|
1635
|
+
if (requestHost && !isLoopbackHost(requestHost) && !addresses.some((item) => item.host === requestHost)) {
|
|
1636
|
+
addresses.unshift(createShareAddress(protocol, requestHost, port, "\u5F53\u524D\u8BBF\u95EE\u5730\u5740"));
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
primary: lanReachable ? addresses[0] ?? null : null,
|
|
1640
|
+
addresses,
|
|
1641
|
+
local: createShareAddress(protocol, "127.0.0.1", port, "\u672C\u673A"),
|
|
1642
|
+
serverHost,
|
|
1643
|
+
serverPort: port,
|
|
1644
|
+
lanReachable
|
|
1645
|
+
};
|
|
1646
|
+
});
|
|
1472
1647
|
app.post("/_gateway/admin/login", async (request) => {
|
|
1473
1648
|
await ctx.authService.login("openai-codex");
|
|
1474
1649
|
await ctx.authService.syncActiveProfileQuota("openai-codex", {
|
|
@@ -1841,6 +2016,8 @@ function createApp(params) {
|
|
|
1841
2016
|
const abortController = new AbortController();
|
|
1842
2017
|
let streamFinished = false;
|
|
1843
2018
|
let headersCommitted = false;
|
|
2019
|
+
let clientDisconnected = false;
|
|
2020
|
+
let clientDrainTimer = null;
|
|
1844
2021
|
let profile = null;
|
|
1845
2022
|
let retryCount = 0;
|
|
1846
2023
|
let failureRecorded = false;
|
|
@@ -1850,7 +2027,15 @@ function createApp(params) {
|
|
|
1850
2027
|
let adventureFallbackReason;
|
|
1851
2028
|
reply.raw.on("close", () => {
|
|
1852
2029
|
if (!streamFinished) {
|
|
1853
|
-
|
|
2030
|
+
clientDisconnected = true;
|
|
2031
|
+
if (!headersCommitted) {
|
|
2032
|
+
abortController.abort();
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
clientDrainTimer = setTimeout(() => {
|
|
2036
|
+
abortController.abort();
|
|
2037
|
+
}, CODEX_STREAM_DRAIN_AFTER_CLIENT_CLOSE_MS);
|
|
2038
|
+
clientDrainTimer.unref?.();
|
|
1854
2039
|
}
|
|
1855
2040
|
});
|
|
1856
2041
|
try {
|
|
@@ -2044,14 +2229,39 @@ function createApp(params) {
|
|
|
2044
2229
|
headersCommitted = true;
|
|
2045
2230
|
reply.raw.flushHeaders?.();
|
|
2046
2231
|
const streamStats = createSseStreamStats();
|
|
2232
|
+
const writeChunkToClient = async (chunk) => {
|
|
2233
|
+
if (clientDisconnected || reply.raw.destroyed || reply.raw.writableEnded) {
|
|
2234
|
+
clientDisconnected = true;
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
try {
|
|
2238
|
+
if (!reply.raw.write(chunk)) {
|
|
2239
|
+
await new Promise((resolve) => {
|
|
2240
|
+
const cleanup = () => {
|
|
2241
|
+
reply.raw.off("drain", cleanup);
|
|
2242
|
+
reply.raw.off("close", cleanup);
|
|
2243
|
+
resolve();
|
|
2244
|
+
};
|
|
2245
|
+
reply.raw.once("drain", cleanup);
|
|
2246
|
+
reply.raw.once("close", cleanup);
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
} catch {
|
|
2250
|
+
clientDisconnected = true;
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2047
2253
|
for await (const chunk of Readable.fromWeb(upstream.body)) {
|
|
2048
2254
|
trackSseChunk(streamStats, chunk);
|
|
2049
|
-
|
|
2050
|
-
await new Promise((resolve) => reply.raw.once("drain", resolve));
|
|
2051
|
-
}
|
|
2255
|
+
await writeChunkToClient(chunk);
|
|
2052
2256
|
}
|
|
2053
2257
|
streamFinished = true;
|
|
2054
|
-
|
|
2258
|
+
if (clientDrainTimer) {
|
|
2259
|
+
clearTimeout(clientDrainTimer);
|
|
2260
|
+
clientDrainTimer = null;
|
|
2261
|
+
}
|
|
2262
|
+
if (!clientDisconnected && !reply.raw.destroyed && !reply.raw.writableEnded) {
|
|
2263
|
+
reply.raw.end();
|
|
2264
|
+
}
|
|
2055
2265
|
for (const responseId of streamStats.responseIds) {
|
|
2056
2266
|
rememberCodexResponseProfile(responseId, profile);
|
|
2057
2267
|
}
|
|
@@ -2093,17 +2303,25 @@ function createApp(params) {
|
|
|
2093
2303
|
completed: streamStats.completed,
|
|
2094
2304
|
terminalEvent: streamStats.terminalEvent,
|
|
2095
2305
|
bytes: streamStats.bytes,
|
|
2096
|
-
usageCaptured: Boolean(streamStats.tokenUsage)
|
|
2306
|
+
usageCaptured: Boolean(streamStats.tokenUsage),
|
|
2307
|
+
tokenUsageStatus: sseTokenUsageStatus(streamStats, upstream.status),
|
|
2308
|
+
parseErrorCount: streamStats.parseErrorCount,
|
|
2309
|
+
clientDisconnected
|
|
2097
2310
|
}
|
|
2098
2311
|
},
|
|
2099
2312
|
usage: {
|
|
2100
2313
|
profile,
|
|
2101
2314
|
tokenUsage: streamStats.tokenUsage,
|
|
2315
|
+
tokenUsageStatus: sseTokenUsageStatus(streamStats, upstream.status),
|
|
2102
2316
|
imageRoute: codexImageRoute
|
|
2103
2317
|
}
|
|
2104
2318
|
});
|
|
2105
2319
|
return reply;
|
|
2106
2320
|
} catch (error) {
|
|
2321
|
+
if (clientDrainTimer) {
|
|
2322
|
+
clearTimeout(clientDrainTimer);
|
|
2323
|
+
clientDrainTimer = null;
|
|
2324
|
+
}
|
|
2107
2325
|
const quota = error.quota;
|
|
2108
2326
|
if (profile && !failureRecorded) {
|
|
2109
2327
|
await ctx.authService.recordProfileRequestFailure(profile.profileId, error, quota, "openai-codex", {
|
|
@@ -2532,7 +2750,9 @@ function createApp(params) {
|
|
|
2532
2750
|
artifactCount: result.artifacts.length
|
|
2533
2751
|
});
|
|
2534
2752
|
if (parsed.data.stream) {
|
|
2535
|
-
|
|
2753
|
+
const rawStreamOptions = parsed.data.stream_options;
|
|
2754
|
+
const streamOptions = isObjectRecord(rawStreamOptions) ? rawStreamOptions : null;
|
|
2755
|
+
sendChatCompletionsStream(reply, result, streamOptions?.include_usage === true);
|
|
2536
2756
|
return reply;
|
|
2537
2757
|
}
|
|
2538
2758
|
return buildChatCompletionsBody(result);
|
package/docs/DESKTOP_RELEASE.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
This project ships the desktop app with Electron. The desktop main process starts the existing local Fastify gateway and loads the React management UI served by that gateway.
|
|
4
4
|
|
|
5
|
+
## 2.0.9 Release Notes
|
|
6
|
+
|
|
7
|
+
Version `2.0.9` clarifies automatic account rotation eligibility and tightens Codex auth refresh handling:
|
|
8
|
+
|
|
9
|
+
- Settings now separates manually excluded accounts from accounts that are runtime-ineligible because login is unavailable or quota is exhausted.
|
|
10
|
+
- Account filters and stats distinguish configured rotation participation from the actual automatic rotation candidate pool.
|
|
11
|
+
- Refreshed Codex tokens preserve account identity metadata when upstream token payloads omit profile claims.
|
|
12
|
+
- Saved profiles validate `id_token` expiry before Codex image and web flows use them, with clearer recovery messaging when a fresh `id_token` is unavailable.
|
|
13
|
+
|
|
5
14
|
## 2.0.6 Release Notes
|
|
6
15
|
|
|
7
16
|
Version `2.0.6` adds Free-account image routing controls and local usage/account statistics:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-zero-token",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
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,4 +0,0 @@
|
|
|
1
|
-
import{a as e,r as t,t as n}from"./jsx-runtime-DqpGtLhh.js";import{t as r}from"./earth-DFdZaQIi.js";import{t as i}from"./refresh-cw-CAAH2rqe.js";import{t as a}from"./search-B2hz41D3.js";import{C as o,_ as s,a as c,b as l,d as u,f as d,g as f,h as p,i as m,m as h,n as g,o as _,p as v,r as y,s as b,t as x,u as S,v as C,w,y as T}from"./profiles-C5SmQvju.js";import{_ as E,d as D,p as O,r as k,x as A}from"./index-CCiBaGwU.js";import{t as j}from"./InfoRow-0ULI9iI3.js";var M=e(t(),1),N=n();function P(e){let t=e.config?.codex?.accountId,n=e.profiles.length<=0?``:e.profiles.length===1?`profile-count-1`:e.profiles.length===2?`profile-count-2`:e.profiles.length===3?`profile-count-3`:`profile-count-many`;return(0,N.jsxs)(`section`,{className:`card`,id:`accounts`,children:[(0,N.jsxs)(`div`,{className:`section-head`,children:[(0,N.jsxs)(`div`,{children:[(0,N.jsx)(`h2`,{children:`账号额度预览`}),(0,N.jsx)(`p`,{children:`账号信息采用卡片式布局展示,支持搜索、状态筛选和额度排序。`})]}),(0,N.jsxs)(`div`,{className:`section-actions`,children:[(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onLocate,children:`定位当前账号`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onExportSelected,children:`导出所选`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onSelectVisible,disabled:e.visibleCount===0,children:`全选筛选结果`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onClearSelected,disabled:e.selectedCount===0,children:`取消选择`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:e.onRemoveSelected,disabled:e.selectedCount===0||e.busy===`bulk-remove`,children:`删除所选`}),(0,N.jsx)(`button`,{className:`btn-primary`,type:`button`,onClick:e.onAddAccount,children:`新增账号`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:e.onRefreshStatus,children:`刷新状态`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:e.onClearAccounts,children:`清空账号`})]})]}),(0,N.jsx)(`div`,{className:`account-stat-strip`,"aria-label":`账号池统计`,children:e.accountStats.map(t=>(0,N.jsxs)(`button`,{className:`account-stat-pill tone-${t.tone} ${e.filter.status===t.key?`is-active`:``}`,type:`button`,onClick:()=>e.onFilter({...e.filter,status:t.key}),children:[(0,N.jsx)(`span`,{children:t.label}),(0,N.jsx)(`strong`,{children:t.value})]},t.key))}),(0,N.jsxs)(`div`,{className:`filter-row`,children:[(0,N.jsxs)(`label`,{className:`search-box`,children:[(0,N.jsx)(a,{size:16}),(0,N.jsx)(`input`,{value:e.filter.search,onChange:t=>e.onFilter({...e.filter,search:t.target.value}),placeholder:`搜索邮箱、账号 ID 或 Profile ID`})]}),(0,N.jsxs)(`select`,{className:`control`,value:e.filter.status,onChange:t=>e.onFilter({...e.filter,status:t.target.value}),children:[(0,N.jsx)(`option`,{value:`all`,children:`全部状态`}),(0,N.jsx)(`option`,{value:`available`,children:`可用`}),(0,N.jsx)(`option`,{value:`unavailable`,children:`不可用`}),(0,N.jsx)(`option`,{value:`active`,children:`使用中`}),(0,N.jsx)(`option`,{value:`api-active`,children:`API 使用中`}),(0,N.jsx)(`option`,{value:`codex-active`,children:`Codex 使用中`}),(0,N.jsx)(`option`,{value:`healthy`,children:`健康`}),(0,N.jsx)(`option`,{value:`warning`,children:`即将耗尽`}),(0,N.jsx)(`option`,{value:`unknown`,children:`待请求验证`}),(0,N.jsx)(`option`,{value:`exhausted`,children:`额度耗尽`}),(0,N.jsx)(`option`,{value:`invalid`,children:`登录/认证异常`}),(0,N.jsx)(`option`,{value:`login-invalid`,children:`登录失效`}),(0,N.jsx)(`option`,{value:`auth-error`,children:`认证异常`}),(0,N.jsx)(`option`,{value:`expired`,children:`已过期`}),(0,N.jsx)(`option`,{value:`free`,children:`Free`}),(0,N.jsx)(`option`,{value:`plus`,children:`Plus`}),(0,N.jsx)(`option`,{value:`pro-team`,children:`Pro/Team`}),(0,N.jsx)(`option`,{value:`auto-included`,children:`参与轮换`}),(0,N.jsx)(`option`,{value:`auto-excluded`,children:`排除轮换`})]}),(0,N.jsxs)(`select`,{className:`control`,value:e.filter.sort,onChange:t=>e.onFilter({...e.filter,sort:t.target.value}),children:[(0,N.jsx)(`option`,{value:`quota-desc`,children:`默认排序`}),(0,N.jsx)(`option`,{value:`latency-asc`,children:`按额度更新时间`}),(0,N.jsx)(`option`,{value:`expiry-asc`,children:`按过期时间`}),(0,N.jsx)(`option`,{value:`name-asc`,children:`按名称排序`}),(0,N.jsx)(`option`,{value:`quota-asc`,children:`按剩余额度升序`}),(0,N.jsx)(`option`,{value:`plan-desc`,children:`按套餐排序`}),(0,N.jsx)(`option`,{value:`email-asc`,children:`按邮箱排序`})]}),(0,N.jsxs)(`span`,{className:`account-selected-count`,children:[`已选择 `,e.selectedCount,` 个`]})]}),(0,N.jsx)(`div`,{className:`account-grid ${n}`,children:e.profiles.length===0?(0,N.jsx)(`div`,{className:`empty-state`,children:`还没有匹配的账号。可以新增账号或调整筛选条件。`}):e.profiles.map(n=>{let a=d(n),o=u(n),p=T(n),y=!!e.expandedProfiles[n.profileId],b=!!(t&&n.accountId===t),S=l(n,b),w=_(n),D=c(n),O=n.exportAudit,k=O?.exported?`已导出 ${O.count} 次`:`未导出`,M=typeof e.busy==`string`&&e.busy.startsWith(`profile:`)&&e.busy.endsWith(n.profileId),P=e.busy===`profile:sync-quota:${n.profileId}`;return(0,N.jsxs)(`article`,{className:`account-card plan-${g(n)} ${w?`is-auth-invalid`:``}`,"data-profile-card":n.profileId,title:w?x(n):void 0,children:[S&&(0,N.jsx)(`span`,{className:`usage-corner ${S.className}`,children:(0,N.jsx)(`span`,{children:S.label})}),(0,N.jsxs)(`div`,{className:`account-head`,children:[(0,N.jsxs)(`div`,{className:`account-title`,children:[(0,N.jsxs)(`div`,{className:`account-name`,children:[(0,N.jsx)(`span`,{className:`avatar`,children:v(n)}),(0,N.jsx)(`strong`,{children:h(n,e.showEmails)}),(0,N.jsx)(`button`,{"aria-label":`刷新额度`,className:`account-icon-btn`,disabled:M,onClick:()=>e.onAction(`sync-quota`,n),title:`刷新额度`,type:`button`,children:P?(0,N.jsx)(E,{className:`spin`,size:14}):(0,N.jsx)(i,{size:14})})]}),(0,N.jsxs)(`div`,{className:`badge-row`,children:[(0,N.jsx)(`span`,{className:`badge brand`,children:m(n)}),(0,N.jsx)(`span`,{className:`badge ${a.tone}`,children:a.label}),(0,N.jsx)(`span`,{className:`badge ${D.ok?`green`:`orange`}`,children:`gpt-image-2`}),(0,N.jsx)(`span`,{className:`badge ${O?.exported?`orange`:`muted`}`,children:k})]})]}),(0,N.jsxs)(`label`,{className:`account-select`,children:[(0,N.jsx)(`input`,{type:`checkbox`,checked:!!e.selectedProfiles[n.profileId],onChange:t=>e.onSelect(n.profileId,t.target.checked)}),(0,N.jsx)(`span`,{children:`选择`})]})]}),(0,N.jsxs)(`div`,{className:`account-metrics`,children:[(0,N.jsx)(L,{label:s(n,`primary`),value:o,tone:f(o)}),(0,N.jsx)(L,{label:s(n,`secondary`),value:p,tone:f(p)})]}),(0,N.jsxs)(`div`,{className:`usage-status-row`,children:[(0,N.jsxs)(`span`,{className:`usage-status ${n.isActive?`is-active`:``}`,children:[(0,N.jsx)(r,{size:14}),(0,N.jsx)(`span`,{children:`API`}),(0,N.jsx)(`span`,{className:`usage-dot ${n.isActive?`active`:``}`}),(0,N.jsx)(`span`,{className:`usage-state-text`,children:n.isActive?`使用中`:`未使用`})]}),(0,N.jsxs)(`span`,{className:`usage-status ${b?`is-active`:``}`,children:[(0,N.jsx)(A,{size:14}),(0,N.jsx)(`span`,{children:`Codex`}),(0,N.jsx)(`span`,{className:`usage-dot ${b?`active`:``}`}),(0,N.jsx)(`span`,{className:`usage-state-text`,children:b?`使用中`:`未使用`})]})]}),(0,N.jsxs)(`div`,{className:`compact-meta-row`,children:[(0,N.jsxs)(`div`,{className:`compact-reset-list`,children:[(0,N.jsxs)(`div`,{className:`compact-meta-item`,children:[(0,N.jsx)(`label`,{children:s(n,`primary`)}),(0,N.jsx)(`strong`,{children:C(n,`primary`)})]}),(0,N.jsxs)(`div`,{className:`compact-meta-item`,children:[(0,N.jsx)(`label`,{children:s(n,`secondary`)}),(0,N.jsx)(`strong`,{children:C(n,`secondary`)})]})]}),(0,N.jsx)(`div`,{className:`compact-meta-actions`,children:(0,N.jsxs)(`button`,{className:`details-toggle ${y?`is-expanded`:``}`,type:`button`,onClick:()=>e.onToggle(n.profileId),children:[(0,N.jsx)(`span`,{children:y?`收起详情`:`查看详情`}),(0,N.jsx)(I,{})]})})]}),y&&(0,N.jsxs)(`div`,{className:`meta-grid`,children:[(0,N.jsx)(j,{label:`套餐`,value:m(n)}),(0,N.jsx)(j,{label:`Account ID`,value:(e.showEmails,n.accountId),code:!0}),(0,N.jsx)(j,{label:`Profile ID`,value:(e.showEmails,n.profileId),code:!0}),(0,N.jsx)(j,{label:`认证状态`,value:x(n)}),(0,N.jsx)(j,{label:`生图能力`,value:D.ok?`gpt-image-2 可用`:D.detail}),(0,N.jsx)(j,{label:`导出记录`,value:F(O)}),(0,N.jsx)(j,{label:`过期时间`,value:n.expiresAt?new Date(n.expiresAt).toLocaleString(`zh-CN`):`-`}),(0,N.jsx)(j,{label:`额度快照`,value:n.quota?.capturedAt?new Date(n.quota.capturedAt).toLocaleString(`zh-CN`):`-`})]}),(0,N.jsxs)(`div`,{className:`account-actions`,children:[(0,N.jsx)(`button`,{className:`btn-secondary ${n.isActive?`is-current`:``}`,type:`button`,onClick:()=>e.onAction(`activate`,n),disabled:n.isActive||M||w,children:w?`网关不可用`:n.isActive?`网关使用中`:`应用网关`}),(0,N.jsx)(`button`,{className:`btn-secondary ${b?`is-current codex`:``}`,type:`button`,onClick:()=>e.onAction(`apply-codex`,n),disabled:b||M||w,children:w?`Codex 不可用`:b?`Codex 使用中`:`应用 Codex`}),(0,N.jsx)(`button`,{className:`btn-secondary`,type:`button`,onClick:()=>e.onAction(`export`,n),disabled:M,children:`导出`}),(0,N.jsx)(`button`,{className:`btn-danger`,type:`button`,onClick:()=>e.onAction(`remove`,n),disabled:M,children:`删除`})]})]},n.profileId)})})]})}function F(e){if(!e?.exported)return`未导出`;let t=e.lastExportKind===`single`?`单账号导出`:e.lastExportKind===`batch`?`批量导出`:`全部导出`;return`${e.count} 次,最近 ${o(e.lastExportedAt)},方式 ${t}`}function I(){return(0,N.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,"aria-hidden":`true`,children:(0,N.jsx)(`path`,{d:`m6 9 6 6 6-6`})})}function L(e){return(0,N.jsxs)(`div`,{className:`quota-row`,children:[(0,N.jsxs)(`div`,{className:`quota-line`,children:[(0,N.jsxs)(`span`,{children:[e.label,` · 已用 `,e.value,`% / 剩余 `,100-e.value,`%`]}),(0,N.jsxs)(`strong`,{children:[`剩余 `,100-e.value,`%`]})]}),(0,N.jsx)(`div`,{className:`progress-track`,children:(0,N.jsx)(`div`,{className:`progress-bar ${e.tone}`,style:{width:`${e.value}%`}})})]})}function R(e){let[t,n]=(0,M.useState)({}),[r,i]=(0,M.useState)({}),[a,o]=(0,M.useState)({search:``,status:`all`,sort:`quota-desc`}),s=(0,M.useMemo)(()=>{let t=e.config?.profiles?[...e.config.profiles]:[],n=new Set(e.config?.settings.autoSwitch.excludedProfileIds||[]),r=a.search.trim().toLowerCase(),i=t.filter(t=>{let i=[h(t,!0).toLowerCase(),t.accountId,t.profileId,t.email||``].join(` `).toLowerCase(),o=d(t),s=!!(e.codexAccountId&&t.accountId===e.codexAccountId),c=g(t);return r&&!i.includes(r)?!1:a.status===`active`?t.isActive||s:a.status===`healthy`?o.key===`healthy`:a.status===`warning`?o.key===`warning`:a.status===`unknown`?o.key===`unknown`:a.status===`exhausted`?o.key===`exhausted`:a.status===`expired`?o.key===`expired`:a.status===`invalid`?o.key===`invalid`:a.status===`login-invalid`?t.authStatus?.state===`token_invalidated`:a.status===`auth-error`?t.authStatus?.state===`auth_error`:a.status===`available`?o.key===`healthy`||o.key===`warning`||o.key===`unknown`:a.status===`unavailable`?o.key===`invalid`||o.key===`expired`||o.key===`exhausted`:a.status===`free`?c===`free`:a.status===`plus`?c===`plus`:a.status===`pro-team`?c===`pro`||c===`team`||c===`enterprise`||c===`premium`:a.status===`api-active`?t.isActive:a.status===`codex-active`?s:a.status===`auto-included`?!n.has(t.profileId):a.status===`auto-excluded`?n.has(t.profileId):!0});return i.sort((t,n)=>{let r=p(t,e.codexAccountId)-p(n,e.codexAccountId);if(r!==0)return r;let i=y(n)-y(t);if(i!==0)return i;let o=S(n)-S(t);return o===0?a.sort===`latency-asc`?(n.quota?.capturedAt||0)-(t.quota?.capturedAt||0):a.sort===`expiry-asc`?(t.expiresAt||2**53-1)-(n.expiresAt||2**53-1):a.sort===`name-asc`?h(t,!0).localeCompare(h(n,!0),`zh-CN`):a.sort===`quota-asc`?100-u(n)-(100-u(t)):a.sort===`plan-desc`?y(n)-y(t):a.sort===`email-asc`?h(t,!0).localeCompare(h(n,!0)):u(n)-u(t):o}),i},[a,e.codexAccountId,e.config?.profiles,e.config?.settings.autoSwitch.excludedProfileIds]),c=(0,M.useMemo)(()=>{let t=e.config?.profiles||[],n=new Set(e.config?.settings.autoSwitch.excludedProfileIds||[]),r=e=>t.filter(e).length,i=r(t=>!!(e.codexAccountId&&t.accountId===e.codexAccountId));return[{key:`all`,label:`总账号`,value:t.length,tone:`blue`},{key:`available`,label:`可用`,value:r(e=>[`healthy`,`warning`,`unknown`].includes(d(e).key)),tone:`green`},{key:`unavailable`,label:`不可用`,value:r(e=>[`invalid`,`expired`,`exhausted`].includes(d(e).key)),tone:`red`},{key:`unknown`,label:`待请求验证`,value:r(e=>d(e).key===`unknown`),tone:`blue`},{key:`login-invalid`,label:`登录失效`,value:r(e=>e.authStatus?.state===`token_invalidated`),tone:`red`},{key:`auth-error`,label:`认证异常`,value:r(e=>e.authStatus?.state===`auth_error`),tone:`red`},{key:`exhausted`,label:`额度耗尽`,value:r(e=>d(e).key===`exhausted`),tone:`orange`},{key:`free`,label:`Free`,value:r(e=>g(e)===`free`),tone:`muted`},{key:`plus`,label:`Plus`,value:r(e=>g(e)===`plus`),tone:`brand`},{key:`pro-team`,label:`Pro/Team`,value:r(e=>[`pro`,`team`,`enterprise`,`premium`].includes(g(e))),tone:`blue`},{key:`api-active`,label:`API 使用中`,value:r(e=>e.isActive),tone:`green`},{key:`codex-active`,label:`Codex 使用中`,value:i,tone:`green`},{key:`auto-included`,label:`参与轮换`,value:r(e=>!n.has(e.profileId)),tone:`blue`},{key:`auto-excluded`,label:`排除轮换`,value:r(e=>n.has(e.profileId)),tone:`orange`}]},[e.codexAccountId,e.config?.profiles,e.config?.settings.autoSwitch.excludedProfileIds]),l=Object.values(t).filter(Boolean).length,f=Object.keys(t).filter(e=>t[e]),m=(0,M.useMemo)(()=>s.map(e=>e.profileId),[s]);async function v(t,n){let r=await O(`/_gateway/admin/profiles/export`,{method:`POST`,headers:{"Content-Type":`application/json`},body:w(n?{profileIds:n}:{profileId:t})});D(`ai-zero-token-${n?`profiles-${n.length}`:t||`active`}.json`,r.profile),r.config?e.setConfig(r.config):await e.refreshConfig({silent:!0}),e.setStatus(n?`已导出 ${n.length} 个账号。`:`账号配置已导出。`)}async function x(t,n){if(!(t===`remove`&&!window.confirm(`确认删除 ${h(n,e.showEmails)}?`))){if((t===`activate`||t===`apply-codex`)&&_(n)){e.setStatus(`${h(n,e.showEmails)} 登录已失效,不能应用到${t===`activate`?`网关`:`Codex`}。`);return}if((t===`activate`||t===`apply-codex`)&&b(n)){let r=t===`activate`?`网关`:`Codex`;if(!window.confirm(`${h(n,e.showEmails)} 的额度看起来已耗尽,仍要应用到${r}吗?`))return}if(t===`export`){await v(n.profileId);return}e.setBusy(`profile:${t}:${n.profileId}`);try{let r=await O({activate:`/_gateway/admin/profiles/activate`,"apply-codex":`/_gateway/admin/codex/apply`,"sync-quota":`/_gateway/admin/profiles/sync-quota`,remove:`/_gateway/admin/profiles/remove`}[t],{method:`POST`,headers:{"Content-Type":`application/json`},body:w({profileId:n.profileId})}),i=`config`in r?r.config:r;if(e.setConfig(i),e.setStatus(t===`activate`?`已应用到网关。`:t===`apply-codex`?`已应用到本机 Codex。`:t===`sync-quota`?`额度信息已同步。`:`账号已删除。`),t===`apply-codex`)if(i.codexRestartSupported&&window.confirm(`Codex 账号已切换,是否现在重启 Codex 客户端?
|
|
2
|
-
|
|
3
|
-
Codex 通常在启动时读取本机 auth.json,重启后新账号会立即生效。`))try{await O(`/_gateway/admin/desktop/restart-codex`,{method:`POST`}),e.setStatus(`已应用到本机 Codex,并已重启 Codex 客户端。`)}catch(t){e.setStatus(`已应用到本机 Codex,但重启 Codex 失败: ${k(t)}`)}else e.setStatus(`已应用到本机 Codex,重启 Codex 客户端后生效。`)}catch(t){e.setStatus(k(t))}finally{e.setBusy(null)}}}async function C(){let t=f;if(t.length===0){e.setStatus(`请先勾选要删除的账号。`);return}let r=e.config?.profiles.filter(e=>t.includes(e.profileId)).slice(0,3).map(t=>h(t,e.showEmails)),i=r?.length?`\n\n${r.join(`
|
|
4
|
-
`)}${t.length>r.length?`\n等 ${t.length} 个账号`:``}`:``;if(window.confirm(`确认删除所选 ${t.length} 个账号?此操作不可撤销。${i}`)){e.setBusy(`bulk-remove`),e.setStatus(`正在删除 ${t.length} 个账号...`);try{let r=await O(`/_gateway/admin/profiles/remove-batch`,{method:`POST`,headers:{"Content-Type":`application/json`},body:w({profileIds:t})});e.setConfig(r),n({}),e.setStatus(`已删除 ${r.removedProfileCount??t.length} 个账号。`)}catch(t){e.setStatus(`删除所选失败: ${k(t)}`)}finally{e.setBusy(null)}}}function T(t,r){if(t.length===0){e.setStatus(`没有可选择的账号。`);return}n(e=>{let n={...e};for(let e of t)n[e]=!0;return n}),e.setStatus(r)}return(0,N.jsx)(P,{config:e.config,profiles:s,accountStats:c,showEmails:e.showEmails,filter:a,selectedProfiles:t,expandedProfiles:r,selectedCount:l,visibleCount:m.length,busy:e.busy,onFilter:o,onSelect:(e,t)=>n(n=>({...n,[e]:t})),onSelectVisible:()=>T(m,`已选择当前筛选结果 ${m.length} 个账号。`),onClearSelected:()=>{n({}),e.setStatus(`已取消选择。`)},onToggle:e=>i(t=>({...t,[e]:!t[e]})),onAction:x,onLocate:()=>e.activeProfile&&document.querySelector(`[data-profile-card="${e.activeProfile.profileId}"]`)?.scrollIntoView({behavior:`smooth`,block:`center`}),onExportSelected:()=>{let t=f;if(t.length===0){e.setStatus(`请先勾选要导出的账号。`);return}v(void 0,t).catch(t=>e.setStatus(t instanceof Error?t.message:String(t)))},onRemoveSelected:()=>void C(),onAddAccount:()=>e.setAccountModalOpen(!0),onRefreshStatus:()=>e.refreshConfig({runtime:!0}),onClearAccounts:()=>e.logout()})}export{R as AccountsPage};
|