@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/docs/ui-style.md
CHANGED
package/index.ts
CHANGED
|
@@ -147,6 +147,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
147
147
|
sendRecordVoiceAction,
|
|
148
148
|
sendMessageDraft,
|
|
149
149
|
sendMessage,
|
|
150
|
+
sendRichMessage,
|
|
151
|
+
sendRichMessageDraft,
|
|
150
152
|
downloadFile: downloadTelegramBridgeFile,
|
|
151
153
|
editMessageText: editTelegramMessageText,
|
|
152
154
|
answerCallbackQuery,
|
|
@@ -161,7 +163,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
161
163
|
// --- Message Delivery ---
|
|
162
164
|
|
|
163
165
|
const sendGuestReply = Replies.createGuestMarkdownReplySender({
|
|
164
|
-
renderTelegramMessage: Replies.renderTelegramMessage,
|
|
165
166
|
answerGuestQuery,
|
|
166
167
|
});
|
|
167
168
|
|
|
@@ -189,6 +190,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
189
190
|
|
|
190
191
|
const replyRuntime = Replies.createTelegramRenderedMessageDeliveryRuntime({
|
|
191
192
|
sendMessage,
|
|
193
|
+
sendRichMessage,
|
|
192
194
|
editMessage: editTelegramMessageText,
|
|
193
195
|
});
|
|
194
196
|
const { replyTransport, editInteractiveMessage, sendInteractiveMessage } =
|
|
@@ -221,9 +223,16 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
221
223
|
isAssistantMessage: Replies.isAssistantAgentMessage,
|
|
222
224
|
getMessageText: Replies.getAgentMessageText,
|
|
223
225
|
getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
|
|
224
|
-
sendDraft:
|
|
226
|
+
sendDraft: TelegramApi.createTelegramNativeMarkdownDraftSender({
|
|
227
|
+
sendMessageDraft,
|
|
228
|
+
sendRichMessageDraft,
|
|
229
|
+
}),
|
|
225
230
|
sendMessage,
|
|
226
231
|
editMessageText: editTelegramMessageText,
|
|
232
|
+
sendMarkdownReply,
|
|
233
|
+
editMarkdownMessage: Preview.createTelegramNativeMarkdownMessageEditor({
|
|
234
|
+
editMessageText: editTelegramMessageText,
|
|
235
|
+
}),
|
|
227
236
|
recordRuntimeEvent,
|
|
228
237
|
...replyTransport,
|
|
229
238
|
});
|
package/lib/commands.ts
CHANGED
|
@@ -494,7 +494,7 @@ export interface TelegramCompactConfirmationDeps {
|
|
|
494
494
|
sendInteractiveMessage: (
|
|
495
495
|
chatId: number,
|
|
496
496
|
text: string,
|
|
497
|
-
mode: "html" | "plain",
|
|
497
|
+
mode: "markdown" | "html" | "plain",
|
|
498
498
|
replyMarkup: TelegramCompactConfirmationReplyMarkup,
|
|
499
499
|
) => Promise<number | undefined>;
|
|
500
500
|
}
|
|
@@ -515,7 +515,7 @@ export interface TelegramCompactConfirmationCallbackDeps<TContext> {
|
|
|
515
515
|
chatId: number,
|
|
516
516
|
messageId: number,
|
|
517
517
|
text: string,
|
|
518
|
-
mode: "html" | "plain",
|
|
518
|
+
mode: "markdown" | "html" | "plain",
|
|
519
519
|
replyMarkup: TelegramCompactConfirmationReplyMarkup,
|
|
520
520
|
) => Promise<void>;
|
|
521
521
|
runCompact: (
|
package/lib/keyboard.ts
CHANGED
|
@@ -12,3 +12,42 @@ export interface TelegramInlineKeyboardButton {
|
|
|
12
12
|
export interface TelegramInlineKeyboardMarkup {
|
|
13
13
|
inline_keyboard: TelegramInlineKeyboardButton[][];
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
|
|
17
|
+
|
|
18
|
+
export function getTelegramCallbackDataByteLength(value: string): number {
|
|
19
|
+
return new TextEncoder().encode(value).byteLength;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function assertTelegramCallbackData(
|
|
23
|
+
callbackData: string,
|
|
24
|
+
context = "Telegram callback_data",
|
|
25
|
+
): string {
|
|
26
|
+
const byteLength = getTelegramCallbackDataByteLength(callbackData);
|
|
27
|
+
if (byteLength > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`${context} exceeds ${TELEGRAM_CALLBACK_DATA_MAX_BYTES} bytes (${byteLength}). Use a shorter action/payload or store state behind a compact key.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return callbackData;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assertTelegramInlineKeyboardCallbackData(
|
|
36
|
+
replyMarkup: unknown,
|
|
37
|
+
context = "Telegram inline keyboard callback_data",
|
|
38
|
+
): void {
|
|
39
|
+
if (!replyMarkup || typeof replyMarkup !== "object") return;
|
|
40
|
+
const keyboard = (replyMarkup as { inline_keyboard?: unknown })
|
|
41
|
+
.inline_keyboard;
|
|
42
|
+
if (!Array.isArray(keyboard)) return;
|
|
43
|
+
for (const row of keyboard) {
|
|
44
|
+
if (!Array.isArray(row)) continue;
|
|
45
|
+
for (const button of row) {
|
|
46
|
+
if (!button || typeof button !== "object") continue;
|
|
47
|
+
const callbackData = (button as { callback_data?: unknown })
|
|
48
|
+
.callback_data;
|
|
49
|
+
if (typeof callbackData !== "string") continue;
|
|
50
|
+
assertTelegramCallbackData(callbackData, context);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/lib/lifecycle.ts
CHANGED
|
@@ -138,6 +138,11 @@ export function createTelegramSessionContextTracker(
|
|
|
138
138
|
|
|
139
139
|
type TelegramLifecycleTimer = number | ReturnType<typeof setTimeout>;
|
|
140
140
|
|
|
141
|
+
function unrefTelegramLifecycleTimer(timer: TelegramLifecycleTimer): void {
|
|
142
|
+
if (!timer || typeof timer !== "object") return;
|
|
143
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
144
|
+
}
|
|
145
|
+
|
|
141
146
|
export interface TelegramCompactionObserverRuntimeDeps<TContext> {
|
|
142
147
|
setCompactionInProgress: (inProgress: boolean) => void;
|
|
143
148
|
updateStatus: (ctx: TContext) => void;
|
|
@@ -196,6 +201,7 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
196
201
|
);
|
|
197
202
|
requestDispatch();
|
|
198
203
|
}, timeoutMs);
|
|
204
|
+
unrefTelegramLifecycleTimer(fallbackTimer);
|
|
199
205
|
},
|
|
200
206
|
onSessionCompact: (_event, ctx) => {
|
|
201
207
|
clearFallbackTimer();
|
package/lib/menu-model.ts
CHANGED
|
@@ -128,13 +128,13 @@ export interface TelegramMenuMessageRuntimeDeps {
|
|
|
128
128
|
chatId: number,
|
|
129
129
|
messageId: number,
|
|
130
130
|
text: string,
|
|
131
|
-
mode: "html" | "plain",
|
|
131
|
+
mode: "markdown" | "html" | "plain",
|
|
132
132
|
replyMarkup: TelegramReplyMarkup,
|
|
133
133
|
) => Promise<void>;
|
|
134
134
|
sendInteractiveMessage: (
|
|
135
135
|
chatId: number,
|
|
136
136
|
text: string,
|
|
137
|
-
mode: "html" | "plain",
|
|
137
|
+
mode: "markdown" | "html" | "plain",
|
|
138
138
|
replyMarkup: TelegramReplyMarkup,
|
|
139
139
|
) => Promise<number | undefined>;
|
|
140
140
|
}
|
|
@@ -200,7 +200,7 @@ export interface TelegramModelMenuPage<TModel extends MenuModel = MenuModel> {
|
|
|
200
200
|
export interface TelegramMenuRenderPayload {
|
|
201
201
|
nextMode: TelegramModelMenuState["mode"];
|
|
202
202
|
text: string;
|
|
203
|
-
mode: "html" | "plain";
|
|
203
|
+
mode: "markdown" | "html" | "plain";
|
|
204
204
|
replyMarkup: TelegramReplyMarkup;
|
|
205
205
|
}
|
|
206
206
|
|
package/lib/menu-settings.ts
CHANGED
|
@@ -98,13 +98,13 @@ export interface TelegramSettingsMenuRuntimeDeps<
|
|
|
98
98
|
chatId: number,
|
|
99
99
|
messageId: number,
|
|
100
100
|
text: string,
|
|
101
|
-
mode: "html" | "plain",
|
|
101
|
+
mode: "markdown" | "html" | "plain",
|
|
102
102
|
replyMarkup: TelegramSettingsMenuReplyMarkup,
|
|
103
103
|
) => Promise<void>;
|
|
104
104
|
sendInteractiveMessage: (
|
|
105
105
|
chatId: number,
|
|
106
106
|
text: string,
|
|
107
|
-
mode: "html" | "plain",
|
|
107
|
+
mode: "markdown" | "html" | "plain",
|
|
108
108
|
replyMarkup: TelegramSettingsMenuReplyMarkup,
|
|
109
109
|
) => Promise<number | undefined>;
|
|
110
110
|
answerCallbackQuery: (
|
package/lib/menu.ts
CHANGED
|
@@ -228,13 +228,13 @@ export interface TelegramMenuCallbackRuntimeDeps<
|
|
|
228
228
|
chatId: number,
|
|
229
229
|
messageId: number,
|
|
230
230
|
text: string,
|
|
231
|
-
mode: "html" | "plain",
|
|
231
|
+
mode: "markdown" | "html" | "plain",
|
|
232
232
|
replyMarkup: TelegramReplyMarkup,
|
|
233
233
|
) => Promise<void>;
|
|
234
234
|
sendInteractiveMessage?: (
|
|
235
235
|
chatId: number,
|
|
236
236
|
text: string,
|
|
237
|
-
mode: "html" | "plain",
|
|
237
|
+
mode: "markdown" | "html" | "plain",
|
|
238
238
|
replyMarkup: TelegramReplyMarkup,
|
|
239
239
|
) => Promise<number | undefined>;
|
|
240
240
|
enqueueSectionPrompt?: (prompt: string, ctx: TContext) => Promise<void>;
|
|
@@ -464,13 +464,13 @@ export interface TelegramMenuCallbackRuntimeAdapterDeps<
|
|
|
464
464
|
chatId: number,
|
|
465
465
|
messageId: number,
|
|
466
466
|
text: string,
|
|
467
|
-
mode: "html" | "plain",
|
|
467
|
+
mode: "markdown" | "html" | "plain",
|
|
468
468
|
replyMarkup: TelegramReplyMarkup,
|
|
469
469
|
) => Promise<void>;
|
|
470
470
|
sendInteractiveMessage?: (
|
|
471
471
|
chatId: number,
|
|
472
472
|
text: string,
|
|
473
|
-
mode: "html" | "plain",
|
|
473
|
+
mode: "markdown" | "html" | "plain",
|
|
474
474
|
replyMarkup: TelegramReplyMarkup,
|
|
475
475
|
) => Promise<number | undefined>;
|
|
476
476
|
enqueueSectionPrompt?: (prompt: string, ctx: TContext) => Promise<void>;
|
|
@@ -474,6 +474,7 @@ export async function sendQueuedTelegramOutboundAttachments(
|
|
|
474
474
|
const method = isPhoto ? "sendPhoto" : "sendDocument";
|
|
475
475
|
const fieldName = isPhoto ? "photo" : "document";
|
|
476
476
|
const replyParameters = buildTelegramMultipartReplyParameters(
|
|
477
|
+
turn.chatId,
|
|
477
478
|
turn.replyToMessageId,
|
|
478
479
|
);
|
|
479
480
|
await deps.sendMultipart(
|
package/lib/outbound-buttons.ts
CHANGED
|
@@ -135,6 +135,7 @@ export function createTelegramButtonActionStore(
|
|
|
135
135
|
cleanup(currentTime);
|
|
136
136
|
const action = actions.get(callbackData);
|
|
137
137
|
if (!action) return undefined;
|
|
138
|
+
actions.delete(callbackData);
|
|
138
139
|
return { text: action.text, prompt: action.prompt };
|
|
139
140
|
},
|
|
140
141
|
};
|
package/lib/outbound-voice.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { unlink } from "node:fs/promises";
|
|
8
8
|
import { basename, extname } from "node:path";
|
|
9
9
|
|
|
10
|
+
import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
|
|
10
11
|
import { getTelegramVoiceSynthesisProviders } from "./voice.ts";
|
|
11
12
|
|
|
12
13
|
export interface TelegramVoiceReplyTurnView {
|
|
@@ -125,6 +126,7 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
125
126
|
},
|
|
126
127
|
): Promise<void> {
|
|
127
128
|
const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
|
|
129
|
+
assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
|
|
128
130
|
await sendVoiceChatAction(deps, turn.chatId);
|
|
129
131
|
const replyParameters = buildVoiceReplyParameters(
|
|
130
132
|
options?.replyToPrompt,
|
package/lib/polling.ts
CHANGED
|
@@ -187,11 +187,21 @@ export function shouldStartTelegramPolling(
|
|
|
187
187
|
export async function stopTelegramPollingRuntime<TContext>(
|
|
188
188
|
deps: TelegramPollingRuntimeDeps<TContext>,
|
|
189
189
|
): Promise<void> {
|
|
190
|
-
deps.
|
|
191
|
-
deps.getPollingController()
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
const pollingPromise = deps.getPollingPromise();
|
|
191
|
+
const pollingController = deps.getPollingController();
|
|
192
|
+
try {
|
|
193
|
+
deps.stopTypingLoop();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
deps.recordRuntimeEvent?.("polling", error, { phase: "typing-stop" });
|
|
196
|
+
}
|
|
197
|
+
pollingController?.abort();
|
|
198
|
+
await pollingPromise?.catch(() => undefined);
|
|
199
|
+
if (deps.getPollingPromise() === pollingPromise) {
|
|
200
|
+
deps.setPollingPromise(undefined);
|
|
201
|
+
}
|
|
202
|
+
if (deps.getPollingController() === pollingController) {
|
|
203
|
+
deps.setPollingController(undefined);
|
|
204
|
+
}
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
function updateTelegramPollingStatusSafely<TContext>(
|
|
@@ -224,9 +234,12 @@ export function startTelegramPollingRuntime<TContext>(
|
|
|
224
234
|
}
|
|
225
235
|
const controller = deps.createAbortController?.() ?? new AbortController();
|
|
226
236
|
deps.setPollingController(controller);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
deps.
|
|
237
|
+
let promise: Promise<void>;
|
|
238
|
+
promise = deps.runPollLoop(ctx, controller.signal).finally(() => {
|
|
239
|
+
if (deps.getPollingPromise() === promise) deps.setPollingPromise(undefined);
|
|
240
|
+
if (deps.getPollingController() === controller) {
|
|
241
|
+
deps.setPollingController(undefined);
|
|
242
|
+
}
|
|
230
243
|
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
231
244
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
232
245
|
});
|