@foxden-app/foxclaw 0.4.12 → 0.4.14
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 +12 -2
- package/CHANGELOG.md +24 -0
- package/README.md +5 -3
- package/README_EN.md +5 -3
- package/dist/auth/cross_node_sync.d.ts +112 -0
- package/dist/auth/cross_node_sync.js +682 -0
- package/dist/auth/mirror.d.ts +37 -1
- package/dist/auth/mirror.js +136 -3
- package/dist/config.d.ts +10 -0
- package/dist/config.js +14 -0
- package/dist/controller/controller.d.ts +21 -0
- package/dist/controller/controller.js +200 -9
- package/dist/i18n.d.ts +42 -2
- package/dist/i18n.js +42 -2
- package/dist/main.js +192 -8
- package/dist/telegram/api.d.ts +6 -0
- package/dist/telegram/api.js +50 -0
- package/dist/telegram/gateway.d.ts +9 -0
- package/dist/telegram/gateway.js +33 -2
- package/dist/types.d.ts +16 -0
- package/dist/update.d.ts +2 -0
- package/dist/update.js +44 -6
- package/docs/user-manual.md +40 -4
- package/docs/zh/user-manual.md +40 -4
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +4 -2
package/dist/main.js
CHANGED
|
@@ -141,7 +141,7 @@ Usage:
|
|
|
141
141
|
foxclaw --help`);
|
|
142
142
|
}
|
|
143
143
|
async function runServeCli() {
|
|
144
|
-
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror },] = await Promise.all([
|
|
144
|
+
const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync },] = await Promise.all([
|
|
145
145
|
import('./channels/bridge_messaging_router.js'),
|
|
146
146
|
import('./channels/telegram/telegram_messaging_port.js'),
|
|
147
147
|
import('./channels/weixin/weixin_channel_adapter.js'),
|
|
@@ -155,6 +155,7 @@ async function runServeCli() {
|
|
|
155
155
|
import('./controller/controller.js'),
|
|
156
156
|
import('./channels/telegram/telegram_channel_adapter.js'),
|
|
157
157
|
import('./auth/mirror.js'),
|
|
158
|
+
import('./auth/cross_node_sync.js'),
|
|
158
159
|
]);
|
|
159
160
|
const config = loadConfig();
|
|
160
161
|
const logger = new Logger(config.logLevel, config.logPath);
|
|
@@ -166,6 +167,7 @@ async function runServeCli() {
|
|
|
166
167
|
let activeTelegramAdapters = [];
|
|
167
168
|
let managedApps = [];
|
|
168
169
|
let activeAuthMirror = null;
|
|
170
|
+
let activeAuthSync = null;
|
|
169
171
|
try {
|
|
170
172
|
store = new BridgeStore(config.storePath);
|
|
171
173
|
if (config.tgMultiBotMode) {
|
|
@@ -206,6 +208,7 @@ async function runServeCli() {
|
|
|
206
208
|
const app = new CodexAppClient(runtimeConfig.codexCliBin, runtimeConfig.codexAppLaunchCmd, runtimeConfig.codexAppAutolaunch, runtimeConfig.codexAppServerStatePath, runtimeConfig.codexAppServerLogPath, logger, childEnv, sharedDefaultRuntime ? [] : ['cli_auth_credentials_store="file"']);
|
|
207
209
|
seeds.push({ id, home, authDir, sharedDefaultRuntime, config: runtimeConfig, bot, app });
|
|
208
210
|
}
|
|
211
|
+
let authSync = null;
|
|
209
212
|
const mirror = new AuthCandidateMirror(canonicalAuthDir, seeds.map((runtime) => ({
|
|
210
213
|
id: runtime.id,
|
|
211
214
|
label: runtime.bot.username ? `@${runtime.bot.username}` : runtime.id,
|
|
@@ -217,9 +220,12 @@ async function runServeCli() {
|
|
|
217
220
|
await runtime.bot.sendMessage(chatId, message);
|
|
218
221
|
}
|
|
219
222
|
},
|
|
220
|
-
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json')
|
|
223
|
+
})), logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
224
|
+
onSynced: async (event) => {
|
|
225
|
+
await authSync?.publishCandidate(event.record.candidateName);
|
|
226
|
+
},
|
|
227
|
+
});
|
|
221
228
|
await mirror.initialize();
|
|
222
|
-
mirror.start();
|
|
223
229
|
activeAuthMirror = mirror;
|
|
224
230
|
managedApps = seeds.map((runtime) => runtime.app);
|
|
225
231
|
const selfUpdater = createSelfUpdateRuntime({
|
|
@@ -271,15 +277,35 @@ async function runServeCli() {
|
|
|
271
277
|
},
|
|
272
278
|
} : {}),
|
|
273
279
|
authMirror: mirror.getStatus(),
|
|
280
|
+
authSync: authSync?.getStatus() ?? null,
|
|
274
281
|
lastUpdate: lastSelfUpdate,
|
|
275
282
|
});
|
|
276
283
|
};
|
|
284
|
+
const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
|
|
285
|
+
&& (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
|
|
286
|
+
&& mirror.isIdle();
|
|
277
287
|
const coordinator = {
|
|
278
|
-
canSelfUpdate: () =>
|
|
279
|
-
&& (!
|
|
280
|
-
&& mirror.isIdle(),
|
|
288
|
+
canSelfUpdate: () => authSyncLocalIdle()
|
|
289
|
+
&& (!authSync || authSync.isIdle()),
|
|
281
290
|
authCandidateUpdated: (runtimeId, candidateName) => mirror.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined),
|
|
282
|
-
recoverAuthCandidate:
|
|
291
|
+
recoverAuthCandidate: async (runtimeId, candidateName) => {
|
|
292
|
+
const local = await mirror.recoverRuntimeCandidate(runtimeId, candidateName);
|
|
293
|
+
if (local)
|
|
294
|
+
return true;
|
|
295
|
+
const current = await mirror.readRuntimeCandidate(runtimeId, candidateName)
|
|
296
|
+
?? await mirror.readNewestCandidate(candidateName);
|
|
297
|
+
return await authSync?.requestRecovery(candidateName, {
|
|
298
|
+
accountId: current?.accountId ?? null,
|
|
299
|
+
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
300
|
+
}) ?? false;
|
|
301
|
+
},
|
|
302
|
+
acquireAuthRefreshLease: (reason) => authSync?.acquireRefreshLease(reason)
|
|
303
|
+
?? Promise.resolve({ ok: true, leaseId: null }),
|
|
304
|
+
releaseAuthRefreshLease: (leaseId) => authSync?.releaseRefreshLease(leaseId)
|
|
305
|
+
?? Promise.resolve(),
|
|
306
|
+
getAuthSyncStatus: () => authSync?.getStatus() ?? null,
|
|
307
|
+
authSyncPushAll: () => authSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
308
|
+
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0 }),
|
|
283
309
|
statusUpdated: () => writeAggregateStatus(),
|
|
284
310
|
getServiceStatus: async () => ({
|
|
285
311
|
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
@@ -302,6 +328,7 @@ async function runServeCli() {
|
|
|
302
328
|
},
|
|
303
329
|
} : {}),
|
|
304
330
|
authMirror: mirror.getStatus(),
|
|
331
|
+
authSync: authSync?.getStatus() ?? null,
|
|
305
332
|
lastUpdate: lastSelfUpdate,
|
|
306
333
|
}),
|
|
307
334
|
selfUpdateCompleted: (status) => {
|
|
@@ -324,6 +351,33 @@ async function runServeCli() {
|
|
|
324
351
|
weixinAdapter = new WeixinChannelAdapter(activeWeixinCore, store, config, logger);
|
|
325
352
|
}
|
|
326
353
|
activeTelegramAdapters = runtimes.map((runtime) => runtime.telegram);
|
|
354
|
+
if (config.authSyncEnabled) {
|
|
355
|
+
authSync = new CrossNodeAuthSync(buildAuthSyncConfig(config), logger, {
|
|
356
|
+
send: async (peer, envelope) => {
|
|
357
|
+
await seeds[0].bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
358
|
+
},
|
|
359
|
+
}, {
|
|
360
|
+
readLocalCandidate: (candidateName) => mirror.readNewestCandidate(candidateName),
|
|
361
|
+
listLocalCandidates: () => mirror.listNewestCandidates(),
|
|
362
|
+
validateCandidate: async (candidateName, raw, expectedAccountId) => {
|
|
363
|
+
if (!authSyncLocalIdle()) {
|
|
364
|
+
return { ok: false, reason: 'runtime is not idle' };
|
|
365
|
+
}
|
|
366
|
+
const runtime = runtimes.find((entry) => entry.core.isIdleForServiceUpdate()) ?? runtimes[0] ?? null;
|
|
367
|
+
if (!runtime) {
|
|
368
|
+
return { ok: false, reason: 'no validation runtime is available' };
|
|
369
|
+
}
|
|
370
|
+
return runtime.core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
371
|
+
},
|
|
372
|
+
importCandidate: (candidateName, raw, source) => mirror.importExternalCandidate(candidateName, raw, source),
|
|
373
|
+
isIdle: authSyncLocalIdle,
|
|
374
|
+
});
|
|
375
|
+
await authSync.initialize();
|
|
376
|
+
activeAuthSync = authSync;
|
|
377
|
+
attachTelegramAuthSync(seeds[0].bot, authSync, config, logger);
|
|
378
|
+
authSync.start();
|
|
379
|
+
}
|
|
380
|
+
mirror.start();
|
|
327
381
|
process.on('unhandledRejection', (error) => {
|
|
328
382
|
logger.error('process.unhandled_rejection', { error: serializeError(error) });
|
|
329
383
|
});
|
|
@@ -341,6 +395,7 @@ async function runServeCli() {
|
|
|
341
395
|
logger.info('bridge.started', { bots: runtimes.map((runtime) => runtime.id) });
|
|
342
396
|
const shutdown = async (signal) => {
|
|
343
397
|
logger.info('bridge.shutting_down', { signal });
|
|
398
|
+
authSync?.stop();
|
|
344
399
|
mirror.stop();
|
|
345
400
|
await weixinAdapter?.stop();
|
|
346
401
|
await activeWeixinCore?.stop();
|
|
@@ -372,7 +427,85 @@ async function runServeCli() {
|
|
|
372
427
|
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
373
428
|
codexCliBin: config.codexCliBin,
|
|
374
429
|
});
|
|
375
|
-
|
|
430
|
+
let singleAuthSync = null;
|
|
431
|
+
let singleMirror = null;
|
|
432
|
+
let core = null;
|
|
433
|
+
const singleAuthDir = config.codexAuthDir ?? config.codexHome ?? process.env.CODEX_AUTH_DIR ?? path.join(os.homedir(), '.codex');
|
|
434
|
+
const singleAuthSyncLocalIdle = () => Boolean(core?.isIdleForServiceUpdate())
|
|
435
|
+
&& (!singleMirror || singleMirror.isIdle());
|
|
436
|
+
const singleCoordinator = config.authSyncEnabled ? {
|
|
437
|
+
canSelfUpdate: () => singleAuthSyncLocalIdle()
|
|
438
|
+
&& (!singleAuthSync || singleAuthSync.isIdle()),
|
|
439
|
+
authCandidateUpdated: (runtimeId, candidateName) => singleMirror?.syncRuntimeCandidate(runtimeId, candidateName).then(() => undefined) ?? Promise.resolve(),
|
|
440
|
+
recoverAuthCandidate: async (runtimeId, candidateName) => {
|
|
441
|
+
const local = await singleMirror?.recoverRuntimeCandidate(runtimeId, candidateName) ?? null;
|
|
442
|
+
if (local)
|
|
443
|
+
return true;
|
|
444
|
+
const current = await singleMirror?.readRuntimeCandidate(runtimeId, candidateName)
|
|
445
|
+
?? await singleMirror?.readNewestCandidate(candidateName)
|
|
446
|
+
?? null;
|
|
447
|
+
return await singleAuthSync?.requestRecovery(candidateName, {
|
|
448
|
+
accountId: current?.accountId ?? null,
|
|
449
|
+
lastRefreshMs: current?.lastRefreshMs ?? null,
|
|
450
|
+
}) ?? false;
|
|
451
|
+
},
|
|
452
|
+
acquireAuthRefreshLease: (reason) => singleAuthSync?.acquireRefreshLease(reason)
|
|
453
|
+
?? Promise.resolve({ ok: true, leaseId: null }),
|
|
454
|
+
releaseAuthRefreshLease: (leaseId) => singleAuthSync?.releaseRefreshLease(leaseId)
|
|
455
|
+
?? Promise.resolve(),
|
|
456
|
+
getAuthSyncStatus: () => singleAuthSync?.getStatus() ?? null,
|
|
457
|
+
authSyncPushAll: () => singleAuthSync?.pushAll() ?? Promise.resolve({ sent: 0, skipped: 0 }),
|
|
458
|
+
authSyncTest: () => singleAuthSync?.testPeers() ?? Promise.resolve({ sent: 0 }),
|
|
459
|
+
statusUpdated: (status) => {
|
|
460
|
+
writeRuntimeStatus(config.statusPath, {
|
|
461
|
+
...status,
|
|
462
|
+
authMirror: singleMirror?.getStatus() ?? null,
|
|
463
|
+
authSync: singleAuthSync?.getStatus() ?? null,
|
|
464
|
+
});
|
|
465
|
+
},
|
|
466
|
+
} : null;
|
|
467
|
+
if (config.authSyncEnabled) {
|
|
468
|
+
singleMirror = new AuthCandidateMirror(singleAuthDir, [{
|
|
469
|
+
id: 'default',
|
|
470
|
+
label: bot.username ? `@${bot.username}` : 'default',
|
|
471
|
+
authDir: singleAuthDir,
|
|
472
|
+
validate: async (context) => validateRefreshedAuthCandidate({
|
|
473
|
+
id: 'default',
|
|
474
|
+
authDir: singleAuthDir,
|
|
475
|
+
app,
|
|
476
|
+
}, context.candidateName),
|
|
477
|
+
}], logger, path.join(APP_HOME, 'runtime', 'auth-mirror.json'), {
|
|
478
|
+
onSynced: async (event) => {
|
|
479
|
+
await singleAuthSync?.publishCandidate(event.record.candidateName);
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
await singleMirror.initialize();
|
|
483
|
+
activeAuthMirror = singleMirror;
|
|
484
|
+
}
|
|
485
|
+
core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater, singleCoordinator);
|
|
486
|
+
if (config.authSyncEnabled && singleMirror) {
|
|
487
|
+
singleAuthSync = new CrossNodeAuthSync(buildAuthSyncConfig(config), logger, {
|
|
488
|
+
send: async (peer, envelope) => {
|
|
489
|
+
await bot.sendDocument(peer, `foxclaw-auth-sync-${Date.now()}.json`, Buffer.from(envelope, 'utf8'), 'FOXCLAW_AUTH_SYNC_V1');
|
|
490
|
+
},
|
|
491
|
+
}, {
|
|
492
|
+
readLocalCandidate: (candidateName) => singleMirror.readNewestCandidate(candidateName),
|
|
493
|
+
listLocalCandidates: () => singleMirror.listNewestCandidates(),
|
|
494
|
+
validateCandidate: async (candidateName, raw, expectedAccountId) => {
|
|
495
|
+
if (!singleAuthSyncLocalIdle()) {
|
|
496
|
+
return { ok: false, reason: 'runtime is not idle' };
|
|
497
|
+
}
|
|
498
|
+
return core.validateExternalCodexAuthCandidate(candidateName, raw, expectedAccountId);
|
|
499
|
+
},
|
|
500
|
+
importCandidate: (candidateName, raw, source) => singleMirror.importExternalCandidate(candidateName, raw, source),
|
|
501
|
+
isIdle: singleAuthSyncLocalIdle,
|
|
502
|
+
});
|
|
503
|
+
await singleAuthSync.initialize();
|
|
504
|
+
activeAuthSync = singleAuthSync;
|
|
505
|
+
attachTelegramAuthSync(bot, singleAuthSync, config, logger);
|
|
506
|
+
singleAuthSync.start();
|
|
507
|
+
singleMirror.start();
|
|
508
|
+
}
|
|
376
509
|
const telegram = new TelegramChannelAdapter(core);
|
|
377
510
|
managedApps = [app];
|
|
378
511
|
activeTelegramAdapters = [telegram];
|
|
@@ -392,6 +525,8 @@ async function runServeCli() {
|
|
|
392
525
|
logger.info('bridge.started', core.getRuntimeStatus());
|
|
393
526
|
const shutdown = async (signal) => {
|
|
394
527
|
logger.info('bridge.shutting_down', { signal });
|
|
528
|
+
singleAuthSync?.stop();
|
|
529
|
+
singleMirror?.stop();
|
|
395
530
|
await weixinAdapter?.stop();
|
|
396
531
|
await telegram.stop();
|
|
397
532
|
writeRuntimeStatus(config.statusPath, {
|
|
@@ -419,6 +554,7 @@ async function runServeCli() {
|
|
|
419
554
|
process.on('SIGTERM', () => void shutdown('SIGTERM'));
|
|
420
555
|
}
|
|
421
556
|
catch (error) {
|
|
557
|
+
activeAuthSync?.stop();
|
|
422
558
|
activeAuthMirror?.stop();
|
|
423
559
|
await weixinAdapter?.stop().catch(() => { });
|
|
424
560
|
await activeWeixinCore?.stop().catch(() => { });
|
|
@@ -429,6 +565,54 @@ async function runServeCli() {
|
|
|
429
565
|
throw error;
|
|
430
566
|
}
|
|
431
567
|
}
|
|
568
|
+
const AUTH_SYNC_TELEGRAM_CAPTION = 'FOXCLAW_AUTH_SYNC_V1';
|
|
569
|
+
function buildAuthSyncConfig(config) {
|
|
570
|
+
return {
|
|
571
|
+
enabled: config.authSyncEnabled,
|
|
572
|
+
transport: config.authSyncTransport,
|
|
573
|
+
key: config.authSyncKey,
|
|
574
|
+
peers: config.authSyncPeers,
|
|
575
|
+
nodeId: config.authSyncNodeId,
|
|
576
|
+
clusterId: config.authSyncClusterId,
|
|
577
|
+
statePath: config.authSyncStatePath,
|
|
578
|
+
tempDir: config.authSyncTempDir,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function attachTelegramAuthSync(bot, sync, config, logger) {
|
|
582
|
+
bot.on('peerDocument', (event) => {
|
|
583
|
+
void handleTelegramAuthSyncDocument(bot, sync, config, logger, event).catch((error) => {
|
|
584
|
+
logger.warn('auth.sync.telegram_document_failed', { error: serializeError(error) });
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
async function handleTelegramAuthSyncDocument(bot, sync, config, logger, event) {
|
|
589
|
+
const fileName = event.attachment.fileName ?? '';
|
|
590
|
+
if (event.text.trim() !== AUTH_SYNC_TELEGRAM_CAPTION && !fileName.startsWith('foxclaw-auth-sync-')) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const remoteFile = await bot.getFile(event.attachment.fileId);
|
|
594
|
+
if (!remoteFile.file_path) {
|
|
595
|
+
throw new Error('Telegram did not return file_path for auth sync document');
|
|
596
|
+
}
|
|
597
|
+
await fs.promises.mkdir(config.authSyncTempDir, { recursive: true, mode: 0o700 });
|
|
598
|
+
const destination = path.join(config.authSyncTempDir, `inbound-${process.pid}-${Date.now()}-${event.messageId}.json`);
|
|
599
|
+
try {
|
|
600
|
+
await bot.downloadResolvedFile(remoteFile.file_path, destination);
|
|
601
|
+
const raw = await fs.promises.readFile(destination, 'utf8');
|
|
602
|
+
const handled = await sync.handleIncomingEnvelope(raw, {
|
|
603
|
+
userId: event.userId,
|
|
604
|
+
username: event.username,
|
|
605
|
+
});
|
|
606
|
+
if (handled) {
|
|
607
|
+
logger.info('auth.sync.telegram_document_handled', {
|
|
608
|
+
peer: event.username ? `@${event.username}` : event.userId,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
await fs.promises.rm(destination, { force: true }).catch(() => undefined);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
432
616
|
async function validateRefreshedAuthCandidate(runtime, candidateName) {
|
|
433
617
|
const authPath = path.join(runtime.authDir, 'auth.json');
|
|
434
618
|
const candidatePath = path.join(runtime.authDir, candidateName);
|
package/dist/telegram/api.d.ts
CHANGED
|
@@ -10,5 +10,11 @@ export interface TelegramRemoteFile {
|
|
|
10
10
|
file_path?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>): Promise<TelegramApiResult<T>>;
|
|
13
|
+
export declare function callTelegramMultipartApi<T>(botToken: string, method: string, fields: Record<string, string>, files: Array<{
|
|
14
|
+
fieldName: string;
|
|
15
|
+
filename: string;
|
|
16
|
+
contents: Buffer;
|
|
17
|
+
contentType: string;
|
|
18
|
+
}>): Promise<TelegramApiResult<T>>;
|
|
13
19
|
export declare function getTelegramFile(botToken: string, fileId: string): Promise<TelegramRemoteFile>;
|
|
14
20
|
export declare function downloadTelegramFile(botToken: string, remoteFilePath: string, destinationPath: string): Promise<number>;
|
package/dist/telegram/api.js
CHANGED
|
@@ -39,6 +39,53 @@ export async function callTelegramApi(botToken, method, body) {
|
|
|
39
39
|
request.end();
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
|
+
export async function callTelegramMultipartApi(botToken, method, fields, files) {
|
|
43
|
+
const boundary = `foxclaw-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (const [name, value] of Object.entries(fields)) {
|
|
46
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(name)}"\r\n\r\n${value}\r\n`, 'utf8'));
|
|
47
|
+
}
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${escapeMultipartName(file.fieldName)}"; filename="${escapeMultipartName(file.filename)}"\r\nContent-Type: ${file.contentType}\r\n\r\n`, 'utf8'));
|
|
50
|
+
parts.push(file.contents);
|
|
51
|
+
parts.push(Buffer.from('\r\n', 'utf8'));
|
|
52
|
+
}
|
|
53
|
+
parts.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
|
|
54
|
+
const payload = Buffer.concat(parts);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const request = https.request({
|
|
57
|
+
host: API_HOST,
|
|
58
|
+
port: 443,
|
|
59
|
+
path: `/bot${botToken}/${method}`,
|
|
60
|
+
method: 'POST',
|
|
61
|
+
family: 4,
|
|
62
|
+
headers: {
|
|
63
|
+
'content-type': `multipart/form-data; boundary=${boundary}`,
|
|
64
|
+
'content-length': payload.length,
|
|
65
|
+
},
|
|
66
|
+
}, (response) => {
|
|
67
|
+
const chunks = [];
|
|
68
|
+
response.on('data', (chunk) => {
|
|
69
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
70
|
+
});
|
|
71
|
+
response.on('end', () => {
|
|
72
|
+
try {
|
|
73
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
74
|
+
resolve(JSON.parse(text));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
reject(new Error(`Failed to parse Telegram response: ${String(error)}`));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
request.on('error', reject);
|
|
82
|
+
request.setTimeout(20_000, () => {
|
|
83
|
+
request.destroy(new Error(`Telegram API request timed out for ${method}`));
|
|
84
|
+
});
|
|
85
|
+
request.write(payload);
|
|
86
|
+
request.end();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
42
89
|
export async function getTelegramFile(botToken, fileId) {
|
|
43
90
|
const result = await callTelegramApi(botToken, 'getFile', { file_id: fileId });
|
|
44
91
|
if (!result.ok || !result.result) {
|
|
@@ -87,3 +134,6 @@ export async function downloadTelegramFile(botToken, remoteFilePath, destination
|
|
|
87
134
|
response?.destroy();
|
|
88
135
|
}
|
|
89
136
|
}
|
|
137
|
+
function escapeMultipartName(value) {
|
|
138
|
+
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\r', '').replaceAll('\n', '');
|
|
139
|
+
}
|
|
@@ -17,6 +17,14 @@ export interface TelegramTextEvent {
|
|
|
17
17
|
replyToBot: boolean;
|
|
18
18
|
languageCode?: string;
|
|
19
19
|
}
|
|
20
|
+
export interface TelegramPeerDocumentEvent {
|
|
21
|
+
chatId: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
username: string | null;
|
|
24
|
+
messageId: number;
|
|
25
|
+
text: string;
|
|
26
|
+
attachment: TelegramInboundAttachment;
|
|
27
|
+
}
|
|
20
28
|
export interface TelegramCallbackEvent {
|
|
21
29
|
chatId: string;
|
|
22
30
|
topicId: number | null;
|
|
@@ -53,6 +61,7 @@ export declare class TelegramGateway extends EventEmitter {
|
|
|
53
61
|
text: string;
|
|
54
62
|
callback_data: string;
|
|
55
63
|
}>>, messageThreadId?: number | null): Promise<number>;
|
|
64
|
+
sendDocument(chatId: string, filename: string, contents: Buffer, caption?: string): Promise<number>;
|
|
56
65
|
sendMessageDraft(chatId: string, draftId: number, text: string, messageThreadId?: number | null): Promise<void>;
|
|
57
66
|
editMessage(chatId: string, messageId: number, text: string, inlineKeyboard?: Array<Array<{
|
|
58
67
|
text: string;
|
package/dist/telegram/gateway.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
-
import { callTelegramApi, downloadTelegramFile, getTelegramFile } from './api.js';
|
|
3
|
+
import { callTelegramApi, callTelegramMultipartApi, downloadTelegramFile, getTelegramFile } from './api.js';
|
|
4
4
|
import { getTelegramCommands } from '../i18n.js';
|
|
5
5
|
import { toTelegramBridgeScopeId } from '../core/bridge_scope.js';
|
|
6
6
|
import { createTelegramScopeId } from './scope.js';
|
|
@@ -56,6 +56,21 @@ export class TelegramGateway extends EventEmitter {
|
|
|
56
56
|
async sendHtmlMessage(chatId, text, inlineKeyboard, messageThreadId) {
|
|
57
57
|
return this.sendMessageWithOptions(chatId, text, inlineKeyboard, 'HTML', messageThreadId);
|
|
58
58
|
}
|
|
59
|
+
async sendDocument(chatId, filename, contents, caption) {
|
|
60
|
+
const result = await callTelegramMultipartApi(this.botToken, 'sendDocument', {
|
|
61
|
+
chat_id: chatId,
|
|
62
|
+
...(caption ? { caption } : {}),
|
|
63
|
+
}, [{
|
|
64
|
+
fieldName: 'document',
|
|
65
|
+
filename,
|
|
66
|
+
contents,
|
|
67
|
+
contentType: 'application/json',
|
|
68
|
+
}]);
|
|
69
|
+
if (!result.ok || !result.result) {
|
|
70
|
+
throw new Error(result.description || 'Failed to send Telegram document');
|
|
71
|
+
}
|
|
72
|
+
return result.result.message_id;
|
|
73
|
+
}
|
|
59
74
|
async sendMessageDraft(chatId, draftId, text, messageThreadId) {
|
|
60
75
|
const result = await callTelegramApi(this.botToken, 'sendMessageDraft', {
|
|
61
76
|
chat_id: chatId,
|
|
@@ -197,8 +212,24 @@ export class TelegramGateway extends EventEmitter {
|
|
|
197
212
|
}
|
|
198
213
|
async handleUpdate(update) {
|
|
199
214
|
if (update.message && update.message.from && this.isAllowedChat(update.message.chat)) {
|
|
200
|
-
if (String(update.message.from.id) !== this.allowedUserId)
|
|
215
|
+
if (String(update.message.from.id) !== this.allowedUserId) {
|
|
216
|
+
if (update.message.chat.type === 'private') {
|
|
217
|
+
const text = update.message.text ?? update.message.caption ?? '';
|
|
218
|
+
const attachments = extractAttachments(update.message);
|
|
219
|
+
const document = attachments.find((attachment) => attachment.kind === 'document');
|
|
220
|
+
if (document) {
|
|
221
|
+
this.emit('peerDocument', {
|
|
222
|
+
chatId: String(update.message.chat.id),
|
|
223
|
+
userId: String(update.message.from.id),
|
|
224
|
+
username: update.message.from.username ?? null,
|
|
225
|
+
text,
|
|
226
|
+
messageId: update.message.message_id,
|
|
227
|
+
attachment: document,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
201
231
|
return;
|
|
232
|
+
}
|
|
202
233
|
const attachments = extractAttachments(update.message);
|
|
203
234
|
const text = update.message.text ?? update.message.caption ?? '';
|
|
204
235
|
const topicId = update.message.message_thread_id ?? null;
|
package/dist/types.d.ts
CHANGED
|
@@ -360,11 +360,27 @@ export interface RuntimeStatus {
|
|
|
360
360
|
sourceLabel: string;
|
|
361
361
|
syncedAt: string;
|
|
362
362
|
} | null;
|
|
363
|
+
authSync?: {
|
|
364
|
+
enabled: boolean;
|
|
365
|
+
nodeId: string | null;
|
|
366
|
+
peers: string[];
|
|
367
|
+
pendingImports: number;
|
|
368
|
+
lastSentAt: string | null;
|
|
369
|
+
lastReceivedAt: string | null;
|
|
370
|
+
lastImportedAt: string | null;
|
|
371
|
+
lastImportCandidate: string | null;
|
|
372
|
+
lastPullAt: string | null;
|
|
373
|
+
lastPullCandidate: string | null;
|
|
374
|
+
lastError: string | null;
|
|
375
|
+
activeLeaseId: string | null;
|
|
376
|
+
} | null;
|
|
363
377
|
lastUpdate?: {
|
|
364
378
|
state: string;
|
|
365
379
|
fromVersion: string;
|
|
366
380
|
toVersion: string | null;
|
|
367
381
|
codexUpdate?: string | null;
|
|
382
|
+
codexFromVersion?: string | null;
|
|
383
|
+
codexToVersion?: string | null;
|
|
368
384
|
updatedAt: string;
|
|
369
385
|
} | null;
|
|
370
386
|
}
|
package/dist/update.d.ts
CHANGED
package/dist/update.js
CHANGED
|
@@ -132,6 +132,8 @@ export function readSelfUpdateStatus(statusFile) {
|
|
|
132
132
|
fromVersion: parsed.fromVersion,
|
|
133
133
|
toVersion: typeof parsed.toVersion === 'string' ? parsed.toVersion : null,
|
|
134
134
|
...(typeof parsed.codexUpdate === 'string' ? { codexUpdate: parsed.codexUpdate } : {}),
|
|
135
|
+
...(typeof parsed.codexFromVersion === 'string' ? { codexFromVersion: parsed.codexFromVersion } : {}),
|
|
136
|
+
...(typeof parsed.codexToVersion === 'string' ? { codexToVersion: parsed.codexToVersion } : {}),
|
|
135
137
|
error: typeof parsed.error === 'string' ? parsed.error : null,
|
|
136
138
|
updatedAt: parsed.updatedAt,
|
|
137
139
|
};
|
|
@@ -161,6 +163,8 @@ export function createSelfUpdateRuntime(options) {
|
|
|
161
163
|
fromVersion: options.version,
|
|
162
164
|
toVersion: null,
|
|
163
165
|
codexUpdate: null,
|
|
166
|
+
codexFromVersion: null,
|
|
167
|
+
codexToVersion: null,
|
|
164
168
|
error: null,
|
|
165
169
|
updatedAt: new Date().toISOString(),
|
|
166
170
|
});
|
|
@@ -182,6 +186,8 @@ export function createSelfUpdateRuntime(options) {
|
|
|
182
186
|
fromVersion: options.version,
|
|
183
187
|
toVersion: null,
|
|
184
188
|
codexUpdate: null,
|
|
189
|
+
codexFromVersion: null,
|
|
190
|
+
codexToVersion: null,
|
|
185
191
|
error: formatError(error),
|
|
186
192
|
updatedAt: new Date().toISOString(),
|
|
187
193
|
});
|
|
@@ -205,7 +211,7 @@ export function performSelfUpdate(options) {
|
|
|
205
211
|
let codexUpdate = null;
|
|
206
212
|
try {
|
|
207
213
|
codexUpdate = updateManagedCodexCli(options.codexCliBin ?? env.CODEX_CLI_BIN ?? '', options.nodePath, env);
|
|
208
|
-
console.log(`[UPDATE] ${codexUpdate}`);
|
|
214
|
+
console.log(`[UPDATE] ${codexUpdate.message}`);
|
|
209
215
|
const installer = resolveSelfUpdateInstaller(options.entryPoint, options.nodePath, fs.existsSync, env);
|
|
210
216
|
const installerEnv = buildInstallerEnv(options.entryPoint, installer, env);
|
|
211
217
|
console.log(`[UPDATE] Installing ${PACKAGE_SPEC} with ${installer.manager}...`);
|
|
@@ -236,22 +242,52 @@ export function performSelfUpdate(options) {
|
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
function updateManagedCodexCli(codexCliBin, nodePath, env) {
|
|
245
|
+
const fromVersion = readCodexCliVersion(codexCliBin, env);
|
|
239
246
|
if (!codexCliBin) {
|
|
240
|
-
return
|
|
247
|
+
return {
|
|
248
|
+
message: 'Codex CLI update skipped: CODEX_CLI_BIN is not configured.',
|
|
249
|
+
fromVersion,
|
|
250
|
+
toVersion: fromVersion,
|
|
251
|
+
};
|
|
241
252
|
}
|
|
242
253
|
const installer = resolveCodexUpdateInstaller(codexCliBin, nodePath, fs.existsSync, env);
|
|
243
254
|
if (!installer) {
|
|
244
|
-
return
|
|
255
|
+
return {
|
|
256
|
+
message: 'Codex CLI update skipped: configured installation is not a recognized global npm/pnpm package.',
|
|
257
|
+
fromVersion,
|
|
258
|
+
toVersion: fromVersion,
|
|
259
|
+
};
|
|
245
260
|
}
|
|
246
261
|
try {
|
|
247
262
|
const installerEnv = buildInstallerEnv(fs.realpathSync(codexCliBin), installer, env);
|
|
248
263
|
runInherited(installer.command, installer.installArgs, installerEnv);
|
|
249
|
-
return
|
|
264
|
+
return {
|
|
265
|
+
message: `Codex CLI updated with ${installer.manager}.`,
|
|
266
|
+
fromVersion,
|
|
267
|
+
toVersion: readCodexCliVersion(codexCliBin, installerEnv) ?? fromVersion,
|
|
268
|
+
};
|
|
250
269
|
}
|
|
251
270
|
catch (error) {
|
|
252
|
-
return
|
|
271
|
+
return {
|
|
272
|
+
message: `Codex CLI update failed without blocking FoxClaw update: ${formatError(error)}`,
|
|
273
|
+
fromVersion,
|
|
274
|
+
toVersion: readCodexCliVersion(codexCliBin, env) ?? fromVersion,
|
|
275
|
+
};
|
|
253
276
|
}
|
|
254
277
|
}
|
|
278
|
+
function readCodexCliVersion(codexCliBin, env) {
|
|
279
|
+
if (!codexCliBin) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const result = spawnSync(codexCliBin, ['--version'], { encoding: 'utf8', env });
|
|
283
|
+
if (result.error || result.status !== 0) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
return parseCodexCliVersion(`${result.stdout}\n${result.stderr}`);
|
|
287
|
+
}
|
|
288
|
+
function parseCodexCliVersion(output) {
|
|
289
|
+
return output.match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/)?.[0] ?? null;
|
|
290
|
+
}
|
|
255
291
|
function executableCandidates(commandName, nodePath, env, preferred = []) {
|
|
256
292
|
return [
|
|
257
293
|
...preferred,
|
|
@@ -329,7 +365,9 @@ function completeNotification(notificationFile, state, toVersion, codexUpdate, e
|
|
|
329
365
|
...pending,
|
|
330
366
|
state,
|
|
331
367
|
toVersion,
|
|
332
|
-
codexUpdate,
|
|
368
|
+
codexUpdate: codexUpdate?.message ?? null,
|
|
369
|
+
codexFromVersion: codexUpdate?.fromVersion ?? null,
|
|
370
|
+
codexToVersion: codexUpdate?.toVersion ?? null,
|
|
333
371
|
error,
|
|
334
372
|
updatedAt: new Date().toISOString(),
|
|
335
373
|
});
|