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.
Files changed (28) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/admin-ui/dist/assets/accounts-D0XoMUO2.js +4 -0
  3. package/admin-ui/dist/assets/{docs-BO-aSEzh.js → docs-Ctyhx0QT.js} +1 -1
  4. package/admin-ui/dist/assets/{image-bed-Dql7Vqd9.js → image-bed-BZF7fike.js} +1 -1
  5. package/admin-ui/dist/assets/{index-CCiBaGwU.js → index-BM5N4YUY.js} +3 -3
  6. package/admin-ui/dist/assets/index-BgT1IdcO.css +1 -0
  7. package/admin-ui/dist/assets/{launch-DXLo-NIM.js → launch-DMZlZ2Eq.js} +1 -1
  8. package/admin-ui/dist/assets/{logs-Cwn8-rDu.js → logs-Cyn5SyNG.js} +1 -1
  9. package/admin-ui/dist/assets/{network-detect-vzWfL-Tz.js → network-detect-D_SP0lTT.js} +1 -1
  10. package/admin-ui/dist/assets/overview-CqmN2aqg.js +1 -0
  11. package/admin-ui/dist/assets/{profiles-C5SmQvju.js → profiles-iNTmJFRe.js} +1 -1
  12. package/admin-ui/dist/assets/settings-BFKavypz.js +8 -0
  13. package/admin-ui/dist/assets/{tester-BKoMSoCz.js → tester-9eNSYAOK.js} +2 -2
  14. package/admin-ui/dist/assets/usage-Bsdlw9XG.js +1 -0
  15. package/admin-ui/dist/index.html +3 -3
  16. package/dist/core/providers/openai-codex/chat.js +139 -8
  17. package/dist/core/providers/openai-codex/oauth.js +8 -4
  18. package/dist/core/services/auth-service.js +25 -3
  19. package/dist/core/services/usage-service.js +402 -31
  20. package/dist/core/store/codex-auth-store.js +82 -7
  21. package/dist/server/app.js +234 -14
  22. package/docs/DESKTOP_RELEASE.md +9 -0
  23. package/package.json +1 -1
  24. package/admin-ui/dist/assets/accounts-D3tsDc3k.js +0 -4
  25. package/admin-ui/dist/assets/index-C22_3Mxq.css +0 -1
  26. package/admin-ui/dist/assets/overview-B_yad8ge.js +0 -1
  27. package/admin-ui/dist/assets/settings-BdRWcKJb.js +0 -5
  28. package/admin-ui/dist/assets/usage-B-qQxXzQ.js +0 -1
@@ -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 totalTokens = tokenNumber(value.total_tokens) ?? (inputTokens !== null || outputTokens !== null ? (inputTokens ?? 0) + (outputTokens ?? 0) : null);
262
- if (inputTokens === null && outputTokens === null && totalTokens === null) {
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
- abortController.abort();
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
- if (!reply.raw.write(chunk)) {
2050
- await new Promise((resolve) => reply.raw.once("drain", resolve));
2051
- }
2255
+ await writeChunkToClient(chunk);
2052
2256
  }
2053
2257
  streamFinished = true;
2054
- reply.raw.end();
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
- sendChatCompletionsStream(reply, result);
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);
@@ -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.7",
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};