@llblab/pi-telegram 0.16.5 → 0.17.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 +14 -8
- package/BACKLOG.md +8 -3
- package/CHANGELOG.md +21 -0
- package/README.md +9 -5
- package/docs/README.md +2 -1
- package/docs/architecture.md +14 -12
- package/docs/outbound.md +2 -2
- package/docs/public-api.md +3 -0
- package/docs/sections.md +4 -2
- package/docs/telegram-bot-api-rich-messages.md +890 -0
- package/docs/ui-style.md +1 -1
- package/index.ts +11 -2
- package/lib/commands.ts +2 -2
- package/lib/keyboard.ts +39 -0
- package/lib/lifecycle.ts +6 -0
- package/lib/menu-model.ts +3 -3
- package/lib/menu-settings.ts +2 -2
- package/lib/menu.ts +4 -4
- package/lib/outbound-attachments.ts +1 -0
- package/lib/outbound-buttons.ts +1 -0
- package/lib/outbound-voice.ts +2 -0
- package/lib/polling.ts +21 -8
- package/lib/preview.ts +152 -196
- package/lib/prompts.ts +3 -3
- package/lib/queue.ts +1 -1
- package/lib/rendering.ts +3 -349
- package/lib/replies.ts +199 -37
- package/lib/routing.ts +2 -2
- package/lib/sections.ts +15 -18
- package/lib/telegram-api.ts +103 -15
- package/lib/updates.ts +2 -1
- package/package.json +1 -1
package/lib/preview.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram preview streaming helpers
|
|
3
|
-
* Zones: telegram outbound,
|
|
3
|
+
* Zones: telegram outbound, native rich markdown drafts, fallback preview transport
|
|
4
4
|
* Owns preview transport selection, runtime updates, and preview finalization
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -10,23 +10,13 @@ import type {
|
|
|
10
10
|
TelegramSendMessageBody,
|
|
11
11
|
TelegramSentMessage,
|
|
12
12
|
} from "./telegram-api.ts";
|
|
13
|
-
import {
|
|
14
|
-
buildTelegramPreviewSnapshot,
|
|
15
|
-
MAX_MESSAGE_LENGTH,
|
|
16
|
-
renderMarkdownPreviewText,
|
|
17
|
-
renderTelegramMessage,
|
|
18
|
-
type TelegramPreviewRenderStrategy,
|
|
19
|
-
type TelegramPreviewSnapshot,
|
|
20
|
-
type TelegramRenderedChunk,
|
|
21
|
-
type TelegramRenderMode,
|
|
22
|
-
} from "./rendering.ts";
|
|
23
|
-
|
|
24
13
|
import { buildTelegramReplyParameters } from "./replies.ts";
|
|
25
14
|
import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
|
|
26
15
|
import { shouldSuppressPreviewForVoice } from "./voice.ts";
|
|
27
16
|
|
|
28
|
-
const TELEGRAM_PREVIEW_THROTTLE_MS =
|
|
17
|
+
const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
|
|
29
18
|
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
|
19
|
+
const TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS = 4096;
|
|
30
20
|
|
|
31
21
|
export type TelegramDraftSupport = "unknown" | "supported" | "unsupported";
|
|
32
22
|
|
|
@@ -37,7 +27,6 @@ export interface TelegramPreviewState {
|
|
|
37
27
|
pendingText: string;
|
|
38
28
|
lastSentText: string;
|
|
39
29
|
lastSentParseMode?: "HTML";
|
|
40
|
-
lastSentStrategy?: TelegramPreviewRenderStrategy;
|
|
41
30
|
}
|
|
42
31
|
|
|
43
32
|
export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
|
|
@@ -56,7 +45,6 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
56
45
|
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
57
46
|
clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
|
|
58
47
|
maxMessageLength: number;
|
|
59
|
-
renderPreviewText: (markdown: string) => string;
|
|
60
48
|
getDraftSupport: () => TelegramDraftSupport;
|
|
61
49
|
setDraftSupport: (support: TelegramDraftSupport) => void;
|
|
62
50
|
allocateDraftId: () => number;
|
|
@@ -81,21 +69,6 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
81
69
|
text: string,
|
|
82
70
|
options?: { parseMode?: "HTML" },
|
|
83
71
|
) => Promise<unknown>;
|
|
84
|
-
renderTelegramMessage: (
|
|
85
|
-
text: string,
|
|
86
|
-
options?: { mode?: TelegramRenderMode },
|
|
87
|
-
) => TelegramRenderedChunk[];
|
|
88
|
-
sendRenderedChunks: (
|
|
89
|
-
chatId: number,
|
|
90
|
-
chunks: TelegramRenderedChunk[],
|
|
91
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
92
|
-
) => Promise<number | undefined>;
|
|
93
|
-
editRenderedMessage: (
|
|
94
|
-
chatId: number,
|
|
95
|
-
messageId: number,
|
|
96
|
-
chunks: TelegramRenderedChunk[],
|
|
97
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
98
|
-
) => Promise<number | undefined>;
|
|
99
72
|
canSend?: () => boolean;
|
|
100
73
|
recordRuntimeEvent?: (
|
|
101
74
|
category: string,
|
|
@@ -162,7 +135,6 @@ export interface TelegramPreviewControllerDeps<
|
|
|
162
135
|
> {
|
|
163
136
|
getDefaultReplyToMessageId?: () => number | undefined;
|
|
164
137
|
maxMessageLength?: number;
|
|
165
|
-
renderPreviewText?: (markdown: string) => string;
|
|
166
138
|
initialDraftSupport?: TelegramDraftSupport;
|
|
167
139
|
sendDraft: (
|
|
168
140
|
chatId: number,
|
|
@@ -186,22 +158,6 @@ export interface TelegramPreviewControllerDeps<
|
|
|
186
158
|
text: string,
|
|
187
159
|
options?: { parseMode?: "HTML" },
|
|
188
160
|
) => Promise<unknown>;
|
|
189
|
-
renderTelegramMessage?: (
|
|
190
|
-
text: string,
|
|
191
|
-
options?: { mode?: TelegramRenderMode },
|
|
192
|
-
) => TelegramRenderedChunk[];
|
|
193
|
-
sendRenderedChunks: (
|
|
194
|
-
chatId: number,
|
|
195
|
-
chunks: TelegramRenderedChunk[],
|
|
196
|
-
replyToMessageId: number | undefined,
|
|
197
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
198
|
-
) => Promise<number | undefined>;
|
|
199
|
-
editRenderedMessage: (
|
|
200
|
-
chatId: number,
|
|
201
|
-
messageId: number,
|
|
202
|
-
chunks: TelegramRenderedChunk[],
|
|
203
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
204
|
-
) => Promise<number | undefined>;
|
|
205
161
|
canSend?: () => boolean;
|
|
206
162
|
throttleMs?: number;
|
|
207
163
|
maxDraftId?: number;
|
|
@@ -229,18 +185,13 @@ export interface TelegramPreviewController<
|
|
|
229
185
|
flush: (chatId: number) => Promise<void>;
|
|
230
186
|
scheduleFlush: (chatId: number) => void;
|
|
231
187
|
finalize: (chatId: number, replyToMessageId?: number) => Promise<boolean>;
|
|
232
|
-
finalizeMarkdown: (
|
|
233
|
-
chatId: number,
|
|
234
|
-
markdown: string,
|
|
235
|
-
replyToMessageId?: number,
|
|
236
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
237
|
-
) => Promise<boolean>;
|
|
238
188
|
}
|
|
239
189
|
|
|
240
190
|
export interface TelegramPreviewMessageTransportDeps {
|
|
241
191
|
sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
|
|
242
192
|
editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
|
|
243
193
|
buildReplyParameters?: (
|
|
194
|
+
chatId: number,
|
|
244
195
|
replyToMessageId: number | undefined,
|
|
245
196
|
) => TelegramReplyParameters | undefined;
|
|
246
197
|
}
|
|
@@ -252,7 +203,7 @@ export function createTelegramPreviewMessageTransport(
|
|
|
252
203
|
deps.buildReplyParameters ?? buildTelegramReplyParameters;
|
|
253
204
|
return {
|
|
254
205
|
sendMessage: (chatId, text, options, replyToMessageId) => {
|
|
255
|
-
const replyParameters = getReplyParameters(replyToMessageId);
|
|
206
|
+
const replyParameters = getReplyParameters(chatId, replyToMessageId);
|
|
256
207
|
return deps.sendMessage({
|
|
257
208
|
chat_id: chatId,
|
|
258
209
|
text,
|
|
@@ -270,55 +221,13 @@ export function createTelegramPreviewMessageTransport(
|
|
|
270
221
|
};
|
|
271
222
|
}
|
|
272
223
|
|
|
273
|
-
export interface TelegramPreviewRenderedChunkTransportDeps<
|
|
274
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
275
|
-
> {
|
|
276
|
-
sendRenderedChunks: (
|
|
277
|
-
chatId: number,
|
|
278
|
-
chunks: TelegramRenderedChunk[],
|
|
279
|
-
options?: {
|
|
280
|
-
replyToMessageId?: number;
|
|
281
|
-
replyMarkup?: TReplyMarkup;
|
|
282
|
-
},
|
|
283
|
-
) => Promise<number | undefined>;
|
|
284
|
-
editRenderedMessage: (
|
|
285
|
-
chatId: number,
|
|
286
|
-
messageId: number,
|
|
287
|
-
chunks: TelegramRenderedChunk[],
|
|
288
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
289
|
-
) => Promise<number | undefined>;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function createTelegramPreviewRenderedChunkTransport<
|
|
293
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
294
|
-
>(
|
|
295
|
-
deps: TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>,
|
|
296
|
-
): Pick<
|
|
297
|
-
TelegramPreviewControllerDeps<TReplyMarkup>,
|
|
298
|
-
"sendRenderedChunks" | "editRenderedMessage"
|
|
299
|
-
> {
|
|
300
|
-
return {
|
|
301
|
-
sendRenderedChunks: (chatId, chunks, replyToMessageId, options) =>
|
|
302
|
-
deps.sendRenderedChunks(chatId, chunks, {
|
|
303
|
-
replyToMessageId,
|
|
304
|
-
...(options?.replyMarkup ? { replyMarkup: options.replyMarkup } : {}),
|
|
305
|
-
}),
|
|
306
|
-
editRenderedMessage: (chatId, messageId, chunks, options) =>
|
|
307
|
-
deps.editRenderedMessage(chatId, messageId, chunks, options),
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
224
|
export type TelegramPreviewControllerRuntimeDeps<
|
|
312
225
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
313
226
|
> = Omit<
|
|
314
227
|
TelegramPreviewControllerDeps<TReplyMarkup>,
|
|
315
|
-
| "
|
|
316
|
-
| "editMessageText"
|
|
317
|
-
| "sendRenderedChunks"
|
|
318
|
-
| "editRenderedMessage"
|
|
228
|
+
"sendMessage" | "editMessageText"
|
|
319
229
|
> &
|
|
320
|
-
TelegramPreviewMessageTransportDeps
|
|
321
|
-
TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>;
|
|
230
|
+
TelegramPreviewMessageTransportDeps;
|
|
322
231
|
|
|
323
232
|
export function createTelegramPreviewControllerRuntime<
|
|
324
233
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
@@ -328,7 +237,6 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
328
237
|
return createTelegramPreviewController({
|
|
329
238
|
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
|
|
330
239
|
maxMessageLength: deps.maxMessageLength,
|
|
331
|
-
renderPreviewText: deps.renderPreviewText,
|
|
332
240
|
initialDraftSupport: deps.initialDraftSupport,
|
|
333
241
|
sendDraft: deps.sendDraft,
|
|
334
242
|
...createTelegramPreviewMessageTransport({
|
|
@@ -336,11 +244,6 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
336
244
|
editMessageText: deps.editMessageText,
|
|
337
245
|
buildReplyParameters: deps.buildReplyParameters,
|
|
338
246
|
}),
|
|
339
|
-
renderTelegramMessage: deps.renderTelegramMessage,
|
|
340
|
-
...createTelegramPreviewRenderedChunkTransport({
|
|
341
|
-
sendRenderedChunks: deps.sendRenderedChunks,
|
|
342
|
-
editRenderedMessage: deps.editRenderedMessage,
|
|
343
|
-
}),
|
|
344
247
|
throttleMs: deps.throttleMs,
|
|
345
248
|
maxDraftId: deps.maxDraftId,
|
|
346
249
|
setTimer: deps.setTimer,
|
|
@@ -356,13 +259,91 @@ export interface TelegramAssistantPreviewRuntimeDeps<
|
|
|
356
259
|
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
|
357
260
|
isAssistantMessage: (message: TMessage) => boolean;
|
|
358
261
|
getMessageText: (message: TMessage) => string;
|
|
262
|
+
sendMarkdownReply: (
|
|
263
|
+
chatId: number,
|
|
264
|
+
replyToMessageId: number | undefined,
|
|
265
|
+
markdown: string,
|
|
266
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
267
|
+
) => Promise<number | undefined>;
|
|
268
|
+
editMarkdownMessage?: (
|
|
269
|
+
chatId: number,
|
|
270
|
+
messageId: number,
|
|
271
|
+
markdown: string,
|
|
272
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
273
|
+
) => Promise<unknown>;
|
|
359
274
|
}
|
|
360
275
|
|
|
361
276
|
export type TelegramAssistantPreviewRuntime<
|
|
362
277
|
TMessage,
|
|
363
278
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
364
279
|
> = TelegramPreviewController<TReplyMarkup> &
|
|
365
|
-
TelegramAssistantMessagePreviewHooks<TMessage
|
|
280
|
+
TelegramAssistantMessagePreviewHooks<TMessage> & {
|
|
281
|
+
finalizeMarkdown: (
|
|
282
|
+
chatId: number,
|
|
283
|
+
markdown: string,
|
|
284
|
+
replyToMessageId?: number,
|
|
285
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
286
|
+
) => Promise<boolean>;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export function createTelegramNativeMarkdownMessageEditor<TReplyMarkup>(deps: {
|
|
290
|
+
editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
|
|
291
|
+
}): (
|
|
292
|
+
chatId: number,
|
|
293
|
+
messageId: number,
|
|
294
|
+
markdown: string,
|
|
295
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
296
|
+
) => Promise<unknown> {
|
|
297
|
+
return (chatId, messageId, markdown, options) =>
|
|
298
|
+
deps.editMessageText({
|
|
299
|
+
chat_id: chatId,
|
|
300
|
+
message_id: messageId,
|
|
301
|
+
rich_message: { markdown, skip_entity_detection: true },
|
|
302
|
+
...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(deps: {
|
|
307
|
+
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
308
|
+
clear: (chatId: number) => Promise<void>;
|
|
309
|
+
discard?: () => void;
|
|
310
|
+
sendMarkdownReply: (
|
|
311
|
+
chatId: number,
|
|
312
|
+
replyToMessageId: number | undefined,
|
|
313
|
+
markdown: string,
|
|
314
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
315
|
+
) => Promise<number | undefined>;
|
|
316
|
+
editMarkdownMessage?: (
|
|
317
|
+
chatId: number,
|
|
318
|
+
messageId: number,
|
|
319
|
+
markdown: string,
|
|
320
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
321
|
+
) => Promise<unknown>;
|
|
322
|
+
}): (
|
|
323
|
+
chatId: number,
|
|
324
|
+
markdown: string,
|
|
325
|
+
replyToMessageId?: number,
|
|
326
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
327
|
+
) => Promise<boolean> {
|
|
328
|
+
return async (chatId, markdown, replyToMessageId, options) => {
|
|
329
|
+
const state = deps.getState();
|
|
330
|
+
if (state?.mode === "message" && state.messageId !== undefined) {
|
|
331
|
+
try {
|
|
332
|
+
await deps.editMarkdownMessage?.(chatId, state.messageId, markdown, options);
|
|
333
|
+
await deps.clear(chatId);
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
// Fall through to a fresh native Rich Message so final text is not lost.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (state?.flushPromise) {
|
|
340
|
+
await state.flushPromise.catch(() => {});
|
|
341
|
+
}
|
|
342
|
+
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
|
|
343
|
+
deps.discard?.();
|
|
344
|
+
return true;
|
|
345
|
+
};
|
|
346
|
+
}
|
|
366
347
|
|
|
367
348
|
export function createTelegramAssistantPreviewRuntime<
|
|
368
349
|
TMessage,
|
|
@@ -371,8 +352,16 @@ export function createTelegramAssistantPreviewRuntime<
|
|
|
371
352
|
deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
|
|
372
353
|
): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
|
|
373
354
|
const controller = createTelegramPreviewControllerRuntime(deps);
|
|
355
|
+
const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
|
|
356
|
+
getState: controller.getState,
|
|
357
|
+
clear: controller.clear,
|
|
358
|
+
discard: () => controller.setState(undefined),
|
|
359
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
360
|
+
editMarkdownMessage: deps.editMarkdownMessage,
|
|
361
|
+
});
|
|
374
362
|
return {
|
|
375
363
|
...controller,
|
|
364
|
+
finalizeMarkdown: finalizeMarkdownPreview,
|
|
376
365
|
...createTelegramAssistantMessagePreviewHooks({
|
|
377
366
|
getActiveTurn: deps.getActiveTurn,
|
|
378
367
|
isAssistantMessage: deps.isAssistantMessage,
|
|
@@ -380,7 +369,7 @@ export function createTelegramAssistantPreviewRuntime<
|
|
|
380
369
|
setState: controller.setState,
|
|
381
370
|
createPreviewState: controller.createState,
|
|
382
371
|
finalizePreview: controller.finalize,
|
|
383
|
-
finalizeMarkdownPreview
|
|
372
|
+
finalizeMarkdownPreview,
|
|
384
373
|
getMessageText: deps.getMessageText,
|
|
385
374
|
schedulePreviewFlush: controller.scheduleFlush,
|
|
386
375
|
}),
|
|
@@ -400,9 +389,8 @@ export function createTelegramPreviewController<
|
|
|
400
389
|
setTimeout(callback, ms));
|
|
401
390
|
const throttleMs = deps.throttleMs ?? TELEGRAM_PREVIEW_THROTTLE_MS;
|
|
402
391
|
const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
|
|
403
|
-
const maxMessageLength =
|
|
404
|
-
|
|
405
|
-
const renderMessage = deps.renderTelegramMessage ?? renderTelegramMessage;
|
|
392
|
+
const maxMessageLength =
|
|
393
|
+
deps.maxMessageLength ?? TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS;
|
|
406
394
|
let draftSupport = deps.initialDraftSupport ?? "unknown";
|
|
407
395
|
let nextDraftId = 0;
|
|
408
396
|
const getRuntimeDeps = (
|
|
@@ -418,7 +406,6 @@ export function createTelegramPreviewController<
|
|
|
418
406
|
nextState.flushTimer = undefined;
|
|
419
407
|
},
|
|
420
408
|
maxMessageLength,
|
|
421
|
-
renderPreviewText: renderPreview,
|
|
422
409
|
getDraftSupport: () => draftSupport,
|
|
423
410
|
setDraftSupport: (support) => {
|
|
424
411
|
draftSupport = support;
|
|
@@ -436,15 +423,6 @@ export function createTelegramPreviewController<
|
|
|
436
423
|
replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
|
|
437
424
|
),
|
|
438
425
|
editMessageText: deps.editMessageText,
|
|
439
|
-
renderTelegramMessage: renderMessage,
|
|
440
|
-
sendRenderedChunks: (chatId, chunks, options) =>
|
|
441
|
-
deps.sendRenderedChunks(
|
|
442
|
-
chatId,
|
|
443
|
-
chunks,
|
|
444
|
-
replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
|
|
445
|
-
options,
|
|
446
|
-
),
|
|
447
|
-
editRenderedMessage: deps.editRenderedMessage,
|
|
448
426
|
canSend: deps.canSend,
|
|
449
427
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
450
428
|
});
|
|
@@ -464,6 +442,10 @@ export function createTelegramPreviewController<
|
|
|
464
442
|
flush: (chatId) => flushTelegramPreview(chatId, getRuntimeDeps()),
|
|
465
443
|
scheduleFlush: (chatId) => {
|
|
466
444
|
if (!state || state.flushTimer) return;
|
|
445
|
+
if (throttleMs <= 0) {
|
|
446
|
+
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
467
449
|
state.flushTimer = setTimer(() => {
|
|
468
450
|
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
469
451
|
}, throttleMs);
|
|
@@ -471,13 +453,6 @@ export function createTelegramPreviewController<
|
|
|
471
453
|
},
|
|
472
454
|
finalize: (chatId, replyToMessageId) =>
|
|
473
455
|
finalizeTelegramPreview(chatId, getRuntimeDeps(replyToMessageId)),
|
|
474
|
-
finalizeMarkdown: (chatId, markdown, replyToMessageId, options) =>
|
|
475
|
-
finalizeTelegramMarkdownPreview(
|
|
476
|
-
chatId,
|
|
477
|
-
markdown,
|
|
478
|
-
getRuntimeDeps(replyToMessageId),
|
|
479
|
-
options,
|
|
480
|
-
),
|
|
481
456
|
};
|
|
482
457
|
}
|
|
483
458
|
|
|
@@ -553,10 +528,7 @@ export function buildTelegramPreviewFinalText(
|
|
|
553
528
|
): string | undefined {
|
|
554
529
|
const finalText = state.pendingText.trim();
|
|
555
530
|
if (finalText) return finalText;
|
|
556
|
-
if (
|
|
557
|
-
state.lastSentStrategy === "rich-stable-blocks" ||
|
|
558
|
-
state.lastSentParseMode === "HTML"
|
|
559
|
-
) {
|
|
531
|
+
if (state.lastSentParseMode === "HTML") {
|
|
560
532
|
return undefined;
|
|
561
533
|
}
|
|
562
534
|
return state.lastSentText.trim() || undefined;
|
|
@@ -579,14 +551,16 @@ export function allocateTelegramDraftId(
|
|
|
579
551
|
return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
|
|
580
552
|
}
|
|
581
553
|
|
|
554
|
+
interface TelegramNativeMarkdownPreviewSnapshot {
|
|
555
|
+
text: string;
|
|
556
|
+
sourceText: string;
|
|
557
|
+
}
|
|
558
|
+
|
|
582
559
|
export function shouldUseTelegramDraftPreview(options: {
|
|
583
560
|
draftSupport: TelegramDraftSupport;
|
|
584
|
-
snapshot?:
|
|
561
|
+
snapshot?: TelegramNativeMarkdownPreviewSnapshot;
|
|
585
562
|
}): boolean {
|
|
586
|
-
return
|
|
587
|
-
options.draftSupport !== "unsupported" &&
|
|
588
|
-
(options.snapshot === undefined || options.snapshot.strategy === "plain")
|
|
589
|
-
);
|
|
563
|
+
return options.draftSupport !== "unsupported";
|
|
590
564
|
}
|
|
591
565
|
|
|
592
566
|
export async function clearTelegramPreview<
|
|
@@ -594,12 +568,43 @@ export async function clearTelegramPreview<
|
|
|
594
568
|
>(
|
|
595
569
|
chatId: number,
|
|
596
570
|
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
571
|
+
options: { awaitFlush?: boolean } = {},
|
|
597
572
|
): Promise<void> {
|
|
598
|
-
void chatId;
|
|
599
573
|
const state = deps.getState();
|
|
600
574
|
if (!state) return;
|
|
601
575
|
deps.clearScheduledFlush(state);
|
|
576
|
+
if (state.flushPromise && options.awaitFlush !== false) {
|
|
577
|
+
state.flushRequested = false;
|
|
578
|
+
await state.flushPromise.catch(() => {});
|
|
579
|
+
if (deps.getState() !== state) return;
|
|
580
|
+
}
|
|
602
581
|
deps.setState(undefined);
|
|
582
|
+
if (state.mode === "draft" && state.draftId !== undefined) {
|
|
583
|
+
try {
|
|
584
|
+
await deps.sendDraft(chatId, state.draftId, undefined);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
deps.recordRuntimeEvent?.("preview", error, {
|
|
587
|
+
phase: "clear-draft",
|
|
588
|
+
chatId,
|
|
589
|
+
draftId: state.draftId,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function buildTelegramNativeMarkdownPreviewSnapshot(
|
|
596
|
+
state: TelegramPreviewState,
|
|
597
|
+
maxMessageLength: number,
|
|
598
|
+
): TelegramNativeMarkdownPreviewSnapshot | undefined {
|
|
599
|
+
const sourceText = state.pendingText.trim();
|
|
600
|
+
if (!sourceText || sourceText === state.lastSentText) return undefined;
|
|
601
|
+
return {
|
|
602
|
+
text:
|
|
603
|
+
sourceText.length > maxMessageLength
|
|
604
|
+
? sourceText.slice(0, maxMessageLength)
|
|
605
|
+
: sourceText,
|
|
606
|
+
sourceText,
|
|
607
|
+
};
|
|
603
608
|
}
|
|
604
609
|
|
|
605
610
|
async function performTelegramPreviewFlush<
|
|
@@ -610,15 +615,13 @@ async function performTelegramPreviewFlush<
|
|
|
610
615
|
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
611
616
|
): Promise<void> {
|
|
612
617
|
if (deps.canSend && !deps.canSend()) {
|
|
613
|
-
await clearTelegramPreview(chatId, deps);
|
|
618
|
+
await clearTelegramPreview(chatId, deps, { awaitFlush: false });
|
|
614
619
|
return;
|
|
615
620
|
}
|
|
616
|
-
const snapshot =
|
|
621
|
+
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
|
|
617
622
|
state,
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
renderTelegramMessage: deps.renderTelegramMessage,
|
|
621
|
-
});
|
|
623
|
+
deps.maxMessageLength,
|
|
624
|
+
);
|
|
622
625
|
if (!snapshot) return;
|
|
623
626
|
if (
|
|
624
627
|
shouldUseTelegramDraftPreview({
|
|
@@ -629,35 +632,28 @@ async function performTelegramPreviewFlush<
|
|
|
629
632
|
const draftId = state.draftId ?? deps.allocateDraftId();
|
|
630
633
|
state.draftId = draftId;
|
|
631
634
|
try {
|
|
632
|
-
await deps.sendDraft(chatId, draftId, snapshot.
|
|
635
|
+
await deps.sendDraft(chatId, draftId, snapshot.sourceText);
|
|
633
636
|
deps.setDraftSupport("supported");
|
|
634
637
|
state.mode = "draft";
|
|
635
638
|
state.lastSentText = snapshot.text;
|
|
636
|
-
state.lastSentParseMode =
|
|
637
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
639
|
+
state.lastSentParseMode = undefined;
|
|
638
640
|
return;
|
|
639
641
|
} catch {
|
|
640
642
|
deps.setDraftSupport("unsupported");
|
|
641
643
|
}
|
|
642
644
|
}
|
|
643
645
|
if (state.messageId === undefined) {
|
|
644
|
-
const sent = await deps.sendMessage(chatId, snapshot.text,
|
|
645
|
-
parseMode: snapshot.parseMode,
|
|
646
|
-
});
|
|
646
|
+
const sent = await deps.sendMessage(chatId, snapshot.text, undefined);
|
|
647
647
|
state.messageId = sent.message_id;
|
|
648
648
|
state.mode = "message";
|
|
649
649
|
state.lastSentText = snapshot.text;
|
|
650
|
-
state.lastSentParseMode =
|
|
651
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
650
|
+
state.lastSentParseMode = undefined;
|
|
652
651
|
return;
|
|
653
652
|
}
|
|
654
|
-
await deps.editMessageText(chatId, state.messageId, snapshot.text,
|
|
655
|
-
parseMode: snapshot.parseMode,
|
|
656
|
-
});
|
|
653
|
+
await deps.editMessageText(chatId, state.messageId, snapshot.text, undefined);
|
|
657
654
|
state.mode = "message";
|
|
658
655
|
state.lastSentText = snapshot.text;
|
|
659
|
-
state.lastSentParseMode =
|
|
660
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
656
|
+
state.lastSentParseMode = undefined;
|
|
661
657
|
}
|
|
662
658
|
|
|
663
659
|
export async function flushTelegramPreview<
|
|
@@ -718,49 +714,9 @@ export async function finalizeTelegramPreview<
|
|
|
718
714
|
}
|
|
719
715
|
if (state.mode === "draft") {
|
|
720
716
|
await deps.sendMessage(chatId, finalText);
|
|
721
|
-
|
|
717
|
+
deps.setState(undefined);
|
|
722
718
|
return true;
|
|
723
719
|
}
|
|
724
720
|
deps.setState(undefined);
|
|
725
721
|
return state.messageId !== undefined;
|
|
726
722
|
}
|
|
727
|
-
|
|
728
|
-
export async function finalizeTelegramMarkdownPreview<
|
|
729
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
730
|
-
>(
|
|
731
|
-
chatId: number,
|
|
732
|
-
markdown: string,
|
|
733
|
-
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
734
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
735
|
-
): Promise<boolean> {
|
|
736
|
-
const state = deps.getState();
|
|
737
|
-
if (!state) return false;
|
|
738
|
-
if (deps.canSend && !deps.canSend()) {
|
|
739
|
-
await clearTelegramPreview(chatId, deps);
|
|
740
|
-
return false;
|
|
741
|
-
}
|
|
742
|
-
await flushTelegramPreview(chatId, deps);
|
|
743
|
-
const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
|
|
744
|
-
if (chunks.length === 0) {
|
|
745
|
-
await clearTelegramPreview(chatId, deps);
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
try {
|
|
749
|
-
if (state.mode === "draft") {
|
|
750
|
-
await deps.sendRenderedChunks(chatId, chunks, options);
|
|
751
|
-
await clearTelegramPreview(chatId, deps);
|
|
752
|
-
return true;
|
|
753
|
-
}
|
|
754
|
-
if (state.messageId === undefined) return false;
|
|
755
|
-
await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
|
|
756
|
-
deps.setState(undefined);
|
|
757
|
-
return true;
|
|
758
|
-
} catch (error) {
|
|
759
|
-
deps.recordRuntimeEvent?.("preview", error, {
|
|
760
|
-
phase: "finalize-markdown",
|
|
761
|
-
chatId,
|
|
762
|
-
messageId: state.messageId,
|
|
763
|
-
});
|
|
764
|
-
return false;
|
|
765
|
-
}
|
|
766
|
-
}
|
package/lib/prompts.ts
CHANGED
|
@@ -28,13 +28,13 @@ Telegram-originated turn context:
|
|
|
28
28
|
- Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
|
|
29
29
|
|
|
30
30
|
Telegram-visible output:
|
|
31
|
-
- Telegram is
|
|
32
|
-
-
|
|
31
|
+
- Telegram is mobile-first: keep answers easy to scan, use headings/lists when useful, and avoid unnecessarily huge blocks of text.
|
|
32
|
+
- For formulas, use math delimiters like \`$E = mc^2$\` for inline formulas and \`$$\\nE = mc^2\\n$$\` for block formulas; do not wrap formulas in backticks unless they should render as literal code.
|
|
33
33
|
- Wide monospace blocks can become unreadable on mobile; use them only when structure or literal code requires them.
|
|
34
34
|
- For requested/generated files, call \`telegram_attach(local_path)\`; during Telegram turns it attaches files to the active reply, and during explicit local/TUI Telegram-delivery requests it sends files directly to the paired/default chat or an explicit \`chat_id\`. If a local/TUI user explicitly asks to send a text message to Telegram, use \`telegram_message\` with Markdown text; embed the same top-level \`telegram_button\` comments when inline prompt buttons are needed, because Telegram buttons must belong to a message. Direct local/TUI Telegram delivery requires this π instance to own \`/telegram-connect\`; if ownership is elsewhere, connect/take over first instead of bypassing the lock.
|
|
35
35
|
|
|
36
36
|
Native outbound actions:
|
|
37
|
-
- Use normal Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
|
|
37
|
+
- Use normal Rich Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
|
|
38
38
|
- \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line text, or \`<!-- telegram_voice: Short summary -->\` for one-line text with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
|
|
39
39
|
- \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts. Do not put button comments inline after visible text, inside code fences, block quotes, lists, or indented examples; those are literal Markdown, not buttons.
|
|
40
40
|
- If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.
|
package/lib/queue.ts
CHANGED
|
@@ -1439,6 +1439,7 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
|
|
|
1439
1439
|
deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
|
|
1440
1440
|
): Promise<void> {
|
|
1441
1441
|
deps.unbindDeferredDispatchContext?.();
|
|
1442
|
+
await deps.stopPolling();
|
|
1442
1443
|
deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
|
|
1443
1444
|
deps.clearPendingMediaGroups();
|
|
1444
1445
|
deps.clearModelMenuState();
|
|
@@ -1448,7 +1449,6 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
|
|
|
1448
1449
|
}
|
|
1449
1450
|
deps.clearActiveTurn();
|
|
1450
1451
|
deps.clearAbort();
|
|
1451
|
-
await deps.stopPolling();
|
|
1452
1452
|
}
|
|
1453
1453
|
|
|
1454
1454
|
export type TelegramSessionLifecycleRuntimeDeps<
|