@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 +3 -1
- package/CHANGELOG.md +22 -0
- package/README.md +2 -1
- package/README_EN.md +2 -1
- package/dist/auth/cross_node_sync.d.ts +12 -3
- package/dist/auth/cross_node_sync.js +68 -4
- package/dist/controller/controller.d.ts +2 -0
- package/dist/controller/controller.js +7 -1
- package/dist/i18n.d.ts +8 -4
- package/dist/i18n.js +8 -4
- package/dist/main.js +12 -7
- package/dist/types.d.ts +1 -0
- package/docs/cross-node-auth-sync.md +195 -0
- package/docs/user-manual.md +17 -4
- package/docs/zh/cross-node-auth-sync.md +195 -0
- package/docs/zh/user-manual.md +17 -4
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +1 -1
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
|
|
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=@
|
|
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=@
|
|
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
|
-
|
|
245
|
-
|
|
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) {
|
|
@@ -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
|
-
|
|
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
|
|
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: "
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
@@ -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.
|
package/docs/user-manual.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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 当作日常保活。
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -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
|
|
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.
|
|
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
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -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
|
|
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
|