@foxden-app/foxclaw 0.7.2 → 0.10.0
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 +14 -0
- package/CHANGELOG.md +129 -0
- package/README.md +22 -2
- package/README_EN.md +22 -2
- package/dist/antigravity/adapter.d.ts +17 -0
- package/dist/antigravity/adapter.js +256 -0
- package/dist/antigravity/auth.d.ts +121 -0
- package/dist/antigravity/auth.js +896 -0
- package/dist/antigravity/client.d.ts +32 -0
- package/dist/antigravity/client.js +238 -0
- package/dist/antigravity/controller.d.ts +96 -0
- package/dist/antigravity/controller.js +1922 -0
- package/dist/antigravity/conversations.d.ts +29 -0
- package/dist/antigravity/conversations.js +289 -0
- package/dist/antigravity/events.d.ts +101 -0
- package/dist/antigravity/events.js +114 -0
- package/dist/antigravity/runtime.d.ts +47 -0
- package/dist/antigravity/runtime.js +82 -0
- package/dist/auth/cross_node_sync.js +2 -2
- package/dist/channels/telegram/telegram_channel_adapter.d.ts +10 -3
- package/dist/channels/telegram/telegram_channel_adapter.js +2 -1
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +5 -1
- package/dist/codex_app/adapter.d.ts +20 -0
- package/dist/codex_app/adapter.js +213 -0
- package/dist/codex_app/client.d.ts +2 -1
- package/dist/codex_app/client.js +47 -4
- package/dist/codex_app/force_takeover.d.ts +12 -0
- package/dist/codex_app/force_takeover.js +27 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.js +42 -0
- package/dist/controller/controller.d.ts +15 -5
- package/dist/controller/controller.js +252 -20
- package/dist/core/attachments.d.ts +11 -0
- package/dist/core/attachments.js +56 -0
- package/dist/core/engine_spi.d.ts +76 -0
- package/dist/core/engine_spi.js +1 -0
- package/dist/core/orchestrator.d.ts +101 -0
- package/dist/core/orchestrator.js +1404 -0
- package/dist/core/stream_preview.d.ts +29 -0
- package/dist/core/stream_preview.js +146 -0
- package/dist/core/turn_queue.d.ts +19 -0
- package/dist/core/turn_queue.js +41 -0
- package/dist/i18n.d.ts +36 -6
- package/dist/i18n.js +81 -6
- package/dist/main.js +190 -20
- package/dist/opencode/adapter.d.ts +11 -0
- package/dist/opencode/adapter.js +168 -0
- package/dist/store/database.d.ts +41 -0
- package/dist/store/database.js +242 -43
- package/dist/store/token_usage.d.ts +23 -0
- package/dist/store/token_usage.js +61 -0
- package/dist/telegram/api.js +34 -1
- package/dist/telegram/bot_home.d.ts +3 -0
- package/dist/telegram/bot_home.js +107 -0
- package/dist/telegram/gateway.d.ts +5 -0
- package/dist/telegram/gateway.js +21 -0
- package/dist/types.d.ts +1 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +78 -6
- package/dist/voice/target.js +2 -1
- package/dist/voice/tts.js +4 -2
- package/docs/devlogs//346/212/200/346/234/257/351/200/232/350/256/257/02./347/273/237/344/270/200/351/200/232/351/201/223/350/260/203/345/272/246/345/231/250/344/270/216/345/217/257/346/217/222/346/213/224/345/274/225/346/223/216SPI/357/274/232FoxClaw/344/273/216/345/215/225/344/275/223/350/265/260/345/220/221/345/244/232Agent/345/272/225/345/272/247.md +192 -0
- package/docs/devlogs//347/254/254/344/270/200/344/272/272/347/247/260/347/211/210/02./347/257/207/344/272/214_/345/275/223/344/270/200/344/270/252/346/241/245/346/216/245/345/231/250/345/274/200/345/247/213/345/255/246/344/274/232/345/220/254/346/207/202/346/211/200/346/234/211Agent.md +45 -0
- 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 +2 -2
- 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';
|
|
@@ -25,6 +26,7 @@ import { normalizeAccessPreset, resolveAccessMode } from './access.js';
|
|
|
25
26
|
import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
|
|
26
27
|
import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './session_observer.js';
|
|
27
28
|
import { renderActiveTurnStatus } from './status.js';
|
|
29
|
+
import { formatMetricTokenCount } from '../store/token_usage.js';
|
|
28
30
|
import { writeRuntimeStatus } from '../runtime.js';
|
|
29
31
|
const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
|
|
30
32
|
const RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS = [3000, 7000, 15_000, 30_000, 45_000, 60_000, 60_000, 60_000];
|
|
@@ -64,6 +66,8 @@ const DYNAMIC_HELP_COMMANDS = [
|
|
|
64
66
|
{ key: 'quota', line: '/quota' },
|
|
65
67
|
{ key: 'update', line: '/update' },
|
|
66
68
|
{ key: 'login_device', line: '/login_device' },
|
|
69
|
+
{ key: 'login_cancel', line: '/login_cancel' },
|
|
70
|
+
{ key: 'cli', line: '/cli' },
|
|
67
71
|
{ key: 'threads_archived', line: '/threads archived [query]' },
|
|
68
72
|
{ key: 'open', line: '/open <n>' },
|
|
69
73
|
{ key: 'goal', line: '/goal [objective|pause|resume|done|budget <tokens|off>|clear confirm]' },
|
|
@@ -122,6 +126,9 @@ export class BridgeSessionCore {
|
|
|
122
126
|
pendingUserInputs = new Map();
|
|
123
127
|
pendingMcpElicitations = new Map();
|
|
124
128
|
pendingLoginsByScope = new Map();
|
|
129
|
+
externalWriterControl = externalWriterControl;
|
|
130
|
+
forceTakeoversInProgress = new Set();
|
|
131
|
+
pendingForceTakeovers = new Map();
|
|
125
132
|
pendingLoginScopesById = new Map();
|
|
126
133
|
pendingAuthAddsByLoginId = new Map();
|
|
127
134
|
latestTurnDiffs = new Map();
|
|
@@ -182,9 +189,7 @@ export class BridgeSessionCore {
|
|
|
182
189
|
this.updateStatus();
|
|
183
190
|
});
|
|
184
191
|
this.bot.on('text', (event) => {
|
|
185
|
-
|
|
186
|
-
void this.handleAsyncError('telegram.text', error, event.scopeId);
|
|
187
|
-
});
|
|
192
|
+
this.dispatchInboundLikeTelegramText(event);
|
|
188
193
|
});
|
|
189
194
|
this.bot.on('callback', (event) => {
|
|
190
195
|
void this.handleCallback(event).catch((error) => {
|
|
@@ -197,10 +202,23 @@ export class BridgeSessionCore {
|
|
|
197
202
|
* (used by the Weixin adapter).
|
|
198
203
|
*/
|
|
199
204
|
dispatchInboundLikeTelegramText(event) {
|
|
200
|
-
|
|
205
|
+
const command = event.attachments.length === 0 ? parseCommand(event.text) : null;
|
|
206
|
+
const name = command?.name;
|
|
207
|
+
// Recovery commands must remain reachable while a normal command is waiting.
|
|
208
|
+
// handleText still performs normal addressing and authorization checks.
|
|
209
|
+
const urgent = name && (['status', 'cli', 'login_cancel', 'interrupt'].includes(name)
|
|
210
|
+
|| (name === 'auth' && command?.args[0] === 'sync' && ['status', 'events', 'trace'].includes(command.args[1] ?? 'status')));
|
|
211
|
+
const task = urgent ? this.handleText(event) : this.withLock(event.scopeId, async () => this.handleText(event));
|
|
212
|
+
void task.catch((error) => {
|
|
201
213
|
void this.handleAsyncError('channel.text', error, event.scopeId);
|
|
202
214
|
});
|
|
203
215
|
}
|
|
216
|
+
hasPendingInteraction(scopeId) {
|
|
217
|
+
return this.pendingThreadRenames.has(scopeId)
|
|
218
|
+
|| this.pendingThreadNewCwds.has(scopeId)
|
|
219
|
+
|| this.pendingUserInputs.has(scopeId)
|
|
220
|
+
|| this.pendingForceTakeovers.has(scopeId);
|
|
221
|
+
}
|
|
204
222
|
/** Start Codex app-server transport and attach RPC listeners. */
|
|
205
223
|
async startCodexApp() {
|
|
206
224
|
this.stopping = false;
|
|
@@ -230,6 +248,15 @@ export class BridgeSessionCore {
|
|
|
230
248
|
this.attachedThreads.clear();
|
|
231
249
|
this.threadTokenUsageAlerts.clear();
|
|
232
250
|
if (!this.stopping) {
|
|
251
|
+
if (!this.codexReconnectPending) {
|
|
252
|
+
for (const scopeId of new Set([...this.activeTurns.values()].filter(active => !active.isObserved).map(active => active.scopeId))) {
|
|
253
|
+
const locale = this.localeForChat(scopeId);
|
|
254
|
+
void this.sendMessage(scopeId, locale === 'zh'
|
|
255
|
+
? 'Codex 连接中断,正在重连。任务结果尚未确认,请先用 /status 检查;也可用 /cli 从本机继续。'
|
|
256
|
+
: 'Codex disconnected; reconnecting. The task result is unconfirmed. Check /status or use /cli locally.')
|
|
257
|
+
.catch(error => this.logger.warn('codex.disconnect_notice_failed', { scopeId, error: toErrorMeta(error) }));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
233
260
|
this.codexReconnectPending = true;
|
|
234
261
|
this.pauseAppSnapshotWatchers();
|
|
235
262
|
}
|
|
@@ -304,12 +331,13 @@ export class BridgeSessionCore {
|
|
|
304
331
|
this.updateStatus();
|
|
305
332
|
}
|
|
306
333
|
getRuntimeStatus() {
|
|
334
|
+
const appServer = typeof this.app.getServerStatus === 'function' ? this.app.getServerStatus() : null;
|
|
307
335
|
return {
|
|
308
336
|
running: true,
|
|
309
|
-
connected: this.app.isConnected(),
|
|
310
|
-
userAgent: this.app.getUserAgent(),
|
|
337
|
+
connected: typeof this.app.isConnected === 'function' ? this.app.isConnected() : false,
|
|
338
|
+
userAgent: typeof this.app.getUserAgent === 'function' ? this.app.getUserAgent() : null,
|
|
311
339
|
codexHome: this.config.codexHome ?? path.join(os.homedir(), '.codex'),
|
|
312
|
-
codexAppServer:
|
|
340
|
+
...(appServer ? { codexAppServer: appServer } : {}),
|
|
313
341
|
botUsername: this.botUsername,
|
|
314
342
|
currentBindings: this.store.countBindings(),
|
|
315
343
|
pendingApprovals: this.store.countPendingApprovals(),
|
|
@@ -539,6 +567,9 @@ export class BridgeSessionCore {
|
|
|
539
567
|
if (codexUpdateLine) {
|
|
540
568
|
lines.push(t(locale, 'status_last_codex_update', { value: codexUpdateLine }));
|
|
541
569
|
}
|
|
570
|
+
if (lastUpdate.agyUpdate) {
|
|
571
|
+
lines.push(locale === 'zh' ? `Antigravity CLI:${lastUpdate.agyUpdate}` : `Antigravity CLI: ${lastUpdate.agyUpdate}`);
|
|
572
|
+
}
|
|
542
573
|
}
|
|
543
574
|
else {
|
|
544
575
|
lines.push(t(locale, 'status_last_update_none'));
|
|
@@ -582,6 +613,20 @@ export class BridgeSessionCore {
|
|
|
582
613
|
await this.handleLoginCancelCommand(scopeId, locale, args);
|
|
583
614
|
return;
|
|
584
615
|
}
|
|
616
|
+
case 'cli': {
|
|
617
|
+
const binding = this.store.getBinding(scopeId);
|
|
618
|
+
const server = this.app.getServerStatus();
|
|
619
|
+
if (!server.running || !server.port) {
|
|
620
|
+
await this.sendMessage(scopeId, locale === 'zh' ? 'Codex 服务不可用,请在本机检查 foxclaw status。' : 'Codex server unavailable. Check foxclaw status locally.');
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const threadArg = binding ? ` ${JSON.stringify(binding.threadId)}` : '';
|
|
624
|
+
await this.sendMessage(scopeId, (locale === 'zh'
|
|
625
|
+
? '在运行桥的这台机器上执行,进入同一个 Codex 服务后可中断或继续会话:\n'
|
|
626
|
+
: 'Run on the bridge host to interrupt or continue in the same Codex server:\n')
|
|
627
|
+
+ `codex resume --remote ws://127.0.0.1:${server.port}${threadArg}`);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
585
630
|
case 'logout': {
|
|
586
631
|
await this.handleLogoutCommand(scopeId, locale, args);
|
|
587
632
|
return;
|
|
@@ -1072,6 +1117,21 @@ export class BridgeSessionCore {
|
|
|
1072
1117
|
async handleCallback(event) {
|
|
1073
1118
|
const scopeId = event.scopeId;
|
|
1074
1119
|
const locale = this.localeForChat(scopeId, event.languageCode);
|
|
1120
|
+
if (this.forceTakeoversInProgress.has(scopeId)) {
|
|
1121
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'force_takeover_busy'));
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const forceTakeoverMatch = /^takeover:(confirm|cancel):([a-f0-9]+)$/.exec(event.data);
|
|
1125
|
+
if (forceTakeoverMatch) {
|
|
1126
|
+
await this.handleForceTakeoverCallback(event, locale, forceTakeoverMatch[1], forceTakeoverMatch[2]);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
const loginCancelMatch = /^login:cancel:(.+)$/.exec(event.data);
|
|
1130
|
+
if (loginCancelMatch) {
|
|
1131
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'button_cancel'));
|
|
1132
|
+
await this.handleLoginCancelCommand(scopeId, locale, [loginCancelMatch[1]]);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1075
1135
|
const interruptMatch = /^turn:interrupt:(.+)$/.exec(event.data);
|
|
1076
1136
|
if (interruptMatch) {
|
|
1077
1137
|
await this.handleTurnInterruptCallback(event, interruptMatch[1], locale);
|
|
@@ -3432,7 +3492,9 @@ export class BridgeSessionCore {
|
|
|
3432
3492
|
this.coordinator.statusUpdated(status);
|
|
3433
3493
|
return;
|
|
3434
3494
|
}
|
|
3435
|
-
|
|
3495
|
+
if (this.config.statusPath) {
|
|
3496
|
+
writeRuntimeStatus(this.config.statusPath, status);
|
|
3497
|
+
}
|
|
3436
3498
|
}
|
|
3437
3499
|
async sendMessage(scopeId, text, inlineKeyboard) {
|
|
3438
3500
|
return this.sendRichMarkdownMessage(scopeId, text, inlineKeyboard);
|
|
@@ -3654,7 +3716,9 @@ export class BridgeSessionCore {
|
|
|
3654
3716
|
if (!scopeId)
|
|
3655
3717
|
return;
|
|
3656
3718
|
try {
|
|
3657
|
-
|
|
3719
|
+
const locale = this.localeForChat(scopeId);
|
|
3720
|
+
const hint = isThreadActiveWriterError(error) ? `\n${t(locale, 'force_takeover_hint')}` : '';
|
|
3721
|
+
await this.sendMessage(scopeId, t(locale, 'bridge_error', { error: formatUserError(error) }) + hint);
|
|
3658
3722
|
}
|
|
3659
3723
|
catch (notifyError) {
|
|
3660
3724
|
this.logger.error('telegram.error_notification_failed', { error: toErrorMeta(notifyError), scopeId });
|
|
@@ -3887,7 +3951,7 @@ export class BridgeSessionCore {
|
|
|
3887
3951
|
}
|
|
3888
3952
|
}
|
|
3889
3953
|
for (const active of [...this.activeTurns.values()]) {
|
|
3890
|
-
if (recoveredScopes.has(active.scopeId) || !this.getActiveTurn(active.scopeId, active.turnId)) {
|
|
3954
|
+
if (active.isObserved || recoveredScopes.has(active.scopeId) || !this.getActiveTurn(active.scopeId, active.turnId)) {
|
|
3891
3955
|
continue;
|
|
3892
3956
|
}
|
|
3893
3957
|
try {
|
|
@@ -3900,6 +3964,10 @@ export class BridgeSessionCore {
|
|
|
3900
3964
|
turnId: active.turnId,
|
|
3901
3965
|
error: toErrorMeta(error),
|
|
3902
3966
|
});
|
|
3967
|
+
await this.sendMessage(active.scopeId, this.localeForChat(active.scopeId) === 'zh'
|
|
3968
|
+
? '连接已恢复,但无法确认原任务状态。任务未自动重发,请用 /cli 检查原会话后再继续。'
|
|
3969
|
+
: 'Connected again, but the original task state could not be confirmed. It was not resent. Use /cli to inspect the session.')
|
|
3970
|
+
.catch(notifyError => this.logger.warn('codex.recovery_notice_failed', { error: toErrorMeta(notifyError) }));
|
|
3903
3971
|
}
|
|
3904
3972
|
}
|
|
3905
3973
|
await this.recoverQueuedTurns();
|
|
@@ -4246,6 +4314,10 @@ export class BridgeSessionCore {
|
|
|
4246
4314
|
}
|
|
4247
4315
|
}
|
|
4248
4316
|
async handleTakeoverCommand(event, locale, args) {
|
|
4317
|
+
if (args[0] === '--force') {
|
|
4318
|
+
await this.prepareForceTakeover(event, locale, args.slice(1).join(' ').trim());
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4249
4321
|
const scopeId = event.scopeId;
|
|
4250
4322
|
const nextPrompt = args.join(' ').trim();
|
|
4251
4323
|
if (!nextPrompt) {
|
|
@@ -4263,10 +4335,113 @@ export class BridgeSessionCore {
|
|
|
4263
4335
|
await this.requestInterrupt(active);
|
|
4264
4336
|
}
|
|
4265
4337
|
await this.sendMessage(scopeId, t(locale, 'interrupt_requested_waiting'));
|
|
4266
|
-
|
|
4338
|
+
let timer;
|
|
4339
|
+
try {
|
|
4340
|
+
await Promise.race([
|
|
4341
|
+
active.completion,
|
|
4342
|
+
new Promise((_, reject) => {
|
|
4343
|
+
timer = setTimeout(() => reject(new Error(locale === 'zh'
|
|
4344
|
+
? '等待中断完成超时,尚未发送新任务。请用 /status 检查,或用 /cli 从终端恢复。'
|
|
4345
|
+
: 'Interrupt confirmation timed out. No replacement task was sent. Check /status or use /cli to recover.')), 30_000);
|
|
4346
|
+
}),
|
|
4347
|
+
]);
|
|
4348
|
+
}
|
|
4349
|
+
finally {
|
|
4350
|
+
clearTimeout(timer);
|
|
4351
|
+
}
|
|
4267
4352
|
}
|
|
4268
4353
|
await this.startBoundTurnFromEvent(event, locale, nextPrompt);
|
|
4269
4354
|
}
|
|
4355
|
+
async prepareForceTakeover(event, locale, prompt) {
|
|
4356
|
+
const scopeId = event.scopeId;
|
|
4357
|
+
this.pendingForceTakeovers.delete(scopeId);
|
|
4358
|
+
if (!prompt) {
|
|
4359
|
+
await this.sendMessage(scopeId, t(locale, 'usage_takeover'));
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
if (!scopeId.startsWith('telegram:') || event.userId !== this.config.tgAllowedUserId) {
|
|
4363
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_trusted_telegram_only'));
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
const binding = this.store.getBinding(scopeId);
|
|
4367
|
+
if (!binding) {
|
|
4368
|
+
await this.sendMessage(scopeId, t(locale, 'watch_no_thread_bound'));
|
|
4369
|
+
return;
|
|
4370
|
+
}
|
|
4371
|
+
const home = this.config.codexHome ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex');
|
|
4372
|
+
try {
|
|
4373
|
+
const writer = await this.externalWriterControl.inspect(home, binding.threadId);
|
|
4374
|
+
const id = crypto.randomBytes(8).toString('hex');
|
|
4375
|
+
this.pendingForceTakeovers.set(scopeId, {
|
|
4376
|
+
id, expiresAt: Date.now() + 60_000, event, threadId: binding.threadId, home, prompt, writer,
|
|
4377
|
+
});
|
|
4378
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_confirm', {
|
|
4379
|
+
threadId: binding.threadId, pid: String(writer.pid), cwd: writer.cwd, prompt,
|
|
4380
|
+
}), [[
|
|
4381
|
+
{ text: t(locale, 'button_force_takeover'), callback_data: `takeover:confirm:${id}` },
|
|
4382
|
+
{ text: t(locale, 'button_cancel'), callback_data: `takeover:cancel:${id}` },
|
|
4383
|
+
]]);
|
|
4384
|
+
}
|
|
4385
|
+
catch (error) {
|
|
4386
|
+
this.pendingForceTakeovers.delete(scopeId);
|
|
4387
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_failed', { error: formatUserError(error) }));
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
async handleForceTakeoverCallback(event, locale, action, id) {
|
|
4391
|
+
const scopeId = event.scopeId;
|
|
4392
|
+
const pending = this.pendingForceTakeovers.get(scopeId);
|
|
4393
|
+
if (!pending || pending.id !== id || pending.event.userId !== event.userId
|
|
4394
|
+
|| event.userId !== this.config.tgAllowedUserId || pending.expiresAt <= Date.now()) {
|
|
4395
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'force_takeover_expired'));
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
// Claim before any await: duplicate callbacks cannot stop or submit twice.
|
|
4399
|
+
this.pendingForceTakeovers.delete(scopeId);
|
|
4400
|
+
await this.messaging.answerCallback(event.callbackQueryId, t(locale, action === 'cancel' ? 'button_cancel' : 'button_force_takeover'));
|
|
4401
|
+
if (action === 'cancel') {
|
|
4402
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_cancelled'));
|
|
4403
|
+
return;
|
|
4404
|
+
}
|
|
4405
|
+
await this.withLock(scopeId, async () => {
|
|
4406
|
+
if (pending.expiresAt <= Date.now() || this.store.getBinding(scopeId)?.threadId !== pending.threadId) {
|
|
4407
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_expired'));
|
|
4408
|
+
return;
|
|
4409
|
+
}
|
|
4410
|
+
if (this.externalAuthValidationInProgress) {
|
|
4411
|
+
await this.sendMessage(scopeId, t(locale, 'auth_sync_validation_busy'));
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
this.forceTakeoversInProgress.add(scopeId);
|
|
4415
|
+
this.turnStartInProgress += 1;
|
|
4416
|
+
try {
|
|
4417
|
+
try {
|
|
4418
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_stopping', { pid: String(pending.writer.pid) }));
|
|
4419
|
+
await this.externalWriterControl.stop(pending.home, pending.threadId, pending.writer);
|
|
4420
|
+
this.logger.warn('codex.external_writer_stopped', { scopeId, threadId: pending.threadId, pid: pending.writer.pid });
|
|
4421
|
+
await this.stopWatchingScopeThread(scopeId);
|
|
4422
|
+
this.attachedThreads.delete(attachedThreadKey(scopeId, pending.threadId));
|
|
4423
|
+
// Never replace a missing thread with an unrelated new conversation.
|
|
4424
|
+
const binding = this.store.getBinding(scopeId);
|
|
4425
|
+
if (binding?.threadId !== pending.threadId)
|
|
4426
|
+
throw new Error(t(locale, 'force_takeover_expired'));
|
|
4427
|
+
await this.ensureThreadReady(scopeId, binding, { recoverMissingThread: false });
|
|
4428
|
+
}
|
|
4429
|
+
catch (error) {
|
|
4430
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_failed', { error: formatUserError(error) }));
|
|
4431
|
+
return;
|
|
4432
|
+
}
|
|
4433
|
+
this.store.cancelQueuedTurnInputs(scopeId);
|
|
4434
|
+
await this.sendMessage(scopeId, t(locale, 'force_takeover_acquired', { threadId: pending.threadId }));
|
|
4435
|
+
// Normal turn-start error handling preserves ambiguous submission failures;
|
|
4436
|
+
// do not automatically retry a possibly accepted prompt.
|
|
4437
|
+
await this.startBoundTurnFromEvent(pending.event, locale, pending.prompt);
|
|
4438
|
+
}
|
|
4439
|
+
finally {
|
|
4440
|
+
this.forceTakeoversInProgress.delete(scopeId);
|
|
4441
|
+
this.turnStartInProgress -= 1;
|
|
4442
|
+
}
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4270
4445
|
async handleQueueCommand(event, locale, args) {
|
|
4271
4446
|
const scopeId = event.scopeId;
|
|
4272
4447
|
const nextPrompt = args.join(' ').trim();
|
|
@@ -4882,6 +5057,9 @@ export class BridgeSessionCore {
|
|
|
4882
5057
|
const rows = [
|
|
4883
5058
|
['FoxClaw', `${status.fromVersion} -> ${status.toVersion ?? t(status.locale, 'unknown')}`, status.locale === 'zh' ? '升级完成,服务已重启' : 'Updated; service restarted'],
|
|
4884
5059
|
['Codex CLI', formatSelfUpdateVersionTransition(status.codexFromVersion, status.codexToVersion, status.locale), formatCodexUpdateState(status)],
|
|
5060
|
+
...(status.agyFromVersion || status.agyUpdate ? [
|
|
5061
|
+
['Antigravity (AGY)', formatSelfUpdateVersionTransition(status.agyFromVersion ?? null, status.agyToVersion ?? null, status.locale), status.agyUpdate ?? (status.locale === 'zh' ? '已检查更新' : 'Checked')]
|
|
5062
|
+
] : []),
|
|
4885
5063
|
[status.locale === 'zh' ? '集群广播' : 'Cluster broadcast', status.toVersion ?? t(status.locale, 'unknown'), broadcastLine],
|
|
4886
5064
|
];
|
|
4887
5065
|
const notes = status.releaseNotes?.filter(note => note.trim()) ?? [];
|
|
@@ -4899,6 +5077,7 @@ export class BridgeSessionCore {
|
|
|
4899
5077
|
telegramBold(title),
|
|
4900
5078
|
escapeTelegramHtml(foxclawResult),
|
|
4901
5079
|
escapeTelegramHtml(codexUpdateLine ?? (status.locale === 'zh' ? 'Codex CLI:未执行升级。' : 'Codex CLI: not updated.')),
|
|
5080
|
+
...(status.agyUpdate ? [escapeTelegramHtml(`Antigravity CLI:${status.agyUpdate}`)] : []),
|
|
4902
5081
|
escapeTelegramHtml(broadcastLine),
|
|
4903
5082
|
releaseNotes ? escapeTelegramHtml(releaseNotes) : '',
|
|
4904
5083
|
].filter(Boolean).join('\n');
|
|
@@ -5376,7 +5555,7 @@ export class BridgeSessionCore {
|
|
|
5376
5555
|
t(locale, 'login_code', { value: login.userCode }),
|
|
5377
5556
|
t(locale, 'login_id', { value: login.loginId }),
|
|
5378
5557
|
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
5379
|
-
].join('\n'));
|
|
5558
|
+
].join('\n'), this.loginCancelKeyboard(login.loginId, locale));
|
|
5380
5559
|
}
|
|
5381
5560
|
catch (error) {
|
|
5382
5561
|
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, state.currentTargetPath);
|
|
@@ -5419,7 +5598,7 @@ export class BridgeSessionCore {
|
|
|
5419
5598
|
t(locale, 'login_code', { value: login.userCode }),
|
|
5420
5599
|
t(locale, 'login_id', { value: login.loginId }),
|
|
5421
5600
|
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
5422
|
-
].join('\n'));
|
|
5601
|
+
].join('\n'), this.loginCancelKeyboard(login.loginId, locale));
|
|
5423
5602
|
}
|
|
5424
5603
|
catch (error) {
|
|
5425
5604
|
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, state.currentTargetPath);
|
|
@@ -5584,6 +5763,11 @@ export class BridgeSessionCore {
|
|
|
5584
5763
|
}
|
|
5585
5764
|
}
|
|
5586
5765
|
async handleLoginDeviceCommand(scopeId, locale) {
|
|
5766
|
+
const pendingLoginId = this.pendingLoginsByScope.get(scopeId);
|
|
5767
|
+
if (pendingLoginId) {
|
|
5768
|
+
await this.sendMessage(scopeId, t(locale, 'login_cancel_hint', { value: pendingLoginId }), this.loginCancelKeyboard(pendingLoginId, locale));
|
|
5769
|
+
return;
|
|
5770
|
+
}
|
|
5587
5771
|
const login = await this.app.startDeviceLogin();
|
|
5588
5772
|
const oldLoginId = this.pendingLoginsByScope.get(scopeId);
|
|
5589
5773
|
if (oldLoginId) {
|
|
@@ -5598,24 +5782,41 @@ export class BridgeSessionCore {
|
|
|
5598
5782
|
t(locale, 'login_code', { value: login.userCode }),
|
|
5599
5783
|
t(locale, 'login_id', { value: login.loginId }),
|
|
5600
5784
|
t(locale, 'login_cancel_hint', { value: login.loginId }),
|
|
5601
|
-
].join('\n'));
|
|
5785
|
+
].join('\n'), this.loginCancelKeyboard(login.loginId, locale));
|
|
5786
|
+
}
|
|
5787
|
+
loginCancelKeyboard(loginId, locale) {
|
|
5788
|
+
return [[{ text: t(locale, 'button_cancel'), callback_data: `login:cancel:${loginId}` }]];
|
|
5602
5789
|
}
|
|
5603
5790
|
async handleLoginCancelCommand(scopeId, locale, args) {
|
|
5604
5791
|
const loginId = args[0]?.trim() || this.pendingLoginsByScope.get(scopeId) || null;
|
|
5605
|
-
if (!loginId) {
|
|
5792
|
+
if (!loginId || this.pendingLoginsByScope.get(scopeId) !== loginId) {
|
|
5606
5793
|
await this.sendMessage(scopeId, t(locale, 'login_cancel_no_pending'));
|
|
5607
5794
|
return;
|
|
5608
5795
|
}
|
|
5609
5796
|
const pendingAuthAdd = this.pendingAuthAddsByLoginId.get(loginId) ?? null;
|
|
5610
|
-
|
|
5797
|
+
// Claim cancellation before awaiting RPC: completion notifications may arrive first.
|
|
5611
5798
|
this.pendingLoginsByScope.delete(scopeId);
|
|
5612
5799
|
this.pendingLoginScopesById.delete(loginId);
|
|
5613
5800
|
this.pendingAuthAddsByLoginId.delete(loginId);
|
|
5801
|
+
let cancelError;
|
|
5802
|
+
try {
|
|
5803
|
+
await this.app.cancelLogin(loginId);
|
|
5804
|
+
}
|
|
5805
|
+
catch (error) {
|
|
5806
|
+
cancelError = error;
|
|
5807
|
+
this.logger.warn('codex.login_cancel_failed', { error: toErrorMeta(error) });
|
|
5808
|
+
}
|
|
5614
5809
|
if (pendingAuthAdd) {
|
|
5615
5810
|
await this.restorePendingAuthAdd(pendingAuthAdd);
|
|
5616
5811
|
await this.sendMessage(scopeId, t(locale, 'auth_add_cancelled'));
|
|
5617
5812
|
return;
|
|
5618
5813
|
}
|
|
5814
|
+
if (cancelError) {
|
|
5815
|
+
await this.sendMessage(scopeId, locale === 'zh'
|
|
5816
|
+
? '已退出本地登录流程,但未能确认服务端取消。请勿继续使用旧验证码;可重新登录或用 /codex_restart 重置登录服务。'
|
|
5817
|
+
: 'Local login flow cleared, but server cancellation is unconfirmed. Do not use the old code; start a new login or use /codex_restart.');
|
|
5818
|
+
return;
|
|
5819
|
+
}
|
|
5619
5820
|
await this.sendMessage(scopeId, t(locale, 'login_cancelled'));
|
|
5620
5821
|
}
|
|
5621
5822
|
async handleLogoutCommand(scopeId, locale, args) {
|
|
@@ -6162,6 +6363,20 @@ export class BridgeSessionCore {
|
|
|
6162
6363
|
await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
6163
6364
|
}
|
|
6164
6365
|
}
|
|
6366
|
+
async refreshRepairedAuthChoice(event, record, candidateName, locale) {
|
|
6367
|
+
const state = await this.listCodexAuthState();
|
|
6368
|
+
if (state.candidates.find(candidate => candidate.name === candidateName)?.state === 'needs_repair')
|
|
6369
|
+
return false;
|
|
6370
|
+
record.candidates = state.candidates;
|
|
6371
|
+
record.createdAt = Date.now();
|
|
6372
|
+
clampCodexAuthListOffset(record);
|
|
6373
|
+
await this.messaging.answerCallback(event.callbackQueryId, locale === 'zh'
|
|
6374
|
+
? '授权状态已变化,已刷新面板。' : 'Auth state changed; panel refreshed.');
|
|
6375
|
+
if (record.messageId !== null) {
|
|
6376
|
+
await this.editAuthPanelMessage(event.scopeId, record.messageId, renderAuthListMessage(locale, state, this.authDisplayBotLabel(), parseWeixinBridgeScope(event.scopeId) !== null, record), authChoiceKeyboard(locale, record));
|
|
6377
|
+
}
|
|
6378
|
+
return true;
|
|
6379
|
+
}
|
|
6165
6380
|
async handleAuthRepairMenuCallback(event, localId, index, locale) {
|
|
6166
6381
|
const record = this.pendingAuthChoiceLists.get(localId);
|
|
6167
6382
|
if (!record) {
|
|
@@ -6177,6 +6392,8 @@ export class BridgeSessionCore {
|
|
|
6177
6392
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
6178
6393
|
return;
|
|
6179
6394
|
}
|
|
6395
|
+
if (await this.refreshRepairedAuthChoice(event, record, candidate.name, locale))
|
|
6396
|
+
return;
|
|
6180
6397
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'auth_repair_actions_short'));
|
|
6181
6398
|
if (record.messageId !== null) {
|
|
6182
6399
|
await this.editAuthPanelMessage(event.scopeId, record.messageId, t(locale, 'auth_repair_actions_message', { value: formatCodexAuthCandidateDisplayName(candidate.name) }), authRepairKeyboard(locale, record, index));
|
|
@@ -6197,6 +6414,8 @@ export class BridgeSessionCore {
|
|
|
6197
6414
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'unsupported_action'));
|
|
6198
6415
|
return;
|
|
6199
6416
|
}
|
|
6417
|
+
if (action !== 'cancel' && await this.refreshRepairedAuthChoice(event, record, candidate.name, locale))
|
|
6418
|
+
return;
|
|
6200
6419
|
if (action === 'cancel') {
|
|
6201
6420
|
await this.messaging.answerCallback(event.callbackQueryId, t(locale, 'decision_recorded'));
|
|
6202
6421
|
const state = await this.listCodexAuthState();
|
|
@@ -6847,11 +7066,11 @@ export class BridgeSessionCore {
|
|
|
6847
7066
|
events: formatTokenCount(stats.usageEvents),
|
|
6848
7067
|
}),
|
|
6849
7068
|
t(locale, 'status_codex_local_tokens', {
|
|
6850
|
-
total:
|
|
6851
|
-
input:
|
|
7069
|
+
total: formatCodexTokenCountWithMetric(stats.totals.totalTokens),
|
|
7070
|
+
input: formatCodexTokenCountWithMetric(stats.totals.inputTokens),
|
|
6852
7071
|
visible: formatTokenCount(Math.max(0, stats.totals.outputTokens - stats.totals.reasoningOutputTokens)),
|
|
6853
|
-
output:
|
|
6854
|
-
cached:
|
|
7072
|
+
output: formatCodexTokenCountWithMetric(stats.totals.outputTokens),
|
|
7073
|
+
cached: formatCodexTokenCountWithMetric(stats.totals.cachedInputTokens),
|
|
6855
7074
|
reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
|
|
6856
7075
|
}),
|
|
6857
7076
|
...this.formatCodexLocalResponseThroughputStatusLines(locale, stats),
|
|
@@ -6920,6 +7139,13 @@ export class BridgeSessionCore {
|
|
|
6920
7139
|
this.localUsageCache = snapshot;
|
|
6921
7140
|
this.localUsageCacheLoaded = true;
|
|
6922
7141
|
await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
|
|
7142
|
+
this.store.setBackendCumulativeTokenUsage({
|
|
7143
|
+
inputTokens: stats.totals.inputTokens,
|
|
7144
|
+
outputTokens: stats.totals.outputTokens,
|
|
7145
|
+
cachedTokens: stats.totals.cachedInputTokens,
|
|
7146
|
+
totalTokens: stats.totals.totalTokens,
|
|
7147
|
+
turnsCount: stats.turns,
|
|
7148
|
+
}, 'codex');
|
|
6923
7149
|
}
|
|
6924
7150
|
codexLocalUsageSnapshotPath() {
|
|
6925
7151
|
return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME));
|
|
@@ -11829,6 +12055,12 @@ function formatTokenCount(value) {
|
|
|
11829
12055
|
}
|
|
11830
12056
|
return Math.round(value).toLocaleString('en-US');
|
|
11831
12057
|
}
|
|
12058
|
+
function formatCodexTokenCountWithMetric(value) {
|
|
12059
|
+
if (value >= 1_000_000) {
|
|
12060
|
+
return `${formatMetricTokenCount(value)} (${formatTokenCount(value)})`;
|
|
12061
|
+
}
|
|
12062
|
+
return formatTokenCount(value);
|
|
12063
|
+
}
|
|
11832
12064
|
function formatLocalTimestamp(seconds) {
|
|
11833
12065
|
const date = new Date(seconds * 1000);
|
|
11834
12066
|
const pad = (value) => String(value).padStart(2, '0');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Logger } from '../logger.js';
|
|
2
|
+
import { type StagedTelegramAttachment, type TelegramInboundAttachment } from '../telegram/media.js';
|
|
3
|
+
export interface TelegramFileDownloader {
|
|
4
|
+
getFile(fileId: string): Promise<{
|
|
5
|
+
file_id: string;
|
|
6
|
+
file_path?: string;
|
|
7
|
+
file_size?: number;
|
|
8
|
+
}>;
|
|
9
|
+
downloadResolvedFile(filePath: string, destination: string): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
export declare function stageInboundAttachments(downloader: TelegramFileDownloader, cwd: string, threadId: string, attachments: readonly TelegramInboundAttachment[], logger?: Logger): Promise<StagedTelegramAttachment[]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fsPromises from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { planAttachmentStoragePath, isNativeImageAttachment, TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, } from '../telegram/media.js';
|
|
4
|
+
export async function stageInboundAttachments(downloader, cwd, threadId, attachments, logger) {
|
|
5
|
+
const staged = [];
|
|
6
|
+
for (const attachment of attachments) {
|
|
7
|
+
try {
|
|
8
|
+
if (attachment.localPath) {
|
|
9
|
+
const planned = planAttachmentStoragePath(cwd, threadId, attachment, path.basename(attachment.localPath));
|
|
10
|
+
await fsPromises.mkdir(path.dirname(planned.localPath), { recursive: true });
|
|
11
|
+
await fsPromises.copyFile(attachment.localPath, planned.localPath);
|
|
12
|
+
const stat = await fsPromises.stat(planned.localPath);
|
|
13
|
+
const resolvedSize = stat.size;
|
|
14
|
+
staged.push({
|
|
15
|
+
...attachment,
|
|
16
|
+
fileName: planned.fileName,
|
|
17
|
+
fileSize: resolvedSize,
|
|
18
|
+
localPath: planned.localPath,
|
|
19
|
+
relativePath: planned.relativePath,
|
|
20
|
+
nativeImage: isNativeImageAttachment(attachment),
|
|
21
|
+
});
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const remoteFile = await downloader.getFile(attachment.fileId);
|
|
25
|
+
const resolvedSize = attachment.fileSize ?? remoteFile.file_size ?? null;
|
|
26
|
+
if (resolvedSize !== null && resolvedSize > TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES) {
|
|
27
|
+
logger?.warn('attachments.too_large', {
|
|
28
|
+
fileId: attachment.fileId,
|
|
29
|
+
size: resolvedSize,
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!remoteFile.file_path) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const planned = planAttachmentStoragePath(cwd, threadId, attachment, remoteFile.file_path);
|
|
37
|
+
await fsPromises.mkdir(path.dirname(planned.localPath), { recursive: true });
|
|
38
|
+
await downloader.downloadResolvedFile(remoteFile.file_path, planned.localPath);
|
|
39
|
+
staged.push({
|
|
40
|
+
...attachment,
|
|
41
|
+
fileName: planned.fileName,
|
|
42
|
+
fileSize: resolvedSize,
|
|
43
|
+
localPath: planned.localPath,
|
|
44
|
+
relativePath: planned.relativePath,
|
|
45
|
+
nativeImage: isNativeImageAttachment(attachment),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
logger?.warn('attachments.download_failed', {
|
|
50
|
+
fileId: attachment.fileId,
|
|
51
|
+
error: error instanceof Error ? error.message : String(error),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return staged;
|
|
56
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { StagedTelegramAttachment } from '../telegram/media.js';
|
|
2
|
+
import type { AppLocale } from '../types.js';
|
|
3
|
+
export interface EngineModel {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string | undefined;
|
|
7
|
+
isDefault?: boolean | undefined;
|
|
8
|
+
}
|
|
9
|
+
export interface EngineTurnRequest {
|
|
10
|
+
scopeId: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
stagedAttachments?: StagedTelegramAttachment[] | undefined;
|
|
13
|
+
threadId: string | null;
|
|
14
|
+
cwd: string;
|
|
15
|
+
model: string;
|
|
16
|
+
effort?: string | null | undefined;
|
|
17
|
+
serviceTier?: string | null | undefined;
|
|
18
|
+
locale: AppLocale;
|
|
19
|
+
}
|
|
20
|
+
export interface EngineToolEvent {
|
|
21
|
+
name: string;
|
|
22
|
+
args?: Record<string, unknown> | string | undefined;
|
|
23
|
+
output?: string | undefined;
|
|
24
|
+
status?: 'running' | 'completed' | 'failed' | undefined;
|
|
25
|
+
stepIndex?: number | undefined;
|
|
26
|
+
summary?: string | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface EngineTurnResult {
|
|
29
|
+
kind: 'result';
|
|
30
|
+
status: 'SUCCESS' | 'ERROR' | 'INTERRUPTED';
|
|
31
|
+
response: string;
|
|
32
|
+
error?: string | undefined;
|
|
33
|
+
conversationId: string | null;
|
|
34
|
+
usage?: {
|
|
35
|
+
inputTokens?: number | undefined;
|
|
36
|
+
outputTokens?: number | undefined;
|
|
37
|
+
cachedTokens?: number | undefined;
|
|
38
|
+
totalTokens?: number | undefined;
|
|
39
|
+
} | undefined;
|
|
40
|
+
}
|
|
41
|
+
export interface EngineTurnExecution {
|
|
42
|
+
turnId?: string | undefined;
|
|
43
|
+
cancel(): void;
|
|
44
|
+
waitForResult(): Promise<EngineTurnResult | null>;
|
|
45
|
+
on(event: 'delta', listener: (text: string) => void): void;
|
|
46
|
+
on(event: 'tool', listener: (tool: EngineToolEvent) => void): void;
|
|
47
|
+
on(event: 'result', listener: (result: EngineTurnResult) => void): void;
|
|
48
|
+
on(event: 'error', listener: (error: Error) => void): void;
|
|
49
|
+
on(event: 'exit', listener: (code: number | null) => void): void;
|
|
50
|
+
}
|
|
51
|
+
export interface EngineTurnErrorContext {
|
|
52
|
+
error: string;
|
|
53
|
+
request: EngineTurnRequest;
|
|
54
|
+
retryCount: number;
|
|
55
|
+
retryTurn: (retryCount: number) => Promise<void>;
|
|
56
|
+
sendMessage: (text: string) => Promise<number>;
|
|
57
|
+
editMessage: (messageId: number, text: string) => Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export interface IEngineAdapter {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly name: string;
|
|
62
|
+
listModels(): Promise<EngineModel[]>;
|
|
63
|
+
executeTurn(request: EngineTurnRequest): EngineTurnExecution;
|
|
64
|
+
preflightTurn?(request: EngineTurnRequest): Promise<void>;
|
|
65
|
+
handleTurnError?(context: EngineTurnErrorContext): Promise<boolean>;
|
|
66
|
+
}
|
|
67
|
+
export interface BackendDescriptor {
|
|
68
|
+
id: string;
|
|
69
|
+
name: string;
|
|
70
|
+
engineType: string;
|
|
71
|
+
adapter: IEngineAdapter;
|
|
72
|
+
account?: string | undefined;
|
|
73
|
+
details?: string | undefined;
|
|
74
|
+
isDefault?: boolean | undefined;
|
|
75
|
+
onSelect?: (scopeId: string) => Promise<void>;
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|