@foxden-app/foxclaw 0.7.2 → 0.7.3
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 +1 -0
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/README_EN.md +1 -1
- package/dist/auth/cross_node_sync.js +2 -2
- package/dist/codex_app/client.d.ts +1 -0
- package/dist/codex_app/client.js +35 -4
- package/dist/codex_app/force_takeover.d.ts +12 -0
- package/dist/codex_app/force_takeover.js +27 -0
- package/dist/controller/controller.d.ts +7 -0
- package/dist/controller/controller.js +214 -12
- package/dist/i18n.d.ts +30 -6
- package/dist/i18n.js +32 -6
- package/dist/main.js +44 -8
- package/dist/telegram/api.js +4 -0
- package/dist/telegram/bot_home.d.ts +3 -0
- package/dist/telegram/bot_home.js +107 -0
- package/dist/telegram/gateway.d.ts +1 -0
- package/dist/telegram/gateway.js +10 -0
- package/dist/voice/target.js +2 -1
- package/docs/user-manual.md +11 -1
- package/docs/zh/2026-09-05-bot-home-migration.md +26 -0
- package/docs/zh/2026-09-05-reliability-review.md +53 -0
- package/docs/zh/troubleshooting.md +12 -0
- package/docs/zh/user-manual.md +13 -1
- package/package.json +1 -1
- package/scripts/force-takeover.py +118 -0
- package/scripts/force-takeover.test.py +122 -0
package/dist/i18n.d.ts
CHANGED
|
@@ -52,6 +52,8 @@ declare const MESSAGES: {
|
|
|
52
52
|
readonly cmd_desc_account: "Codex account";
|
|
53
53
|
readonly cmd_desc_quota: "Codex quota";
|
|
54
54
|
readonly cmd_desc_login_device: "ChatGPT device login";
|
|
55
|
+
readonly cmd_desc_login_cancel: "Cancel the pending login";
|
|
56
|
+
readonly cmd_desc_cli: "Continue this session in the local CLI";
|
|
55
57
|
readonly cmd_desc_threads: "Recent threads";
|
|
56
58
|
readonly cmd_desc_open: "Open a cached thread";
|
|
57
59
|
readonly cmd_desc_goal: "Thread goal";
|
|
@@ -246,13 +248,13 @@ declare const MESSAGES: {
|
|
|
246
248
|
readonly auth_sync_test_sent: "Auth sync test complete: sent {sent}, replies {replied}.";
|
|
247
249
|
readonly auth_sync_test_missing: "Missing replies: {value}";
|
|
248
250
|
readonly auth_sync_push_blocked_active: "Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.";
|
|
249
|
-
readonly auth_sync_safe_starting: "Auditing every node
|
|
251
|
+
readonly auth_sync_safe_starting: "Auditing every node and distributing valid auth. An unreachable peer can add up to 5 minutes per audit phase. Use /auth sync status to check progress.";
|
|
250
252
|
readonly auth_sync_safe_done: "Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.";
|
|
251
253
|
readonly auth_cluster_audit_starting: "Asking all auth-sync peers to validate accounts, reconcile the newest usable credentials, and refresh credentials at least 8 days old...";
|
|
252
254
|
readonly auth_cluster_audit_blocked_active: "Cluster auth check requires every local runtime, approval, input, login, and auth mirror write to be idle.";
|
|
253
255
|
readonly auth_cluster_audit_failed: "Cluster auth check failed: {error}";
|
|
254
256
|
readonly auth_cluster_audit_summary: "Cluster auth check: nodes {responded}/{expected}; accounts checked {checked}, usable {valid}, unusable {invalid}.";
|
|
255
|
-
readonly auth_cluster_audit_synced: "
|
|
257
|
+
readonly auth_cluster_audit_synced: "Valid auth selected and sent (remote import not confirmed): {value}";
|
|
256
258
|
readonly auth_cluster_audit_repair: "Marked for manual repair after multi-node consensus: {value}";
|
|
257
259
|
readonly auth_cluster_audit_missing: "No response: {value}";
|
|
258
260
|
readonly auth_cluster_audit_busy: "Busy nodes: {value}";
|
|
@@ -301,7 +303,17 @@ declare const MESSAGES: {
|
|
|
301
303
|
readonly working: "Working...";
|
|
302
304
|
readonly usage_open: "Usage: /open <n>";
|
|
303
305
|
readonly usage_watch: "Usage: /watch [n]";
|
|
304
|
-
readonly usage_takeover: "Usage: /takeover <message>";
|
|
306
|
+
readonly usage_takeover: "Usage: /takeover <message> or /takeover --force <message> (confirm stopping a local CLI)";
|
|
307
|
+
readonly force_takeover_trusted_telegram_only: "Force takeover is only available to the configured trusted Telegram user.";
|
|
308
|
+
readonly force_takeover_confirm: "Force takeover of thread {threadId}?\nLocal Codex CLI PID: {pid}\nWorking directory: {cwd}\nNext prompt: {prompt}\nThis will terminate that CLI (SIGTERM, then SIGKILL after 5 seconds if needed). Unfinished work may be interrupted; child commands may continue running. No edits are rolled back. Confirmation expires in 60 seconds.";
|
|
309
|
+
readonly button_force_takeover: "Confirm force takeover";
|
|
310
|
+
readonly force_takeover_busy: "Force takeover is in progress. Please wait before switching threads.";
|
|
311
|
+
readonly force_takeover_hint: "Use /watch then /queue <message> to queue for that CLI. To stop a local CLI and hand the thread to this bridge, use /takeover --force <message> and confirm.";
|
|
312
|
+
readonly force_takeover_expired: "Confirmation expired or thread changed. Send /takeover --force <message> again.";
|
|
313
|
+
readonly force_takeover_cancelled: "Force takeover cancelled. Nothing was stopped or submitted.";
|
|
314
|
+
readonly force_takeover_stopping: "Stopping the confirmed CLI PID {pid} and checking its writer lock...";
|
|
315
|
+
readonly force_takeover_failed: "Force takeover not completed; no new prompt was sent. If stopping already began, the CLI may have exited. Check /status before retrying. Reason: {error}";
|
|
316
|
+
readonly force_takeover_acquired: "Bridge acquired thread {threadId}. Preparing the requested prompt; this is not task completion.";
|
|
305
317
|
readonly usage_queue: "Usage: /queue <message>";
|
|
306
318
|
readonly unknown_cached_thread: "Unknown cached thread. Run /threads first.";
|
|
307
319
|
readonly cached_thread_unavailable: "That cached thread is no longer available. Run /threads again.";
|
|
@@ -780,6 +792,8 @@ declare const MESSAGES: {
|
|
|
780
792
|
readonly cmd_desc_account: "Codex 账号";
|
|
781
793
|
readonly cmd_desc_quota: "Codex 用量";
|
|
782
794
|
readonly cmd_desc_login_device: "ChatGPT 设备登录";
|
|
795
|
+
readonly cmd_desc_login_cancel: "取消当前登录流程";
|
|
796
|
+
readonly cmd_desc_cli: "从本机 CLI 继续当前会话";
|
|
783
797
|
readonly cmd_desc_threads: "最近线程";
|
|
784
798
|
readonly cmd_desc_open: "打开缓存线程";
|
|
785
799
|
readonly cmd_desc_goal: "线程目标";
|
|
@@ -974,13 +988,13 @@ declare const MESSAGES: {
|
|
|
974
988
|
readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
|
|
975
989
|
readonly auth_sync_test_missing: "未回应:{value}";
|
|
976
990
|
readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
|
|
977
|
-
readonly auth_sync_safe_starting: "
|
|
991
|
+
readonly auth_sync_safe_starting: "正在自检各节点并分发有效授权。节点未回应时,每轮自检最多等待 5 分钟;可用 /auth sync status 查看进展。";
|
|
978
992
|
readonly auth_sync_safe_done: "安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。";
|
|
979
993
|
readonly auth_cluster_audit_starting: "正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...";
|
|
980
994
|
readonly auth_cluster_audit_blocked_active: "集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。";
|
|
981
995
|
readonly auth_cluster_audit_failed: "集群 auth 自检失败:{error}";
|
|
982
996
|
readonly auth_cluster_audit_summary: "集群 auth 自检:节点 {responded}/{expected};检查账号 {checked},有效 {valid},无有效副本 {invalid}。";
|
|
983
|
-
readonly auth_cluster_audit_synced: "
|
|
997
|
+
readonly auth_cluster_audit_synced: "已选出并发送有效授权(远端导入尚未确认):{value}";
|
|
984
998
|
readonly auth_cluster_audit_repair: "多节点确认无效,已标记问号等待人工处理:{value}";
|
|
985
999
|
readonly auth_cluster_audit_missing: "未回应节点:{value}";
|
|
986
1000
|
readonly auth_cluster_audit_busy: "忙碌节点:{value}";
|
|
@@ -1029,7 +1043,17 @@ declare const MESSAGES: {
|
|
|
1029
1043
|
readonly working: "处理中...";
|
|
1030
1044
|
readonly usage_open: "用法:/open <编号>";
|
|
1031
1045
|
readonly usage_watch: "用法:/watch [编号]";
|
|
1032
|
-
readonly usage_takeover: "用法:/takeover
|
|
1046
|
+
readonly usage_takeover: "用法:/takeover <消息>;或 /takeover --force <消息>(确认后停止本机 CLI)";
|
|
1047
|
+
readonly force_takeover_trusted_telegram_only: "强制接管仅对配置中的可信 Telegram 用户开放。";
|
|
1048
|
+
readonly force_takeover_confirm: "强制接管 thread {threadId}?\n本机 Codex CLI PID:{pid}\n工作目录:{cwd}\n接管后消息:{prompt}\n将终止这个 CLI(先 SIGTERM,5 秒后仍未退出则 SIGKILL)。未完成任务可能被中断,已启动的子命令可能继续运行;不会回滚文件修改。确认 60 秒内有效。";
|
|
1049
|
+
readonly button_force_takeover: "确认强制接管";
|
|
1050
|
+
readonly force_takeover_busy: "正在强制接管,请等待结果后再切换会话。";
|
|
1051
|
+
readonly force_takeover_hint: "可先 /watch 再 /queue <消息> 排队给原 CLI;如需停止本机 CLI、让桥接管,请发送 /takeover --force <消息> 并确认。";
|
|
1052
|
+
readonly force_takeover_expired: "确认已过期或 thread 已切换,请重新发送 /takeover --force <消息>。";
|
|
1053
|
+
readonly force_takeover_cancelled: "已取消强制接管,没有停止进程或发送新任务。";
|
|
1054
|
+
readonly force_takeover_stopping: "正在停止已确认的 CLI PID {pid},并检查写入锁是否释放…";
|
|
1055
|
+
readonly force_takeover_failed: "强制接管未完成,没有发送新任务。如果已开始停止,CLI 可能已经退出。请先 /status 检查再重试。原因:{error}";
|
|
1056
|
+
readonly force_takeover_acquired: "桥已取得 thread {threadId} 的写入权,准备发送指定消息;这不代表任务已完成。";
|
|
1033
1057
|
readonly usage_queue: "用法:/queue <消息>";
|
|
1034
1058
|
readonly unknown_cached_thread: "找不到这个缓存线程,请先运行 /threads。";
|
|
1035
1059
|
readonly cached_thread_unavailable: "这个缓存线程已经不可用,请重新运行 /threads。";
|
package/dist/i18n.js
CHANGED
|
@@ -50,6 +50,8 @@ const MESSAGES = {
|
|
|
50
50
|
cmd_desc_account: 'Codex account',
|
|
51
51
|
cmd_desc_quota: 'Codex quota',
|
|
52
52
|
cmd_desc_login_device: 'ChatGPT device login',
|
|
53
|
+
cmd_desc_login_cancel: 'Cancel the pending login',
|
|
54
|
+
cmd_desc_cli: 'Continue this session in the local CLI',
|
|
53
55
|
cmd_desc_threads: 'Recent threads',
|
|
54
56
|
cmd_desc_open: 'Open a cached thread',
|
|
55
57
|
cmd_desc_goal: 'Thread goal',
|
|
@@ -244,13 +246,13 @@ const MESSAGES = {
|
|
|
244
246
|
auth_sync_test_sent: 'Auth sync test complete: sent {sent}, replies {replied}.',
|
|
245
247
|
auth_sync_test_missing: 'Missing replies: {value}',
|
|
246
248
|
auth_sync_push_blocked_active: 'Cannot push auth sync while any runtime, approval, input, login, or auth mirror write is active.',
|
|
247
|
-
auth_sync_safe_starting: 'Auditing every node
|
|
249
|
+
auth_sync_safe_starting: 'Auditing every node and distributing valid auth. An unreachable peer can add up to 5 minutes per audit phase. Use /auth sync status to check progress.',
|
|
248
250
|
auth_sync_safe_done: 'Safe auth sync complete: local synced {localSynced}, local skipped {localSkipped}; cross-node sent {sent}, skipped {skipped}.',
|
|
249
251
|
auth_cluster_audit_starting: 'Asking all auth-sync peers to validate accounts, reconcile the newest usable credentials, and refresh credentials at least 8 days old...',
|
|
250
252
|
auth_cluster_audit_blocked_active: 'Cluster auth check requires every local runtime, approval, input, login, and auth mirror write to be idle.',
|
|
251
253
|
auth_cluster_audit_failed: 'Cluster auth check failed: {error}',
|
|
252
254
|
auth_cluster_audit_summary: 'Cluster auth check: nodes {responded}/{expected}; accounts checked {checked}, usable {valid}, unusable {invalid}.',
|
|
253
|
-
auth_cluster_audit_synced: '
|
|
255
|
+
auth_cluster_audit_synced: 'Valid auth selected and sent (remote import not confirmed): {value}',
|
|
254
256
|
auth_cluster_audit_repair: 'Marked for manual repair after multi-node consensus: {value}',
|
|
255
257
|
auth_cluster_audit_missing: 'No response: {value}',
|
|
256
258
|
auth_cluster_audit_busy: 'Busy nodes: {value}',
|
|
@@ -299,7 +301,17 @@ const MESSAGES = {
|
|
|
299
301
|
working: 'Working...',
|
|
300
302
|
usage_open: 'Usage: /open <n>',
|
|
301
303
|
usage_watch: 'Usage: /watch [n]',
|
|
302
|
-
usage_takeover: 'Usage: /takeover <message>',
|
|
304
|
+
usage_takeover: 'Usage: /takeover <message> or /takeover --force <message> (confirm stopping a local CLI)',
|
|
305
|
+
force_takeover_trusted_telegram_only: 'Force takeover is only available to the configured trusted Telegram user.',
|
|
306
|
+
force_takeover_confirm: 'Force takeover of thread {threadId}?\nLocal Codex CLI PID: {pid}\nWorking directory: {cwd}\nNext prompt: {prompt}\nThis will terminate that CLI (SIGTERM, then SIGKILL after 5 seconds if needed). Unfinished work may be interrupted; child commands may continue running. No edits are rolled back. Confirmation expires in 60 seconds.',
|
|
307
|
+
button_force_takeover: 'Confirm force takeover',
|
|
308
|
+
force_takeover_busy: 'Force takeover is in progress. Please wait before switching threads.',
|
|
309
|
+
force_takeover_hint: 'Use /watch then /queue <message> to queue for that CLI. To stop a local CLI and hand the thread to this bridge, use /takeover --force <message> and confirm.',
|
|
310
|
+
force_takeover_expired: 'Confirmation expired or thread changed. Send /takeover --force <message> again.',
|
|
311
|
+
force_takeover_cancelled: 'Force takeover cancelled. Nothing was stopped or submitted.',
|
|
312
|
+
force_takeover_stopping: 'Stopping the confirmed CLI PID {pid} and checking its writer lock...',
|
|
313
|
+
force_takeover_failed: 'Force takeover not completed; no new prompt was sent. If stopping already began, the CLI may have exited. Check /status before retrying. Reason: {error}',
|
|
314
|
+
force_takeover_acquired: 'Bridge acquired thread {threadId}. Preparing the requested prompt; this is not task completion.',
|
|
303
315
|
usage_queue: 'Usage: /queue <message>',
|
|
304
316
|
unknown_cached_thread: 'Unknown cached thread. Run /threads first.',
|
|
305
317
|
cached_thread_unavailable: 'That cached thread is no longer available. Run /threads again.',
|
|
@@ -778,6 +790,8 @@ const MESSAGES = {
|
|
|
778
790
|
cmd_desc_account: 'Codex 账号',
|
|
779
791
|
cmd_desc_quota: 'Codex 用量',
|
|
780
792
|
cmd_desc_login_device: 'ChatGPT 设备登录',
|
|
793
|
+
cmd_desc_login_cancel: '取消当前登录流程',
|
|
794
|
+
cmd_desc_cli: '从本机 CLI 继续当前会话',
|
|
781
795
|
cmd_desc_threads: '最近线程',
|
|
782
796
|
cmd_desc_open: '打开缓存线程',
|
|
783
797
|
cmd_desc_goal: '线程目标',
|
|
@@ -972,13 +986,13 @@ const MESSAGES = {
|
|
|
972
986
|
auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
|
|
973
987
|
auth_sync_test_missing: '未回应:{value}',
|
|
974
988
|
auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
|
|
975
|
-
auth_sync_safe_starting: '
|
|
989
|
+
auth_sync_safe_starting: '正在自检各节点并分发有效授权。节点未回应时,每轮自检最多等待 5 分钟;可用 /auth sync status 查看进展。',
|
|
976
990
|
auth_sync_safe_done: '安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。',
|
|
977
991
|
auth_cluster_audit_starting: '正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...',
|
|
978
992
|
auth_cluster_audit_blocked_active: '集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。',
|
|
979
993
|
auth_cluster_audit_failed: '集群 auth 自检失败:{error}',
|
|
980
994
|
auth_cluster_audit_summary: '集群 auth 自检:节点 {responded}/{expected};检查账号 {checked},有效 {valid},无有效副本 {invalid}。',
|
|
981
|
-
auth_cluster_audit_synced: '
|
|
995
|
+
auth_cluster_audit_synced: '已选出并发送有效授权(远端导入尚未确认):{value}',
|
|
982
996
|
auth_cluster_audit_repair: '多节点确认无效,已标记问号等待人工处理:{value}',
|
|
983
997
|
auth_cluster_audit_missing: '未回应节点:{value}',
|
|
984
998
|
auth_cluster_audit_busy: '忙碌节点:{value}',
|
|
@@ -1027,7 +1041,17 @@ const MESSAGES = {
|
|
|
1027
1041
|
working: '处理中...',
|
|
1028
1042
|
usage_open: '用法:/open <编号>',
|
|
1029
1043
|
usage_watch: '用法:/watch [编号]',
|
|
1030
|
-
usage_takeover: '用法:/takeover
|
|
1044
|
+
usage_takeover: '用法:/takeover <消息>;或 /takeover --force <消息>(确认后停止本机 CLI)',
|
|
1045
|
+
force_takeover_trusted_telegram_only: '强制接管仅对配置中的可信 Telegram 用户开放。',
|
|
1046
|
+
force_takeover_confirm: '强制接管 thread {threadId}?\n本机 Codex CLI PID:{pid}\n工作目录:{cwd}\n接管后消息:{prompt}\n将终止这个 CLI(先 SIGTERM,5 秒后仍未退出则 SIGKILL)。未完成任务可能被中断,已启动的子命令可能继续运行;不会回滚文件修改。确认 60 秒内有效。',
|
|
1047
|
+
button_force_takeover: '确认强制接管',
|
|
1048
|
+
force_takeover_busy: '正在强制接管,请等待结果后再切换会话。',
|
|
1049
|
+
force_takeover_hint: '可先 /watch 再 /queue <消息> 排队给原 CLI;如需停止本机 CLI、让桥接管,请发送 /takeover --force <消息> 并确认。',
|
|
1050
|
+
force_takeover_expired: '确认已过期或 thread 已切换,请重新发送 /takeover --force <消息>。',
|
|
1051
|
+
force_takeover_cancelled: '已取消强制接管,没有停止进程或发送新任务。',
|
|
1052
|
+
force_takeover_stopping: '正在停止已确认的 CLI PID {pid},并检查写入锁是否释放…',
|
|
1053
|
+
force_takeover_failed: '强制接管未完成,没有发送新任务。如果已开始停止,CLI 可能已经退出。请先 /status 检查再重试。原因:{error}',
|
|
1054
|
+
force_takeover_acquired: '桥已取得 thread {threadId} 的写入权,准备发送指定消息;这不代表任务已完成。',
|
|
1031
1055
|
usage_queue: '用法:/queue <消息>',
|
|
1032
1056
|
unknown_cached_thread: '找不到这个缓存线程,请先运行 /threads。',
|
|
1033
1057
|
cached_thread_unavailable: '这个缓存线程已经不可用,请重新运行 /threads。',
|
|
@@ -1479,6 +1503,8 @@ export function getTelegramCommands(locale) {
|
|
|
1479
1503
|
{ command: 'account', description: t(locale, 'cmd_desc_account') },
|
|
1480
1504
|
{ command: 'quota', description: t(locale, 'cmd_desc_quota') },
|
|
1481
1505
|
{ command: 'login_device', description: t(locale, 'cmd_desc_login_device') },
|
|
1506
|
+
{ command: 'login_cancel', description: t(locale, 'cmd_desc_login_cancel') },
|
|
1507
|
+
{ command: 'cli', description: t(locale, 'cmd_desc_cli') },
|
|
1482
1508
|
{ command: 'open', description: t(locale, 'cmd_desc_open') },
|
|
1483
1509
|
{ command: 'goal', description: t(locale, 'cmd_desc_goal') },
|
|
1484
1510
|
{ command: 'history', description: t(locale, 'cmd_desc_history') },
|
package/dist/main.js
CHANGED
|
@@ -18,6 +18,7 @@ import { clearPendingClusterUpdateBroadcast, createSelfUpdateRuntime, inferPnpmH
|
|
|
18
18
|
import { TELEGRAM_VOICE_MAX_BYTES, TELEGRAM_VOICE_SUPPORTED_EXTENSIONS, telegramVoiceContentType, } from './voice/files.js';
|
|
19
19
|
import { inferTelegramBotId, resolveTelegramVoiceTarget } from './voice/target.js';
|
|
20
20
|
import { shouldShowRuntimeLastUpdate } from './update_status.js';
|
|
21
|
+
import { prepareTelegramBotHome } from './telegram/bot_home.js';
|
|
21
22
|
const rawCommand = process.argv[2];
|
|
22
23
|
const command = rawCommand || 'serve';
|
|
23
24
|
loadEnv();
|
|
@@ -225,6 +226,40 @@ async function main() {
|
|
|
225
226
|
}
|
|
226
227
|
return;
|
|
227
228
|
}
|
|
229
|
+
if (command === 'resume') {
|
|
230
|
+
const args = process.argv.slice(3);
|
|
231
|
+
const botIndex = args.indexOf('--bot-id');
|
|
232
|
+
const botId = botIndex >= 0 ? args[botIndex + 1] : null;
|
|
233
|
+
if (botIndex >= 0)
|
|
234
|
+
args.splice(botIndex, 2);
|
|
235
|
+
if ((botIndex >= 0 && !botId) || args.length > 1 || args[0]?.startsWith('-')) {
|
|
236
|
+
throw new Error('Usage: foxclaw resume [thread-id] [--bot-id <bot-id>]');
|
|
237
|
+
}
|
|
238
|
+
const status = readRuntimeStatus(process.env.STATUS_PATH || DEFAULT_STATUS_PATH);
|
|
239
|
+
const runtime = botId ? status?.bots?.find(bot => bot.id === botId) : status;
|
|
240
|
+
if (!runtime)
|
|
241
|
+
throw new Error('No matching runtime status. Check foxclaw status.');
|
|
242
|
+
if (!botId && new Set(status?.bots?.map(bot => bot.codexAppServer?.port).filter(Boolean)).size > 1) {
|
|
243
|
+
throw new Error('Multiple Codex servers found. Select one with --bot-id from foxclaw status --json.');
|
|
244
|
+
}
|
|
245
|
+
const server = runtime.codexAppServer;
|
|
246
|
+
if (!server?.running || !server.pid || !server.port) {
|
|
247
|
+
throw new Error('Codex app-server is not running. Check foxclaw status.');
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
process.kill(server.pid, 0);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
throw new Error('Codex app-server status is stale; the process has exited.');
|
|
254
|
+
}
|
|
255
|
+
const result = spawnSync(process.env.CODEX_CLI_BIN || 'codex', [
|
|
256
|
+
'resume', '--remote', `ws://127.0.0.1:${server.port}`, ...args,
|
|
257
|
+
], { stdio: 'inherit' });
|
|
258
|
+
if (result.error)
|
|
259
|
+
throw result.error;
|
|
260
|
+
process.exitCode = result.status ?? 1;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
228
263
|
if (command === 'doctor') {
|
|
229
264
|
const failed = !runDoctorChecks();
|
|
230
265
|
process.exit(failed ? 1 : 0);
|
|
@@ -278,6 +313,7 @@ Usage:
|
|
|
278
313
|
foxclaw init
|
|
279
314
|
foxclaw doctor
|
|
280
315
|
foxclaw status
|
|
316
|
+
foxclaw resume [thread-id] [--bot-id <bot-id>]
|
|
281
317
|
foxclaw start|restart|stop
|
|
282
318
|
foxclaw update
|
|
283
319
|
foxclaw send-voice <path> [caption]
|
|
@@ -593,16 +629,18 @@ async function runServeCli() {
|
|
|
593
629
|
if (config.tgMultiBotMode) {
|
|
594
630
|
const seeds = [];
|
|
595
631
|
const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
596
|
-
|
|
632
|
+
const identities = await Promise.all(config.tgBotTokens.map(async (token) => {
|
|
597
633
|
const bot = new TelegramGateway(token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, true);
|
|
598
634
|
const id = await bot.initializeIdentity();
|
|
635
|
+
const username = await bot.resolveUsername();
|
|
636
|
+
return { token, bot, id, username };
|
|
637
|
+
}));
|
|
638
|
+
for (const { token, bot, id, username } of identities) {
|
|
599
639
|
if (seeds.some((runtime) => runtime.id === id)) {
|
|
600
640
|
throw new Error(`TG_BOT_TOKENS contains duplicate Telegram bot identity: ${id}`);
|
|
601
641
|
}
|
|
602
642
|
const sharedDefaultRuntime = config.tgDefaultRuntimeBotToken === token;
|
|
603
|
-
const home = sharedDefaultRuntime
|
|
604
|
-
? (config.codexHome ?? path.join(os.homedir(), '.codex'))
|
|
605
|
-
: path.join(DEFAULT_CODEX_TELEGRAM_HOME, id, 'home');
|
|
643
|
+
const home = prepareTelegramBotHome(DEFAULT_CODEX_TELEGRAM_HOME, id, username, sharedDefaultRuntime ? (config.codexHome ?? path.join(os.homedir(), '.codex')) : null);
|
|
606
644
|
const authDir = sharedDefaultRuntime ? canonicalAuthDir : home;
|
|
607
645
|
if (!sharedDefaultRuntime) {
|
|
608
646
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
@@ -614,7 +652,7 @@ async function runServeCli() {
|
|
|
614
652
|
tgBotTokens: [token],
|
|
615
653
|
tgScopeBotId: id,
|
|
616
654
|
codexAuthDir: sharedDefaultRuntime ? config.codexAuthDir : home,
|
|
617
|
-
codexHome:
|
|
655
|
+
codexHome: home,
|
|
618
656
|
codexAppAutolaunch: sharedDefaultRuntime ? config.codexAppAutolaunch : false,
|
|
619
657
|
codexAppServerStatePath: sharedDefaultRuntime
|
|
620
658
|
? config.codexAppServerStatePath
|
|
@@ -623,9 +661,7 @@ async function runServeCli() {
|
|
|
623
661
|
? config.codexAppServerLogPath
|
|
624
662
|
: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`),
|
|
625
663
|
};
|
|
626
|
-
const childEnv =
|
|
627
|
-
? (config.codexHome ? { CODEX_HOME: config.codexHome } : null)
|
|
628
|
-
: { CODEX_HOME: home };
|
|
664
|
+
const childEnv = { CODEX_HOME: home };
|
|
629
665
|
const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, [
|
|
630
666
|
...codexApiProviderOverrides,
|
|
631
667
|
...(sharedDefaultRuntime ? [] : ['cli_auth_credentials_store="file"']),
|
package/dist/telegram/api.js
CHANGED
|
@@ -14,7 +14,9 @@ export async function callTelegramApi(botToken, method, body) {
|
|
|
14
14
|
'content-type': 'application/json',
|
|
15
15
|
'content-length': Buffer.byteLength(payload),
|
|
16
16
|
},
|
|
17
|
+
signal: AbortSignal.timeout(telegramApiTimeoutMs(false)),
|
|
17
18
|
}, (response) => {
|
|
19
|
+
response.on('error', reject);
|
|
18
20
|
const chunks = [];
|
|
19
21
|
response.on('data', (chunk) => {
|
|
20
22
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
@@ -58,7 +60,9 @@ export async function callTelegramMultipartApi(botToken, method, fields, files)
|
|
|
58
60
|
'content-type': `multipart/form-data; boundary=${boundary}`,
|
|
59
61
|
'content-length': payload.length,
|
|
60
62
|
},
|
|
63
|
+
signal: AbortSignal.timeout(telegramApiTimeoutMs(true)),
|
|
61
64
|
}, (response) => {
|
|
65
|
+
response.on('error', reject);
|
|
62
66
|
const chunks = [];
|
|
63
67
|
response.on('data', (chunk) => {
|
|
64
68
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Keep storage names readable while bot IDs remain the stable routing identity. */
|
|
2
|
+
export declare function prepareTelegramBotHome(baseDir: string, botId: string, username: string | null, sharedHome?: string | null): string;
|
|
3
|
+
export declare function readTelegramBotHomeIdentity(codexHome: string): string | null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const ID_FILE = '.foxclaw-bot.json';
|
|
4
|
+
/** Keep storage names readable while bot IDs remain the stable routing identity. */
|
|
5
|
+
export function prepareTelegramBotHome(baseDir, botId, username, sharedHome = null) {
|
|
6
|
+
if (!/^bot\d+$/.test(botId))
|
|
7
|
+
throw new Error('Invalid Telegram bot identity');
|
|
8
|
+
if (username !== null && !/^[A-Za-z0-9_]{1,64}$/.test(username))
|
|
9
|
+
throw new Error('Invalid Telegram username for directory');
|
|
10
|
+
const base = path.resolve(baseDir);
|
|
11
|
+
fs.mkdirSync(base, { recursive: true, mode: 0o700 });
|
|
12
|
+
const legacy = path.join(base, botId);
|
|
13
|
+
const legacyStat = lstat(legacy);
|
|
14
|
+
const source = legacyStat?.isSymbolicLink() ? fs.realpathSync(legacy) : legacy;
|
|
15
|
+
if (path.dirname(source) !== base)
|
|
16
|
+
throw new Error(`Bot directory points outside its storage root: ${legacy}`);
|
|
17
|
+
const target = username ? path.join(base, `@${username}`) : source;
|
|
18
|
+
const sourceStat = lstat(source);
|
|
19
|
+
const targetStat = lstat(target);
|
|
20
|
+
if (sourceStat && !sourceStat.isDirectory())
|
|
21
|
+
throw new Error(`Bot storage is not a directory: ${source}`);
|
|
22
|
+
if (targetStat && (targetStat.isSymbolicLink() || !targetStat.isDirectory())) {
|
|
23
|
+
throw new Error(`Bot directory name already occupied: ${target}`);
|
|
24
|
+
}
|
|
25
|
+
if (sourceStat)
|
|
26
|
+
assertOwner(source, botId, source === legacy);
|
|
27
|
+
if (targetStat && target !== source) {
|
|
28
|
+
assertOwner(target, botId, false);
|
|
29
|
+
if (sourceStat)
|
|
30
|
+
throw new Error(`Both old and named bot directories exist; refusing to merge: ${source}, ${target}`);
|
|
31
|
+
}
|
|
32
|
+
if (sourceStat && source !== target) {
|
|
33
|
+
writeOwner(source, botId);
|
|
34
|
+
fs.renameSync(source, target);
|
|
35
|
+
// Keep absolute auth links and historical paths working after the move.
|
|
36
|
+
linkDirectory(target, source);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
40
|
+
}
|
|
41
|
+
writeOwner(target, botId);
|
|
42
|
+
if (legacy !== target && (!legacyStat || legacyStat.isSymbolicLink())) {
|
|
43
|
+
linkDirectory(target, legacy);
|
|
44
|
+
}
|
|
45
|
+
const home = path.join(target, 'home');
|
|
46
|
+
if (sharedHome) {
|
|
47
|
+
const shared = path.resolve(sharedHome);
|
|
48
|
+
if (shared === home || shared.startsWith(`${target}${path.sep}`))
|
|
49
|
+
throw new Error('Shared Codex home would create a directory cycle');
|
|
50
|
+
fs.mkdirSync(shared, { recursive: true, mode: 0o700 });
|
|
51
|
+
if (lstat(home)) {
|
|
52
|
+
if (!fs.lstatSync(home).isSymbolicLink() || fs.realpathSync(home) !== fs.realpathSync(shared)) {
|
|
53
|
+
throw new Error(`Existing bot home differs from the configured shared home: ${home}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
linkDirectory(shared, home);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
if (lstat(home)?.isSymbolicLink())
|
|
62
|
+
throw new Error(`Isolated bot home unexpectedly shares another directory: ${home}`);
|
|
63
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
64
|
+
}
|
|
65
|
+
return home;
|
|
66
|
+
}
|
|
67
|
+
export function readTelegramBotHomeIdentity(codexHome) {
|
|
68
|
+
try {
|
|
69
|
+
const value = JSON.parse(fs.readFileSync(path.join(path.dirname(codexHome), ID_FILE), 'utf8'));
|
|
70
|
+
return typeof value.botId === 'string' && /^bot\d+$/.test(value.botId) ? value.botId : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function assertOwner(directory, botId, allowLegacy) {
|
|
77
|
+
const marker = path.join(directory, ID_FILE);
|
|
78
|
+
if (allowLegacy && !lstat(marker))
|
|
79
|
+
return;
|
|
80
|
+
if (readTelegramBotHomeIdentity(path.join(directory, 'home')) !== botId) {
|
|
81
|
+
throw new Error(`Bot directory belongs to another identity or has no identity record: ${directory}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function writeOwner(directory, botId) {
|
|
85
|
+
fs.writeFileSync(path.join(directory, ID_FILE), `${JSON.stringify({ botId })}\n`, { mode: 0o600 });
|
|
86
|
+
}
|
|
87
|
+
function linkDirectory(target, link) {
|
|
88
|
+
const temp = `${link}.link-${process.pid}`;
|
|
89
|
+
fs.symlinkSync(target, temp, process.platform === 'win32' ? 'junction' : 'dir');
|
|
90
|
+
try {
|
|
91
|
+
fs.renameSync(temp, link);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (lstat(temp))
|
|
95
|
+
fs.unlinkSync(temp);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function lstat(filename) {
|
|
99
|
+
try {
|
|
100
|
+
return fs.lstatSync(filename);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code === 'ENOENT')
|
|
104
|
+
return null;
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -58,6 +58,7 @@ export declare class TelegramGateway extends EventEmitter {
|
|
|
58
58
|
get username(): string | null;
|
|
59
59
|
get identity(): string | null;
|
|
60
60
|
initializeIdentity(): Promise<string>;
|
|
61
|
+
resolveUsername(): Promise<string | null>;
|
|
61
62
|
start(): Promise<void>;
|
|
62
63
|
stop(): void;
|
|
63
64
|
sendMessage(chatId: string, text: string, inlineKeyboard?: Array<Array<{
|
package/dist/telegram/gateway.js
CHANGED
|
@@ -44,6 +44,16 @@ export class TelegramGateway extends EventEmitter {
|
|
|
44
44
|
await this.resolveBotIdentity(true);
|
|
45
45
|
return this.identity;
|
|
46
46
|
}
|
|
47
|
+
async resolveUsername() {
|
|
48
|
+
try {
|
|
49
|
+
await this.resolveBotIdentity(true);
|
|
50
|
+
return this.username;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
this.logger.warn('telegram.username_unavailable', { botId: this.identity });
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
47
57
|
async start() {
|
|
48
58
|
if (this.running)
|
|
49
59
|
return;
|
package/dist/voice/target.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { readTelegramBotHomeIdentity } from '../telegram/bot_home.js';
|
|
1
2
|
export function inferTelegramBotId(codexHome) {
|
|
2
3
|
if (!codexHome)
|
|
3
4
|
return null;
|
|
4
5
|
const match = codexHome.match(/(?:^|[\\/])(bot\d+)(?:[\\/]|$)/i);
|
|
5
|
-
return match?.[1]?.toLowerCase() ??
|
|
6
|
+
return match?.[1]?.toLowerCase() ?? readTelegramBotHomeIdentity(codexHome);
|
|
6
7
|
}
|
|
7
8
|
export function resolveTelegramVoiceTarget(tokens, requestedBotId) {
|
|
8
9
|
if (requestedBotId) {
|
package/docs/user-manual.md
CHANGED
|
@@ -357,6 +357,14 @@ Watch mode mirrors live turn progress and approval requests. It still does not t
|
|
|
357
357
|
|
|
358
358
|
Older Codex versions do not expose this queue API. FoxClaw reports the required upgrade instead of falling back to writer takeover or session-file mutation. Use `/unwatch` first only when you want Telegram to start a separate turn itself.
|
|
359
359
|
|
|
360
|
+
### `/takeover --force <message>`: hand off a local CLI writer
|
|
361
|
+
|
|
362
|
+
For `already has an active writer`, send `/takeover --force continue the task`, verify the thread, PID and working directory, then confirm or cancel. Confirmation is scoped to the requesting Telegram user/chat and expires after 60 seconds. Ordinary `/watch`, `/queue`, and `/takeover` never automatically stop an external CLI.
|
|
363
|
+
|
|
364
|
+
Requires Linux/WSL, Python 3.9+ and kernel pidfd support. Only an interactive, same-OS-user Codex CLI holding the target bot home's thread lock is eligible. Servers, remote clients, bridge ancestors and processes holding other thread locks are refused. After revalidating process/lock identity, the helper sends SIGTERM via pidfd, escalating to SIGKILL after 5 seconds. Only after verifying lock release does the bridge resume the original thread and submit the prompt. It never deletes locks, edits session files, or retries submissions automatically.
|
|
365
|
+
|
|
366
|
+
Unfinished work can be interrupted, child commands may remain running, and file edits are not rolled back. Identity changes, retained locks and resume failures are reported without submitting a new prompt. The bridge's old pending queue is cancelled only after acquiring the writer; the CLI cross-client queue is not cleared. Multi-thread CLI processes require manual handoff.
|
|
367
|
+
|
|
360
368
|
## 6. Codex Login And Auth Rotation
|
|
361
369
|
|
|
362
370
|
This is a key FoxClaw feature. Codex auth is usually stored at `~/.codex/auth.json`. FoxClaw stores multiple accounts as candidate files and switches which candidate the active `auth.json` points to. In `TG_BOT_TOKENS` mode, each bot has an isolated Codex home, app-server, and current candidate by default, so bots can run and switch accounts independently; isolated Telegram runtimes force file-backed credential storage. Validated login/refresh credentials are safely mirrored between bot homes, but isolated sessions are never shared.
|
|
@@ -365,7 +373,9 @@ To keep one Telegram bot interoperable with terminal Codex sessions, put the sam
|
|
|
365
373
|
|
|
366
374
|
### 6.1 File Format
|
|
367
375
|
|
|
368
|
-
In single-bot compatibility mode, candidate files live in the Codex auth directory, usually `~/.codex/`. If `CODEX_AUTH_DIR` is set, FoxClaw uses that directory. Multi-bot mode
|
|
376
|
+
In single-bot compatibility mode, candidate files live in the Codex auth directory, usually `~/.codex/`. If `CODEX_AUTH_DIR` is set, FoxClaw uses that directory. Multi-bot mode stores isolated homes under `~/.foxclaw/codex/telegram/@TelegramUsername/home/`. Shared-terminal bots get a named home link to their existing Codex home, preserving terminal interoperability.
|
|
377
|
+
|
|
378
|
+
Startup resolves usernames in parallel. Existing numeric directories move to the named location with compatibility links retained; username changes also retain old paths. Offline restarts reuse the previous name. A first offline startup uses `bot<id>` until a later startup can resolve its username. Keep sessions idle during migration. Name collisions fail explicitly without merging or overwriting data. The parent `.foxclaw-bot.json` stores only the stable bot ID for media routing, never a token. Keep it and the compatibility links. Database bindings, logs, and runtime identities still use numeric bot IDs. Project working directories remain controlled by `DEFAULT_CWD` or `/new <path>`.
|
|
369
379
|
|
|
370
380
|
Recommended layout:
|
|
371
381
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Telegram 名称目录迁移验收
|
|
2
|
+
|
|
3
|
+
功能随 `0.7.3` 发布,预览版已安装到 16P 和 T490。本次迁移针对 T490 的 6 个多 bot runtime;16P 单 bot 兼容模式继续使用原默认 home。
|
|
4
|
+
|
|
5
|
+
T490 的实际运行目录如下。
|
|
6
|
+
|
|
7
|
+
| Telegram bot | CODEX_HOME |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| @WuguiAI_Bot | /home/wuya/.foxclaw/codex/telegram/@WuguiAI_Bot/home |
|
|
10
|
+
| @WuguiAI2_Bot | /home/wuya/.foxclaw/codex/telegram/@WuguiAI2_Bot/home |
|
|
11
|
+
| @WuguiAI3_Bot | /home/wuya/.foxclaw/codex/telegram/@WuguiAI3_Bot/home |
|
|
12
|
+
| @WuguiAI4_Bot | /home/wuya/.foxclaw/codex/telegram/@WuguiAI4_Bot/home |
|
|
13
|
+
| @WuguiAI5_Bot | /home/wuya/.foxclaw/codex/telegram/@WuguiAI5_Bot/home |
|
|
14
|
+
| @walma10bot | /home/wuya/.foxclaw/codex/telegram/@walma10bot/home |
|
|
15
|
+
|
|
16
|
+
`@WuguiAI_Bot/home` 链接到 `/home/wuya/.codex-gjzn`,保留原终端共享数据。其他 5 个 bot 的原数字目录已重命名,数字路径作为兼容链接保留。项目工作目录、数据库绑定、日志及运行状态 ID 没有改名。
|
|
17
|
+
|
|
18
|
+
迁移前确认桥内没有活动轮次、审批、输入请求或排队任务;逐个查询真实 app-server 的 `thread/loaded/list`,6 个服务均为空,然后停止桥。迁移程序逐文件计算 SHA-256,并比较目录/文件权限及符号链接目标。5 个独立目录分别校验 351、351、345、345、8420 个条目,合计 9812 个,迁移前后完全一致。共享 home 只验证链接与原目录解析到相同位置,没有搬移共享数据。
|
|
19
|
+
|
|
20
|
+
新版本启动后,status 中的 6 个 home 均为上述名称路径,6 个子进程的真实 `CODEX_HOME` 环境变量也一致;数字旧路径均解析到相同数据。6 个 bot connected=true,systemd active/running、NRestarts=0、ExecMainStatus=0。媒体路由从名称目录身份记录解析到原数字 bot ID,6 个均正确。
|
|
21
|
+
|
|
22
|
+
自动化验证为 410 项全量测试通过,typecheck、lint、build 和 diff check 通过。测试覆盖新目录、原目录迁移、用户名变化、离线启动、共享 home、同名冲突、非法路径、权限/链接保留及媒体目标路由。
|
|
23
|
+
|
|
24
|
+
以后按 `.env.example` 配置 `TG_BOT_TOKENS` 即默认启用名称目录。用户名由 Telegram getMe 获取;已有目录断网时继续使用记录的名称。首次离线启动暂用数字 ID,后续启动获取用户名后再迁移。名称冲突明确报错,不自动合并目录。`.foxclaw-bot.json` 只记录 bot ID,不保存凭据。
|
|
25
|
+
|
|
26
|
+
检查还发现一个原有授权问题:共享 home 中的 `auth.json` 指向 `/home/wuya/.codex/auth.json_GamsGo2`,该目标不存在。原目录本身即可复现断链;此次目录迁移保持该链接不变。桥连接正常不代表这个授权已可调用模型,本次没有为目录改名另行切换账号。
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 2026-09-05 桥可靠性修复与现场验收
|
|
2
|
+
|
|
3
|
+
## 现场结论
|
|
4
|
+
|
|
5
|
+
- 16P 的 Codex 为 0.153.4,查询 npm 得到相同版本。真实 app-server 模型列表包含 gpt-6-astra。T490 当前为 0.153.0,也支持 `codex resume --remote`。
|
|
6
|
+
- 检查时最近两天日志有 75 次 Telegram polling 错误与 29 次 Codex WebSocket 警告。TLS 断连、明确的 `401 token_revoked`、本地连接状态必须分别判断。
|
|
7
|
+
- 旧 RPC 没有截止时间;`/takeover` 无限等待 completion 会一直占用 scope 队列。登录取消先等待 RPC,失败时不清理状态,且完成通知可能抢先触发重复清理。
|
|
8
|
+
- 旧 app-server attach 失败会清除仍存活服务的记录,可能另起服务并留下写入锁。外部观察轮次也可能被重连恢复错误地 resume。
|
|
9
|
+
|
|
10
|
+
## GamsGo2024 同步
|
|
11
|
+
|
|
12
|
+
时间为北京时间,证据来自 16P 与 T490 的 systemd 日志及只读 SQLite 查询。
|
|
13
|
+
|
|
14
|
+
- 12:29:45,16P 启动集群审计,request id 为 `6bce5971-d26f-497f-9dbb-bd906fae86a6`。
|
|
15
|
+
- T490 收到审计后在 12:30:02 返回报告。
|
|
16
|
+
- 12:40:07,T490 的 `auth.mirror.remote_imported` 与 `auth.sync.imported` 明确记录 `auth.json_GamsGo2024` 已导入。
|
|
17
|
+
- 12:40:18 的 “local candidate is already newer or equal” 是后续重复推送被跳过,不能据此推断前一次同步失败。
|
|
18
|
+
- 最终只读查询显示全局和 6 个 runtime 均为 `active`,未禁用。旧 Telegram 面板保留的是候选快照。
|
|
19
|
+
- 16P 原 peer 列表同时含 `@WuguiAI_Bot` 与 `@walma10bot`。两者在 T490 同一桥进程中,统一由第一个 bot 回复,因此后者被视为未回应,导致两轮各 5 分钟的等待。
|
|
20
|
+
- 16P 本地配置已去掉重复的 `@walma10bot` peer,保留 `@WuguiAI_Bot` 和 `@walte2026_bot`。T490 仍沿用 `workstation-GJZN` 节点名,本次未重命名或迁移账号。
|
|
21
|
+
|
|
22
|
+
## 实现
|
|
23
|
+
|
|
24
|
+
- Codex RPC 30 秒超时,清理 pending,不自动重发结果未知的任务;WebSocket 握手有截止时间。
|
|
25
|
+
- Telegram 请求增加总时限与响应流错误处理,避免持续零碎数据或半断连接拖住请求。
|
|
26
|
+
- 活动会话断连与恢复失败有明确提示;保留存活但无法连接的 app-server 记录,重连共用启动锁。
|
|
27
|
+
- `/takeover` 中断确认等待 30 秒后退出,不会延迟启动替换任务。
|
|
28
|
+
- `/status`、`/cli`、`/interrupt`、`/login_cancel` 及只读 auth sync 查询可绕过普通消息队列,仍经过原有权限和目标检查。
|
|
29
|
+
- 设备登录、auth add、auth repair 提供取消按钮;本地状态在等待取消响应前认领清理,旧按钮及其他会话无法取消当前登录。
|
|
30
|
+
- 新增 `foxclaw resume [thread-id] [--bot-id <bot-id>]` 及 Telegram `/cli`,进入桥正在使用的同一 app-server,避免另起 writer。
|
|
31
|
+
- 重连恢复跳过独立 CLI 的只读观察轮次。独立 CLI 的 `/watch` 不增加强抢或删锁行为。
|
|
32
|
+
- 新增显式 `/takeover --force <消息>`。仅可信 Telegram 用户可在 60 秒内确认;实现按目标 thread 的真实 flock 定位同用户交互式 CLI,通过 pidfd 防 PID 复用,拒绝 app-server、远程客户端、桥祖先进程及持有多个 thread 的进程。先 SIGTERM,5 秒后仍存活才 SIGKILL;只有锁已释放且原 thread 恢复成功后才提交消息。不会删除锁、修改 session 文件或自动重试结果未知的提交。
|
|
33
|
+
- 点击旧问号、登录修复或删除按钮前重读授权状态,已恢复时刷新面板。
|
|
34
|
+
- 同步文案明确“已发送、远端导入未确认”;状态更新未满足身份/时间约束时记录 `skipped`。
|
|
35
|
+
|
|
36
|
+
## 验证与部署
|
|
37
|
+
|
|
38
|
+
- 稳定版全量测试共 421 项:420 项通过,1 项因当前进程环境没有 OpenCode CLI 按既有条件跳过;此前带 OpenCode 环境的预览验收为 421 项全部通过。另有 8 项隔离进程测试,覆盖真实 flock、pidfd、SIGTERM/SIGKILL、PID 身份变化和不安全进程拒绝。typecheck、lint、build、diff check 通过。
|
|
39
|
+
- 新增回归覆盖 RPC 超时后的迟到响应、不重发、发送失败清理、存活服务记录保护、登录取消失败/竞态/旧按钮、中断超时不延迟发送、观察轮次不获取 writer、旧授权面板刷新、审计状态跳过、Telegram 流式拖延总超时、多 bot CLI 路由。
|
|
40
|
+
- 在独立临时 CODEX_HOME 中启动真实 Codex 0.153.4,两个客户端连接同一个 app-server 并 resume 同一个已有记录的 thread,验证同线程、同服务。测试不使用用户授权,不调用模型完成任务。空线程在产生记录前不能 resume。
|
|
41
|
+
- 预览安装包最终为 `/tmp/foxden-app-foxclaw-0.7.3-dev.3.tgz`。16P 使用 npm 安装,T490 使用其 pnpm 安装并更新 systemd 到实际包路径。
|
|
42
|
+
- 重启前确认两端桥内无活动任务,并通过只读 `thread/loaded/list` 确认所有受管 app-server 均无加载线程。
|
|
43
|
+
- 16P 与 T490 实际 runtime userAgent 均包含 `foxclaw; 0.7.3-dev.3`;两端 systemd active/running、NRestarts=0、ExecMainStatus=0。T490 六个 bot 均 connected=true。
|
|
44
|
+
- 真实强制接管验收中,目标 thread `01a06fd3-668d-7c81-96e1-d6394c2cf782`、PID `289937`、工作目录 `/home/wuya/git/foxclaw` 经用户确认后停止;日志记录 `codex.external_writer_stopped`。随后同一 thread 由桥的 app-server 持锁并启动新 turn,未删除锁文件。
|
|
45
|
+
- 调用真实 Telegram `getMyCommands`:16P 中英文菜单、T490 同步联系人与 walma10bot 中文菜单均包含 `login_cancel` 和 `cli`。
|
|
46
|
+
|
|
47
|
+
## 剩余边界
|
|
48
|
+
|
|
49
|
+
- 本报告先记录本地预览与真实接管验收;正式 npm 和 GitHub Release 状态以发布后的 registry/workflow 验证为准。
|
|
50
|
+
- 没有代用户执行真实登录和 Telegram 按钮点击;按钮行为由回归测试验证,菜单已通过真实 Telegram API 验收。
|
|
51
|
+
- 去重后未再次触发整个集群的安全同步;原同步导入结果和去重后的运行配置已核实。
|
|
52
|
+
- 网络完全断开时无法即时发送 Telegram 错误提示,可使用本机 `foxclaw resume`。底层服务完全失联时需检查 `foxclaw status`;重启应确认其他会话空闲。
|
|
53
|
+
- 其他候选仍有过期或明确 revoked 的错误,不能视为本次 GamsGo2024 修复失败,也不能通过无条件覆盖或反复刷新解决。
|
|
@@ -21,6 +21,18 @@ launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
|
21
21
|
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
## Telegram 无响应、登录取消与 CLI 恢复
|
|
25
|
+
|
|
26
|
+
Codex RPC 等待上限为 30 秒;超时代表结果未确认,不会自动重发原任务。先用 `/status` 检查。`/status`、`/cli`、`/interrupt`、`/login_cancel` 和 `/auth sync status` 不会排在普通消息的等待队列后面。网络完全不可用时 Telegram 无法送达错误提示,改用本机终端。
|
|
27
|
+
|
|
28
|
+
在运行桥的机器上执行 `foxclaw resume <thread-id>`,CLI 会连接桥正在使用的 Codex app-server。省略 thread-id 会打开选择器。多服务时用 `--bot-id <bot-id>` 指定 `foxclaw status --json` 中的 bot。Telegram 的 `/cli` 也能显示当前线程的直接连接命令。该入口已在 Codex 0.153.4 的 CLI 参数中确认支持。
|
|
29
|
+
|
|
30
|
+
普通 `codex resume` 会启动另一个服务,可能遭遇 `already has an active writer`。同一服务连接可避免争用写入锁。`/watch` 对独立 CLI 仍是只读观察;不要删除 writer lock 文件来强抢线程。`/takeover` 等待中断确认超过 30 秒就退出,不会在稍后继续发送替换任务。
|
|
31
|
+
|
|
32
|
+
设备登录、新增授权和修复登录均提供取消按钮,也可输入 `/login_cancel`。取消按钮只作用于对应会话当前的登录。服务端取消失败时会明确提示“本地流程已退出、远端取消未确认”,不要再使用旧验证码。
|
|
33
|
+
|
|
34
|
+
安全同步显示“已发送”并不等于对端已导入。每个桥进程只配置一个同步联系人;同一进程里的两个 bot 不应当作两个 peer。多 bot 当前使用配置列表中的第一个 bot 回复同步请求。节点未回应时,自检和复核各自最多等待 5 分钟。查看 `/auth sync events <候选名>` 与对端 `/auth`;旧问号按钮会重新读取修复后的状态。
|
|
35
|
+
|
|
24
36
|
## Doctor 检查失败
|
|
25
37
|
|
|
26
38
|
| 现象 | 含义 | 处理方式 |
|