@foxden-app/foxclaw 0.4.13 → 0.4.14
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 +12 -2
- package/CHANGELOG.md +14 -0
- package/README.md +3 -1
- package/README_EN.md +3 -1
- package/dist/auth/cross_node_sync.d.ts +112 -0
- package/dist/auth/cross_node_sync.js +682 -0
- package/dist/auth/mirror.d.ts +37 -1
- package/dist/auth/mirror.js +136 -3
- package/dist/config.d.ts +10 -0
- package/dist/config.js +14 -0
- package/dist/controller/controller.d.ts +20 -0
- package/dist/controller/controller.js +173 -4
- package/dist/i18n.d.ts +40 -2
- package/dist/i18n.js +40 -2
- package/dist/main.js +192 -8
- package/dist/telegram/api.d.ts +6 -0
- package/dist/telegram/api.js +50 -0
- package/dist/telegram/gateway.d.ts +9 -0
- package/dist/telegram/gateway.js +33 -2
- package/dist/types.d.ts +14 -0
- package/docs/user-manual.md +36 -0
- package/docs/zh/user-manual.md +36 -0
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +2 -0
package/dist/telegram/gateway.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
-
import { callTelegramApi, downloadTelegramFile, getTelegramFile } from './api.js';
|
|
3
|
+
import { callTelegramApi, callTelegramMultipartApi, downloadTelegramFile, getTelegramFile } from './api.js';
|
|
4
4
|
import { getTelegramCommands } from '../i18n.js';
|
|
5
5
|
import { toTelegramBridgeScopeId } from '../core/bridge_scope.js';
|
|
6
6
|
import { createTelegramScopeId } from './scope.js';
|
|
@@ -56,6 +56,21 @@ export class TelegramGateway extends EventEmitter {
|
|
|
56
56
|
async sendHtmlMessage(chatId, text, inlineKeyboard, messageThreadId) {
|
|
57
57
|
return this.sendMessageWithOptions(chatId, text, inlineKeyboard, 'HTML', messageThreadId);
|
|
58
58
|
}
|
|
59
|
+
async sendDocument(chatId, filename, contents, caption) {
|
|
60
|
+
const result = await callTelegramMultipartApi(this.botToken, 'sendDocument', {
|
|
61
|
+
chat_id: chatId,
|
|
62
|
+
...(caption ? { caption } : {}),
|
|
63
|
+
}, [{
|
|
64
|
+
fieldName: 'document',
|
|
65
|
+
filename,
|
|
66
|
+
contents,
|
|
67
|
+
contentType: 'application/json',
|
|
68
|
+
}]);
|
|
69
|
+
if (!result.ok || !result.result) {
|
|
70
|
+
throw new Error(result.description || 'Failed to send Telegram document');
|
|
71
|
+
}
|
|
72
|
+
return result.result.message_id;
|
|
73
|
+
}
|
|
59
74
|
async sendMessageDraft(chatId, draftId, text, messageThreadId) {
|
|
60
75
|
const result = await callTelegramApi(this.botToken, 'sendMessageDraft', {
|
|
61
76
|
chat_id: chatId,
|
|
@@ -197,8 +212,24 @@ export class TelegramGateway extends EventEmitter {
|
|
|
197
212
|
}
|
|
198
213
|
async handleUpdate(update) {
|
|
199
214
|
if (update.message && update.message.from && this.isAllowedChat(update.message.chat)) {
|
|
200
|
-
if (String(update.message.from.id) !== this.allowedUserId)
|
|
215
|
+
if (String(update.message.from.id) !== this.allowedUserId) {
|
|
216
|
+
if (update.message.chat.type === 'private') {
|
|
217
|
+
const text = update.message.text ?? update.message.caption ?? '';
|
|
218
|
+
const attachments = extractAttachments(update.message);
|
|
219
|
+
const document = attachments.find((attachment) => attachment.kind === 'document');
|
|
220
|
+
if (document) {
|
|
221
|
+
this.emit('peerDocument', {
|
|
222
|
+
chatId: String(update.message.chat.id),
|
|
223
|
+
userId: String(update.message.from.id),
|
|
224
|
+
username: update.message.from.username ?? null,
|
|
225
|
+
text,
|
|
226
|
+
messageId: update.message.message_id,
|
|
227
|
+
attachment: document,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
201
231
|
return;
|
|
232
|
+
}
|
|
202
233
|
const attachments = extractAttachments(update.message);
|
|
203
234
|
const text = update.message.text ?? update.message.caption ?? '';
|
|
204
235
|
const topicId = update.message.message_thread_id ?? null;
|
package/dist/types.d.ts
CHANGED
|
@@ -360,6 +360,20 @@ export interface RuntimeStatus {
|
|
|
360
360
|
sourceLabel: string;
|
|
361
361
|
syncedAt: string;
|
|
362
362
|
} | null;
|
|
363
|
+
authSync?: {
|
|
364
|
+
enabled: boolean;
|
|
365
|
+
nodeId: string | null;
|
|
366
|
+
peers: string[];
|
|
367
|
+
pendingImports: number;
|
|
368
|
+
lastSentAt: string | null;
|
|
369
|
+
lastReceivedAt: string | null;
|
|
370
|
+
lastImportedAt: string | null;
|
|
371
|
+
lastImportCandidate: string | null;
|
|
372
|
+
lastPullAt: string | null;
|
|
373
|
+
lastPullCandidate: string | null;
|
|
374
|
+
lastError: string | null;
|
|
375
|
+
activeLeaseId: string | null;
|
|
376
|
+
} | null;
|
|
363
377
|
lastUpdate?: {
|
|
364
378
|
state: string;
|
|
365
379
|
fromVersion: string;
|
package/docs/user-manual.md
CHANGED
|
@@ -417,6 +417,41 @@ The right-side `✅` / `⏸️` button controls whether the candidate participat
|
|
|
417
417
|
|
|
418
418
|
OpenAI does not publish a fixed ChatGPT refresh-token lifetime or an old-token replay grace period. Codex refreshes automatically when an access token approaches expiry; when it cannot parse the access-token `exp`, current Codex uses a `last_refresh` fallback of about 8 days. The panel labels candidates without a refresh record in that interval as `not recently refreshed`, but this is only a maintenance hint. It does not prove expiry and does not trigger bulk keepalive refreshes. Do not use `/auth refresh all` as a routine keepalive command.
|
|
419
419
|
|
|
420
|
+
### 6.4 Cross-Node Auth Sync
|
|
421
|
+
|
|
422
|
+
Cross-node auth sync is disabled by default. It is for multiple machines you control that share the same legally owned ChatGPT auth candidate pool, so a token refreshed by Codex on one node can be copied to the others. v1 uses Telegram Bot-to-Bot private messages to carry encrypted files, so it does not require public IPs or FRP. Enable Bot-to-Bot Communication Mode for the participating bots in BotFather first.
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
|
|
426
|
+
```dotenv
|
|
427
|
+
AUTH_SYNC_ENABLED=true
|
|
428
|
+
AUTH_SYNC_KEY=<shared key with at least 32 bytes>
|
|
429
|
+
AUTH_SYNC_PEERS=@other_node_bot,@third_node_bot
|
|
430
|
+
AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
|
|
431
|
+
# Optional; FoxClaw generates and persists a local node id when omitted.
|
|
432
|
+
AUTH_SYNC_NODE_ID=workstation-a
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
Safety boundaries:
|
|
436
|
+
|
|
437
|
+
- Telegram only carries ciphertext. Auth contents, candidate names, account ids, and `last_refresh` live inside the AES-256-GCM encrypted payload.
|
|
438
|
+
- FoxClaw only accepts sync files from bots listed in `AUTH_SYNC_PEERS`; wrong key, cluster, nonce, or payload validation never writes files.
|
|
439
|
+
- Remote imports wait for global local idleness, temporarily switch to the candidate for app-server usage validation, and only then write the candidate.
|
|
440
|
+
- A same-name candidate known to belong to a different account id is never overwritten.
|
|
441
|
+
- Cross-node recovery only pulls an already-held valid peer copy. It does not automatically rotate refresh tokens; if no peer has a usable copy, it stops and asks you to maintain auth on one node manually.
|
|
442
|
+
|
|
443
|
+
Dual-active behavior:
|
|
444
|
+
|
|
445
|
+
- Push: after local login, Codex automatic refresh, or `/auth refresh all` succeeds and passes local mirror validation, the newer candidate is encrypted and pushed to peers.
|
|
446
|
+
- Pull: before auth switch or reload, FoxClaw first searches local runtimes for a newer same-account candidate; if none is found, it asks peers for a newer same-name same-account copy.
|
|
447
|
+
- Lease: `/auth refresh all confirm` requests a cross-node refresh lease before rotating tokens. Any busy, denying, or non-responsive peer blocks the refresh.
|
|
448
|
+
|
|
449
|
+
Commands:
|
|
450
|
+
|
|
451
|
+
- `/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.
|
|
454
|
+
|
|
420
455
|
Equivalent commands:
|
|
421
456
|
|
|
422
457
|
- `/auth` or `/auth list [keyword]`: show candidates, optionally filtered by filename.
|
|
@@ -428,6 +463,7 @@ Equivalent commands:
|
|
|
428
463
|
- `/auth reload` or `/auth_reload`: restart app-server and reload the current `auth.json`.
|
|
429
464
|
- `/auth refresh all`: show the refresh-token rotation risk confirmation.
|
|
430
465
|
- `/auth refresh all confirm`: run Refresh all after accepting the token-rotation risk.
|
|
466
|
+
- `/auth sync status|test|push all`: inspect, test, or manually push cross-node auth sync.
|
|
431
467
|
|
|
432
468
|
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.
|
|
433
469
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -417,6 +417,41 @@ Candidates: 2
|
|
|
417
417
|
|
|
418
418
|
OpenAI 没有公开 ChatGPT refresh token 的固定有效期或旧 token 重放宽限期。Codex 会在 access token 临近到期时自动刷新;如果 access token 里无法解析 `exp`,Codex 当前使用 `last_refresh` 超过约 8 天作为兜底刷新条件。面板把超过 8 天没有刷新记录的候选标为“长期未刷新”,但这只是维护提醒,不代表 refresh token 已经过期,也不会触发批量保活刷新。不要把 `/auth refresh all` 当作日常保活命令。
|
|
419
419
|
|
|
420
|
+
### 6.4 跨节点 auth 同步
|
|
421
|
+
|
|
422
|
+
跨节点 auth 同步默认关闭。它适合你在多台自己控制的机器上使用同一组合法 ChatGPT 账号候选,并希望某台机器上 Codex 自动刷新出的新 token 能同步到其他机器。v1 使用 Telegram Bot-to-Bot 私聊传输加密文件,不需要公网 IP 或 FRP;需要先在 BotFather 为参与同步的 bot 开启 Bot-to-Bot Communication Mode。
|
|
423
|
+
|
|
424
|
+
配置示例:
|
|
425
|
+
|
|
426
|
+
```dotenv
|
|
427
|
+
AUTH_SYNC_ENABLED=true
|
|
428
|
+
AUTH_SYNC_KEY=<至少32字节的共享密钥>
|
|
429
|
+
AUTH_SYNC_PEERS=@other_node_bot,@third_node_bot
|
|
430
|
+
AUTH_SYNC_CLUSTER_ID=my-codex-auth-pool
|
|
431
|
+
# 可选;不填时 FoxClaw 会生成并持久化本机 node id
|
|
432
|
+
AUTH_SYNC_NODE_ID=workstation-a
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
安全边界:
|
|
436
|
+
|
|
437
|
+
- Telegram 只承载密文;auth 文件内容、候选名、account id、`last_refresh` 都在 AES-256-GCM 加密 payload 内。
|
|
438
|
+
- 只接收 `AUTH_SYNC_PEERS` 中 peer bot 发来的同步文件;密钥、cluster、nonce 或 payload 校验失败时不会写盘。
|
|
439
|
+
- 远端导入必须等本机全局空闲,再临时切换到待验证 auth、重启 app-server、读取 usage 验证成功后才写入候选。
|
|
440
|
+
- 同名候选如果已知属于不同 account id,永远拒绝覆盖。
|
|
441
|
+
- 跨节点恢复只拉取 peer 已持有的有效副本,不会自动触发 refresh token 轮换;找不到有效副本时会停止,提示你在一个节点手动维护授权。
|
|
442
|
+
|
|
443
|
+
双主动流程:
|
|
444
|
+
|
|
445
|
+
- push:本节点登录、Codex 自动刷新或 `/auth refresh all` 成功并通过本机镜像验证后,会主动把较新的候选加密推送给 peer。
|
|
446
|
+
- pull:本节点切换或重载 auth 前如果发现本地候选不是最新,会先查本机其他 runtime;仍找不到时,再向 peer 拉取同名同账号的较新副本。
|
|
447
|
+
- lease:执行会旋转 refresh token 的 `/auth refresh all confirm` 前,会向 peer 申请跨节点刷新锁。任一 peer 忙碌、拒绝或无响应都会阻止刷新。
|
|
448
|
+
|
|
449
|
+
命令:
|
|
450
|
+
|
|
451
|
+
- `/auth sync status`:查看 node id、peer、最近收发、最近导入、待导入和最近错误。
|
|
452
|
+
- `/auth sync test`:发送加密 ping,确认 peer、共享密钥和 Bot-to-Bot 私聊可用。
|
|
453
|
+
- `/auth sync push all`:手动广播当前节点已验证的全部候选,不刷新 token。
|
|
454
|
+
|
|
420
455
|
命令等价用法:
|
|
421
456
|
|
|
422
457
|
- `/auth` 或 `/auth list [关键词]`:查看候选,可按文件名搜索。
|
|
@@ -428,6 +463,7 @@ OpenAI 没有公开 ChatGPT refresh token 的固定有效期或旧 token 重放
|
|
|
428
463
|
- `/auth reload` 或 `/auth_reload`:重启 app-server,重新加载当前 `auth.json`。
|
|
429
464
|
- `/auth refresh all`:显示 refresh token 轮换风险确认。
|
|
430
465
|
- `/auth refresh all confirm`:接受 token 轮换风险后执行刷新全部。
|
|
466
|
+
- `/auth sync status|test|push all`:查看、测试或手动推送跨节点 auth 同步。
|
|
431
467
|
|
|
432
468
|
切换 auth 时,如果当前 bot runtime 还有活跃 turn、待审批、待用户输入或 MCP elicitation,FoxClaw 会先拒绝切换,避免中途换号破坏正在进行的请求;另一个空闲 bot 不受影响。
|
|
433
469
|
|
package/package.json
CHANGED
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -70,6 +70,8 @@ Telegram behavior:
|
|
|
70
70
|
- `/auth` paginates large candidate inventories at 8 rows per page, supports all/enabled/attention filters and filename search, renders actual observed quota windows in text rows, uses compact two-number remaining quota labels on buttons, and omits the repeated `auth.json_` prefix from panel labels without renaming files
|
|
71
71
|
- treat the `/auth` `not recently refreshed` state as a maintenance hint only: OpenAI does not publish a fixed ChatGPT refresh-token lifetime or replay grace period, and FoxClaw must not add routine bulk keepalive refreshes
|
|
72
72
|
- `/auth refresh all` remains a command-only maintenance action, not a panel button; it requires explicit token-rotation risk confirmation, then force-refreshes every ChatGPT candidate through Codex `account/read refreshToken=true`, validates usage, mirrors successful candidates, and restores the original current auth while every runtime is globally idle
|
|
73
|
+
- optional cross-node auth sync uses Telegram Bot-to-Bot private messages with encrypted auth bundles; configure `AUTH_SYNC_ENABLED=true`, a shared `AUTH_SYNC_KEY`, and `AUTH_SYNC_PEERS`, then use `/auth sync status`, `/auth sync test`, and `/auth sync push all`; cross-node recovery pulls already-held valid peer copies and does not auto-refresh tokens
|
|
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
|
|
73
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
|
|
74
76
|
- if Weixin is enabled alongside multiple Telegram bots, it remains on the default Codex runtime instead of borrowing a Telegram bot runtime
|
|
75
77
|
- in a group with multiple configured bots, address a bot by mention, reply, or suffixed command such as `/status@botname`
|