@foxden-app/foxclaw 0.4.16 → 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,18 @@
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
+
5
17
  ## 0.4.16 - 2026-06-04
6
18
 
7
19
  ### 中文
package/README.md CHANGED
@@ -212,7 +212,7 @@ TG_BOT_TOKEN=123456:token_a
212
212
 
213
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 摘要。
214
214
 
215
- 多台机器共享同一合法账号池时,可以启用可选跨节点 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` 会先申请跨节点刷新锁。完整配置、`@BotFather` 操作和验证步骤见 [跨节点 auth 同步配置指南](./docs/zh/cross-node-auth-sync.md)。
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)。
216
216
 
217
217
  如果你需要一路 Telegram 与终端互通 session,把同一个 token 同时填入 `TG_BOT_TOKENS` 和 `TG_BOT_TOKEN`。这个 bot 使用默认 `CODEX_HOME`(未设置时通常是 `~/.codex`)和默认 auth,因此能看到终端 Codex 的本地线程;它不再享有隔离 runtime 的“互不影响”保证,切换 auth 会影响终端和其他默认 runtime。
218
218
 
package/README_EN.md CHANGED
@@ -212,7 +212,7 @@ TG_BOT_TOKEN=123456:token_a
212
212
 
213
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.
214
214
 
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_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
+ 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).
216
216
 
217
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.
218
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;
@@ -11,6 +11,7 @@ Use it when:
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
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.
14
15
 
15
16
  Do not use it when:
16
17
 
@@ -44,6 +45,8 @@ Assume two machines:
44
45
  - Node A: bot `@foxclaw_node_a_bot`
45
46
  - Node B: bot `@foxclaw_node_b_bot`
46
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
+
47
50
  Each node should already work independently:
48
51
 
49
52
  ```bash
@@ -62,16 +65,20 @@ In a private Telegram chat with each bot, verify:
62
65
 
63
66
  Repeat this for every participating bot:
64
67
 
65
- 1. Open Telegram and enter `@BotFather`.
66
- 2. Send `/mybots`.
67
- 3. Select the bot that will participate in auth sync.
68
- 4. Open the bot settings / Mini App settings interface.
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.
69
72
  5. Find **Bot-to-Bot Communication Mode**.
70
73
  6. Enable it.
71
- 7. Repeat for every peer bot.
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.
72
77
 
73
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.
74
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
+
75
82
  ## .env Configuration
76
83
 
77
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`.
@@ -79,6 +86,7 @@ Use the same `AUTH_SYNC_KEY` and `AUTH_SYNC_CLUSTER_ID` on all nodes, but give e
79
86
  Node A:
80
87
 
81
88
  ```dotenv
89
+ TG_BOT_TOKENS=<node-a-contact-token>,<node-a-other-bot-token>
82
90
  AUTH_SYNC_ENABLED=true
83
91
  AUTH_SYNC_KEY=<shared key with at least 32 bytes>
84
92
  AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
@@ -89,6 +97,7 @@ AUTH_SYNC_PEERS=@foxclaw_node_b_bot
89
97
  Node B:
90
98
 
91
99
  ```dotenv
100
+ TG_BOT_TOKENS=<node-b-contact-token>,<node-b-other-bot-token>
92
101
  AUTH_SYNC_ENABLED=true
93
102
  AUTH_SYNC_KEY=<shared key with at least 32 bytes>
94
103
  AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
@@ -132,6 +141,14 @@ You should see the node id, peer list, and pending imports.
132
141
 
133
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.
134
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
+
135
152
  3. Use a low-risk candidate for the first broadcast. Make sure every runtime is idle, then run on node A:
136
153
 
137
154
  ```text
@@ -147,6 +164,8 @@ Node A should report that it sent a test ping. Node B's `/auth sync status` shou
147
164
 
148
165
  Confirm that pending imports were processed, or that the candidate exists or has a newer timestamp.
149
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
+
150
169
  5. Only test refresh-token rotation after you understand the risk:
151
170
 
152
171
  ```text
@@ -174,4 +193,3 @@ With cross-node sync enabled, this command first requests a cross-node refresh l
174
193
  **Should I periodically run `/auth refresh all confirm` as keepalive?**
175
194
 
176
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.
177
-
@@ -419,18 +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
423
 
424
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
425
 
426
426
  In `@BotFather`, repeat this for every participating bot:
427
427
 
428
- 1. Open `@BotFather`.
429
- 2. Send `/mybots`.
430
- 3. Select the bot that will participate in sync.
431
- 4. Open the bot settings / Mini App settings interface.
432
- 5. Find and enable **Bot-to-Bot Communication Mode**.
433
- 6. Repeat for every peer bot; private sync requires this mode on both sender and recipient.
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.
434
435
 
435
436
  Example:
436
437
 
@@ -460,8 +461,8 @@ Dual-active behavior:
460
461
  Commands:
461
462
 
462
463
  - `/auth sync status`: show node id, peers, recent sends/receives/imports, pending imports, and the latest error.
463
- - `/auth sync test`: send an encrypted ping to verify peer config, shared key, and Bot-to-Bot private messages.
464
- - `/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.
465
466
 
466
467
  Equivalent commands:
467
468
 
@@ -11,6 +11,7 @@
11
11
  - 这些 ChatGPT 账号和 auth 文件都由你合法拥有和维护。
12
12
  - 多台机器都运行 FoxClaw,并且每台机器至少有一个 Telegram bot。
13
13
  - 你希望 auth 文件在节点间自动保持较新,但不希望日常主动旋转 refresh token。
14
+ - 默认推荐每台机器只选择一个“联系人 bot”参与跨节点同步;同一节点内其他 bot 继续使用原本的本机 auth 镜像。
14
15
 
15
16
  不适合:
16
17
 
@@ -44,6 +45,8 @@ Telegram 官方 Bot Features 文档说明:私聊 bot-to-bot 需要发送方和
44
45
  - 节点 A:bot `@foxclaw_node_a_bot`
45
46
  - 节点 B:bot `@foxclaw_node_b_bot`
46
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
+
47
50
  每个节点都应该先能独立使用 FoxClaw:
48
51
 
49
52
  ```bash
@@ -62,16 +65,20 @@ foxclaw start
62
65
 
63
66
  对参与同步的每一个 bot 都做一遍:
64
67
 
65
- 1. 打开 Telegram,进入 `@BotFather`。
66
- 2. 发送 `/mybots`。
67
- 3. 选择要参与同步的 bot。
68
- 4. 打开 BotFather 的 bot settings / Mini App 设置界面。
68
+ 1. 建议使用最新版 Telegram 手机客户端;部分桌面端或旧客户端看不到这个开关。
69
+ 2. 打开 `https://t.me/BotFather?startapp`,或进入 `@BotFather` 资料页后点击 **Open App / 打开应用**。
70
+ 3. BotFather MiniApp 中选择要参与同步的联系人 bot。
71
+ 4. 进入 Settings / Bot Settings。
69
72
  5. 找到 **Bot-to-Bot Communication Mode**。
70
73
  6. 启用该开关。
71
- 7. 对所有 peer bot 重复以上步骤。
74
+ 7. 对所有节点的联系人 bot 重复以上步骤。
75
+
76
+ 不要走 `/mybots` → Bot Settings → **Configure Mini App**。那是配置你自己 bot 的 Mini App URL,不是 Bot-to-Bot Communication Mode。
72
77
 
73
78
  私聊跨节点同步要求双方都开启这个模式。只开一个通常不足以让两个 bot 互相私聊传输同步包。
74
79
 
80
+ 如果你看到 `Bad Request: USER_BOT_TO_BOT_DISABLED`,优先确认两件事:发送方联系人 bot 和接收方联系人 bot 都已经开启 Bot-to-Bot;多 bot 模式下,发送方联系人默认是 `TG_BOT_TOKENS` 的第一个 token,不一定是你当前输入命令的 bot。
81
+
75
82
  ## .env 配置
76
83
 
77
84
  两台机器使用相同的 `AUTH_SYNC_KEY` 和 `AUTH_SYNC_CLUSTER_ID`,但 `AUTH_SYNC_NODE_ID` 必须不同。
@@ -79,6 +86,7 @@ foxclaw start
79
86
  节点 A:
80
87
 
81
88
  ```dotenv
89
+ TG_BOT_TOKENS=<node-a-contact-token>,<node-a-other-bot-token>
82
90
  AUTH_SYNC_ENABLED=true
83
91
  AUTH_SYNC_KEY=<至少32字节的共享密钥>
84
92
  AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
@@ -89,6 +97,7 @@ AUTH_SYNC_PEERS=@foxclaw_node_b_bot
89
97
  节点 B:
90
98
 
91
99
  ```dotenv
100
+ TG_BOT_TOKENS=<node-b-contact-token>,<node-b-other-bot-token>
92
101
  AUTH_SYNC_ENABLED=true
93
102
  AUTH_SYNC_KEY=<至少32字节的共享密钥>
94
103
  AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
@@ -132,6 +141,14 @@ foxclaw restart
132
141
 
133
142
  节点 A 应提示已向 peer 发送测试 ping。节点 B 的 `/auth sync status` 应能看到最近收到的同步事件或测试状态变化。
134
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
+
135
152
  3. 用低风险候选做第一次广播。先确认所有 bot runtime 空闲,然后在节点 A 执行:
136
153
 
137
154
  ```text
@@ -147,6 +164,8 @@ foxclaw restart
147
164
 
148
165
  确认待导入清单被处理,或候选已经出现/更新时间变新。
149
166
 
167
+ 注意:`/auth sync push all` 的“已发送”只代表本节点把加密包发给 Telegram 成功,不代表对端已经写盘。对端只有在全局空闲、usage 验证通过、同名候选 account id 一致,并且远端 `last_refresh` 比本地更新时才会覆盖文件。如果本地已经是相同或更新版本,文件不会变化,`最近导入` 也可能保持为空。
168
+
150
169
  5. 只有在完全理解 refresh token 轮换风险时,才测试:
151
170
 
152
171
  ```text
@@ -174,4 +193,3 @@ foxclaw restart
174
193
  **要不要定期 `/auth refresh all confirm` 保活**
175
194
 
176
195
  不要。Codex 会按 access token 到期自动刷新。FoxClaw 的跨节点同步会同步“已经成功刷新的新 auth”,不应该把 refresh all 当作日常保活。
177
-
@@ -419,18 +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
423
 
424
424
  完整设计、安全边界、`.env` 示例和排查步骤见 [跨节点 auth 同步配置指南](./cross-node-auth-sync.md)。
425
425
 
426
426
  在 `@BotFather` 中对每个参与同步的 bot 执行:
427
427
 
428
- 1. 打开 `@BotFather`。
429
- 2. 发送 `/mybots`。
430
- 3. 选择参与同步的 bot
431
- 4. 打开 bot settings / Mini App 设置界面。
432
- 5. 找到并启用 **Bot-to-Bot Communication Mode**。
433
- 6. 对所有 peer bot 重复;私聊同步要求发送方和接收方都开启。
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。
434
435
 
435
436
  配置示例:
436
437
 
@@ -460,8 +461,8 @@ AUTH_SYNC_NODE_ID=workstation-a
460
461
  命令:
461
462
 
462
463
  - `/auth sync status`:查看 node id、peer、最近收发、最近导入、待导入和最近错误。
463
- - `/auth sync test`:发送加密 ping,确认 peer、共享密钥和 Bot-to-Bot 私聊可用。
464
- - `/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`。
465
466
 
466
467
  命令等价用法:
467
468
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.4.16",
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