@foxden-app/foxclaw 0.7.1 → 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 +30 -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 +4 -0
- package/dist/codex_app/client.js +43 -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 +8 -0
- package/dist/controller/controller.js +266 -17
- package/dist/i18n.d.ts +36 -8
- package/dist/i18n.js +38 -8
- 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 +14 -2
- 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 +16 -2
- package/package.json +1 -1
- package/scripts/force-takeover.py +118 -0
- package/scripts/force-takeover.test.py +122 -0
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.',
|
|
@@ -307,7 +319,9 @@ const MESSAGES = {
|
|
|
307
319
|
watch_already_enabled: 'Already watching thread {threadId}.',
|
|
308
320
|
watch_started_active: 'Watching thread {threadId}. Live Codex updates from the current turn will appear here.',
|
|
309
321
|
watch_started_idle: 'Watching thread {threadId}. I will mirror the next live turn from Codex CLI here.',
|
|
310
|
-
watch_read_only_active: 'This chat is watching that turn read-only.
|
|
322
|
+
watch_read_only_active: 'This chat is watching that turn read-only, so this operation cannot take ownership. Send plain text or use /queue to queue the next prompt, or /unwatch to start separately.',
|
|
323
|
+
watch_prompt_queued: 'Queued for the watched Codex CLI session ({id}). The CLI will continue with it after the current turn.',
|
|
324
|
+
watch_queue_unsupported: 'This Codex version does not support cross-client session queues. Update Codex to 0.151.0 or later.',
|
|
311
325
|
watch_not_enabled: 'This chat is not watching any thread.',
|
|
312
326
|
watch_stopped: 'Stopped watching thread {threadId}.',
|
|
313
327
|
queued_prompt_set: 'Queued #{position} ({id}). I will send it after the current turn finishes.',
|
|
@@ -776,6 +790,8 @@ const MESSAGES = {
|
|
|
776
790
|
cmd_desc_account: 'Codex 账号',
|
|
777
791
|
cmd_desc_quota: 'Codex 用量',
|
|
778
792
|
cmd_desc_login_device: 'ChatGPT 设备登录',
|
|
793
|
+
cmd_desc_login_cancel: '取消当前登录流程',
|
|
794
|
+
cmd_desc_cli: '从本机 CLI 继续当前会话',
|
|
779
795
|
cmd_desc_threads: '最近线程',
|
|
780
796
|
cmd_desc_open: '打开缓存线程',
|
|
781
797
|
cmd_desc_goal: '线程目标',
|
|
@@ -970,13 +986,13 @@ const MESSAGES = {
|
|
|
970
986
|
auth_sync_test_sent: 'auth sync 测试完成:已发送 {sent},收到回应 {replied}。',
|
|
971
987
|
auth_sync_test_missing: '未回应:{value}',
|
|
972
988
|
auth_sync_push_blocked_active: '当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。',
|
|
973
|
-
auth_sync_safe_starting: '
|
|
989
|
+
auth_sync_safe_starting: '正在自检各节点并分发有效授权。节点未回应时,每轮自检最多等待 5 分钟;可用 /auth sync status 查看进展。',
|
|
974
990
|
auth_sync_safe_done: '安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。',
|
|
975
991
|
auth_cluster_audit_starting: '正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...',
|
|
976
992
|
auth_cluster_audit_blocked_active: '集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。',
|
|
977
993
|
auth_cluster_audit_failed: '集群 auth 自检失败:{error}',
|
|
978
994
|
auth_cluster_audit_summary: '集群 auth 自检:节点 {responded}/{expected};检查账号 {checked},有效 {valid},无有效副本 {invalid}。',
|
|
979
|
-
auth_cluster_audit_synced: '
|
|
995
|
+
auth_cluster_audit_synced: '已选出并发送有效授权(远端导入尚未确认):{value}',
|
|
980
996
|
auth_cluster_audit_repair: '多节点确认无效,已标记问号等待人工处理:{value}',
|
|
981
997
|
auth_cluster_audit_missing: '未回应节点:{value}',
|
|
982
998
|
auth_cluster_audit_busy: '忙碌节点:{value}',
|
|
@@ -1025,7 +1041,17 @@ const MESSAGES = {
|
|
|
1025
1041
|
working: '处理中...',
|
|
1026
1042
|
usage_open: '用法:/open <编号>',
|
|
1027
1043
|
usage_watch: '用法:/watch [编号]',
|
|
1028
|
-
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} 的写入权,准备发送指定消息;这不代表任务已完成。',
|
|
1029
1055
|
usage_queue: '用法:/queue <消息>',
|
|
1030
1056
|
unknown_cached_thread: '找不到这个缓存线程,请先运行 /threads。',
|
|
1031
1057
|
cached_thread_unavailable: '这个缓存线程已经不可用,请重新运行 /threads。',
|
|
@@ -1033,7 +1059,9 @@ const MESSAGES = {
|
|
|
1033
1059
|
watch_already_enabled: '已经在观察线程 {threadId}。',
|
|
1034
1060
|
watch_started_active: '已开始观察线程 {threadId}。当前这轮 Codex 更新会同步到这里。',
|
|
1035
1061
|
watch_started_idle: '已开始观察线程 {threadId}。之后 CLI 里这条线程一旦有新 live turn,我会同步到这里。',
|
|
1036
|
-
watch_read_only_active: '
|
|
1062
|
+
watch_read_only_active: '当前聊天正在只读观察这轮,不能接管这项操作。直接发送文字或使用 /queue 可排入下一轮;要另行启动请先 /unwatch。',
|
|
1063
|
+
watch_prompt_queued: '已排入正在观察的 Codex CLI 会话({id})。当前轮结束后,CLI 会自动接着执行。',
|
|
1064
|
+
watch_queue_unsupported: '当前 Codex 不支持跨客户端会话队列,请升级到 Codex 0.151.0 或更高版本。',
|
|
1037
1065
|
watch_not_enabled: '当前没有正在观察的线程。',
|
|
1038
1066
|
watch_stopped: '已停止观察线程 {threadId}。',
|
|
1039
1067
|
queued_prompt_set: '已排队 #{position}({id})。当前回复结束后我会自动发送。',
|
|
@@ -1475,6 +1503,8 @@ export function getTelegramCommands(locale) {
|
|
|
1475
1503
|
{ command: 'account', description: t(locale, 'cmd_desc_account') },
|
|
1476
1504
|
{ command: 'quota', description: t(locale, 'cmd_desc_quota') },
|
|
1477
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') },
|
|
1478
1508
|
{ command: 'open', description: t(locale, 'cmd_desc_open') },
|
|
1479
1509
|
{ command: 'goal', description: t(locale, 'cmd_desc_goal') },
|
|
1480
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
|
@@ -353,7 +353,17 @@ Usage:
|
|
|
353
353
|
- `/watch <n>`: watch item n from the latest `/threads` list.
|
|
354
354
|
- `/unwatch`: stop watching.
|
|
355
355
|
|
|
356
|
-
Watch mode mirrors live turn progress and approval requests.
|
|
356
|
+
Watch mode mirrors live turn progress and approval requests. It still does not take ownership of the CLI writer, and `/steer` cannot modify the in-flight turn. With Codex CLI 0.151.0 or later, however, plain text and `/queue <message>` use Codex's cross-client queue for the same thread. The desktop CLI automatically continues with those messages, in order, after the current turn finishes.
|
|
357
|
+
|
|
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
|
+
|
|
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.
|
|
357
367
|
|
|
358
368
|
## 6. Codex Login And Auth Rotation
|
|
359
369
|
|
|
@@ -363,7 +373,9 @@ To keep one Telegram bot interoperable with terminal Codex sessions, put the sam
|
|
|
363
373
|
|
|
364
374
|
### 6.1 File Format
|
|
365
375
|
|
|
366
|
-
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>`.
|
|
367
379
|
|
|
368
380
|
Recommended layout:
|
|
369
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
|
| 现象 | 含义 | 处理方式 |
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -353,7 +353,17 @@ FoxClaw 的聊天是“绑定线程”的。你在手机上打开某个 Codex
|
|
|
353
353
|
- `/watch <n>`:观察最近一次 `/threads` 列表里的第 n 个线程。
|
|
354
354
|
- `/unwatch`:停止观察。
|
|
355
355
|
|
|
356
|
-
观察模式会同步 live turn
|
|
356
|
+
观察模式会同步 live turn 进展和审批请求。观察中的聊天仍然不接管 CLI writer,也不能用 `/steer` 修改正在执行的 turn;但从 Codex CLI 0.151.0 开始,直接发送文字或使用 `/queue <消息>` 会通过 Codex 的跨客户端队列排入同一线程,由桌面 CLI 在当前 turn 结束后自动继续执行。连续发送多条会按顺序排队。
|
|
357
|
+
|
|
358
|
+
旧版 Codex 没有这个队列接口,FoxClaw 会明确提示升级,不会退回到抢占 writer 或改写 session 文件。要让 Telegram 自己另行启动 turn,仍需先 `/unwatch`。
|
|
359
|
+
|
|
360
|
+
### `/takeover --force <消息>`:从本机 CLI 强制交接
|
|
361
|
+
|
|
362
|
+
遇到 `already has an active writer` 时,可发送 `/takeover --force 继续处理`,核对 thread、PID 和工作目录,再点击“确认强制接管”;也可点“取消”。确认仅 60 秒有效,并绑定发起用户和聊天。普通 `/watch`、`/queue` 和 `/takeover` 不会自动停止外部 CLI。
|
|
363
|
+
|
|
364
|
+
仅支持 Linux/WSL,需 `python3` 3.9+ 和内核 pidfd 支持。只接受当前 bot 的 Codex home 中、同一系统用户的交互式 Codex CLI。拒绝 app-server、远程客户端、桥的祖先进程,以及同时持有其他 thread 锁的进程。确认后重新核实进程启动时间及锁身份,通过 pidfd 先发 SIGTERM,5 秒不退出再发 SIGKILL;检查锁释放后,才恢复原 thread 并提交指定消息。不会删除锁或修改 session 文件,不会自动重试提交。
|
|
365
|
+
|
|
366
|
+
强停可能打断未完成任务,已启动的子命令可能继续运行,文件修改不会回滚。身份变化、锁未释放或恢复失败都会明确报错,不投递新任务。桥自己的旧待执行队列只在成功取得写入权后取消;原 CLI 的跨客户端队列不会被此功能清空。多线程 CLI 请在终端手动交接。
|
|
357
367
|
|
|
358
368
|
## 6. Codex 登录和 auth 轮转
|
|
359
369
|
|
|
@@ -363,7 +373,11 @@ FoxClaw 的聊天是“绑定线程”的。你在手机上打开某个 Codex
|
|
|
363
373
|
|
|
364
374
|
### 6.1 文件格式
|
|
365
375
|
|
|
366
|
-
单 bot 兼容模式的候选文件放在 Codex auth 目录,默认是 `~/.codex/`。如果你设置了 `CODEX_AUTH_DIR`,则使用那个目录。多 bot 模式以这个目录作为候选源,并在 `~/.foxclaw/codex/telegram
|
|
376
|
+
单 bot 兼容模式的候选文件放在 Codex auth 目录,默认是 `~/.codex/`。如果你设置了 `CODEX_AUTH_DIR`,则使用那个目录。多 bot 模式以这个目录作为候选源,并在 `~/.foxclaw/codex/telegram/@Telegram用户名/home/` 下为隔离 bot 保存副本,例如 `@WuguiAI_Bot/home/`。默认/终端共享 bot 的名称目录通过链接指向原来的 Codex home,保留终端互通能力。
|
|
377
|
+
|
|
378
|
+
启动时并行读取 Telegram 的真实用户名。已有 `bot<id>` 目录会迁移到名称目录,旧路径保留兼容链接;更改用户名后,下次启动会更新目录名称并保留旧名称链接。首次启动无法联网时临时使用 `bot<id>`,之后启动取到用户名再迁移;已有名称在断网重启时继续使用。迁移前请让相关会话空闲。遇到同名冲突会明确停止,不合并或覆盖目录。
|
|
379
|
+
|
|
380
|
+
名称目录中的 `.foxclaw-bot.json` 只保存稳定的数字 bot ID,不保存 token,供媒体发送判断目标账号。不要删除该文件或旧路径链接。数据库绑定、运行日志及服务状态仍用稳定 bot ID 标识;项目工作目录仍由 `DEFAULT_CWD` 或 `/new <目录>` 决定。
|
|
367
381
|
|
|
368
382
|
推荐命名:
|
|
369
383
|
|