@llblab/pi-telegram 0.11.2 → 0.12.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 +15 -11
- package/BACKLOG.md +0 -10
- package/CHANGELOG.md +18 -0
- package/README.md +18 -14
- package/api/inbound.ts +14 -0
- package/api/keyboard.ts +10 -0
- package/api/outbound.ts +11 -0
- package/api/sections.ts +17 -0
- package/api/updates.ts +11 -0
- package/api/voice.ts +24 -0
- package/docs/README.md +7 -5
- package/docs/architecture.md +160 -225
- package/docs/callback-namespaces.md +3 -3
- package/docs/{inbound-handlers.md → inbound.md} +13 -10
- package/docs/locks.md +3 -3
- package/docs/{outbound-handlers.md → outbound.md} +13 -10
- package/docs/public-api.md +266 -0
- package/docs/{extension-sections.md → sections.md} +31 -27
- package/docs/ui-style.md +165 -0
- package/docs/{external-handlers.md → updates.md} +33 -31
- package/docs/voice.md +17 -14
- package/index.ts +84 -238
- package/lib/bindings.ts +301 -0
- package/lib/commands.ts +114 -1
- package/lib/config.ts +43 -2
- package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
- package/lib/lifecycle.ts +41 -6
- package/lib/menu-model.ts +3 -3
- package/lib/menu-queue.ts +1 -1
- package/lib/menu-settings.ts +21 -10
- package/lib/menu-status.ts +1 -1
- package/lib/menu.ts +1 -1
- package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
- package/lib/polling.ts +4 -3
- package/lib/preview.ts +1 -1
- package/lib/routing.ts +44 -3
- package/lib/{extension-sections.ts → sections.ts} +37 -8
- package/lib/updates.ts +121 -1
- package/lib/voice.ts +33 -14
- package/package.json +11 -1
- package/lib/external-handlers.ts +0 -166
package/lib/menu-settings.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {
|
|
8
8
|
getTelegramExtensionSettingsRows,
|
|
9
9
|
type TelegramSectionRegistry,
|
|
10
|
-
} from "./
|
|
10
|
+
} from "./sections.ts";
|
|
11
11
|
import type { TelegramTimeMode } from "./config.ts";
|
|
12
12
|
import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
|
|
13
13
|
import type { TelegramModelMenuState } from "./menu-model.ts";
|
|
@@ -115,7 +115,8 @@ export interface TelegramSettingsMenuRuntimeDeps<
|
|
|
115
115
|
|
|
116
116
|
export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
|
|
117
117
|
export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
|
|
118
|
-
export const TIME_INJECTION_MODE_SETTINGS_TITLE =
|
|
118
|
+
export const TIME_INJECTION_MODE_SETTINGS_TITLE =
|
|
119
|
+
"<b>🕒 Time injection mode:</b>";
|
|
119
120
|
export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
|
|
120
121
|
|
|
121
122
|
type TelegramVoiceReplyModeSetting = TelegramVoiceReplyMode | "hidden";
|
|
@@ -124,6 +125,10 @@ function getVoiceReplyModeLabel(mode: TelegramVoiceReplyModeSetting): string {
|
|
|
124
125
|
return mode;
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
function getTelegramSettingsStateValueLabel(value: string): string {
|
|
129
|
+
return value.length > 0 ? value[0]!.toUpperCase() + value.slice(1) : value;
|
|
130
|
+
}
|
|
131
|
+
|
|
127
132
|
function getVoiceReplyModeSetting(
|
|
128
133
|
mode: TelegramVoiceReplyMode,
|
|
129
134
|
configured: boolean,
|
|
@@ -197,21 +202,23 @@ export function buildTelegramSettingsMenuReplyMarkup(
|
|
|
197
202
|
rows.push(
|
|
198
203
|
[
|
|
199
204
|
{
|
|
200
|
-
text: `👄 Voice reply: ${
|
|
201
|
-
|
|
205
|
+
text: `👄 Voice reply: ${getTelegramSettingsStateValueLabel(
|
|
206
|
+
getVoiceReplyModeLabel(
|
|
207
|
+
getVoiceReplyModeSetting(voiceReplyMode, voiceReplyModeConfigured),
|
|
208
|
+
),
|
|
202
209
|
)}`,
|
|
203
210
|
callback_data: "settings:open:voice-reply",
|
|
204
211
|
},
|
|
205
212
|
],
|
|
206
213
|
[
|
|
207
214
|
{
|
|
208
|
-
text: `🕒 Time injection: ${timeInjectionMode}`,
|
|
215
|
+
text: `🕒 Time injection: ${getTelegramSettingsStateValueLabel(timeInjectionMode)}`,
|
|
209
216
|
callback_data: "settings:open:time-injection",
|
|
210
217
|
},
|
|
211
218
|
],
|
|
212
219
|
[
|
|
213
220
|
{
|
|
214
|
-
text: `📌 Proactive push: ${proactivePushEnabled ? "
|
|
221
|
+
text: `📌 Proactive push: ${proactivePushEnabled ? "On" : "Off"}`,
|
|
215
222
|
callback_data: "settings:open:proactive",
|
|
216
223
|
},
|
|
217
224
|
],
|
|
@@ -251,11 +258,11 @@ export function buildProactivePushSettingsReplyMarkup(
|
|
|
251
258
|
[{ text: "⬆️ Back", callback_data: "settings:list" }],
|
|
252
259
|
[
|
|
253
260
|
{
|
|
254
|
-
text: proactivePushEnabled ? "🟢
|
|
261
|
+
text: proactivePushEnabled ? "🟢 On" : "⚫️ On",
|
|
255
262
|
callback_data: "settings:set:proactive:on",
|
|
256
263
|
},
|
|
257
264
|
{
|
|
258
|
-
text: proactivePushEnabled ? "⚫️
|
|
265
|
+
text: proactivePushEnabled ? "⚫️ Off" : "🟡 Off",
|
|
259
266
|
callback_data: "settings:set:proactive:off",
|
|
260
267
|
},
|
|
261
268
|
],
|
|
@@ -372,7 +379,10 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
372
379
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
373
380
|
return true;
|
|
374
381
|
}
|
|
375
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
data === "settings:open:time-injection" ||
|
|
384
|
+
data === "settings:open:time"
|
|
385
|
+
) {
|
|
376
386
|
await updateTimeInjectionModeSettingsMessage(deps);
|
|
377
387
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
378
388
|
return true;
|
|
@@ -506,7 +516,8 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
506
516
|
? query.data.slice("settings:set:time-injection:".length)
|
|
507
517
|
: query.data.slice("settings:set:time:".length);
|
|
508
518
|
if (
|
|
509
|
-
(hasTimeInjectionPrefix ||
|
|
519
|
+
(hasTimeInjectionPrefix ||
|
|
520
|
+
query.data.startsWith("settings:set:time:")) &&
|
|
510
521
|
(timeMode === "off" ||
|
|
511
522
|
timeMode === "hidden" ||
|
|
512
523
|
timeMode === "always" ||
|
package/lib/menu-status.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { formatTelegramCommandEmojiPrefix } from "./commands.ts";
|
|
|
8
8
|
import {
|
|
9
9
|
getTelegramSectionMainMenuRows,
|
|
10
10
|
type TelegramSectionRegistry,
|
|
11
|
-
} from "./
|
|
11
|
+
} from "./sections.ts";
|
|
12
12
|
import {
|
|
13
13
|
formatStatusButtonLabel,
|
|
14
14
|
type TelegramMenuMessageRuntimeDeps,
|
package/lib/menu.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
handleTelegramSectionSettingsOpen,
|
|
11
11
|
parseTelegramSectionCallback,
|
|
12
12
|
type TelegramSectionRegistry,
|
|
13
|
-
} from "./
|
|
13
|
+
} from "./sections.ts";
|
|
14
14
|
import {
|
|
15
15
|
createTelegramModelMenuStateBuilder,
|
|
16
16
|
handleTelegramModelMenuCallbackAction,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram outbound
|
|
2
|
+
* Telegram outbound surface helpers
|
|
3
3
|
* Zones: telegram outbound, assistant markup, command templates, callback routing
|
|
4
|
-
* Owns assistant-authored outbound markup extraction, configured artifact generation, callback actions, and Telegram outbound delivery
|
|
4
|
+
* Owns assistant-authored outbound markup extraction, configured artifact generation, callback actions, runtime-event bridge, and Telegram outbound delivery
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
@@ -61,6 +61,18 @@ const DEFAULT_VOICE_TIMEOUT_MS = 120_000;
|
|
|
61
61
|
* diagnostics alongside pi-telegram's own events. Events are silently dropped
|
|
62
62
|
* when pi-telegram is not loaded.
|
|
63
63
|
*/
|
|
64
|
+
export type TelegramRuntimeEventRecorder = (
|
|
65
|
+
category: string,
|
|
66
|
+
error: unknown,
|
|
67
|
+
details?: Record<string, unknown>,
|
|
68
|
+
) => void;
|
|
69
|
+
|
|
70
|
+
export function bindTelegramRuntimeEventRecorder(
|
|
71
|
+
recorder: TelegramRuntimeEventRecorder,
|
|
72
|
+
): void {
|
|
73
|
+
(globalThis as Record<string, unknown>)[VOICE_EVENT_RECORDER_KEY] = recorder;
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
export function recordTelegramRuntimeEvent(
|
|
65
77
|
category: string,
|
|
66
78
|
error: unknown,
|
|
@@ -70,13 +82,7 @@ export function recordTelegramRuntimeEvent(
|
|
|
70
82
|
VOICE_EVENT_RECORDER_KEY
|
|
71
83
|
];
|
|
72
84
|
if (typeof recorder === "function") {
|
|
73
|
-
(
|
|
74
|
-
recorder as (
|
|
75
|
-
category: string,
|
|
76
|
-
error: unknown,
|
|
77
|
-
details?: Record<string, unknown>,
|
|
78
|
-
) => void
|
|
79
|
-
)(category, error, details);
|
|
85
|
+
(recorder as TelegramRuntimeEventRecorder)(category, error, details);
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -1028,7 +1034,9 @@ export function createTelegramVoiceReplySender(
|
|
|
1028
1034
|
});
|
|
1029
1035
|
return;
|
|
1030
1036
|
} catch (error) {
|
|
1031
|
-
deps.recordRuntimeEvent?.("voice", error, {
|
|
1037
|
+
deps.recordRuntimeEvent?.("voice", error, {
|
|
1038
|
+
phase: "template-handler-send",
|
|
1039
|
+
});
|
|
1032
1040
|
}
|
|
1033
1041
|
}
|
|
1034
1042
|
|
|
@@ -1045,7 +1053,9 @@ export function createTelegramVoiceReplySender(
|
|
|
1045
1053
|
});
|
|
1046
1054
|
return;
|
|
1047
1055
|
} catch (error) {
|
|
1048
|
-
deps.recordRuntimeEvent?.("voice", error, {
|
|
1056
|
+
deps.recordRuntimeEvent?.("voice", error, {
|
|
1057
|
+
phase: "programmatic-handler-send",
|
|
1058
|
+
});
|
|
1049
1059
|
}
|
|
1050
1060
|
}
|
|
1051
1061
|
|
package/lib/polling.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram polling domain helpers
|
|
2
|
+
* Telegram polling runtime domain helpers
|
|
3
3
|
* Zones: telegram transport, polling runtime
|
|
4
4
|
* Owns polling request builders, stop conditions, and the long-poll loop runtime for Telegram updates
|
|
5
5
|
*/
|
|
@@ -91,8 +91,9 @@ export function createTelegramPollingActivityReader(
|
|
|
91
91
|
return () => isTelegramPollingControllerActive(state);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
export interface TelegramPollingRuntimeDeps<
|
|
95
|
-
|
|
94
|
+
export interface TelegramPollingRuntimeDeps<
|
|
95
|
+
TContext,
|
|
96
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
96
97
|
hasBotToken: () => boolean;
|
|
97
98
|
getPollingPromise: () => Promise<void> | undefined;
|
|
98
99
|
setPollingPromise: (promise: Promise<void> | undefined) => void;
|
package/lib/preview.ts
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
} from "./rendering.ts";
|
|
23
23
|
|
|
24
24
|
import { buildTelegramReplyParameters } from "./replies.ts";
|
|
25
|
-
import { stripTelegramCommentMarkupForPreview } from "./outbound
|
|
25
|
+
import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
|
|
26
26
|
import { shouldSuppressPreviewForVoice } from "./voice.ts";
|
|
27
27
|
|
|
28
28
|
const TELEGRAM_PREVIEW_THROTTLE_MS = 750;
|
package/lib/routing.ts
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import * as Commands from "./commands.ts";
|
|
9
9
|
import type { TelegramConfigStore } from "./config.ts";
|
|
10
|
-
import type { TelegramSectionRegistry } from "./
|
|
11
|
-
import type { TelegramInboundHandlerRuntime } from "./inbound
|
|
10
|
+
import type { TelegramSectionRegistry } from "./sections.ts";
|
|
11
|
+
import type { TelegramInboundHandlerRuntime } from "./inbound.ts";
|
|
12
12
|
import * as Media from "./media.ts";
|
|
13
13
|
import * as Menu from "./menu.ts";
|
|
14
14
|
import * as Model from "./model.ts";
|
|
15
|
-
import * as OutboundHandlers from "./outbound
|
|
15
|
+
import * as OutboundHandlers from "./outbound.ts";
|
|
16
16
|
import * as PromptTemplates from "./prompt-templates.ts";
|
|
17
17
|
import * as Queue from "./queue.ts";
|
|
18
18
|
import type { TelegramBridgeRuntime } from "./runtime.ts";
|
|
@@ -138,6 +138,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
|
|
141
|
+
"compact:",
|
|
141
142
|
"menu:",
|
|
142
143
|
"model:",
|
|
143
144
|
"queue:",
|
|
@@ -262,6 +263,45 @@ export function createTelegramInboundRouteRuntime<
|
|
|
262
263
|
);
|
|
263
264
|
if (handled) return;
|
|
264
265
|
}
|
|
266
|
+
const handledByCompact =
|
|
267
|
+
await Commands.handleTelegramCompactConfirmationCallback(query, {
|
|
268
|
+
ctx,
|
|
269
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
270
|
+
editInteractiveMessage: deps.editInteractiveMessage ?? (async () => {}),
|
|
271
|
+
runCompact: async (compactCtx, chatId, replyToMessageId) => {
|
|
272
|
+
await Commands.handleTelegramCompactCommand({
|
|
273
|
+
isIdle: () => deps.isIdle(compactCtx),
|
|
274
|
+
hasPendingMessages: () => deps.hasPendingMessages(compactCtx),
|
|
275
|
+
hasActiveTelegramTurn: deps.activeTurnRuntime.has,
|
|
276
|
+
hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
|
|
277
|
+
hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
|
|
278
|
+
isCompactionInProgress:
|
|
279
|
+
deps.bridgeRuntime.lifecycle.isCompactionInProgress,
|
|
280
|
+
setCompactionInProgress:
|
|
281
|
+
deps.bridgeRuntime.lifecycle.setCompactionInProgress,
|
|
282
|
+
updateStatus: () => deps.updateStatus(compactCtx),
|
|
283
|
+
dispatchNextQueuedTelegramTurn: () =>
|
|
284
|
+
deps.dispatchNextQueuedTelegramTurn(compactCtx),
|
|
285
|
+
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
286
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn
|
|
287
|
+
? (dispatch) =>
|
|
288
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn?.(() =>
|
|
289
|
+
dispatch(),
|
|
290
|
+
)
|
|
291
|
+
: undefined,
|
|
292
|
+
compact: (callbacks) => deps.compact(compactCtx, callbacks),
|
|
293
|
+
startTypingLoop: deps.startTypingLoop
|
|
294
|
+
? () => deps.startTypingLoop?.(compactCtx, chatId)
|
|
295
|
+
: undefined,
|
|
296
|
+
stopTypingLoop: deps.stopTypingLoop,
|
|
297
|
+
sendTextReply: (text) =>
|
|
298
|
+
deps.sendTextReply(chatId, replyToMessageId, text).then(() => {}),
|
|
299
|
+
suppressStartNotice: true,
|
|
300
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
if (handledByCompact) return;
|
|
265
305
|
const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
|
|
266
306
|
if (handledByQueue) return;
|
|
267
307
|
const handledBySettings = await deps.settingsMenuCallbackHandler?.(
|
|
@@ -372,6 +412,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
372
412
|
getPromptTemplateCommands,
|
|
373
413
|
persistConfig: deps.configStore.persist,
|
|
374
414
|
sendTextReply: deps.sendTextReply,
|
|
415
|
+
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
375
416
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
376
417
|
});
|
|
377
418
|
const promptEnqueue = Queue.createTelegramPromptEnqueueController<
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram Extension Sections registry and callback routing
|
|
3
3
|
* Zones: telegram ui, extension platform, callback routing
|
|
4
|
-
* Owns section registration, token mapping, main-menu/settings row injection, and section callback dispatch
|
|
4
|
+
* Owns section registration, global registry binding, token mapping, main-menu/settings row injection, and section callback dispatch
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
|
|
8
8
|
|
|
9
9
|
const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
|
|
10
|
+
const TELEGRAM_CALLBACK_DATA_MAX_BYTES = 64;
|
|
10
11
|
|
|
11
12
|
// --- Core Types ---
|
|
12
13
|
|
|
@@ -181,9 +182,7 @@ function buildTelegramSectionContext(
|
|
|
181
182
|
.then(() => {}),
|
|
182
183
|
enqueuePrompt: deps.enqueuePrompt,
|
|
183
184
|
callbackData: (action, payload) =>
|
|
184
|
-
payload
|
|
185
|
-
? `section:${token}:${action}:${payload}`
|
|
186
|
-
: `section:${token}:${action}`,
|
|
185
|
+
buildTelegramSectionCallbackData(token, action, payload),
|
|
187
186
|
deleteMessage: () =>
|
|
188
187
|
messageId !== undefined
|
|
189
188
|
? deps.deleteMessage(chatId, messageId)
|
|
@@ -231,9 +230,7 @@ function buildTelegramSectionCallbackContext(
|
|
|
231
230
|
.then(() => {}),
|
|
232
231
|
enqueuePrompt: deps.enqueuePrompt,
|
|
233
232
|
callbackData: (action, payload) =>
|
|
234
|
-
payload
|
|
235
|
-
? `section:${token}:${action}:${payload}`
|
|
236
|
-
: `section:${token}:${action}`,
|
|
233
|
+
buildTelegramSectionCallbackData(token, action, payload),
|
|
237
234
|
deleteMessage: () =>
|
|
238
235
|
messageId !== undefined
|
|
239
236
|
? deps.deleteMessage(chatId, messageId)
|
|
@@ -250,6 +247,13 @@ export function setGlobalTelegramSectionRegistry(
|
|
|
250
247
|
(globalThis as Record<string, unknown>)[SECTION_REGISTRY_KEY] = registry;
|
|
251
248
|
}
|
|
252
249
|
|
|
250
|
+
/** @internal */
|
|
251
|
+
export function createAndBindTelegramSectionRegistry(): TelegramSectionRegistry {
|
|
252
|
+
const registry = createTelegramExtensionSectionRegistry();
|
|
253
|
+
setGlobalTelegramSectionRegistry(registry);
|
|
254
|
+
return registry;
|
|
255
|
+
}
|
|
256
|
+
|
|
253
257
|
/**
|
|
254
258
|
* Register a Telegram Extension Section from any pi extension.
|
|
255
259
|
* Returns a disposer. Throws if no section registry is active.
|
|
@@ -271,8 +275,8 @@ export function registerTelegramSection(
|
|
|
271
275
|
|
|
272
276
|
/**
|
|
273
277
|
* Get current section diagnostics. Returns empty array when registry is absent.
|
|
278
|
+
* @internal
|
|
274
279
|
*/
|
|
275
|
-
/** @internal */
|
|
276
280
|
export function getTelegramSectionDiagnostics(): TelegramSectionDiagnostic[] {
|
|
277
281
|
const registry = (globalThis as Record<string, unknown>)[
|
|
278
282
|
SECTION_REGISTRY_KEY
|
|
@@ -291,6 +295,27 @@ const BACK_NAV_ROW = {
|
|
|
291
295
|
text: "⬆️ Back",
|
|
292
296
|
} as const;
|
|
293
297
|
|
|
298
|
+
function getUtf8ByteLength(value: string): number {
|
|
299
|
+
return new TextEncoder().encode(value).byteLength;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildTelegramSectionCallbackData(
|
|
303
|
+
token: TelegramSectionToken,
|
|
304
|
+
action: string,
|
|
305
|
+
payload?: string,
|
|
306
|
+
): string {
|
|
307
|
+
const data = payload
|
|
308
|
+
? `section:${token}:${action}:${payload}`
|
|
309
|
+
: `section:${token}:${action}`;
|
|
310
|
+
const byteLength = getUtf8ByteLength(data);
|
|
311
|
+
if (byteLength > TELEGRAM_CALLBACK_DATA_MAX_BYTES) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Telegram section callback_data exceeds ${TELEGRAM_CALLBACK_DATA_MAX_BYTES} bytes (${byteLength}). Use a shorter action/payload or store state behind a compact key.`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return data;
|
|
317
|
+
}
|
|
318
|
+
|
|
294
319
|
function prependBackRow(
|
|
295
320
|
replyMarkup: TelegramInlineKeyboardMarkup | undefined,
|
|
296
321
|
backCallback: string,
|
|
@@ -319,6 +344,10 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
|
|
|
319
344
|
let nextToken = 0;
|
|
320
345
|
|
|
321
346
|
function register(section: TelegramSectionRegistration): () => void {
|
|
347
|
+
const duplicate = [...sections.values()].find((s) => s.id === section.id);
|
|
348
|
+
if (duplicate) {
|
|
349
|
+
throw new Error(`Telegram section id already registered: ${section.id}`);
|
|
350
|
+
}
|
|
322
351
|
const token = String(nextToken++);
|
|
323
352
|
const registered: RegisteredTelegramSection = {
|
|
324
353
|
id: section.id,
|
package/lib/updates.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram updates domain helpers
|
|
3
3
|
* Zones: telegram inbound, authorization, routing plans
|
|
4
|
-
* Owns update extraction, authorization, classification, execution planning,
|
|
4
|
+
* Owns update extraction, authorization, classification, execution planning, runtime execution, and the public update-handler registry
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
@@ -851,3 +851,123 @@ export async function executeTelegramUpdatePlan<
|
|
|
851
851
|
if (!isTelegramStaleContextError(error)) throw error;
|
|
852
852
|
}
|
|
853
853
|
}
|
|
854
|
+
|
|
855
|
+
// --- Public update handler registry ---
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Verdict returned by a public Telegram update handler.
|
|
859
|
+
*
|
|
860
|
+
* - `"consume"` — the handler processed this update; pi-telegram skips default routing.
|
|
861
|
+
* - `"pass"` (or `void`/`undefined`) — pi-telegram routes the update normally.
|
|
862
|
+
*/
|
|
863
|
+
export type TelegramUpdateHandlerVerdict = "consume" | "pass";
|
|
864
|
+
|
|
865
|
+
export type TelegramUpdateHandler = (
|
|
866
|
+
update: unknown,
|
|
867
|
+
) =>
|
|
868
|
+
| TelegramUpdateHandlerVerdict
|
|
869
|
+
| void
|
|
870
|
+
| Promise<TelegramUpdateHandlerVerdict | void>;
|
|
871
|
+
|
|
872
|
+
export interface TelegramUpdateHandlerRegistry {
|
|
873
|
+
/** Schema version of this registry shape. */
|
|
874
|
+
readonly version: 1;
|
|
875
|
+
/**
|
|
876
|
+
* Register an update handler. Returns a disposer that removes it.
|
|
877
|
+
*
|
|
878
|
+
* Handlers are invoked in registration order on every Telegram update,
|
|
879
|
+
* before pi-telegram's own routing. The first handler that returns
|
|
880
|
+
* `"consume"` wins and stops the chain for that update.
|
|
881
|
+
*/
|
|
882
|
+
add: (handler: TelegramUpdateHandler) => () => void;
|
|
883
|
+
/**
|
|
884
|
+
* Run all registered handlers against an update.
|
|
885
|
+
*
|
|
886
|
+
* Used by pi-telegram's polling runtime; companion extensions should call
|
|
887
|
+
* {@link registerTelegramUpdateHandler} or `add` instead of dispatching directly.
|
|
888
|
+
*/
|
|
889
|
+
dispatch: (update: unknown) => Promise<TelegramUpdateHandlerVerdict>;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const UPDATE_HANDLER_REGISTRY_KEY = "__piTelegramUpdateHandlerRegistry__";
|
|
893
|
+
|
|
894
|
+
function isValidV1UpdateHandlerRegistry(
|
|
895
|
+
candidate: unknown,
|
|
896
|
+
): candidate is TelegramUpdateHandlerRegistry {
|
|
897
|
+
if (!candidate || typeof candidate !== "object") return false;
|
|
898
|
+
const r = candidate as Partial<TelegramUpdateHandlerRegistry>;
|
|
899
|
+
return (
|
|
900
|
+
r.version === 1 &&
|
|
901
|
+
typeof r.add === "function" &&
|
|
902
|
+
typeof r.dispatch === "function"
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function getOrCreateUpdateHandlerRegistry(): TelegramUpdateHandlerRegistry {
|
|
907
|
+
const g = globalThis as Record<string, unknown>;
|
|
908
|
+
const existing = g[UPDATE_HANDLER_REGISTRY_KEY];
|
|
909
|
+
if (isValidV1UpdateHandlerRegistry(existing)) return existing;
|
|
910
|
+
const handlers = new Set<TelegramUpdateHandler>();
|
|
911
|
+
const registry: TelegramUpdateHandlerRegistry = {
|
|
912
|
+
version: 1,
|
|
913
|
+
add(handler) {
|
|
914
|
+
handlers.add(handler);
|
|
915
|
+
return () => handlers.delete(handler);
|
|
916
|
+
},
|
|
917
|
+
async dispatch(update) {
|
|
918
|
+
for (const handler of handlers) {
|
|
919
|
+
try {
|
|
920
|
+
const result = await handler(update);
|
|
921
|
+
if (result === "consume") return "consume";
|
|
922
|
+
} catch {
|
|
923
|
+
// Update handler errors must not break polling.
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return "pass";
|
|
927
|
+
},
|
|
928
|
+
};
|
|
929
|
+
g[UPDATE_HANDLER_REGISTRY_KEY] = registry;
|
|
930
|
+
return registry;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/**
|
|
934
|
+
* Called by pi-telegram's own runtime to obtain the registry it dispatches
|
|
935
|
+
* through. Companion extensions should not call this; use
|
|
936
|
+
* {@link registerTelegramUpdateHandler} instead.
|
|
937
|
+
*/
|
|
938
|
+
export function getTelegramUpdateHandlerRegistry(): TelegramUpdateHandlerRegistry {
|
|
939
|
+
return getOrCreateUpdateHandlerRegistry();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export interface TelegramUpdateHandlerWrapDeps<TUpdate, TContext> {
|
|
943
|
+
defaultHandle: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
944
|
+
registry?: TelegramUpdateHandlerRegistry;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Wrap a default polling `handleUpdate` with the public update handler registry.
|
|
949
|
+
*/
|
|
950
|
+
export function createTelegramUpdateHandle<TUpdate, TContext>(
|
|
951
|
+
deps: TelegramUpdateHandlerWrapDeps<TUpdate, TContext>,
|
|
952
|
+
): (update: TUpdate, ctx: TContext) => Promise<void> {
|
|
953
|
+
const registry = deps.registry ?? getOrCreateUpdateHandlerRegistry();
|
|
954
|
+
const { defaultHandle } = deps;
|
|
955
|
+
return async function handleTelegramUpdate(update, ctx) {
|
|
956
|
+
const verdict = await registry.dispatch(update);
|
|
957
|
+
if (verdict === "consume") return;
|
|
958
|
+
await defaultHandle(update, ctx);
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Register a handler that runs before pi-telegram routes a Telegram update
|
|
964
|
+
* through its built-in handlers.
|
|
965
|
+
*
|
|
966
|
+
* This is the low-level public surface for companion extensions that share
|
|
967
|
+
* the same bot and pi process with pi-telegram.
|
|
968
|
+
*/
|
|
969
|
+
export function registerTelegramUpdateHandler(
|
|
970
|
+
handler: TelegramUpdateHandler,
|
|
971
|
+
): () => void {
|
|
972
|
+
return getOrCreateUpdateHandlerRegistry().add(handler);
|
|
973
|
+
}
|
package/lib/voice.ts
CHANGED
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Separation of concerns:
|
|
14
14
|
* - All decision logic and domain rules live here.
|
|
15
|
-
* - Actual delivery (sending the audio via Telegram) stays in outbound
|
|
15
|
+
* - Actual delivery (sending the audio via Telegram) stays in outbound.ts.
|
|
16
16
|
*
|
|
17
17
|
* Keeps voice policy, turn tagging, prompt contributions, and markup helpers
|
|
18
18
|
* out of the queue, preview, turn-building, and delivery domains.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
const VOICE_SYNTHESIS_PROVIDER_REGISTRY_KEY =
|
|
21
|
+
const VOICE_SYNTHESIS_PROVIDER_REGISTRY_KEY =
|
|
22
|
+
"__piTelegramVoiceSynthesisProviders__";
|
|
22
23
|
const VOICE_TRANSCRIPTION_PROVIDER_REGISTRY_KEY =
|
|
23
24
|
"__piTelegramVoiceTranscriptionProviders__";
|
|
24
25
|
|
|
@@ -81,11 +82,20 @@ function getOrCreateVoiceSynthesisProviderRegistry(): Map<
|
|
|
81
82
|
if (existing instanceof Map)
|
|
82
83
|
return existing as Map<string, TelegramVoiceSynthesisProvider>;
|
|
83
84
|
const registry = new Map<string, TelegramVoiceSynthesisProvider>();
|
|
84
|
-
(globalThis as Record<string, unknown>)[
|
|
85
|
-
|
|
85
|
+
(globalThis as Record<string, unknown>)[
|
|
86
|
+
VOICE_SYNTHESIS_PROVIDER_REGISTRY_KEY
|
|
87
|
+
] = registry;
|
|
86
88
|
return registry;
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Register a high-level Telegram voice synthesis provider.
|
|
93
|
+
*
|
|
94
|
+
* Stable public API callers must pass a stable `options.id` so diagnostics,
|
|
95
|
+
* replacement, and cleanup can identify the provider. Omitted ids remain a
|
|
96
|
+
* compatibility path for pre-matrix callers and receive generated session-local
|
|
97
|
+
* ids.
|
|
98
|
+
*/
|
|
89
99
|
export function registerTelegramVoiceSynthesisProvider(
|
|
90
100
|
provider:
|
|
91
101
|
| TelegramVoiceSynthesisProvider
|
|
@@ -103,9 +113,11 @@ export function registerTelegramVoiceSynthesisProvider(
|
|
|
103
113
|
(text: string, options?: { lang?: string; rate?: string }) =>
|
|
104
114
|
provider(text, options),
|
|
105
115
|
{
|
|
106
|
-
getVoicePolicy: (provider as TelegramVoiceSynthesisProvider)
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
getVoicePolicy: (provider as TelegramVoiceSynthesisProvider)
|
|
117
|
+
.getVoicePolicy,
|
|
118
|
+
getVoicePromptContribution: (
|
|
119
|
+
provider as TelegramVoiceSynthesisProvider
|
|
120
|
+
).getVoicePromptContribution,
|
|
109
121
|
},
|
|
110
122
|
) as TelegramVoiceSynthesisProvider)
|
|
111
123
|
: provider;
|
|
@@ -144,6 +156,13 @@ function getOrCreateVoiceTranscriptionProviderRegistry(): Map<
|
|
|
144
156
|
return registry;
|
|
145
157
|
}
|
|
146
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Register a high-level Telegram voice transcription provider.
|
|
161
|
+
*
|
|
162
|
+
* Stable public API callers must pass a stable `options.id`. Omitted ids remain
|
|
163
|
+
* a compatibility path for pre-matrix callers and receive generated
|
|
164
|
+
* session-local ids.
|
|
165
|
+
*/
|
|
147
166
|
export function registerTelegramVoiceTranscriptionProvider(
|
|
148
167
|
provider: TelegramVoiceTranscriptionProvider,
|
|
149
168
|
options?: { id?: string },
|
|
@@ -182,9 +201,9 @@ export const TELEGRAM_VOICE_REPLY_MODES = [
|
|
|
182
201
|
* Pi-telegram owns reply-mode policy through telegram.json. If
|
|
183
202
|
* config.voice.replyMode is missing or invalid, the safe default is manual.
|
|
184
203
|
*/
|
|
185
|
-
export function getTelegramVoiceReplyMode(
|
|
186
|
-
|
|
187
|
-
): TelegramVoiceReplyMode {
|
|
204
|
+
export function getTelegramVoiceReplyMode(config?: {
|
|
205
|
+
voice?: { replyMode?: string };
|
|
206
|
+
}): TelegramVoiceReplyMode {
|
|
188
207
|
const configMode = config?.voice?.replyMode;
|
|
189
208
|
if (
|
|
190
209
|
configMode &&
|
|
@@ -202,9 +221,9 @@ export function getTelegramVoiceReplyMode(
|
|
|
202
221
|
* Reads from `config.voice.sendTranscript`.
|
|
203
222
|
* Default: false (no transcript text sent at all).
|
|
204
223
|
*/
|
|
205
|
-
export function getTelegramVoiceSendTranscript(
|
|
206
|
-
|
|
207
|
-
): boolean {
|
|
224
|
+
export function getTelegramVoiceSendTranscript(config?: {
|
|
225
|
+
voice?: { sendTranscript?: boolean };
|
|
226
|
+
}): boolean {
|
|
208
227
|
return !!config?.voice?.sendTranscript;
|
|
209
228
|
}
|
|
210
229
|
|
|
@@ -292,4 +311,4 @@ export {
|
|
|
292
311
|
stripTelegramCommentMarkupForDelivery,
|
|
293
312
|
stripTelegramVoiceMarkupForPreview,
|
|
294
313
|
normalizeMarkdownAfterVoiceExtraction,
|
|
295
|
-
} from "./outbound
|
|
314
|
+
} from "./outbound.ts";
|
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"description": "Telegram runtime adapter for π",
|
|
9
9
|
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./index.ts",
|
|
12
|
+
"./inbound": "./api/inbound.ts",
|
|
13
|
+
"./outbound": "./api/outbound.ts",
|
|
14
|
+
"./updates": "./api/updates.ts",
|
|
15
|
+
"./sections": "./api/sections.ts",
|
|
16
|
+
"./voice": "./api/voice.ts",
|
|
17
|
+
"./keyboard": "./api/keyboard.ts"
|
|
18
|
+
},
|
|
10
19
|
"keywords": [
|
|
11
20
|
"pi-package",
|
|
12
21
|
"pi",
|
|
@@ -35,6 +44,7 @@
|
|
|
35
44
|
},
|
|
36
45
|
"files": [
|
|
37
46
|
"index.ts",
|
|
47
|
+
"api/",
|
|
38
48
|
"lib/",
|
|
39
49
|
"README.md",
|
|
40
50
|
"AGENTS.md",
|