@llblab/pi-telegram 0.9.9 → 0.10.1
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 +19 -2
- package/BACKLOG.md +0 -2
- package/CHANGELOG.md +24 -1
- package/README.md +8 -3
- package/docs/README.md +1 -1
- package/docs/callback-namespaces.md +1 -1
- package/docs/extension-sections.md +304 -163
- package/index.ts +25 -12
- package/lib/commands.ts +0 -36
- package/lib/extension-sections.ts +627 -0
- package/lib/menu-model.ts +1 -1
- package/lib/menu-settings.ts +68 -33
- package/lib/menu-status.ts +18 -0
- package/lib/menu.ts +130 -1
- package/lib/replies.ts +1 -2
- package/lib/routing.ts +63 -16
- package/package.json +2 -2
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Extension Sections registry and callback routing
|
|
3
|
+
* Zones: telegram ui, extension platform, callback routing
|
|
4
|
+
* Owns section registration, token mapping, main-menu/settings row injection, and section callback dispatch
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
|
|
8
|
+
|
|
9
|
+
// --- Core Types ---
|
|
10
|
+
|
|
11
|
+
export type TelegramSectionId = string;
|
|
12
|
+
export type TelegramSectionToken = string;
|
|
13
|
+
export type TelegramSectionCallbackResult = "handled" | "pass";
|
|
14
|
+
|
|
15
|
+
export interface TelegramSectionView {
|
|
16
|
+
text: string;
|
|
17
|
+
parseMode?: "html" | "plain";
|
|
18
|
+
replyMarkup?: TelegramInlineKeyboardMarkup;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TelegramSectionSettingsRegistration {
|
|
22
|
+
label: string;
|
|
23
|
+
order?: number;
|
|
24
|
+
getLabel?: () => string;
|
|
25
|
+
open: (
|
|
26
|
+
ctx: TelegramSectionContext,
|
|
27
|
+
) => TelegramSectionView | Promise<TelegramSectionView>;
|
|
28
|
+
handleCallback?: (
|
|
29
|
+
ctx: TelegramSectionCallbackContext,
|
|
30
|
+
) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TelegramSectionRegistration {
|
|
34
|
+
id: TelegramSectionId;
|
|
35
|
+
label: string;
|
|
36
|
+
order?: number;
|
|
37
|
+
render: (
|
|
38
|
+
ctx: TelegramSectionContext,
|
|
39
|
+
) => TelegramSectionView | Promise<TelegramSectionView>;
|
|
40
|
+
handleCallback?: (
|
|
41
|
+
ctx: TelegramSectionCallbackContext,
|
|
42
|
+
) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
|
|
43
|
+
settings?: TelegramSectionSettingsRegistration;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TelegramSectionContext {
|
|
47
|
+
sectionId: string;
|
|
48
|
+
chatId: number;
|
|
49
|
+
messageId?: number;
|
|
50
|
+
answerCallback(text?: string): Promise<void>;
|
|
51
|
+
edit(view: TelegramSectionView): Promise<void>;
|
|
52
|
+
open(view: TelegramSectionView): Promise<void>;
|
|
53
|
+
enqueuePrompt(prompt: string): Promise<void>;
|
|
54
|
+
callbackData(action: string, payload?: string): string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TelegramSectionCallbackContext {
|
|
58
|
+
sectionId: string;
|
|
59
|
+
chatId: number;
|
|
60
|
+
messageId?: number;
|
|
61
|
+
action: string;
|
|
62
|
+
payload: string;
|
|
63
|
+
answerCallback(text?: string): Promise<void>;
|
|
64
|
+
edit(view: TelegramSectionView): Promise<void>;
|
|
65
|
+
open(view: TelegramSectionView): Promise<void>;
|
|
66
|
+
enqueuePrompt(prompt: string): Promise<void>;
|
|
67
|
+
callbackData(action: string, payload?: string): string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RegisteredTelegramSection {
|
|
71
|
+
id: TelegramSectionId;
|
|
72
|
+
token: TelegramSectionToken;
|
|
73
|
+
label: string;
|
|
74
|
+
order: number;
|
|
75
|
+
registration: TelegramSectionRegistration;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TelegramSectionDiagnostic {
|
|
79
|
+
id: TelegramSectionId;
|
|
80
|
+
token: TelegramSectionToken;
|
|
81
|
+
label: string;
|
|
82
|
+
status: "active" | "stale" | "error";
|
|
83
|
+
lastError?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface TelegramSectionRegistry {
|
|
87
|
+
register(section: TelegramSectionRegistration): () => void;
|
|
88
|
+
getSections(): RegisteredTelegramSection[];
|
|
89
|
+
getByToken(
|
|
90
|
+
token: TelegramSectionToken,
|
|
91
|
+
): RegisteredTelegramSection | undefined;
|
|
92
|
+
getDiagnostics(): TelegramSectionDiagnostic[];
|
|
93
|
+
clear(): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface TelegramSectionMainMenuRow {
|
|
97
|
+
text: string;
|
|
98
|
+
callback_data: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface TelegramSectionSettingsRow {
|
|
102
|
+
label: string;
|
|
103
|
+
callback_data: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Runtime Port Builders ---
|
|
107
|
+
|
|
108
|
+
export interface TelegramSectionRuntimeDeps {
|
|
109
|
+
answerCallbackQuery: (id: string, text?: string) => Promise<void>;
|
|
110
|
+
editInteractiveMessage: (
|
|
111
|
+
chatId: number,
|
|
112
|
+
messageId: number,
|
|
113
|
+
text: string,
|
|
114
|
+
mode: "html" | "plain",
|
|
115
|
+
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
116
|
+
) => Promise<void>;
|
|
117
|
+
sendInteractiveMessage: (
|
|
118
|
+
chatId: number,
|
|
119
|
+
text: string,
|
|
120
|
+
mode: "html" | "plain",
|
|
121
|
+
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
122
|
+
) => Promise<number | undefined>;
|
|
123
|
+
enqueuePrompt: (prompt: string) => Promise<void>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildTelegramSectionContext(
|
|
127
|
+
sectionId: string,
|
|
128
|
+
token: TelegramSectionToken,
|
|
129
|
+
chatId: number,
|
|
130
|
+
messageId: number | undefined,
|
|
131
|
+
callbackQueryId: string | undefined,
|
|
132
|
+
deps: TelegramSectionRuntimeDeps,
|
|
133
|
+
backCallback = "menu:back",
|
|
134
|
+
backLabel = "⬆️ Main menu",
|
|
135
|
+
): TelegramSectionContext {
|
|
136
|
+
return {
|
|
137
|
+
sectionId,
|
|
138
|
+
chatId,
|
|
139
|
+
messageId,
|
|
140
|
+
answerCallback: (text) =>
|
|
141
|
+
callbackQueryId
|
|
142
|
+
? deps.answerCallbackQuery(callbackQueryId, text)
|
|
143
|
+
: Promise.resolve(),
|
|
144
|
+
edit: (view) =>
|
|
145
|
+
messageId !== undefined
|
|
146
|
+
? deps.editInteractiveMessage(
|
|
147
|
+
chatId,
|
|
148
|
+
messageId,
|
|
149
|
+
view.text,
|
|
150
|
+
view.parseMode ?? "html",
|
|
151
|
+
prependBackRow(view.replyMarkup, backCallback, backLabel),
|
|
152
|
+
)
|
|
153
|
+
: Promise.resolve(),
|
|
154
|
+
open: (view) =>
|
|
155
|
+
deps
|
|
156
|
+
.sendInteractiveMessage(
|
|
157
|
+
chatId,
|
|
158
|
+
view.text,
|
|
159
|
+
view.parseMode ?? "html",
|
|
160
|
+
view.replyMarkup ?? { inline_keyboard: [] },
|
|
161
|
+
)
|
|
162
|
+
.then(() => {}),
|
|
163
|
+
enqueuePrompt: deps.enqueuePrompt,
|
|
164
|
+
callbackData: (action, payload) =>
|
|
165
|
+
payload
|
|
166
|
+
? `section:${token}:${action}:${payload}`
|
|
167
|
+
: `section:${token}:${action}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildTelegramSectionCallbackContext(
|
|
172
|
+
sectionId: string,
|
|
173
|
+
token: TelegramSectionToken,
|
|
174
|
+
chatId: number,
|
|
175
|
+
messageId: number | undefined,
|
|
176
|
+
action: string,
|
|
177
|
+
payload: string,
|
|
178
|
+
callbackQueryId: string,
|
|
179
|
+
deps: TelegramSectionRuntimeDeps,
|
|
180
|
+
backCallback = "menu:back",
|
|
181
|
+
backLabel = "⬆️ Back",
|
|
182
|
+
): TelegramSectionCallbackContext {
|
|
183
|
+
return {
|
|
184
|
+
sectionId,
|
|
185
|
+
chatId,
|
|
186
|
+
messageId,
|
|
187
|
+
action,
|
|
188
|
+
payload,
|
|
189
|
+
answerCallback: (text) => deps.answerCallbackQuery(callbackQueryId, text),
|
|
190
|
+
edit: (view) =>
|
|
191
|
+
messageId !== undefined
|
|
192
|
+
? deps.editInteractiveMessage(
|
|
193
|
+
chatId,
|
|
194
|
+
messageId,
|
|
195
|
+
view.text,
|
|
196
|
+
view.parseMode ?? "html",
|
|
197
|
+
prependBackRow(view.replyMarkup, backCallback, backLabel),
|
|
198
|
+
)
|
|
199
|
+
: Promise.resolve(),
|
|
200
|
+
open: (view) =>
|
|
201
|
+
deps
|
|
202
|
+
.sendInteractiveMessage(
|
|
203
|
+
chatId,
|
|
204
|
+
view.text,
|
|
205
|
+
view.parseMode ?? "html",
|
|
206
|
+
view.replyMarkup ?? { inline_keyboard: [] },
|
|
207
|
+
)
|
|
208
|
+
.then(() => {}),
|
|
209
|
+
enqueuePrompt: deps.enqueuePrompt,
|
|
210
|
+
callbackData: (action, payload) =>
|
|
211
|
+
payload
|
|
212
|
+
? `section:${token}:${action}:${payload}`
|
|
213
|
+
: `section:${token}:${action}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- GlobalThis Bridge ---
|
|
218
|
+
|
|
219
|
+
const GLOBAL_SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__" as const;
|
|
220
|
+
|
|
221
|
+
declare global {
|
|
222
|
+
// eslint-disable-next-line no-var
|
|
223
|
+
var __piTelegramSectionRegistry__: TelegramSectionRegistry | undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function setGlobalTelegramSectionRegistry(
|
|
227
|
+
registry: TelegramSectionRegistry,
|
|
228
|
+
): void {
|
|
229
|
+
globalThis[GLOBAL_SECTION_REGISTRY_KEY] = registry;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Register a Telegram Extension Section from any pi extension.
|
|
234
|
+
* Returns a disposer. Throws if no section registry is active.
|
|
235
|
+
*/
|
|
236
|
+
export function registerTelegramSection(
|
|
237
|
+
section: TelegramSectionRegistration,
|
|
238
|
+
): () => void {
|
|
239
|
+
const registry = globalThis[GLOBAL_SECTION_REGISTRY_KEY];
|
|
240
|
+
if (!registry) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"Telegram section registry not available. Is pi-telegram loaded?",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
return registry.register(section);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get current section diagnostics. Returns empty array when registry is absent.
|
|
250
|
+
*/
|
|
251
|
+
export function getTelegramSectionDiagnostics(): TelegramSectionDiagnostic[] {
|
|
252
|
+
const registry = globalThis[GLOBAL_SECTION_REGISTRY_KEY];
|
|
253
|
+
return registry ? registry.getDiagnostics() : [];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// --- Registry ---
|
|
257
|
+
|
|
258
|
+
const MAIN_MENU_ROW = {
|
|
259
|
+
text: "⬆️ Main menu",
|
|
260
|
+
callback_data: "menu:back",
|
|
261
|
+
} as const;
|
|
262
|
+
|
|
263
|
+
const BACK_NAV_ROW = {
|
|
264
|
+
text: "⬆️ Back",
|
|
265
|
+
} as const;
|
|
266
|
+
|
|
267
|
+
function prependBackRow(
|
|
268
|
+
replyMarkup: TelegramInlineKeyboardMarkup | undefined,
|
|
269
|
+
backCallback: string,
|
|
270
|
+
backLabel?: string,
|
|
271
|
+
): TelegramInlineKeyboardMarkup {
|
|
272
|
+
const backRow = {
|
|
273
|
+
text: backLabel ?? BACK_NAV_ROW.text,
|
|
274
|
+
callback_data: backCallback,
|
|
275
|
+
};
|
|
276
|
+
if (!replyMarkup || replyMarkup.inline_keyboard.length === 0) {
|
|
277
|
+
return { inline_keyboard: [[backRow]] };
|
|
278
|
+
}
|
|
279
|
+
const firstRow = replyMarkup.inline_keyboard[0];
|
|
280
|
+
const firstIsBack =
|
|
281
|
+
firstRow.length === 1 && firstRow[0].callback_data === backCallback;
|
|
282
|
+
if (firstIsBack) return replyMarkup;
|
|
283
|
+
return {
|
|
284
|
+
inline_keyboard: [[backRow], ...replyMarkup.inline_keyboard],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistry {
|
|
289
|
+
const sections = new Map<TelegramSectionToken, RegisteredTelegramSection>();
|
|
290
|
+
const errors = new Map<TelegramSectionToken, string>();
|
|
291
|
+
let nextToken = 0;
|
|
292
|
+
|
|
293
|
+
function register(section: TelegramSectionRegistration): () => void {
|
|
294
|
+
const token = String(nextToken++);
|
|
295
|
+
const registered: RegisteredTelegramSection = {
|
|
296
|
+
id: section.id,
|
|
297
|
+
token,
|
|
298
|
+
label: section.label,
|
|
299
|
+
order: section.order ?? 0,
|
|
300
|
+
registration: section,
|
|
301
|
+
};
|
|
302
|
+
sections.set(token, registered);
|
|
303
|
+
return () => {
|
|
304
|
+
sections.delete(token);
|
|
305
|
+
errors.delete(token);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getSections(): RegisteredTelegramSection[] {
|
|
310
|
+
return [...sections.values()].sort((a, b) => {
|
|
311
|
+
if (a.order !== b.order) return a.order - b.order;
|
|
312
|
+
return a.id.localeCompare(b.id);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function getByToken(
|
|
317
|
+
token: TelegramSectionToken,
|
|
318
|
+
): RegisteredTelegramSection | undefined {
|
|
319
|
+
return sections.get(token);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function getDiagnostics(): TelegramSectionDiagnostic[] {
|
|
323
|
+
return getSections().map((s) => ({
|
|
324
|
+
id: s.id,
|
|
325
|
+
token: s.token,
|
|
326
|
+
label: s.label,
|
|
327
|
+
status: errors.has(s.token) ? "error" : "active",
|
|
328
|
+
lastError: errors.get(s.token),
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function clear(): void {
|
|
333
|
+
sections.clear();
|
|
334
|
+
errors.clear();
|
|
335
|
+
nextToken = 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { register, getSections, getByToken, getDiagnostics, clear };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function getTelegramSectionMainMenuRows(
|
|
342
|
+
registry: TelegramSectionRegistry,
|
|
343
|
+
): TelegramSectionMainMenuRow[] {
|
|
344
|
+
return registry.getSections().map((s) => ({
|
|
345
|
+
text: s.label,
|
|
346
|
+
callback_data: `section:${s.token}:open`,
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function getTelegramExtensionSettingsRows(
|
|
351
|
+
registry: TelegramSectionRegistry,
|
|
352
|
+
): TelegramSectionSettingsRow[] {
|
|
353
|
+
return registry
|
|
354
|
+
.getSections()
|
|
355
|
+
.filter((s) => s.registration.settings)
|
|
356
|
+
.sort((a, b) => {
|
|
357
|
+
const orderA = a.registration.settings!.order ?? 0;
|
|
358
|
+
const orderB = b.registration.settings!.order ?? 0;
|
|
359
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
360
|
+
return a.id.localeCompare(b.id);
|
|
361
|
+
})
|
|
362
|
+
.map((s) => ({
|
|
363
|
+
label:
|
|
364
|
+
s.registration.settings!.getLabel?.() ?? s.registration.settings!.label,
|
|
365
|
+
callback_data: `section:${s.token}:settings:open`,
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function parseTelegramSectionCallback(
|
|
370
|
+
data: string,
|
|
371
|
+
): { token: string; action: string; payload: string } | undefined {
|
|
372
|
+
if (!data.startsWith("section:")) return undefined;
|
|
373
|
+
const rest = data.slice("section:".length);
|
|
374
|
+
const firstColon = rest.indexOf(":");
|
|
375
|
+
if (firstColon === -1) return undefined;
|
|
376
|
+
const token = rest.slice(0, firstColon);
|
|
377
|
+
const afterToken = rest.slice(firstColon + 1);
|
|
378
|
+
const secondColon = afterToken.indexOf(":");
|
|
379
|
+
if (secondColon === -1) {
|
|
380
|
+
return { token, action: afterToken, payload: "" };
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
token,
|
|
384
|
+
action: afterToken.slice(0, secondColon),
|
|
385
|
+
payload: afterToken.slice(secondColon + 1),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface TelegramSectionCallbackHandlerDeps {
|
|
390
|
+
answerCallbackQuery: (id: string, text?: string) => Promise<void>;
|
|
391
|
+
editInteractiveMessage: (
|
|
392
|
+
chatId: number,
|
|
393
|
+
messageId: number,
|
|
394
|
+
text: string,
|
|
395
|
+
mode: "html" | "plain",
|
|
396
|
+
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
397
|
+
) => Promise<void>;
|
|
398
|
+
sendInteractiveMessage: (
|
|
399
|
+
chatId: number,
|
|
400
|
+
text: string,
|
|
401
|
+
mode: "html" | "plain",
|
|
402
|
+
replyMarkup: TelegramInlineKeyboardMarkup,
|
|
403
|
+
) => Promise<number | undefined>;
|
|
404
|
+
enqueuePrompt: (prompt: string) => Promise<void>;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export async function handleTelegramSectionOpen(
|
|
408
|
+
registry: TelegramSectionRegistry,
|
|
409
|
+
token: TelegramSectionToken,
|
|
410
|
+
chatId: number,
|
|
411
|
+
messageId: number,
|
|
412
|
+
callbackQueryId: string,
|
|
413
|
+
deps: TelegramSectionCallbackHandlerDeps,
|
|
414
|
+
): Promise<boolean> {
|
|
415
|
+
const section = registry.getByToken(token);
|
|
416
|
+
if (!section) {
|
|
417
|
+
await deps.answerCallbackQuery(
|
|
418
|
+
callbackQueryId,
|
|
419
|
+
"This section is no longer available.",
|
|
420
|
+
);
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
const sectionCtx = buildTelegramSectionContext(
|
|
425
|
+
section.id,
|
|
426
|
+
token,
|
|
427
|
+
chatId,
|
|
428
|
+
messageId,
|
|
429
|
+
callbackQueryId,
|
|
430
|
+
deps,
|
|
431
|
+
);
|
|
432
|
+
const view = await section.registration.render(sectionCtx);
|
|
433
|
+
const viewWithBack = {
|
|
434
|
+
...view,
|
|
435
|
+
replyMarkup: prependBackRow(
|
|
436
|
+
view.replyMarkup,
|
|
437
|
+
"menu:back",
|
|
438
|
+
"⬆️ Main menu",
|
|
439
|
+
),
|
|
440
|
+
};
|
|
441
|
+
await deps.editInteractiveMessage(
|
|
442
|
+
chatId,
|
|
443
|
+
messageId,
|
|
444
|
+
viewWithBack.text,
|
|
445
|
+
viewWithBack.parseMode ?? "html",
|
|
446
|
+
viewWithBack.replyMarkup,
|
|
447
|
+
);
|
|
448
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
449
|
+
return true;
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
452
|
+
await deps.answerCallbackQuery(
|
|
453
|
+
callbackQueryId,
|
|
454
|
+
`Section error: ${message}`,
|
|
455
|
+
);
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export async function handleTelegramSectionCallback(
|
|
461
|
+
registry: TelegramSectionRegistry,
|
|
462
|
+
token: TelegramSectionToken,
|
|
463
|
+
action: string,
|
|
464
|
+
payload: string,
|
|
465
|
+
chatId: number,
|
|
466
|
+
messageId: number,
|
|
467
|
+
callbackQueryId: string,
|
|
468
|
+
deps: TelegramSectionCallbackHandlerDeps,
|
|
469
|
+
): Promise<boolean> {
|
|
470
|
+
const section = registry.getByToken(token);
|
|
471
|
+
if (!section) {
|
|
472
|
+
await deps.answerCallbackQuery(
|
|
473
|
+
callbackQueryId,
|
|
474
|
+
"This section is no longer available.",
|
|
475
|
+
);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
// Try main handleCallback first, then settings handleCallback as fallback
|
|
479
|
+
const handler =
|
|
480
|
+
section.registration.handleCallback ??
|
|
481
|
+
section.registration.settings?.handleCallback;
|
|
482
|
+
if (!handler) {
|
|
483
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const ctx = buildTelegramSectionCallbackContext(
|
|
488
|
+
section.id,
|
|
489
|
+
token,
|
|
490
|
+
chatId,
|
|
491
|
+
messageId,
|
|
492
|
+
action,
|
|
493
|
+
payload,
|
|
494
|
+
callbackQueryId,
|
|
495
|
+
deps,
|
|
496
|
+
`section:${token}:open`,
|
|
497
|
+
);
|
|
498
|
+
let result = await handler(ctx);
|
|
499
|
+
// Fallback: if main handler passed and settings handler exists, try settings
|
|
500
|
+
// with the correct navigation context (back → settings list)
|
|
501
|
+
if (
|
|
502
|
+
result === "pass" &&
|
|
503
|
+
handler !== section.registration.settings?.handleCallback &&
|
|
504
|
+
section.registration.settings?.handleCallback
|
|
505
|
+
) {
|
|
506
|
+
const settingsCtx = buildTelegramSectionCallbackContext(
|
|
507
|
+
section.id,
|
|
508
|
+
token,
|
|
509
|
+
chatId,
|
|
510
|
+
messageId,
|
|
511
|
+
action,
|
|
512
|
+
payload,
|
|
513
|
+
callbackQueryId,
|
|
514
|
+
deps,
|
|
515
|
+
"settings:list",
|
|
516
|
+
);
|
|
517
|
+
result = await section.registration.settings.handleCallback(settingsCtx);
|
|
518
|
+
}
|
|
519
|
+
if (result === "pass") {
|
|
520
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
521
|
+
}
|
|
522
|
+
return true;
|
|
523
|
+
} catch (error) {
|
|
524
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
525
|
+
await deps.answerCallbackQuery(
|
|
526
|
+
callbackQueryId,
|
|
527
|
+
`Section error: ${message}`,
|
|
528
|
+
);
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export async function handleTelegramSectionSettingsOpen(
|
|
534
|
+
registry: TelegramSectionRegistry,
|
|
535
|
+
token: TelegramSectionToken,
|
|
536
|
+
chatId: number,
|
|
537
|
+
messageId: number,
|
|
538
|
+
callbackQueryId: string,
|
|
539
|
+
deps: TelegramSectionCallbackHandlerDeps,
|
|
540
|
+
): Promise<boolean> {
|
|
541
|
+
const section = registry.getByToken(token);
|
|
542
|
+
if (!section || !section.registration.settings) {
|
|
543
|
+
await deps.answerCallbackQuery(
|
|
544
|
+
callbackQueryId,
|
|
545
|
+
"This section is no longer available.",
|
|
546
|
+
);
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const sectionCtx = buildTelegramSectionContext(
|
|
551
|
+
section.id,
|
|
552
|
+
token,
|
|
553
|
+
chatId,
|
|
554
|
+
messageId,
|
|
555
|
+
callbackQueryId,
|
|
556
|
+
deps,
|
|
557
|
+
"settings:list",
|
|
558
|
+
"⬆️ Back",
|
|
559
|
+
);
|
|
560
|
+
const view = await section.registration.settings.open(sectionCtx);
|
|
561
|
+
const viewWithBack = {
|
|
562
|
+
...view,
|
|
563
|
+
replyMarkup: prependBackRow(view.replyMarkup, "settings:list", "⬆️ Back"),
|
|
564
|
+
};
|
|
565
|
+
await deps.editInteractiveMessage(
|
|
566
|
+
chatId,
|
|
567
|
+
messageId,
|
|
568
|
+
viewWithBack.text,
|
|
569
|
+
viewWithBack.parseMode ?? "html",
|
|
570
|
+
viewWithBack.replyMarkup,
|
|
571
|
+
);
|
|
572
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
573
|
+
return true;
|
|
574
|
+
} catch (error) {
|
|
575
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
576
|
+
await deps.answerCallbackQuery(
|
|
577
|
+
callbackQueryId,
|
|
578
|
+
`Section error: ${message}`,
|
|
579
|
+
);
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export async function handleTelegramSectionSettingsCallback(
|
|
585
|
+
registry: TelegramSectionRegistry,
|
|
586
|
+
token: TelegramSectionToken,
|
|
587
|
+
action: string,
|
|
588
|
+
payload: string,
|
|
589
|
+
chatId: number,
|
|
590
|
+
messageId: number,
|
|
591
|
+
callbackQueryId: string,
|
|
592
|
+
deps: TelegramSectionCallbackHandlerDeps,
|
|
593
|
+
): Promise<boolean> {
|
|
594
|
+
const section = registry.getByToken(token);
|
|
595
|
+
if (!section || !section.registration.settings?.handleCallback) {
|
|
596
|
+
await deps.answerCallbackQuery(
|
|
597
|
+
callbackQueryId,
|
|
598
|
+
"This section is no longer available.",
|
|
599
|
+
);
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const ctx = buildTelegramSectionCallbackContext(
|
|
604
|
+
section.id,
|
|
605
|
+
token,
|
|
606
|
+
chatId,
|
|
607
|
+
messageId,
|
|
608
|
+
action,
|
|
609
|
+
payload,
|
|
610
|
+
callbackQueryId,
|
|
611
|
+
deps,
|
|
612
|
+
`settings:list`,
|
|
613
|
+
);
|
|
614
|
+
const result = await section.registration.settings.handleCallback(ctx);
|
|
615
|
+
if (result === "pass") {
|
|
616
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
617
|
+
}
|
|
618
|
+
return true;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
621
|
+
await deps.answerCallbackQuery(
|
|
622
|
+
callbackQueryId,
|
|
623
|
+
`Section error: ${message}`,
|
|
624
|
+
);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
}
|
package/lib/menu-model.ts
CHANGED
|
@@ -345,7 +345,7 @@ export function formatScopedModelButtonText<
|
|
|
345
345
|
entry: ScopedTelegramModel<TModel>,
|
|
346
346
|
currentModel: TModel | undefined,
|
|
347
347
|
): string {
|
|
348
|
-
let label = `${modelsMatch(entry.model, currentModel) ? "🟢 " : ""}${entry.model.
|
|
348
|
+
let label = `${modelsMatch(entry.model, currentModel) ? "🟢 " : ""}${entry.model.provider}/${entry.model.id}`;
|
|
349
349
|
if (entry.thinkingLevel) {
|
|
350
350
|
label += ` · ${entry.thinkingLevel}`;
|
|
351
351
|
}
|