@foxden-app/foxclaw 0.4.15 → 0.4.17

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/.env.example CHANGED
@@ -40,7 +40,9 @@ CODEX_CLI_BIN=/absolute/path/to/codex
40
40
  # Optional cross-node Codex auth sync.
41
41
  # Disabled by default. When enabled, FoxClaw uses Telegram Bot-to-Bot private
42
42
  # messages to move encrypted auth bundles between nodes. Enable Bot-to-Bot
43
- # Communication Mode for the participating bots in BotFather first.
43
+ # Communication Mode for each node's contact bot in BotFather first.
44
+ # In TG_BOT_TOKENS multi-bot mode, the first token is the default contact bot;
45
+ # put the node's contact token first and list peer contact @usernames below.
44
46
  # AUTH_SYNC_ENABLED=true
45
47
  # AUTH_SYNC_KEY=<shared-key-with-at-least-32-bytes>
46
48
  # AUTH_SYNC_PEERS=@other_node_bot,@third_node_bot
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
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.4.17 - 2026-06-04
6
+
7
+ ### 中文
8
+ - 明确跨节点 auth 同步推荐架构:每节点一个联系人 bot;多 bot 模式默认使用 `TG_BOT_TOKENS` 的第一个 token 作为联系人,同节点其他 bot 继续走本机 auth 镜像。
9
+ - `/auth sync status` 和 `/status` 现在显示联系人 bot,避免在非联系人 bot 上执行命令时误判实际发包身份。
10
+ - `/auth sync test` 升级为等待 peer 加密 pong 的真实握手,并显示未回应 peer;同时文档补充 BotFather MiniApp 入口、不要误进 Configure Mini App,以及 `push all` 只代表发送成功、不代表对端已导入。
11
+
12
+ ### English
13
+ - Clarified the recommended cross-node auth sync topology: one contact bot per node. In multi-bot mode, the first `TG_BOT_TOKENS` entry is the default contact while other same-node bots keep using local auth mirroring.
14
+ - `/auth sync status` and `/status` now show the contact bot so commands run from a non-contact bot no longer obscure the actual sender identity.
15
+ - `/auth sync test` now waits for encrypted peer pong replies and reports missing peers. Documentation now covers the BotFather MiniApp entry point, avoids the Configure Mini App confusion, and explains that `push all` only proves send success, not peer import.
16
+
17
+ ## 0.4.16 - 2026-06-04
18
+
19
+ ### 中文
20
+ - 新增独立的跨节点 auth 同步中文/英文配置指南,覆盖设计模型、`.env` 配置、`@BotFather` Bot-to-Bot Communication Mode 操作、验证步骤和故障排查。
21
+ - 在 README、README_EN 和用户手册中加入可发现入口,让新用户能从 GitHub/npm 首页一路点到多节点 auth 同步配置说明。
22
+
23
+ ### English
24
+ - Added standalone Chinese and English cross-node auth sync setup guides covering the design model, `.env` config, `@BotFather` Bot-to-Bot Communication Mode steps, verification, and troubleshooting.
25
+ - Linked the guides from README, README_EN, and the user manuals so new users can find multi-node auth sync setup from the GitHub/npm landing page.
26
+
5
27
  ## 0.4.15 - 2026-06-04
6
28
 
7
29
  ### 中文
package/README.md CHANGED
@@ -37,6 +37,7 @@ FoxClaw(狸爪)的目标很直接:让你用手机控制本机的 Codex,
37
37
  - 手头有 Codex、OpenClaw、QwenPaw、Hermes、OpenCode、Kimi CLI 之类能跑 shell 的 agent?推荐走 [Agent 辅助安装](./docs/zh/agent-assisted-install.md)。
38
38
  - 对 Node、Telegram 机器人、Codex CLI 不太熟?看 [新手安装指南](./docs/zh/install-for-beginners.md)。
39
39
  - 已经装好,想系统了解 `/help`、`/setup`、`/threads`、`/watch`、`/auth` 和账号轮转?看 [用户手册](./docs/zh/user-manual.md)。
40
+ - 想把同一组合法 ChatGPT auth 候选同步到多台机器?看 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
40
41
  - 想了解每个版本改了什么?看 [更新日志](./CHANGELOG.md)。
41
42
  - Git、Node、`.env` 都玩得转?直接往下看快速设置。
42
43
  - 卡住了?看 [故障排查](./docs/zh/troubleshooting.md)。
@@ -211,7 +212,7 @@ TG_BOT_TOKEN=123456:token_a
211
212
 
212
213
  FoxClaw 仍然只运行一个系统服务。默认情况下,它会为每个 bot 启动独立 `codex app-server` 和独立 `CODEX_HOME`。因此 A 私聊运行 turn 时,B 私聊仍可独立切换自己的 `/auth`。候选凭据由 FoxClaw 在登录或刷新在线验证后镜像同步;切换或重载前还会从其他 Codex home 恢复同账号较新凭据。各 bot 的当前选择互不影响。每个 bot 首次私聊发送 `/help` 和 `/status`;`/auth` 会标明正在操作的 bot runtime,`/status` 会列出全部 bot 的连接、runtime 类型、当前 auth 和活动 turn 摘要。
213
214
 
214
- 多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_bot`。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本。跨节点恢复不会自动刷新 token,`/auth refresh all confirm` 会先申请跨节点刷新锁。
215
+ 多台机器共享同一合法账号池时,可以启用可选跨节点 auth 同步:`AUTH_SYNC_ENABLED=true`、`AUTH_SYNC_KEY` 和 `AUTH_SYNC_PEERS=@peer_contact_bot`。推荐每台机器只选一个联系人 bot;同一节点内的其他 bot 继续走本机 auth 镜像。多 bot 模式下,默认用 `TG_BOT_TOKENS` 的第一个 token 作为联系人 bot。FoxClaw 会通过 Telegram Bot-to-Bot 私聊传输加密 auth 包;本机验证刷新后主动 push,发现本机候选失效时主动 pull peer 已持有的有效副本。跨节点恢复不会自动刷新 token,`/auth refresh all confirm` 会先申请跨节点刷新锁。完整配置、`@BotFather` 操作和验证步骤见 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
215
216
 
216
217
  如果你需要一路 Telegram 与终端互通 session,把同一个 token 同时填入 `TG_BOT_TOKENS` 和 `TG_BOT_TOKEN`。这个 bot 使用默认 `CODEX_HOME`(未设置时通常是 `~/.codex`)和默认 auth,因此能看到终端 Codex 的本地线程;它不再享有隔离 runtime 的“互不影响”保证,切换 auth 会影响终端和其他默认 runtime。
217
218
 
package/README_EN.md CHANGED
@@ -37,6 +37,7 @@ FoxClaw is more than message forwarding. It provides Telegram panels for Codex w
37
37
  - Already have a shell-capable agent such as Codex, OpenClaw, QwenPaw, Hermes, OpenCode, or Kimi CLI? Use the [Agent-Assisted Install](./docs/agent-assisted-install.md) first. This is the recommended path.
38
38
  - New to Node, Telegram bots, or Codex CLI? Use the [Beginner Install Guide](./docs/install-for-beginners.md).
39
39
  - Already installed and want the full command guide for `/help`, `/setup`, `/threads`, `/watch`, `/auth`, and auth rotation? Read the [User Manual](./docs/user-manual.md).
40
+ - Want to sync the same legally owned ChatGPT auth candidate pool across multiple machines? Read the [Cross-Node Auth Sync Setup Guide](./docs/cross-node-auth-sync.md).
40
41
  - Want to see what changed in each release? Read the [Changelog](./CHANGELOG.md).
41
42
  - Already comfortable with Git, Node, and `.env` files? Use the quick setup below.
42
43
  - Something failed? Check [Troubleshooting](./docs/troubleshooting.md).
@@ -211,7 +212,7 @@ TG_BOT_TOKEN=123456:token_a
211
212
 
212
213
  FoxClaw remains one system service. By default, it starts an independent `codex app-server` and `CODEX_HOME` for each bot. While bot A is running a turn, bot B can switch its own `/auth` selection. Candidate credentials are mirrored only after online-validated login or refresh; before switching or reloading, FoxClaw also restores a newer same-account credential from another Codex home when available. Current selections remain independent. Send `/help` and `/status` in a private chat with each bot after installation; `/auth` names the runtime being managed, and `/status` summarizes every bot's connection, runtime type, selected auth, and active turns.
213
214
 
214
- When multiple machines share the same legally owned account pool, optional cross-node auth sync can be enabled with `AUTH_SYNC_ENABLED=true`, `AUTH_SYNC_KEY`, and `AUTH_SYNC_PEERS=@peer_bot`. FoxClaw sends encrypted auth bundles through Telegram Bot-to-Bot private messages. A locally verified refresh is pushed to peers, and a node with a bad local candidate can pull an already-held valid peer copy. Cross-node recovery never auto-refreshes tokens; `/auth refresh all confirm` first requests a cross-node refresh lease.
215
+ When multiple machines share the same legally owned account pool, optional cross-node auth sync can be enabled with `AUTH_SYNC_ENABLED=true`, `AUTH_SYNC_KEY`, and `AUTH_SYNC_PEERS=@peer_contact_bot`. The recommended topology is one contact bot per node; other bots on the same node continue to use local auth mirroring. In multi-bot mode, the first token in `TG_BOT_TOKENS` is the default contact bot. FoxClaw sends encrypted auth bundles through Telegram Bot-to-Bot private messages. A locally verified refresh is pushed to peers, and a node with a bad local candidate can pull an already-held valid peer copy. Cross-node recovery never auto-refreshes tokens; `/auth refresh all confirm` first requests a cross-node refresh lease. For full config, `@BotFather` operations, and verification steps, read the [Cross-Node Auth Sync Setup Guide](./docs/cross-node-auth-sync.md).
215
216
 
216
217
  To keep one Telegram bot interoperable with terminal Codex sessions, put the same token in both `TG_BOT_TOKENS` and `TG_BOT_TOKEN`. That bot uses the default `CODEX_HOME` (usually `~/.codex` when unset) and default auth, so it can see local terminal sessions. It no longer has the isolated runtime guarantee: `/auth` switches also affect the terminal and other default runtimes.
217
218
 
@@ -3,6 +3,7 @@ import type { AuthMirrorCandidateRecord, AuthMirrorImportResult } from './mirror
3
3
  export interface AuthSyncConfig {
4
4
  enabled: boolean;
5
5
  transport: 'telegram-private';
6
+ transportLabel?: string | null;
6
7
  key: string | null;
7
8
  peers: string[];
8
9
  nodeId: string | null;
@@ -18,6 +19,7 @@ export interface AuthSyncStatus {
18
19
  enabled: boolean;
19
20
  nodeId: string | null;
20
21
  transport: 'telegram-private';
22
+ transportLabel: string | null;
21
23
  peers: string[];
22
24
  pendingImports: number;
23
25
  lastSentAt: string | null;
@@ -51,6 +53,11 @@ export interface AuthSyncLeaseResult {
51
53
  leaseId: string | null;
52
54
  reason?: string | null;
53
55
  }
56
+ export interface AuthSyncTestResult {
57
+ sent: number;
58
+ replied: number;
59
+ missing: string[];
60
+ }
54
61
  export declare class CrossNodeAuthSync {
55
62
  private readonly config;
56
63
  private readonly logger;
@@ -63,6 +70,7 @@ export declare class CrossNodeAuthSync {
63
70
  private readonly pendingImports;
64
71
  private readonly pendingPulls;
65
72
  private readonly pendingLeases;
73
+ private readonly pendingTests;
66
74
  private seenNonces;
67
75
  private timer;
68
76
  private activeRemoteLease;
@@ -86,9 +94,7 @@ export declare class CrossNodeAuthSync {
86
94
  }): Promise<boolean>;
87
95
  acquireRefreshLease(reason: string): Promise<AuthSyncLeaseResult>;
88
96
  releaseRefreshLease(leaseId: string | null): Promise<void>;
89
- testPeers(): Promise<{
90
- sent: number;
91
- }>;
97
+ testPeers(): Promise<AuthSyncTestResult>;
92
98
  handleIncomingEnvelope(rawEnvelope: string, peer: AuthSyncPeerIdentity): Promise<boolean>;
93
99
  private handleMessage;
94
100
  private handlePullRequest;
@@ -101,10 +107,13 @@ export declare class CrossNodeAuthSync {
101
107
  private validateAndImport;
102
108
  private sendToAll;
103
109
  private sendToPeer;
110
+ private handleTestPong;
111
+ private finishPendingTest;
104
112
  private sealEnvelope;
105
113
  private openEnvelope;
106
114
  private isReady;
107
115
  private isAllowedPeer;
116
+ private matchConfiguredPeer;
108
117
  private expireLeases;
109
118
  private recordError;
110
119
  private writeState;
@@ -6,6 +6,7 @@ import { isAuthCandidateName, parseChatGptAuthMetadata } from './mirror.js';
6
6
  const ENVELOPE_MAGIC = 'foxclaw-auth-sync';
7
7
  const NONCE_RETENTION_MS = 7 * 24 * 60 * 60_000;
8
8
  const PULL_TIMEOUT_MS = 12_000;
9
+ const TEST_TIMEOUT_MS = 8_000;
9
10
  const LEASE_TIMEOUT_MS = 8_000;
10
11
  const LEASE_TTL_MS = 60_000;
11
12
  const REMOTE_ACCESS_TOKEN_MIN_TTL_MS = 60_000;
@@ -21,6 +22,7 @@ export class CrossNodeAuthSync {
21
22
  pendingImports = [];
22
23
  pendingPulls = new Map();
23
24
  pendingLeases = new Map();
25
+ pendingTests = new Map();
24
26
  seenNonces = new Map();
25
27
  timer = null;
26
28
  activeRemoteLease = null;
@@ -89,6 +91,7 @@ export class CrossNodeAuthSync {
89
91
  enabled: this.config.enabled,
90
92
  nodeId: this.nodeId,
91
93
  transport: this.config.transport,
94
+ transportLabel: this.config.transportLabel?.trim() || null,
92
95
  peers: this.peers,
93
96
  pendingImports: this.pendingImports.length,
94
97
  lastSentAt: this.state.lastSentAt,
@@ -104,7 +107,8 @@ export class CrossNodeAuthSync {
104
107
  isIdle() {
105
108
  return this.pendingImports.length === 0
106
109
  && this.pendingPulls.size === 0
107
- && this.pendingLeases.size === 0;
110
+ && this.pendingLeases.size === 0
111
+ && this.pendingTests.size === 0;
108
112
  }
109
113
  async publishCandidate(candidateName) {
110
114
  if (!this.isReady())
@@ -240,9 +244,37 @@ export class CrossNodeAuthSync {
240
244
  }
241
245
  async testPeers() {
242
246
  if (!this.isReady())
243
- return { sent: 0 };
244
- await this.sendToAll({ kind: 'test.ping', requestId: crypto.randomUUID() });
245
- return { sent: this.peers.length };
247
+ return { sent: 0, replied: 0, missing: [] };
248
+ const requestId = crypto.randomUUID();
249
+ const peers = [...this.peers];
250
+ const resultPromise = new Promise((resolve) => {
251
+ const timer = setTimeout(() => {
252
+ this.finishPendingTest(requestId);
253
+ }, TEST_TIMEOUT_MS);
254
+ timer.unref();
255
+ this.pendingTests.set(requestId, {
256
+ peers,
257
+ replies: new Set(),
258
+ resolve,
259
+ timer,
260
+ finished: false,
261
+ });
262
+ });
263
+ try {
264
+ await this.sendToAll({ kind: 'test.ping', requestId });
265
+ }
266
+ catch (error) {
267
+ const pending = this.pendingTests.get(requestId);
268
+ if (pending) {
269
+ clearTimeout(pending.timer);
270
+ this.pendingTests.delete(requestId);
271
+ }
272
+ throw error;
273
+ }
274
+ if (peers.length === 0) {
275
+ this.finishPendingTest(requestId);
276
+ }
277
+ return resultPromise;
246
278
  }
247
279
  async handleIncomingEnvelope(rawEnvelope, peer) {
248
280
  if (!this.isReady() || !this.isAllowedPeer(peer)) {
@@ -285,6 +317,7 @@ export class CrossNodeAuthSync {
285
317
  });
286
318
  return;
287
319
  case 'test.pong':
320
+ this.handleTestPong(message.requestId, normalizePeerIdentity(peer));
288
321
  this.logger.info('auth.sync.test_pong', { peer: normalizePeerIdentity(peer), nodeId: message.nodeId });
289
322
  return;
290
323
  case 'lease.request':
@@ -462,8 +495,35 @@ export class CrossNodeAuthSync {
462
495
  const envelope = this.sealEnvelope(message);
463
496
  await this.transport.send(peer, envelope);
464
497
  this.state.lastSentAt = new Date().toISOString();
498
+ if (this.state.lastError?.includes('USER_BOT_TO_BOT_DISABLED')) {
499
+ this.state.lastError = null;
500
+ }
465
501
  await this.writeState();
466
502
  }
503
+ handleTestPong(requestId, peer) {
504
+ const pending = this.pendingTests.get(requestId);
505
+ if (!pending || pending.finished)
506
+ return;
507
+ const matchedPeer = this.matchConfiguredPeer(peer) ?? peer;
508
+ pending.replies.add(matchedPeer);
509
+ if (pending.peers.every(peerName => pending.replies.has(peerName))) {
510
+ this.finishPendingTest(requestId);
511
+ }
512
+ }
513
+ finishPendingTest(requestId) {
514
+ const pending = this.pendingTests.get(requestId);
515
+ if (!pending || pending.finished)
516
+ return;
517
+ pending.finished = true;
518
+ clearTimeout(pending.timer);
519
+ this.pendingTests.delete(requestId);
520
+ const missing = pending.peers.filter(peer => !pending.replies.has(peer));
521
+ pending.resolve({
522
+ sent: pending.peers.length,
523
+ replied: pending.replies.size,
524
+ missing,
525
+ });
526
+ }
467
527
  sealEnvelope(message) {
468
528
  if (!this.key || !this.nodeId) {
469
529
  throw new Error('auth sync is not initialized');
@@ -528,6 +588,10 @@ export class CrossNodeAuthSync {
528
588
  ];
529
589
  return keys.some(key => this.peerKeys.has(key));
530
590
  }
591
+ matchConfiguredPeer(peer) {
592
+ const keys = new Set(expandPeerKeys(peer));
593
+ return this.peers.find(configuredPeer => expandPeerKeys(configuredPeer).some(key => keys.has(key))) ?? null;
594
+ }
531
595
  expireLeases() {
532
596
  const now = Date.now();
533
597
  if (this.activeRemoteLease && this.activeRemoteLease.expiresAt <= now) {
@@ -23,6 +23,8 @@ export interface CoreCoordinator {
23
23
  }>;
24
24
  authSyncTest?: () => Promise<{
25
25
  sent: number;
26
+ replied: number;
27
+ missing: string[];
26
28
  }>;
27
29
  statusUpdated?: (status: RuntimeStatus) => void;
28
30
  getServiceStatus?: () => Promise<{
@@ -430,6 +430,7 @@ export class BridgeSessionCore {
430
430
  if (serviceStatus.authSync?.enabled) {
431
431
  lines.push(t(locale, 'status_auth_sync', {
432
432
  node: serviceStatus.authSync.nodeId ?? t(locale, 'unknown'),
433
+ contact: serviceStatus.authSync.transportLabel ?? t(locale, 'unknown'),
433
434
  peers: serviceStatus.authSync.peers.length,
434
435
  pending: serviceStatus.authSync.pendingImports,
435
436
  }));
@@ -4009,7 +4010,11 @@ export class BridgeSessionCore {
4009
4010
  await this.sendMessage(scopeId, t(locale, 'auth_sync_disabled'));
4010
4011
  return;
4011
4012
  }
4012
- await this.sendMessage(scopeId, t(locale, 'auth_sync_test_sent', { count: result.sent }));
4013
+ const message = [
4014
+ t(locale, 'auth_sync_test_sent', { sent: result.sent, replied: result.replied }),
4015
+ ...(result.missing.length > 0 ? [t(locale, 'auth_sync_test_missing', { value: result.missing.join(', ') })] : []),
4016
+ ].join('\n');
4017
+ await this.sendMessage(scopeId, message);
4013
4018
  return;
4014
4019
  }
4015
4020
  if (action === 'push' && args[1]?.toLowerCase() === 'all') {
@@ -8117,6 +8122,7 @@ function formatAuthSyncStatus(locale, status) {
8117
8122
  const lines = [
8118
8123
  t(locale, 'auth_sync_status_title'),
8119
8124
  t(locale, 'auth_sync_status_node', { value: status.nodeId ?? t(locale, 'unknown') }),
8125
+ t(locale, 'auth_sync_status_transport', { value: status.transportLabel ?? t(locale, 'unknown') }),
8120
8126
  t(locale, 'auth_sync_status_peers', { value: status.peers.length === 0 ? t(locale, 'none') : status.peers.join(', ') }),
8121
8127
  t(locale, 'auth_sync_status_pending', { value: status.pendingImports }),
8122
8128
  t(locale, 'auth_sync_status_sent', { value: status.lastSentAt ?? t(locale, 'none') }),
package/dist/i18n.d.ts CHANGED
@@ -125,7 +125,7 @@ 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}";
128
+ readonly status_auth_sync: "Cross-node auth sync: node {node}, contact {contact}, peers {peers}, pending imports {pending}";
129
129
  readonly status_auth_sync_error: "Cross-node auth sync error: {value}";
130
130
  readonly status_last_update_none: "Last service update: none recorded";
131
131
  readonly status_last_update: "Last service update: {from} -> {to} at {time}";
@@ -203,11 +203,13 @@ declare const MESSAGES: {
203
203
  readonly auth_add_reverted: "Restored previous auth.";
204
204
  readonly auth_add_missing_file: "Login completed, but the new auth file was not created: {value}";
205
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).";
206
+ readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
207
+ readonly auth_sync_test_missing: "Missing replies: {value}";
207
208
  readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
208
209
  readonly auth_sync_push_done: "Auth sync push complete: sent {sent}, skipped {skipped}.";
209
210
  readonly auth_sync_status_title: "Cross-node auth sync:";
210
211
  readonly auth_sync_status_node: "Node: {value}";
212
+ readonly auth_sync_status_transport: "Contact bot: {value}";
211
213
  readonly auth_sync_status_peers: "Peers: {value}";
212
214
  readonly auth_sync_status_pending: "Pending imports: {value}";
213
215
  readonly auth_sync_status_sent: "Last sent: {value}";
@@ -750,7 +752,7 @@ declare const MESSAGES: {
750
752
  readonly status_runtime_weixin: "- 微信默认运行时:连接 {connected},进行中回复 {turns}";
751
753
  readonly status_auth_mirror_none: "最近 auth 镜像:暂无记录";
752
754
  readonly status_auth_mirror_synced: "最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步";
753
- readonly status_auth_sync: "跨节点 auth 同步:节点 {node},peer {peers},待导入 {pending}";
755
+ readonly status_auth_sync: "跨节点 auth 同步:节点 {node},联系人 {contact},peer {peers},待导入 {pending}";
754
756
  readonly status_auth_sync_error: "跨节点 auth 同步错误:{value}";
755
757
  readonly status_last_update_none: "最近服务升级:暂无记录";
756
758
  readonly status_last_update: "最近服务升级:{from} -> {to}({time})";
@@ -828,11 +830,13 @@ declare const MESSAGES: {
828
830
  readonly auth_add_reverted: "已恢复之前的 auth。";
829
831
  readonly auth_add_missing_file: "登录已完成,但没有创建新的 auth 文件:{value}";
830
832
  readonly auth_sync_disabled: "跨节点 auth 同步未启用。";
831
- readonly auth_sync_test_sent: "已向 {count} 个 peer 发送 auth sync 测试 ping。";
833
+ readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
834
+ readonly auth_sync_test_missing: "未回应:{value}";
832
835
  readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
833
836
  readonly auth_sync_push_done: "auth 同步推送完成:已发送 {sent},已跳过 {skipped}。";
834
837
  readonly auth_sync_status_title: "跨节点 auth 同步:";
835
838
  readonly auth_sync_status_node: "节点:{value}";
839
+ readonly auth_sync_status_transport: "联系人 bot:{value}";
836
840
  readonly auth_sync_status_peers: "Peer:{value}";
837
841
  readonly auth_sync_status_pending: "待导入:{value}";
838
842
  readonly auth_sync_status_sent: "最近发送:{value}";
package/dist/i18n.js CHANGED
@@ -123,7 +123,7 @@ 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}',
126
+ status_auth_sync: 'Cross-node auth sync: node {node}, contact {contact}, peers {peers}, pending imports {pending}',
127
127
  status_auth_sync_error: 'Cross-node auth sync error: {value}',
128
128
  status_last_update_none: 'Last service update: none recorded',
129
129
  status_last_update: 'Last service update: {from} -> {to} at {time}',
@@ -201,11 +201,13 @@ const MESSAGES = {
201
201
  auth_add_reverted: 'Restored previous auth.',
202
202
  auth_add_missing_file: 'Login completed, but the new auth file was not created: {value}',
203
203
  auth_sync_disabled: 'Cross-node auth sync is disabled.',
204
- auth_sync_test_sent: 'Auth sync test ping sent to {count} peer(s).',
204
+ auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
205
+ auth_sync_test_missing: 'Missing replies: {value}',
205
206
  auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
206
207
  auth_sync_push_done: 'Auth sync push complete: sent {sent}, skipped {skipped}.',
207
208
  auth_sync_status_title: 'Cross-node auth sync:',
208
209
  auth_sync_status_node: 'Node: {value}',
210
+ auth_sync_status_transport: 'Contact bot: {value}',
209
211
  auth_sync_status_peers: 'Peers: {value}',
210
212
  auth_sync_status_pending: 'Pending imports: {value}',
211
213
  auth_sync_status_sent: 'Last sent: {value}',
@@ -748,7 +750,7 @@ const MESSAGES = {
748
750
  status_runtime_weixin: '- 微信默认运行时:连接 {connected},进行中回复 {turns}',
749
751
  status_auth_mirror_none: '最近 auth 镜像:暂无记录',
750
752
  status_auth_mirror_synced: '最近 auth 镜像:{candidate} 由 {source} 于 {time} 同步',
751
- status_auth_sync: '跨节点 auth 同步:节点 {node},peer {peers},待导入 {pending}',
753
+ status_auth_sync: '跨节点 auth 同步:节点 {node},联系人 {contact},peer {peers},待导入 {pending}',
752
754
  status_auth_sync_error: '跨节点 auth 同步错误:{value}',
753
755
  status_last_update_none: '最近服务升级:暂无记录',
754
756
  status_last_update: '最近服务升级:{from} -> {to}({time})',
@@ -826,11 +828,13 @@ const MESSAGES = {
826
828
  auth_add_reverted: '已恢复之前的 auth。',
827
829
  auth_add_missing_file: '登录已完成,但没有创建新的 auth 文件:{value}',
828
830
  auth_sync_disabled: '跨节点 auth 同步未启用。',
829
- auth_sync_test_sent: '已向 {count} 个 peer 发送 auth sync 测试 ping。',
831
+ auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
832
+ auth_sync_test_missing: '未回应:{value}',
830
833
  auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
831
834
  auth_sync_push_done: 'auth 同步推送完成:已发送 {sent},已跳过 {skipped}。',
832
835
  auth_sync_status_title: '跨节点 auth 同步:',
833
836
  auth_sync_status_node: '节点:{value}',
837
+ auth_sync_status_transport: '联系人 bot:{value}',
834
838
  auth_sync_status_peers: 'Peer:{value}',
835
839
  auth_sync_status_pending: '待导入:{value}',
836
840
  auth_sync_status_sent: '最近发送:{value}',
package/dist/main.js CHANGED
@@ -305,7 +305,7 @@ async function runServeCli() {
305
305
  ?? Promise.resolve(),
306
306
  getAuthSyncStatus: () => authSync?.getStatus() ?? null,
307
307
  authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
308
- authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0 }),
308
+ authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
309
309
  statusUpdated: () => writeAggregateStatus(),
310
310
  getServiceStatus: async () => ({
311
311
  bots: await Promise.all(runtimes.map(async (runtime) => {
@@ -352,9 +352,13 @@ async function runServeCli() {
352
352
  }
353
353
  activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
354
354
  if (config.authSyncEnabled) {
355
- authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config), logger, {
355
+ const authSyncTransportBot = seeds[0];
356
+ const authSyncTransportLabel = authSyncTransportBot.bot.username
357
+ ? `@${authSyncTransportBot.bot.username}`
358
+ : authSyncTransportBot.id;
359
+ authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, authSyncTransportLabel), logger, {
356
360
  send: async (peer, envelope) => {
357
- await seeds[0].bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
361
+ await authSyncTransportBot.bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
358
362
  },
359
363
  }, {
360
364
  readLocalCandidate: (candidateName) => mirror.readNewestCandidate(candidateName),
@@ -374,7 +378,7 @@ async function runServeCli() {
374
378
  });
375
379
  await authSync.initialize();
376
380
  activeAuthSync = authSync;
377
- attachTelegramAuthSync(seeds[0].bot, authSync, config, logger);
381
+ attachTelegramAuthSync(authSyncTransportBot.bot, authSync, config, logger);
378
382
  authSync.start();
379
383
  }
380
384
  mirror.start();
@@ -455,7 +459,7 @@ async function runServeCli() {
455
459
  ?? Promise.resolve(),
456
460
  getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
457
461
  authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
458
- authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0 }),
462
+ authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
459
463
  statusUpdated: (status) => {
460
464
  writeRuntimeStatus(config.statusPath, {
461
465
  ...status,
@@ -484,7 +488,7 @@ async function runServeCli() {
484
488
  }
485
489
  core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater, singleCoordinator);
486
490
  if (config.authSyncEnabled && singleMirror) {
487
- singleAuthSync = new CrossNodeAuthSync(buildAuthSyncConfig(config), logger, {
491
+ singleAuthSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, bot.username ? `@${bot.username}` : 'default'), logger, {
488
492
  send: async (peer, envelope) => {
489
493
  await bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
490
494
  },
@@ -566,10 +570,11 @@ async function runServeCli() {
566
570
  }
567
571
  }
568
572
  const AUTH_SYNC_TELEGRAM_CAPTION = 'FOXCLAW_AUTH_SYNC_V1';
569
- function buildAuthSyncConfig(config) {
573
+ function buildAuthSyncConfig(config, transportLabel = null) {
570
574
  return {
571
575
  enabled: config.authSyncEnabled,
572
576
  transport: config.authSyncTransport,
577
+ transportLabel,
573
578
  key: config.authSyncKey,
574
579
  peers: config.authSyncPeers,
575
580
  nodeId: config.authSyncNodeId,
package/dist/types.d.ts CHANGED
@@ -363,6 +363,7 @@ export interface RuntimeStatus {
363
363
  authSync?: {
364
364
  enabled: boolean;
365
365
  nodeId: string | null;
366
+ transportLabel: string | null;
366
367
  peers: string[];
367
368
  pendingImports: number;
368
369
  lastSentAt: string | null;
@@ -0,0 +1,195 @@
1
+ # Cross-Node Auth Sync Setup Guide
2
+
3
+ This guide is for multiple machines you control that share the same legally owned ChatGPT auth candidate pool. It extends same-host auth mirroring across machines: when Codex refreshes a token on one node, FoxClaw can send an encrypted auth bundle to peer nodes through Telegram Bot-to-Bot private messages; when one node finds a local candidate unusable, it can pull an already-held valid peer copy.
4
+
5
+ The feature is disabled by default. It does not require a public IP, FRP, or reverse proxy, but it requires Telegram Bot-to-Bot Communication Mode.
6
+
7
+ ## Scope
8
+
9
+ Use it when:
10
+
11
+ - You legally own and maintain the ChatGPT accounts and auth files.
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.
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
+
16
+ Do not use it when:
17
+
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.
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
+
22
+ ## Design And Safety Model
23
+
24
+ Cross-node sync combines three active paths:
25
+
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
+ - **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.
29
+
30
+ Safety boundaries:
31
+
32
+ - Telegram only carries ciphertext. Candidate contents, candidate names, account ids, and `last_refresh` are inside an AES-256-GCM payload.
33
+ - FoxClaw only accepts sync files from bots listed in `AUTH_SYNC_PEERS`.
34
+ - Wrong `AUTH_SYNC_KEY`, cluster, nonce, or payload validation never writes files.
35
+ - Remote imports wait for global local idleness, then run temporary usage validation before writing a candidate.
36
+ - A same-name candidate known to belong to a different account id is never overwritten.
37
+ - Sync packets do not create reply chains. FoxClaw filters by packet type, nonce, and peer allowlist to avoid bot-to-bot loops.
38
+
39
+ Telegram's official Bot Features documentation says private bot-to-bot messaging requires Bot-to-Bot Communication Mode on both sender and recipient, and it calls out loop-prevention requirements. See https://core.telegram.org/bots/features#bot-to-bot-communication
40
+
41
+ ## Before You Configure
42
+
43
+ Assume two machines:
44
+
45
+ - Node A: bot `@foxclaw_node_a_bot`
46
+ - Node B: bot `@foxclaw_node_b_bot`
47
+
48
+ These are the two node contact bots. `AUTH_SYNC_PEERS` only needs peer node contact bots; you do not need to list every bot running on the same machine. In multi-bot mode, FoxClaw uses the first token in `TG_BOT_TOKENS` as the local contact bot by default. If you want bot 5 to be the contact, put bot 5's token first, or enable Bot-to-Bot for every local bot as a temporary fallback.
49
+
50
+ Each node should already work independently:
51
+
52
+ ```bash
53
+ foxclaw doctor
54
+ foxclaw start
55
+ ```
56
+
57
+ In a private Telegram chat with each bot, verify:
58
+
59
+ ```text
60
+ /status
61
+ /auth
62
+ ```
63
+
64
+ ## Enable Bot-to-Bot In @BotFather
65
+
66
+ Repeat this for every participating bot:
67
+
68
+ 1. Prefer the latest Telegram mobile client; some desktop or older clients do not show the setting.
69
+ 2. Open `https://t.me/BotFather?startapp`, or open the `@BotFather` profile and tap **Open App**.
70
+ 3. In the BotFather MiniApp, select the contact bot that will participate in auth sync.
71
+ 4. Open Settings / Bot Settings.
72
+ 5. Find **Bot-to-Bot Communication Mode**.
73
+ 6. Enable it.
74
+ 7. Repeat for every node contact bot.
75
+
76
+ Do not use `/mybots` → Bot Settings → **Configure Mini App**. That configures your bot's Mini App URL, not Bot-to-Bot Communication Mode.
77
+
78
+ Private cross-node sync requires this mode on both bots. Enabling it on only one side is usually not enough for two bots to exchange private sync packets.
79
+
80
+ If you see `Bad Request: USER_BOT_TO_BOT_DISABLED`, first confirm that both the sender contact bot and recipient contact bot have Bot-to-Bot enabled. In multi-bot mode, the sender contact is the first token in `TG_BOT_TOKENS` by default; it may not be the bot where you typed the command.
81
+
82
+ ## .env Configuration
83
+
84
+ Use the same `AUTH_SYNC_KEY` and `AUTH_SYNC_CLUSTER_ID` on all nodes, but give each node a different `AUTH_SYNC_NODE_ID`.
85
+
86
+ Node A:
87
+
88
+ ```dotenv
89
+ TG_BOT_TOKENS=<node-a-contact-token>,<node-a-other-bot-token>
90
+ AUTH_SYNC_ENABLED=true
91
+ AUTH_SYNC_KEY=<shared key with at least 32 bytes>
92
+ AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
93
+ AUTH_SYNC_NODE_ID=workstation-a
94
+ AUTH_SYNC_PEERS=@foxclaw_node_b_bot
95
+ ```
96
+
97
+ Node B:
98
+
99
+ ```dotenv
100
+ TG_BOT_TOKENS=<node-b-contact-token>,<node-b-other-bot-token>
101
+ AUTH_SYNC_ENABLED=true
102
+ AUTH_SYNC_KEY=<shared key with at least 32 bytes>
103
+ AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
104
+ AUTH_SYNC_NODE_ID=workstation-b
105
+ AUTH_SYNC_PEERS=@foxclaw_node_a_bot
106
+ ```
107
+
108
+ For more peers, separate bot usernames with commas:
109
+
110
+ ```dotenv
111
+ AUTH_SYNC_PEERS=@foxclaw_node_a_bot,@foxclaw_node_b_bot,@foxclaw_node_c_bot
112
+ ```
113
+
114
+ Generate a shared key with a password manager or `openssl`:
115
+
116
+ ```bash
117
+ openssl rand -base64 32
118
+ ```
119
+
120
+ Restart FoxClaw on every node after editing config:
121
+
122
+ ```bash
123
+ foxclaw restart
124
+ ```
125
+
126
+ ## Verification
127
+
128
+ 1. In each node's bot private chat, run:
129
+
130
+ ```text
131
+ /auth sync status
132
+ ```
133
+
134
+ You should see the node id, peer list, and pending imports.
135
+
136
+ 2. On node A, run:
137
+
138
+ ```text
139
+ /auth sync test
140
+ ```
141
+
142
+ Node A should report that it sent a test ping. Node B's `/auth sync status` should show a recent receive or test-state change.
143
+
144
+ Starting in 0.4.17, `/auth sync test` waits for an encrypted pong from peers. A healthy result looks like:
145
+
146
+ ```text
147
+ Auth sync test complete: sent 1, replies 1.
148
+ ```
149
+
150
+ If it shows `Missing replies: @peer_bot`, Telegram delivery may have succeeded, but the peer did not receive, decrypt, pass allowlist validation, or run the same auth sync configuration.
151
+
152
+ 3. Use a low-risk candidate for the first broadcast. Make sure every runtime is idle, then run on node A:
153
+
154
+ ```text
155
+ /auth sync push all
156
+ ```
157
+
158
+ 4. On node B, run:
159
+
160
+ ```text
161
+ /auth sync status
162
+ /auth
163
+ ```
164
+
165
+ Confirm that pending imports were processed, or that the candidate exists or has a newer timestamp.
166
+
167
+ Note: `/auth sync push all` saying “sent” only means this node successfully handed encrypted packages to Telegram. It does not prove the peer wrote files. The peer imports only when it is globally idle, usage validation succeeds, same-name candidates belong to the same account id, and the remote `last_refresh` is newer than the local copy. If the local file is already equal or newer, it will not change and `Last import` may remain empty.
168
+
169
+ 5. Only test refresh-token rotation after you understand the risk:
170
+
171
+ ```text
172
+ /auth refresh all
173
+ /auth refresh all confirm
174
+ ```
175
+
176
+ With cross-node sync enabled, this command first requests a cross-node refresh lease. Any busy, denying, or timed-out peer blocks refresh.
177
+
178
+ ## Troubleshooting
179
+
180
+ **`/auth sync test` does nothing**
181
+
182
+ - Confirm both bots have Bot-to-Bot Communication Mode enabled in `@BotFather`.
183
+ - Confirm `AUTH_SYNC_PEERS` contains peer `@username` values, not tokens.
184
+ - Confirm `AUTH_SYNC_KEY` and `AUTH_SYNC_CLUSTER_ID` match exactly on both sides.
185
+ - Confirm both nodes were restarted after config changes.
186
+
187
+ **A sync packet arrived but no candidate was written**
188
+
189
+ - The local node may not be globally idle. Active turns, approvals, inputs, login flows, and mirror writes make imports wait.
190
+ - Usage validation failure rejects the write.
191
+ - Same-name candidates from different account ids are refused.
192
+
193
+ **Should I periodically run `/auth refresh all confirm` as keepalive?**
194
+
195
+ 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.
@@ -419,7 +419,19 @@ OpenAI does not publish a fixed ChatGPT refresh-token lifetime or an old-token r
419
419
 
420
420
  ### 6.4 Cross-Node Auth Sync
421
421
 
422
- Cross-node auth sync is disabled by default. It is for multiple machines you control that share the same legally owned ChatGPT auth candidate pool, so a token refreshed by Codex on one node can be copied to the others. v1 uses Telegram Bot-to-Bot private messages to carry encrypted files, so it does not require public IPs or FRP. Enable Bot-to-Bot Communication Mode for the participating bots in BotFather first.
422
+ Cross-node auth sync is disabled by default. It is for multiple machines you control that share the same legally owned ChatGPT auth candidate pool, so a token refreshed by Codex on one node can be copied to the others. v1 uses Telegram Bot-to-Bot private messages to carry encrypted files, so it does not require public IPs or FRP. The recommended default is one contact bot per node; other bots on the same node keep using local auth mirroring. In multi-bot mode, the default contact is the first token in `TG_BOT_TOKENS`.
423
+
424
+ For the full design, safety boundaries, `.env` examples, and troubleshooting, read the [Cross-Node Auth Sync Setup Guide](./cross-node-auth-sync.md).
425
+
426
+ In `@BotFather`, repeat this for every participating bot:
427
+
428
+ 1. Use the latest Telegram mobile client to open `https://t.me/BotFather?startapp`, or open the `@BotFather` profile and tap **Open App**.
429
+ 2. In the BotFather MiniApp, select the contact bot that will participate in sync.
430
+ 3. Open Settings / Bot Settings.
431
+ 4. Find and enable **Bot-to-Bot Communication Mode**.
432
+ 5. Repeat for every node contact bot; private sync requires this mode on both sender and recipient.
433
+
434
+ Do not use `/mybots` → Bot Settings → **Configure Mini App**. That configures your bot's Mini App URL, not Bot-to-Bot Communication Mode.
423
435
 
424
436
  Example:
425
437
 
@@ -449,8 +461,8 @@ Dual-active behavior:
449
461
  Commands:
450
462
 
451
463
  - `/auth sync status`: show node id, peers, recent sends/receives/imports, pending imports, and the latest error.
452
- - `/auth sync test`: send an encrypted ping to verify peer config, shared key, and Bot-to-Bot private messages.
453
- - `/auth sync push all`: manually broadcast all locally verified candidates without refreshing tokens.
464
+ - `/auth sync test`: send an encrypted ping and wait for peer pong replies to verify peer config, shared key, and Bot-to-Bot private messages.
465
+ - `/auth sync push all`: manually broadcast all locally verified candidates without refreshing tokens. “Sent” does not mean the peer imported files; check `/auth sync status` and `/auth` on the peer.
454
466
 
455
467
  Equivalent commands:
456
468
 
@@ -467,7 +479,7 @@ Equivalent commands:
467
479
 
468
480
  If the requesting bot runtime has active turns, pending approvals, pending user inputs, or MCP elicitations, FoxClaw refuses manual auth switching to avoid changing accounts mid-request; another idle bot is unaffected.
469
481
 
470
- ### 6.4 How Auto-Rotation Works
482
+ ### 6.5 How Auto-Rotation Works
471
483
 
472
484
  When Codex reports a usage limit, missing login, expired auth, or similar auth error, FoxClaw tries to rotate automatically:
473
485
 
@@ -511,4 +523,5 @@ auth.json_backup # backup account, enable or disable as needed
511
523
 
512
524
  - [Beginner Install Guide](./install-for-beginners.md)
513
525
  - [Agent-Assisted Install](./agent-assisted-install.md)
526
+ - [Cross-Node Auth Sync Setup Guide](./cross-node-auth-sync.md)
514
527
  - [Troubleshooting](./troubleshooting.md)
@@ -0,0 +1,195 @@
1
+ # 跨节点 auth 同步配置指南
2
+
3
+ 本指南面向“多台自己控制的机器使用同一组合法 ChatGPT auth 候选”的场景。它把同一节点内的 auth 镜像扩展到跨机器:某台机器上的 Codex 自动刷新出新 token 后,FoxClaw 可以通过 Telegram Bot-to-Bot 私聊把加密 auth 包同步给其他节点;某台机器发现本地候选不可用时,也可以向 peer 拉取它已经持有的有效副本。
4
+
5
+ 这个功能默认关闭。它不需要公网 IP、FRP 或反向代理,但需要 Telegram Bot-to-Bot Communication Mode。
6
+
7
+ ## 适用边界
8
+
9
+ 适合:
10
+
11
+ - 这些 ChatGPT 账号和 auth 文件都由你合法拥有和维护。
12
+ - 多台机器都运行 FoxClaw,并且每台机器至少有一个 Telegram bot。
13
+ - 你希望 auth 文件在节点间自动保持较新,但不希望日常主动旋转 refresh token。
14
+ - 默认推荐每台机器只选择一个“联系人 bot”参与跨节点同步;同一节点内其他 bot 继续使用原本的本机 auth 镜像。
15
+
16
+ 不适合:
17
+
18
+ - 同步来源不可信、账号来源不合法,或你无法确认每台机器的管理员。
19
+ - 希望用 `/auth refresh all` 当作 refresh token 保活工具。
20
+ - 同一个 bot token 被多台机器同时 polling;这会破坏 Telegram update 分发和 FoxClaw 的运行假设。
21
+
22
+ ## 设计与安全模型
23
+
24
+ 跨节点同步使用“主动 push + 主动 pull + 跨节点刷新锁”三段互补:
25
+
26
+ - **Push**:本节点登录、Codex 自动刷新或 `/auth refresh all confirm` 成功并通过本机 usage 验证后,把较新的候选加密发送给 peer。
27
+ - **Pull**:本节点切换或重载 auth 前,如果本机其他 runtime 没有更新副本,会向 peer 请求同名、同账号的较新候选。
28
+ - **Lease**:执行会旋转 refresh token 的 `/auth refresh all confirm` 前,先向 peer 申请跨节点刷新锁;任一 peer 忙碌、拒绝或无响应都会阻止刷新。
29
+
30
+ 安全边界:
31
+
32
+ - Telegram 只承载密文。候选文件内容、候选名、account id 和 `last_refresh` 都在 AES-256-GCM payload 内。
33
+ - 只接收 `AUTH_SYNC_PEERS` 中列出的 peer bot 发来的同步文件。
34
+ - `AUTH_SYNC_KEY`、cluster、nonce 或 payload 校验失败时不会写盘。
35
+ - 远端导入必须等本机全局空闲,再临时验证 usage;验证成功后才写入候选。
36
+ - 同名候选如果已知属于不同 account id,永远拒绝覆盖。
37
+ - 同步包不会触发自动回复链路;FoxClaw 对包类型、nonce 和 peer allowlist 做过滤,避免 bot-to-bot 循环。
38
+
39
+ Telegram 官方 Bot Features 文档说明:私聊 bot-to-bot 需要发送方和接收方都启用 Bot-to-Bot Communication Mode,并提醒开发者处理 loop prevention。参考:https://core.telegram.org/bots/features#bot-to-bot-communication
40
+
41
+ ## 配置前准备
42
+
43
+ 假设有两台机器:
44
+
45
+ - 节点 A:bot `@foxclaw_node_a_bot`
46
+ - 节点 B:bot `@foxclaw_node_b_bot`
47
+
48
+ 这两个 bot 就是两个节点的联系人。`AUTH_SYNC_PEERS` 只需要写 peer 节点的联系人 bot,不需要把同一台机器上的所有 bot 都互相列进去。多 bot 模式下,FoxClaw 默认使用 `TG_BOT_TOKENS` 里的第一个 token 作为本节点联系人;如果你希望 5 号 bot 当联系人,就把 5 号 token 放到 `TG_BOT_TOKENS` 第一位,或者给全部 bot 都开启 Bot-to-Bot 作为临时兜底。
49
+
50
+ 每个节点都应该先能独立使用 FoxClaw:
51
+
52
+ ```bash
53
+ foxclaw doctor
54
+ foxclaw start
55
+ ```
56
+
57
+ 并且你能分别在 Telegram 私聊两个 bot,执行:
58
+
59
+ ```text
60
+ /status
61
+ /auth
62
+ ```
63
+
64
+ ## 在 @BotFather 开启 Bot-to-Bot
65
+
66
+ 对参与同步的每一个 bot 都做一遍:
67
+
68
+ 1. 建议使用最新版 Telegram 手机客户端;部分桌面端或旧客户端看不到这个开关。
69
+ 2. 打开 `https://t.me/BotFather?startapp`,或进入 `@BotFather` 资料页后点击 **Open App / 打开应用**。
70
+ 3. 在 BotFather MiniApp 中选择要参与同步的联系人 bot。
71
+ 4. 进入 Settings / Bot Settings。
72
+ 5. 找到 **Bot-to-Bot Communication Mode**。
73
+ 6. 启用该开关。
74
+ 7. 对所有节点的联系人 bot 重复以上步骤。
75
+
76
+ 不要走 `/mybots` → Bot Settings → **Configure Mini App**。那是配置你自己 bot 的 Mini App URL,不是 Bot-to-Bot Communication Mode。
77
+
78
+ 私聊跨节点同步要求双方都开启这个模式。只开一个通常不足以让两个 bot 互相私聊传输同步包。
79
+
80
+ 如果你看到 `Bad Request: USER_BOT_TO_BOT_DISABLED`,优先确认两件事:发送方联系人 bot 和接收方联系人 bot 都已经开启 Bot-to-Bot;多 bot 模式下,发送方联系人默认是 `TG_BOT_TOKENS` 的第一个 token,不一定是你当前输入命令的 bot。
81
+
82
+ ## .env 配置
83
+
84
+ 两台机器使用相同的 `AUTH_SYNC_KEY` 和 `AUTH_SYNC_CLUSTER_ID`,但 `AUTH_SYNC_NODE_ID` 必须不同。
85
+
86
+ 节点 A:
87
+
88
+ ```dotenv
89
+ TG_BOT_TOKENS=<node-a-contact-token>,<node-a-other-bot-token>
90
+ AUTH_SYNC_ENABLED=true
91
+ AUTH_SYNC_KEY=<至少32字节的共享密钥>
92
+ AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
93
+ AUTH_SYNC_NODE_ID=workstation-a
94
+ AUTH_SYNC_PEERS=@foxclaw_node_b_bot
95
+ ```
96
+
97
+ 节点 B:
98
+
99
+ ```dotenv
100
+ TG_BOT_TOKENS=<node-b-contact-token>,<node-b-other-bot-token>
101
+ AUTH_SYNC_ENABLED=true
102
+ AUTH_SYNC_KEY=<至少32字节的共享密钥>
103
+ AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
104
+ AUTH_SYNC_NODE_ID=workstation-b
105
+ AUTH_SYNC_PEERS=@foxclaw_node_a_bot
106
+ ```
107
+
108
+ 多节点时,`AUTH_SYNC_PEERS` 用英文逗号分隔:
109
+
110
+ ```dotenv
111
+ AUTH_SYNC_PEERS=@foxclaw_node_a_bot,@foxclaw_node_b_bot,@foxclaw_node_c_bot
112
+ ```
113
+
114
+ 建议用密码管理器或 `openssl` 生成共享密钥:
115
+
116
+ ```bash
117
+ openssl rand -base64 32
118
+ ```
119
+
120
+ 改完配置后重启每台机器的 FoxClaw:
121
+
122
+ ```bash
123
+ foxclaw restart
124
+ ```
125
+
126
+ ## 验证步骤
127
+
128
+ 1. 在每台节点的 bot 私聊里执行:
129
+
130
+ ```text
131
+ /auth sync status
132
+ ```
133
+
134
+ 应能看到 node id、peer 列表和 pending imports。
135
+
136
+ 2. 在节点 A 执行:
137
+
138
+ ```text
139
+ /auth sync test
140
+ ```
141
+
142
+ 节点 A 应提示已向 peer 发送测试 ping。节点 B 的 `/auth sync status` 应能看到最近收到的同步事件或测试状态变化。
143
+
144
+ 从 0.4.17 起,`/auth sync test` 会等待 peer 返回加密 pong。正常结果应该类似:
145
+
146
+ ```text
147
+ auth sync 测试完成:已发送 1,收到回应 1。
148
+ ```
149
+
150
+ 如果显示 `未回应:@peer_bot`,说明 Telegram 发送可能成功,但对方没有成功接收、解密、通过 allowlist,或没有运行同一组 auth sync 配置。
151
+
152
+ 3. 用低风险候选做第一次广播。先确认所有 bot runtime 空闲,然后在节点 A 执行:
153
+
154
+ ```text
155
+ /auth sync push all
156
+ ```
157
+
158
+ 4. 在节点 B 执行:
159
+
160
+ ```text
161
+ /auth sync status
162
+ /auth
163
+ ```
164
+
165
+ 确认待导入清单被处理,或候选已经出现/更新时间变新。
166
+
167
+ 注意:`/auth sync push all` 的“已发送”只代表本节点把加密包发给 Telegram 成功,不代表对端已经写盘。对端只有在全局空闲、usage 验证通过、同名候选 account id 一致,并且远端 `last_refresh` 比本地更新时才会覆盖文件。如果本地已经是相同或更新版本,文件不会变化,`最近导入` 也可能保持为空。
168
+
169
+ 5. 只有在完全理解 refresh token 轮换风险时,才测试:
170
+
171
+ ```text
172
+ /auth refresh all
173
+ /auth refresh all confirm
174
+ ```
175
+
176
+ 启用跨节点同步后,这个命令会先申请跨节点刷新锁。任一 peer 忙碌、拒绝或超时都会阻止刷新。
177
+
178
+ ## 常见问题
179
+
180
+ **`/auth sync test` 没反应**
181
+
182
+ - 确认两个 bot 都已在 `@BotFather` 开启 Bot-to-Bot Communication Mode。
183
+ - 确认 `AUTH_SYNC_PEERS` 写的是 peer 的 `@username`,不是 token。
184
+ - 确认两边 `AUTH_SYNC_KEY` 和 `AUTH_SYNC_CLUSTER_ID` 完全一致。
185
+ - 确认两个节点都重启过 FoxClaw。
186
+
187
+ **收到同步包但没有写入候选**
188
+
189
+ - 本机可能不是全局空闲;有 turn、审批、待输入、登录流程或镜像写入时会排队。
190
+ - usage 验证失败会拒绝写盘。
191
+ - 同名候选属于不同 account id 时会拒绝覆盖。
192
+
193
+ **要不要定期 `/auth refresh all confirm` 保活**
194
+
195
+ 不要。Codex 会按 access token 到期自动刷新。FoxClaw 的跨节点同步会同步“已经成功刷新的新 auth”,不应该把 refresh all 当作日常保活。
@@ -419,7 +419,19 @@ OpenAI 没有公开 ChatGPT refresh token 的固定有效期或旧 token 重放
419
419
 
420
420
  ### 6.4 跨节点 auth 同步
421
421
 
422
- 跨节点 auth 同步默认关闭。它适合你在多台自己控制的机器上使用同一组合法 ChatGPT 账号候选,并希望某台机器上 Codex 自动刷新出的新 token 能同步到其他机器。v1 使用 Telegram Bot-to-Bot 私聊传输加密文件,不需要公网 IP 或 FRP;需要先在 BotFather 为参与同步的 bot 开启 Bot-to-Bot Communication Mode
422
+ 跨节点 auth 同步默认关闭。它适合你在多台自己控制的机器上使用同一组合法 ChatGPT 账号候选,并希望某台机器上 Codex 自动刷新出的新 token 能同步到其他机器。v1 使用 Telegram Bot-to-Bot 私聊传输加密文件,不需要公网 IP 或 FRP。推荐每台机器选择一个联系人 bot;同一节点内其他 bot 继续使用本机 auth 镜像。多 bot 模式下,默认联系人是 `TG_BOT_TOKENS` 的第一个 token
423
+
424
+ 完整设计、安全边界、`.env` 示例和排查步骤见 [跨节点 auth 同步配置指南](./cross-node-auth-sync.md)。
425
+
426
+ 在 `@BotFather` 中对每个参与同步的 bot 执行:
427
+
428
+ 1. 用最新版 Telegram 手机客户端打开 `https://t.me/BotFather?startapp`,或进入 `@BotFather` 资料页点 **Open App / 打开应用**。
429
+ 2. 在 BotFather MiniApp 中选择参与同步的联系人 bot。
430
+ 3. 打开 Settings / Bot Settings。
431
+ 4. 找到并启用 **Bot-to-Bot Communication Mode**。
432
+ 5. 对所有节点的联系人 bot 重复;私聊同步要求发送方和接收方都开启。
433
+
434
+ 不要走 `/mybots` → Bot Settings → **Configure Mini App**;那是配置 bot 的 Mini App URL,不是 Bot-to-Bot Communication Mode。
423
435
 
424
436
  配置示例:
425
437
 
@@ -449,8 +461,8 @@ AUTH_SYNC_NODE_ID=workstation-a
449
461
  命令:
450
462
 
451
463
  - `/auth sync status`:查看 node id、peer、最近收发、最近导入、待导入和最近错误。
452
- - `/auth sync test`:发送加密 ping,确认 peer、共享密钥和 Bot-to-Bot 私聊可用。
453
- - `/auth sync push all`:手动广播当前节点已验证的全部候选,不刷新 token
464
+ - `/auth sync test`:发送加密 ping 并等待 peer 返回 pong,确认 peer、共享密钥和 Bot-to-Bot 私聊可用。
465
+ - `/auth sync push all`:手动广播当前节点已验证的全部候选,不刷新 token;“已发送”不等于对端已经导入,需要在 peer 上看 `/auth sync status` 和 `/auth`。
454
466
 
455
467
  命令等价用法:
456
468
 
@@ -467,7 +479,7 @@ AUTH_SYNC_NODE_ID=workstation-a
467
479
 
468
480
  切换 auth 时,如果当前 bot runtime 还有活跃 turn、待审批、待用户输入或 MCP elicitation,FoxClaw 会先拒绝切换,避免中途换号破坏正在进行的请求;另一个空闲 bot 不受影响。
469
481
 
470
- ### 6.4 自动轮转如何工作
482
+ ### 6.5 自动轮转如何工作
471
483
 
472
484
  当 Codex 返回用量限制、未登录、认证失效或类似 auth 错误时,FoxClaw 会尝试自动轮转:
473
485
 
@@ -511,4 +523,5 @@ auth.json_backup # 备用账号,可按需 enable/disable
511
523
 
512
524
  - [新手安装指南](./install-for-beginners.md)
513
525
  - [Agent 辅助安装](./agent-assisted-install.md)
526
+ - [跨节点 auth 同步配置指南](./cross-node-auth-sync.md)
514
527
  - [故障排查](./troubleshooting.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -70,7 +70,7 @@ Telegram behavior:
70
70
  - `/auth` paginates large candidate inventories at 8 rows per page, supports all/enabled/attention filters and filename search, renders actual observed quota windows in text rows, uses compact two-number remaining quota labels on buttons, and omits the repeated `auth.json_` prefix from panel labels without renaming files
71
71
  - treat the `/auth` `not recently refreshed` state as a maintenance hint only: OpenAI does not publish a fixed ChatGPT refresh-token lifetime or replay grace period, and FoxClaw must not add routine bulk keepalive refreshes
72
72
  - `/auth refresh all` remains a command-only maintenance action, not a panel button; it requires explicit token-rotation risk confirmation, then force-refreshes every ChatGPT candidate through Codex `account/read refreshToken=true`, validates usage, mirrors successful candidates, and restores the original current auth while every runtime is globally idle
73
- - optional cross-node auth sync uses Telegram Bot-to-Bot private messages with encrypted auth bundles; configure `AUTH_SYNC_ENABLED=true`, a shared `AUTH_SYNC_KEY`, and `AUTH_SYNC_PEERS`, then use `/auth sync status`, `/auth sync test`, and `/auth sync push all`; cross-node recovery pulls already-held valid peer copies and does not auto-refresh tokens
73
+ - optional cross-node auth sync uses Telegram Bot-to-Bot private messages with encrypted auth bundles; the default topology is one contact bot per node, and in multi-bot mode the first `TG_BOT_TOKENS` entry is the contact transport; configure `AUTH_SYNC_ENABLED=true`, a shared `AUTH_SYNC_KEY`, and `AUTH_SYNC_PEERS` containing peer contact bot usernames, then use `/auth sync status`, `/auth sync test`, and `/auth sync push all`; `/auth sync test` waits for encrypted peer pong replies, while push-all only proves send success and peer import must be checked on the receiver
74
74
  - when cross-node auth sync is enabled, `/auth refresh all confirm` must obtain a cross-node refresh lease before rotating refresh tokens; any busy, denying, or non-responsive peer blocks the refresh
75
75
  - if `TG_BOT_TOKEN` is also set to one exact token from `TG_BOT_TOKENS`, that bot uses the default/shared-terminal runtime instead of an isolated Telegram home
76
76
  - if Weixin is enabled alongside multiple Telegram bots, it remains on the default Codex runtime instead of borrowing a Telegram bot runtime