@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
package/dist/main.js
CHANGED
|
@@ -7,8 +7,9 @@ import process from 'node:process';
|
|
|
7
7
|
import { createInterface } from 'node:readline/promises';
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
-
import { APP_HOME, buildCodexApiProviderOverrides, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
|
|
10
|
+
import { APP_HOME, buildCodexApiProviderOverrides, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ANTIGRAVITY_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
|
|
11
11
|
import { createAuthRefreshNotificationAggregator, } from './auth/notifications.js';
|
|
12
|
+
import { getAntigravityTelegramCommands, getTelegramCommands } from './i18n.js';
|
|
12
13
|
import { installBundledCodexSkills } from './codex_skills.js';
|
|
13
14
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
14
15
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
@@ -190,6 +191,9 @@ async function main() {
|
|
|
190
191
|
...(process.env.CODEX_CLI_BIN || resolveCommand('codex')
|
|
191
192
|
? { codexCliBin: process.env.CODEX_CLI_BIN || resolveCommand('codex') }
|
|
192
193
|
: {}),
|
|
194
|
+
...(process.env.AGY_CLI_BIN || resolveCommand('agy')
|
|
195
|
+
? { agyCliBin: process.env.AGY_CLI_BIN || resolveCommand('agy') }
|
|
196
|
+
: {}),
|
|
193
197
|
...(notificationFile ? { notificationFile } : {}),
|
|
194
198
|
};
|
|
195
199
|
const outcome = performSelfUpdate(options);
|
|
@@ -345,7 +349,8 @@ async function runSendVoiceCli() {
|
|
|
345
349
|
throw new Error('Telegram voice files must be 50MB or smaller.');
|
|
346
350
|
}
|
|
347
351
|
const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
|
|
348
|
-
const
|
|
352
|
+
const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
|
|
353
|
+
const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
|
|
349
354
|
const { BridgeStore } = await import('./store/database.js');
|
|
350
355
|
const store = new BridgeStore(config.storePath);
|
|
351
356
|
let chatId = parsed.chatId;
|
|
@@ -390,7 +395,8 @@ async function runSendMediaCli() {
|
|
|
390
395
|
}
|
|
391
396
|
const plan = planTelegramOutboundMedia(filePath);
|
|
392
397
|
const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
|
|
393
|
-
const
|
|
398
|
+
const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
|
|
399
|
+
const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
|
|
394
400
|
const { BridgeStore } = await import('./store/database.js');
|
|
395
401
|
const store = new BridgeStore(config.storePath);
|
|
396
402
|
let chatId = parsed.chatId;
|
|
@@ -522,6 +528,17 @@ function formatRuntimeStatusSummary(status) {
|
|
|
522
528
|
else if (status.codexHome) {
|
|
523
529
|
lines.push(`Codex home: ${status.codexHome}`);
|
|
524
530
|
}
|
|
531
|
+
const agyHomes = (status.bots ?? [])
|
|
532
|
+
.flatMap(bot => bot.antigravityHome ? [{ label: bot.username ? `@${bot.username}` : bot.id, home: bot.antigravityHome }] : []);
|
|
533
|
+
if (agyHomes.length > 1) {
|
|
534
|
+
lines.push('Antigravity homes:');
|
|
535
|
+
for (const { label, home } of agyHomes) {
|
|
536
|
+
lines.push(` ${label}: ${home}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else if (agyHomes.length === 1) {
|
|
540
|
+
lines.push(`Antigravity home: ${agyHomes[0].home}`);
|
|
541
|
+
}
|
|
525
542
|
if (status.codexAppServer) {
|
|
526
543
|
lines.push(`App server: ${status.codexAppServer.running ? 'running' : 'stopped'}${status.codexAppServer.pid ? ` pid=${status.codexAppServer.pid}` : ''}${status.codexAppServer.port ? ` port=${status.codexAppServer.port}` : ''}`);
|
|
527
544
|
}
|
|
@@ -588,7 +605,7 @@ function truncateStatusLine(value, maxLength) {
|
|
|
588
605
|
return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
|
|
589
606
|
}
|
|
590
607
|
async function runServeCli() {
|
|
591
|
-
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime },] = await Promise.all([
|
|
608
|
+
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
|
|
592
609
|
import('./channels/bridge_messaging_router.js'),
|
|
593
610
|
import('./channels/telegram/telegram_messaging_port.js'),
|
|
594
611
|
import('./channels/weixin/weixin_channel_adapter.js'),
|
|
@@ -604,6 +621,9 @@ async function runServeCli() {
|
|
|
604
621
|
import('./auth/mirror.js'),
|
|
605
622
|
import('./auth/cross_node_sync.js'),
|
|
606
623
|
import('./opencode/runtime.js'),
|
|
624
|
+
import('./antigravity/controller.js'),
|
|
625
|
+
import('./antigravity/client.js'),
|
|
626
|
+
import('./antigravity/auth.js'),
|
|
607
627
|
]);
|
|
608
628
|
const config = loadConfig();
|
|
609
629
|
const logger = new Logger(config.logLevel, config.logPath);
|
|
@@ -619,386 +639,250 @@ async function runServeCli() {
|
|
|
619
639
|
let activeAuthMirror = null;
|
|
620
640
|
let activeAuthSync = null;
|
|
621
641
|
let activeOpencodeRuntime = null;
|
|
642
|
+
let sharedCodexApp = null;
|
|
643
|
+
let sharedAntigravityApp = null;
|
|
644
|
+
let sharedAntigravityAuth = null;
|
|
622
645
|
try {
|
|
623
646
|
store = new BridgeStore(config.storePath);
|
|
647
|
+
const selfUpdater = createSelfUpdateRuntime({
|
|
648
|
+
entryPoint,
|
|
649
|
+
nodePath: process.execPath,
|
|
650
|
+
version: readPackageVersion(),
|
|
651
|
+
statusPath: config.statusPath,
|
|
652
|
+
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
653
|
+
codexCliBin: config.codexCliBin,
|
|
654
|
+
agyCliBin: config.antigravityCliBin,
|
|
655
|
+
});
|
|
656
|
+
sharedAntigravityAuth = new AntigravityAuthManager(config.antigravityAuthDir, logger);
|
|
657
|
+
sharedAntigravityApp = new AntigravityAppClient(config.antigravityCliBin, logger);
|
|
658
|
+
if (config.codexCliBin) {
|
|
659
|
+
sharedCodexApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
|
|
660
|
+
}
|
|
624
661
|
if (config.opencodeBotToken) {
|
|
625
662
|
activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
|
|
626
663
|
await activeOpencodeRuntime.start();
|
|
627
664
|
logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
|
|
628
665
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
+
const totalBots = config.codexBotTokens.length + config.antigravityBotTokens.length;
|
|
667
|
+
const namespacedScopes = totalBots > 1;
|
|
668
|
+
const seeds = [];
|
|
669
|
+
const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
|
|
670
|
+
const specs = [
|
|
671
|
+
...config.codexBotTokens.map((token, i) => ({
|
|
672
|
+
token,
|
|
673
|
+
backend: 'codex',
|
|
674
|
+
isDefaultRuntimeForBackend: config.tgDefaultRuntimeBotToken
|
|
675
|
+
? config.tgDefaultRuntimeBotToken === token
|
|
676
|
+
: i === 0,
|
|
677
|
+
})),
|
|
678
|
+
...config.antigravityBotTokens.map((token, i) => ({
|
|
679
|
+
token,
|
|
680
|
+
backend: 'antigravity',
|
|
681
|
+
isDefaultRuntimeForBackend: config.antigravityDefaultRuntimeBotToken
|
|
682
|
+
? config.antigravityDefaultRuntimeBotToken === token
|
|
683
|
+
: i === 0,
|
|
684
|
+
})),
|
|
685
|
+
];
|
|
686
|
+
const identities = await Promise.all(specs.map(async (spec) => {
|
|
687
|
+
const bot = new TelegramGateway(spec.token, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger, namespacedScopes, spec.backend === 'antigravity' ? getAntigravityTelegramCommands : getTelegramCommands);
|
|
688
|
+
const id = await bot.initializeIdentity();
|
|
689
|
+
const username = await bot.resolveUsername();
|
|
690
|
+
return { ...spec, bot, id, username };
|
|
691
|
+
}));
|
|
692
|
+
for (const { token, backend, isDefaultRuntimeForBackend, bot, id, username } of identities) {
|
|
693
|
+
if (seeds.some((runtime) => runtime.id === id)) {
|
|
694
|
+
throw new Error(`Duplicate Telegram bot identity detected: ${id}`);
|
|
695
|
+
}
|
|
696
|
+
// Codex environment
|
|
697
|
+
const isCodexDefault = backend === 'codex' && isDefaultRuntimeForBackend;
|
|
698
|
+
const codexHome = isCodexDefault
|
|
699
|
+
? (config.codexHome ?? path.join(os.homedir(), '.codex'))
|
|
700
|
+
: prepareTelegramBotHome(DEFAULT_CODEX_TELEGRAM_HOME, id, username, null);
|
|
701
|
+
const codexAuthDir = isCodexDefault ? (config.codexAuthDir ?? codexHome) : codexHome;
|
|
702
|
+
if (!isCodexDefault) {
|
|
703
|
+
fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 });
|
|
704
|
+
}
|
|
705
|
+
installBundledCodexSkills(packageRoot, codexHome);
|
|
706
|
+
const codexAppAutolaunch = isCodexDefault ? config.codexAppAutolaunch : false;
|
|
707
|
+
const codexAppServerStatePath = isCodexDefault
|
|
708
|
+
? config.codexAppServerStatePath
|
|
709
|
+
: path.join(APP_HOME, 'runtime', `codex-app-server-${id}.json`);
|
|
710
|
+
const codexAppServerLogPath = isCodexDefault
|
|
711
|
+
? config.codexAppServerLogPath
|
|
712
|
+
: path.join(APP_HOME, 'logs', `codex-app-server-${id}.log`);
|
|
713
|
+
const codexApp = (isCodexDefault && sharedCodexApp)
|
|
714
|
+
? sharedCodexApp
|
|
715
|
+
: new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, codexAppAutolaunch, codexAppServerStatePath, codexAppServerLogPath, logger, { CODEX_HOME: codexHome }, [
|
|
666
716
|
...codexApiProviderOverrides,
|
|
667
|
-
...(
|
|
717
|
+
...(isCodexDefault ? [] : ['cli_auth_credentials_store="file"']),
|
|
668
718
|
]);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
let
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
suppressBackgroundNotifications: () => true,
|
|
681
|
-
}),
|
|
682
|
-
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
683
|
-
onSynced: async (event) => {
|
|
684
|
-
await authSync?.publishCandidate(event.record.candidateName);
|
|
685
|
-
},
|
|
686
|
-
onRemoteImported: (event) => {
|
|
687
|
-
restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
|
|
688
|
-
},
|
|
689
|
-
});
|
|
690
|
-
await mirror.initialize();
|
|
691
|
-
activeAuthMirror = mirror;
|
|
692
|
-
managedApps = seeds.map((runtime) => runtime.app);
|
|
693
|
-
const selfUpdater = createSelfUpdateRuntime({
|
|
694
|
-
entryPoint,
|
|
695
|
-
nodePath: process.execPath,
|
|
696
|
-
version: readPackageVersion(),
|
|
697
|
-
statusPath: config.statusPath,
|
|
698
|
-
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
699
|
-
codexCliBin: config.codexCliBin,
|
|
700
|
-
});
|
|
701
|
-
const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
|
|
702
|
-
let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
|
|
703
|
-
const runtimes = [];
|
|
704
|
-
const localAuthRefreshLease = createLocalAuthRefreshLease();
|
|
705
|
-
const writeAggregateStatus = (running = true) => {
|
|
706
|
-
const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
|
|
707
|
-
const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
|
|
708
|
-
const authProactiveRefresh = newestAuthProactiveRefreshStatus([...statuses, ...(weixinStatus ? [weixinStatus] : [])]);
|
|
709
|
-
const first = statuses[0] ?? null;
|
|
710
|
-
writeRuntimeStatus(config.statusPath, {
|
|
711
|
-
running,
|
|
712
|
-
connected: running
|
|
713
|
-
&& statuses.every((status) => status.connected)
|
|
714
|
-
&& (!weixinStatus || weixinStatus.connected),
|
|
715
|
-
userAgent: first?.userAgent ?? null,
|
|
716
|
-
codexHome: first?.codexHome ?? null,
|
|
717
|
-
...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
|
|
718
|
-
botUsername: first?.botUsername ?? null,
|
|
719
|
-
currentBindings: store.countBindings(),
|
|
720
|
-
pendingApprovals: store.countPendingApprovals(),
|
|
721
|
-
pendingUserInputs: store.countPendingUserInputs(),
|
|
722
|
-
queuedTurns: store.countQueuedTurnInputs(),
|
|
723
|
-
activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
|
|
724
|
-
+ (weixinStatus?.activeTurns ?? 0),
|
|
725
|
-
lastError: statuses.find((status) => status.lastError)?.lastError
|
|
726
|
-
?? weixinStatus?.lastError
|
|
727
|
-
?? null,
|
|
728
|
-
updatedAt: new Date().toISOString(),
|
|
729
|
-
channels: { telegram: running, weixin: running && config.wxEnabled },
|
|
730
|
-
bots: runtimes.map((runtime, index) => ({
|
|
731
|
-
id: runtime.id,
|
|
732
|
-
username: statuses[index]?.botUsername ?? runtime.bot.username,
|
|
733
|
-
connected: running && Boolean(statuses[index]?.connected),
|
|
734
|
-
activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
|
|
735
|
-
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
736
|
-
codexHome: statuses[index]?.codexHome ?? runtime.home,
|
|
737
|
-
...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
|
|
738
|
-
})),
|
|
739
|
-
...(weixinStatus ? {
|
|
740
|
-
weixinRuntime: {
|
|
741
|
-
connected: running && weixinStatus.connected,
|
|
742
|
-
activeTurns: running ? weixinStatus.activeTurns : 0,
|
|
743
|
-
...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
|
|
744
|
-
},
|
|
745
|
-
} : {}),
|
|
746
|
-
authMirror: mirror.getStatus(),
|
|
747
|
-
authSync: authSync?.getStatus() ?? null,
|
|
748
|
-
authProactiveRefresh,
|
|
749
|
-
lastUpdate: lastSelfUpdate,
|
|
750
|
-
});
|
|
751
|
-
};
|
|
752
|
-
const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
|
|
753
|
-
&& (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
|
|
754
|
-
&& mirror.isIdle();
|
|
755
|
-
const clusterUpdateScheduler = createClusterUpdateScheduler({
|
|
756
|
-
selfUpdater,
|
|
757
|
-
canUpdate: () => authSyncLocalIdle()
|
|
758
|
-
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
759
|
-
currentVersion: readPackageVersion,
|
|
760
|
-
logger,
|
|
761
|
-
});
|
|
762
|
-
const coordinator = {
|
|
763
|
-
canSelfUpdate: () => authSyncLocalIdle()
|
|
764
|
-
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
765
|
-
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
766
|
-
authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
|
|
767
|
-
await mirror.deleteCandidate(candidateName);
|
|
768
|
-
await authSync?.publishCandidateDeletion(candidateName, reason);
|
|
769
|
-
},
|
|
770
|
-
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
771
|
-
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
772
|
-
if (local)
|
|
773
|
-
return true;
|
|
774
|
-
if (options.crossNode === false)
|
|
775
|
-
return false;
|
|
776
|
-
const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
|
|
777
|
-
?? await mirror.readNewestCandidate(candidateName);
|
|
778
|
-
return await authSync?.requestRecovery(candidateName, {
|
|
779
|
-
accountId: current?.accountId ?? null,
|
|
780
|
-
quotaIdentityId: current?.quotaIdentityId ?? null,
|
|
781
|
-
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
782
|
-
}) ?? false;
|
|
783
|
-
},
|
|
784
|
-
acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
|
|
785
|
-
?? localAuthRefreshLease.acquire(reason),
|
|
786
|
-
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
787
|
-
?? localAuthRefreshLease.release(leaseId),
|
|
788
|
-
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
789
|
-
authSyncSafeAll: async () => {
|
|
790
|
-
const local = await mirror.syncAllRuntimeCandidates();
|
|
791
|
-
const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
792
|
-
return {
|
|
793
|
-
localSynced: local.synced,
|
|
794
|
-
localSkipped: local.skipped,
|
|
795
|
-
sent: remote.sent,
|
|
796
|
-
skipped: remote.skipped,
|
|
797
|
-
};
|
|
798
|
-
},
|
|
799
|
-
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
800
|
-
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
801
|
-
authSyncAudit: () => authSync?.auditCluster() ?? Promise.resolve(null),
|
|
802
|
-
statusUpdated: () => writeAggregateStatus(),
|
|
803
|
-
getServiceStatus: async () => ({
|
|
804
|
-
currentVersion: readPackageVersion(),
|
|
805
|
-
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
806
|
-
const status = runtime.core.getRuntimeStatus();
|
|
807
|
-
return {
|
|
808
|
-
id: runtime.id,
|
|
809
|
-
username: status.botUsername ?? runtime.bot.username,
|
|
810
|
-
connected: status.connected,
|
|
811
|
-
activeTurns: status.activeTurns,
|
|
812
|
-
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
813
|
-
currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
|
|
814
|
-
codexHome: status.codexHome ?? runtime.home,
|
|
815
|
-
...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
|
|
816
|
-
};
|
|
817
|
-
})),
|
|
818
|
-
...(activeWeixinCore ? {
|
|
819
|
-
weixinRuntime: {
|
|
820
|
-
connected: activeWeixinCore.getRuntimeStatus().connected,
|
|
821
|
-
activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
|
|
822
|
-
codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
|
|
823
|
-
},
|
|
824
|
-
} : {}),
|
|
825
|
-
authMirror: mirror.getStatus(),
|
|
826
|
-
authSync: authSync?.getStatus() ?? null,
|
|
827
|
-
authProactiveRefresh: newestAuthProactiveRefreshStatus([
|
|
828
|
-
...runtimes.map(runtime => runtime.core.getRuntimeStatus()),
|
|
829
|
-
...(activeWeixinCore ? [activeWeixinCore.getRuntimeStatus()] : []),
|
|
830
|
-
]),
|
|
831
|
-
lastUpdate: lastSelfUpdate,
|
|
832
|
-
}),
|
|
833
|
-
selfUpdateCompleted: (status) => {
|
|
834
|
-
lastSelfUpdate = status;
|
|
835
|
-
writeSelfUpdateStatus(lastSelfUpdatePath, status);
|
|
836
|
-
writeAggregateStatus();
|
|
837
|
-
},
|
|
838
|
-
};
|
|
839
|
-
for (const seed of seeds) {
|
|
840
|
-
const telegramMessaging = new TelegramMessagingPort(seed.bot);
|
|
841
|
-
const outbound = new BridgeMessagingRouter(telegramMessaging, null);
|
|
842
|
-
const core = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator);
|
|
843
|
-
runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
|
|
719
|
+
// Antigravity environment
|
|
720
|
+
const isAntigravityDefault = backend === 'antigravity' && isDefaultRuntimeForBackend;
|
|
721
|
+
let antigravityHome;
|
|
722
|
+
let antigravityAuthDir;
|
|
723
|
+
let antigravityApp;
|
|
724
|
+
let antigravityAuth;
|
|
725
|
+
if (isAntigravityDefault) {
|
|
726
|
+
antigravityHome = os.homedir();
|
|
727
|
+
antigravityAuthDir = config.antigravityAuthDir;
|
|
728
|
+
antigravityApp = sharedAntigravityApp;
|
|
729
|
+
antigravityAuth = sharedAntigravityAuth;
|
|
844
730
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
731
|
+
else {
|
|
732
|
+
antigravityHome = prepareTelegramBotHome(DEFAULT_ANTIGRAVITY_TELEGRAM_HOME, id, username, null);
|
|
733
|
+
antigravityAuthDir = path.join(antigravityHome, '.gemini', 'antigravity-cli');
|
|
734
|
+
fs.mkdirSync(antigravityAuthDir, { recursive: true, mode: 0o700 });
|
|
735
|
+
linkAntigravityAuthTokens(config.antigravityAuthDir, antigravityAuthDir);
|
|
736
|
+
antigravityApp = new AntigravityAppClient(config.antigravityCliBin, logger, { HOME: antigravityHome });
|
|
737
|
+
antigravityAuth = new AntigravityAuthManager(antigravityAuthDir, logger);
|
|
851
738
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
if (!authSyncLocalIdle()) {
|
|
868
|
-
return { ok: false, reason: 'runtime is not idle' };
|
|
869
|
-
}
|
|
870
|
-
const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
|
|
871
|
-
if (!runtime) {
|
|
872
|
-
return { ok: false, reason: 'no validation runtime is available' };
|
|
873
|
-
}
|
|
874
|
-
return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
875
|
-
},
|
|
876
|
-
importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
|
|
877
|
-
markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store, mirror, candidateName, state, expected, authRuntimeIds),
|
|
878
|
-
deleteLocalCandidate: async (candidateName, source) => {
|
|
879
|
-
if (!authSyncLocalIdle()) {
|
|
880
|
-
return { ok: false, deleted: false, reason: 'runtime is not idle' };
|
|
881
|
-
}
|
|
882
|
-
await mirror.deleteCandidate(candidateName);
|
|
883
|
-
store.deleteCodexAuthCandidate(candidateName);
|
|
884
|
-
await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
|
|
885
|
-
if (activeWeixinCore) {
|
|
886
|
-
await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
887
|
-
}
|
|
888
|
-
return { ok: true, deleted: true, reason: source.reason ?? null };
|
|
889
|
-
},
|
|
890
|
-
scheduleServiceUpdate: (source) => clusterUpdateScheduler.schedule(source),
|
|
891
|
-
isIdle: authSyncLocalIdle,
|
|
892
|
-
notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
|
|
893
|
-
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
894
|
-
suppressBackgroundNotifications: () => true,
|
|
895
|
-
}),
|
|
896
|
-
});
|
|
897
|
-
await authSync.initialize();
|
|
898
|
-
activeAuthSync = authSync;
|
|
899
|
-
attachTelegramAuthSync(authSyncTransportBot.bot, authSync, config, logger);
|
|
900
|
-
authSync.start();
|
|
901
|
-
void publishPendingClusterUpdateBroadcast(authSync, logger);
|
|
739
|
+
const runtimeConfig = {
|
|
740
|
+
...config,
|
|
741
|
+
tgBotToken: token,
|
|
742
|
+
tgBotTokens: [token],
|
|
743
|
+
tgScopeBotId: id,
|
|
744
|
+
codexAuthDir,
|
|
745
|
+
codexHome,
|
|
746
|
+
antigravityHome,
|
|
747
|
+
antigravityAuthDir,
|
|
748
|
+
codexAppAutolaunch,
|
|
749
|
+
codexAppServerStatePath,
|
|
750
|
+
codexAppServerLogPath,
|
|
751
|
+
};
|
|
752
|
+
if (namespacedScopes && (isCodexDefault || isAntigravityDefault)) {
|
|
753
|
+
store.migrateTelegramUnnamespacedScope(id, config.tgAllowedUserId);
|
|
902
754
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
755
|
+
seeds.push({
|
|
756
|
+
id,
|
|
757
|
+
username,
|
|
758
|
+
home: codexHome,
|
|
759
|
+
authDir: codexAuthDir,
|
|
760
|
+
sharedDefaultRuntime: isCodexDefault || isAntigravityDefault,
|
|
761
|
+
config: runtimeConfig,
|
|
762
|
+
bot,
|
|
763
|
+
app: codexApp,
|
|
764
|
+
antigravityApp,
|
|
765
|
+
antigravityAuth,
|
|
766
|
+
antigravityHome,
|
|
767
|
+
antigravityAuthDir,
|
|
768
|
+
defaultBackend: backend,
|
|
906
769
|
});
|
|
907
|
-
process.on('uncaughtException', (error) => {
|
|
908
|
-
logger.error('process.uncaught_exception', { error: serializeError(error) });
|
|
909
|
-
});
|
|
910
|
-
for (const runtime of runtimes) {
|
|
911
|
-
await runtime.telegram.start();
|
|
912
|
-
}
|
|
913
|
-
if (weixinAdapter) {
|
|
914
|
-
await activeWeixinCore.startCodexApp();
|
|
915
|
-
await weixinAdapter.start();
|
|
916
|
-
}
|
|
917
|
-
writeAggregateStatus();
|
|
918
|
-
logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
|
|
919
|
-
const shutdown = async (signal) => {
|
|
920
|
-
logger.info('bridge.shutting_down', { signal });
|
|
921
|
-
await authNotificationAggregator.flushAll();
|
|
922
|
-
authSync?.stop();
|
|
923
|
-
mirror.stop();
|
|
924
|
-
await weixinAdapter?.stop();
|
|
925
|
-
await activeWeixinCore?.stop();
|
|
926
|
-
await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
|
|
927
|
-
await activeOpencodeRuntime?.stop();
|
|
928
|
-
writeAggregateStatus(false);
|
|
929
|
-
await Promise.all(managedApps.map((app) => app.stop({ terminateServer: true }).catch((error) => {
|
|
930
|
-
logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
|
|
931
|
-
})));
|
|
932
|
-
store?.close();
|
|
933
|
-
processLock.release();
|
|
934
|
-
process.exit(0);
|
|
935
|
-
};
|
|
936
|
-
process.on('SIGINT', () => void shutdown('SIGINT'));
|
|
937
|
-
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
938
|
-
return;
|
|
939
770
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
:
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
771
|
+
let authSync = null;
|
|
772
|
+
const authRuntimeIds = seeds.map((runtime) => runtime.id);
|
|
773
|
+
const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
|
|
774
|
+
id: runtime.id,
|
|
775
|
+
label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
|
|
776
|
+
authDir: runtime.authDir,
|
|
777
|
+
validate: async (context) => validateRefreshedAuthCandidate(runtime, context.candidateName),
|
|
778
|
+
notify: createAuthMirrorNotifier(store, runtime.id, runtime.bot, authNotificationAggregator, {
|
|
779
|
+
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
780
|
+
suppressBackgroundNotifications: () => true,
|
|
781
|
+
}),
|
|
782
|
+
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
783
|
+
onSynced: async (event) => {
|
|
784
|
+
await authSync?.publishCandidate(event.record.candidateName);
|
|
785
|
+
},
|
|
786
|
+
onRemoteImported: (event) => {
|
|
787
|
+
restoreImportedAuthCandidateState(store, event.record.candidateName, authRuntimeIds);
|
|
788
|
+
},
|
|
956
789
|
});
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
const
|
|
961
|
-
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
const
|
|
790
|
+
await mirror.initialize();
|
|
791
|
+
activeAuthMirror = mirror;
|
|
792
|
+
managedApps = seeds.map((runtime) => runtime.app);
|
|
793
|
+
const lastSelfUpdatePath = path.join(APP_HOME, 'runtime', 'last-self-update.json');
|
|
794
|
+
let lastSelfUpdate = readSelfUpdateStatus(lastSelfUpdatePath);
|
|
795
|
+
const runtimes = [];
|
|
796
|
+
const localAuthRefreshLease = createLocalAuthRefreshLease();
|
|
797
|
+
const writeAggregateStatus = (running = true) => {
|
|
798
|
+
const statuses = runtimes.map((runtime) => runtime.core.getRuntimeStatus());
|
|
799
|
+
const weixinStatus = activeWeixinCore?.getRuntimeStatus() ?? null;
|
|
800
|
+
const authProactiveRefresh = newestAuthProactiveRefreshStatus([...statuses, ...(weixinStatus ? [weixinStatus] : [])]);
|
|
801
|
+
const first = statuses[0] ?? null;
|
|
802
|
+
writeRuntimeStatus(config.statusPath, {
|
|
803
|
+
running,
|
|
804
|
+
connected: running
|
|
805
|
+
&& statuses.every((status) => status.connected)
|
|
806
|
+
&& (!weixinStatus || weixinStatus.connected),
|
|
807
|
+
userAgent: first?.userAgent ?? null,
|
|
808
|
+
codexHome: first?.codexHome ?? null,
|
|
809
|
+
...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
|
|
810
|
+
botUsername: first?.botUsername ?? null,
|
|
811
|
+
currentBindings: store.countBindings(),
|
|
812
|
+
pendingApprovals: store.countPendingApprovals(),
|
|
813
|
+
pendingUserInputs: store.countPendingUserInputs(),
|
|
814
|
+
queuedTurns: store.countQueuedTurnInputs(),
|
|
815
|
+
activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
|
|
816
|
+
+ (weixinStatus?.activeTurns ?? 0),
|
|
817
|
+
lastError: statuses.find((status) => status.lastError)?.lastError
|
|
818
|
+
?? weixinStatus?.lastError
|
|
819
|
+
?? null,
|
|
820
|
+
updatedAt: new Date().toISOString(),
|
|
821
|
+
channels: { telegram: running, weixin: running && config.wxEnabled },
|
|
822
|
+
bots: runtimes.map((runtime, index) => ({
|
|
823
|
+
id: runtime.id,
|
|
824
|
+
username: statuses[index]?.botUsername ?? runtime.bot.username,
|
|
825
|
+
connected: running && Boolean(statuses[index]?.connected),
|
|
826
|
+
activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
|
|
827
|
+
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
828
|
+
defaultBackend: runtime.defaultBackend,
|
|
829
|
+
codexHome: statuses[index]?.codexHome ?? runtime.home,
|
|
830
|
+
antigravityHome: runtime.antigravityHome,
|
|
831
|
+
...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
|
|
832
|
+
})),
|
|
833
|
+
...(weixinStatus ? {
|
|
834
|
+
weixinRuntime: {
|
|
835
|
+
connected: running && weixinStatus.connected,
|
|
836
|
+
activeTurns: running ? weixinStatus.activeTurns : 0,
|
|
837
|
+
...(weixinStatus.codexAppServer ? { codexAppServer: weixinStatus.codexAppServer } : {}),
|
|
838
|
+
},
|
|
839
|
+
} : {}),
|
|
840
|
+
authMirror: mirror.getStatus(),
|
|
841
|
+
authSync: authSync?.getStatus() ?? null,
|
|
842
|
+
authProactiveRefresh,
|
|
843
|
+
lastUpdate: lastSelfUpdate,
|
|
844
|
+
});
|
|
845
|
+
};
|
|
846
|
+
const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
|
|
847
|
+
&& (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
|
|
848
|
+
&& mirror.isIdle();
|
|
849
|
+
const clusterUpdateScheduler = createClusterUpdateScheduler({
|
|
965
850
|
selfUpdater,
|
|
966
|
-
canUpdate: () =>
|
|
967
|
-
&& (
|
|
851
|
+
canUpdate: () => authSyncLocalIdle()
|
|
852
|
+
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
968
853
|
currentVersion: readPackageVersion,
|
|
969
854
|
logger,
|
|
970
855
|
});
|
|
971
|
-
const
|
|
972
|
-
canSelfUpdate: () =>
|
|
973
|
-
&& (
|
|
974
|
-
authCandidateUpdated: (runtimeId, candidateName) =>
|
|
856
|
+
const coordinator = {
|
|
857
|
+
canSelfUpdate: () => authSyncLocalIdle()
|
|
858
|
+
&& (authSync ? authSync.isIdle() : localAuthRefreshLease.isIdle()),
|
|
859
|
+
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
975
860
|
authCandidateDeleted: async (_runtimeId, candidateName, reason = null) => {
|
|
976
|
-
await
|
|
977
|
-
await
|
|
861
|
+
await mirror.deleteCandidate(candidateName);
|
|
862
|
+
await authSync?.publishCandidateDeletion(candidateName, reason);
|
|
978
863
|
},
|
|
979
864
|
recoverAuthCandidate: async (runtimeId, candidateName, options = {}) => {
|
|
980
|
-
const local = await
|
|
865
|
+
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
981
866
|
if (local)
|
|
982
867
|
return true;
|
|
983
868
|
if (options.crossNode === false)
|
|
984
869
|
return false;
|
|
985
|
-
const current = await
|
|
986
|
-
?? await
|
|
987
|
-
|
|
988
|
-
return await singleAuthSync?.requestRecovery(candidateName, {
|
|
870
|
+
const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
|
|
871
|
+
?? await mirror.readNewestCandidate(candidateName);
|
|
872
|
+
return await authSync?.requestRecovery(candidateName, {
|
|
989
873
|
accountId: current?.accountId ?? null,
|
|
990
874
|
quotaIdentityId: current?.quotaIdentityId ?? null,
|
|
991
875
|
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
992
876
|
}) ?? false;
|
|
993
877
|
},
|
|
994
|
-
acquireAuthRefreshLease: (reason) =>
|
|
995
|
-
??
|
|
996
|
-
releaseAuthRefreshLease: (leaseId) =>
|
|
997
|
-
??
|
|
998
|
-
getAuthSyncStatus: () =>
|
|
878
|
+
acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
|
|
879
|
+
?? localAuthRefreshLease.acquire(reason),
|
|
880
|
+
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
881
|
+
?? localAuthRefreshLease.release(leaseId),
|
|
882
|
+
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
999
883
|
authSyncSafeAll: async () => {
|
|
1000
|
-
const local = await
|
|
1001
|
-
const remote = await
|
|
884
|
+
const local = await mirror.syncAllRuntimeCandidates();
|
|
885
|
+
const remote = await authSync?.pushAll() ?? { sent: 0, skipped: 0 };
|
|
1002
886
|
return {
|
|
1003
887
|
localSynced: local.synced,
|
|
1004
888
|
localSkipped: local.skipped,
|
|
@@ -1006,124 +890,148 @@ async function runServeCli() {
|
|
|
1006
890
|
skipped: remote.skipped,
|
|
1007
891
|
};
|
|
1008
892
|
},
|
|
1009
|
-
authSyncPushAll: () =>
|
|
1010
|
-
authSyncTest: () =>
|
|
1011
|
-
authSyncAudit: () =>
|
|
1012
|
-
statusUpdated: (
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
893
|
+
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
894
|
+
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
895
|
+
authSyncAudit: () => authSync?.auditCluster() ?? Promise.resolve(null),
|
|
896
|
+
statusUpdated: () => writeAggregateStatus(),
|
|
897
|
+
getServiceStatus: async () => ({
|
|
898
|
+
currentVersion: readPackageVersion(),
|
|
899
|
+
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
900
|
+
const status = runtime.core.getRuntimeStatus();
|
|
901
|
+
return {
|
|
902
|
+
id: runtime.id,
|
|
903
|
+
username: status.botUsername ?? runtime.bot.username,
|
|
904
|
+
connected: status.connected,
|
|
905
|
+
activeTurns: status.activeTurns,
|
|
906
|
+
runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
|
|
907
|
+
currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
|
|
908
|
+
codexHome: status.codexHome ?? runtime.home,
|
|
909
|
+
...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
|
|
910
|
+
};
|
|
911
|
+
})),
|
|
912
|
+
...(activeWeixinCore ? {
|
|
913
|
+
weixinRuntime: {
|
|
914
|
+
connected: activeWeixinCore.getRuntimeStatus().connected,
|
|
915
|
+
activeTurns: activeWeixinCore.getRuntimeStatus().activeTurns,
|
|
916
|
+
codexAppServer: activeWeixinCore.getRuntimeStatus().codexAppServer,
|
|
917
|
+
},
|
|
918
|
+
} : {}),
|
|
919
|
+
authMirror: mirror.getStatus(),
|
|
920
|
+
authSync: authSync?.getStatus() ?? null,
|
|
921
|
+
authProactiveRefresh: newestAuthProactiveRefreshStatus([
|
|
922
|
+
...runtimes.map(runtime => runtime.core.getRuntimeStatus()),
|
|
923
|
+
...(activeWeixinCore ? [activeWeixinCore.getRuntimeStatus()] : []),
|
|
924
|
+
]),
|
|
925
|
+
lastUpdate: lastSelfUpdate,
|
|
926
|
+
}),
|
|
927
|
+
selfUpdateCompleted: (status) => {
|
|
928
|
+
lastSelfUpdate = status;
|
|
929
|
+
writeSelfUpdateStatus(lastSelfUpdatePath, status);
|
|
930
|
+
writeAggregateStatus();
|
|
1018
931
|
},
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
onSynced: async (event) => {
|
|
1032
|
-
await singleAuthSync?.publishCandidate(event.record.candidateName);
|
|
1033
|
-
},
|
|
1034
|
-
onRemoteImported: (event) => {
|
|
1035
|
-
restoreImportedAuthCandidateState(store, event.record.candidateName, ['default']);
|
|
1036
|
-
},
|
|
932
|
+
};
|
|
933
|
+
for (const seed of seeds) {
|
|
934
|
+
const telegramMessaging = new TelegramMessagingPort(seed.bot);
|
|
935
|
+
const outbound = new BridgeMessagingRouter(telegramMessaging, null);
|
|
936
|
+
const codexCore = new BridgeSessionCore(seed.config, store, logger, seed.bot, seed.app, outbound, selfUpdater, coordinator, false);
|
|
937
|
+
const core = new UnifiedBridgeCore(seed.config, store, logger, seed.bot, seed.antigravityApp, seed.antigravityAuth, telegramMessaging, {
|
|
938
|
+
codexApp: seed.app,
|
|
939
|
+
codexCore,
|
|
940
|
+
antigravityApp: seed.antigravityApp,
|
|
941
|
+
antigravityAuth: seed.antigravityAuth,
|
|
942
|
+
defaultBackendId: seed.defaultBackend,
|
|
943
|
+
selfUpdater,
|
|
1037
944
|
});
|
|
1038
|
-
|
|
1039
|
-
|
|
945
|
+
runtimes.push({ ...seed, core, telegram: new TelegramChannelAdapter(core) });
|
|
946
|
+
}
|
|
947
|
+
if (config.wxEnabled) {
|
|
948
|
+
const weixinApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, null, codexApiProviderOverrides);
|
|
949
|
+
const outbound = new BridgeMessagingRouter(new TelegramMessagingPort(seeds[0].bot), new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id)));
|
|
950
|
+
activeWeixinCore = new BridgeSessionCore(config, store, logger, seeds[0].bot, weixinApp, outbound, selfUpdater, coordinator, false);
|
|
951
|
+
managedApps.push(weixinApp);
|
|
952
|
+
weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
|
|
1040
953
|
}
|
|
1041
|
-
|
|
1042
|
-
if (config.authSyncEnabled
|
|
1043
|
-
|
|
1044
|
-
|
|
954
|
+
activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
|
|
955
|
+
if (config.authSyncEnabled) {
|
|
956
|
+
const authSyncTransportBot = seeds[0];
|
|
957
|
+
const authSyncTransportLabel = authSyncTransportBot.bot.username
|
|
958
|
+
? `@${authSyncTransportBot.bot.username}`
|
|
959
|
+
: authSyncTransportBot.id;
|
|
960
|
+
authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config, authSyncTransportLabel), logger, {
|
|
1045
961
|
send: async (peer, envelope) => {
|
|
1046
|
-
await bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
962
|
+
await authSyncTransportBot.bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
1047
963
|
},
|
|
1048
964
|
}, {
|
|
1049
|
-
readLocalCandidate: (candidateName) =>
|
|
1050
|
-
listLocalCandidates: () =>
|
|
1051
|
-
listLocalCandidateCopies: () =>
|
|
965
|
+
readLocalCandidate: (candidateName) => mirror.readNewestCandidate(candidateName),
|
|
966
|
+
listLocalCandidates: () => mirror.listNewestCandidates(),
|
|
967
|
+
listLocalCandidateCopies: () => mirror.listCandidateCopies(),
|
|
1052
968
|
validateCandidate: async (candidateName, raw, expectedAccountId) => {
|
|
1053
|
-
if (!
|
|
969
|
+
if (!authSyncLocalIdle()) {
|
|
1054
970
|
return { ok: false, reason: 'runtime is not idle' };
|
|
1055
971
|
}
|
|
1056
|
-
|
|
972
|
+
const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
|
|
973
|
+
if (!runtime) {
|
|
974
|
+
return { ok: false, reason: 'no validation runtime is available' };
|
|
975
|
+
}
|
|
976
|
+
return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
1057
977
|
},
|
|
1058
|
-
importCandidate: (candidateName, raw, source) =>
|
|
1059
|
-
markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store,
|
|
978
|
+
importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
|
|
979
|
+
markCandidateState: (candidateName, state, expected) => markAuditedAuthCandidateState(store, mirror, candidateName, state, expected, authRuntimeIds),
|
|
1060
980
|
deleteLocalCandidate: async (candidateName, source) => {
|
|
1061
|
-
if (!
|
|
981
|
+
if (!authSyncLocalIdle()) {
|
|
1062
982
|
return { ok: false, deleted: false, reason: 'runtime is not idle' };
|
|
1063
983
|
}
|
|
1064
|
-
await
|
|
984
|
+
await mirror.deleteCandidate(candidateName);
|
|
1065
985
|
store.deleteCodexAuthCandidate(candidateName);
|
|
1066
|
-
await core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
986
|
+
await Promise.all(runtimes.map(runtime => runtime.core.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null)));
|
|
987
|
+
if (activeWeixinCore) {
|
|
988
|
+
await activeWeixinCore.handleExternalCodexAuthCandidateDeleted(candidateName, source.reason ?? null);
|
|
989
|
+
}
|
|
1067
990
|
return { ok: true, deleted: true, reason: source.reason ?? null };
|
|
1068
991
|
},
|
|
1069
|
-
scheduleServiceUpdate: (source) =>
|
|
1070
|
-
isIdle:
|
|
1071
|
-
notify: createAuthSyncNotifier(store, bot, authNotificationAggregator, {
|
|
992
|
+
scheduleServiceUpdate: (source) => clusterUpdateScheduler.schedule(source),
|
|
993
|
+
isIdle: authSyncLocalIdle,
|
|
994
|
+
notify: createAuthSyncNotifier(store, authSyncTransportBot.bot, authNotificationAggregator, {
|
|
1072
995
|
quietAuthPoolMode: () => config.authAutoDeleteNeedsRepair,
|
|
1073
996
|
suppressBackgroundNotifications: () => true,
|
|
1074
997
|
}),
|
|
1075
998
|
});
|
|
1076
|
-
await
|
|
1077
|
-
activeAuthSync =
|
|
1078
|
-
attachTelegramAuthSync(bot,
|
|
1079
|
-
|
|
1080
|
-
void publishPendingClusterUpdateBroadcast(
|
|
1081
|
-
singleMirror.start();
|
|
1082
|
-
}
|
|
1083
|
-
const telegram = new TelegramChannelAdapter(core);
|
|
1084
|
-
managedApps = [app];
|
|
1085
|
-
activeTelegramAdapters = [telegram];
|
|
1086
|
-
if (config.wxEnabled) {
|
|
1087
|
-
weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
|
|
999
|
+
await authSync.initialize();
|
|
1000
|
+
activeAuthSync = authSync;
|
|
1001
|
+
attachTelegramAuthSync(authSyncTransportBot.bot, authSync, config, logger);
|
|
1002
|
+
authSync.start();
|
|
1003
|
+
void publishPendingClusterUpdateBroadcast(authSync, logger);
|
|
1088
1004
|
}
|
|
1005
|
+
mirror.start();
|
|
1089
1006
|
process.on('unhandledRejection', (error) => {
|
|
1090
1007
|
logger.error('process.unhandled_rejection', { error: serializeError(error) });
|
|
1091
1008
|
});
|
|
1092
1009
|
process.on('uncaughtException', (error) => {
|
|
1093
1010
|
logger.error('process.uncaught_exception', { error: serializeError(error) });
|
|
1094
1011
|
});
|
|
1095
|
-
|
|
1012
|
+
for (const runtime of runtimes) {
|
|
1013
|
+
await runtime.telegram.start();
|
|
1014
|
+
}
|
|
1096
1015
|
if (weixinAdapter) {
|
|
1016
|
+
await activeWeixinCore.startCodexApp();
|
|
1097
1017
|
await weixinAdapter.start();
|
|
1098
1018
|
}
|
|
1099
|
-
|
|
1019
|
+
writeAggregateStatus();
|
|
1020
|
+
logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
|
|
1100
1021
|
const shutdown = async (signal) => {
|
|
1101
1022
|
logger.info('bridge.shutting_down', { signal });
|
|
1102
1023
|
await authNotificationAggregator.flushAll();
|
|
1103
|
-
|
|
1104
|
-
|
|
1024
|
+
authSync?.stop();
|
|
1025
|
+
mirror.stop();
|
|
1105
1026
|
await weixinAdapter?.stop();
|
|
1106
|
-
await
|
|
1027
|
+
await activeWeixinCore?.stop();
|
|
1028
|
+
await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
|
|
1107
1029
|
await activeOpencodeRuntime?.stop();
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
userAgent: app.getUserAgent(),
|
|
1112
|
-
codexHome: singleCodexHome,
|
|
1113
|
-
codexAppServer: app.getServerStatus(),
|
|
1114
|
-
botUsername: bot.username,
|
|
1115
|
-
currentBindings: 0,
|
|
1116
|
-
pendingApprovals: 0,
|
|
1117
|
-
pendingUserInputs: 0,
|
|
1118
|
-
queuedTurns: 0,
|
|
1119
|
-
activeTurns: 0,
|
|
1120
|
-
lastError: null,
|
|
1121
|
-
updatedAt: new Date().toISOString(),
|
|
1122
|
-
channels: { telegram: false, weixin: false },
|
|
1123
|
-
});
|
|
1124
|
-
await app.stop({ terminateServer: true }).catch((error) => {
|
|
1030
|
+
writeAggregateStatus(false);
|
|
1031
|
+
const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
|
|
1032
|
+
await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
|
|
1125
1033
|
logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
|
|
1126
|
-
});
|
|
1034
|
+
})));
|
|
1127
1035
|
store?.close();
|
|
1128
1036
|
processLock.release();
|
|
1129
1037
|
process.exit(0);
|
|
@@ -1599,15 +1507,22 @@ async function configureEnvInteractively(envPath, existed) {
|
|
|
1599
1507
|
const skipped = [];
|
|
1600
1508
|
const warnings = [];
|
|
1601
1509
|
Object.assign(updates, await maybeSaveProxyEnvFromShell(rl, envPath));
|
|
1602
|
-
const
|
|
1603
|
-
if (
|
|
1604
|
-
updates.
|
|
1605
|
-
if (
|
|
1606
|
-
warnings.push('One or more
|
|
1510
|
+
const codexTokens = sanitizeEnvInput(await rl.question('Codex Telegram bot token(s), comma-separated (CODEX_BOT_TOKENS): '));
|
|
1511
|
+
if (codexTokens) {
|
|
1512
|
+
updates.CODEX_BOT_TOKENS = codexTokens;
|
|
1513
|
+
if (codexTokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
|
|
1514
|
+
warnings.push('One or more CODEX_BOT_TOKENS values do not look like standard Telegram bot tokens.');
|
|
1607
1515
|
}
|
|
1608
1516
|
}
|
|
1609
1517
|
else {
|
|
1610
|
-
skipped.push('
|
|
1518
|
+
skipped.push('CODEX_BOT_TOKENS');
|
|
1519
|
+
}
|
|
1520
|
+
const agyTokens = sanitizeEnvInput(await rl.question('Antigravity Telegram bot token(s), comma-separated (ANTIGRAVITY_BOT_TOKENS) [optional]: '));
|
|
1521
|
+
if (agyTokens) {
|
|
1522
|
+
updates.ANTIGRAVITY_BOT_TOKENS = agyTokens;
|
|
1523
|
+
if (agyTokens.split(',').map((token) => token.trim()).some((token) => !/^\d+:[A-Za-z0-9_-]+$/.test(token))) {
|
|
1524
|
+
warnings.push('One or more ANTIGRAVITY_BOT_TOKENS values do not look like standard Telegram bot tokens.');
|
|
1525
|
+
}
|
|
1611
1526
|
}
|
|
1612
1527
|
const userId = sanitizeEnvInput(await rl.question('Telegram numeric user ID (TG_ALLOWED_USER_ID): '));
|
|
1613
1528
|
if (userId) {
|
|
@@ -1818,20 +1733,34 @@ function stopService() {
|
|
|
1818
1733
|
}
|
|
1819
1734
|
function runDoctorChecks() {
|
|
1820
1735
|
const configuredCodexBin = process.env.CODEX_CLI_BIN;
|
|
1736
|
+
const codexTokens = [
|
|
1737
|
+
...(process.env.CODEX_BOT_TOKENS ?? '').split(','),
|
|
1738
|
+
process.env.CODEX_BOT_TOKEN ?? '',
|
|
1739
|
+
].map((value) => value.trim()).filter(Boolean);
|
|
1740
|
+
const antigravityTokens = [
|
|
1741
|
+
...(process.env.ANTIGRAVITY_BOT_TOKENS ?? '').split(','),
|
|
1742
|
+
process.env.ANTIGRAVITY_BOT_TOKEN ?? '',
|
|
1743
|
+
].map((value) => value.trim()).filter(Boolean);
|
|
1821
1744
|
const checks = [
|
|
1822
1745
|
['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
|
|
1823
1746
|
['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')],
|
|
1824
|
-
['telegram bot token(s) configured',
|
|
1747
|
+
['telegram bot token(s) configured', codexTokens.length > 0 || antigravityTokens.length > 0],
|
|
1825
1748
|
['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
|
|
1826
1749
|
];
|
|
1827
1750
|
if (process.env.OPENCODE_BOT_TOKEN?.trim()) {
|
|
1828
1751
|
const configuredOpencodeBin = process.env.OPENCODE_CLI_BIN;
|
|
1829
1752
|
checks.push(['opencode cli available', hasConfiguredCommand(configuredOpencodeBin, 'opencode')]);
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1753
|
+
const allOtherTokens = [...codexTokens, ...antigravityTokens];
|
|
1754
|
+
checks.push(['OpenCode uses an independent Telegram bot', !allOtherTokens.includes(process.env.OPENCODE_BOT_TOKEN.trim())]);
|
|
1755
|
+
}
|
|
1756
|
+
if (antigravityTokens.length > 0) {
|
|
1757
|
+
const configuredAgyBin = process.env.ANTIGRAVITY_CLI_BIN;
|
|
1758
|
+
checks.push(['antigravity (agy) cli available', hasConfiguredCommand(configuredAgyBin, 'agy')]);
|
|
1759
|
+
const hasConflict = antigravityTokens.some((t) => codexTokens.includes(t));
|
|
1760
|
+
checks.push(['Antigravity uses independent Telegram bot(s)', !hasConflict]);
|
|
1761
|
+
const authDir = process.env.ANTIGRAVITY_AUTH_DIR || path.join(os.homedir(), '.gemini', 'antigravity-cli');
|
|
1762
|
+
const hasAuth = fs.existsSync(path.join(authDir, 'antigravity-oauth-token'));
|
|
1763
|
+
checks.push(['antigravity oauth token available', hasAuth]);
|
|
1835
1764
|
}
|
|
1836
1765
|
if (process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1') {
|
|
1837
1766
|
const accountsDir = process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts');
|