@llblab/pi-telegram 0.20.3 → 0.20.5
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 +3 -3
- package/BACKLOG.md +64 -0
- package/CHANGELOG.md +22 -0
- package/README.md +2 -0
- package/docs/architecture.md +8 -8
- package/docs/multi-instance-bus.md +4 -4
- package/docs/outbound.md +6 -0
- package/index.ts +53 -21
- package/lib/bindings.ts +127 -34
- package/lib/bus-api.ts +5 -3
- package/lib/bus-follower.ts +11 -0
- package/lib/bus-leader.ts +81 -1
- package/lib/bus.ts +47 -1
- package/lib/config.ts +2 -2
- package/lib/lifecycle.ts +4 -4
- package/lib/locks.ts +12 -1
- package/lib/logs.ts +2 -2
- package/lib/outbound-attachments.ts +170 -0
- package/lib/paths.ts +2 -1
- package/lib/queue.ts +46 -1
- package/lib/setup.ts +7 -1
- package/lib/status.ts +15 -14
- package/lib/telegram-api.ts +44 -6
- package/lib/threads.ts +113 -2
- package/package.json +1 -1
|
@@ -88,6 +88,7 @@ export interface TelegramQueuedOutboundAttachmentView {
|
|
|
88
88
|
|
|
89
89
|
export interface TelegramOutboundAttachmentQueueTargetView {
|
|
90
90
|
queuedAttachments: TelegramQueuedOutboundAttachmentView[];
|
|
91
|
+
guestQueryId?: string;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
|
|
@@ -96,6 +97,42 @@ export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutbou
|
|
|
96
97
|
target?: TelegramTarget;
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
export type TelegramGuestCachedAttachmentResult =
|
|
101
|
+
| {
|
|
102
|
+
type: "document";
|
|
103
|
+
id: string;
|
|
104
|
+
title: string;
|
|
105
|
+
document_file_id: string;
|
|
106
|
+
caption?: string;
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
type: "photo";
|
|
110
|
+
id: string;
|
|
111
|
+
photo_file_id: string;
|
|
112
|
+
caption?: string;
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
type: "audio";
|
|
116
|
+
id: string;
|
|
117
|
+
audio_file_id: string;
|
|
118
|
+
caption?: string;
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
type: "voice";
|
|
122
|
+
id: string;
|
|
123
|
+
voice_file_id: string;
|
|
124
|
+
title: string;
|
|
125
|
+
caption?: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
interface TelegramGuestStagingMessage {
|
|
129
|
+
message_id?: number;
|
|
130
|
+
document?: { file_id?: string };
|
|
131
|
+
photo?: Array<{ file_id?: string; file_size?: number }>;
|
|
132
|
+
audio?: { file_id?: string };
|
|
133
|
+
voice?: { file_id?: string };
|
|
134
|
+
}
|
|
135
|
+
|
|
99
136
|
function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
|
|
100
137
|
const normalized = path.toLowerCase();
|
|
101
138
|
return (
|
|
@@ -107,6 +144,27 @@ function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
|
|
|
107
144
|
);
|
|
108
145
|
}
|
|
109
146
|
|
|
147
|
+
function getTelegramGuestAttachmentTransport(path: string): {
|
|
148
|
+
method: "sendDocument" | "sendPhoto" | "sendAudio" | "sendVoice";
|
|
149
|
+
fileField: "document" | "photo" | "audio" | "voice";
|
|
150
|
+
} {
|
|
151
|
+
const normalized = path.toLowerCase();
|
|
152
|
+
if (
|
|
153
|
+
normalized.endsWith(".jpg") ||
|
|
154
|
+
normalized.endsWith(".jpeg") ||
|
|
155
|
+
normalized.endsWith(".png")
|
|
156
|
+
) {
|
|
157
|
+
return { method: "sendPhoto", fileField: "photo" };
|
|
158
|
+
}
|
|
159
|
+
if (normalized.endsWith(".ogg") || normalized.endsWith(".opus")) {
|
|
160
|
+
return { method: "sendVoice", fileField: "voice" };
|
|
161
|
+
}
|
|
162
|
+
if (normalized.endsWith(".mp3")) {
|
|
163
|
+
return { method: "sendAudio", fileField: "audio" };
|
|
164
|
+
}
|
|
165
|
+
return { method: "sendDocument", fileField: "document" };
|
|
166
|
+
}
|
|
167
|
+
|
|
110
168
|
function formatTelegramOutboundAttachmentSizeLimitError(
|
|
111
169
|
size: number,
|
|
112
170
|
maxSize: number,
|
|
@@ -388,6 +446,14 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
388
446
|
statPath: options.statPath,
|
|
389
447
|
});
|
|
390
448
|
}
|
|
449
|
+
if (
|
|
450
|
+
options.activeTurn.guestQueryId &&
|
|
451
|
+
options.activeTurn.queuedAttachments.length + options.paths.length > 1
|
|
452
|
+
) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
"Telegram Guest Mode supports one attachment per reply; no attachment was queued",
|
|
455
|
+
);
|
|
456
|
+
}
|
|
391
457
|
if (
|
|
392
458
|
options.activeTurn.queuedAttachments.length + options.paths.length >
|
|
393
459
|
options.maxAttachmentsPerTurn
|
|
@@ -414,6 +480,110 @@ export async function queueTelegramOutboundAttachments(options: {
|
|
|
414
480
|
};
|
|
415
481
|
}
|
|
416
482
|
|
|
483
|
+
export async function deliverTelegramGuestCachedAttachment(options: {
|
|
484
|
+
guestQueryId: string;
|
|
485
|
+
stagingChatId: number;
|
|
486
|
+
stagingTarget?: TelegramTarget;
|
|
487
|
+
attachment: TelegramQueuedOutboundAttachmentView;
|
|
488
|
+
caption?: string;
|
|
489
|
+
sendMultipart: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
|
|
490
|
+
answerGuestQuery: (
|
|
491
|
+
guestQueryId: string,
|
|
492
|
+
result: TelegramGuestCachedAttachmentResult,
|
|
493
|
+
) => Promise<void>;
|
|
494
|
+
answerGuestText?: (guestQueryId: string, text: string) => Promise<void>;
|
|
495
|
+
fallbackText?: string;
|
|
496
|
+
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
497
|
+
recordRuntimeEvent?: TelegramOutboundAttachmentRuntimeEventRecorderPort["recordRuntimeEvent"];
|
|
498
|
+
}): Promise<void> {
|
|
499
|
+
const transport = getTelegramGuestAttachmentTransport(options.attachment.path);
|
|
500
|
+
let stagingMessageId: number | undefined;
|
|
501
|
+
let answerAttempted = false;
|
|
502
|
+
try {
|
|
503
|
+
const message = (await options.sendMultipart(
|
|
504
|
+
transport.method,
|
|
505
|
+
{
|
|
506
|
+
chat_id: String(options.stagingChatId),
|
|
507
|
+
...getTelegramMultipartTargetFields(options.stagingTarget),
|
|
508
|
+
},
|
|
509
|
+
transport.fileField,
|
|
510
|
+
options.attachment.path,
|
|
511
|
+
options.attachment.fileName,
|
|
512
|
+
)) as TelegramGuestStagingMessage;
|
|
513
|
+
stagingMessageId = message.message_id;
|
|
514
|
+
const caption = options.caption
|
|
515
|
+
? Array.from(options.caption).slice(0, 1024).join("")
|
|
516
|
+
: undefined;
|
|
517
|
+
let result: TelegramGuestCachedAttachmentResult;
|
|
518
|
+
if (transport.fileField === "photo") {
|
|
519
|
+
const photo = [...(message.photo ?? [])]
|
|
520
|
+
.sort((left, right) => (left.file_size ?? 0) - (right.file_size ?? 0))
|
|
521
|
+
.at(-1);
|
|
522
|
+
if (!photo?.file_id) throw new Error("Guest staging upload returned no photo file_id");
|
|
523
|
+
result = {
|
|
524
|
+
type: "photo",
|
|
525
|
+
id: "attachment-1",
|
|
526
|
+
photo_file_id: photo.file_id,
|
|
527
|
+
...(caption ? { caption } : {}),
|
|
528
|
+
};
|
|
529
|
+
} else if (transport.fileField === "audio") {
|
|
530
|
+
if (!message.audio?.file_id)
|
|
531
|
+
throw new Error("Guest staging upload returned no audio file_id");
|
|
532
|
+
result = {
|
|
533
|
+
type: "audio",
|
|
534
|
+
id: "attachment-1",
|
|
535
|
+
audio_file_id: message.audio.file_id,
|
|
536
|
+
...(caption ? { caption } : {}),
|
|
537
|
+
};
|
|
538
|
+
} else if (transport.fileField === "voice") {
|
|
539
|
+
if (!message.voice?.file_id)
|
|
540
|
+
throw new Error("Guest staging upload returned no voice file_id");
|
|
541
|
+
result = {
|
|
542
|
+
type: "voice",
|
|
543
|
+
id: "attachment-1",
|
|
544
|
+
voice_file_id: message.voice.file_id,
|
|
545
|
+
title: options.attachment.fileName,
|
|
546
|
+
...(caption ? { caption } : {}),
|
|
547
|
+
};
|
|
548
|
+
} else {
|
|
549
|
+
if (!message.document?.file_id)
|
|
550
|
+
throw new Error("Guest staging upload returned no document file_id");
|
|
551
|
+
result = {
|
|
552
|
+
type: "document",
|
|
553
|
+
id: "attachment-1",
|
|
554
|
+
title: options.attachment.fileName,
|
|
555
|
+
document_file_id: message.document.file_id,
|
|
556
|
+
...(caption ? { caption } : {}),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
answerAttempted = true;
|
|
560
|
+
await options.answerGuestQuery(options.guestQueryId, result);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
if (
|
|
563
|
+
!answerAttempted &&
|
|
564
|
+
options.answerGuestText &&
|
|
565
|
+
options.fallbackText
|
|
566
|
+
) {
|
|
567
|
+
answerAttempted = true;
|
|
568
|
+
await options.answerGuestText(options.guestQueryId, options.fallbackText);
|
|
569
|
+
} else {
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
} finally {
|
|
573
|
+
if (stagingMessageId !== undefined) {
|
|
574
|
+
try {
|
|
575
|
+
await options.deleteMessage(options.stagingChatId, stagingMessageId);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
options.recordRuntimeEvent?.("attachment", error, {
|
|
578
|
+
phase: "guest-staging-cleanup",
|
|
579
|
+
chatId: options.stagingChatId,
|
|
580
|
+
messageId: stagingMessageId,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
417
587
|
export async function sendTelegramOutboundMessage(options: {
|
|
418
588
|
text: string;
|
|
419
589
|
chatId?: number;
|
package/lib/paths.ts
CHANGED
|
@@ -76,9 +76,10 @@ export function getTelegramDiagnosticsDisplayPaths(profileName?: string): {
|
|
|
76
76
|
logs: string;
|
|
77
77
|
} {
|
|
78
78
|
const suffix = getTelegramProfilePathSuffix(profileName);
|
|
79
|
+
const profileSlug = suffix.slice(1);
|
|
79
80
|
return {
|
|
80
81
|
state: `~/.pi/agent/tmp/telegram/state${suffix}.json`,
|
|
81
|
-
logs: `~/.pi/agent/tmp/telegram/logs${
|
|
82
|
+
logs: `~/.pi/agent/tmp/telegram/logs${profileSlug ? `.${profileSlug}` : ""}.jsonl`,
|
|
82
83
|
};
|
|
83
84
|
}
|
|
84
85
|
|
package/lib/queue.ts
CHANGED
|
@@ -899,6 +899,16 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
899
899
|
options?: { parseMode?: string },
|
|
900
900
|
) => Promise<void>;
|
|
901
901
|
sendGuestReply?: (guestQueryId: string, markdown: string) => Promise<void>;
|
|
902
|
+
sendGuestAttachment?: (
|
|
903
|
+
turn: TTurn,
|
|
904
|
+
attachment: QueuedAttachment,
|
|
905
|
+
caption?: string,
|
|
906
|
+
) => Promise<void>;
|
|
907
|
+
sendGuestVoiceReply?: (
|
|
908
|
+
turn: TTurn,
|
|
909
|
+
plan: TelegramAgentEndOutboundReplyPlan<TReplyMarkup>,
|
|
910
|
+
caption?: string,
|
|
911
|
+
) => Promise<void>;
|
|
902
912
|
planOutboundReply?: (
|
|
903
913
|
markdown: string,
|
|
904
914
|
) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
|
|
@@ -958,6 +968,8 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
958
968
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
959
969
|
answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
|
|
960
970
|
sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
|
|
971
|
+
sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
|
|
972
|
+
sendGuestVoiceReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestVoiceReply"];
|
|
961
973
|
planOutboundReply?: TelegramAgentEndRuntimeDeps<
|
|
962
974
|
TTurn,
|
|
963
975
|
TReplyMarkup
|
|
@@ -1083,6 +1095,8 @@ export function createTelegramAgentEndHook<
|
|
|
1083
1095
|
sendQueuedAttachments: deps.sendQueuedAttachments,
|
|
1084
1096
|
answerGuestQuery: deps.answerGuestQuery,
|
|
1085
1097
|
sendGuestReply: deps.sendGuestReply,
|
|
1098
|
+
sendGuestAttachment: deps.sendGuestAttachment,
|
|
1099
|
+
sendGuestVoiceReply: deps.sendGuestVoiceReply,
|
|
1086
1100
|
planOutboundReply: deps.planOutboundReply,
|
|
1087
1101
|
sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
|
|
1088
1102
|
getDefaultChatId: deps.getDefaultChatId,
|
|
@@ -1181,7 +1195,38 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1181
1195
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1182
1196
|
return;
|
|
1183
1197
|
}
|
|
1184
|
-
|
|
1198
|
+
const [guestAttachment] = turn.queuedAttachments;
|
|
1199
|
+
if (guestAttachment && deps.sendGuestAttachment) {
|
|
1200
|
+
try {
|
|
1201
|
+
await deps.sendGuestAttachment(
|
|
1202
|
+
turn,
|
|
1203
|
+
guestAttachment,
|
|
1204
|
+
finalText || undefined,
|
|
1205
|
+
);
|
|
1206
|
+
} catch (error) {
|
|
1207
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1208
|
+
phase: "guest-attachment",
|
|
1209
|
+
guestQueryId: turn.guestQueryId,
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
} else if (
|
|
1213
|
+
outboundReply &&
|
|
1214
|
+
(outboundReply.voiceText || outboundReply.voiceReplies?.length) &&
|
|
1215
|
+
deps.sendGuestVoiceReply
|
|
1216
|
+
) {
|
|
1217
|
+
try {
|
|
1218
|
+
await deps.sendGuestVoiceReply(
|
|
1219
|
+
turn,
|
|
1220
|
+
outboundReply,
|
|
1221
|
+
finalText || undefined,
|
|
1222
|
+
);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1225
|
+
phase: "guest-voice",
|
|
1226
|
+
guestQueryId: turn.guestQueryId,
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
} else if (finalText) {
|
|
1185
1230
|
if (deps.sendGuestReply) {
|
|
1186
1231
|
await deps.sendGuestReply(turn.guestQueryId, finalText);
|
|
1187
1232
|
} else {
|
package/lib/setup.ts
CHANGED
|
@@ -197,8 +197,14 @@ export function createTelegramSetupPromptRuntime<
|
|
|
197
197
|
promptEditor: (label, value) => ctx.ui.editor(label, value),
|
|
198
198
|
getMe: deps.getMe,
|
|
199
199
|
persistConfig: async (config) => {
|
|
200
|
-
|
|
200
|
+
const previousConfig = deps.getConfig();
|
|
201
201
|
deps.setConfig(config);
|
|
202
|
+
try {
|
|
203
|
+
await deps.persistConfig(config);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
deps.setConfig(previousConfig);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
202
208
|
},
|
|
203
209
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
204
210
|
startPolling: () => deps.startPolling(ctx),
|
package/lib/status.ts
CHANGED
|
@@ -181,6 +181,7 @@ export interface TelegramBridgeStatusLineState {
|
|
|
181
181
|
hasBotToken?: boolean;
|
|
182
182
|
botUsername?: string;
|
|
183
183
|
activeProfileName?: string;
|
|
184
|
+
diagnosticPaths?: { state: string; logs: string };
|
|
184
185
|
allowedUserId?: number;
|
|
185
186
|
botThreadMode?: "unknown" | "enabled" | "disabled";
|
|
186
187
|
botThreadModeUpdatedAtMs?: number;
|
|
@@ -259,6 +260,9 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
259
260
|
statusKey?: string;
|
|
260
261
|
getConfig: () => TelegramBridgeStatusConfig;
|
|
261
262
|
getActiveProfileName?: () => string | undefined;
|
|
263
|
+
getDiagnosticPaths?: (
|
|
264
|
+
profileName?: string,
|
|
265
|
+
) => { state: string; logs: string };
|
|
262
266
|
isPollingActive: () => boolean;
|
|
263
267
|
getActiveSourceMessageIds: () => number[] | undefined;
|
|
264
268
|
hasActiveTurn: () => boolean;
|
|
@@ -620,10 +624,12 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
620
624
|
getBridgeStatusLineState: () => {
|
|
621
625
|
const config = deps.getConfig();
|
|
622
626
|
const botThreadMode = deps.getBotThreadMode?.();
|
|
627
|
+
const activeProfileName = deps.getActiveProfileName?.();
|
|
623
628
|
return {
|
|
624
629
|
hasBotToken: Boolean(config.botToken),
|
|
625
630
|
botUsername: config.botUsername,
|
|
626
|
-
activeProfileName
|
|
631
|
+
activeProfileName,
|
|
632
|
+
diagnosticPaths: deps.getDiagnosticPaths?.(activeProfileName),
|
|
627
633
|
allowedUserId: config.allowedUserId,
|
|
628
634
|
botThreadMode: botThreadMode?.threadMode,
|
|
629
635
|
botThreadModeUpdatedAtMs: botThreadMode?.updatedAtMs,
|
|
@@ -774,9 +780,6 @@ export function buildTelegramStatusBarText(
|
|
|
774
780
|
return `${label} ${theme.fg("warning", "electing")}${queued}`;
|
|
775
781
|
if (!state.pollingActive && state.busRole !== "follower")
|
|
776
782
|
return `${theme.fg("accent", "telegram")} ${theme.fg("muted", "disconnected")}${queued}`;
|
|
777
|
-
if (state.compactionInProgress) {
|
|
778
|
-
return `${label} ${theme.fg("warning", "compacting")}${queued}`;
|
|
779
|
-
}
|
|
780
783
|
if (state.processing) {
|
|
781
784
|
const processingStatus = state.queuedStatus
|
|
782
785
|
? "active"
|
|
@@ -1058,19 +1061,18 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1058
1061
|
? ` (control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount})`
|
|
1059
1062
|
: ""
|
|
1060
1063
|
}`;
|
|
1061
|
-
const executionState = state.
|
|
1062
|
-
? "
|
|
1063
|
-
: state.
|
|
1064
|
-
? "
|
|
1065
|
-
:
|
|
1066
|
-
? "active"
|
|
1067
|
-
: "idle";
|
|
1064
|
+
const executionState = state.pendingDispatch
|
|
1065
|
+
? "pending dispatch"
|
|
1066
|
+
: state.activeSourceMessageIds?.length
|
|
1067
|
+
? "active"
|
|
1068
|
+
: "idle";
|
|
1068
1069
|
const profileSuffix = state.activeProfileName
|
|
1069
1070
|
? `.${state.activeProfileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}`
|
|
1070
1071
|
: "";
|
|
1071
|
-
const
|
|
1072
|
+
const profileSlug = profileSuffix.slice(1);
|
|
1073
|
+
const diagnosticsPaths = state.diagnosticPaths ?? {
|
|
1072
1074
|
state: `~/.pi/agent/tmp/telegram/state${profileSuffix}.json`,
|
|
1073
|
-
logs: `~/.pi/agent/tmp/telegram/logs${
|
|
1075
|
+
logs: `~/.pi/agent/tmp/telegram/logs${profileSlug ? `.${profileSlug}` : ""}.jsonl`,
|
|
1074
1076
|
};
|
|
1075
1077
|
return [
|
|
1076
1078
|
"connection:",
|
|
@@ -1251,7 +1253,6 @@ function buildContextSummary(
|
|
|
1251
1253
|
}
|
|
1252
1254
|
|
|
1253
1255
|
function buildStatusSummary(ctx: TelegramStatusContext): string {
|
|
1254
|
-
if (ctx.isCompactionInProgress?.()) return "compacting";
|
|
1255
1256
|
if (ctx.hasPendingMessages?.()) return "pending";
|
|
1256
1257
|
if (ctx.isIdle?.() === false) return "active";
|
|
1257
1258
|
if (ctx.isIdle?.() === true) return "idle";
|
package/lib/telegram-api.ts
CHANGED
|
@@ -287,6 +287,44 @@ export interface TelegramFileDownloadOptions {
|
|
|
287
287
|
maxFileSizeBytes?: number;
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
+
export type TelegramGuestCachedMediaResult =
|
|
291
|
+
| {
|
|
292
|
+
type: "document";
|
|
293
|
+
id: string;
|
|
294
|
+
title: string;
|
|
295
|
+
document_file_id: string;
|
|
296
|
+
caption?: string;
|
|
297
|
+
parse_mode?: string;
|
|
298
|
+
}
|
|
299
|
+
| {
|
|
300
|
+
type: "photo";
|
|
301
|
+
id: string;
|
|
302
|
+
photo_file_id: string;
|
|
303
|
+
caption?: string;
|
|
304
|
+
parse_mode?: string;
|
|
305
|
+
}
|
|
306
|
+
| {
|
|
307
|
+
type: "audio";
|
|
308
|
+
id: string;
|
|
309
|
+
audio_file_id: string;
|
|
310
|
+
caption?: string;
|
|
311
|
+
parse_mode?: string;
|
|
312
|
+
}
|
|
313
|
+
| {
|
|
314
|
+
type: "voice";
|
|
315
|
+
id: string;
|
|
316
|
+
voice_file_id: string;
|
|
317
|
+
title: string;
|
|
318
|
+
caption?: string;
|
|
319
|
+
parse_mode?: string;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export interface TelegramAnswerGuestQueryOptions {
|
|
323
|
+
parseMode?: string;
|
|
324
|
+
richMessage?: TelegramInputRichMessage;
|
|
325
|
+
result?: TelegramGuestCachedMediaResult;
|
|
326
|
+
}
|
|
327
|
+
|
|
290
328
|
export interface TelegramAnswerCallbackQueryOptions {
|
|
291
329
|
recordRuntimeEvent?: (
|
|
292
330
|
kind: "api",
|
|
@@ -322,7 +360,7 @@ export interface TelegramApiClient {
|
|
|
322
360
|
answerGuestQuery?: (
|
|
323
361
|
guestQueryId: string,
|
|
324
362
|
text?: string,
|
|
325
|
-
options?:
|
|
363
|
+
options?: TelegramAnswerGuestQueryOptions,
|
|
326
364
|
) => Promise<void>;
|
|
327
365
|
}
|
|
328
366
|
|
|
@@ -401,7 +439,7 @@ export interface TelegramBridgeApiRuntime {
|
|
|
401
439
|
answerGuestQuery: (
|
|
402
440
|
guestQueryId: string,
|
|
403
441
|
text?: string,
|
|
404
|
-
options?:
|
|
442
|
+
options?: TelegramAnswerGuestQueryOptions,
|
|
405
443
|
) => Promise<void>;
|
|
406
444
|
deleteMessage: (chatId: number, messageId: number) => Promise<void>;
|
|
407
445
|
prepareTempDir: () => Promise<number>;
|
|
@@ -1312,12 +1350,12 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1312
1350
|
answerGuestQuery: (
|
|
1313
1351
|
guestQueryId: string,
|
|
1314
1352
|
text: string | undefined,
|
|
1315
|
-
options:
|
|
1316
|
-
| { parseMode?: string; richMessage?: TelegramInputRichMessage }
|
|
1317
|
-
| undefined,
|
|
1353
|
+
options: TelegramAnswerGuestQueryOptions | undefined,
|
|
1318
1354
|
) => {
|
|
1319
1355
|
const body: Record<string, unknown> = { guest_query_id: guestQueryId };
|
|
1320
|
-
if (
|
|
1356
|
+
if (options?.result) {
|
|
1357
|
+
body.result = options.result;
|
|
1358
|
+
} else if (text !== undefined || options?.richMessage) {
|
|
1321
1359
|
const inputContent: Record<string, unknown> = options?.richMessage
|
|
1322
1360
|
? { rich_message: options.richMessage }
|
|
1323
1361
|
: { message_text: text };
|
package/lib/threads.ts
CHANGED
|
@@ -183,6 +183,9 @@ export interface TelegramTopicTargetStore {
|
|
|
183
183
|
load: () => Promise<void>;
|
|
184
184
|
persist: () => Promise<void>;
|
|
185
185
|
list: () => TelegramTopicTargetRecord[];
|
|
186
|
+
getFollowerRecoveryHintByTarget?: (
|
|
187
|
+
target: TelegramTarget,
|
|
188
|
+
) => { slot?: string; threadName?: string } | undefined;
|
|
186
189
|
listReservations: () => TelegramThreadReservation[];
|
|
187
190
|
listPendingProvisions: () => TelegramThreadPendingProvision[];
|
|
188
191
|
listSyncObservations: () => TelegramTopicSyncObservation[];
|
|
@@ -266,6 +269,7 @@ export function reconcileTelegramFreshAllocationCursor(
|
|
|
266
269
|
export interface TelegramTopicTargetStoreOptions {
|
|
267
270
|
path: string | (() => string);
|
|
268
271
|
getNowMs?: () => number;
|
|
272
|
+
canPersist?: () => boolean;
|
|
269
273
|
}
|
|
270
274
|
|
|
271
275
|
export interface TelegramTopicTargetProvisionerDeps {
|
|
@@ -869,6 +873,54 @@ function targetMatches(left: TelegramTarget, right: TelegramTarget): boolean {
|
|
|
869
873
|
return left.chatId === right.chatId && left.threadId === right.threadId;
|
|
870
874
|
}
|
|
871
875
|
|
|
876
|
+
function getTargetRecoveryHintKey(target: TelegramTarget): string {
|
|
877
|
+
return `${target.chatId}:${target.threadId ?? "private"}`;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function parseFollowerRecoveryHints(
|
|
881
|
+
value: unknown,
|
|
882
|
+
): Map<string, { slot?: string; threadName?: string }> {
|
|
883
|
+
const hints = new Map<string, { slot?: string; threadName?: string }>();
|
|
884
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return hints;
|
|
885
|
+
const liveRoster = (value as Record<string, unknown>).liveRoster;
|
|
886
|
+
if (!liveRoster || typeof liveRoster !== "object" || Array.isArray(liveRoster))
|
|
887
|
+
return hints;
|
|
888
|
+
const followers = (liveRoster as Record<string, unknown>).busFollowers;
|
|
889
|
+
if (!Array.isArray(followers)) return hints;
|
|
890
|
+
for (const follower of followers) {
|
|
891
|
+
if (!follower || typeof follower !== "object" || Array.isArray(follower))
|
|
892
|
+
continue;
|
|
893
|
+
const record = follower as Record<string, unknown>;
|
|
894
|
+
const target = record.target;
|
|
895
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) continue;
|
|
896
|
+
const targetRecord = target as Record<string, unknown>;
|
|
897
|
+
if (typeof targetRecord.chatId !== "number") continue;
|
|
898
|
+
const normalizedTarget: TelegramTarget = {
|
|
899
|
+
chatId: targetRecord.chatId,
|
|
900
|
+
...(typeof targetRecord.threadId === "number"
|
|
901
|
+
? { threadId: targetRecord.threadId }
|
|
902
|
+
: {}),
|
|
903
|
+
};
|
|
904
|
+
const slot =
|
|
905
|
+
typeof targetRecord.slot === "string" && /^[A-Z]$/.test(targetRecord.slot)
|
|
906
|
+
? targetRecord.slot
|
|
907
|
+
: typeof record.slot === "string" && /^[A-Z]$/.test(record.slot)
|
|
908
|
+
? record.slot
|
|
909
|
+
: undefined;
|
|
910
|
+
const threadName =
|
|
911
|
+
typeof targetRecord.threadName === "string"
|
|
912
|
+
? targetRecord.threadName
|
|
913
|
+
: typeof record.threadName === "string"
|
|
914
|
+
? record.threadName
|
|
915
|
+
: undefined;
|
|
916
|
+
hints.set(getTargetRecoveryHintKey(normalizedTarget), {
|
|
917
|
+
...(slot ? { slot } : {}),
|
|
918
|
+
...(threadName ? { threadName } : {}),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
return hints;
|
|
922
|
+
}
|
|
923
|
+
|
|
872
924
|
function getInstanceProcessKey(
|
|
873
925
|
instanceId: string | undefined,
|
|
874
926
|
): string | undefined {
|
|
@@ -905,6 +957,10 @@ export function createTelegramTopicTargetStore(
|
|
|
905
957
|
let reservations: TelegramThreadReservation[] = [];
|
|
906
958
|
let pendingProvisions: TelegramThreadPendingProvision[] = [];
|
|
907
959
|
let syncObservations: TelegramTopicSyncObservation[] = [];
|
|
960
|
+
let followerRecoveryHints = new Map<
|
|
961
|
+
string,
|
|
962
|
+
{ slot?: string; threadName?: string }
|
|
963
|
+
>();
|
|
908
964
|
let loaded = false;
|
|
909
965
|
let loadedPath: string | undefined;
|
|
910
966
|
let dirty = false;
|
|
@@ -939,6 +995,7 @@ export function createTelegramTopicTargetStore(
|
|
|
939
995
|
reservations = [];
|
|
940
996
|
pendingProvisions = [];
|
|
941
997
|
syncObservations = [];
|
|
998
|
+
followerRecoveryHints = new Map();
|
|
942
999
|
statusSnapshot = {};
|
|
943
1000
|
loaded = false;
|
|
944
1001
|
dirty = false;
|
|
@@ -955,11 +1012,14 @@ export function createTelegramTopicTargetStore(
|
|
|
955
1012
|
reservations = [];
|
|
956
1013
|
pendingProvisions = [];
|
|
957
1014
|
syncObservations = [];
|
|
1015
|
+
followerRecoveryHints = new Map();
|
|
958
1016
|
loaded = true;
|
|
959
1017
|
return;
|
|
960
1018
|
}
|
|
961
1019
|
const content = await readFile(path, "utf8");
|
|
962
|
-
const
|
|
1020
|
+
const rawFile: unknown = JSON.parse(content);
|
|
1021
|
+
const file = parseTopicTargetFile(rawFile);
|
|
1022
|
+
followerRecoveryHints = parseFollowerRecoveryHints(rawFile);
|
|
963
1023
|
botState = file.bot;
|
|
964
1024
|
records = new Map(
|
|
965
1025
|
file.threads.map((record) => [
|
|
@@ -993,6 +1053,7 @@ export function createTelegramTopicTargetStore(
|
|
|
993
1053
|
target: { ...observation.target },
|
|
994
1054
|
}));
|
|
995
1055
|
loaded = true;
|
|
1056
|
+
dirty = false;
|
|
996
1057
|
};
|
|
997
1058
|
|
|
998
1059
|
return {
|
|
@@ -1003,7 +1064,11 @@ export function createTelegramTopicTargetStore(
|
|
|
1003
1064
|
async persist() {
|
|
1004
1065
|
const path = getPath();
|
|
1005
1066
|
if (loadedPath !== path && !dirty) resetForPath(path);
|
|
1006
|
-
if (
|
|
1067
|
+
if (options.canPersist && !options.canPersist()) {
|
|
1068
|
+
await loadFromDisk();
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (!dirty || !loaded) await loadFromDisk();
|
|
1007
1072
|
await mkdir(dirname(path), { recursive: true });
|
|
1008
1073
|
const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
|
|
1009
1074
|
const nowMs = getNowMs();
|
|
@@ -1058,6 +1123,10 @@ export function createTelegramTopicTargetStore(
|
|
|
1058
1123
|
list() {
|
|
1059
1124
|
return Array.from(records.values()).map(cloneRecord);
|
|
1060
1125
|
},
|
|
1126
|
+
getFollowerRecoveryHintByTarget(target) {
|
|
1127
|
+
const hint = followerRecoveryHints.get(getTargetRecoveryHintKey(target));
|
|
1128
|
+
return hint ? { ...hint } : undefined;
|
|
1129
|
+
},
|
|
1061
1130
|
listReservations() {
|
|
1062
1131
|
const nowMs = getNowMs();
|
|
1063
1132
|
return reservations
|
|
@@ -1962,6 +2031,48 @@ export async function provisionOwnBusTopic(
|
|
|
1962
2031
|
};
|
|
1963
2032
|
}
|
|
1964
2033
|
|
|
2034
|
+
export interface TelegramInstanceThreadIdentityCandidate {
|
|
2035
|
+
target?: TelegramTarget;
|
|
2036
|
+
slot?: string;
|
|
2037
|
+
threadName?: string;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
export function resolveTelegramInstanceThreadIdentity(options: {
|
|
2041
|
+
target?: TelegramTarget;
|
|
2042
|
+
follower?: TelegramInstanceThreadIdentityCandidate;
|
|
2043
|
+
leader?: TelegramInstanceThreadIdentityCandidate;
|
|
2044
|
+
record?: TelegramTopicTargetRecord;
|
|
2045
|
+
}): TelegramInstanceThreadIdentityCandidate {
|
|
2046
|
+
const targetMatchesCandidate = (
|
|
2047
|
+
candidate: TelegramInstanceThreadIdentityCandidate | undefined,
|
|
2048
|
+
) => {
|
|
2049
|
+
if (!candidate) return false;
|
|
2050
|
+
if (!options.target) return true;
|
|
2051
|
+
return !!candidate.target && targetMatches(candidate.target, options.target);
|
|
2052
|
+
};
|
|
2053
|
+
const local = targetMatchesCandidate(options.follower)
|
|
2054
|
+
? options.follower
|
|
2055
|
+
: targetMatchesCandidate(options.leader)
|
|
2056
|
+
? options.leader
|
|
2057
|
+
: undefined;
|
|
2058
|
+
const record =
|
|
2059
|
+
options.record &&
|
|
2060
|
+
(!options.target || targetMatches(options.record.target, options.target))
|
|
2061
|
+
? options.record
|
|
2062
|
+
: undefined;
|
|
2063
|
+
return {
|
|
2064
|
+
...(local?.target ?? record?.target
|
|
2065
|
+
? { target: local?.target ?? record?.target }
|
|
2066
|
+
: {}),
|
|
2067
|
+
...(local?.slot ?? record?.slot
|
|
2068
|
+
? { slot: local?.slot ?? record?.slot }
|
|
2069
|
+
: {}),
|
|
2070
|
+
...(local?.threadName ?? record?.threadName
|
|
2071
|
+
? { threadName: local?.threadName ?? record?.threadName }
|
|
2072
|
+
: {}),
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1965
2076
|
export function findCurrentTelegramInstanceThreadRecord(options: {
|
|
1966
2077
|
records: readonly TelegramTopicTargetRecord[];
|
|
1967
2078
|
instanceId: string;
|