codex-multi-auth 2.22.0 → 2.24.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 (117) hide show
  1. package/.codex-plugin/plugin.json +9 -9
  2. package/LICENSE +21 -21
  3. package/README.md +159 -150
  4. package/assets/codex-multi-auth-icon.svg +39 -39
  5. package/assets/readme-hero.svg +31 -31
  6. package/config/README.md +31 -31
  7. package/config/codex-legacy.json +1021 -1021
  8. package/config/codex-modern.json +347 -347
  9. package/config/minimal-codex.json +15 -15
  10. package/config/schema/config.schema.json +8 -0
  11. package/dist/index.js +33 -10
  12. package/dist/lib/account-policy.d.ts +2 -0
  13. package/dist/lib/account-policy.js +3 -1
  14. package/dist/lib/accounts/rate-limits.d.ts +6 -0
  15. package/dist/lib/accounts/rate-limits.js +14 -1
  16. package/dist/lib/accounts.d.ts +6 -1
  17. package/dist/lib/accounts.js +33 -8
  18. package/dist/lib/api-route-store.d.ts +1 -0
  19. package/dist/lib/api-route-store.js +2 -1
  20. package/dist/lib/auth/account-access.js +2 -1
  21. package/dist/lib/auth/auth.js +3 -2
  22. package/dist/lib/auth/device-auth.js +2 -1
  23. package/dist/lib/auth/http.d.ts +1 -0
  24. package/dist/lib/auth/http.js +1 -0
  25. package/dist/lib/auth/token-utils.d.ts +5 -0
  26. package/dist/lib/auth/token-utils.js +10 -0
  27. package/dist/lib/codex-manager/account-manager-commands.js +1 -0
  28. package/dist/lib/codex-manager/commands/account.js +15 -0
  29. package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
  30. package/dist/lib/codex-manager/commands/locale.js +25 -0
  31. package/dist/lib/codex-manager/commands/resets.js +18 -5
  32. package/dist/lib/codex-manager/commands/status.d.ts +4 -0
  33. package/dist/lib/codex-manager/commands/status.js +476 -5
  34. package/dist/lib/codex-manager/commands/workspace.d.ts +4 -2
  35. package/dist/lib/codex-manager/commands/workspace.js +33 -2
  36. package/dist/lib/codex-manager/help.js +5 -3
  37. package/dist/lib/codex-manager.js +112 -30
  38. package/dist/lib/config.js +3 -0
  39. package/dist/lib/constants.d.ts +2 -0
  40. package/dist/lib/constants.js +2 -0
  41. package/dist/lib/context-budget-response.js +8 -8
  42. package/dist/lib/context-overflow.js +10 -10
  43. package/dist/lib/locale.d.ts +7 -0
  44. package/dist/lib/locale.js +35 -0
  45. package/dist/lib/oauth-success.html +125 -125
  46. package/dist/lib/policy/runtime-policy-cache.js +1 -1
  47. package/dist/lib/policy/runtime-policy.d.ts +1 -0
  48. package/dist/lib/policy/runtime-policy.js +4 -0
  49. package/dist/lib/preemptive-quota-scheduler.js +12 -3
  50. package/dist/lib/prompts/codex-host-bridge.js +138 -138
  51. package/dist/lib/prompts/codex.js +79 -79
  52. package/dist/lib/quota-cache.d.ts +4 -0
  53. package/dist/lib/quota-probe.js +2 -1
  54. package/dist/lib/quota-readiness.d.ts +1 -1
  55. package/dist/lib/quota-readiness.js +9 -2
  56. package/dist/lib/request/proxy-fetch.d.ts +5 -0
  57. package/dist/lib/request/proxy-fetch.js +37 -0
  58. package/dist/lib/request/rate-limit-decision.js +1 -1
  59. package/dist/lib/request/url-rewriting.js +17 -0
  60. package/dist/lib/runtime/account-model-catalog.d.ts +2 -0
  61. package/dist/lib/runtime/account-model-catalog.js +7 -2
  62. package/dist/lib/runtime/account-reset-credits.d.ts +11 -2
  63. package/dist/lib/runtime/account-reset-credits.js +35 -12
  64. package/dist/lib/runtime/app-bind.d.ts +4 -1
  65. package/dist/lib/runtime/app-bind.js +63 -3
  66. package/dist/lib/runtime/automatic-account-checks.js +6 -5
  67. package/dist/lib/runtime/automatic-subscription-checks.js +4 -3
  68. package/dist/lib/runtime/model-discovery-status.js +2 -2
  69. package/dist/lib/runtime/reset-credit-routing.d.ts +11 -3
  70. package/dist/lib/runtime/reset-credit-routing.js +66 -21
  71. package/dist/lib/runtime/reset-credits.d.ts +31 -7
  72. package/dist/lib/runtime/reset-credits.js +135 -28
  73. package/dist/lib/runtime/responses-websocket.d.ts +30 -0
  74. package/dist/lib/runtime/responses-websocket.js +243 -29
  75. package/dist/lib/runtime/rotation-account-selection.d.ts +5 -4
  76. package/dist/lib/runtime/rotation-proxy-state.d.ts +3 -0
  77. package/dist/lib/runtime/rotation-server-types.d.ts +4 -1
  78. package/dist/lib/runtime/subscription-quota-order.d.ts +1 -0
  79. package/dist/lib/runtime/subscription-quota-order.js +7 -3
  80. package/dist/lib/runtime/workspace-model-scopes.d.ts +2 -0
  81. package/dist/lib/runtime/workspace-model-scopes.js +14 -4
  82. package/dist/lib/runtime-rotation-proxy.js +138 -38
  83. package/dist/lib/schemas.d.ts +5 -0
  84. package/dist/lib/schemas.js +1 -0
  85. package/dist/lib/storage/snapshot-merge.js +15 -4
  86. package/dist/lib/web-dashboard-html.d.ts +1 -0
  87. package/dist/lib/web-dashboard-html.js +66 -0
  88. package/dist/lib/web-dashboard.d.ts +36 -0
  89. package/dist/lib/web-dashboard.js +322 -0
  90. package/node_modules/@codex-ai/plugin/dist/index.d.ts +2 -2
  91. package/node_modules/@codex-ai/plugin/dist/index.js +2 -2
  92. package/node_modules/@codex-ai/plugin/dist/tool.d.ts +42 -42
  93. package/node_modules/@codex-ai/plugin/dist/tool.js +29 -29
  94. package/node_modules/@codex-ai/plugin/package.json +9 -9
  95. package/node_modules/@codex-ai/sdk/dist/index.d.ts +4 -4
  96. package/node_modules/@codex-ai/sdk/dist/index.js +2 -2
  97. package/node_modules/@codex-ai/sdk/package.json +8 -8
  98. package/package.json +209 -207
  99. package/scripts/codex-app-launcher.js +653 -653
  100. package/scripts/codex-app-router.js +446 -402
  101. package/scripts/codex-bin-resolver.js +332 -332
  102. package/scripts/codex-multi-auth.js +65 -65
  103. package/scripts/codex-routing.js +63 -62
  104. package/scripts/codex.js +7143 -7006
  105. package/scripts/install-codex-auth-utils.js +213 -213
  106. package/scripts/mcodex.js +367 -367
  107. package/scripts/postinstall.js +114 -114
  108. package/scripts/preuninstall.js +265 -265
  109. package/skills/codex-auth-setup/SKILL.md +73 -73
  110. package/vendor/codex-ai-plugin/dist/index.d.ts +2 -2
  111. package/vendor/codex-ai-plugin/dist/index.js +2 -2
  112. package/vendor/codex-ai-plugin/dist/tool.d.ts +42 -42
  113. package/vendor/codex-ai-plugin/dist/tool.js +29 -29
  114. package/vendor/codex-ai-plugin/package.json +9 -9
  115. package/vendor/codex-ai-sdk/dist/index.d.ts +4 -4
  116. package/vendor/codex-ai-sdk/dist/index.js +2 -2
  117. package/vendor/codex-ai-sdk/package.json +8 -8
@@ -14,6 +14,7 @@ import { findQuotaCacheEntryForAccount, isQuotaCacheEntryExhausted, } from "../.
14
14
  import { formatWebsocketDisconnect } from "../../runtime/app-bind.js";
15
15
  import { resolveAccountCurrentMarkers, resolveRuntimeCurrentAccount, } from "../../runtime/runtime-current-account.js";
16
16
  import { isRateLimitedMarker } from "../rate-limit-markers.js";
17
+ import { localized, resolveOutputLocale } from "../../locale.js";
17
18
  function isRestoreReason(value) {
18
19
  return (value === "empty-storage" ||
19
20
  value === "intentional-reset" ||
@@ -71,6 +72,187 @@ function formatRuntimeLastAccount(runtimeSnapshot) {
71
72
  }
72
73
  return null;
73
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
+ }
74
256
  /** Expired observations must not impose pressure after their window has reset. */
75
257
  function forecastQuotaWindow(window, updatedAt, now) {
76
258
  const expiresAt = window.resetAtMs ?? (window.windowMinutes && window.windowMinutes > 0 ? updatedAt + window.windowMinutes * 60_000 : null);
@@ -85,18 +267,19 @@ export async function runStatusCommand(deps) {
85
267
  const path = deps.getStoragePath();
86
268
  const storageHealth = await deps.inspectStorageHealth?.();
87
269
  const logInfo = deps.logInfo ?? console.log;
88
- if (apiConfigurationUnavailable && !deps.json)
270
+ const locale = deps.locale ?? resolveOutputLocale(loadPluginConfig().locale);
271
+ if (apiConfigurationUnavailable && !deps.json && !deps.compact && locale !== "zh")
89
272
  logInfo("API configuration unavailable; subscription status remains available.");
90
273
  const modelInventory = await deps.loadModelInventory?.();
91
274
  const appBindStatus = await deps.loadAppBindStatus?.() ?? null;
92
275
  const disconnectLine = formatWebsocketDisconnect(appBindStatus?.lastWebsocketDisconnect);
93
- if (!deps.json && disconnectLine)
276
+ if (!deps.json && !deps.compact && locale !== "zh" && disconnectLine)
94
277
  logInfo(paint(disconnectLine, "warning"));
95
278
  const resetCredits = await deps.loadResetCreditState?.().catch(() => null);
96
279
  const resetSnapshot = (account) => { const target = resetTargetForStoredAccount(account); return target ? resetCredits?.snapshots[target.key] : undefined; };
97
280
  let policiesUnavailable = false;
98
281
  const accountPolicies = await (deps.loadAccountPolicies ?? loadAccountPolicyStore)({ strict: true }).catch(() => { policiesUnavailable = true; return { version: 1, accounts: {} }; });
99
- if (!deps.json && deps.loadModelInventory)
282
+ if (!deps.json && !deps.compact && locale !== "zh" && deps.loadModelInventory)
100
283
  for (const line of formatModelInventory(modelInventory ?? null))
101
284
  logInfo(line);
102
285
  if (!storage || (storage.accounts.length === 0 && apiRoutes.length === 0)) {
@@ -125,10 +308,39 @@ export async function runStatusCommand(deps) {
125
308
  recommendedIndex: null,
126
309
  recommendationReason: null,
127
310
  runtimeInUseIndex: null,
311
+ runtime: null,
128
312
  accounts: [],
129
313
  }, null, 2));
130
314
  return 0;
131
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
+ }
132
344
  logInfo(effectiveState === "intentional-reset"
133
345
  ? "No accounts configured. Storage was intentionally reset."
134
346
  : effectiveState === "recoverable"
@@ -168,7 +380,7 @@ export async function runStatusCommand(deps) {
168
380
  liveQuota: forecastQuotas[index] ?? undefined,
169
381
  })));
170
382
  const recommendation = recommendForecastAccount(forecastResults);
171
- if (!deps.json) {
383
+ if (!deps.json && !deps.compact && locale !== "zh") {
172
384
  logInfo(paint(apiRoutes.length ? `Accounts (${storage.accounts.length + apiRoutes.length}: ${storage.accounts.length} subscription, ${apiRoutes.length} API/ZDR)` : `Accounts (${storage.accounts.length})`, "heading"));
173
385
  logInfo(paint(`Storage: ${path}`, "muted"));
174
386
  if (recommendation.recommendedIndex !== null) {
@@ -229,6 +441,97 @@ export async function runStatusCommand(deps) {
229
441
  appBindStatus,
230
442
  appHelperStatus,
231
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
+ }
232
535
  // cli-manager-03: machine-readable output for status/list. Build a single
233
536
  // object from the same data the text path renders, then emit and return.
234
537
  if (deps.json) {
@@ -249,6 +552,14 @@ export async function runStatusCommand(deps) {
249
552
  forecastRiskScore: forecastResults[i]?.riskScore ?? null,
250
553
  forecastRiskLevel: forecastResults[i]?.riskLevel ?? null,
251
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
+ })(),
252
563
  automaticOrder: automaticOrder.get(i) ?? null,
253
564
  subscriptionReserve: usesSubscriptionReserve(quotaPreferences[i]),
254
565
  quotaResetAt: quotaPreferences[i]?.resetAtMs ?? null,
@@ -283,9 +594,169 @@ export async function runStatusCommand(deps) {
283
594
  accountIndex: runtimeSnapshot.lastAccountIndex ?? null,
284
595
  requestedAt: runtimeSnapshot.lastAccountUpdatedAt ?? null,
285
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,
286
604
  }, null, 2));
287
605
  return 0;
288
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
+ }
289
760
  if (runtimeSnapshot) {
290
761
  const runtimeMetrics = runtimeSnapshot.runtimeMetrics;
291
762
  const poolCooldown = typeof runtimeSnapshot.poolExhaustionCooldownUntil === "number" &&
@@ -371,7 +842,7 @@ export async function runStatusCommand(deps) {
371
842
  if (quota) {
372
843
  const windows = ["primary", "secondary"].flatMap(key => {
373
844
  const used = quota[key].usedPercent;
374
- 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")] : [];
375
846
  });
376
847
  logInfo(` quota: ${windows.join("; ")} (${paint(`cached quota ${formatWaitTime(Math.max(0, now - quota.updatedAt))} ago`, "muted")})`);
377
848
  }
@@ -1,18 +1,20 @@
1
- import type { AccountStorageV3 } from "../../storage.js";
1
+ import type { AccountStorageV3, withAccountStorageTransaction } from "../../storage.js";
2
2
  type LoadedStorage = AccountStorageV3 | null;
3
3
  export interface WorkspaceCommandDeps {
4
4
  setStoragePath: (path: string | null) => void;
5
5
  loadAccounts: () => Promise<LoadedStorage>;
6
6
  saveAccounts: (storage: AccountStorageV3) => Promise<void>;
7
+ withAccountStorageTransaction: typeof withAccountStorageTransaction;
7
8
  logError?: (message: string) => void;
8
9
  logInfo?: (message: string) => void;
9
10
  }
10
11
  /**
11
12
  * `codex-multi-auth workspace <account> [workspace]`
12
13
  *
13
- * With only an account index, lists the workspaces that account can rotate
14
+ * With only an account index, lists the workspaces that account can select
14
15
  * between (personal Plus vs business/team under one email, issue #491). With a
15
16
  * workspace index too, sets it as the active workspace for that account.
17
+ * A final enable/disable action changes availability without changing selection.
16
18
  */
17
19
  export declare function runWorkspaceCommand(args: string[], deps: WorkspaceCommandDeps): Promise<number>;
18
20
  export {};
@@ -1,11 +1,12 @@
1
- import { formatAccountLabel, formatWorkspaceLines, } from "../../accounts.js";
1
+ import { formatAccountLabel, formatWorkspaceLines, resolveAccountRecordId, } from "../../accounts.js";
2
2
  import { saveAccountsWithRetry } from "../forecast-report-shared.js";
3
3
  /**
4
4
  * `codex-multi-auth workspace <account> [workspace]`
5
5
  *
6
- * With only an account index, lists the workspaces that account can rotate
6
+ * With only an account index, lists the workspaces that account can select
7
7
  * between (personal Plus vs business/team under one email, issue #491). With a
8
8
  * workspace index too, sets it as the active workspace for that account.
9
+ * A final enable/disable action changes availability without changing selection.
9
10
  */
10
11
  export async function runWorkspaceCommand(args, deps) {
11
12
  deps.setStoragePath(null);
@@ -76,6 +77,36 @@ export async function runWorkspaceCommand(args, deps) {
76
77
  return 1;
77
78
  }
78
79
  const targetName = target.name?.trim() || "(unnamed)";
80
+ const action = args[2];
81
+ if (args.length > 3 || (action && action !== "enable" && action !== "disable")) {
82
+ logError("Invalid workspace action. Use enable or disable.");
83
+ return 1;
84
+ }
85
+ if (action) {
86
+ const recordId = resolveAccountRecordId(account);
87
+ const workspaceId = target.id;
88
+ return deps.withAccountStorageTransaction(async (current, persist) => {
89
+ // An explicit command is an operation, even if its initial snapshot already
90
+ // had that value. Apply it to the latest row under the storage lock.
91
+ const matches = current?.accounts.filter(row => resolveAccountRecordId(row) === recordId) ?? [];
92
+ const latest = matches.length === 1 ? matches[0] : undefined;
93
+ const latestWorkspaces = latest?.workspaces ?? [];
94
+ const workspaceMatches = latestWorkspaces.filter(row => row.id === workspaceId);
95
+ const workspace = workspaceMatches.length === 1 ? workspaceMatches[0] : undefined;
96
+ if (!current || !latest || !workspace) {
97
+ logError("Account or workspace changed concurrently; reload before changing workspace availability.");
98
+ return 1;
99
+ }
100
+ workspace.enabled = action === "enable";
101
+ // Absence of disabledAt distinguishes an operator exclusion from runtime health.
102
+ delete workspace.disabledAt;
103
+ await saveAccountsWithRetry(current, persist);
104
+ const currentIndex = latestWorkspaces.indexOf(workspace) + 1;
105
+ const currentName = workspace.name?.trim() || "(unnamed)";
106
+ logInfo(`Workspace ${currentIndex} [${currentName}] ${workspace.enabled ? "enabled" : "disabled"}. Selected workspace unchanged.`);
107
+ return 0;
108
+ });
109
+ }
79
110
  if (target.enabled === false) {
80
111
  logError(`Workspace ${parsedWorkspace} ([${targetName}]) is disabled and cannot be selected.`);
81
112
  return 1;