@foxden-app/foxclaw 0.5.4 → 0.5.6

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.6 - 2026-06-04
6
+
7
+ ### 中文
8
+ - 新增后台主动 auth 刷新:已启用的 ChatGPT 候选 `last_refresh` 超过 9 天时,FoxClaw 每小时检查并在全局空闲后只刷新这批候选。
9
+ - 主动刷新会先申请跨节点刷新锁;没有启用跨节点同步时使用同进程本地锁,避免同节点多个 bot 并发轮换 refresh token。
10
+ - 跨节点刷新锁持有窗口延长到 10 分钟,刷新中会让 auth sync 处于非空闲状态,重复申请会被拒绝;完成后会私聊报告刷新、跳过和失败数量。
11
+ - 中文/英文用户手册和跨节点同步文档补充 9 天主动刷新策略,明确 `/auth refresh all confirm` 仍是人工维护命令。
12
+
13
+ ### English
14
+ - Added background proactive auth refresh: every hour FoxClaw checks enabled ChatGPT candidates whose `last_refresh` is older than 9 days and refreshes only that due batch after the node is globally idle.
15
+ - Proactive refresh first requests the cross-node refresh lease; when cross-node sync is disabled, an in-process local lease prevents same-node bot runtimes from rotating refresh tokens concurrently.
16
+ - Extended the cross-node refresh lease window to 10 minutes, marks auth sync non-idle while a lease is active, rejects duplicate lease requests, and reports refreshed/skipped/failed counts in private chat.
17
+ - Updated the Chinese and English user manuals plus cross-node sync docs with the 9-day proactive policy, while keeping `/auth refresh all confirm` as a manual maintenance command.
18
+
19
+ ## 0.5.5 - 2026-06-04
20
+
21
+ ### 中文
22
+ - 在 `/login_device`、`/auth add <name>` 和 `/auth` 面板设备登录返回内容中加入 ChatGPT 设备代码授权前置条件:在 ChatGPT 左下角用户名菜单进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。
23
+ - README、安装指南、用户手册和故障排查补充同一指引,并强调 workspace 账号可能需要管理员允许设备码登录,以及设备代码不要分享给他人或粘贴到不可信页面。
24
+
25
+ ### English
26
+ - Added a device-code authorization prerequisite to `/login_device`, `/auth add <name>`, and the `/auth` panel Login response: in ChatGPT, open the lower-left username menu, then Settings > Security, and enable device code authorization for Codex.
27
+ - Added the same guidance to the README, install guide, user manual, and troubleshooting docs, including workspace-admin requirements and the warning not to share device codes or paste them into untrusted pages.
28
+
5
29
  ## 0.5.4 - 2026-06-04
6
30
 
7
31
  ### 中文
package/README.md CHANGED
@@ -50,6 +50,7 @@ FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex,
50
50
 
51
51
  - macOS 或 Linux,`codex` CLI 可用
52
52
  - Codex 已完成登录认证
53
+ - 如果要通过 `/auth add` 或 `/login_device` 从 Telegram 发起设备码登录,需要先在 ChatGPT 左下角用户名菜单进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”
53
54
  - Node.js 24+
54
55
  - 一个 `@BotFather` 创建的 Telegram bot token
55
56
  - 你的 Telegram 数字用户 ID
@@ -129,6 +130,8 @@ FoxClaw 的一大特色是自动多账号切换。当一个账号触发用量限
129
130
  3. 用 `/auth` 查看所有候选账号状态。
130
131
  4. 用 `/auth enable <n>` / `/auth disable <n>` 控制哪些账号参与自动轮换。
131
132
 
133
+ 通过 `/auth add <name>` 或 `/login_device` 进行设备码登录前,需要在 ChatGPT 网页左下角点用户名,进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。设备代码等同于一次登录授权,切勿转发给他人或粘贴到不可信页面。
134
+
132
135
  候选较多时,`/auth` 每页显示 8 个账号,并提供翻页、`全部 / 已启用 / 需关注` 筛选。也可以用 `/auth list <关键词>` 搜索文件名,或用 `/auth page <页码>` 直接跳页。文本列表的额度窗口按 Codex 实际返回值展示,例如 `5h:20|7d:25` 或单月窗口 `30d:97`;按钮只显示紧凑的两个剩余数字,例如 `20|25`,未知值显示为 `—`。面板会省略候选名中重复的 `auth.json_` 前缀,磁盘文件名保持不变。
133
136
 
134
137
  当 Codex 报告用量限制错误时,FoxClaw 会自动:
package/README_EN.md CHANGED
@@ -50,6 +50,7 @@ The minimum install needs one or more Telegram bot tokens, your numeric Telegram
50
50
 
51
51
  - macOS or Linux with a working `codex` CLI
52
52
  - Codex authenticated on the host machine
53
+ - If you want to start device-code login from Telegram with `/auth add` or `/login_device`, first open the username menu in the lower-left corner of ChatGPT, then Settings > Security, and enable device code authorization for Codex
53
54
  - Node.js 24+
54
55
  - A Telegram bot token from `@BotFather`
55
56
  - Your Telegram numeric user id
@@ -129,6 +130,8 @@ Setup:
129
130
  3. Use `/auth` to view all candidate account statuses.
130
131
  4. Use `/auth enable <n>` / `/auth disable <n>` to control which accounts participate in auto-rotation.
131
132
 
133
+ Before using `/auth add <name>` or `/login_device` for device-code login, open ChatGPT, click the username menu in the lower-left corner, go to Settings > Security, and enable device code authorization for Codex. Treat the device code as a login grant: never forward it or paste it into an untrusted page.
134
+
132
135
  For larger inventories, `/auth` shows 8 candidates per page with paging and `All / Enabled / Attention` filters. Use `/auth list <keyword>` to search filenames or `/auth page <n>` to jump directly. Candidate rows use the quota windows reported by Codex, for example `5h:20|7d:25` or a single monthly window such as `30d:97`; buttons use compact remaining numbers such as `20|25`, with `—` for unknown values. The panel omits the repeated `auth.json_` prefix from candidate labels without renaming files on disk.
133
136
 
134
137
  When Codex reports a usage-limit error, FoxClaw automatically:
@@ -8,7 +8,7 @@ const NONCE_RETENTION_MS = 7 * 24 * 60 * 60_000;
8
8
  const PULL_TIMEOUT_MS = 12_000;
9
9
  const TEST_TIMEOUT_MS = 8_000;
10
10
  const LEASE_TIMEOUT_MS = 8_000;
11
- const LEASE_TTL_MS = 60_000;
11
+ const LEASE_TTL_MS = 10 * 60_000;
12
12
  const REMOTE_ACCESS_TOKEN_MIN_TTL_MS = 60_000;
13
13
  export class CrossNodeAuthSync {
14
14
  config;
@@ -116,7 +116,9 @@ export class CrossNodeAuthSync {
116
116
  && !this.importProcessorActive
117
117
  && this.pendingPulls.size === 0
118
118
  && this.pendingLeases.size === 0
119
- && this.pendingTests.size === 0;
119
+ && this.pendingTests.size === 0
120
+ && this.activeLocalLease === null
121
+ && this.activeRemoteLease === null;
120
122
  }
121
123
  async publishCandidate(candidateName) {
122
124
  if (!this.isReady())
@@ -263,6 +265,10 @@ export class CrossNodeAuthSync {
263
265
  return result;
264
266
  }
265
267
  async acquireRefreshLease(reason) {
268
+ this.expireLeases();
269
+ if (this.activeLocalLease || this.activeRemoteLease) {
270
+ return { ok: false, leaseId: null, reason: 'another refresh lease is active' };
271
+ }
266
272
  if (!this.isReady() || this.peers.length === 0) {
267
273
  const leaseId = crypto.randomUUID();
268
274
  this.activeLocalLease = { leaseId, expiresAt: Date.now() + LEASE_TTL_MS };
@@ -80,6 +80,8 @@ export declare class BridgeSessionCore {
80
80
  private approvalTimers;
81
81
  private submittedUserInputTimers;
82
82
  private selfUpdatePollTimer;
83
+ private proactiveAuthRefreshTimer;
84
+ private proactiveAuthRefreshInProgress;
83
85
  private attachedThreads;
84
86
  private botUsername;
85
87
  private lastError;
@@ -262,6 +264,11 @@ export declare class BridgeSessionCore {
262
264
  private pollSelfUpdateStatus;
263
265
  private formatSelfUpdateResult;
264
266
  private formatCodexUpdateResult;
267
+ private scheduleProactiveAuthRefresh;
268
+ private clearProactiveAuthRefreshTimer;
269
+ private runProactiveAuthRefresh;
270
+ private proactiveAuthRefreshLocale;
271
+ private notifyProactiveAuthRefresh;
265
272
  private handleAuthCommand;
266
273
  private handleAuthSyncCommand;
267
274
  private handleAuthRefreshAllCommand;
@@ -318,6 +325,7 @@ export declare class BridgeSessionCore {
318
325
  private recoverCodexAuthCandidate;
319
326
  private syncCodexAuthCandidate;
320
327
  private refreshAllCodexAuthCandidates;
328
+ private refreshCodexAuthCandidates;
321
329
  private buildNativeCollaborationMode;
322
330
  private buildCodexUsageStatusLines;
323
331
  private buildCodexLocalUsageStatusLines;
@@ -30,6 +30,9 @@ const CODEX_AUTH_QUOTA_SNAPSHOT_FILENAME = 'codex-auth-quota.json';
30
30
  const CODEX_AUTH_LIST_PAGE_SIZE = 8;
31
31
  const CODEX_AUTH_LOW_QUOTA_PERCENT = 10;
32
32
  const CODEX_AUTH_STALE_CREDENTIAL_DAYS = 8;
33
+ const CODEX_AUTH_PROACTIVE_REFRESH_DAYS = 9;
34
+ const CODEX_AUTH_PROACTIVE_REFRESH_INTERVAL_MS = 60 * 60_000;
35
+ const CODEX_AUTH_PROACTIVE_REFRESH_INITIAL_DELAY_MS = 5 * 60_000;
33
36
  const USER_INPUT_SUBMITTED_NOTICE_MS = 90_000;
34
37
  const SELF_UPDATE_STATUS_POLL_MS = 1000;
35
38
  const PLAN_IMPLEMENTATION_CODING_MESSAGE = 'Implement the plan.';
@@ -132,6 +135,8 @@ export class BridgeSessionCore {
132
135
  approvalTimers = new Map();
133
136
  submittedUserInputTimers = new Map();
134
137
  selfUpdatePollTimer = null;
138
+ proactiveAuthRefreshTimer = null;
139
+ proactiveAuthRefreshInProgress = false;
135
140
  attachedThreads = new Set();
136
141
  botUsername = null;
137
142
  lastError = null;
@@ -206,6 +211,7 @@ export class BridgeSessionCore {
206
211
  });
207
212
  this.updateStatus();
208
213
  this.scheduleSelfUpdateStatusPoll(0);
214
+ this.scheduleProactiveAuthRefresh(CODEX_AUTH_PROACTIVE_REFRESH_INITIAL_DELAY_MS);
209
215
  }
210
216
  /** Begin Telegram Bot API long-polling after handlers and Codex are ready. */
211
217
  async startTelegramPolling() {
@@ -249,6 +255,7 @@ export class BridgeSessionCore {
249
255
  }
250
256
  this.submittedUserInputTimers.clear();
251
257
  this.clearSelfUpdateStatusPoll();
258
+ this.clearProactiveAuthRefreshTimer();
252
259
  await this.app.stop({ terminateServer: false });
253
260
  this.updateStatus();
254
261
  }
@@ -3960,6 +3967,82 @@ export class BridgeSessionCore {
3960
3967
  }
3961
3968
  return status.codexUpdate ?? null;
3962
3969
  }
3970
+ scheduleProactiveAuthRefresh(delayMs = CODEX_AUTH_PROACTIVE_REFRESH_INTERVAL_MS) {
3971
+ if (this.proactiveAuthRefreshTimer) {
3972
+ return;
3973
+ }
3974
+ this.proactiveAuthRefreshTimer = setTimeout(() => {
3975
+ this.proactiveAuthRefreshTimer = null;
3976
+ void this.runProactiveAuthRefresh().catch((error) => {
3977
+ this.logger.warn('codex.auth_proactive_refresh_failed', { error: toErrorMeta(error) });
3978
+ }).finally(() => {
3979
+ this.scheduleProactiveAuthRefresh();
3980
+ });
3981
+ }, delayMs);
3982
+ this.proactiveAuthRefreshTimer.unref();
3983
+ }
3984
+ clearProactiveAuthRefreshTimer() {
3985
+ if (!this.proactiveAuthRefreshTimer) {
3986
+ return;
3987
+ }
3988
+ clearTimeout(this.proactiveAuthRefreshTimer);
3989
+ this.proactiveAuthRefreshTimer = null;
3990
+ }
3991
+ async runProactiveAuthRefresh() {
3992
+ if (this.proactiveAuthRefreshInProgress || !this.canRunGlobalAuthRefresh()) {
3993
+ return;
3994
+ }
3995
+ const state = await this.listCodexAuthState();
3996
+ const dueCandidates = state.candidates.filter(candidate => !candidate.disabled
3997
+ && candidate.credentialKind === 'chatgpt'
3998
+ && candidate.credentialLastRefreshMs !== null
3999
+ && candidate.credentialLastRefreshMs <= Date.now() - CODEX_AUTH_PROACTIVE_REFRESH_DAYS * 24 * 60 * 60_000);
4000
+ if (dueCandidates.length === 0) {
4001
+ return;
4002
+ }
4003
+ this.proactiveAuthRefreshInProgress = true;
4004
+ const locale = this.proactiveAuthRefreshLocale();
4005
+ await this.notifyProactiveAuthRefresh(locale, t(locale, 'auth_proactive_refresh_starting', {
4006
+ value: dueCandidates.map(candidate => candidate.name).join(', '),
4007
+ }));
4008
+ let lease;
4009
+ try {
4010
+ lease = await this.coordinator?.acquireAuthRefreshLease?.(`proactive auth refresh: ${dueCandidates.map(candidate => candidate.name).join(', ')}`);
4011
+ if (lease && !lease.ok) {
4012
+ this.logger.warn('codex.auth_proactive_refresh_lease_failed', { reason: lease.reason });
4013
+ await this.notifyProactiveAuthRefresh(locale, t(locale, 'auth_proactive_refresh_lease_failed', {
4014
+ error: lease.reason ?? t(locale, 'unknown'),
4015
+ }));
4016
+ return;
4017
+ }
4018
+ const result = await this.refreshCodexAuthCandidates(new Set(dueCandidates.map(candidate => candidate.name)));
4019
+ await this.notifyProactiveAuthRefresh(locale, formatAuthRefreshAllResult(locale, result, 'proactive'));
4020
+ }
4021
+ finally {
4022
+ await this.coordinator?.releaseAuthRefreshLease?.(lease?.leaseId ?? null);
4023
+ this.proactiveAuthRefreshInProgress = false;
4024
+ }
4025
+ }
4026
+ proactiveAuthRefreshLocale() {
4027
+ const identity = this.bot.identity;
4028
+ if (!identity)
4029
+ return 'en';
4030
+ const privateScope = this.store.getTelegramPrivateScope(identity);
4031
+ if (!privateScope)
4032
+ return 'en';
4033
+ return this.localeForChat(privateScope.scopeId);
4034
+ }
4035
+ async notifyProactiveAuthRefresh(locale, message) {
4036
+ const identity = this.bot.identity;
4037
+ if (!identity)
4038
+ return;
4039
+ const privateScope = this.store.getTelegramPrivateScope(identity);
4040
+ if (!privateScope)
4041
+ return;
4042
+ await this.bot.sendMessage(privateScope.chatId, message).catch((error) => {
4043
+ this.logger.warn('codex.auth_proactive_refresh_notify_failed', { error: toErrorMeta(error) });
4044
+ });
4045
+ }
3963
4046
  async handleAuthCommand(scopeId, locale, args) {
3964
4047
  const action = args[0]?.toLowerCase() ?? 'list';
3965
4048
  if (action === 'sync') {
@@ -4175,6 +4258,7 @@ export class BridgeSessionCore {
4175
4258
  });
4176
4259
  await this.sendMessage(scopeId, [
4177
4260
  t(locale, 'auth_add_started', { value: candidateName }),
4261
+ t(locale, 'login_device_prereq'),
4178
4262
  t(locale, 'login_url', { value: login.verificationUrl }),
4179
4263
  t(locale, 'login_code', { value: login.userCode }),
4180
4264
  t(locale, 'login_id', { value: login.loginId }),
@@ -4227,6 +4311,7 @@ export class BridgeSessionCore {
4227
4311
  this.pendingLoginScopesById.set(login.loginId, scopeId);
4228
4312
  await this.sendMessage(scopeId, [
4229
4313
  t(locale, 'login_device_started'),
4314
+ t(locale, 'login_device_prereq'),
4230
4315
  t(locale, 'login_url', { value: login.verificationUrl }),
4231
4316
  t(locale, 'login_code', { value: login.userCode }),
4232
4317
  t(locale, 'login_id', { value: login.loginId }),
@@ -4900,12 +4985,18 @@ export class BridgeSessionCore {
4900
4985
  }
4901
4986
  }
4902
4987
  async refreshAllCodexAuthCandidates() {
4988
+ return this.refreshCodexAuthCandidates();
4989
+ }
4990
+ async refreshCodexAuthCandidates(candidateNames = null) {
4903
4991
  if (this.authRefreshAllInProgress) {
4904
4992
  throw new UserFacingError('Auth refresh all is already running.');
4905
4993
  }
4906
4994
  const initialState = await this.listCodexAuthState();
4907
4995
  const result = { refreshed: [], skipped: [], failed: [] };
4908
- if (initialState.candidates.length === 0) {
4996
+ const candidates = candidateNames
4997
+ ? initialState.candidates.filter(candidate => candidateNames.has(candidate.name))
4998
+ : initialState.candidates;
4999
+ if (candidates.length === 0) {
4909
5000
  return result;
4910
5001
  }
4911
5002
  const authStat = await fs.lstat(initialState.authPath).catch(() => null);
@@ -4915,7 +5006,7 @@ export class BridgeSessionCore {
4915
5006
  let changedAuthTarget = false;
4916
5007
  this.authRefreshAllInProgress = true;
4917
5008
  try {
4918
- for (const candidate of initialState.candidates) {
5009
+ for (const candidate of candidates) {
4919
5010
  const before = await readChatGptAuthMetadata(candidate.path);
4920
5011
  if (!before) {
4921
5012
  result.skipped.push(candidate.name);
@@ -8102,8 +8193,8 @@ function authRefreshAllConfirmKeyboard(locale, record) {
8102
8193
  [{ text: t(locale, 'button_cancel'), callback_data: `auth:${record.localId}:refresh_all_cancel` }],
8103
8194
  ];
8104
8195
  }
8105
- function formatAuthRefreshAllResult(locale, result) {
8106
- const lines = [t(locale, 'auth_refresh_all_done', {
8196
+ function formatAuthRefreshAllResult(locale, result, mode = 'manual') {
8197
+ const lines = [t(locale, mode === 'proactive' ? 'auth_proactive_refresh_done' : 'auth_refresh_all_done', {
8107
8198
  refreshed: String(result.refreshed.length),
8108
8199
  skipped: String(result.skipped.length),
8109
8200
  failed: String(result.failed.length),
package/dist/i18n.d.ts CHANGED
@@ -186,6 +186,9 @@ declare const MESSAGES: {
186
186
  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.";
187
187
  readonly auth_refresh_all_lease_failed: "Cross-node refresh lock was not granted: {error}";
188
188
  readonly auth_refresh_all_done: "Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.";
189
+ readonly auth_proactive_refresh_starting: "Proactive auth refresh started for candidates older than 9 days: {value}";
190
+ readonly auth_proactive_refresh_lease_failed: "Proactive auth refresh skipped because the cross-node refresh lock was not granted: {error}";
191
+ readonly auth_proactive_refresh_done: "Proactive auth refresh complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.";
189
192
  readonly auth_refresh_all_refreshed: "Refreshed: {value}";
190
193
  readonly auth_refresh_all_skipped: "Skipped non-ChatGPT/invalid candidates: {value}";
191
194
  readonly auth_refresh_all_failed: "Failed: {value}";
@@ -474,6 +477,7 @@ declare const MESSAGES: {
474
477
  readonly usage_quota_nudge: "Usage: /quota_nudge <credits|usage_limit> confirm";
475
478
  readonly quota_nudge_sent: "Nudge email requested.";
476
479
  readonly login_device_started: "Device login started.";
480
+ readonly login_device_prereq: "Prerequisite: in ChatGPT, open your username menu at the lower left, then Settings > Security, and enable device code authorization for Codex. For workspace accounts, an admin may need to allow device code login. Never share the device code.";
477
481
  readonly login_url: "URL: {value}";
478
482
  readonly login_code: "Code: {value}";
479
483
  readonly login_id: "Login id: {value}";
@@ -816,6 +820,9 @@ declare const MESSAGES: {
816
820
  readonly auth_refresh_all_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。";
817
821
  readonly auth_refresh_all_lease_failed: "跨节点刷新锁未授予:{error}";
818
822
  readonly auth_refresh_all_done: "全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。";
823
+ readonly auth_proactive_refresh_starting: "开始主动刷新超过 9 天未更新的 auth 候选:{value}";
824
+ readonly auth_proactive_refresh_lease_failed: "主动 auth 刷新已跳过:跨节点刷新锁未授予:{error}";
825
+ readonly auth_proactive_refresh_done: "主动 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。";
819
826
  readonly auth_refresh_all_refreshed: "已刷新:{value}";
820
827
  readonly auth_refresh_all_skipped: "已跳过非 ChatGPT/无效候选:{value}";
821
828
  readonly auth_refresh_all_failed: "失败:{value}";
@@ -1104,6 +1111,7 @@ declare const MESSAGES: {
1104
1111
  readonly usage_quota_nudge: "用法:/quota_nudge <credits|usage_limit> confirm";
1105
1112
  readonly quota_nudge_sent: "已请求发送额度提醒邮件。";
1106
1113
  readonly login_device_started: "已开始设备码登录。";
1114
+ readonly login_device_prereq: "前置条件:在 ChatGPT 左下角点用户名,进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。Workspace 账号可能需要管理员允许设备码登录。切勿分享设备代码。";
1107
1115
  readonly login_url: "地址:{value}";
1108
1116
  readonly login_code: "验证码:{value}";
1109
1117
  readonly login_id: "登录 ID:{value}";
package/dist/i18n.js CHANGED
@@ -184,6 +184,9 @@ const MESSAGES = {
184
184
  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.',
185
185
  auth_refresh_all_lease_failed: 'Cross-node refresh lock was not granted: {error}',
186
186
  auth_refresh_all_done: 'Auth refresh all complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.',
187
+ auth_proactive_refresh_starting: 'Proactive auth refresh started for candidates older than 9 days: {value}',
188
+ auth_proactive_refresh_lease_failed: 'Proactive auth refresh skipped because the cross-node refresh lock was not granted: {error}',
189
+ auth_proactive_refresh_done: 'Proactive auth refresh complete: {refreshed} refreshed, {skipped} skipped, {failed} failed.',
187
190
  auth_refresh_all_refreshed: 'Refreshed: {value}',
188
191
  auth_refresh_all_skipped: 'Skipped non-ChatGPT/invalid candidates: {value}',
189
192
  auth_refresh_all_failed: 'Failed: {value}',
@@ -472,6 +475,7 @@ const MESSAGES = {
472
475
  usage_quota_nudge: 'Usage: /quota_nudge <credits|usage_limit> confirm',
473
476
  quota_nudge_sent: 'Nudge email requested.',
474
477
  login_device_started: 'Device login started.',
478
+ login_device_prereq: 'Prerequisite: in ChatGPT, open your username menu at the lower left, then Settings > Security, and enable device code authorization for Codex. For workspace accounts, an admin may need to allow device code login. Never share the device code.',
475
479
  login_url: 'URL: {value}',
476
480
  login_code: 'Code: {value}',
477
481
  login_id: 'Login id: {value}',
@@ -814,6 +818,9 @@ const MESSAGES = {
814
818
  auth_refresh_all_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能刷新全部 auth。请先等待,或使用 /interrupt。',
815
819
  auth_refresh_all_lease_failed: '跨节点刷新锁未授予:{error}',
816
820
  auth_refresh_all_done: '全部 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。',
821
+ auth_proactive_refresh_starting: '开始主动刷新超过 9 天未更新的 auth 候选:{value}',
822
+ auth_proactive_refresh_lease_failed: '主动 auth 刷新已跳过:跨节点刷新锁未授予:{error}',
823
+ auth_proactive_refresh_done: '主动 auth 刷新完成:已刷新 {refreshed},已跳过 {skipped},失败 {failed}。',
817
824
  auth_refresh_all_refreshed: '已刷新:{value}',
818
825
  auth_refresh_all_skipped: '已跳过非 ChatGPT/无效候选:{value}',
819
826
  auth_refresh_all_failed: '失败:{value}',
@@ -1102,6 +1109,7 @@ const MESSAGES = {
1102
1109
  usage_quota_nudge: '用法:/quota_nudge <credits|usage_limit> confirm',
1103
1110
  quota_nudge_sent: '已请求发送额度提醒邮件。',
1104
1111
  login_device_started: '已开始设备码登录。',
1112
+ login_device_prereq: '前置条件:在 ChatGPT 左下角点用户名,进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。Workspace 账号可能需要管理员允许设备码登录。切勿分享设备代码。',
1105
1113
  login_url: '地址:{value}',
1106
1114
  login_code: '验证码:{value}',
1107
1115
  login_id: '登录 ID:{value}',
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
2
3
  import fs from 'node:fs';
3
4
  import os from 'node:os';
4
5
  import path from 'node:path';
@@ -27,6 +28,39 @@ const PROXY_ENV_KEYS = [
27
28
  'no_proxy',
28
29
  ];
29
30
  const STANDARD_NODE_PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'];
31
+ const LOCAL_AUTH_REFRESH_LEASE_TTL_MS = 10 * 60_000;
32
+ function createLocalAuthRefreshLease() {
33
+ let active = null;
34
+ const expire = () => {
35
+ if (active && active.expiresAt <= Date.now()) {
36
+ active = null;
37
+ }
38
+ };
39
+ return {
40
+ isIdle: () => {
41
+ expire();
42
+ return active === null;
43
+ },
44
+ acquire: async (reason) => {
45
+ expire();
46
+ if (active) {
47
+ return {
48
+ ok: false,
49
+ leaseId: null,
50
+ reason: `another local auth refresh lease is active: ${active.reason}`,
51
+ };
52
+ }
53
+ const leaseId = crypto.randomUUID();
54
+ active = { leaseId, reason, expiresAt: Date.now() + LOCAL_AUTH_REFRESH_LEASE_TTL_MS };
55
+ return { ok: true, leaseId };
56
+ },
57
+ release: async (leaseId) => {
58
+ if (leaseId && active?.leaseId === leaseId) {
59
+ active = null;
60
+ }
61
+ },
62
+ };
63
+ }
30
64
  async function main() {
31
65
  if (isVersionCommand(command)) {
32
66
  console.log(readPackageVersion());
@@ -239,6 +273,7 @@ async function runServeCli() {
239
273
  const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
240
274
  let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
241
275
  const runtimes = [];
276
+ const localAuthRefreshLease = createLocalAuthRefreshLease();
242
277
  const writeAggregateStatus = (running = true) => {
243
278
  const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
244
279
  const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
@@ -286,7 +321,7 @@ async function runServeCli() {
286
321
  && mirror.isIdle();
287
322
  const coordinator = {
288
323
  canSelfUpdate: () => authSyncLocalIdle()
289
- && (!authSync || authSync.isIdle()),
324
+ && (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
290
325
  authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
291
326
  recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
292
327
  const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
@@ -302,9 +337,9 @@ async function runServeCli() {
302
337
  }) ?? false;
303
338
  },
304
339
  acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
305
- ?? Promise.resolve({ ok: true, leaseId: null }),
340
+ ?? localAuthRefreshLease.acquire(reason),
306
341
  releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
307
- ?? Promise.resolve(),
342
+ ?? localAuthRefreshLease.release(leaseId),
308
343
  getAuthSyncStatus: () => authSync?.getStatus() ?? null,
309
344
  authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
310
345
  authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
@@ -438,11 +473,12 @@ async function runServeCli() {
438
473
  let singleMirror = null;
439
474
  let core = null;
440
475
  const singleAuthDir = config.codexAuthDir ?? config.codexHome ?? process.env.CODEX_AUTH_DIR ?? path.join(os.homedir(), '.codex');
476
+ const singleLocalAuthRefreshLease = createLocalAuthRefreshLease();
441
477
  const singleAuthSyncLocalIdle = () => Boolean(core?.isIdleForServiceUpdate())
442
478
  && (!singleMirror || singleMirror.isIdle());
443
479
  const singleCoordinator = config.authSyncEnabled ? {
444
480
  canSelfUpdate: () => singleAuthSyncLocalIdle()
445
- && (!singleAuthSync || singleAuthSync.isIdle()),
481
+ && (singleAuthSync ? singleAuthSync.isIdle() : singleLocalAuthRefreshLease.isIdle()),
446
482
  authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
447
483
  recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
448
484
  const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
@@ -459,9 +495,9 @@ async function runServeCli() {
459
495
  }) ?? false;
460
496
  },
461
497
  acquireAuthRefreshLease: (reason) => singleAuthSync?.acquireRefreshLease(reason)
462
- ?? Promise.resolve({ ok: true, leaseId: null }),
498
+ ?? singleLocalAuthRefreshLease.acquire(reason),
463
499
  releaseAuthRefreshLease: (leaseId) => singleAuthSync?.releaseRefreshLease(leaseId)
464
- ?? Promise.resolve(),
500
+ ?? singleLocalAuthRefreshLease.release(leaseId),
465
501
  getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
466
502
  authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
467
503
  authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
@@ -10,13 +10,13 @@ Use it when:
10
10
 
11
11
  - You legally own and maintain the ChatGPT accounts and auth files.
12
12
  - Multiple machines run FoxClaw, and each machine has at least one Telegram bot.
13
- - You want auth files to stay fresh across nodes without routinely rotating refresh tokens.
13
+ - You want auth files to stay fresh across nodes, and you allow FoxClaw to proactively refresh enabled ChatGPT candidates whose `last_refresh` is older than 9 days after it obtains the cross-node refresh lease.
14
14
  - The recommended default is one contact bot per node for cross-node sync. Other bots on the same node continue to use local auth mirroring.
15
15
 
16
16
  Do not use it when:
17
17
 
18
18
  - The auth source is untrusted, account ownership is unclear, or you do not control every machine administrator.
19
- - You plan to use `/auth refresh all` as a refresh-token keepalive.
19
+ - You want to force refresh-token keepalive without a cross-node lease, while nodes are busy, or for disabled candidates.
20
20
  - The same bot token is being polled by multiple machines at the same time. That breaks Telegram update delivery and FoxClaw's assumptions.
21
21
 
22
22
  ## Design And Safety Model
@@ -25,7 +25,7 @@ Cross-node sync combines three active paths:
25
25
 
26
26
  - **Push**: after local login, Codex automatic refresh, or `/auth refresh all confirm` succeeds and passes usage validation, FoxClaw sends the newer candidate to peers.
27
27
  - **Pull**: before auth switch or reload, FoxClaw first searches local runtimes for a newer candidate. If none exists, it asks peers for a newer same-name, same-account candidate.
28
- - **Lease**: before `/auth refresh all confirm` rotates refresh tokens, FoxClaw requests a cross-node refresh lease. Any busy, denying, or non-responsive peer blocks the refresh.
28
+ - **Lease**: before `/auth refresh all confirm` or the background 9-day proactive refresh rotates refresh tokens, FoxClaw requests a cross-node refresh lease. Any busy, denying, or non-responsive peer blocks the refresh.
29
29
 
30
30
  Safety boundaries:
31
31
 
@@ -200,4 +200,4 @@ With cross-node sync enabled, this command first requests a cross-node refresh l
200
200
 
201
201
  **Should I periodically run `/auth refresh all confirm` as keepalive?**
202
202
 
203
- No. Codex refreshes automatically when access tokens expire. Cross-node auth sync propagates auth files that have already refreshed successfully; Refresh all should remain a maintenance command, not a routine keepalive.
203
+ Do not force it manually on a schedule. Codex refreshes automatically when access tokens expire, and FoxClaw now proactively refreshes enabled ChatGPT candidates whose `last_refresh` is older than 9 days after the node is globally idle and obtains the cross-node refresh lease. `/auth refresh all confirm` remains a manual maintenance command for cases where you explicitly accept refresh-token rotation risk.
@@ -71,6 +71,8 @@ Log in:
71
71
  codex login
72
72
  ```
73
73
 
74
+ If you are logging in from a headless host, remote SSH session, WSL setup, or any environment where browser callback login is unreliable, you may use Codex device-code login later through FoxClaw's `/login_device`, `/auth add <name>`, or the Login button in the `/auth` panel. First open `https://chatgpt.com/`, click your username in the lower-left corner, go to Settings > Security, and enable device code authorization for Codex. Workspace accounts may require an admin to allow device-code login. Never share the device code or paste it into an untrusted page.
75
+
74
76
  Check the CLI exists:
75
77
 
76
78
  ```bash
@@ -156,6 +156,8 @@ If Codex is not logged in:
156
156
  codex login
157
157
  ```
158
158
 
159
+ If FoxClaw's `/login_device`, `/auth add <name>`, or `/auth` panel Login button gives you a device code but the login page says it is not allowed, falls back to the regular browser flow, or cannot complete on a headless host, check ChatGPT security settings first: open `https://chatgpt.com/`, click your username in the lower-left corner, go to Settings > Security, and enable device code authorization for Codex. Workspace accounts may require an admin to allow device-code login. Treat the device code as a login grant and never share it.
160
+
159
161
  `codex --version` only verifies the command exists. To verify auth, run:
160
162
 
161
163
  ```bash
@@ -376,6 +376,10 @@ The safer path is adding candidates from the phone with `/auth add <name>`.
376
376
  - `/logout confirm`: logs out the current Codex account.
377
377
  - `/auth add <name>`: adds a candidate account. For example, `/auth add work` creates `auth.json_work` and starts device login.
378
378
 
379
+ Device-code login is the Codex login path for headless hosts. Your ChatGPT account may not allow it by default. Before using `/login_device`, `/auth add <name>`, or the Login button in the `/auth` panel, open `https://chatgpt.com/`, click your username in the lower-left corner, go to Settings > Security, and enable device code authorization for Codex. Workspace or enterprise accounts may require a workspace admin to allow device-code login.
380
+
381
+ Use this carefully: a device code can be stolen by phishing and acts as a login grant. Enter the code only on the ChatGPT/Codex login page you trust. Never send the device code to another person or paste it into an untrusted page.
382
+
379
383
  `/auth add <name>` flow:
380
384
 
381
385
  1. FoxClaw prepares `auth.json_<name>`.
@@ -415,7 +419,7 @@ The right-side `✅` / `⏸️` button controls whether the candidate participat
415
419
 
416
420
  `/auth refresh all` is a command-only maintenance action because ChatGPT refresh tokens are rotated. It is allowed only when every Telegram runtime, the Weixin runtime, approvals, inputs, logins, and auth mirroring are idle. The command first shows a risk confirmation: if OpenAI/Codex consumes an old refresh token but the new token cannot be saved because of network, process, or disk failure, that candidate may require device login or phone verification again. After confirmation, FoxClaw visits every ChatGPT candidate, asks Codex to force-refresh tokens with `account/read refreshToken=true`, verifies the result through the usage endpoint, mirrors successful candidates, restores the original current auth, and shows a summary.
417
421
 
418
- OpenAI does not publish a fixed ChatGPT refresh-token lifetime or an old-token replay grace period. Codex refreshes automatically when an access token approaches expiry; when it cannot parse the access-token `exp`, current Codex uses a `last_refresh` fallback of about 8 days. The panel labels candidates without a refresh record in that interval as `not recently refreshed`, but this is only a maintenance hint. It does not prove expiry and does not trigger bulk keepalive refreshes. Do not use `/auth refresh all` as a routine keepalive command.
422
+ OpenAI does not publish a fixed ChatGPT refresh-token lifetime or an old-token replay grace period. Codex refreshes automatically when an access token approaches expiry; when it cannot parse the access-token `exp`, current Codex uses a `last_refresh` fallback of about 8 days. The panel labels candidates without a refresh record in that interval as `not recently refreshed`. FoxClaw also checks once per hour in the background: if an enabled ChatGPT candidate has a `last_refresh` older than 9 days, FoxClaw proactively refreshes that batch only when every runtime is idle, no approvals/inputs/logins/auth mirror writes are active, and the node holds the cross-node refresh lease. The private bot chat reports the proactive refresh result, and newer candidates continue through same-node mirroring and cross-node sync.
419
423
 
420
424
  ### 6.4 Cross-Node Auth Sync
421
425
 
@@ -450,13 +454,13 @@ Safety boundaries:
450
454
  - FoxClaw only accepts sync files from bots listed in `AUTH_SYNC_PEERS`; wrong key, cluster, nonce, or payload validation never writes files.
451
455
  - Remote imports wait for global local idleness, temporarily switch to the candidate for app-server usage validation, and only then write the candidate.
452
456
  - A same-name candidate known to belong to a different account id is never overwritten.
453
- - Cross-node recovery only pulls an already-held valid peer copy. It does not automatically rotate refresh tokens; if no peer has a usable copy, it stops and asks you to maintain auth on one node manually.
457
+ - Cross-node recovery only pulls an already-held valid peer copy and does not rotate refresh tokens during recovery. If no peer has a usable copy, it stops and asks you to maintain auth manually. The background 9-day proactive refresh separately requests the cross-node refresh lease and skips that cycle if the lease is not granted.
454
458
 
455
459
  Dual-active behavior:
456
460
 
457
461
  - Push: after local login, Codex automatic refresh, or `/auth refresh all` succeeds and passes local mirror validation, the newer candidate is encrypted and pushed to peers.
458
462
  - Pull: before auth switch or reload, FoxClaw first searches local runtimes for a newer same-account candidate; if none is found, it asks peers for a newer same-name same-account copy.
459
- - Lease: `/auth refresh all confirm` requests a cross-node refresh lease before rotating tokens. Any busy, denying, or non-responsive peer blocks the refresh.
463
+ - Lease: `/auth refresh all confirm` and the background 9-day proactive refresh request a cross-node refresh lease before rotating tokens. Any busy, denying, or non-responsive peer blocks the refresh.
460
464
 
461
465
  Commands:
462
466
 
@@ -10,13 +10,13 @@
10
10
 
11
11
  - 这些 ChatGPT 账号和 auth 文件都由你合法拥有和维护。
12
12
  - 多台机器都运行 FoxClaw,并且每台机器至少有一个 Telegram bot。
13
- - 你希望 auth 文件在节点间自动保持较新,但不希望日常主动旋转 refresh token。
13
+ - 你希望 auth 文件在节点间自动保持较新,并允许 FoxClaw 在已启用 ChatGPT 候选 `last_refresh` 超过 9 天时,持有跨节点刷新锁后主动刷新。
14
14
  - 默认推荐每台机器只选择一个“联系人 bot”参与跨节点同步;同一节点内其他 bot 继续使用原本的本机 auth 镜像。
15
15
 
16
16
  不适合:
17
17
 
18
18
  - 同步来源不可信、账号来源不合法,或你无法确认每台机器的管理员。
19
- - 希望用 `/auth refresh all` 当作 refresh token 保活工具。
19
+ - 希望在没有跨节点锁、节点忙碌或候选被禁用时仍强制做 refresh token 保活。
20
20
  - 同一个 bot token 被多台机器同时 polling;这会破坏 Telegram update 分发和 FoxClaw 的运行假设。
21
21
 
22
22
  ## 设计与安全模型
@@ -25,7 +25,7 @@
25
25
 
26
26
  - **Push**:本节点登录、Codex 自动刷新或 `/auth refresh all confirm` 成功并通过本机 usage 验证后,把较新的候选加密发送给 peer。
27
27
  - **Pull**:本节点切换或重载 auth 前,如果本机其他 runtime 没有更新副本,会向 peer 请求同名、同账号的较新候选。
28
- - **Lease**:执行会旋转 refresh token 的 `/auth refresh all confirm` 前,先向 peer 申请跨节点刷新锁;任一 peer 忙碌、拒绝或无响应都会阻止刷新。
28
+ - **Lease**:执行会旋转 refresh token 的 `/auth refresh all confirm` 或后台 9 天主动刷新前,先向 peer 申请跨节点刷新锁;任一 peer 忙碌、拒绝或无响应都会阻止刷新。
29
29
 
30
30
  安全边界:
31
31
 
@@ -200,4 +200,4 @@ auth sync 测试完成:已发送 1,收到回应 1。
200
200
 
201
201
  **要不要定期 `/auth refresh all confirm` 保活**
202
202
 
203
- 不要。Codex 会按 access token 到期自动刷新。FoxClaw 的跨节点同步会同步“已经成功刷新的新 auth”,不应该把 refresh all 当作日常保活。
203
+ 不要手动定期强刷。Codex 会按 access token 到期自动刷新;FoxClaw 也会在已启用 ChatGPT 候选 `last_refresh` 超过 9 天时,等全局空闲并拿到跨节点刷新锁后主动刷新。`/auth refresh all confirm` 仍然是人工维护命令,用于你明确接受 refresh token 轮换风险的场景。
@@ -69,6 +69,8 @@ npm install -g @openai/codex
69
69
  codex login
70
70
  ```
71
71
 
72
+ 如果你是在无头主机、远程 SSH、WSL 或没有可用浏览器回调的环境里登录,后续可能会使用 Codex 的设备码登录,包括 FoxClaw 的 `/login_device`、`/auth add <name>` 和 `/auth` 面板里的“设备登录”。先在浏览器打开 `https://chatgpt.com/`,左下角点用户名,进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。Workspace 账号可能需要管理员允许设备码登录。设备代码不要分享给任何人,也不要粘贴到不可信页面。
73
+
72
74
  检查命令是否存在:
73
75
 
74
76
  ```bash
@@ -157,6 +157,8 @@ codex --version
157
157
  codex login
158
158
  ```
159
159
 
160
+ 如果你通过 FoxClaw 的 `/login_device`、`/auth add <name>` 或 `/auth` 面板“设备登录”拿到了设备码,但登录页面提示没有权限、退回普通浏览器流程,或无头环境无法完成登录,先检查 ChatGPT 安全设置:打开 `https://chatgpt.com/`,左下角点用户名,进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。Workspace 账号可能需要管理员在 workspace 权限里允许设备码登录。设备代码等同于登录授权,切勿分享给他人。
161
+
160
162
  `codex --version` 只证明命令存在。要验证认证真的可用,运行:
161
163
 
162
164
  ```bash
@@ -376,6 +376,10 @@ cp -L ~/.codex/auth.json ~/.codex/auth.json_personal
376
376
  - `/logout confirm`:退出当前 Codex 登录。
377
377
  - `/auth add <name>`:新增一个候选账号,例如 `/auth add work` 会创建 `auth.json_work` 并发起设备码登录。
378
378
 
379
+ 设备码登录是无头主机上的 Codex 登录流程。默认情况下 ChatGPT 账号可能没有打开这个权限。使用 `/login_device`、`/auth add <name>` 或 `/auth` 面板里的“设备登录”按钮前,先在浏览器打开 `https://chatgpt.com/`,左下角点用户名,进入“设置 > 安全”,启用“为 Codex 启用设备代码授权”。如果是 workspace/企业账号,可能需要管理员在 workspace 权限里允许设备码登录。
380
+
381
+ 开启后仍要谨慎:设备代码可以被钓鱼页面窃取,等同于一次登录授权。只在你确认的 ChatGPT/Codex 登录页面输入设备代码,不要把设备代码发给别人,也不要粘贴到不可信网页。
382
+
379
383
  `/auth add <name>` 的流程:
380
384
 
381
385
  1. FoxClaw 准备 `auth.json_<name>`。
@@ -415,7 +419,7 @@ Candidates: 2
415
419
 
416
420
  `/auth refresh all` 是仅命令入口的维护操作,因为 ChatGPT refresh token 会被轮换。只有所有 Telegram runtime、微信 runtime、审批、待输入、登录流程和 auth 镜像写入都空闲时才允许执行。命令会先显示风险确认:如果 OpenAI/Codex 已经消费旧 refresh token,但因为网络、进程或磁盘故障导致新 token 没能成功保存,该候选可能需要重新设备登录,甚至重新手机号验证。确认后,它会逐个访问 ChatGPT 候选,让 Codex 通过 `account/read refreshToken=true` 强制刷新 token,再用 usage 接口验证,成功后镜像到其他 bot home,最后恢复原本的当前 auth 并显示摘要。
417
421
 
418
- OpenAI 没有公开 ChatGPT refresh token 的固定有效期或旧 token 重放宽限期。Codex 会在 access token 临近到期时自动刷新;如果 access token 里无法解析 `exp`,Codex 当前使用 `last_refresh` 超过约 8 天作为兜底刷新条件。面板把超过 8 天没有刷新记录的候选标为“长期未刷新”,但这只是维护提醒,不代表 refresh token 已经过期,也不会触发批量保活刷新。不要把 `/auth refresh all` 当作日常保活命令。
422
+ OpenAI 没有公开 ChatGPT refresh token 的固定有效期或旧 token 重放宽限期。Codex 会在 access token 临近到期时自动刷新;如果 access token 里无法解析 `exp`,Codex 当前使用 `last_refresh` 超过约 8 天作为兜底刷新条件。面板把超过 8 天没有刷新记录的候选标为“长期未刷新”。FoxClaw 还会在后台每小时检查一次:已启用的 ChatGPT 候选如果 `last_refresh` 超过 9 天,会在所有 runtime 空闲、没有审批/待输入/登录/auth 镜像写入,并且拿到跨节点刷新锁后,主动刷新这一批候选。主动刷新完成后会私聊通知,并把较新的候选继续镜像和跨节点同步。
419
423
 
420
424
  ### 6.4 跨节点 auth 同步
421
425
 
@@ -450,13 +454,13 @@ AUTH_SYNC_NODE_ID=workstation-a
450
454
  - 只接收 `AUTH_SYNC_PEERS` 中 peer bot 发来的同步文件;密钥、cluster、nonce 或 payload 校验失败时不会写盘。
451
455
  - 远端导入必须等本机全局空闲,再临时切换到待验证 auth、重启 app-server、读取 usage 验证成功后才写入候选。
452
456
  - 同名候选如果已知属于不同 account id,永远拒绝覆盖。
453
- - 跨节点恢复只拉取 peer 已持有的有效副本,不会自动触发 refresh token 轮换;找不到有效副本时会停止,提示你在一个节点手动维护授权。
457
+ - 跨节点恢复只拉取 peer 已持有的有效副本,不会在恢复过程中直接轮换 refresh token;找不到有效副本时会停止,提示你人工维护授权。后台 9 天主动刷新会单独申请跨节点刷新锁,拿不到锁就跳过本轮。
454
458
 
455
459
  双主动流程:
456
460
 
457
461
  - push:本节点登录、Codex 自动刷新或 `/auth refresh all` 成功并通过本机镜像验证后,会主动把较新的候选加密推送给 peer。
458
462
  - pull:本节点切换或重载 auth 前如果发现本地候选不是最新,会先查本机其他 runtime;仍找不到时,再向 peer 拉取同名同账号的较新副本。
459
- - lease:执行会旋转 refresh token 的 `/auth refresh all confirm` 前,会向 peer 申请跨节点刷新锁。任一 peer 忙碌、拒绝或无响应都会阻止刷新。
463
+ - lease:执行会旋转 refresh token 的 `/auth refresh all confirm` 或后台 9 天主动刷新前,会向 peer 申请跨节点刷新锁。任一 peer 忙碌、拒绝或无响应都会阻止刷新。
460
464
 
461
465
  命令:
462
466
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",