@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
|
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { externalWriterControl } from '../codex_app/force_takeover.js';
|
|
5
6
|
import { TELEGRAM_VOICE_MAX_BYTES, TELEGRAM_VOICE_SUPPORTED_EXTENSIONS, telegramVoiceContentType, } from '../voice/files.js';
|
|
6
7
|
import { normalizeLocale, t } from '../i18n.js';
|
|
7
8
|
import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthRecord, readChatGptAuthMetadata, } from '../auth/mirror.js';
|
|
@@ -64,6 +65,8 @@ const DYNAMIC_HELP_COMMANDS = [
|
|
|
64
65
|
{ key: 'quota', line: '/quota' },
|
|
65
66
|
{ key: 'update', line: '/update' },
|
|
66
67
|
{ key: 'login_device', line: '/login_device' },
|
|
68
|
+
{ key: 'login_cancel', line: '/login_cancel' },
|
|
69
|
+
{ key: 'cli', line: '/cli' },
|
|
67
70
|
{ key: 'threads_archived', line: '/threads archived [query]' },
|
|
68
71
|
{ key: 'open', line: '/open <n>' },
|
|
69
72
|
{ key: 'goal', line: '/goal [objective|pause|resume|done|budget <tokens|off>|clear confirm]' },
|
|
@@ -122,6 +125,9 @@ export class BridgeSessionCore {
|
|
|
122
125
|
pendingUserInputs = new Map();
|
|
123
126
|
pendingMcpElicitations = new Map();
|
|
124
127
|
pendingLoginsByScope = new Map();
|
|
128
|
+
externalWriterControl = externalWriterControl;
|
|
129
|
+
forceTakeoversInProgress = new Set();
|
|
130
|
+
pendingForceTakeovers = new Map();
|
|
125
131
|
pendingLoginScopesById = new Map();
|
|
126
132
|
pendingAuthAddsByLoginId = new Map();
|
|
127
133
|
latestTurnDiffs = new Map();
|
|
@@ -182,9 +188,7 @@ export class BridgeSessionCore {
|
|
|
182
188
|
this.updateStatus();
|
|
183
189
|
});
|
|
184
190
|
this.bot.on('text', (event) => {
|
|
185
|
-
|
|
186
|
-
void this.handleAsyncError('telegram.text', error, event.scopeId);
|
|
187
|
-
});
|
|
191
|
+
this.dispatchInboundLikeTelegramText(event);
|
|
188
192
|
});
|
|
189
193
|
this.bot.on('callback', (event) => {
|
|
190
194
|
void this.handleCallback(event).catch((error) => {
|
|
@@ -197,7 +201,14 @@ export class BridgeSessionCore {
|
|
|
197
201
|
* (used by the Weixin adapter).
|
|
198
202
|
*/
|
|
199
203
|
dispatchInboundLikeTelegramText(event) {
|
|
200
|
-
|
|
204
|
+
const command = event.attachments.length === 0 ? parseCommand(event.text) : null;
|
|
205
|
+
const name = command?.name;
|
|
206
|
+
// Recovery commands must remain reachable while a normal command is waiting.
|
|
207
|
+
// handleText still performs normal addressing and authorization checks.
|
|
208
|
+
const urgent = name && (['status', 'cli', 'login_cancel', 'interrupt'].includes(name)
|
|
209
|
+
|| (name === 'auth' && command?.args[0] === 'sync' && ['status', 'events', 'trace'].includes(command.args[1] ?? 'status')));
|
|
210
|
+
const task = urgent ? this.handleText(event) : this.withLock(event.scopeId, async () => this.handleText(event));
|
|
211
|
+
void task.catch((error) => {
|
|
201
212
|
void this.handleAsyncError('channel.text', error, event.scopeId);
|
|
202
213
|
});
|
|
203
214
|
}
|
|
@@ -230,6 +241,15 @@ export class BridgeSessionCore {
|
|
|
230
241
|
this.attachedThreads.clear();
|
|
231
242
|
this.threadTokenUsageAlerts.clear();
|
|
232
243
|
if (!this.stopping) {
|
|
244
|
+
if (!this.codexReconnectPending) {
|
|
245
|
+
for (const scopeId of new Set([...this.activeTurns.values()].filter(active => !active.isObserved).map(active => active.scopeId))) {
|
|
246
|
+
const locale = this.localeForChat(scopeId);
|
|
247
|
+
void this.sendMessage(scopeId, locale === 'zh'
|
|
248
|
+
? 'Codex 连接中断,正在重连。任务结果尚未确认,请先用 /status 检查;也可用 /cli 从本机继续。'
|
|
249
|
+
: 'Codex disconnected; reconnecting. The task result is unconfirmed. Check /status or use /cli locally.')
|
|
250
|
+
.catch(error => this.logger.warn('codex.disconnect_notice_failed', { scopeId, error: toErrorMeta(error) }));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
233
253
|
this.codexReconnectPending = true;
|
|
234
254
|
this.pauseAppSnapshotWatchers();
|
|
235
255
|
}
|
|
@@ -359,6 +379,9 @@ export class BridgeSessionCore {
|
|
|
359
379
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
|
|
360
380
|
return;
|
|
361
381
|
}
|
|
382
|
+
if (await this.queueObservedThreadMessage(event, locale, event.text.trim())) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
362
385
|
await this.startBoundTurnFromEvent(event, locale, event.text.trim());
|
|
363
386
|
return;
|
|
364
387
|
}
|
|
@@ -417,6 +440,9 @@ export class BridgeSessionCore {
|
|
|
417
440
|
await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
|
|
418
441
|
return;
|
|
419
442
|
}
|
|
443
|
+
if (await this.queueObservedThreadMessage(event, locale, decision.text)) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
420
446
|
await this.startBoundTurnFromEvent(event, locale, decision.text);
|
|
421
447
|
}
|
|
422
448
|
async handleCommand(event, locale, name, args) {
|
|
@@ -576,6 +602,20 @@ export class BridgeSessionCore {
|
|
|
576
602
|
await this.handleLoginCancelCommand(scopeId, locale, args);
|
|
577
603
|
return;
|
|
578
604
|
}
|
|
605
|
+
case 'cli': {
|
|
606
|
+
const binding = this.store.getBinding(scopeId);
|
|
607
|
+
const server = this.app.getServerStatus();
|
|
608
|
+
if (!server.running || !server.port) {
|
|
609
|
+
await this.sendMessage(scopeId, locale === 'zh' ? 'Codex 服务不可用,请在本机检查 foxclaw status。' : 'Codex server unavailable. Check foxclaw status locally.');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const threadArg = binding ? ` ${JSON.stringify(binding.threadId)}` : '';
|
|
613
|
+
await this.sendMessage(scopeId, (locale === 'zh'
|
|
614
|
+
? '在运行桥的这台机器上执行,进入同一个 Codex 服务后可中断或继续会话:\n'
|
|
615
|
+
: 'Run on the bridge host to interrupt or continue in the same Codex server:\n')
|
|
616
|
+
+ `codex resume --remote ws://127.0.0.1:${server.port}${threadArg}`);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
579
619
|
case 'logout': {
|
|
580
620
|
await this.handleLogoutCommand(scopeId, locale, args);
|
|
581
621
|
return;
|
|
@@ -1066,6 +1106,21 @@ export class BridgeSessionCore {
|
|
|
1066
1106
|
async handleCallback(event) {
|
|
1067
1107
|
const scopeId = event.scopeId;
|
|
1068
1108
|
const locale = this.localeForChat(scopeId, event.languageCode);
|
|
1109
|
+
if (this.forceTakeoversInProgress.has(scopeId)) {
|
|
1110
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'force_takeover_busy'));
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const forceTakeoverMatch = /^takeover:(confirm|cancel):([a-f0-9]+)$/.exec(event.data);
|
|
1114
|
+
if (forceTakeoverMatch) {
|
|
1115
|
+
await this.handleForceTakeoverCallback(event, locale, forceTakeoverMatch[1], forceTakeoverMatch[2]);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const loginCancelMatch = /^login:cancel:(.+)$/.exec(event.data);
|
|
1119
|
+
if (loginCancelMatch) {
|
|
1120
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'button_cancel'));
|
|
1121
|
+
await this.handleLoginCancelCommand(scopeId, locale, [loginCancelMatch[1]]);
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1069
1124
|
const interruptMatch = /^turn:interrupt:(.+)$/.exec(event.data);
|
|
1070
1125
|
if (interruptMatch) {
|
|
1071
1126
|
await this.handleTurnInterruptCallback(event, interruptMatch[1], locale);
|
|
@@ -3648,7 +3703,9 @@ export class BridgeSessionCore {
|
|
|
3648
3703
|
if (!scopeId)
|
|
3649
3704
|
return;
|
|
3650
3705
|
try {
|
|
3651
|
-
|
|
3706
|
+
const locale = this.localeForChat(scopeId);
|
|
3707
|
+
const hint = isThreadActiveWriterError(error) ? `\n${t(locale, 'force_takeover_hint')}` : '';
|
|
3708
|
+
await this.sendMessage(scopeId, t(locale, 'bridge_error', { error: formatUserError(error) }) + hint);
|
|
3652
3709
|
}
|
|
3653
3710
|
catch (notifyError) {
|
|
3654
3711
|
this.logger.error('telegram.error_notification_failed', { error: toErrorMeta(notifyError), scopeId });
|
|
@@ -3881,7 +3938,7 @@ export class BridgeSessionCore {
|
|
|
3881
3938
|
}
|
|
3882
3939
|
}
|
|
3883
3940
|
for (const active of [...this.activeTurns.values()]) {
|
|
3884
|
-
if (recoveredScopes.has(active.scopeId) || !this.getActiveTurn(active.scopeId, active.turnId)) {
|
|
3941
|
+
if (active.isObserved || recoveredScopes.has(active.scopeId) || !this.getActiveTurn(active.scopeId, active.turnId)) {
|
|
3885
3942
|
continue;
|
|
3886
3943
|
}
|
|
3887
3944
|
try {
|
|
@@ -3894,6 +3951,10 @@ export class BridgeSessionCore {
|
|
|
3894
3951
|
turnId: active.turnId,
|
|
3895
3952
|
error: toErrorMeta(error),
|
|
3896
3953
|
});
|
|
3954
|
+
await this.sendMessage(active.scopeId, this.localeForChat(active.scopeId) === 'zh'
|
|
3955
|
+
? '连接已恢复,但无法确认原任务状态。任务未自动重发,请用 /cli 检查原会话后再继续。'
|
|
3956
|
+
: 'Connected again, but the original task state could not be confirmed. It was not resent. Use /cli to inspect the session.')
|
|
3957
|
+
.catch(notifyError => this.logger.warn('codex.recovery_notice_failed', { error: toErrorMeta(notifyError) }));
|
|
3897
3958
|
}
|
|
3898
3959
|
}
|
|
3899
3960
|
await this.recoverQueuedTurns();
|
|
@@ -4240,6 +4301,10 @@ export class BridgeSessionCore {
|
|
|
4240
4301
|
}
|
|
4241
4302
|
}
|
|
4242
4303
|
async handleTakeoverCommand(event, locale, args) {
|
|
4304
|
+
if (args[0] === '--force') {
|
|
4305
|
+
await this.prepareForceTakeover(event, locale, args.slice(1).join(' ').trim());
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4243
4308
|
const scopeId = event.scopeId;
|
|
4244
4309
|
const nextPrompt = args.join(' ').trim();
|
|
4245
4310
|
if (!nextPrompt) {
|
|
@@ -4257,10 +4322,113 @@ export class BridgeSessionCore {
|
|
|
4257
4322
|
await this.requestInterrupt(active);
|
|
4258
4323
|
}
|
|
4259
4324
|
await this.sendMessage(scopeId, t(locale, 'interrupt_requested_waiting'));
|
|
4260
|
-
|
|
4325
|
+
let timer;
|
|
4326
|
+
try {
|
|
4327
|
+
await Promise.race([
|
|
4328
|
+
active.completion,
|
|
4329
|
+
new Promise((_, reject) => {
|
|
4330
|
+
timer = setTimeout(() => reject(new Error(locale === 'zh'
|
|
4331
|
+
? '等待中断完成超时,尚未发送新任务。请用 /status 检查,或用 /cli 从终端恢复。'
|
|
4332
|
+
: 'Interrupt confirmation timed out. No replacement task was sent. Check /status or use /cli to recover.')), 30_000);
|
|
4333
|
+
}),
|
|
4334
|
+
]);
|
|
4335
|
+
}
|
|
4336
|
+
finally {
|
|
4337
|
+
clearTimeout(timer);
|
|
4338
|
+
}
|
|
4261
4339
|
}
|
|
4262
4340
|
await this.startBoundTurnFromEvent(event, locale, nextPrompt);
|
|
4263
4341
|
}
|
|
4342
|
+
async prepareForceTakeover(event, locale, prompt) {
|
|
4343
|
+
const scopeId = event.scopeId;
|
|
4344
|
+
this.pendingForceTakeovers.delete(scopeId);
|
|
4345
|
+
if (!prompt) {
|
|
4346
|
+
await this.sendMessage(scopeId, t(locale, 'usage_takeover'));
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
if (!scopeId.startsWith('telegram:') || event.userId !== this.config.tgAllowedUserId) {
|
|
4350
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_trusted_telegram_only'));
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
const binding = this.store.getBinding(scopeId);
|
|
4354
|
+
if (!binding) {
|
|
4355
|
+
await this.sendMessage(scopeId, t(locale, 'watch_no_thread_bound'));
|
|
4356
|
+
return;
|
|
4357
|
+
}
|
|
4358
|
+
const home = this.config.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
|
|
4359
|
+
try {
|
|
4360
|
+
const writer = await this.externalWriterControl.inspect(home, binding.threadId);
|
|
4361
|
+
const id = crypto.randomBytes(8).toString('hex');
|
|
4362
|
+
this.pendingForceTakeovers.set(scopeId, {
|
|
4363
|
+
id, expiresAt: Date.now() + 60_000, event, threadId: binding.threadId, home, prompt, writer,
|
|
4364
|
+
});
|
|
4365
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_confirm', {
|
|
4366
|
+
threadId: binding.threadId, pid: String(writer.pid), cwd: writer.cwd, prompt,
|
|
4367
|
+
}), [[
|
|
4368
|
+
{ text: t(locale, 'button_force_takeover'), callback_data: `takeover:confirm:${id}` },
|
|
4369
|
+
{ text: t(locale, 'button_cancel'), callback_data: `takeover:cancel:${id}` },
|
|
4370
|
+
]]);
|
|
4371
|
+
}
|
|
4372
|
+
catch (error) {
|
|
4373
|
+
this.pendingForceTakeovers.delete(scopeId);
|
|
4374
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_failed', { error: formatUserError(error) }));
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
async handleForceTakeoverCallback(event, locale, action, id) {
|
|
4378
|
+
const scopeId = event.scopeId;
|
|
4379
|
+
const pending = this.pendingForceTakeovers.get(scopeId);
|
|
4380
|
+
if (!pending || pending.id !== id || pending.event.userId !== event.userId
|
|
4381
|
+
|| event.userId !== this.config.tgAllowedUserId || pending.expiresAt <= Date.now()) {
|
|
4382
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'force_takeover_expired'));
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
// Claim before any await: duplicate callbacks cannot stop or submit twice.
|
|
4386
|
+
this.pendingForceTakeovers.delete(scopeId);
|
|
4387
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, action === 'cancel' ? 'button_cancel' : 'button_force_takeover'));
|
|
4388
|
+
if (action === 'cancel') {
|
|
4389
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_cancelled'));
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
await this.withLock(scopeId, async () => {
|
|
4393
|
+
if (pending.expiresAt <= Date.now() || this.store.getBinding(scopeId)?.threadId !== pending.threadId) {
|
|
4394
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_expired'));
|
|
4395
|
+
return;
|
|
4396
|
+
}
|
|
4397
|
+
if (this.externalAuthValidationInProgress) {
|
|
4398
|
+
await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
this.forceTakeoversInProgress.add(scopeId);
|
|
4402
|
+
this.turnStartInProgress += 1;
|
|
4403
|
+
try {
|
|
4404
|
+
try {
|
|
4405
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_stopping', { pid: String(pending.writer.pid) }));
|
|
4406
|
+
await this.externalWriterControl.stop(pending.home, pending.threadId, pending.writer);
|
|
4407
|
+
this.logger.warn('codex.external_writer_stopped', { scopeId, threadId: pending.threadId, pid: pending.writer.pid });
|
|
4408
|
+
await this.stopWatchingScopeThread(scopeId);
|
|
4409
|
+
this.attachedThreads.delete(attachedThreadKey(scopeId, pending.threadId));
|
|
4410
|
+
// Never replace a missing thread with an unrelated new conversation.
|
|
4411
|
+
const binding = this.store.getBinding(scopeId);
|
|
4412
|
+
if (binding?.threadId !== pending.threadId)
|
|
4413
|
+
throw new Error(t(locale, 'force_takeover_expired'));
|
|
4414
|
+
await this.ensureThreadReady(scopeId, binding, { recoverMissingThread: false });
|
|
4415
|
+
}
|
|
4416
|
+
catch (error) {
|
|
4417
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_failed', { error: formatUserError(error) }));
|
|
4418
|
+
return;
|
|
4419
|
+
}
|
|
4420
|
+
this.store.cancelQueuedTurnInputs(scopeId);
|
|
4421
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_acquired', { threadId: pending.threadId }));
|
|
4422
|
+
// Normal turn-start error handling preserves ambiguous submission failures;
|
|
4423
|
+
// do not automatically retry a possibly accepted prompt.
|
|
4424
|
+
await this.startBoundTurnFromEvent(pending.event, locale, pending.prompt);
|
|
4425
|
+
}
|
|
4426
|
+
finally {
|
|
4427
|
+
this.forceTakeoversInProgress.delete(scopeId);
|
|
4428
|
+
this.turnStartInProgress -= 1;
|
|
4429
|
+
}
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4264
4432
|
async handleQueueCommand(event, locale, args) {
|
|
4265
4433
|
const scopeId = event.scopeId;
|
|
4266
4434
|
const nextPrompt = args.join(' ').trim();
|
|
@@ -4269,12 +4437,15 @@ export class BridgeSessionCore {
|
|
|
4269
4437
|
return;
|
|
4270
4438
|
}
|
|
4271
4439
|
const active = this.findActiveTurn(scopeId);
|
|
4272
|
-
|
|
4273
|
-
|
|
4440
|
+
const observedThreadId = active?.isObserved
|
|
4441
|
+
? active.threadId
|
|
4442
|
+
: this.observedThreadWatchers.get(scopeId)?.threadId ?? null;
|
|
4443
|
+
if (observedThreadId) {
|
|
4444
|
+
await this.queueObservedThreadMessage(event, locale, nextPrompt, observedThreadId);
|
|
4274
4445
|
return;
|
|
4275
4446
|
}
|
|
4276
|
-
if (active
|
|
4277
|
-
await this.
|
|
4447
|
+
if (!active) {
|
|
4448
|
+
await this.startBoundTurnFromEvent(event, locale, nextPrompt);
|
|
4278
4449
|
return;
|
|
4279
4450
|
}
|
|
4280
4451
|
await this.queuePromptAfterActiveTurn(event, locale, nextPrompt);
|
|
@@ -4285,7 +4456,7 @@ export class BridgeSessionCore {
|
|
|
4285
4456
|
return;
|
|
4286
4457
|
}
|
|
4287
4458
|
if (active.isObserved) {
|
|
4288
|
-
await this.
|
|
4459
|
+
await this.queueObservedThreadMessage(event, locale, text, active.threadId);
|
|
4289
4460
|
return;
|
|
4290
4461
|
}
|
|
4291
4462
|
const settings = this.store.getChatSettings(event.scopeId);
|
|
@@ -4311,6 +4482,34 @@ export class BridgeSessionCore {
|
|
|
4311
4482
|
await this.startBoundTurnFromEvent(event, locale, text);
|
|
4312
4483
|
}
|
|
4313
4484
|
}
|
|
4485
|
+
async queueObservedThreadMessage(event, locale, text, threadId = this.observedThreadWatchers.get(event.scopeId)?.threadId ?? null) {
|
|
4486
|
+
if (!threadId) {
|
|
4487
|
+
return false;
|
|
4488
|
+
}
|
|
4489
|
+
const input = await this.buildTurnInput({
|
|
4490
|
+
threadId,
|
|
4491
|
+
cwd: this.store.getBinding(event.scopeId)?.cwd ?? this.config.defaultCwd,
|
|
4492
|
+
}, { ...event, text }, locale);
|
|
4493
|
+
const clientUserMessageId = `foxclaw:${event.scopeId}:${event.messageId}`;
|
|
4494
|
+
try {
|
|
4495
|
+
const queued = await this.app.queueThreadInput(threadId, clientUserMessageId, input);
|
|
4496
|
+
await this.sendMessage(event.scopeId, t(locale, 'watch_prompt_queued', {
|
|
4497
|
+
id: shortId(queued.queuedSubmissionId),
|
|
4498
|
+
}));
|
|
4499
|
+
}
|
|
4500
|
+
catch (error) {
|
|
4501
|
+
if (!isThreadQueueUnsupportedError(error)) {
|
|
4502
|
+
throw error;
|
|
4503
|
+
}
|
|
4504
|
+
this.logger.warn('codex.observed_thread_queue_unsupported', {
|
|
4505
|
+
scopeId: event.scopeId,
|
|
4506
|
+
threadId,
|
|
4507
|
+
error: toErrorMeta(error),
|
|
4508
|
+
});
|
|
4509
|
+
await this.sendMessage(event.scopeId, t(locale, 'watch_queue_unsupported'));
|
|
4510
|
+
}
|
|
4511
|
+
return true;
|
|
4512
|
+
}
|
|
4314
4513
|
async queuePromptAfterActiveTurn(event, locale, text) {
|
|
4315
4514
|
const active = this.findActiveTurn(event.scopeId);
|
|
4316
4515
|
if (!active) {
|
|
@@ -5339,7 +5538,7 @@ export class BridgeSessionCore {
|
|
|
5339
5538
|
t(locale, 'login_code', { value: login.userCode }),
|
|
5340
5539
|
t(locale, 'login_id', { value: login.loginId }),
|
|
5341
5540
|
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
5342
|
-
].join('\n'));
|
|
5541
|
+
].join('\n'), this.loginCancelKeyboard(login.loginId, locale));
|
|
5343
5542
|
}
|
|
5344
5543
|
catch (error) {
|
|
5345
5544
|
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, state.currentTargetPath);
|
|
@@ -5382,7 +5581,7 @@ export class BridgeSessionCore {
|
|
|
5382
5581
|
t(locale, 'login_code', { value: login.userCode }),
|
|
5383
5582
|
t(locale, 'login_id', { value: login.loginId }),
|
|
5384
5583
|
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
5385
|
-
].join('\n'));
|
|
5584
|
+
].join('\n'), this.loginCancelKeyboard(login.loginId, locale));
|
|
5386
5585
|
}
|
|
5387
5586
|
catch (error) {
|
|
5388
5587
|
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, state.currentTargetPath);
|
|
@@ -5547,6 +5746,11 @@ export class BridgeSessionCore {
|
|
|
5547
5746
|
}
|
|
5548
5747
|
}
|
|
5549
5748
|
async handleLoginDeviceCommand(scopeId, locale) {
|
|
5749
|
+
const pendingLoginId = this.pendingLoginsByScope.get(scopeId);
|
|
5750
|
+
if (pendingLoginId) {
|
|
5751
|
+
await this.sendMessage(scopeId, t(locale, 'login_cancel_hint', { value: pendingLoginId }), this.loginCancelKeyboard(pendingLoginId, locale));
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
5550
5754
|
const login = await this.app.startDeviceLogin();
|
|
5551
5755
|
const oldLoginId = this.pendingLoginsByScope.get(scopeId);
|
|
5552
5756
|
if (oldLoginId) {
|
|
@@ -5561,24 +5765,41 @@ export class BridgeSessionCore {
|
|
|
5561
5765
|
t(locale, 'login_code', { value: login.userCode }),
|
|
5562
5766
|
t(locale, 'login_id', { value: login.loginId }),
|
|
5563
5767
|
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
5564
|
-
].join('\n'));
|
|
5768
|
+
].join('\n'), this.loginCancelKeyboard(login.loginId, locale));
|
|
5769
|
+
}
|
|
5770
|
+
loginCancelKeyboard(loginId, locale) {
|
|
5771
|
+
return [[{ text: t(locale, 'button_cancel'), callback_data: `login:cancel:${loginId}` }]];
|
|
5565
5772
|
}
|
|
5566
5773
|
async handleLoginCancelCommand(scopeId, locale, args) {
|
|
5567
5774
|
const loginId = args[0]?.trim() || this.pendingLoginsByScope.get(scopeId) || null;
|
|
5568
|
-
if (!loginId) {
|
|
5775
|
+
if (!loginId || this.pendingLoginsByScope.get(scopeId) !== loginId) {
|
|
5569
5776
|
await this.sendMessage(scopeId, t(locale, 'login_cancel_no_pending'));
|
|
5570
5777
|
return;
|
|
5571
5778
|
}
|
|
5572
5779
|
const pendingAuthAdd = this.pendingAuthAddsByLoginId.get(loginId) ?? null;
|
|
5573
|
-
|
|
5780
|
+
// Claim cancellation before awaiting RPC: completion notifications may arrive first.
|
|
5574
5781
|
this.pendingLoginsByScope.delete(scopeId);
|
|
5575
5782
|
this.pendingLoginScopesById.delete(loginId);
|
|
5576
5783
|
this.pendingAuthAddsByLoginId.delete(loginId);
|
|
5784
|
+
let cancelError;
|
|
5785
|
+
try {
|
|
5786
|
+
await this.app.cancelLogin(loginId);
|
|
5787
|
+
}
|
|
5788
|
+
catch (error) {
|
|
5789
|
+
cancelError = error;
|
|
5790
|
+
this.logger.warn('codex.login_cancel_failed', { error: toErrorMeta(error) });
|
|
5791
|
+
}
|
|
5577
5792
|
if (pendingAuthAdd) {
|
|
5578
5793
|
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
5579
5794
|
await this.sendMessage(scopeId, t(locale, 'auth_add_cancelled'));
|
|
5580
5795
|
return;
|
|
5581
5796
|
}
|
|
5797
|
+
if (cancelError) {
|
|
5798
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
5799
|
+
? '已退出本地登录流程,但未能确认服务端取消。请勿继续使用旧验证码;可重新登录或用 /codex_restart 重置登录服务。'
|
|
5800
|
+
: 'Local login flow cleared, but server cancellation is unconfirmed. Do not use the old code; start a new login or use /codex_restart.');
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5582
5803
|
await this.sendMessage(scopeId, t(locale, 'login_cancelled'));
|
|
5583
5804
|
}
|
|
5584
5805
|
async handleLogoutCommand(scopeId, locale, args) {
|
|
@@ -5601,6 +5822,10 @@ export class BridgeSessionCore {
|
|
|
5601
5822
|
await this.sendMessage(scopeId, t(locale, 'no_active_turn'));
|
|
5602
5823
|
return;
|
|
5603
5824
|
}
|
|
5825
|
+
if (active.isObserved) {
|
|
5826
|
+
await this.sendMessage(scopeId, t(locale, 'watch_read_only_active'));
|
|
5827
|
+
return;
|
|
5828
|
+
}
|
|
5604
5829
|
await this.app.steerTurn(active.threadId, active.turnId, [{
|
|
5605
5830
|
type: 'text',
|
|
5606
5831
|
text,
|
|
@@ -6121,6 +6346,20 @@ export class BridgeSessionCore {
|
|
|
6121
6346
|
await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
6122
6347
|
}
|
|
6123
6348
|
}
|
|
6349
|
+
async refreshRepairedAuthChoice(event, record, candidateName, locale) {
|
|
6350
|
+
const state = await this.listCodexAuthState();
|
|
6351
|
+
if (state.candidates.find(candidate => candidate.name === candidateName)?.state === 'needs_repair')
|
|
6352
|
+
return false;
|
|
6353
|
+
record.candidates = state.candidates;
|
|
6354
|
+
record.createdAt = Date.now();
|
|
6355
|
+
clampCodexAuthListOffset(record);
|
|
6356
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh'
|
|
6357
|
+
? '授权状态已变化,已刷新面板。' : 'Auth state changed; panel refreshed.');
|
|
6358
|
+
if (record.messageId !== null) {
|
|
6359
|
+
await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
6360
|
+
}
|
|
6361
|
+
return true;
|
|
6362
|
+
}
|
|
6124
6363
|
async handleAuthRepairMenuCallback(event, localId, index, locale) {
|
|
6125
6364
|
const record = this.pendingAuthChoiceLists.get(localId);
|
|
6126
6365
|
if (!record) {
|
|
@@ -6136,6 +6375,8 @@ export class BridgeSessionCore {
|
|
|
6136
6375
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
6137
6376
|
return;
|
|
6138
6377
|
}
|
|
6378
|
+
if (await this.refreshRepairedAuthChoice(event, record, candidate.name, locale))
|
|
6379
|
+
return;
|
|
6139
6380
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_repair_actions_short'));
|
|
6140
6381
|
if (record.messageId !== null) {
|
|
6141
6382
|
await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_actions_message', { value: formatCodexAuthCandidateDisplayName(candidate.name) }), authRepairKeyboard(locale, record, index));
|
|
@@ -6156,6 +6397,8 @@ export class BridgeSessionCore {
|
|
|
6156
6397
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
6157
6398
|
return;
|
|
6158
6399
|
}
|
|
6400
|
+
if (action !== 'cancel' && await this.refreshRepairedAuthChoice(event, record, candidate.name, locale))
|
|
6401
|
+
return;
|
|
6159
6402
|
if (action === 'cancel') {
|
|
6160
6403
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
6161
6404
|
const state = await this.listCodexAuthState();
|
|
@@ -11811,6 +12054,12 @@ function isThreadActiveWriterError(error) {
|
|
|
11811
12054
|
function isNoActiveTurnToSteerError(error) {
|
|
11812
12055
|
return error instanceof Error && /no active turn to steer/i.test(error.message);
|
|
11813
12056
|
}
|
|
12057
|
+
function isThreadQueueUnsupportedError(error) {
|
|
12058
|
+
const message = formatUserError(error).toLowerCase();
|
|
12059
|
+
return message.includes('method not found')
|
|
12060
|
+
|| message.includes('unknown method')
|
|
12061
|
+
|| message.includes('thread/queue/add') && message.includes('not supported');
|
|
12062
|
+
}
|
|
11814
12063
|
function isThreadNewCwdCreateConfirmation(text) {
|
|
11815
12064
|
return /^(y|yes|ok|okay|confirm|create|mkdir|确定|确认|创建|新建|好)$/i.test(text.trim());
|
|
11816
12065
|
}
|
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.";
|
|
@@ -309,7 +321,9 @@ declare const MESSAGES: {
|
|
|
309
321
|
readonly watch_already_enabled: "Already watching thread {threadId}.";
|
|
310
322
|
readonly watch_started_active: "Watching thread {threadId}. Live Codex updates from the current turn will appear here.";
|
|
311
323
|
readonly watch_started_idle: "Watching thread {threadId}. I will mirror the next live turn from Codex CLI here.";
|
|
312
|
-
readonly watch_read_only_active: "This chat is watching that turn read-only.
|
|
324
|
+
readonly 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.";
|
|
325
|
+
readonly watch_prompt_queued: "Queued for the watched Codex CLI session ({id}). The CLI will continue with it after the current turn.";
|
|
326
|
+
readonly watch_queue_unsupported: "This Codex version does not support cross-client session queues. Update Codex to 0.151.0 or later.";
|
|
313
327
|
readonly watch_not_enabled: "This chat is not watching any thread.";
|
|
314
328
|
readonly watch_stopped: "Stopped watching thread {threadId}.";
|
|
315
329
|
readonly queued_prompt_set: "Queued #{position} ({id}). I will send it after the current turn finishes.";
|
|
@@ -778,6 +792,8 @@ declare const MESSAGES: {
|
|
|
778
792
|
readonly cmd_desc_account: "Codex 账号";
|
|
779
793
|
readonly cmd_desc_quota: "Codex 用量";
|
|
780
794
|
readonly cmd_desc_login_device: "ChatGPT 设备登录";
|
|
795
|
+
readonly cmd_desc_login_cancel: "取消当前登录流程";
|
|
796
|
+
readonly cmd_desc_cli: "从本机 CLI 继续当前会话";
|
|
781
797
|
readonly cmd_desc_threads: "最近线程";
|
|
782
798
|
readonly cmd_desc_open: "打开缓存线程";
|
|
783
799
|
readonly cmd_desc_goal: "线程目标";
|
|
@@ -972,13 +988,13 @@ declare const MESSAGES: {
|
|
|
972
988
|
readonly auth_sync_test_sent: "auth sync 测试完成:已发送 {sent},收到回应 {replied}。";
|
|
973
989
|
readonly auth_sync_test_missing: "未回应:{value}";
|
|
974
990
|
readonly auth_sync_push_blocked_active: "当前有任一 runtime、审批、待输入、登录或 auth 镜像写入在进行中,不能推送 auth 同步。";
|
|
975
|
-
readonly auth_sync_safe_starting: "
|
|
991
|
+
readonly auth_sync_safe_starting: "正在自检各节点并分发有效授权。节点未回应时,每轮自检最多等待 5 分钟;可用 /auth sync status 查看进展。";
|
|
976
992
|
readonly auth_sync_safe_done: "安全 auth 同步完成:本机同步 {localSynced},本机跳过 {localSkipped};跨节点发送 {sent},跳过 {skipped}。";
|
|
977
993
|
readonly auth_cluster_audit_starting: "正在通知所有 auth sync 节点逐账号自检,协商最新有效凭据,并由本节点刷新已满 8 天的 auth...";
|
|
978
994
|
readonly auth_cluster_audit_blocked_active: "集群 auth 自检要求本机所有 runtime、审批、待输入、登录和 auth 镜像写入均为空闲。";
|
|
979
995
|
readonly auth_cluster_audit_failed: "集群 auth 自检失败:{error}";
|
|
980
996
|
readonly auth_cluster_audit_summary: "集群 auth 自检:节点 {responded}/{expected};检查账号 {checked},有效 {valid},无有效副本 {invalid}。";
|
|
981
|
-
readonly auth_cluster_audit_synced: "
|
|
997
|
+
readonly auth_cluster_audit_synced: "已选出并发送有效授权(远端导入尚未确认):{value}";
|
|
982
998
|
readonly auth_cluster_audit_repair: "多节点确认无效,已标记问号等待人工处理:{value}";
|
|
983
999
|
readonly auth_cluster_audit_missing: "未回应节点:{value}";
|
|
984
1000
|
readonly auth_cluster_audit_busy: "忙碌节点:{value}";
|
|
@@ -1027,7 +1043,17 @@ declare const MESSAGES: {
|
|
|
1027
1043
|
readonly working: "处理中...";
|
|
1028
1044
|
readonly usage_open: "用法:/open <编号>";
|
|
1029
1045
|
readonly usage_watch: "用法:/watch [编号]";
|
|
1030
|
-
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} 的写入权,准备发送指定消息;这不代表任务已完成。";
|
|
1031
1057
|
readonly usage_queue: "用法:/queue <消息>";
|
|
1032
1058
|
readonly unknown_cached_thread: "找不到这个缓存线程,请先运行 /threads。";
|
|
1033
1059
|
readonly cached_thread_unavailable: "这个缓存线程已经不可用,请重新运行 /threads。";
|
|
@@ -1035,7 +1061,9 @@ declare const MESSAGES: {
|
|
|
1035
1061
|
readonly watch_already_enabled: "已经在观察线程 {threadId}。";
|
|
1036
1062
|
readonly watch_started_active: "已开始观察线程 {threadId}。当前这轮 Codex 更新会同步到这里。";
|
|
1037
1063
|
readonly watch_started_idle: "已开始观察线程 {threadId}。之后 CLI 里这条线程一旦有新 live turn,我会同步到这里。";
|
|
1038
|
-
readonly watch_read_only_active: "
|
|
1064
|
+
readonly watch_read_only_active: "当前聊天正在只读观察这轮,不能接管这项操作。直接发送文字或使用 /queue 可排入下一轮;要另行启动请先 /unwatch。";
|
|
1065
|
+
readonly watch_prompt_queued: "已排入正在观察的 Codex CLI 会话({id})。当前轮结束后,CLI 会自动接着执行。";
|
|
1066
|
+
readonly watch_queue_unsupported: "当前 Codex 不支持跨客户端会话队列,请升级到 Codex 0.151.0 或更高版本。";
|
|
1039
1067
|
readonly watch_not_enabled: "当前没有正在观察的线程。";
|
|
1040
1068
|
readonly watch_stopped: "已停止观察线程 {threadId}。";
|
|
1041
1069
|
readonly queued_prompt_set: "已排队 #{position}({id})。当前回复结束后我会自动发送。";
|