@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 +3 -1
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/README_EN.md +1 -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 +24 -6
- package/docs/user-manual.md +10 -9
- package/docs/zh/cross-node-auth-sync.md +24 -6
- package/docs/zh/user-manual.md +10 -9
- 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,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=@
|
|
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=@
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
66
|
-
2.
|
|
67
|
-
3.
|
|
68
|
-
4. Open
|
|
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
|
|
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
|
-
|
package/docs/user-manual.md
CHANGED
|
@@ -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.
|
|
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.
|
|
429
|
-
2.
|
|
430
|
-
3.
|
|
431
|
-
4.
|
|
432
|
-
5.
|
|
433
|
-
|
|
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.
|
|
66
|
-
2.
|
|
67
|
-
3.
|
|
68
|
-
4.
|
|
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.
|
|
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
|
-
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -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
|
|
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.
|
|
429
|
-
2.
|
|
430
|
-
3.
|
|
431
|
-
4.
|
|
432
|
-
5.
|
|
433
|
-
|
|
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
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
|