codex-multi-auth 2.21.0 → 2.23.0

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 (70) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +8 -5
  3. package/config/schema/config.schema.json +8 -0
  4. package/dist/lib/accounts.d.ts +5 -0
  5. package/dist/lib/accounts.js +33 -4
  6. package/dist/lib/api-route-store.d.ts +7 -0
  7. package/dist/lib/api-route-store.js +100 -31
  8. package/dist/lib/auth/account-access.js +2 -1
  9. package/dist/lib/auth/auth.js +3 -2
  10. package/dist/lib/auth/device-auth.js +2 -1
  11. package/dist/lib/auth/http.d.ts +1 -0
  12. package/dist/lib/auth/http.js +1 -0
  13. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  14. package/dist/lib/codex-manager/api-login-menu.d.ts +1 -1
  15. package/dist/lib/codex-manager/api-login-menu.js +4 -6
  16. package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
  17. package/dist/lib/codex-manager/commands/locale.js +25 -0
  18. package/dist/lib/codex-manager/commands/status.d.ts +5 -1
  19. package/dist/lib/codex-manager/commands/status.js +501 -13
  20. package/dist/lib/codex-manager/help.js +3 -1
  21. package/dist/lib/codex-manager.js +98 -23
  22. package/dist/lib/config.js +3 -0
  23. package/dist/lib/constants.d.ts +1 -0
  24. package/dist/lib/constants.js +1 -0
  25. package/dist/lib/locale.d.ts +7 -0
  26. package/dist/lib/locale.js +35 -0
  27. package/dist/lib/model-route-policy.d.ts +1 -0
  28. package/dist/lib/model-route-policy.js +2 -1
  29. package/dist/lib/quota-probe.js +2 -1
  30. package/dist/lib/request/proxy-fetch.d.ts +5 -0
  31. package/dist/lib/request/proxy-fetch.js +37 -0
  32. package/dist/lib/request/url-rewriting.js +17 -0
  33. package/dist/lib/runtime/account-model-catalog.d.ts +3 -0
  34. package/dist/lib/runtime/account-model-catalog.js +7 -2
  35. package/dist/lib/runtime/api-model-capabilities.d.ts +11 -2
  36. package/dist/lib/runtime/api-model-capabilities.js +155 -24
  37. package/dist/lib/runtime/app-bind.d.ts +7 -0
  38. package/dist/lib/runtime/app-bind.js +92 -1
  39. package/dist/lib/runtime/automatic-api-checks.d.ts +15 -0
  40. package/dist/lib/runtime/automatic-api-checks.js +188 -0
  41. package/dist/lib/runtime/model-discovery-status.d.ts +11 -1
  42. package/dist/lib/runtime/model-discovery-status.js +64 -49
  43. package/dist/lib/runtime/persisted-api-capabilities.d.ts +20 -0
  44. package/dist/lib/runtime/persisted-api-capabilities.js +387 -0
  45. package/dist/lib/runtime/responses-websocket.d.ts +48 -5
  46. package/dist/lib/runtime/responses-websocket.js +275 -39
  47. package/dist/lib/runtime/rotation-account-selection.d.ts +25 -5
  48. package/dist/lib/runtime/rotation-account-selection.js +94 -4
  49. package/dist/lib/runtime/rotation-proxy-state.d.ts +26 -0
  50. package/dist/lib/runtime/rotation-proxy-state.js +376 -4
  51. package/dist/lib/runtime/rotation-server-types.d.ts +6 -0
  52. package/dist/lib/runtime/rotation-token-refresh.d.ts +23 -1
  53. package/dist/lib/runtime/rotation-token-refresh.js +218 -27
  54. package/dist/lib/runtime-rotation-proxy.js +601 -105
  55. package/dist/lib/schemas.d.ts +5 -0
  56. package/dist/lib/schemas.js +1 -0
  57. package/dist/lib/storage/credential-sidecars.js +34 -2
  58. package/dist/lib/storage/file-lock.d.ts +2 -0
  59. package/dist/lib/storage/file-lock.js +44 -30
  60. package/dist/lib/usage/ledger.js +1 -0
  61. package/dist/lib/usage/redaction.js +1 -0
  62. package/dist/lib/usage/types.d.ts +1 -1
  63. package/dist/lib/web-dashboard-html.d.ts +1 -0
  64. package/dist/lib/web-dashboard-html.js +66 -0
  65. package/dist/lib/web-dashboard.d.ts +36 -0
  66. package/dist/lib/web-dashboard.js +322 -0
  67. package/package.json +3 -1
  68. package/scripts/codex-app-router.js +86 -14
  69. package/scripts/codex-routing.js +1 -0
  70. package/scripts/codex.js +150 -13
@@ -6,12 +6,15 @@ import { styleReportText as paint } from "../../ui/format.js";
6
6
  import { inferenceAccountKey } from "../../runtime/inference-activity.js";
7
7
  import { modelScopeId } from "../../runtime/workspace-model-scopes.js";
8
8
  import { loadAccountPolicyStore, getAccountPolicyKey } from "../../account-policy.js";
9
+ import { getSchedulingStrategy, loadPluginConfig } from "../../config.js";
9
10
  import { formatModelInventory } from "../../runtime/model-discovery-status.js";
10
11
  import { AUTH_INVALIDATION_MARKER, formatAccountLabel, formatCooldown, formatWaitTime, formatWorkspaceLines, } from "../../accounts.js";
11
12
  import { evaluateForecastAccounts, recommendForecastAccount, } from "../../forecast.js";
12
13
  import { findQuotaCacheEntryForAccount, isQuotaCacheEntryExhausted, } from "../../quota-readiness.js";
14
+ import { formatWebsocketDisconnect } from "../../runtime/app-bind.js";
13
15
  import { resolveAccountCurrentMarkers, resolveRuntimeCurrentAccount, } from "../../runtime/runtime-current-account.js";
14
16
  import { isRateLimitedMarker } from "../rate-limit-markers.js";
17
+ import { localized, resolveOutputLocale } from "../../locale.js";
15
18
  function isRestoreReason(value) {
16
19
  return (value === "empty-storage" ||
17
20
  value === "intentional-reset" ||
@@ -69,6 +72,187 @@ function formatRuntimeLastAccount(runtimeSnapshot) {
69
72
  }
70
73
  return null;
71
74
  }
75
+ function boundedPercent(value) {
76
+ return Math.max(0, Math.min(100, Math.round(value)));
77
+ }
78
+ /** Render usage as a visual bar while retaining exact percentages for copy/paste. */
79
+ function formatQuotaBar(usedPercent, width = 20) {
80
+ const used = boundedPercent(usedPercent);
81
+ const filled = Math.round((used / 100) * width);
82
+ return `${"█".repeat(filled)}${"░".repeat(Math.max(0, width - filled))}`;
83
+ }
84
+ /**
85
+ * Human label for a quota window length. Derived from the reported
86
+ * `windowMinutes` (300→"5h"/"5小时", 10080→"7d"/"7天"); falls back to a
87
+ * neutral primary/secondary label when the duration is unknown.
88
+ */
89
+ function formatQuotaWindowDuration(windowMinutes, locale) {
90
+ const zh = locale === "zh";
91
+ if (windowMinutes % 1440 === 0) {
92
+ const days = windowMinutes / 1440;
93
+ return zh ? `${days}天` : `${days}d`;
94
+ }
95
+ if (windowMinutes % 60 === 0) {
96
+ const hours = windowMinutes / 60;
97
+ return zh ? `${hours}小时` : `${hours}h`;
98
+ }
99
+ return zh ? `${windowMinutes}分钟` : `${windowMinutes}m`;
100
+ }
101
+ function formatQuotaWindowLabel(window, key, locale) {
102
+ const minutes = window.windowMinutes;
103
+ if (typeof minutes === "number" && Number.isFinite(minutes) && minutes > 0) {
104
+ return formatQuotaWindowDuration(minutes, locale);
105
+ }
106
+ return localized(locale, key === "primary" ? "primary" : "secondary", key === "primary" ? "主窗口" : "次窗口");
107
+ }
108
+ function formatQuotaWindow(key, window, locale, width = 20) {
109
+ const used = boundedPercent(window.usedPercent ?? 0);
110
+ const left = 100 - used;
111
+ const label = formatQuotaWindowLabel(window, key, locale);
112
+ if (locale === "zh") {
113
+ return `${label} [${formatQuotaBar(used, width)}] 已用 ${used}% · 剩余 ${left}%`;
114
+ }
115
+ return `${label} [${formatQuotaBar(used, width)}] used ${used}% · ${left}% left`;
116
+ }
117
+ function formatQuotaCell(key, window, locale) {
118
+ const used = boundedPercent(window.usedPercent ?? 0);
119
+ const label = formatQuotaWindowLabel(window, key, locale);
120
+ return locale === "zh"
121
+ ? `${label} ${formatQuotaBar(used, 8)} ${used}%用/${100 - used}%余`
122
+ : `${label} ${formatQuotaBar(used, 8)} ${used}%/${100 - used}%`;
123
+ }
124
+ function formatQuotaSummary(quota, locale, width = 20) {
125
+ return ["primary", "secondary"].flatMap((key) => {
126
+ const used = quota[key]?.usedPercent;
127
+ return typeof used === "number" && Number.isFinite(used)
128
+ ? [formatQuotaWindow(key, quota[key], locale, width)]
129
+ : [];
130
+ });
131
+ }
132
+ function translateMarker(marker) {
133
+ const direct = {
134
+ current: "当前",
135
+ selected: "已选中",
136
+ "in-use": "使用中",
137
+ "strict pin": "严格固定",
138
+ disabled: "已禁用",
139
+ "rate-limited": "限流中",
140
+ "quota-exhausted": "配额已用尽",
141
+ "token-invalid — re-login needed": "令牌无效——需要重新登录",
142
+ };
143
+ if (direct[marker])
144
+ return direct[marker];
145
+ if (marker.startsWith("cooldown:"))
146
+ return `冷却中:${marker.slice("cooldown:".length)}`;
147
+ return marker
148
+ .replace(/^account is disabled$/, "账号已禁用")
149
+ .replace(/^runtime policy blocked account$/, "运行时策略阻止了账号")
150
+ .replace(/^quota cache exhausted$/, "配额缓存已用尽")
151
+ .replace(/^live probe returned 429$/, "实时探测返回 429")
152
+ .replace(/^primary quota (.+) used$/, "主窗口配额已使用 $1")
153
+ .replace(/^secondary quota (.+) used$/, "次窗口配额已使用 $1")
154
+ .replace(/^hard auth failure: /, "认证失败:")
155
+ .replace(/^refresh warning: /, "刷新警告:")
156
+ .replace(/^runtime skip: /, "运行时跳过:")
157
+ .replace(/^cooldown remaining /, "冷却剩余 ")
158
+ .replace(/^rate limit resets in /, "限流将在 ")
159
+ .replace(/^quota resets in /, "配额将在 ");
160
+ }
161
+ function translateDisconnectLine(line) {
162
+ return line
163
+ .replace(/^Last WebSocket disconnect: /, "上次 WebSocket 断开:")
164
+ .replace(/; cause=/, ";原因=")
165
+ .replace(/; stage=/, ";阶段=")
166
+ .replace(/; closeCode=/, ";关闭码=")
167
+ .replace(/; route=/, ";路由=")
168
+ .replace(/; connectionAgeMs=/, ";连接时长(ms)=")
169
+ .replace(/; events=/, ";事件数=");
170
+ }
171
+ function translateReason(reason) {
172
+ if (reason === "token-invalid — re-login needed")
173
+ return "令牌无效——需要重新登录";
174
+ if (reason === "primary quota 0% used")
175
+ return "主窗口配额已使用 0%";
176
+ if (reason === "primary quota 97% used")
177
+ return "主窗口配额已使用 97%";
178
+ if (reason === "No recommendation available.")
179
+ return "暂无推荐账号。";
180
+ return translateMarker(reason)
181
+ .replace(/^Lowest risk ready account \(([^,]+), score (\d+)\)\.$/, (_match, level, score) => `最低风险可用账号(${translateRiskLevel(level)},分数 ${score})。`)
182
+ .replace(/^No account is immediately ready; pick shortest wait \((.+)\)\.$/, "当前没有立即可用账号;选择等待时间最短的账号($1)。")
183
+ .replace(/^All accounts are blocked or exhausted\..*$/, "所有账号都被阻止或配额已用尽。")
184
+ .replace(/^No healthy accounts are available\..*$/, "没有健康可用的账号。")
185
+ .replace(/^primary quota (\d+)% used$/, "主窗口配额已使用 $1%")
186
+ .replace(/^secondary quota (\d+)% used$/, "次窗口配额已使用 $1%");
187
+ }
188
+ function formatLocalizedSubscriptionStatus(value) {
189
+ const plan = {
190
+ "Unknown plan": "未知套餐",
191
+ Business: "商业版",
192
+ Enterprise: "企业版",
193
+ Edu: "教育版",
194
+ };
195
+ const freshness = value.freshness === "stale"
196
+ ? "(数据已过期;请运行账号检查)"
197
+ : value.freshness === "missing" ? "(尚未观测)" : "";
198
+ const autoPrime = value.autoPrime === null ? "未知" : value.autoPrime ? "开启" : "关闭";
199
+ const priming = {
200
+ unknown: "首次使用状态未知",
201
+ "awaiting-first-use": "等待首次使用",
202
+ "timer-running": "重置计时运行中",
203
+ used: "已记录使用",
204
+ completed: "首次使用已完成;尚未确认计时器",
205
+ failed: "首次使用失败",
206
+ "not-applicable": "首次使用不适用",
207
+ };
208
+ const block = {
209
+ "sign-in required": "需要重新登录",
210
+ "account disabled": "账号已禁用",
211
+ paused: "已暂停",
212
+ drained: "已排空",
213
+ "cooling down": "冷却中",
214
+ "workspace unavailable": "工作区不可用",
215
+ };
216
+ const workspace = value.selectedWorkspace
217
+ .replace(/Token account/g, "令牌账号")
218
+ .replace(/Unnamed workspace/g, "未命名工作区")
219
+ .replace(/Invalid workspace selection/g, "工作区选择无效")
220
+ .replace(/Stored binding \(workspace unknown\)/g, "已存绑定(工作区未知)");
221
+ const planLabel = plan[value.planLabel] ?? value.planLabel;
222
+ const blocked = value.automaticBlock ? `;阻断:${block[value.automaticBlock] ?? value.automaticBlock}` : "";
223
+ return `${workspace}${freshness} · ${planLabel} · 自动预热 ${autoPrime} · ${priming[value.primingState] ?? value.primingState}${blocked}`;
224
+ }
225
+ function formatStorageHealthZh(state) {
226
+ const labels = {
227
+ healthy: "正常",
228
+ corrupt: "损坏",
229
+ empty: "空",
230
+ "intentional-reset": "已主动重置",
231
+ recoverable: "可恢复",
232
+ };
233
+ return labels[state] ?? state;
234
+ }
235
+ function translateAccountLabel(label) {
236
+ return label
237
+ .replace(/^Account /, "账号 ")
238
+ .replace(/Token account/g, "令牌账号")
239
+ .replace(/, id:/g, ",ID:")
240
+ .replace(/, /g, ",");
241
+ }
242
+ function translateRiskLevel(level) {
243
+ return { low: "低", medium: "中", high: "高" }[level] ?? level;
244
+ }
245
+ function formatModelInventoryZh(inventory) {
246
+ if (!inventory)
247
+ return ["模型:未检查;请运行 codex-multi-auth check。"];
248
+ const lines = [`模型发现(仅目录访问,不会发起推理探测):${inventory.entries.length} 个条目`];
249
+ for (const entry of inventory.entries) {
250
+ const state = !entry.enabled ? "已禁用" : entry.error ? "发现失败" : `${entry.models.length} 个可用`;
251
+ lines.push(` ${entry.label} [${entry.kind.toUpperCase()}]:${state}`);
252
+ lines.push(` 可用模型:${entry.models.join("、") || "无"}`);
253
+ }
254
+ return lines;
255
+ }
72
256
  /** Expired observations must not impose pressure after their window has reset. */
73
257
  function forecastQuotaWindow(window, updatedAt, now) {
74
258
  const expiresAt = window.resetAtMs ?? (window.windowMinutes && window.windowMinutes > 0 ? updatedAt + window.windowMinutes * 60_000 : null);
@@ -83,14 +267,19 @@ export async function runStatusCommand(deps) {
83
267
  const path = deps.getStoragePath();
84
268
  const storageHealth = await deps.inspectStorageHealth?.();
85
269
  const logInfo = deps.logInfo ?? console.log;
86
- if (apiConfigurationUnavailable && !deps.json)
270
+ const locale = deps.locale ?? resolveOutputLocale(loadPluginConfig().locale);
271
+ if (apiConfigurationUnavailable && !deps.json && !deps.compact && locale !== "zh")
87
272
  logInfo("API configuration unavailable; subscription status remains available.");
88
273
  const modelInventory = await deps.loadModelInventory?.();
274
+ const appBindStatus = await deps.loadAppBindStatus?.() ?? null;
275
+ const disconnectLine = formatWebsocketDisconnect(appBindStatus?.lastWebsocketDisconnect);
276
+ if (!deps.json && !deps.compact && locale !== "zh" && disconnectLine)
277
+ logInfo(paint(disconnectLine, "warning"));
89
278
  const resetCredits = await deps.loadResetCreditState?.().catch(() => null);
90
279
  const resetSnapshot = (account) => { const target = resetTargetForStoredAccount(account); return target ? resetCredits?.snapshots[target.key] : undefined; };
91
280
  let policiesUnavailable = false;
92
281
  const accountPolicies = await (deps.loadAccountPolicies ?? loadAccountPolicyStore)({ strict: true }).catch(() => { policiesUnavailable = true; return { version: 1, accounts: {} }; });
93
- if (!deps.json && deps.loadModelInventory)
282
+ if (!deps.json && !deps.compact && locale !== "zh" && deps.loadModelInventory)
94
283
  for (const line of formatModelInventory(modelInventory ?? null))
95
284
  logInfo(line);
96
285
  if (!storage || (storage.accounts.length === 0 && apiRoutes.length === 0)) {
@@ -111,6 +300,7 @@ export async function runStatusCommand(deps) {
111
300
  totalAccountCount: 0,
112
301
  apiAccounts: [],
113
302
  modelInventory: modelInventory ?? null,
303
+ lastWebsocketDisconnect: appBindStatus?.lastWebsocketDisconnect ?? null,
114
304
  // Emit the same keys the populated branch does (as null) so a
115
305
  // --json consumer sees one stable shape regardless of account count.
116
306
  activeIndex: null,
@@ -118,10 +308,39 @@ export async function runStatusCommand(deps) {
118
308
  recommendedIndex: null,
119
309
  recommendationReason: null,
120
310
  runtimeInUseIndex: null,
311
+ runtime: null,
121
312
  accounts: [],
122
313
  }, null, 2));
123
314
  return 0;
124
315
  }
316
+ if (deps.compact) {
317
+ logInfo(localized(locale, "Codex Multi-Auth status", "Codex Multi-Auth 状态"));
318
+ logInfo(localized(locale, "Accounts: 0 total", "账号:共 0 个"));
319
+ logInfo(`${localized(locale, "Storage", "存储")}: ${path}`);
320
+ if (effectiveState)
321
+ logInfo(`${localized(locale, "Storage health", "存储健康")}: ${effectiveState}`);
322
+ return 0;
323
+ }
324
+ if (locale === "zh") {
325
+ if (apiConfigurationUnavailable)
326
+ logInfo("警告:API 配置不可用;仍会显示订阅状态。");
327
+ if (deps.loadModelInventory) {
328
+ for (const line of formatModelInventoryZh(modelInventory ?? null))
329
+ logInfo(line);
330
+ }
331
+ const message = effectiveState === "intentional-reset"
332
+ ? "未配置账号。存储已主动重置。"
333
+ : effectiveState === "recoverable"
334
+ ? "未配置账号。存在可用的恢复文件。"
335
+ : effectiveState === "corrupt"
336
+ ? "未配置账号。存储似乎已损坏。"
337
+ : "未配置账号。";
338
+ logInfo(message);
339
+ logInfo(`存储:${path}`);
340
+ if (effectiveState)
341
+ logInfo(`存储健康:${formatStorageHealthZh(effectiveState)}`);
342
+ return 0;
343
+ }
125
344
  logInfo(effectiveState === "intentional-reset"
126
345
  ? "No accounts configured. Storage was intentionally reset."
127
346
  : effectiveState === "recoverable"
@@ -161,7 +380,7 @@ export async function runStatusCommand(deps) {
161
380
  liveQuota: forecastQuotas[index] ?? undefined,
162
381
  })));
163
382
  const recommendation = recommendForecastAccount(forecastResults);
164
- if (!deps.json) {
383
+ if (!deps.json && !deps.compact && locale !== "zh") {
165
384
  logInfo(paint(apiRoutes.length ? `Accounts (${storage.accounts.length + apiRoutes.length}: ${storage.accounts.length} subscription, ${apiRoutes.length} API/ZDR)` : `Accounts (${storage.accounts.length})`, "heading"));
166
385
  logInfo(paint(`Storage: ${path}`, "muted"));
167
386
  if (recommendation.recommendedIndex !== null) {
@@ -172,17 +391,26 @@ export async function runStatusCommand(deps) {
172
391
  }
173
392
  }
174
393
  const appHelperStatus = deps.loadAppHelperStatus?.() ?? null;
175
- const [runtimeSnapshot, appBindStatus] = await Promise.all([
176
- deps.loadRuntimeObservabilitySnapshot?.() ?? Promise.resolve(null),
177
- deps.loadAppBindStatus?.() ?? Promise.resolve(null),
178
- ]);
394
+ const runtimeSnapshot = await deps.loadRuntimeObservabilitySnapshot?.() ?? null;
179
395
  const quotaPreferences = forecastQuotas.map(quota => subscriptionQuotaPreference(quota, now));
180
396
  const automaticOrder = new Map();
181
397
  if (appBindStatus?.nativeOpenai) {
182
- const candidates = storage.accounts.map((account, index) => ({ account, index, quota: quotaPreferences[index] ?? subscriptionQuotaPreference(null, now), tier: accountPolicies.accounts[getAccountPolicyKey(account, index)]?.priority ?? 1 }))
183
- .filter(a => forecastResults[a.index]?.availability === "ready" && !a.quota.exhausted);
184
- candidates.sort((a, b) => Number(usesSubscriptionReserve(a.quota)) - Number(usesSubscriptionReserve(b.quota)) ||
185
- a.tier - b.tier || compareSubscriptionQuota(a.quota, b.quota) || Number(b.index === activeIndex) - Number(a.index === activeIndex) || a.index - b.index);
398
+ const schedulingStrategy = appBindStatus.schedulingStrategy ?? getSchedulingStrategy(loadPluginConfig());
399
+ let candidates = storage.accounts.map((account, index) => ({ account, index, quota: quotaPreferences[index] ?? subscriptionQuotaPreference(null, now), tier: accountPolicies.accounts[getAccountPolicyKey(account, index)]?.priority ?? 1 }));
400
+ const useSavedOrder = candidates.some(account => account.tier !== 1);
401
+ candidates = candidates.filter(a => forecastResults[a.index]?.availability === "ready" && !a.quota.exhausted);
402
+ candidates.sort((a, b) => {
403
+ const tierOrder = Number(usesSubscriptionReserve(a.quota)) - Number(usesSubscriptionReserve(b.quota)) || a.tier - b.tier;
404
+ if (tierOrder)
405
+ return tierOrder;
406
+ if (schedulingStrategy === "sequential") {
407
+ if (useSavedOrder)
408
+ return a.index - b.index;
409
+ const count = storage.accounts.length;
410
+ return (a.index - activeIndex + count) % count - (b.index - activeIndex + count) % count;
411
+ }
412
+ return compareSubscriptionQuota(a.quota, b.quota) || Number(b.index === activeIndex) - Number(a.index === activeIndex) || a.index - b.index;
413
+ });
186
414
  // A switch pin is strict in native mode too: no other account is tried, so none gets an order.
187
415
  const pinned = typeof storage.pinnedAccountIndex === "number" ? candidates.filter(a => a.index === storage.pinnedAccountIndex) : candidates;
188
416
  pinned.forEach((a, index) => { automaticOrder.set(a.index, index + 1); });
@@ -213,6 +441,97 @@ export async function runStatusCommand(deps) {
213
441
  appBindStatus,
214
442
  appHelperStatus,
215
443
  }, { now });
444
+ if (!deps.json && deps.compact) {
445
+ const compactRows = storage.accounts.map((account, index) => {
446
+ const markers = buildAccountMarkers(account, index, activeIndex, runtimeCurrent, now, quotaCache, storage.accounts, deps.formatRateLimitEntry);
447
+ const needsLogin = markers.some((marker) => /(re-login|sign-in|invalid)/i.test(marker));
448
+ const disabled = account.enabled === false;
449
+ const state = disabled
450
+ ? localized(locale, "disabled", "已禁用")
451
+ : needsLogin
452
+ ? localized(locale, "needs login", "需重新登录")
453
+ : markers.length > 0
454
+ ? markers
455
+ .map((marker) => marker === "current" ? localized(locale, "current", "当前") : marker === "selected" ? localized(locale, "selected", "已选中") : marker === "in-use" ? localized(locale, "in use", "使用中") : marker)
456
+ .join(", ")
457
+ : localized(locale, "ready", "可用");
458
+ const subscription = subscriptionStatuses[index];
459
+ const plan = subscription?.planLabel === "Unknown plan"
460
+ ? localized(locale, "unknown", "未知")
461
+ : subscription?.planLabel ?? localized(locale, "unknown", "未知");
462
+ const quota = forecastQuotas[index];
463
+ const quotaText = quota
464
+ ? ["primary", "secondary"]
465
+ .map((key) => {
466
+ const used = quota[key].usedPercent;
467
+ return typeof used === "number" && Number.isFinite(used)
468
+ ? formatQuotaCell(key, quota[key], locale)
469
+ : null;
470
+ })
471
+ .filter(Boolean)
472
+ .join(" / ") || "?"
473
+ : "?";
474
+ const last = inferenceTimes[inferenceAccountKey(account)];
475
+ const activity = typeof last === "number" && last > 0 ? formatWaitTime(Math.max(0, now - last)) : "—";
476
+ // Counts derive from the unlocalized marker set, not the rendered
477
+ // label: matching translated text miscounts states like a raw
478
+ // "disabled" marker embedded in a joined reason list.
479
+ const stateKind = disabled
480
+ ? "disabled"
481
+ : needsLogin
482
+ ? "login"
483
+ : markers.some((marker) => isRateLimitedMarker(marker) ||
484
+ marker === "quota-exhausted" ||
485
+ marker.startsWith("cooldown:"))
486
+ ? "blocked"
487
+ : "ready";
488
+ return { index: index + 1, plan, state, stateKind, quota: quotaText, activity };
489
+ });
490
+ const apiRows = apiAccounts.map((account) => ({
491
+ index: `${account.kind}${account.index + 1}`,
492
+ plan: account.label || localized(locale, "API", "API"),
493
+ state: account.enabled
494
+ ? localized(locale, "enabled", "已启用")
495
+ : localized(locale, "disabled", "已禁用"),
496
+ enabled: account.enabled,
497
+ quota: "—",
498
+ activity: typeof account.lastInferenceRequestAt === "number" && account.lastInferenceRequestAt > 0
499
+ ? formatWaitTime(Math.max(0, now - account.lastInferenceRequestAt))
500
+ : "—",
501
+ }));
502
+ const needsLoginCount = compactRows.filter((row) => row.stateKind === "login").length;
503
+ const disabledCount = compactRows.filter((row) => row.stateKind === "disabled").length;
504
+ const blockedCount = compactRows.filter((row) => row.stateKind === "blocked").length;
505
+ const readyCount = compactRows.length - needsLoginCount - disabledCount - blockedCount;
506
+ const route = typeof storage.pinnedAccountIndex === "number"
507
+ ? localized(locale, `pinned to account ${storage.pinnedAccountIndex + 1}`, `固定到账号 ${storage.pinnedAccountIndex + 1}`)
508
+ : activeIndex >= 0
509
+ ? localized(locale, `automatic (active account ${activeIndex + 1})`, `自动轮换(当前账号 ${activeIndex + 1})`)
510
+ : localized(locale, "automatic", "自动轮换");
511
+ logInfo(localized(locale, "Codex Multi-Auth status", "Codex Multi-Auth 状态"));
512
+ logInfo(`${localized(locale, "Accounts", "账号")}: ${readyCount} ${localized(locale, "ready", "可用")} / ${needsLoginCount} ${localized(locale, "need login", "需登录")}${blockedCount > 0 ? ` / ${blockedCount} ${localized(locale, "blocked", "受限")}` : ""} / ${disabledCount} ${localized(locale, "disabled", "已禁用")} (${compactRows.length} ${localized(locale, "total", "总计")})`);
513
+ if (apiRows.length > 0) {
514
+ const apiDisabled = apiRows.filter((row) => !row.enabled).length;
515
+ logInfo(`${localized(locale, "API/ZDR accounts", "API/ZDR 账号")}: ${apiRows.length - apiDisabled} ${localized(locale, "enabled", "已启用")} / ${apiDisabled} ${localized(locale, "disabled", "已禁用")}`);
516
+ }
517
+ logInfo(`${localized(locale, "Routing", "路由")}: ${route}; ${localized(locale, "recommended", "推荐")} ${recommendation.recommendedIndex === null ? "—" : `#${recommendation.recommendedIndex + 1}`}`);
518
+ if (runtimeSnapshot) {
519
+ const metrics = runtimeSnapshot.runtimeMetrics;
520
+ logInfo(`${localized(locale, "Runtime", "运行时")}: ${runtimeSnapshot.responsesRequests} ${localized(locale, "requests", "请求")}, ${metrics.successfulRequests} ${localized(locale, "ok", "成功")}, ${metrics.failedRequests} ${localized(locale, "failed", "失败")}`);
521
+ }
522
+ logInfo("");
523
+ logInfo(localized(locale, "# Plan Quota State Last request", "# 套餐 配额 状态 最近请求"));
524
+ for (const row of [...compactRows, ...apiRows]) {
525
+ logInfo(`${row.index} ${row.plan.padEnd(10)} ${row.quota.padEnd(14)} ${row.state.padEnd(17)} ${row.activity}`);
526
+ }
527
+ if (needsLoginCount > 0) {
528
+ logInfo("");
529
+ logInfo(localized(locale, "Action: codex-multi-auth login --account <index>", "操作:codex-multi-auth login --account <序号>"));
530
+ }
531
+ logInfo(`${localized(locale, "Storage", "存储")}: ${path}`);
532
+ logInfo(localized(locale, "Use --details for the full diagnostic view; --json is stable machine-readable output.", "使用 --details 查看完整诊断;--json 输出稳定的机器可读格式。"));
533
+ return 0;
534
+ }
216
535
  // cli-manager-03: machine-readable output for status/list. Build a single
217
536
  // object from the same data the text path renders, then emit and return.
218
537
  if (deps.json) {
@@ -233,6 +552,14 @@ export async function runStatusCommand(deps) {
233
552
  forecastRiskScore: forecastResults[i]?.riskScore ?? null,
234
553
  forecastRiskLevel: forecastResults[i]?.riskLevel ?? null,
235
554
  forecastQuotaUpdatedAt: forecastQuotas[i]?.updatedAt ?? null,
555
+ quota: (() => {
556
+ const entry = forecastQuotas[i];
557
+ return entry ? {
558
+ updatedAt: entry.updatedAt,
559
+ primary: entry.primary,
560
+ secondary: entry.secondary,
561
+ } : null;
562
+ })(),
236
563
  automaticOrder: automaticOrder.get(i) ?? null,
237
564
  subscriptionReserve: usesSubscriptionReserve(quotaPreferences[i]),
238
565
  quotaResetAt: quotaPreferences[i]?.resetAtMs ?? null,
@@ -260,15 +587,176 @@ export async function runStatusCommand(deps) {
260
587
  recommendationReason: recommendation.reason,
261
588
  runtimeInUseIndex: runtimeCurrent ? runtimeCurrent.index : null,
262
589
  modelInventory: modelInventory ?? null,
590
+ lastWebsocketDisconnect: appBindStatus?.lastWebsocketDisconnect ?? null,
263
591
  accounts,
264
592
  lastRequestedWorkspace: runtimeSnapshot?.lastRequestedWorkspaceId ? {
265
593
  id: runtimeSnapshot.lastRequestedWorkspaceId,
266
594
  accountIndex: runtimeSnapshot.lastAccountIndex ?? null,
267
595
  requestedAt: runtimeSnapshot.lastAccountUpdatedAt ?? null,
268
596
  } : null,
597
+ runtime: runtimeSnapshot ? {
598
+ responsesRequests: runtimeSnapshot.responsesRequests,
599
+ authRefreshRequests: runtimeSnapshot.authRefreshRequests,
600
+ diagnosticProbeRequests: runtimeSnapshot.diagnosticProbeRequests,
601
+ successfulRequests: runtimeSnapshot.runtimeMetrics.successfulRequests,
602
+ failedRequests: runtimeSnapshot.runtimeMetrics.failedRequests,
603
+ } : null,
269
604
  }, null, 2));
270
605
  return 0;
271
606
  }
607
+ // The Chinese detailed view is rendered as one localized block so no
608
+ // English diagnostic labels leak through when --locale zh is requested.
609
+ if (locale === "zh") {
610
+ if (apiConfigurationUnavailable)
611
+ logInfo("警告:API 配置不可用;仍会显示订阅状态。");
612
+ if (disconnectLine)
613
+ logInfo(translateDisconnectLine(disconnectLine));
614
+ if (deps.loadModelInventory) {
615
+ for (const line of formatModelInventoryZh(modelInventory ?? null))
616
+ logInfo(line);
617
+ }
618
+ const totalLabel = apiAccounts.length
619
+ ? storage.accounts.length + apiAccounts.length + ":"
620
+ + storage.accounts.length + " 个订阅账号," + apiAccounts.length + " 个 API/ZDR 账号"
621
+ : String(storage.accounts.length);
622
+ logInfo("账号(" + totalLabel + ")");
623
+ logInfo("存储:" + path);
624
+ logInfo("预测建议:" + (recommendation.recommendedIndex === null
625
+ ? translateReason(recommendation.reason)
626
+ : "账号 " + (recommendation.recommendedIndex + 1) + "(" + translateReason(recommendation.reason) + ")"));
627
+ if (storageHealth)
628
+ logInfo("存储健康:" + formatStorageHealthZh(storageHealth.state));
629
+ if (runtimeSnapshot) {
630
+ const runtimeMetrics = runtimeSnapshot.runtimeMetrics;
631
+ logInfo("运行时:请求=" + runtimeSnapshot.responsesRequests
632
+ + ",刷新=" + runtimeSnapshot.authRefreshRequests
633
+ + ",探测=" + runtimeSnapshot.diagnosticProbeRequests
634
+ + ",保留模型回退=" + (runtimeSnapshot.lunaReserveFallbacks ?? 0)
635
+ + ",预算耗尽=" + runtimeMetrics.requestAttemptBudgetExhaustions);
636
+ const lastRuntimeAccount = formatRuntimeLastAccount(runtimeSnapshot);
637
+ if (lastRuntimeAccount) {
638
+ logInfo("最近运行账号:" + translateAccountLabel(lastRuntimeAccount));
639
+ if (typeof runtimeSnapshot.lastAccountUpdatedAt === "number" && runtimeSnapshot.lastAccountUpdatedAt > 0) {
640
+ logInfo("最近运行请求:" + formatWaitTime(Math.max(0, now - runtimeSnapshot.lastAccountUpdatedAt)) + " 前");
641
+ }
642
+ }
643
+ const requestedId = runtimeSnapshot.lastRequestedWorkspaceId;
644
+ if (requestedId) {
645
+ const index = resolveRuntimeCurrentAccount(storage, { runtimeSnapshot }, { now })?.index;
646
+ const workspace = index === undefined ? undefined : storage.accounts[index]?.workspaces?.find((w) => w.id === requestedId);
647
+ logInfo("最近请求工作区:" + (index === undefined ? "" : "账号 " + (index + 1) + " / ")
648
+ + (workspace?.name?.trim() || "未命名工作区").replace(/Token account/g, "令牌账号") + "(ID:" + requestedId.slice(-6)
649
+ + ";发送范围,计费套餐未验证)");
650
+ }
651
+ else {
652
+ logInfo("最近请求工作区:未记录");
653
+ }
654
+ const poolCooldown = typeof runtimeSnapshot.poolExhaustionCooldownUntil === "number" && runtimeSnapshot.poolExhaustionCooldownUntil > now
655
+ ? formatWaitTime(runtimeSnapshot.poolExhaustionCooldownUntil - now) : null;
656
+ const serverCooldown = typeof runtimeSnapshot.serverBurstCooldownUntil === "number" && runtimeSnapshot.serverBurstCooldownUntil > now
657
+ ? formatWaitTime(runtimeSnapshot.serverBurstCooldownUntil - now) : null;
658
+ if (poolCooldown || serverCooldown)
659
+ logInfo("冷却:账号池=" + (poolCooldown ?? "无") + ",服务突发=" + (serverCooldown ?? "无"));
660
+ if (runtimeSnapshot.currentRequestId)
661
+ logInfo("最近请求跟踪 ID:" + runtimeSnapshot.currentRequestId);
662
+ }
663
+ if (runtimeCurrent)
664
+ logInfo("当前运行账号:账号 " + (runtimeCurrent.index + 1) + "(来源:" + runtimeCurrent.source + ")");
665
+ if (appBindStatus?.nativeOpenai)
666
+ logInfo("自动顺序来自最近一次检查;请求模型、工作区和实时健康状态可能改变顺序。");
667
+ logInfo("下面的账号/工作区标签表示保存的设置,不代表最近一次路由的工作区。");
668
+ const pinnedAccountIndex = storage.pinnedAccountIndex;
669
+ if (typeof pinnedAccountIndex === "number") {
670
+ if (!Number.isInteger(pinnedAccountIndex) || pinnedAccountIndex < 0 || pinnedAccountIndex >= storage.accounts.length) {
671
+ logInfo("固定账号:索引 " + pinnedAccountIndex + " 无效;请运行 codex-multi-auth unpin");
672
+ }
673
+ else {
674
+ logInfo("固定账号:账号 " + (pinnedAccountIndex + 1) + "(严格固定,由 switch 设置)");
675
+ if (!appBindStatus?.nativeOpenai && runtimeCurrent && runtimeCurrent.index !== pinnedAccountIndex) {
676
+ logInfo("警告:当前运行账号是 " + (runtimeCurrent.index + 1) + ",但固定账号是 "
677
+ + (pinnedAccountIndex + 1) + ";下一次请求时代理会采用固定账号。");
678
+ }
679
+ }
680
+ }
681
+ logInfo("");
682
+ for (let i = 0; i < storage.accounts.length; i += 1) {
683
+ const account = storage.accounts[i];
684
+ if (!account)
685
+ continue;
686
+ const label = translateAccountLabel(formatAccountLabel(account, i));
687
+ const markers = buildAccountMarkers(account, i, activeIndex, runtimeCurrent, now, quotaCache, storage.accounts, deps.formatRateLimitEntry);
688
+ if (storage.pinnedAccountIndex === i)
689
+ markers.push("strict pin");
690
+ const markerLabel = markers.length > 0 ? " [" + markers.map(translateMarker).join(",") + "]" : "";
691
+ const lastInferenceAt = inferenceTimes[inferenceAccountKey(account)];
692
+ const activity = typeof account.lastUsed === "number" && account.lastUsed > 0
693
+ ? "账号活动于 " + formatWaitTime(Math.max(0, now - account.lastUsed)) + " 前"
694
+ : "尚无账号活动记录";
695
+ const lastUsed = typeof lastInferenceAt === "number" && lastInferenceAt > 0
696
+ ? "最近推理请求于 " + formatWaitTime(Math.max(0, now - lastInferenceAt)) + " 前"
697
+ : "尚未记录推理请求;" + activity;
698
+ logInfo((i + 1) + ". " + label + markerLabel + " " + lastUsed);
699
+ logInfo(" 优先级:" + (accountPolicies.accounts[getAccountPolicyKey(account, i)]?.priority ?? 1));
700
+ const subscription = subscriptionStatuses[i];
701
+ if (subscription)
702
+ logInfo(" 订阅:" + formatLocalizedSubscriptionStatus(subscription));
703
+ if (appBindStatus?.nativeOpenai) {
704
+ const preference = quotaPreferences[i];
705
+ const order = automaticOrder.get(i);
706
+ const reset = preference?.resetAtMs
707
+ ? ";限制窗口将在 " + formatWaitTime(Math.max(0, preference.resetAtMs - now)) + " 后重置"
708
+ : ";配额重置时间未知,请运行检查";
709
+ const reserve = usesSubscriptionReserve(preference) ? ";" + SUBSCRIPTION_RESERVE_PERCENT + "% 保留额度(最后使用)" : "";
710
+ const pinnedElsewhere = typeof storage.pinnedAccountIndex === "number" && storage.pinnedAccountIndex !== i;
711
+ const pinnedIndex = storage.pinnedAccountIndex;
712
+ logInfo(" 自动顺序:" + (order ? "#" + order : pinnedElsewhere && typeof pinnedIndex === "number" ? "无(账号 " + (pinnedIndex + 1) + " 已固定)" : "不可用") + reset + reserve);
713
+ }
714
+ const quota = forecastQuotas[i];
715
+ if (quota) {
716
+ const quotaLines = formatQuotaSummary(quota, "zh");
717
+ if (quotaLines.length > 0) {
718
+ logInfo(" 配额:");
719
+ for (const line of quotaLines)
720
+ logInfo(" " + line);
721
+ logInfo(" 缓存于 " + formatWaitTime(Math.max(0, now - quota.updatedAt)) + " 前");
722
+ }
723
+ else {
724
+ logInfo(" 配额:未知;请运行检查(风险评估仅使用其他信号)");
725
+ }
726
+ }
727
+ else {
728
+ logInfo(" 配额:未知;请运行检查(风险评估仅使用其他信号)");
729
+ }
730
+ const resets = resetSnapshot(account);
731
+ logInfo(" 订阅重置额度:" + (resets?.availableCount ?? "未知")
732
+ + (resets ? "(缓存于 " + formatWaitTime(Math.max(0, now - resets.updatedAt)) + " 前)" : ";请运行检查"));
733
+ const forecast = forecastResults[i];
734
+ if (forecast)
735
+ logInfo(" 预测风险:" + forecast.riskScore + "/100(" + translateRiskLevel(forecast.riskLevel) + ";分数越低越好)");
736
+ const primaryReason = forecastResults[i]?.reasons[0];
737
+ if (primaryReason)
738
+ logInfo(" 原因:" + translateReason(primaryReason));
739
+ if ((account.workspaces?.length ?? 0) > 1) {
740
+ logInfo(" 工作区(保存的选择;不是实时路由):");
741
+ for (const workspaceLine of formatWorkspaceLines(account, " ", "保存的选择")) {
742
+ logInfo(workspaceLine.replace(/\(disabled\)/g, "(已禁用)").replace(/ id:/g, " ID:").replace(/\(unnamed\)/g, "(未命名)"));
743
+ }
744
+ }
745
+ }
746
+ if (apiAccounts.length > 0) {
747
+ logInfo("");
748
+ logInfo("API/ZDR 账号(" + apiAccounts.length + " 个;独立隐私池,低优先级数字先用):");
749
+ for (const account of apiAccounts) {
750
+ const last = account.lastInferenceRequestAt === null
751
+ ? "尚未记录推理请求"
752
+ : "最近推理请求于 " + formatWaitTime(Math.max(0, now - account.lastInferenceRequestAt)) + " 前";
753
+ logInfo(account.kind.toUpperCase() + " 账号 " + (account.index + 1) + ":" + account.label
754
+ + (account.enabled ? "" : "(已禁用)") + " " + last);
755
+ logInfo(" 优先级:" + account.priority + ";可见模型:" + account.visibleModelCount + " 个");
756
+ }
757
+ }
758
+ return 0;
759
+ }
272
760
  if (runtimeSnapshot) {
273
761
  const runtimeMetrics = runtimeSnapshot.runtimeMetrics;
274
762
  const poolCooldown = typeof runtimeSnapshot.poolExhaustionCooldownUntil === "number" &&
@@ -345,7 +833,7 @@ export async function runStatusCommand(deps) {
345
833
  if (appBindStatus?.nativeOpenai) {
346
834
  const preference = quotaPreferences[i];
347
835
  const order = automaticOrder.get(i);
348
- const reset = preference?.resetAtMs ? `; limiting window resets in ${formatWaitTime(Math.max(0, preference.resetAtMs - now))}` : "; reset ordering unknown; run check";
836
+ const reset = preference?.resetAtMs ? `; limiting window resets in ${formatWaitTime(Math.max(0, preference.resetAtMs - now))}` : "; quota reset unknown; run check";
349
837
  const reserve = usesSubscriptionReserve(preference) ? `; ${SUBSCRIPTION_RESERVE_PERCENT}% reserve (last resort)` : "";
350
838
  const pinnedElsewhere = typeof storage.pinnedAccountIndex === "number" && storage.pinnedAccountIndex !== i;
351
839
  logInfo(paint(` automatic order: ${order ? `#${order}` : pinnedElsewhere ? `none (account ${(storage.pinnedAccountIndex ?? 0) + 1} is pinned)` : "unavailable"}${reset}${reserve}`, reserve ? "warning" : "accent"));
@@ -354,7 +842,7 @@ export async function runStatusCommand(deps) {
354
842
  if (quota) {
355
843
  const windows = ["primary", "secondary"].flatMap(key => {
356
844
  const used = quota[key].usedPercent;
357
- return typeof used === "number" && Number.isFinite(used) ? [paint(`${key} ${Math.max(0, Math.min(100, Math.round(100 - used)))}% left`, used >= 95 ? "danger" : used >= 80 ? "warning" : "success")] : [];
845
+ return typeof used === "number" && Number.isFinite(used) ? [paint(formatQuotaWindow(key, quota[key], "en"), used >= 95 ? "danger" : used >= 80 ? "warning" : "success")] : [];
358
846
  });
359
847
  logInfo(` quota: ${windows.join("; ")} (${paint(`cached quota ${formatWaitTime(Math.max(0, now - quota.updatedAt))} ago`, "muted")})`);
360
848
  }
@@ -7,7 +7,8 @@ export function printUsage() {
7
7
  "Start here:",
8
8
  " codex-multi-auth login [--device-auth|--manual|--no-browser] [--org <org_id>] [--preserve-selection] [--account <index|email|account_id>]",
9
9
  " codex-multi-auth login --api (API credentials and visible models)",
10
- " codex-multi-auth status [--json] (list is the same command)",
10
+ " codex-multi-auth status [--json|--details] (list is the same command)",
11
+ " codex-multi-auth web [--port 4317] [--no-open] (local real-time pool dashboard)",
11
12
  " codex-multi-auth check [accounts|resets|capabilities] [--prime] (default: all checks)",
12
13
  " codex-multi-auth limits --json [--refresh] (structured quota windows; refresh is age-gated)",
13
14
  "",
@@ -41,6 +42,7 @@ export function printUsage() {
41
42
  " codex-multi-auth rotation reset-rate-limits [--all | --account <idx>] [--dry-run] [--json]",
42
43
  " codex-multi-auth why-selected [--now|-n | --last|-l] [--json]",
43
44
  " codex-multi-auth history [list|show <id>] [--json] (all local sessions, ignores /resume provider filtering)",
45
+ " codex-multi-auth config locale [auto|en|zh] (language; auto follows the system locale)",
44
46
  "",
45
47
  "Advanced:",
46
48
  " codex-multi-auth report [--live] [--json] [--explain] [--model <model>] [--max-accounts <n>] [--max-probes <n>] [--cached-only] [--out <path>]",