codex-slot 0.1.27 → 0.1.29

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.
@@ -245,6 +245,12 @@ function resolveInitialCursorIndex(accounts, statuses, selectedAuthAccountId) {
245
245
  }
246
246
  return 0;
247
247
  }
248
+ function buildInteractiveItems(accounts, relaySlots) {
249
+ return [
250
+ ...accounts.map((account) => ({ type: "account", id: account.id })),
251
+ ...relaySlots.map((slot) => ({ type: "relay", id: slot.id }))
252
+ ];
253
+ }
248
254
  /**
249
255
  * 将状态面板中选中的账号立即应用为 Codex App 主登录态。
250
256
  *
@@ -286,15 +292,27 @@ async function handleInteractiveToggle(initialStatuses) {
286
292
  node_readline_1.default.emitKeypressEvents(stdin);
287
293
  stdin.setRawMode?.(true);
288
294
  const accountsFromConfig = (0, account_service_1.listAccounts)();
289
- if (accountsFromConfig.length === 0) {
290
- console.log((0, text_1.bi)("当前没有已录入账号。", "No managed accounts found."));
295
+ const latestSnapshotForRelays = (0, status_service_1.getStatusSnapshot)();
296
+ const relaySlotsFromConfig = latestSnapshotForRelays.relaySlots;
297
+ if (accountsFromConfig.length === 0 && relaySlotsFromConfig.length === 0) {
298
+ console.log((0, text_1.bi)("当前没有已录入账号或中转槽位。", "No managed accounts or relay slots found."));
291
299
  stdin.setRawMode?.(false);
292
300
  return;
293
301
  }
294
302
  const accounts = [...accountsFromConfig].sort((left, right) => left.name.localeCompare(right.name));
303
+ const relaySlots = [...relaySlotsFromConfig].sort((left, right) => left.name.localeCompare(right.name));
295
304
  let selectedAuthAccountId = (0, state_1.getSelectedCodexAuthAccountId)();
296
- let cursor = resolveInitialCursorIndex(accounts, initialStatuses ?? (0, status_1.collectAccountStatuses)(), selectedAuthAccountId);
297
- let changed = false;
305
+ const initialItems = buildInteractiveItems(accounts, relaySlots);
306
+ const selectedModelRoute = (0, state_1.getSelectedModelRoute)();
307
+ const initialAccountCursor = resolveInitialCursorIndex(accounts, initialStatuses ?? (0, status_1.collectAccountStatuses)(), selectedAuthAccountId);
308
+ const selectedRelayIndex = selectedModelRoute.mode === "relay_slot"
309
+ ? relaySlots.findIndex((slot) => slot.id === selectedModelRoute.relay_slot_id)
310
+ : -1;
311
+ let cursor = selectedRelayIndex >= 0
312
+ ? accounts.length + selectedRelayIndex
313
+ : Math.min(initialAccountCursor, Math.max(0, initialItems.length - 1));
314
+ let accountChanged = false;
315
+ let relayChanged = false;
298
316
  enterInteractiveScreen();
299
317
  return await new Promise((resolve) => {
300
318
  let closed = false;
@@ -304,7 +322,9 @@ async function handleInteractiveToggle(initialStatuses) {
304
322
  const screenWidth = process.stdout.columns ?? 80;
305
323
  const styled = shouldUseAnsiStyle();
306
324
  const latestSnapshot = (0, status_service_1.getStatusSnapshot)();
307
- const statusSource = changed ? latestSnapshot.statuses : (initialStatuses ?? latestSnapshot.statuses);
325
+ const items = buildInteractiveItems(accounts, relaySlots);
326
+ const currentSelection = items[cursor] ?? null;
327
+ const statusSource = accountChanged ? latestSnapshot.statuses : (initialStatuses ?? latestSnapshot.statuses);
308
328
  const statusById = new Map(statusSource.map((item) => [item.id, item]));
309
329
  const autoSelectedId = (0, scheduler_1.pickBestAccount)()?.account.id ?? null;
310
330
  const summary = (0, status_1.summarizeAccountStatuses)(statusSource);
@@ -321,7 +341,20 @@ async function handleInteractiveToggle(initialStatuses) {
321
341
  };
322
342
  })
323
343
  .filter((item) => item !== null);
324
- const currentItem = displayStatuses.find((item) => item.id === accounts[cursor]?.id) ?? null;
344
+ const displayRelays = relaySlots.map((slot) => {
345
+ const selected = latestSnapshot.modelRoute.mode === "relay_slot" &&
346
+ latestSnapshot.modelRoute.relay_slot_id === slot.id;
347
+ return {
348
+ ...slot,
349
+ name: selected ? `${slot.name}*` : slot.name
350
+ };
351
+ });
352
+ const currentAccount = currentSelection?.type === "account"
353
+ ? displayStatuses.find((item) => item.id === currentSelection.id) ?? null
354
+ : null;
355
+ const currentRelay = currentSelection?.type === "relay"
356
+ ? displayRelays.find((item) => item.id === currentSelection.id) ?? null
357
+ : null;
325
358
  const wideLayout = screenWidth >= 104;
326
359
  const leftWidth = wideLayout ? Math.max(68, Math.floor(screenWidth * 0.64)) : screenWidth;
327
360
  const rightWidth = wideLayout ? Math.max(28, screenWidth - leftWidth - 3) : screenWidth;
@@ -333,41 +366,71 @@ async function handleInteractiveToggle(initialStatuses) {
333
366
  styled,
334
367
  selectorColumn: {
335
368
  enabledById: Object.fromEntries(accounts.map((account) => [account.id, account.enabled])),
336
- cursorAccountId: accounts[cursor]?.id ?? null
369
+ cursorAccountId: currentSelection?.type === "account" ? currentSelection.id : null
337
370
  }
338
371
  }).split("\n")
339
372
  ];
373
+ const relayLines = [
374
+ renderSectionHeader("relays", leftWidth, styled),
375
+ ...(displayRelays.length > 0
376
+ ? (0, status_1.renderRelayStatusTable)(displayRelays, {
377
+ compact: true,
378
+ maxWidth: leftWidth,
379
+ styled,
380
+ selectorColumn: {
381
+ enabledById: Object.fromEntries(relaySlots.map((slot) => [slot.id, slot.enabled])),
382
+ cursorRelayId: currentSelection?.type === "relay" ? currentSelection.id : null
383
+ }
384
+ }).split("\n")
385
+ : ["-"])
386
+ ];
387
+ const currentDetails = currentSelection?.type === "relay"
388
+ ? (0, status_1.renderRelayStatusDetails)(currentRelay, { maxWidth: rightWidth, header: false }).split("\n")
389
+ : (0, status_1.renderStatusDetails)(currentAccount, { maxWidth: rightWidth, header: false }).split("\n");
340
390
  const sideLines = [
341
391
  renderSectionHeader("current", rightWidth, styled),
342
- ...(0, status_1.renderStatusDetails)(currentItem, { maxWidth: rightWidth, header: false }).split("\n"),
392
+ ...currentDetails,
343
393
  "",
344
394
  renderSectionHeader("summary", rightWidth, styled),
345
395
  renderSummaryLine(summary, rightWidth < 42, styled),
396
+ `model_route=${latestSnapshot.modelRouteLabel}`,
346
397
  `scheduler=${latestSnapshot.selectedName ?? "none"}`,
347
398
  `codex_auth=${selectedAuthAccountId ?? "none"}`,
399
+ `relay_slots=${latestSnapshot.relaySlots.length}`,
348
400
  ...(refreshStatusText ? [`refresh=${refreshStatusText}`] : []),
349
401
  "",
350
402
  renderSectionHeader("help", rightWidth, styled),
351
- "↑/↓ move Space toggle a app-auth c clear r refresh Enter/q exit"
403
+ "↑/↓ move Space toggle a app-auth m model-route c clear r refresh Enter/q exit"
404
+ ];
405
+ const leftLines = [
406
+ ...accountLines,
407
+ "",
408
+ ...relayLines
352
409
  ];
353
410
  if (wideLayout) {
354
- renderInteractiveScreen(renderColumns(accountLines, sideLines, 3));
411
+ renderInteractiveScreen(renderColumns(leftLines, sideLines, 3));
355
412
  return;
356
413
  }
357
414
  renderInteractiveScreen([
358
- ...accountLines,
415
+ ...leftLines,
359
416
  "",
360
417
  renderDivider(screenWidth, styled),
361
418
  ...sideLines
362
419
  ]);
363
420
  };
364
421
  const applyChanges = () => {
365
- if (!changed) {
422
+ if (!accountChanged && !relayChanged) {
366
423
  return;
367
424
  }
368
- (0, status_service_1.persistAccountEnabledState)(accounts);
369
- changed = false;
370
- initialStatuses = (0, status_1.collectAccountStatuses)();
425
+ if (accountChanged) {
426
+ (0, status_service_1.persistAccountEnabledState)(accounts);
427
+ accountChanged = false;
428
+ initialStatuses = (0, status_1.collectAccountStatuses)();
429
+ }
430
+ if (relayChanged) {
431
+ (0, status_service_1.persistRelayEnabledState)(relaySlots);
432
+ relayChanged = false;
433
+ }
371
434
  };
372
435
  const exitInteractive = () => {
373
436
  if (closed) {
@@ -392,7 +455,7 @@ async function handleInteractiveToggle(initialStatuses) {
392
455
  return;
393
456
  }
394
457
  if (key.name === "down") {
395
- const nextCursor = Math.min(accounts.length - 1, cursor + 1);
458
+ const nextCursor = Math.min(buildInteractiveItems(accounts, relaySlots).length - 1, cursor + 1);
396
459
  if (nextCursor !== cursor) {
397
460
  cursor = nextCursor;
398
461
  render();
@@ -400,14 +463,33 @@ async function handleInteractiveToggle(initialStatuses) {
400
463
  return;
401
464
  }
402
465
  if (key.name === "space") {
403
- accounts[cursor].enabled = !accounts[cursor].enabled;
404
- changed = true;
466
+ const item = buildInteractiveItems(accounts, relaySlots)[cursor];
467
+ if (item?.type === "account") {
468
+ const account = accounts.find((candidate) => candidate.id === item.id);
469
+ if (account) {
470
+ account.enabled = !account.enabled;
471
+ accountChanged = true;
472
+ }
473
+ }
474
+ else if (item?.type === "relay") {
475
+ const slot = relaySlots.find((candidate) => candidate.id === item.id);
476
+ if (slot) {
477
+ slot.enabled = !slot.enabled;
478
+ relayChanged = true;
479
+ }
480
+ }
405
481
  applyChanges();
406
482
  render();
407
483
  return;
408
484
  }
409
485
  if (key.name === "a") {
410
- const account = accounts[cursor];
486
+ const item = buildInteractiveItems(accounts, relaySlots)[cursor];
487
+ if (item?.type !== "account") {
488
+ refreshStatusText = "app-auth requires account";
489
+ render();
490
+ return;
491
+ }
492
+ const account = accounts.find((candidate) => candidate.id === item.id);
411
493
  if (!account) {
412
494
  return;
413
495
  }
@@ -422,6 +504,33 @@ async function handleInteractiveToggle(initialStatuses) {
422
504
  render();
423
505
  return;
424
506
  }
507
+ if (key.name === "m") {
508
+ const item = buildInteractiveItems(accounts, relaySlots)[cursor];
509
+ if (item?.type === "relay") {
510
+ const slot = relaySlots.find((candidate) => candidate.id === item.id);
511
+ if (!slot) {
512
+ return;
513
+ }
514
+ if (!slot.enabled) {
515
+ refreshStatusText = `relay_disabled=${slot.id}`;
516
+ render();
517
+ return;
518
+ }
519
+ (0, state_1.setSelectedModelRoute)({
520
+ mode: "relay_slot",
521
+ relay_slot_id: slot.id
522
+ });
523
+ refreshStatusText = `model_route=relay:${slot.id}`;
524
+ render();
525
+ return;
526
+ }
527
+ (0, state_1.setSelectedModelRoute)({
528
+ mode: "auth_pool"
529
+ });
530
+ refreshStatusText = "model_route=auth_pool";
531
+ render();
532
+ return;
533
+ }
425
534
  if (key.name === "c") {
426
535
  selectedAuthAccountId = null;
427
536
  (0, state_1.setSelectedCodexAuthAccountId)(null);
@@ -485,8 +594,21 @@ async function handleStatus(options) {
485
594
  name: `${item.name}${item.id === (0, scheduler_1.pickBestAccount)()?.account.id ? "*" : ""}${item.id === snapshot.codexAuthAccountId ? "@" : ""}`
486
595
  }));
487
596
  console.log((0, status_1.renderStatusTable)(displayStatuses));
597
+ if (snapshot.relaySlots.length > 0) {
598
+ const displayRelays = snapshot.relaySlots.map((slot) => ({
599
+ ...slot,
600
+ name: snapshot.modelRoute.mode === "relay_slot" &&
601
+ snapshot.modelRoute.relay_slot_id === slot.id
602
+ ? `${slot.name}*`
603
+ : slot.name
604
+ }));
605
+ console.log("");
606
+ console.log((0, status_1.renderRelayStatusTable)(displayRelays));
607
+ }
488
608
  console.log("");
489
609
  console.log(`available=${snapshot.summary.available} 5h_limited=${snapshot.summary.fiveHourLimited} weekly_limited=${snapshot.summary.weeklyLimited}`);
610
+ console.log(`model_route=${snapshot.modelRouteLabel}`);
490
611
  console.log(`scheduler=${snapshot.selectedName ?? "none"}`);
491
612
  console.log(`codex_auth=${snapshot.codexAuthAccountId ?? "none"}`);
613
+ console.log(`relay_slots=${snapshot.relaySlots.length}`);
492
614
  }
package/dist/status.js CHANGED
@@ -4,6 +4,8 @@ exports.collectAccountStatuses = collectAccountStatuses;
4
4
  exports.summarizeAccountStatuses = summarizeAccountStatuses;
5
5
  exports.renderStatusTable = renderStatusTable;
6
6
  exports.renderStatusDetails = renderStatusDetails;
7
+ exports.renderRelayStatusTable = renderRelayStatusTable;
8
+ exports.renderRelayStatusDetails = renderRelayStatusDetails;
7
9
  const config_1 = require("./config");
8
10
  const account_store_1 = require("./account-store");
9
11
  const state_1 = require("./state");
@@ -554,3 +556,73 @@ function renderStatusDetails(item, options) {
554
556
  }
555
557
  return lines.join("\n");
556
558
  }
559
+ /**
560
+ * 将 relay slot 状态渲染为适合终端输出的表格文本。
561
+ *
562
+ * @param slots 待展示的 relay slot 列表。
563
+ * @param options 渲染选项;交互模式下可传入选择列配置。
564
+ * @returns 可直接打印到终端的表格字符串。
565
+ * @throws 无显式抛出。
566
+ */
567
+ function renderRelayStatusTable(slots, options) {
568
+ const selectorColumn = options?.selectorColumn;
569
+ const compact = options?.compact ?? false;
570
+ const maxWidth = options?.maxWidth ?? Number.POSITIVE_INFINITY;
571
+ const compactHeader = maxWidth < 68;
572
+ const relayHeader = compactHeader ? "ID" : "RELAY";
573
+ const statusHeader = compactHeader ? "ST" : "STATUS";
574
+ const relayWidth = Math.max(getDisplayWidth(relayHeader), ...slots.map((item) => getDisplayWidth(item.name)));
575
+ const statusWidth = compactHeader ? 8 : 10;
576
+ const fixedWidth = (selectorColumn ? 4 + 2 : 0) + relayWidth + 2 + statusWidth + 2;
577
+ const baseUrlWidth = Number.isFinite(maxWidth)
578
+ ? Math.max(12, Math.floor(maxWidth) - fixedWidth)
579
+ : Math.max(getDisplayWidth("BASE_URL"), ...slots.map((item) => getDisplayWidth(item.base_url)));
580
+ const rows = [
581
+ [
582
+ ...(selectorColumn ? [" "] : []),
583
+ relayHeader,
584
+ statusHeader,
585
+ "BASE_URL"
586
+ ]
587
+ ];
588
+ for (const slot of slots) {
589
+ const selectorCell = selectorColumn
590
+ ? `${selectorColumn.cursorRelayId === slot.id ? ">" : " "}[${selectorColumn.enabledById[slot.id] ? "x" : " "}]`
591
+ : null;
592
+ const status = slot.enabled ? "enabled" : "disabled";
593
+ rows.push([
594
+ ...(selectorCell ? [selectorCell] : []),
595
+ styleNameCell(truncateCell(slot.name, relayWidth), options?.styled ?? false),
596
+ truncateCell(status, statusWidth),
597
+ truncateCell(slot.base_url, baseUrlWidth)
598
+ ]);
599
+ }
600
+ const widths = rows[0].map((_, columnIndex) => Math.max(...rows.map((row) => getDisplayWidth(row[columnIndex]))));
601
+ return rows
602
+ .map((row) => row.map((cell, index) => padCell(cell, widths[index])).join(" "))
603
+ .join("\n");
604
+ }
605
+ /**
606
+ * 将当前选中 relay slot 渲染为紧凑详情区。
607
+ *
608
+ * @param slot 当前选中的 relay slot;为空时返回占位提示。
609
+ * @param options 详情区渲染选项。
610
+ * @returns 适合直接打印的详情区文本。
611
+ * @throws 无显式抛出。
612
+ */
613
+ function renderRelayStatusDetails(slot, options) {
614
+ const includeHeader = options?.header ?? true;
615
+ if (!slot) {
616
+ return [includeHeader ? "[ relay ]" : "slot -", includeHeader ? "slot -" : ""]
617
+ .filter((line) => line.length > 0)
618
+ .join("\n");
619
+ }
620
+ const maxWidth = options?.maxWidth ?? Number.POSITIVE_INFINITY;
621
+ return [
622
+ ...(includeHeader ? ["[ relay ]"] : []),
623
+ formatDetailLine("slot", slot.name, maxWidth),
624
+ formatDetailLine("status", slot.enabled ? "enabled" : "disabled", maxWidth),
625
+ formatDetailLine("base", slot.base_url, maxWidth),
626
+ formatDetailLine("key", "********", maxWidth)
627
+ ].join("\n");
628
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-slot",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "本地 Codex 多账号切换与状态管理工具",
5
5
  "type": "commonjs",
6
6
  "main": "dist/cli.js",