@foxden-app/foxclaw 0.7.3 → 0.11.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 +22 -8
- package/CHANGELOG.md +134 -0
- package/README.md +21 -1
- package/README_EN.md +21 -1
- package/dist/antigravity/adapter.d.ts +18 -0
- package/dist/antigravity/adapter.js +394 -0
- package/dist/antigravity/auth.d.ts +126 -0
- package/dist/antigravity/auth.js +933 -0
- package/dist/antigravity/client.d.ts +32 -0
- package/dist/antigravity/client.js +244 -0
- package/dist/antigravity/controller.d.ts +101 -0
- package/dist/antigravity/controller.js +2007 -0
- package/dist/antigravity/conversations.d.ts +29 -0
- package/dist/antigravity/conversations.js +289 -0
- package/dist/antigravity/events.d.ts +102 -0
- package/dist/antigravity/events.js +143 -0
- package/dist/antigravity/runtime.d.ts +49 -0
- package/dist/antigravity/runtime.js +82 -0
- package/dist/antigravity/subagents.d.ts +66 -0
- package/dist/antigravity/subagents.js +459 -0
- 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 +1 -1
- package/dist/codex_app/client.js +12 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +65 -16
- package/dist/controller/controller.d.ts +12 -5
- package/dist/controller/controller.js +109 -8
- package/dist/core/attachments.d.ts +11 -0
- package/dist/core/attachments.js +56 -0
- package/dist/core/engine_spi.d.ts +77 -0
- package/dist/core/engine_spi.js +1 -0
- package/dist/core/orchestrator.d.ts +106 -0
- package/dist/core/orchestrator.js +1556 -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 +6 -0
- package/dist/i18n.js +49 -0
- package/dist/main.js +375 -446
- package/dist/opencode/adapter.d.ts +11 -0
- package/dist/opencode/adapter.js +169 -0
- package/dist/opencode/controller.js +2 -2
- package/dist/store/database.d.ts +44 -0
- package/dist/store/database.js +275 -43
- package/dist/store/token_usage.d.ts +23 -0
- package/dist/store/token_usage.js +61 -0
- package/dist/telegram/api.d.ts +1 -0
- package/dist/telegram/api.js +80 -3
- package/dist/telegram/bot_home.js +5 -4
- package/dist/telegram/gateway.d.ts +4 -0
- package/dist/telegram/gateway.js +11 -0
- package/dist/types.d.ts +3 -0
- package/dist/update.d.ts +6 -0
- package/dist/update.js +95 -7
- 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/package.json +2 -2
|
@@ -26,6 +26,7 @@ import { normalizeAccessPreset, resolveAccessMode } from './access.js';
|
|
|
26
26
|
import { diffObservedTurn, findLatestTurn, findLiveTurn } from './observer.js';
|
|
27
27
|
import { applySessionLog, bootstrapSessionLog, splitJsonlChunk, } from './session_observer.js';
|
|
28
28
|
import { renderActiveTurnStatus } from './status.js';
|
|
29
|
+
import { formatMetricTokenCount } from '../store/token_usage.js';
|
|
29
30
|
import { writeRuntimeStatus } from '../runtime.js';
|
|
30
31
|
const AUTH_DELETE_REASON_NEEDS_REPAIR = 'needs_repair';
|
|
31
32
|
const RESTART_PREVIEW_RECOVERY_RETRY_DELAYS_MS = [3000, 7000, 15_000, 30_000, 45_000, 60_000, 60_000, 60_000];
|
|
@@ -130,6 +131,7 @@ export class BridgeSessionCore {
|
|
|
130
131
|
pendingForceTakeovers = new Map();
|
|
131
132
|
pendingLoginScopesById = new Map();
|
|
132
133
|
pendingAuthAddsByLoginId = new Map();
|
|
134
|
+
authLoginRecoveryTimers = new Map();
|
|
133
135
|
latestTurnDiffs = new Map();
|
|
134
136
|
threadTokenUsageAlerts = new Map();
|
|
135
137
|
pendingAuthChoiceLists = new Map();
|
|
@@ -212,6 +214,12 @@ export class BridgeSessionCore {
|
|
|
212
214
|
void this.handleAsyncError('channel.text', error, event.scopeId);
|
|
213
215
|
});
|
|
214
216
|
}
|
|
217
|
+
hasPendingInteraction(scopeId) {
|
|
218
|
+
return this.pendingThreadRenames.has(scopeId)
|
|
219
|
+
|| this.pendingThreadNewCwds.has(scopeId)
|
|
220
|
+
|| this.pendingUserInputs.has(scopeId)
|
|
221
|
+
|| this.pendingForceTakeovers.has(scopeId);
|
|
222
|
+
}
|
|
215
223
|
/** Start Codex app-server transport and attach RPC listeners. */
|
|
216
224
|
async startCodexApp() {
|
|
217
225
|
this.stopping = false;
|
|
@@ -291,6 +299,9 @@ export class BridgeSessionCore {
|
|
|
291
299
|
this.pendingLoginsByScope.clear();
|
|
292
300
|
this.pendingLoginScopesById.clear();
|
|
293
301
|
this.pendingAuthAddsByLoginId.clear();
|
|
302
|
+
for (const timer of this.authLoginRecoveryTimers.values())
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
this.authLoginRecoveryTimers.clear();
|
|
294
305
|
this.latestTurnDiffs.clear();
|
|
295
306
|
this.threadTokenUsageAlerts.clear();
|
|
296
307
|
this.pendingAuthChoiceLists.clear();
|
|
@@ -324,12 +335,13 @@ export class BridgeSessionCore {
|
|
|
324
335
|
this.updateStatus();
|
|
325
336
|
}
|
|
326
337
|
getRuntimeStatus() {
|
|
338
|
+
const appServer = typeof this.app.getServerStatus === 'function' ? this.app.getServerStatus() : null;
|
|
327
339
|
return {
|
|
328
340
|
running: true,
|
|
329
|
-
connected: this.app.isConnected(),
|
|
330
|
-
userAgent: this.app.getUserAgent(),
|
|
341
|
+
connected: typeof this.app.isConnected === 'function' ? this.app.isConnected() : false,
|
|
342
|
+
userAgent: typeof this.app.getUserAgent === 'function' ? this.app.getUserAgent() : null,
|
|
331
343
|
codexHome: this.config.codexHome ?? path.join(os.homedir(), '.codex'),
|
|
332
|
-
codexAppServer:
|
|
344
|
+
...(appServer ? { codexAppServer: appServer } : {}),
|
|
333
345
|
botUsername: this.botUsername,
|
|
334
346
|
currentBindings: this.store.countBindings(),
|
|
335
347
|
pendingApprovals: this.store.countPendingApprovals(),
|
|
@@ -559,6 +571,9 @@ export class BridgeSessionCore {
|
|
|
559
571
|
if (codexUpdateLine) {
|
|
560
572
|
lines.push(t(locale, 'status_last_codex_update', { value: codexUpdateLine }));
|
|
561
573
|
}
|
|
574
|
+
if (lastUpdate.agyUpdate) {
|
|
575
|
+
lines.push(locale === 'zh' ? `Antigravity CLI:${lastUpdate.agyUpdate}` : `Antigravity CLI: ${lastUpdate.agyUpdate}`);
|
|
576
|
+
}
|
|
562
577
|
}
|
|
563
578
|
else {
|
|
564
579
|
lines.push(t(locale, 'status_last_update_none'));
|
|
@@ -1801,6 +1816,15 @@ export class BridgeSessionCore {
|
|
|
1801
1816
|
return;
|
|
1802
1817
|
}
|
|
1803
1818
|
const pendingAuthAdd = loginId ? this.pendingAuthAddsByLoginId.get(loginId) ?? null : null;
|
|
1819
|
+
if (params?.success && pendingAuthAdd)
|
|
1820
|
+
await this.reconcileAuthLoginCredential(pendingAuthAdd);
|
|
1821
|
+
// A recovery check and the server notification may complete concurrently.
|
|
1822
|
+
if (this.pendingLoginScopesById.get(loginId) !== scopeId)
|
|
1823
|
+
return;
|
|
1824
|
+
const timer = this.authLoginRecoveryTimers.get(loginId);
|
|
1825
|
+
if (timer)
|
|
1826
|
+
clearTimeout(timer);
|
|
1827
|
+
this.authLoginRecoveryTimers.delete(loginId);
|
|
1804
1828
|
this.pendingLoginScopesById.delete(loginId);
|
|
1805
1829
|
if (this.pendingLoginsByScope.get(scopeId) === loginId) {
|
|
1806
1830
|
this.pendingLoginsByScope.delete(scopeId);
|
|
@@ -1875,6 +1899,62 @@ export class BridgeSessionCore {
|
|
|
1875
1899
|
const state = await this.listCodexAuthState();
|
|
1876
1900
|
await this.restoreAuthAfterAddFailure(state.authDir, state.authPath, record.previousTargetPath);
|
|
1877
1901
|
}
|
|
1902
|
+
scheduleAuthLoginRecovery(loginId) {
|
|
1903
|
+
const timer = setTimeout(() => {
|
|
1904
|
+
this.authLoginRecoveryTimers.delete(loginId);
|
|
1905
|
+
void this.recoverAuthLogin(loginId).catch(error => {
|
|
1906
|
+
this.logger.warn('codex.auth_login_recovery_failed', { error: toErrorMeta(error) });
|
|
1907
|
+
}).finally(() => {
|
|
1908
|
+
if (!this.stopping && this.pendingAuthAddsByLoginId.has(loginId)) {
|
|
1909
|
+
this.scheduleAuthLoginRecovery(loginId);
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
}, 5_000);
|
|
1913
|
+
timer.unref();
|
|
1914
|
+
this.authLoginRecoveryTimers.set(loginId, timer);
|
|
1915
|
+
}
|
|
1916
|
+
async recoverAuthLogin(loginId) {
|
|
1917
|
+
const record = this.pendingAuthAddsByLoginId.get(loginId);
|
|
1918
|
+
if (!record)
|
|
1919
|
+
return;
|
|
1920
|
+
if (Date.now() - record.createdAt >= 15 * 60_000) {
|
|
1921
|
+
await this.handleAccountLoginCompleted({ loginId, success: false, error: 'Device login timed out; completion was not confirmed.' });
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
await this.reconcileAuthLoginCredential(record);
|
|
1925
|
+
const metadata = await readChatGptAuthMetadata(record.path);
|
|
1926
|
+
if (!metadata || metadata.lastRefreshMs < record.createdAt
|
|
1927
|
+
|| !chatGptAuthMetadataMatchesCandidateName(record.name, metadata))
|
|
1928
|
+
return;
|
|
1929
|
+
const account = await this.app.readAccount(true);
|
|
1930
|
+
const limits = await this.app.readAccountRateLimits();
|
|
1931
|
+
if (!account || !limits || !selectCodexRateLimitSnapshot(limits)
|
|
1932
|
+
|| (metadata.email && account.email !== metadata.email))
|
|
1933
|
+
return;
|
|
1934
|
+
if (!this.pendingAuthAddsByLoginId.has(loginId))
|
|
1935
|
+
return;
|
|
1936
|
+
this.logger.info('codex.auth_login_recovered', { candidate: record.name });
|
|
1937
|
+
await this.handleAccountLoginCompleted({ loginId, success: true });
|
|
1938
|
+
}
|
|
1939
|
+
async reconcileAuthLoginCredential(record) {
|
|
1940
|
+
const authPath = path.join(this.resolveAuthDir(), 'auth.json');
|
|
1941
|
+
const [target, current] = await Promise.all([
|
|
1942
|
+
readChatGptAuthRecord(record.path), readChatGptAuthRecord(authPath),
|
|
1943
|
+
]);
|
|
1944
|
+
// Codex can replace auth.json atomically, breaking our candidate symlink.
|
|
1945
|
+
// Only adopt a newly written credential for the same account identity.
|
|
1946
|
+
if (target && current && current.lastRefreshMs >= record.createdAt
|
|
1947
|
+
&& current.quotaIdentityId === target.quotaIdentityId
|
|
1948
|
+
&& current.lastRefreshMs > target.lastRefreshMs
|
|
1949
|
+
&& chatGptAuthMetadataMatchesCandidateName(record.name, current)) {
|
|
1950
|
+
if (!this.pendingAuthAddsByLoginId.has(record.loginId))
|
|
1951
|
+
return;
|
|
1952
|
+
const temporary = `${record.path}.${process.pid}.tmp`;
|
|
1953
|
+
await fs.writeFile(temporary, current.raw, { mode: 0o600 });
|
|
1954
|
+
await fs.rename(temporary, record.path);
|
|
1955
|
+
await pointCodexAuthAtTarget(this.resolveAuthDir(), authPath, record.path);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1878
1958
|
async restoreAuthAfterAddFailure(authDir, authPath, previousTargetPath) {
|
|
1879
1959
|
if (previousTargetPath) {
|
|
1880
1960
|
await pointCodexAuthAtTarget(authDir, authPath, previousTargetPath);
|
|
@@ -3481,7 +3561,9 @@ export class BridgeSessionCore {
|
|
|
3481
3561
|
this.coordinator.statusUpdated(status);
|
|
3482
3562
|
return;
|
|
3483
3563
|
}
|
|
3484
|
-
|
|
3564
|
+
if (this.config.statusPath) {
|
|
3565
|
+
writeRuntimeStatus(this.config.statusPath, status);
|
|
3566
|
+
}
|
|
3485
3567
|
}
|
|
3486
3568
|
async sendMessage(scopeId, text, inlineKeyboard) {
|
|
3487
3569
|
return this.sendRichMarkdownMessage(scopeId, text, inlineKeyboard);
|
|
@@ -5044,6 +5126,9 @@ export class BridgeSessionCore {
|
|
|
5044
5126
|
const rows = [
|
|
5045
5127
|
['FoxClaw', `${status.fromVersion} -> ${status.toVersion ?? t(status.locale, 'unknown')}`, status.locale === 'zh' ? '升级完成,服务已重启' : 'Updated; service restarted'],
|
|
5046
5128
|
['Codex CLI', formatSelfUpdateVersionTransition(status.codexFromVersion, status.codexToVersion, status.locale), formatCodexUpdateState(status)],
|
|
5129
|
+
...(status.agyFromVersion || status.agyUpdate ? [
|
|
5130
|
+
['Antigravity (AGY)', formatSelfUpdateVersionTransition(status.agyFromVersion ?? null, status.agyToVersion ?? null, status.locale), status.agyUpdate ?? (status.locale === 'zh' ? '已检查更新' : 'Checked')]
|
|
5131
|
+
] : []),
|
|
5047
5132
|
[status.locale === 'zh' ? '集群广播' : 'Cluster broadcast', status.toVersion ?? t(status.locale, 'unknown'), broadcastLine],
|
|
5048
5133
|
];
|
|
5049
5134
|
const notes = status.releaseNotes?.filter(note => note.trim()) ?? [];
|
|
@@ -5061,6 +5146,7 @@ export class BridgeSessionCore {
|
|
|
5061
5146
|
telegramBold(title),
|
|
5062
5147
|
escapeTelegramHtml(foxclawResult),
|
|
5063
5148
|
escapeTelegramHtml(codexUpdateLine ?? (status.locale === 'zh' ? 'Codex CLI:未执行升级。' : 'Codex CLI: not updated.')),
|
|
5149
|
+
...(status.agyUpdate ? [escapeTelegramHtml(`Antigravity CLI:${status.agyUpdate}`)] : []),
|
|
5064
5150
|
escapeTelegramHtml(broadcastLine),
|
|
5065
5151
|
releaseNotes ? escapeTelegramHtml(releaseNotes) : '',
|
|
5066
5152
|
].filter(Boolean).join('\n');
|
|
@@ -5531,6 +5617,7 @@ export class BridgeSessionCore {
|
|
|
5531
5617
|
mode: 'add',
|
|
5532
5618
|
createdAt: Date.now(),
|
|
5533
5619
|
});
|
|
5620
|
+
this.scheduleAuthLoginRecovery(login.loginId);
|
|
5534
5621
|
await this.sendMessage(scopeId, [
|
|
5535
5622
|
t(locale, 'auth_add_started', { value: candidateName }),
|
|
5536
5623
|
t(locale, 'login_device_prereq'),
|
|
@@ -5574,6 +5661,7 @@ export class BridgeSessionCore {
|
|
|
5574
5661
|
mode: 'repair',
|
|
5575
5662
|
createdAt: Date.now(),
|
|
5576
5663
|
});
|
|
5664
|
+
this.scheduleAuthLoginRecovery(login.loginId);
|
|
5577
5665
|
await this.sendMessage(scopeId, [
|
|
5578
5666
|
t(locale, 'auth_repair_started', { value: target.name }),
|
|
5579
5667
|
t(locale, 'login_device_prereq'),
|
|
@@ -7049,11 +7137,11 @@ export class BridgeSessionCore {
|
|
|
7049
7137
|
events: formatTokenCount(stats.usageEvents),
|
|
7050
7138
|
}),
|
|
7051
7139
|
t(locale, 'status_codex_local_tokens', {
|
|
7052
|
-
total:
|
|
7053
|
-
input:
|
|
7140
|
+
total: formatCodexTokenCountWithMetric(stats.totals.totalTokens),
|
|
7141
|
+
input: formatCodexTokenCountWithMetric(stats.totals.inputTokens),
|
|
7054
7142
|
visible: formatTokenCount(Math.max(0, stats.totals.outputTokens - stats.totals.reasoningOutputTokens)),
|
|
7055
|
-
output:
|
|
7056
|
-
cached:
|
|
7143
|
+
output: formatCodexTokenCountWithMetric(stats.totals.outputTokens),
|
|
7144
|
+
cached: formatCodexTokenCountWithMetric(stats.totals.cachedInputTokens),
|
|
7057
7145
|
reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
|
|
7058
7146
|
}),
|
|
7059
7147
|
...this.formatCodexLocalResponseThroughputStatusLines(locale, stats),
|
|
@@ -7122,6 +7210,13 @@ export class BridgeSessionCore {
|
|
|
7122
7210
|
this.localUsageCache = snapshot;
|
|
7123
7211
|
this.localUsageCacheLoaded = true;
|
|
7124
7212
|
await writeCodexLocalUsageSnapshot(this.codexLocalUsageSnapshotPath(), snapshot);
|
|
7213
|
+
this.store.setBackendCumulativeTokenUsage({
|
|
7214
|
+
inputTokens: stats.totals.inputTokens,
|
|
7215
|
+
outputTokens: stats.totals.outputTokens,
|
|
7216
|
+
cachedTokens: stats.totals.cachedInputTokens,
|
|
7217
|
+
totalTokens: stats.totals.totalTokens,
|
|
7218
|
+
turnsCount: stats.turns,
|
|
7219
|
+
}, 'codex');
|
|
7125
7220
|
}
|
|
7126
7221
|
codexLocalUsageSnapshotPath() {
|
|
7127
7222
|
return path.join(path.dirname(this.config.statusPath), this.runtimeSnapshotFilename(CODEX_LOCAL_USAGE_SNAPSHOT_FILENAME));
|
|
@@ -12031,6 +12126,12 @@ function formatTokenCount(value) {
|
|
|
12031
12126
|
}
|
|
12032
12127
|
return Math.round(value).toLocaleString('en-US');
|
|
12033
12128
|
}
|
|
12129
|
+
function formatCodexTokenCountWithMetric(value) {
|
|
12130
|
+
if (value >= 1_000_000) {
|
|
12131
|
+
return `${formatMetricTokenCount(value)} (${formatTokenCount(value)})`;
|
|
12132
|
+
}
|
|
12133
|
+
return formatTokenCount(value);
|
|
12134
|
+
}
|
|
12034
12135
|
function formatLocalTimestamp(seconds) {
|
|
12035
12136
|
const date = new Date(seconds * 1000);
|
|
12036
12137
|
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,77 @@
|
|
|
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
|
+
on(event: 'conversation', listener: (conversationId: string) => void): void;
|
|
51
|
+
}
|
|
52
|
+
export interface EngineTurnErrorContext {
|
|
53
|
+
error: string;
|
|
54
|
+
request: EngineTurnRequest;
|
|
55
|
+
retryCount: number;
|
|
56
|
+
retryTurn: (retryCount: number) => Promise<void>;
|
|
57
|
+
sendMessage: (text: string) => Promise<number>;
|
|
58
|
+
editMessage: (messageId: number, text: string) => Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
export interface IEngineAdapter {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
listModels(): Promise<EngineModel[]>;
|
|
64
|
+
executeTurn(request: EngineTurnRequest): EngineTurnExecution;
|
|
65
|
+
preflightTurn?(request: EngineTurnRequest): Promise<void>;
|
|
66
|
+
handleTurnError?(context: EngineTurnErrorContext): Promise<boolean>;
|
|
67
|
+
}
|
|
68
|
+
export interface BackendDescriptor {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
engineType: string;
|
|
72
|
+
adapter: IEngineAdapter;
|
|
73
|
+
account?: string | undefined;
|
|
74
|
+
details?: string | undefined;
|
|
75
|
+
isDefault?: boolean | undefined;
|
|
76
|
+
onSelect?: (scopeId: string) => Promise<void>;
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { AppConfig } from '../config.js';
|
|
2
|
+
import type { BridgeStore } from '../store/database.js';
|
|
3
|
+
import type { Logger } from '../logger.js';
|
|
4
|
+
import type { AppLocale } from '../types.js';
|
|
5
|
+
import type { TelegramGateway, TelegramTextEvent, TelegramCallbackEvent } from '../telegram/gateway.js';
|
|
6
|
+
import type { TelegramMessagingPort, InlineKeyboard } from '../channels/telegram/telegram_messaging_port.js';
|
|
7
|
+
import { TurnQueueManager } from './turn_queue.js';
|
|
8
|
+
import type { IEngineAdapter, EngineTurnExecution, BackendDescriptor } from './engine_spi.js';
|
|
9
|
+
export declare const STREAM_THROTTLE_MS = 700;
|
|
10
|
+
export declare const TYPING_INTERVAL_MS = 4000;
|
|
11
|
+
export interface UnifiedActiveTurn {
|
|
12
|
+
scopeId: string;
|
|
13
|
+
messageId: number;
|
|
14
|
+
threadId: string | null;
|
|
15
|
+
execution: EngineTurnExecution;
|
|
16
|
+
accumulatedText: string;
|
|
17
|
+
toolLines: string[];
|
|
18
|
+
flushTimer: NodeJS.Timeout | null;
|
|
19
|
+
lastFlushTime: number;
|
|
20
|
+
typingTimer: NodeJS.Timeout | null;
|
|
21
|
+
stepIndex?: number;
|
|
22
|
+
toolCount: number;
|
|
23
|
+
currentTool?: string | null;
|
|
24
|
+
startTime: number;
|
|
25
|
+
}
|
|
26
|
+
export interface EngineCustomUiHook {
|
|
27
|
+
renderCustomStatus?(scopeId: string, locale: AppLocale): Promise<string | null>;
|
|
28
|
+
renderCustomSetupRows?(scopeId: string, locale: AppLocale): Promise<InlineKeyboard>;
|
|
29
|
+
handleCustomCallback?(scopeId: string, data: string, locale: AppLocale, messageId?: number, event?: TelegramCallbackEvent): Promise<boolean>;
|
|
30
|
+
handleCustomCommand?(scopeId: string, command: string, args: string, locale: AppLocale, event?: TelegramTextEvent): Promise<boolean>;
|
|
31
|
+
handleCustomInbound?(event: TelegramTextEvent, locale: AppLocale): Promise<boolean>;
|
|
32
|
+
}
|
|
33
|
+
export declare class UnifiedChannelOrchestrator {
|
|
34
|
+
readonly config: AppConfig;
|
|
35
|
+
readonly store: BridgeStore;
|
|
36
|
+
readonly logger: Logger;
|
|
37
|
+
readonly bot: TelegramGateway;
|
|
38
|
+
readonly messaging: TelegramMessagingPort;
|
|
39
|
+
readonly queueManager: TurnQueueManager;
|
|
40
|
+
readonly customUi?: EngineCustomUiHook | undefined;
|
|
41
|
+
private readonly backends;
|
|
42
|
+
private readonly defaultBackendId;
|
|
43
|
+
private readonly activeTurns;
|
|
44
|
+
private readonly stalePanelDeleteTimers;
|
|
45
|
+
private readonly backendProvider?;
|
|
46
|
+
get adapter(): IEngineAdapter;
|
|
47
|
+
constructor(options: {
|
|
48
|
+
config: AppConfig;
|
|
49
|
+
store: BridgeStore;
|
|
50
|
+
logger: Logger;
|
|
51
|
+
bot: TelegramGateway;
|
|
52
|
+
adapter?: IEngineAdapter;
|
|
53
|
+
adapters?: Map<string, IEngineAdapter> | IEngineAdapter[];
|
|
54
|
+
backends?: BackendDescriptor[];
|
|
55
|
+
backendProvider?: (() => Promise<BackendDescriptor[]> | BackendDescriptor[]) | undefined;
|
|
56
|
+
defaultBackendId?: string;
|
|
57
|
+
messaging: TelegramMessagingPort;
|
|
58
|
+
customUi?: EngineCustomUiHook | undefined;
|
|
59
|
+
});
|
|
60
|
+
getAdapterForScope(scopeId: string): IEngineAdapter;
|
|
61
|
+
getBackendDescriptorForScope(scopeId: string): BackendDescriptor;
|
|
62
|
+
getAdapterForBackend(backendId: string): IEngineAdapter;
|
|
63
|
+
registerBackend(desc: BackendDescriptor): void;
|
|
64
|
+
listBackends(): Promise<BackendDescriptor[]>;
|
|
65
|
+
resolveBackendDescriptor(backendId: string): Promise<BackendDescriptor | null>;
|
|
66
|
+
syncCurrentBackendSettings(scopeId: string): void;
|
|
67
|
+
scheduleStalePanelDeletion(scopeId: string, messageId: number): void;
|
|
68
|
+
switchBackend(scopeId: string, targetBackendId: string, locale: AppLocale): Promise<{
|
|
69
|
+
previousBackendId: string;
|
|
70
|
+
newBackendId: string;
|
|
71
|
+
restoredThreadId: string | null;
|
|
72
|
+
cwd: string | null;
|
|
73
|
+
}>;
|
|
74
|
+
registerInboundHandlers(): void;
|
|
75
|
+
dispatchInboundLikeTelegramText(event: TelegramTextEvent): void;
|
|
76
|
+
start(): Promise<void>;
|
|
77
|
+
stop(): Promise<void>;
|
|
78
|
+
getActiveTurnsCount(): number;
|
|
79
|
+
hasActiveTurn(scopeId: string): boolean;
|
|
80
|
+
sendMessage(scopeId: string, text: string, keyboard?: InlineKeyboard): Promise<number>;
|
|
81
|
+
editMessage(scopeId: string, messageId: number, text: string, keyboard?: InlineKeyboard): Promise<void>;
|
|
82
|
+
private safeDeliverChunks;
|
|
83
|
+
private safeDeliverMessage;
|
|
84
|
+
handleText(event: TelegramTextEvent): Promise<void>;
|
|
85
|
+
handleCallback(event: TelegramCallbackEvent): Promise<void>;
|
|
86
|
+
startPromptTurn(event: TelegramTextEvent, prompt: string, locale: AppLocale, options?: {
|
|
87
|
+
reuseMessageId?: number | undefined;
|
|
88
|
+
forcedThreadId?: string | undefined;
|
|
89
|
+
}): Promise<void>;
|
|
90
|
+
private enqueuePromptTurn;
|
|
91
|
+
private drainNextQueuedTurn;
|
|
92
|
+
private executeTurn;
|
|
93
|
+
private handleEffortCommand;
|
|
94
|
+
private handleBoostCommand;
|
|
95
|
+
private handleActiveCommand;
|
|
96
|
+
private handleQueueCommand;
|
|
97
|
+
private handleSteerCommand;
|
|
98
|
+
handleInterrupt(scopeId: string, locale: AppLocale): Promise<void>;
|
|
99
|
+
handleNewSession(scopeId: string, locale: AppLocale, targetCwdInput?: string): Promise<void>;
|
|
100
|
+
sendStatus(scopeId: string, locale: AppLocale): Promise<void>;
|
|
101
|
+
sendSetupMenu(scopeId: string, locale: AppLocale, editMessageId?: number): Promise<void>;
|
|
102
|
+
sendModelsMenu(scopeId: string, locale: AppLocale, editMessageId?: number): Promise<void>;
|
|
103
|
+
sendHelp(scopeId: string, locale: AppLocale): Promise<void>;
|
|
104
|
+
sendBackendMenu(scopeId: string, locale: AppLocale, editMessageId?: number): Promise<void>;
|
|
105
|
+
handleBackendCommand(scopeId: string, argsString: string, locale: AppLocale): Promise<void>;
|
|
106
|
+
}
|