@foxden-app/foxclaw 0.4.12 → 0.4.14

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.
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { normalizeLocale, t } from '../i18n.js';
6
- import { readChatGptAuthMetadata } from '../auth/mirror.js';
6
+ import { parseChatGptAuthMetadata, readChatGptAuthMetadata } from '../auth/mirror.js';
7
7
  import { parseCommand } from './commands.js';
8
8
  import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadContextSummary, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
9
9
  import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
@@ -427,14 +427,25 @@ export class BridgeSessionCore {
427
427
  time: serviceStatus.authMirror.syncedAt,
428
428
  })
429
429
  : t(locale, 'status_auth_mirror_none'));
430
+ if (serviceStatus.authSync?.enabled) {
431
+ lines.push(t(locale, 'status_auth_sync', {
432
+ node: serviceStatus.authSync.nodeId ?? t(locale, 'unknown'),
433
+ peers: serviceStatus.authSync.peers.length,
434
+ pending: serviceStatus.authSync.pendingImports,
435
+ }));
436
+ if (serviceStatus.authSync.lastError) {
437
+ lines.push(t(locale, 'status_auth_sync_error', { value: serviceStatus.authSync.lastError }));
438
+ }
439
+ }
430
440
  if (serviceStatus.lastUpdate) {
431
441
  lines.push(t(locale, 'status_last_update', {
432
442
  from: serviceStatus.lastUpdate.fromVersion,
433
443
  to: serviceStatus.lastUpdate.toVersion ?? t(locale, 'unknown'),
434
444
  time: serviceStatus.lastUpdate.updatedAt,
435
445
  }));
436
- if (serviceStatus.lastUpdate.codexUpdate) {
437
- lines.push(t(locale, 'status_last_codex_update', { value: serviceStatus.lastUpdate.codexUpdate }));
446
+ const codexUpdateLine = this.formatCodexUpdateResult(serviceStatus.lastUpdate);
447
+ if (codexUpdateLine) {
448
+ lines.push(t(locale, 'status_last_codex_update', { value: codexUpdateLine }));
438
449
  }
439
450
  }
440
451
  else {
@@ -2618,6 +2629,55 @@ export class BridgeSessionCore {
2618
2629
  async getCurrentAuthLabel() {
2619
2630
  return (await this.listCodexAuthState()).currentLabel;
2620
2631
  }
2632
+ async validateExternalCodexAuthCandidate(candidateName, rawAuth, expectedAccountId) {
2633
+ if (!this.isIdleForServiceUpdate()) {
2634
+ return { ok: false, reason: 'runtime is not idle' };
2635
+ }
2636
+ const metadata = parseChatGptAuthMetadata(rawAuth);
2637
+ if (!metadata || metadata.accountId !== expectedAccountId) {
2638
+ return { ok: false, reason: 'remote auth account id mismatch' };
2639
+ }
2640
+ const state = await this.listCodexAuthState();
2641
+ const existing = state.candidates.find(candidate => candidate.name === candidateName) ?? null;
2642
+ if (existing) {
2643
+ const existingMetadata = await readChatGptAuthMetadata(existing.path);
2644
+ if (existingMetadata && existingMetadata.accountId !== expectedAccountId) {
2645
+ return { ok: false, reason: 'same candidate belongs to a different account' };
2646
+ }
2647
+ }
2648
+ const authStat = await fs.lstat(state.authPath).catch(() => null);
2649
+ const originalRegularAuth = authStat?.isFile()
2650
+ ? await fs.readFile(state.authPath, 'utf8').catch(() => null)
2651
+ : null;
2652
+ const tempPath = path.join(state.authDir, `.auth-sync-validate-${process.pid}-${Date.now()}.json`);
2653
+ try {
2654
+ await fs.writeFile(tempPath, rawAuth, { encoding: 'utf8', mode: 0o600 });
2655
+ await pointCodexAuthAtTarget(state.authDir, state.authPath, tempPath);
2656
+ this.pendingTurnErrors.clear();
2657
+ this.attachedThreads.clear();
2658
+ await this.app.restart();
2659
+ const account = await this.app.readAccount(false);
2660
+ const rateLimits = await this.app.readAccountRateLimits();
2661
+ if (!account || !rateLimits || !selectCodexRateLimitSnapshot(rateLimits)) {
2662
+ return { ok: false, reason: 'Codex did not validate ChatGPT usage for remote auth' };
2663
+ }
2664
+ return { ok: true };
2665
+ }
2666
+ catch (error) {
2667
+ return { ok: false, reason: formatUserError(error) };
2668
+ }
2669
+ finally {
2670
+ await restoreCodexAuthTarget(state.authDir, state.authPath, state.currentTargetPath, originalRegularAuth).catch((error) => {
2671
+ this.logger.warn('codex.auth_sync_restore_failed', { error: toErrorMeta(error) });
2672
+ });
2673
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
2674
+ this.pendingTurnErrors.clear();
2675
+ this.attachedThreads.clear();
2676
+ await this.app.restart().catch((error) => {
2677
+ this.logger.warn('codex.auth_sync_restart_restore_failed', { error: toErrorMeta(error) });
2678
+ });
2679
+ }
2680
+ }
2621
2681
  isIdleForServiceUpdate() {
2622
2682
  return this.activeTurns.size === 0
2623
2683
  && this.pendingApprovalMessages.size === 0
@@ -3871,18 +3931,32 @@ export class BridgeSessionCore {
3871
3931
  await this.selfUpdater?.clearStatus();
3872
3932
  }
3873
3933
  formatSelfUpdateResult(status) {
3934
+ const codexUpdateLine = this.formatCodexUpdateResult(status);
3874
3935
  if (status.state === 'succeeded') {
3875
3936
  const result = t(status.locale, 'update_succeeded', {
3876
3937
  from: status.fromVersion,
3877
3938
  to: status.toVersion ?? t(status.locale, 'unknown'),
3878
3939
  });
3879
- return status.codexUpdate ? `${result}\n${status.codexUpdate}` : result;
3940
+ return codexUpdateLine ? `${result}\n${codexUpdateLine}` : result;
3880
3941
  }
3881
3942
  const result = t(status.locale, 'update_failed', { error: status.error ?? t(status.locale, 'unknown') });
3882
- return status.codexUpdate ? `${result}\n${status.codexUpdate}` : result;
3943
+ return codexUpdateLine ? `${result}\n${codexUpdateLine}` : result;
3944
+ }
3945
+ formatCodexUpdateResult(status) {
3946
+ if (status.codexFromVersion || status.codexToVersion) {
3947
+ return t(status.locale, 'update_codex_result', {
3948
+ from: status.codexFromVersion ?? t(status.locale, 'unknown'),
3949
+ to: status.codexToVersion ?? t(status.locale, 'unknown'),
3950
+ });
3951
+ }
3952
+ return status.codexUpdate ?? null;
3883
3953
  }
3884
3954
  async handleAuthCommand(scopeId, locale, args) {
3885
3955
  const action = args[0]?.toLowerCase() ?? 'list';
3956
+ if (action === 'sync') {
3957
+ await this.handleAuthSyncCommand(scopeId, locale, args.slice(1));
3958
+ return;
3959
+ }
3886
3960
  if (action === 'reload' || action === 'restart') {
3887
3961
  await this.handleAuthReloadCommand(scopeId, locale);
3888
3962
  return;
@@ -3923,6 +3997,39 @@ export class BridgeSessionCore {
3923
3997
  const messageId = await this.sendMessage(scopeId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(scopeId) !== null, record), authChoiceKeyboard(locale, record));
3924
3998
  record.messageId = messageId;
3925
3999
  }
4000
+ async handleAuthSyncCommand(scopeId, locale, args) {
4001
+ const action = args[0]?.toLowerCase() ?? 'status';
4002
+ if (action === 'status') {
4003
+ await this.sendMessage(scopeId, formatAuthSyncStatus(locale, this.coordinator?.getAuthSyncStatus?.() ?? null));
4004
+ return;
4005
+ }
4006
+ if (action === 'test') {
4007
+ const result = await this.coordinator?.authSyncTest?.();
4008
+ if (!result) {
4009
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
4010
+ return;
4011
+ }
4012
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_test_sent', { count: result.sent }));
4013
+ return;
4014
+ }
4015
+ if (action === 'push' && args[1]?.toLowerCase() === 'all') {
4016
+ if (!this.canRunGlobalAuthRefresh()) {
4017
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_push_blocked_active'));
4018
+ return;
4019
+ }
4020
+ const result = await this.coordinator?.authSyncPushAll?.();
4021
+ if (!result) {
4022
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
4023
+ return;
4024
+ }
4025
+ await this.sendMessage(scopeId, t(locale, 'auth_sync_push_done', {
4026
+ sent: result.sent,
4027
+ skipped: result.skipped,
4028
+ }));
4029
+ return;
4030
+ }
4031
+ await this.sendMessage(scopeId, t(locale, 'usage_auth_sync'));
4032
+ }
3926
4033
  async handleAuthRefreshAllCommand(scopeId, locale, confirmed = false) {
3927
4034
  if (!this.canRunGlobalAuthRefresh()) {
3928
4035
  await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_blocked_active'));
@@ -3938,7 +4045,18 @@ export class BridgeSessionCore {
3938
4045
  return;
3939
4046
  }
3940
4047
  await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_starting'));
3941
- const result = await this.refreshAllCodexAuthCandidates();
4048
+ const lease = await this.coordinator?.acquireAuthRefreshLease?.('auth refresh all');
4049
+ if (lease && !lease.ok) {
4050
+ await this.sendMessage(scopeId, t(locale, 'auth_refresh_all_lease_failed', { error: lease.reason ?? t(locale, 'unknown') }));
4051
+ return;
4052
+ }
4053
+ let result;
4054
+ try {
4055
+ result = await this.refreshAllCodexAuthCandidates();
4056
+ }
4057
+ finally {
4058
+ await this.coordinator?.releaseAuthRefreshLease?.(lease?.leaseId ?? null);
4059
+ }
3942
4060
  const state = await this.listCodexAuthState();
3943
4061
  await this.applySharedCodexAuthQuotaSnapshots(state);
3944
4062
  const record = createPendingAuthChoiceList(scopeId, state.candidates);
@@ -4449,7 +4567,20 @@ export class BridgeSessionCore {
4449
4567
  if (record.messageId !== null) {
4450
4568
  await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_starting'), []);
4451
4569
  }
4452
- const result = await this.refreshAllCodexAuthCandidates();
4570
+ const lease = await this.coordinator?.acquireAuthRefreshLease?.('auth refresh all');
4571
+ if (lease && !lease.ok) {
4572
+ if (record.messageId !== null) {
4573
+ await this.editMessage(event.scopeId, record.messageId, t(locale, 'auth_refresh_all_lease_failed', { error: lease.reason ?? t(locale, 'unknown') }), authChoiceKeyboard(locale, record));
4574
+ }
4575
+ return;
4576
+ }
4577
+ let result;
4578
+ try {
4579
+ result = await this.refreshAllCodexAuthCandidates();
4580
+ }
4581
+ finally {
4582
+ await this.coordinator?.releaseAuthRefreshLease?.(lease?.leaseId ?? null);
4583
+ }
4453
4584
  const state = await this.listCodexAuthState();
4454
4585
  await this.applySharedCodexAuthQuotaSnapshots(state);
4455
4586
  record.candidates = state.candidates;
@@ -4567,8 +4698,27 @@ export class BridgeSessionCore {
4567
4698
  this.authRotationInProgress = true;
4568
4699
  try {
4569
4700
  const failedTargets = rotation.retry?.failedAuthTargets ?? this.authRotationFailedTargets;
4570
- const selection = await this.selectNextCodexAuthCandidate(failedTargets);
4571
4701
  const locale = this.localeForChat(rotation.scopeId);
4702
+ const current = (await this.listCodexAuthState()).candidates.find(candidate => candidate.isCurrent) ?? null;
4703
+ if (current) {
4704
+ const recoveredCurrent = await this.recoverCodexAuthCandidate(current.name);
4705
+ if (recoveredCurrent) {
4706
+ await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_recovered_current', {
4707
+ value: current.name,
4708
+ error: formatShortStatusError(rotation.reason),
4709
+ }));
4710
+ this.pendingTurnErrors.clear();
4711
+ this.attachedThreads.clear();
4712
+ await this.app.restart();
4713
+ await this.syncCodexAuthCandidate(current.name);
4714
+ if (rotation.retry) {
4715
+ await this.retryTurnAfterAuthRotation(rotation.scopeId, locale, rotation.retry);
4716
+ return true;
4717
+ }
4718
+ return false;
4719
+ }
4720
+ }
4721
+ const selection = await this.selectNextCodexAuthCandidate(failedTargets);
4572
4722
  if (!selection) {
4573
4723
  await this.sendMessage(rotation.scopeId, t(locale, 'auth_auto_no_candidate', {
4574
4724
  error: formatShortStatusError(rotation.reason),
@@ -7834,7 +7984,7 @@ function authChoiceKeyboard(locale, record) {
7834
7984
  const page = codexAuthListPage(record.candidates, record);
7835
7985
  const rows = page.visible.map(({ candidate, index }) => [
7836
7986
  {
7837
- text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaPrefix(locale, candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${candidate.disabled ? ' · off' : ''}`),
7987
+ text: clipButtonText(`${candidate.isCurrent ? '✅ ' : '🔐 '}${formatAuthQuotaButtonPrefix(candidate.quota)}|${formatCodexAuthCandidateDisplayName(candidate.name)}${candidate.disabled ? ' · off' : ''}`),
7838
7988
  callback_data: `auth:${record.localId}:${index}`,
7839
7989
  },
7840
7990
  {
@@ -7960,6 +8110,36 @@ function formatAuthRefreshAllResult(locale, result) {
7960
8110
  }
7961
8111
  return lines.join('\n');
7962
8112
  }
8113
+ function formatAuthSyncStatus(locale, status) {
8114
+ if (!status?.enabled) {
8115
+ return t(locale, 'auth_sync_disabled');
8116
+ }
8117
+ const lines = [
8118
+ t(locale, 'auth_sync_status_title'),
8119
+ t(locale, 'auth_sync_status_node', { value: status.nodeId ?? t(locale, 'unknown') }),
8120
+ t(locale, 'auth_sync_status_peers', { value: status.peers.length === 0 ? t(locale, 'none') : status.peers.join(', ') }),
8121
+ t(locale, 'auth_sync_status_pending', { value: status.pendingImports }),
8122
+ t(locale, 'auth_sync_status_sent', { value: status.lastSentAt ?? t(locale, 'none') }),
8123
+ t(locale, 'auth_sync_status_received', { value: status.lastReceivedAt ?? t(locale, 'none') }),
8124
+ t(locale, 'auth_sync_status_imported', {
8125
+ value: status.lastImportedAt
8126
+ ? `${status.lastImportCandidate ?? t(locale, 'unknown')} @ ${status.lastImportedAt}`
8127
+ : t(locale, 'none'),
8128
+ }),
8129
+ t(locale, 'auth_sync_status_pull', {
8130
+ value: status.lastPullAt
8131
+ ? `${status.lastPullCandidate ?? t(locale, 'unknown')} @ ${status.lastPullAt}`
8132
+ : t(locale, 'none'),
8133
+ }),
8134
+ ];
8135
+ if (status.activeLeaseId) {
8136
+ lines.push(t(locale, 'auth_sync_status_lease', { value: status.activeLeaseId }));
8137
+ }
8138
+ if (status.lastError) {
8139
+ lines.push(t(locale, 'auth_sync_status_error', { value: status.lastError }));
8140
+ }
8141
+ return lines.join('\n');
8142
+ }
7963
8143
  function normalizeHelpUsageKey(name) {
7964
8144
  const normalized = name.toLowerCase();
7965
8145
  switch (normalized) {
@@ -8576,6 +8756,17 @@ function formatAuthQuotaPrefix(locale, snapshot) {
8576
8756
  .map(([duration, remaining, fallback]) => (`${formatCompactRateLimitWindowLabel(locale, duration, fallback)}:${remaining === null ? '--' : formatUsagePercent(remaining)}`));
8577
8757
  return values.length > 0 ? values.join('|') : '--';
8578
8758
  }
8759
+ function formatAuthQuotaButtonPrefix(snapshot) {
8760
+ return [
8761
+ snapshot?.primaryRemainingPercent ?? null,
8762
+ snapshot?.secondaryRemainingPercent ?? null,
8763
+ ].map(formatAuthQuotaButtonValue).join('|');
8764
+ }
8765
+ function formatAuthQuotaButtonValue(value) {
8766
+ return typeof value === 'number' && Number.isFinite(value)
8767
+ ? formatUsagePercent(value)
8768
+ : '—';
8769
+ }
8579
8770
  function isCodexAuthQuotaSnapshot(value) {
8580
8771
  if (!value || typeof value !== 'object') {
8581
8772
  return false;
package/dist/i18n.d.ts CHANGED
@@ -125,11 +125,14 @@ declare const MESSAGES: {
125
125
  readonly status_runtime_weixin: "- Weixin default runtime: connected {connected}, active turns {turns}";
126
126
  readonly status_auth_mirror_none: "Last auth mirror: none recorded";
127
127
  readonly status_auth_mirror_synced: "Last auth mirror: {candidate} from {source} at {time}";
128
+ readonly status_auth_sync: "Cross-node auth sync: node {node}, peers {peers}, pending imports {pending}";
129
+ readonly status_auth_sync_error: "Cross-node auth sync error: {value}";
128
130
  readonly status_last_update_none: "Last service update: none recorded";
129
131
  readonly status_last_update: "Last service update: {from} -> {to} at {time}";
130
132
  readonly status_last_codex_update: "Last Codex update: {value}";
131
133
  readonly update_started: "FoxClaw update started. I will report here after installation, checks, and service restart complete.";
132
134
  readonly update_succeeded: "FoxClaw updated and restarted: {from} -> {to}.";
135
+ readonly update_codex_result: "Codex CLI: {from} -> {to}.";
133
136
  readonly update_failed: "FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.";
134
137
  readonly update_unavailable: "Self-update is unavailable in this runtime. Run foxclaw update in a terminal.";
135
138
  readonly update_already_running: "A FoxClaw update is already running. I will report here when it finishes.";
@@ -137,7 +140,8 @@ declare const MESSAGES: {
137
140
  readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
138
141
  readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
139
142
  readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
140
- readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]";
143
+ readonly usage_auth: "Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]";
144
+ readonly usage_auth_sync: "Usage: /auth sync <status|test|push all>";
141
145
  readonly usage_auth_add: "Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.";
142
146
  readonly auth_list_title: "Codex auth files:";
143
147
  readonly auth_bot: "Bot runtime: {value}";
@@ -179,11 +183,13 @@ declare const MESSAGES: {
179
183
  readonly auth_refresh_all_cancelled: "Refresh all cancelled.";
180
184
  readonly auth_refresh_all_starting: "Refreshing all ChatGPT auth candidates. Every runtime must stay idle...";
181
185
  readonly auth_refresh_all_blocked_active: "Cannot refresh all auth candidates while any runtime, approval, input, login, or auth mirror write is active. Wait or use /interrupt first.";
186
+ readonly auth_refresh_all_lease_failed: "Cross-node refresh lock was not granted: {error}";
182
187
  readonly auth_refresh_all_done: "Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.";
183
188
  readonly auth_refresh_all_refreshed: "Refreshed: {value}";
184
189
  readonly auth_refresh_all_skipped: "Skipped non-ChatGPT/invalid candidates: {value}";
185
190
  readonly auth_refresh_all_failed: "Failed: {value}";
186
191
  readonly auth_auto_switching: "Codex auth problem detected ({error}). Switching: {from} -> {to}...";
192
+ readonly auth_auto_recovered_current: "Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.";
187
193
  readonly auth_auto_done: "Auto-switched Codex auth: {from} -> {to}.";
188
194
  readonly auth_auto_retrying: "Retrying the same request with the new auth...";
189
195
  readonly auth_auto_retry_thread_missing: "Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.";
@@ -196,6 +202,20 @@ declare const MESSAGES: {
196
202
  readonly auth_add_cancelled: "New auth login cancelled. Restored previous auth.";
197
203
  readonly auth_add_reverted: "Restored previous auth.";
198
204
  readonly auth_add_missing_file: "Login completed, but the new auth file was not created: {value}";
205
+ readonly auth_sync_disabled: "Cross-node auth sync is disabled.";
206
+ readonly auth_sync_test_sent: "Auth sync test ping sent to {count} peer(s).";
207
+ readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
208
+ readonly auth_sync_push_done: "Auth sync push complete: sent {sent}, skipped {skipped}.";
209
+ readonly auth_sync_status_title: "Cross-node auth sync:";
210
+ readonly auth_sync_status_node: "Node: {value}";
211
+ readonly auth_sync_status_peers: "Peers: {value}";
212
+ readonly auth_sync_status_pending: "Pending imports: {value}";
213
+ readonly auth_sync_status_sent: "Last sent: {value}";
214
+ readonly auth_sync_status_received: "Last received: {value}";
215
+ readonly auth_sync_status_imported: "Last import: {value}";
216
+ readonly auth_sync_status_pull: "Last pull: {value}";
217
+ readonly auth_sync_status_lease: "Active refresh lease: {value}";
218
+ readonly auth_sync_status_error: "Last error: {value}";
199
219
  readonly button_login_device: "🔑 Login";
200
220
  readonly button_auth_reload: "🔄 Reload auth";
201
221
  readonly button_auth_refresh_all_confirm: "⚠️ Accept risk & refresh";
@@ -730,11 +750,14 @@ declare const MESSAGES: {
730
750
  readonly status_runtime_weixin: "- 微信默认运行时:连接 {connected},进行中回复 {turns}";
731
751
  readonly status_auth_mirror_none: "最近 auth 镜像:暂无记录";
732
752
  readonly status_auth_mirror_synced: "最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步";
753
+ readonly status_auth_sync: "跨节点 auth 同步:节点 {node},peer {peers},待导入 {pending}";
754
+ readonly status_auth_sync_error: "跨节点 auth 同步错误:{value}";
733
755
  readonly status_last_update_none: "最近服务升级:暂无记录";
734
756
  readonly status_last_update: "最近服务升级:{from} -> {to}({time})";
735
757
  readonly status_last_codex_update: "最近 Codex 升级:{value}";
736
758
  readonly update_started: "已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。";
737
759
  readonly update_succeeded: "FoxClaw 已升级并重启:{from} -> {to}。";
760
+ readonly update_codex_result: "Codex CLI:{from} -> {to}。";
738
761
  readonly update_failed: "FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。";
739
762
  readonly update_unavailable: "当前运行方式不支持自升级,请在终端运行 foxclaw update。";
740
763
  readonly update_already_running: "FoxClaw 升级已经在进行中,结束后我会在这里回报结果。";
@@ -742,7 +765,8 @@ declare const MESSAGES: {
742
765
  readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
743
766
  readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
744
767
  readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
745
- readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]";
768
+ readonly usage_auth: "用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]";
769
+ readonly usage_auth_sync: "用法:/auth sync <status|test|push all>";
746
770
  readonly usage_auth_add: "用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。";
747
771
  readonly auth_list_title: "Codex auth 文件:";
748
772
  readonly auth_bot: "Bot runtime:{value}";
@@ -784,11 +808,13 @@ declare const MESSAGES: {
784
808
  readonly auth_refresh_all_cancelled: "已取消刷新全部。";
785
809
  readonly auth_refresh_all_starting: "正在刷新全部 ChatGPT auth 候选。执行期间所有 runtime 必须保持空闲...";
786
810
  readonly auth_refresh_all_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。";
811
+ readonly auth_refresh_all_lease_failed: "跨节点刷新锁未授予:{error}";
787
812
  readonly auth_refresh_all_done: "全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。";
788
813
  readonly auth_refresh_all_refreshed: "已刷新:{value}";
789
814
  readonly auth_refresh_all_skipped: "已跳过非 ChatGPT/无效候选:{value}";
790
815
  readonly auth_refresh_all_failed: "失败:{value}";
791
816
  readonly auth_auto_switching: "检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...";
817
+ readonly auth_auto_recovered_current: "检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。";
792
818
  readonly auth_auto_done: "已自动切换 Codex auth:{from} -> {to}。";
793
819
  readonly auth_auto_retrying: "正在用新的 auth 重试同一条请求...";
794
820
  readonly auth_auto_retry_thread_missing: "Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。";
@@ -801,6 +827,20 @@ declare const MESSAGES: {
801
827
  readonly auth_add_cancelled: "新 auth 登录已取消,已恢复之前的 auth。";
802
828
  readonly auth_add_reverted: "已恢复之前的 auth。";
803
829
  readonly auth_add_missing_file: "登录已完成,但没有创建新的 auth 文件:{value}";
830
+ readonly auth_sync_disabled: "跨节点 auth 同步未启用。";
831
+ readonly auth_sync_test_sent: "已向 {count} 个 peer 发送 auth sync 测试 ping。";
832
+ readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
833
+ readonly auth_sync_push_done: "auth 同步推送完成:已发送 {sent},已跳过 {skipped}。";
834
+ readonly auth_sync_status_title: "跨节点 auth 同步:";
835
+ readonly auth_sync_status_node: "节点:{value}";
836
+ readonly auth_sync_status_peers: "Peer:{value}";
837
+ readonly auth_sync_status_pending: "待导入:{value}";
838
+ readonly auth_sync_status_sent: "最近发送:{value}";
839
+ readonly auth_sync_status_received: "最近接收:{value}";
840
+ readonly auth_sync_status_imported: "最近导入:{value}";
841
+ readonly auth_sync_status_pull: "最近拉取:{value}";
842
+ readonly auth_sync_status_lease: "当前刷新锁:{value}";
843
+ readonly auth_sync_status_error: "最近错误:{value}";
804
844
  readonly button_login_device: "🔑 设备登录";
805
845
  readonly button_auth_reload: "🔄 重载 auth";
806
846
  readonly button_auth_refresh_all_confirm: "⚠️ 接受风险并刷新";
package/dist/i18n.js CHANGED
@@ -123,11 +123,14 @@ const MESSAGES = {
123
123
  status_runtime_weixin: '- Weixin default runtime: connected {connected}, active turns {turns}',
124
124
  status_auth_mirror_none: 'Last auth mirror: none recorded',
125
125
  status_auth_mirror_synced: 'Last auth mirror: {candidate} from {source} at {time}',
126
+ status_auth_sync: 'Cross-node auth sync: node {node}, peers {peers}, pending imports {pending}',
127
+ status_auth_sync_error: 'Cross-node auth sync error: {value}',
126
128
  status_last_update_none: 'Last service update: none recorded',
127
129
  status_last_update: 'Last service update: {from} -> {to} at {time}',
128
130
  status_last_codex_update: 'Last Codex update: {value}',
129
131
  update_started: 'FoxClaw update started. I will report here after installation, checks, and service restart complete.',
130
132
  update_succeeded: 'FoxClaw updated and restarted: {from} -> {to}.',
133
+ update_codex_result: 'Codex CLI: {from} -> {to}.',
131
134
  update_failed: 'FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.',
132
135
  update_unavailable: 'Self-update is unavailable in this runtime. Run foxclaw update in a terminal.',
133
136
  update_already_running: 'A FoxClaw update is already running. I will report here when it finishes.',
@@ -135,7 +138,8 @@ const MESSAGES = {
135
138
  auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
136
139
  auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
137
140
  auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
138
- usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|add <name>]',
141
+ usage_auth: 'Usage: /auth [list [keyword]|filter <all|enabled|attention>|page <n>|use <n>|enable <n>|disable <n>|reload|refresh all [confirm]|sync <status|test|push all>|add <name>]',
142
+ usage_auth_sync: 'Usage: /auth sync <status|test|push all>',
139
143
  usage_auth_add: 'Usage: /auth add <name>. Use letters, numbers, dot, dash, or underscore.',
140
144
  auth_list_title: 'Codex auth files:',
141
145
  auth_bot: 'Bot runtime: {value}',
@@ -177,11 +181,13 @@ const MESSAGES = {
177
181
  auth_refresh_all_cancelled: 'Refresh all cancelled.',
178
182
  auth_refresh_all_starting: 'Refreshing all ChatGPT auth candidates. Every runtime must stay idle...',
179
183
  auth_refresh_all_blocked_active: 'Cannot refresh all auth candidates while any runtime, approval, input, login, or auth mirror write is active. Wait or use /interrupt first.',
184
+ auth_refresh_all_lease_failed: 'Cross-node refresh lock was not granted: {error}',
180
185
  auth_refresh_all_done: 'Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.',
181
186
  auth_refresh_all_refreshed: 'Refreshed: {value}',
182
187
  auth_refresh_all_skipped: 'Skipped non-ChatGPT/invalid candidates: {value}',
183
188
  auth_refresh_all_failed: 'Failed: {value}',
184
189
  auth_auto_switching: 'Codex auth problem detected ({error}). Switching: {from} -> {to}...',
190
+ auth_auto_recovered_current: 'Codex auth problem detected ({error}). Recovered a newer same-account credential for {value} and restarted Codex app-server.',
185
191
  auth_auto_done: 'Auto-switched Codex auth: {from} -> {to}.',
186
192
  auth_auto_retrying: 'Retrying the same request with the new auth...',
187
193
  auth_auto_retry_thread_missing: 'Auth was switched, but the original thread is no longer available ({threadId}). Retry was stopped to avoid creating duplicate sessions.',
@@ -194,6 +200,20 @@ const MESSAGES = {
194
200
  auth_add_cancelled: 'New auth login cancelled. Restored previous auth.',
195
201
  auth_add_reverted: 'Restored previous auth.',
196
202
  auth_add_missing_file: 'Login completed, but the new auth file was not created: {value}',
203
+ auth_sync_disabled: 'Cross-node auth sync is disabled.',
204
+ auth_sync_test_sent: 'Auth sync test ping sent to {count} peer(s).',
205
+ auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
206
+ auth_sync_push_done: 'Auth sync push complete: sent {sent}, skipped {skipped}.',
207
+ auth_sync_status_title: 'Cross-node auth sync:',
208
+ auth_sync_status_node: 'Node: {value}',
209
+ auth_sync_status_peers: 'Peers: {value}',
210
+ auth_sync_status_pending: 'Pending imports: {value}',
211
+ auth_sync_status_sent: 'Last sent: {value}',
212
+ auth_sync_status_received: 'Last received: {value}',
213
+ auth_sync_status_imported: 'Last import: {value}',
214
+ auth_sync_status_pull: 'Last pull: {value}',
215
+ auth_sync_status_lease: 'Active refresh lease: {value}',
216
+ auth_sync_status_error: 'Last error: {value}',
197
217
  button_login_device: '🔑 Login',
198
218
  button_auth_reload: '🔄 Reload auth',
199
219
  button_auth_refresh_all_confirm: '⚠️ Accept risk & refresh',
@@ -728,11 +748,14 @@ const MESSAGES = {
728
748
  status_runtime_weixin: '- 微信默认运行时:连接 {connected},进行中回复 {turns}',
729
749
  status_auth_mirror_none: '最近 auth 镜像:暂无记录',
730
750
  status_auth_mirror_synced: '最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步',
751
+ status_auth_sync: '跨节点 auth 同步:节点 {node},peer {peers},待导入 {pending}',
752
+ status_auth_sync_error: '跨节点 auth 同步错误:{value}',
731
753
  status_last_update_none: '最近服务升级:暂无记录',
732
754
  status_last_update: '最近服务升级:{from} -> {to}({time})',
733
755
  status_last_codex_update: '最近 Codex 升级:{value}',
734
756
  update_started: '已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。',
735
757
  update_succeeded: 'FoxClaw 已升级并重启:{from} -> {to}。',
758
+ update_codex_result: 'Codex CLI:{from} -> {to}。',
736
759
  update_failed: 'FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。',
737
760
  update_unavailable: '当前运行方式不支持自升级,请在终端运行 foxclaw update。',
738
761
  update_already_running: 'FoxClaw 升级已经在进行中,结束后我会在这里回报结果。',
@@ -740,7 +763,8 @@ const MESSAGES = {
740
763
  auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
741
764
  auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
742
765
  auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
743
- usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|add <名称>]',
766
+ usage_auth: '用法:/auth [list [关键词]|filter <all|enabled|attention>|page <页码>|use <编号>|enable <编号>|disable <编号>|reload|refresh all [confirm]|sync <status|test|push all>|add <名称>]',
767
+ usage_auth_sync: '用法:/auth sync <status|test|push all>',
744
768
  usage_auth_add: '用法:/auth add <名称>。名称只能包含字母、数字、点、短横线或下划线。',
745
769
  auth_list_title: 'Codex auth 文件:',
746
770
  auth_bot: 'Bot runtime:{value}',
@@ -782,11 +806,13 @@ const MESSAGES = {
782
806
  auth_refresh_all_cancelled: '已取消刷新全部。',
783
807
  auth_refresh_all_starting: '正在刷新全部 ChatGPT auth 候选。执行期间所有 runtime 必须保持空闲...',
784
808
  auth_refresh_all_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。',
809
+ auth_refresh_all_lease_failed: '跨节点刷新锁未授予:{error}',
785
810
  auth_refresh_all_done: '全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。',
786
811
  auth_refresh_all_refreshed: '已刷新:{value}',
787
812
  auth_refresh_all_skipped: '已跳过非 ChatGPT/无效候选:{value}',
788
813
  auth_refresh_all_failed: '失败:{value}',
789
814
  auth_auto_switching: '检测到 Codex auth 问题({error}),正在切换:{from} -> {to}...',
815
+ auth_auto_recovered_current: '检测到 Codex auth 问题({error}),已为 {value} 恢复同账号较新凭据并重启 Codex app-server。',
790
816
  auth_auto_done: '已自动切换 Codex auth:{from} -> {to}。',
791
817
  auth_auto_retrying: '正在用新的 auth 重试同一条请求...',
792
818
  auth_auto_retry_thread_missing: 'Auth 已切换,但原线程已经不可用({threadId})。已停止重试,避免创建重复 session。',
@@ -799,6 +825,20 @@ const MESSAGES = {
799
825
  auth_add_cancelled: '新 auth 登录已取消,已恢复之前的 auth。',
800
826
  auth_add_reverted: '已恢复之前的 auth。',
801
827
  auth_add_missing_file: '登录已完成,但没有创建新的 auth 文件:{value}',
828
+ auth_sync_disabled: '跨节点 auth 同步未启用。',
829
+ auth_sync_test_sent: '已向 {count} 个 peer 发送 auth sync 测试 ping。',
830
+ auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
831
+ auth_sync_push_done: 'auth 同步推送完成:已发送 {sent},已跳过 {skipped}。',
832
+ auth_sync_status_title: '跨节点 auth 同步:',
833
+ auth_sync_status_node: '节点:{value}',
834
+ auth_sync_status_peers: 'Peer:{value}',
835
+ auth_sync_status_pending: '待导入:{value}',
836
+ auth_sync_status_sent: '最近发送:{value}',
837
+ auth_sync_status_received: '最近接收:{value}',
838
+ auth_sync_status_imported: '最近导入:{value}',
839
+ auth_sync_status_pull: '最近拉取:{value}',
840
+ auth_sync_status_lease: '当前刷新锁:{value}',
841
+ auth_sync_status_error: '最近错误:{value}',
802
842
  button_login_device: '🔑 设备登录',
803
843
  button_auth_reload: '🔄 重载 auth',
804
844
  button_auth_refresh_all_confirm: '⚠️ 接受风险并刷新',