@llblab/pi-telegram 0.21.0 → 0.22.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/AGENTS.md +6 -4
- package/CHANGELOG.md +477 -447
- package/README.md +9 -7
- package/docs/architecture.md +26 -14
- package/docs/delivery.md +2 -1
- package/docs/locks.md +9 -5
- package/docs/multi-instance-bus.md +5 -5
- package/docs/public-api.md +2 -2
- package/index.ts +311 -633
- package/lib/bindings.ts +59 -21
- package/lib/bus-api.ts +12 -2
- package/lib/bus-follower.ts +327 -83
- package/lib/bus-leader.ts +201 -121
- package/lib/bus.ts +345 -37
- package/lib/config.ts +168 -28
- package/lib/delivery.ts +148 -61
- package/lib/lifecycle.ts +199 -8
- package/lib/locks.ts +569 -56
- package/lib/logs.ts +178 -19
- package/lib/media.ts +79 -24
- package/lib/model.ts +5 -10
- package/lib/ownership.ts +150 -6
- package/lib/polling.ts +156 -7
- package/lib/preview.ts +15 -3
- package/lib/queue.ts +167 -20
- package/lib/routing.ts +68 -12
- package/lib/sync.ts +105 -32
- package/lib/telegram-api.ts +86 -10
- package/lib/text-groups.ts +71 -15
- package/lib/thread-reconciler.ts +49 -36
- package/lib/threads.ts +505 -118
- package/package.json +1 -1
package/lib/polling.ts
CHANGED
|
@@ -346,10 +346,36 @@ export interface TelegramThreadCapabilityMonitor<TContext> {
|
|
|
346
346
|
stop: () => void;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
export interface TelegramThreadCapabilityStateRuntime {
|
|
350
|
+
isBusPollingStarted(): boolean;
|
|
351
|
+
setBusPollingStarted(started: boolean): void;
|
|
352
|
+
isTopicModeUnavailable(): boolean;
|
|
353
|
+
setTopicModeUnavailable(unavailable: boolean): void;
|
|
354
|
+
shouldForceFreshLeaderThread(): boolean;
|
|
355
|
+
setForceFreshLeaderThread(forceFresh: boolean): void;
|
|
356
|
+
}
|
|
357
|
+
|
|
349
358
|
export type TelegramThreadTargetObservationHandler<TContext> = (
|
|
350
359
|
ctx: TContext,
|
|
351
360
|
) => Promise<void>;
|
|
352
361
|
|
|
362
|
+
export interface TelegramThreadTargetObservationBinding<TContext> {
|
|
363
|
+
handle: TelegramThreadTargetObservationHandler<TContext>;
|
|
364
|
+
set(handler: TelegramThreadTargetObservationHandler<TContext>): void;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function createTelegramThreadTargetObservationBinding<TContext>(): TelegramThreadTargetObservationBinding<TContext> {
|
|
368
|
+
let handler: TelegramThreadTargetObservationHandler<TContext> | undefined;
|
|
369
|
+
return {
|
|
370
|
+
async handle(ctx) {
|
|
371
|
+
await handler?.(ctx);
|
|
372
|
+
},
|
|
373
|
+
set(nextHandler) {
|
|
374
|
+
handler = nextHandler;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
353
379
|
export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
|
|
354
380
|
startPolling: (
|
|
355
381
|
ctx: TContext,
|
|
@@ -386,6 +412,112 @@ export interface TelegramThreadAwarePollingDeps<
|
|
|
386
412
|
stopFollowerRegistration: () => void;
|
|
387
413
|
}
|
|
388
414
|
|
|
415
|
+
export interface TelegramThreadCapabilityOrchestrationDeps<
|
|
416
|
+
TContext,
|
|
417
|
+
TOwner,
|
|
418
|
+
> extends TelegramThreadCapabilityReaderDeps {
|
|
419
|
+
state: TelegramThreadCapabilityStateRuntime;
|
|
420
|
+
topicTargetStore: TelegramThreadCapabilityStore;
|
|
421
|
+
isBusConfigured: () => boolean;
|
|
422
|
+
isBusRuntimeEnabled: () => boolean;
|
|
423
|
+
ownsLock: (ctx: TContext) => boolean;
|
|
424
|
+
startClassicPolling: (ctx: TContext) => MaybePromise<void>;
|
|
425
|
+
stopClassicPolling: () => Promise<void>;
|
|
426
|
+
startBusLeaderPolling: (ctx: TContext) => Promise<void>;
|
|
427
|
+
stopBusLeaderPolling: () => Promise<void>;
|
|
428
|
+
startLeaderHealth: () => void;
|
|
429
|
+
stopLeaderHealth: () => void;
|
|
430
|
+
registerFollowerWithLeader: (
|
|
431
|
+
ctx: TContext,
|
|
432
|
+
owner: TOwner,
|
|
433
|
+
) => Promise<boolean | undefined>;
|
|
434
|
+
stopFollowerRegistration: () => void;
|
|
435
|
+
isTopicModeUnavailableError: (error: unknown) => boolean;
|
|
436
|
+
updateStatus: (ctx: TContext) => void;
|
|
437
|
+
recordEvent: (
|
|
438
|
+
category: string,
|
|
439
|
+
message: unknown,
|
|
440
|
+
details?: Record<string, unknown>,
|
|
441
|
+
) => void;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface TelegramThreadCapabilityOrchestration<TContext, TOwner> {
|
|
445
|
+
monitor: TelegramThreadCapabilityMonitor<TContext>;
|
|
446
|
+
observeTarget: TelegramThreadTargetObservationHandler<TContext>;
|
|
447
|
+
pollingPorts: TelegramThreadAwarePollingPorts<TContext, TOwner>;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function createTelegramThreadCapabilityStateRuntime(): TelegramThreadCapabilityStateRuntime {
|
|
451
|
+
let busPollingStarted = false;
|
|
452
|
+
let topicModeUnavailable = false;
|
|
453
|
+
let forceFreshLeaderThread = false;
|
|
454
|
+
return {
|
|
455
|
+
isBusPollingStarted: () => busPollingStarted,
|
|
456
|
+
setBusPollingStarted(started) {
|
|
457
|
+
busPollingStarted = started;
|
|
458
|
+
},
|
|
459
|
+
isTopicModeUnavailable: () => topicModeUnavailable,
|
|
460
|
+
setTopicModeUnavailable(unavailable) {
|
|
461
|
+
topicModeUnavailable = unavailable;
|
|
462
|
+
},
|
|
463
|
+
shouldForceFreshLeaderThread: () => forceFreshLeaderThread,
|
|
464
|
+
setForceFreshLeaderThread(forceFresh) {
|
|
465
|
+
forceFreshLeaderThread = forceFresh;
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
|
|
471
|
+
deps: TelegramThreadCapabilityOrchestrationDeps<TContext, TOwner>,
|
|
472
|
+
): TelegramThreadCapabilityOrchestration<TContext, TOwner> {
|
|
473
|
+
const capabilityDeps: TelegramThreadCapabilityRuntimeDeps<TContext> = {
|
|
474
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
475
|
+
callApi: deps.callApi,
|
|
476
|
+
topicTargetStore: deps.topicTargetStore,
|
|
477
|
+
isBusConfigured: deps.isBusConfigured,
|
|
478
|
+
ownsLock: deps.ownsLock,
|
|
479
|
+
getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
|
|
480
|
+
setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
|
|
481
|
+
setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
|
|
482
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
483
|
+
startClassicPolling: deps.startClassicPolling,
|
|
484
|
+
stopClassicPolling: deps.stopClassicPolling,
|
|
485
|
+
startBusPolling: deps.startBusLeaderPolling,
|
|
486
|
+
stopBusPolling: deps.stopBusLeaderPolling,
|
|
487
|
+
startLeaderHealth: deps.startLeaderHealth,
|
|
488
|
+
stopLeaderHealth: deps.stopLeaderHealth,
|
|
489
|
+
isTopicModeUnavailableError: deps.isTopicModeUnavailableError,
|
|
490
|
+
updateStatus: deps.updateStatus,
|
|
491
|
+
recordEvent: deps.recordEvent,
|
|
492
|
+
};
|
|
493
|
+
return {
|
|
494
|
+
monitor: createTelegramThreadCapabilityMonitor(capabilityDeps),
|
|
495
|
+
observeTarget: createTelegramThreadTargetObservationHandler(capabilityDeps),
|
|
496
|
+
pollingPorts: createTelegramThreadAwarePollingPorts({
|
|
497
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
498
|
+
callApi: deps.callApi,
|
|
499
|
+
topicTargetStore: deps.topicTargetStore,
|
|
500
|
+
isBusConfigured: deps.isBusConfigured,
|
|
501
|
+
isBusRuntimeEnabled: deps.isBusRuntimeEnabled,
|
|
502
|
+
isTopicModeUnavailableError: deps.isTopicModeUnavailableError,
|
|
503
|
+
getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
|
|
504
|
+
setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
|
|
505
|
+
setForceFreshLeaderThreadOnNextStart:
|
|
506
|
+
deps.state.setForceFreshLeaderThread,
|
|
507
|
+
startClassicPolling: deps.startClassicPolling,
|
|
508
|
+
stopClassicPolling: deps.stopClassicPolling,
|
|
509
|
+
startBusLeaderPolling: deps.startBusLeaderPolling,
|
|
510
|
+
stopBusLeaderPolling: deps.stopBusLeaderPolling,
|
|
511
|
+
startLeaderHealth: deps.startLeaderHealth,
|
|
512
|
+
stopLeaderHealth: deps.stopLeaderHealth,
|
|
513
|
+
registerFollowerWithLeader: deps.registerFollowerWithLeader,
|
|
514
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
515
|
+
recordEvent: deps.recordEvent,
|
|
516
|
+
setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
|
|
517
|
+
}),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
389
521
|
export async function readTelegramThreadCapability(
|
|
390
522
|
deps: TelegramThreadCapabilityReaderDeps,
|
|
391
523
|
): Promise<boolean | undefined> {
|
|
@@ -429,7 +561,9 @@ export async function probeTelegramStartupThreadCapability(
|
|
|
429
561
|
function hasTelegramClassicRestoreFailure(
|
|
430
562
|
state: TelegramThreadCapabilityState,
|
|
431
563
|
): boolean {
|
|
432
|
-
return
|
|
564
|
+
return (
|
|
565
|
+
state.lastReconcileAction?.endsWith("-classic-restore-failed") ?? false
|
|
566
|
+
);
|
|
433
567
|
}
|
|
434
568
|
|
|
435
569
|
function hasTelegramThreadCapabilityBindings(
|
|
@@ -698,7 +832,10 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
698
832
|
const current = botState.threadMode;
|
|
699
833
|
if (threadModeEnabled && current === "enabled") return;
|
|
700
834
|
if (!threadModeEnabled && current === "disabled") {
|
|
701
|
-
if (
|
|
835
|
+
if (
|
|
836
|
+
!deps.ownsLock(ctx) ||
|
|
837
|
+
!hasTelegramClassicRestoreFailure(botState)
|
|
838
|
+
) {
|
|
702
839
|
return;
|
|
703
840
|
}
|
|
704
841
|
await applyTelegramThreadCapability(
|
|
@@ -770,7 +907,7 @@ export interface TelegramPollLoopDeps<
|
|
|
770
907
|
body: Record<string, unknown>,
|
|
771
908
|
signal: AbortSignal,
|
|
772
909
|
) => Promise<TUpdate[]>;
|
|
773
|
-
persistConfig: () => Promise<void>;
|
|
910
|
+
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
774
911
|
handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
775
912
|
onErrorStatus: (message: string) => void;
|
|
776
913
|
onStatusReset: () => void;
|
|
@@ -788,7 +925,7 @@ export interface TelegramPollLoopRunnerDeps<
|
|
|
788
925
|
body: Record<string, unknown>,
|
|
789
926
|
signal: AbortSignal,
|
|
790
927
|
) => Promise<TUpdate[]>;
|
|
791
|
-
persistConfig: () => Promise<void>;
|
|
928
|
+
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
792
929
|
handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
793
930
|
updateStatus: (ctx: TContext, message?: string) => void;
|
|
794
931
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
@@ -882,7 +1019,7 @@ export async function runTelegramPollLoop<
|
|
|
882
1019
|
const lastUpdateId = getLatestTelegramUpdateId(updates);
|
|
883
1020
|
if (lastUpdateId !== undefined) {
|
|
884
1021
|
deps.config.lastUpdateId = lastUpdateId;
|
|
885
|
-
await deps.persistConfig();
|
|
1022
|
+
await deps.persistConfig(deps.config);
|
|
886
1023
|
}
|
|
887
1024
|
} catch {
|
|
888
1025
|
// ignore
|
|
@@ -893,6 +1030,7 @@ export async function runTelegramPollLoop<
|
|
|
893
1030
|
deps.maxUpdateFailures ?? TELEGRAM_POLLING_DEFAULT_MAX_UPDATE_FAILURES,
|
|
894
1031
|
);
|
|
895
1032
|
const updateFailures = new Map<number, number>();
|
|
1033
|
+
const admittedUpdates = new Set<number>();
|
|
896
1034
|
let handledUpdateFailureRethrown = false;
|
|
897
1035
|
let consecutiveGetUpdatesConflicts = 0;
|
|
898
1036
|
while (!deps.signal.aborted) {
|
|
@@ -903,12 +1041,21 @@ export async function runTelegramPollLoop<
|
|
|
903
1041
|
);
|
|
904
1042
|
consecutiveGetUpdatesConflicts = 0;
|
|
905
1043
|
for (const update of updates) {
|
|
1044
|
+
if (admittedUpdates.has(update.update_id)) {
|
|
1045
|
+
deps.config.lastUpdateId = update.update_id;
|
|
1046
|
+
await deps.persistConfig(deps.config);
|
|
1047
|
+
admittedUpdates.delete(update.update_id);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
906
1050
|
try {
|
|
907
1051
|
await deps.handleUpdate(update, deps.ctx);
|
|
1052
|
+
admittedUpdates.add(update.update_id);
|
|
908
1053
|
deps.config.lastUpdateId = update.update_id;
|
|
909
1054
|
updateFailures.delete(update.update_id);
|
|
910
|
-
await deps.persistConfig();
|
|
1055
|
+
await deps.persistConfig(deps.config);
|
|
1056
|
+
admittedUpdates.delete(update.update_id);
|
|
911
1057
|
} catch (error) {
|
|
1058
|
+
if (admittedUpdates.has(update.update_id)) throw error;
|
|
912
1059
|
const failureCount = (updateFailures.get(update.update_id) ?? 0) + 1;
|
|
913
1060
|
updateFailures.set(update.update_id, failureCount);
|
|
914
1061
|
deps.recordRuntimeEvent?.("polling", error, {
|
|
@@ -924,9 +1071,11 @@ export async function runTelegramPollLoop<
|
|
|
924
1071
|
deps.onErrorStatus(
|
|
925
1072
|
`skipping Telegram update ${update.update_id} after ${failureCount} failures: ${message}`,
|
|
926
1073
|
);
|
|
1074
|
+
admittedUpdates.add(update.update_id);
|
|
927
1075
|
deps.config.lastUpdateId = update.update_id;
|
|
928
1076
|
updateFailures.delete(update.update_id);
|
|
929
|
-
await deps.persistConfig();
|
|
1077
|
+
await deps.persistConfig(deps.config);
|
|
1078
|
+
admittedUpdates.delete(update.update_id);
|
|
930
1079
|
}
|
|
931
1080
|
}
|
|
932
1081
|
} catch (error) {
|
package/lib/preview.ts
CHANGED
|
@@ -145,6 +145,7 @@ export interface TelegramPreviewController {
|
|
|
145
145
|
setPendingText: (text: string) => void;
|
|
146
146
|
createState: () => TelegramPreviewRuntimeState;
|
|
147
147
|
resetState: () => void;
|
|
148
|
+
invalidate: () => void;
|
|
148
149
|
clear: (
|
|
149
150
|
chatId: number,
|
|
150
151
|
options?: { awaitFlush?: boolean; target?: TelegramTarget },
|
|
@@ -175,6 +176,7 @@ export function createTelegramPreviewControllerRuntime(
|
|
|
175
176
|
maxMessageLength: deps.maxMessageLength,
|
|
176
177
|
initialDraftSupport: deps.initialDraftSupport,
|
|
177
178
|
sendDraft: deps.sendDraft,
|
|
179
|
+
canSend: deps.canSend,
|
|
178
180
|
maxDraftId: deps.maxDraftId,
|
|
179
181
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
180
182
|
});
|
|
@@ -233,9 +235,10 @@ export function createTelegramNativeMarkdownPreviewFinalizer<
|
|
|
233
235
|
const state = deps.getState();
|
|
234
236
|
if (state?.flushPromise) {
|
|
235
237
|
await state.flushPromise.catch(() => {});
|
|
238
|
+
if (deps.getState() !== state) return false;
|
|
236
239
|
}
|
|
237
240
|
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
|
|
238
|
-
deps.discard?.();
|
|
241
|
+
if (deps.getState() === state) deps.discard?.();
|
|
239
242
|
return true;
|
|
240
243
|
};
|
|
241
244
|
}
|
|
@@ -275,12 +278,15 @@ export function createTelegramPreviewController(
|
|
|
275
278
|
deps: TelegramPreviewControllerDeps,
|
|
276
279
|
): TelegramPreviewController {
|
|
277
280
|
let state: TelegramPreviewRuntimeState | undefined;
|
|
281
|
+
let generation = 0;
|
|
278
282
|
const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
|
|
279
283
|
const maxMessageLength =
|
|
280
284
|
deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
|
|
281
285
|
let draftSupport = deps.initialDraftSupport ?? "unknown";
|
|
282
286
|
let nextDraftId = 0;
|
|
283
|
-
const getRuntimeDeps = (
|
|
287
|
+
const getRuntimeDeps = (
|
|
288
|
+
operationGeneration = generation,
|
|
289
|
+
): TelegramPreviewRuntimeDeps => ({
|
|
284
290
|
getState: () => state,
|
|
285
291
|
setState: (nextState) => {
|
|
286
292
|
state = nextState;
|
|
@@ -295,7 +301,8 @@ export function createTelegramPreviewController(
|
|
|
295
301
|
return nextDraftId;
|
|
296
302
|
},
|
|
297
303
|
sendDraft: deps.sendDraft,
|
|
298
|
-
canSend:
|
|
304
|
+
canSend: () =>
|
|
305
|
+
operationGeneration === generation && (deps.canSend?.() ?? true),
|
|
299
306
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
300
307
|
});
|
|
301
308
|
return {
|
|
@@ -308,8 +315,13 @@ export function createTelegramPreviewController(
|
|
|
308
315
|
},
|
|
309
316
|
createState: () => createTelegramPreviewRuntimeState(),
|
|
310
317
|
resetState: () => {
|
|
318
|
+
generation += 1;
|
|
311
319
|
state = createTelegramPreviewRuntimeState();
|
|
312
320
|
},
|
|
321
|
+
invalidate: () => {
|
|
322
|
+
generation += 1;
|
|
323
|
+
state = undefined;
|
|
324
|
+
},
|
|
313
325
|
clear: (chatId, options) =>
|
|
314
326
|
clearTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
315
327
|
flush: (chatId, options) =>
|