@llblab/pi-telegram 0.10.8 → 0.11.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 +11 -7
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +29 -1
- package/README.md +62 -23
- package/docs/README.md +3 -2
- package/docs/architecture.md +7 -6
- package/docs/extension-sections.md +6 -4
- package/docs/inbound-handlers.md +41 -1
- package/docs/outbound-handlers.md +32 -49
- package/docs/voice.md +210 -0
- package/index.ts +87 -1
- package/lib/api.ts +35 -2
- package/lib/config.ts +134 -0
- package/lib/extension-sections.ts +39 -20
- package/lib/external-handlers.ts +3 -4
- package/lib/inbound-handlers.ts +197 -0
- package/lib/media.ts +3 -0
- package/lib/menu-settings.ts +233 -9
- package/lib/menu-status.ts +17 -3
- package/lib/menu-thinking.ts +12 -1
- package/lib/menu.ts +10 -1
- package/lib/outbound-handlers.ts +719 -277
- package/lib/preview.ts +9 -0
- package/lib/prompts.ts +3 -1
- package/lib/queue.ts +84 -5
- package/lib/routing.ts +12 -1
- package/lib/time-injection.ts +78 -0
- package/lib/turns.ts +97 -20
- package/lib/voice.ts +295 -0
- package/package.json +1 -1
package/lib/config.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { join, resolve } from "node:path";
|
|
|
12
12
|
import type { TelegramInboundHandlerConfig } from "./inbound-handlers.ts";
|
|
13
13
|
import type { CommandTemplateObjectConfig } from "./command-templates.ts";
|
|
14
14
|
|
|
15
|
+
const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
|
|
16
|
+
|
|
15
17
|
function getAgentDir(): string {
|
|
16
18
|
return process.env.PI_CODING_AGENT_DIR
|
|
17
19
|
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
@@ -33,6 +35,19 @@ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConf
|
|
|
33
35
|
timeout?: number;
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
export type TelegramTimeMode = "off" | "always" | "interval";
|
|
39
|
+
|
|
40
|
+
export interface TelegramTimeConfig {
|
|
41
|
+
injectionMode?: TelegramTimeMode;
|
|
42
|
+
interval?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolvedTelegramTimeConfig {
|
|
46
|
+
injectionMode: TelegramTimeMode;
|
|
47
|
+
interval: number;
|
|
48
|
+
timezone: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export interface TelegramConfig {
|
|
37
52
|
botToken?: string;
|
|
38
53
|
botUsername?: string;
|
|
@@ -43,6 +58,12 @@ export interface TelegramConfig {
|
|
|
43
58
|
attachmentHandlers?: TelegramInboundHandlerConfig[];
|
|
44
59
|
outboundHandlers?: TelegramOutboundHandlerConfig[];
|
|
45
60
|
proactivePush?: boolean;
|
|
61
|
+
voice?: {
|
|
62
|
+
replyMode?: "manual" | "mirror" | "always";
|
|
63
|
+
/** Whether to attach the provider's transcriptText as caption on voice messages */
|
|
64
|
+
sendTranscript?: boolean;
|
|
65
|
+
};
|
|
66
|
+
time?: TelegramTimeConfig;
|
|
46
67
|
}
|
|
47
68
|
|
|
48
69
|
export interface TelegramConfigStore {
|
|
@@ -66,6 +87,29 @@ export interface TelegramConfigStoreOptions {
|
|
|
66
87
|
configPath?: string;
|
|
67
88
|
}
|
|
68
89
|
|
|
90
|
+
export interface TelegramConfigRuntime {
|
|
91
|
+
updateVoiceConfig: (voice: NonNullable<TelegramConfig["voice"]>) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function setGlobalTelegramConfigRuntime(
|
|
95
|
+
runtime: TelegramConfigRuntime | undefined,
|
|
96
|
+
): void {
|
|
97
|
+
const globals = globalThis as Record<string, unknown>;
|
|
98
|
+
if (runtime) globals[CONFIG_RUNTIME_KEY] = runtime;
|
|
99
|
+
else delete globals[CONFIG_RUNTIME_KEY];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function updateTelegramVoiceConfig(
|
|
103
|
+
voice: NonNullable<TelegramConfig["voice"]>,
|
|
104
|
+
): boolean {
|
|
105
|
+
const runtime = (globalThis as Record<string, unknown>)[
|
|
106
|
+
CONFIG_RUNTIME_KEY
|
|
107
|
+
] as TelegramConfigRuntime | undefined;
|
|
108
|
+
if (!runtime || typeof runtime.updateVoiceConfig !== "function") return false;
|
|
109
|
+
runtime.updateVoiceConfig(voice);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
export async function readTelegramConfig(
|
|
70
114
|
configPath: string,
|
|
71
115
|
): Promise<TelegramConfig> {
|
|
@@ -141,6 +185,96 @@ export function createTelegramProactivePushSetter(
|
|
|
141
185
|
};
|
|
142
186
|
}
|
|
143
187
|
|
|
188
|
+
export function createTelegramVoiceReplyModeGetter(
|
|
189
|
+
configStore: Pick<TelegramConfigStore, "get">,
|
|
190
|
+
): () => "manual" | "mirror" | "always" {
|
|
191
|
+
return () => {
|
|
192
|
+
const mode = configStore.get().voice?.replyMode;
|
|
193
|
+
return mode === "mirror" || mode === "always" || mode === "manual"
|
|
194
|
+
? mode
|
|
195
|
+
: "manual";
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createTelegramVoiceReplyModeConfiguredChecker(
|
|
200
|
+
configStore: Pick<TelegramConfigStore, "get">,
|
|
201
|
+
): () => boolean {
|
|
202
|
+
return () => {
|
|
203
|
+
const mode = configStore.get().voice?.replyMode;
|
|
204
|
+
return mode === "mirror" || mode === "always" || mode === "manual";
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createTelegramVoiceReplyModeSetter(
|
|
209
|
+
configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
|
|
210
|
+
): (replyMode: "manual" | "mirror" | "always" | undefined) => Promise<void> {
|
|
211
|
+
return async (replyMode) => {
|
|
212
|
+
const current = configStore.get();
|
|
213
|
+
if (replyMode === undefined) {
|
|
214
|
+
const { replyMode: _replyMode, ...remainingVoice } = current.voice ?? {};
|
|
215
|
+
const next = { ...current };
|
|
216
|
+
if (Object.keys(remainingVoice).length > 0) next.voice = remainingVoice;
|
|
217
|
+
else delete next.voice;
|
|
218
|
+
configStore.set(next);
|
|
219
|
+
await configStore.persist(next);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const next = { ...current, voice: { ...(current.voice ?? {}), replyMode } };
|
|
223
|
+
configStore.set(next);
|
|
224
|
+
await configStore.persist(next);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function getSystemTimezone(): string {
|
|
229
|
+
try {
|
|
230
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
231
|
+
return tz && tz.length > 0 ? tz : "UTC";
|
|
232
|
+
} catch {
|
|
233
|
+
return "UTC";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function resolveTelegramTimeConfig(
|
|
238
|
+
raw: TelegramTimeConfig | undefined,
|
|
239
|
+
): ResolvedTelegramTimeConfig {
|
|
240
|
+
const injectionMode: TelegramTimeMode =
|
|
241
|
+
raw?.injectionMode === "always" || raw?.injectionMode === "interval"
|
|
242
|
+
? raw.injectionMode
|
|
243
|
+
: "off";
|
|
244
|
+
const interval =
|
|
245
|
+
typeof raw?.interval === "number" && raw.interval > 0
|
|
246
|
+
? raw.interval
|
|
247
|
+
: 60 * 60 * 1000;
|
|
248
|
+
const timezone = getSystemTimezone();
|
|
249
|
+
return { injectionMode, interval, timezone };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function createTelegramTimeConfigGetter(
|
|
253
|
+
configStore: Pick<TelegramConfigStore, "get">,
|
|
254
|
+
): () => ResolvedTelegramTimeConfig {
|
|
255
|
+
return () => resolveTelegramTimeConfig(configStore.get().time);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function createTelegramTimeInjectionModeGetter(
|
|
259
|
+
configStore: Pick<TelegramConfigStore, "get">,
|
|
260
|
+
): () => TelegramTimeMode {
|
|
261
|
+
return () => resolveTelegramTimeConfig(configStore.get().time).injectionMode;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function createTelegramTimeInjectionModeSetter(
|
|
265
|
+
configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
|
|
266
|
+
): (injectionMode: TelegramTimeMode) => Promise<void> {
|
|
267
|
+
return async (injectionMode) => {
|
|
268
|
+
const current = configStore.get();
|
|
269
|
+
const next = {
|
|
270
|
+
...current,
|
|
271
|
+
time: { ...(current.time ?? {}), injectionMode },
|
|
272
|
+
};
|
|
273
|
+
configStore.set(next);
|
|
274
|
+
await configStore.persist(next);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
144
278
|
export function createTelegramProactivePushChatIdGetter(deps: {
|
|
145
279
|
getActiveTurnChatId: () => number | undefined;
|
|
146
280
|
getAllowedUserId: () => number | undefined;
|
|
@@ -6,10 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
|
|
8
8
|
|
|
9
|
+
const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
|
|
10
|
+
|
|
9
11
|
// --- Core Types ---
|
|
10
12
|
|
|
13
|
+
/** @internal */
|
|
11
14
|
export type TelegramSectionId = string;
|
|
15
|
+
|
|
16
|
+
/** @internal */
|
|
12
17
|
export type TelegramSectionToken = string;
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
13
20
|
export type TelegramSectionCallbackResult = "handled" | "pass";
|
|
14
21
|
|
|
15
22
|
export interface TelegramSectionView {
|
|
@@ -34,6 +41,7 @@ export interface TelegramSectionRegistration {
|
|
|
34
41
|
id: TelegramSectionId;
|
|
35
42
|
label: string;
|
|
36
43
|
order?: number;
|
|
44
|
+
getLabel?: () => string;
|
|
37
45
|
render: (
|
|
38
46
|
ctx: TelegramSectionContext,
|
|
39
47
|
) => TelegramSectionView | Promise<TelegramSectionView>;
|
|
@@ -71,6 +79,7 @@ export interface TelegramSectionCallbackContext {
|
|
|
71
79
|
deleteMessage(): Promise<void>;
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
/** @internal */
|
|
74
83
|
export interface RegisteredTelegramSection {
|
|
75
84
|
id: TelegramSectionId;
|
|
76
85
|
token: TelegramSectionToken;
|
|
@@ -79,6 +88,7 @@ export interface RegisteredTelegramSection {
|
|
|
79
88
|
registration: TelegramSectionRegistration;
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
/** @internal */
|
|
82
92
|
export interface TelegramSectionDiagnostic {
|
|
83
93
|
id: TelegramSectionId;
|
|
84
94
|
token: TelegramSectionToken;
|
|
@@ -87,6 +97,7 @@ export interface TelegramSectionDiagnostic {
|
|
|
87
97
|
lastError?: string;
|
|
88
98
|
}
|
|
89
99
|
|
|
100
|
+
/** @internal */
|
|
90
101
|
export interface TelegramSectionRegistry {
|
|
91
102
|
register(section: TelegramSectionRegistration): () => void;
|
|
92
103
|
getSections(): RegisteredTelegramSection[];
|
|
@@ -97,11 +108,13 @@ export interface TelegramSectionRegistry {
|
|
|
97
108
|
clear(): void;
|
|
98
109
|
}
|
|
99
110
|
|
|
111
|
+
/** @internal */
|
|
100
112
|
export interface TelegramSectionMainMenuRow {
|
|
101
113
|
text: string;
|
|
102
114
|
callback_data: string;
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
/** @internal */
|
|
105
118
|
export interface TelegramSectionSettingsRow {
|
|
106
119
|
label: string;
|
|
107
120
|
callback_data: string;
|
|
@@ -109,6 +122,7 @@ export interface TelegramSectionSettingsRow {
|
|
|
109
122
|
|
|
110
123
|
// --- Runtime Port Builders ---
|
|
111
124
|
|
|
125
|
+
/** @internal */
|
|
112
126
|
export interface TelegramSectionRuntimeDeps {
|
|
113
127
|
answerCallbackQuery: (id: string, text?: string) => Promise<void>;
|
|
114
128
|
editInteractiveMessage: (
|
|
@@ -229,17 +243,11 @@ function buildTelegramSectionCallbackContext(
|
|
|
229
243
|
|
|
230
244
|
// --- GlobalThis Bridge ---
|
|
231
245
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
declare global {
|
|
235
|
-
// eslint-disable-next-line no-var
|
|
236
|
-
var __piTelegramSectionRegistry__: TelegramSectionRegistry | undefined;
|
|
237
|
-
}
|
|
238
|
-
|
|
246
|
+
/** @internal */
|
|
239
247
|
export function setGlobalTelegramSectionRegistry(
|
|
240
248
|
registry: TelegramSectionRegistry,
|
|
241
249
|
): void {
|
|
242
|
-
globalThis[
|
|
250
|
+
(globalThis as Record<string, unknown>)[SECTION_REGISTRY_KEY] = registry;
|
|
243
251
|
}
|
|
244
252
|
|
|
245
253
|
/**
|
|
@@ -249,20 +257,26 @@ export function setGlobalTelegramSectionRegistry(
|
|
|
249
257
|
export function registerTelegramSection(
|
|
250
258
|
section: TelegramSectionRegistration,
|
|
251
259
|
): () => void {
|
|
252
|
-
const registry = globalThis[
|
|
260
|
+
const registry = (globalThis as Record<string, unknown>)[
|
|
261
|
+
SECTION_REGISTRY_KEY
|
|
262
|
+
] as TelegramSectionRegistry | undefined;
|
|
253
263
|
if (!registry) {
|
|
254
264
|
throw new Error(
|
|
255
|
-
"Telegram section registry not available. Is pi-telegram loaded?",
|
|
265
|
+
"Telegram section registry not available. Is pi-telegram loaded and initialized?",
|
|
256
266
|
);
|
|
257
267
|
}
|
|
268
|
+
|
|
258
269
|
return registry.register(section);
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
/**
|
|
262
273
|
* Get current section diagnostics. Returns empty array when registry is absent.
|
|
263
274
|
*/
|
|
275
|
+
/** @internal */
|
|
264
276
|
export function getTelegramSectionDiagnostics(): TelegramSectionDiagnostic[] {
|
|
265
|
-
const registry = globalThis[
|
|
277
|
+
const registry = (globalThis as Record<string, unknown>)[
|
|
278
|
+
SECTION_REGISTRY_KEY
|
|
279
|
+
] as TelegramSectionRegistry | undefined;
|
|
266
280
|
return registry ? registry.getDiagnostics() : [];
|
|
267
281
|
}
|
|
268
282
|
|
|
@@ -298,6 +312,7 @@ function prependBackRow(
|
|
|
298
312
|
};
|
|
299
313
|
}
|
|
300
314
|
|
|
315
|
+
/** @internal */
|
|
301
316
|
export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistry {
|
|
302
317
|
const sections = new Map<TelegramSectionToken, RegisteredTelegramSection>();
|
|
303
318
|
const errors = new Map<TelegramSectionToken, string>();
|
|
@@ -351,15 +366,7 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
|
|
|
351
366
|
return { register, getSections, getByToken, getDiagnostics, clear };
|
|
352
367
|
}
|
|
353
368
|
|
|
354
|
-
|
|
355
|
-
registry: TelegramSectionRegistry,
|
|
356
|
-
): TelegramSectionMainMenuRow[] {
|
|
357
|
-
return registry.getSections().map((s) => ({
|
|
358
|
-
text: s.label,
|
|
359
|
-
callback_data: `section:${s.token}:open`,
|
|
360
|
-
}));
|
|
361
|
-
}
|
|
362
|
-
|
|
369
|
+
/** @internal */
|
|
363
370
|
export function getTelegramExtensionSettingsRows(
|
|
364
371
|
registry: TelegramSectionRegistry,
|
|
365
372
|
): TelegramSectionSettingsRow[] {
|
|
@@ -379,6 +386,17 @@ export function getTelegramExtensionSettingsRows(
|
|
|
379
386
|
}));
|
|
380
387
|
}
|
|
381
388
|
|
|
389
|
+
/** @internal */
|
|
390
|
+
export function getTelegramSectionMainMenuRows(
|
|
391
|
+
registry: TelegramSectionRegistry,
|
|
392
|
+
): TelegramSectionMainMenuRow[] {
|
|
393
|
+
return registry.getSections().map((s) => ({
|
|
394
|
+
text: s.registration.getLabel?.() ?? s.label,
|
|
395
|
+
callback_data: `section:${s.token}:open`,
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** @internal */
|
|
382
400
|
export function parseTelegramSectionCallback(
|
|
383
401
|
data: string,
|
|
384
402
|
): { token: string; action: string; payload: string } | undefined {
|
|
@@ -399,6 +417,7 @@ export function parseTelegramSectionCallback(
|
|
|
399
417
|
};
|
|
400
418
|
}
|
|
401
419
|
|
|
420
|
+
/** @internal */
|
|
402
421
|
export interface TelegramSectionCallbackHandlerDeps {
|
|
403
422
|
answerCallbackQuery: (id: string, text?: string) => Promise<void>;
|
|
404
423
|
editInteractiveMessage: (
|
package/lib/external-handlers.ts
CHANGED
|
@@ -155,10 +155,9 @@ export function createTelegramExternalHandleUpdate<TUpdate, TContext>(
|
|
|
155
155
|
* off();
|
|
156
156
|
* ```
|
|
157
157
|
*
|
|
158
|
-
* Extensions that prefer zero coupling can also reach the registry
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* `@llblab/pi-telegram` and tolerates either install order.
|
|
158
|
+
* Extensions that prefer zero coupling can also reach the versioned registry
|
|
159
|
+
* directly on `globalThis`. This avoids importing `@llblab/pi-telegram` and
|
|
160
|
+
* tolerates either install order.
|
|
162
161
|
*/
|
|
163
162
|
export function onTelegramExternalUpdate(
|
|
164
163
|
handler: TelegramExternalHandler,
|
package/lib/inbound-handlers.ts
CHANGED
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
type CommandTemplateConfig,
|
|
15
15
|
type CommandTemplateObjectConfig,
|
|
16
16
|
} from "./command-templates.ts";
|
|
17
|
+
import { getTelegramVoiceTranscriptionProviders } from "./voice.ts";
|
|
17
18
|
|
|
18
19
|
const DEFAULT_INBOUND_HANDLER_TIMEOUT_MS = 120_000;
|
|
20
|
+
const INBOUND_HANDLER_REGISTRY_KEY = "__piTelegramInboundHandlers__";
|
|
19
21
|
|
|
20
22
|
type TelegramInboundCommandTemplateConfig =
|
|
21
23
|
| string
|
|
@@ -96,6 +98,27 @@ export interface TelegramInboundHandlerRuntime<TContext> {
|
|
|
96
98
|
) => Promise<TelegramInboundHandlerProcessResult<TFile>>;
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
export type TelegramInboundProgrammaticHandlerResult =
|
|
102
|
+
| string
|
|
103
|
+
| { text: string }
|
|
104
|
+
| undefined;
|
|
105
|
+
|
|
106
|
+
export interface TelegramInboundProgrammaticHandlerInput {
|
|
107
|
+
kind: string;
|
|
108
|
+
text?: string;
|
|
109
|
+
file?: TelegramInboundHandlerFile;
|
|
110
|
+
mimeType?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type TelegramInboundProgrammaticHandler = (
|
|
114
|
+
input: TelegramInboundProgrammaticHandlerInput,
|
|
115
|
+
options?: { cwd?: string },
|
|
116
|
+
) => Promise<TelegramInboundProgrammaticHandlerResult>;
|
|
117
|
+
|
|
118
|
+
export interface TelegramInboundHandlerRegistry {
|
|
119
|
+
handlers: Map<string, TelegramInboundProgrammaticHandler[]>;
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
interface InboundHandlerInvocation {
|
|
100
123
|
command: string;
|
|
101
124
|
args: string[];
|
|
@@ -103,6 +126,74 @@ interface InboundHandlerInvocation {
|
|
|
103
126
|
|
|
104
127
|
const BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES = 1_000_000;
|
|
105
128
|
|
|
129
|
+
function getOrCreateInboundHandlerRegistry(): TelegramInboundHandlerRegistry {
|
|
130
|
+
const existing = (globalThis as Record<string, unknown>)[
|
|
131
|
+
INBOUND_HANDLER_REGISTRY_KEY
|
|
132
|
+
];
|
|
133
|
+
if (
|
|
134
|
+
existing &&
|
|
135
|
+
typeof existing === "object" &&
|
|
136
|
+
existing !== null &&
|
|
137
|
+
"handlers" in existing &&
|
|
138
|
+
existing.handlers instanceof Map
|
|
139
|
+
) {
|
|
140
|
+
return existing as TelegramInboundHandlerRegistry;
|
|
141
|
+
}
|
|
142
|
+
const registry: TelegramInboundHandlerRegistry = {
|
|
143
|
+
handlers: new Map(),
|
|
144
|
+
};
|
|
145
|
+
(globalThis as Record<string, unknown>)[INBOUND_HANDLER_REGISTRY_KEY] =
|
|
146
|
+
registry;
|
|
147
|
+
return registry;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function registerTelegramInboundHandler(
|
|
151
|
+
kind: string,
|
|
152
|
+
handler: TelegramInboundProgrammaticHandler,
|
|
153
|
+
): () => void {
|
|
154
|
+
const normalizedKind = kind.trim() || "*";
|
|
155
|
+
const registry = getOrCreateInboundHandlerRegistry();
|
|
156
|
+
const list = registry.handlers.get(normalizedKind) ?? [];
|
|
157
|
+
list.push(handler);
|
|
158
|
+
registry.handlers.set(normalizedKind, list);
|
|
159
|
+
return () => {
|
|
160
|
+
const updated = registry.handlers.get(normalizedKind) ?? [];
|
|
161
|
+
const index = updated.indexOf(handler);
|
|
162
|
+
if (index !== -1) {
|
|
163
|
+
updated.splice(index, 1);
|
|
164
|
+
registry.handlers.set(normalizedKind, updated);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getTelegramInboundProgrammaticHandlers(
|
|
170
|
+
kind: string,
|
|
171
|
+
): TelegramInboundProgrammaticHandler[] {
|
|
172
|
+
const registry = getOrCreateInboundHandlerRegistry();
|
|
173
|
+
return [
|
|
174
|
+
...(registry.handlers.get(kind) ?? []),
|
|
175
|
+
...(kind === "*" ? [] : (registry.handlers.get("*") ?? [])),
|
|
176
|
+
];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function hasTelegramInboundHandler(kind?: string): boolean {
|
|
180
|
+
const registry = getOrCreateInboundHandlerRegistry();
|
|
181
|
+
if (kind !== undefined) return (registry.handlers.get(kind)?.length ?? 0) > 0;
|
|
182
|
+
return Array.from(registry.handlers.values()).some((list) => list.length > 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function clearTelegramInboundHandlers(): void {
|
|
186
|
+
getOrCreateInboundHandlerRegistry().handlers.clear();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function normalizeInboundProgrammaticHandlerText(
|
|
190
|
+
result: TelegramInboundProgrammaticHandlerResult,
|
|
191
|
+
): string | undefined {
|
|
192
|
+
const text = typeof result === "string" ? result : result?.text;
|
|
193
|
+
const normalized = text?.trim();
|
|
194
|
+
return normalized || undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
106
197
|
function normalizeStringList(value: string | string[] | undefined): string[] {
|
|
107
198
|
if (Array.isArray(value)) {
|
|
108
199
|
return value
|
|
@@ -467,9 +558,84 @@ async function processTelegramTextHandlers(options: {
|
|
|
467
558
|
});
|
|
468
559
|
}
|
|
469
560
|
}
|
|
561
|
+
for (const handler of getTelegramInboundProgrammaticHandlers("text")) {
|
|
562
|
+
try {
|
|
563
|
+
const output = normalizeInboundProgrammaticHandlerText(
|
|
564
|
+
await handler(
|
|
565
|
+
{ kind: "text", text, mimeType: "text/plain" },
|
|
566
|
+
{ cwd: options.cwd },
|
|
567
|
+
),
|
|
568
|
+
);
|
|
569
|
+
if (output) text = output;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
|
|
572
|
+
kind: "text",
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
470
576
|
return text;
|
|
471
577
|
}
|
|
472
578
|
|
|
579
|
+
function isTelegramVoiceLikeFile(file: TelegramInboundHandlerFile): boolean {
|
|
580
|
+
return (
|
|
581
|
+
file.kind === "voice" ||
|
|
582
|
+
file.kind === "audio" ||
|
|
583
|
+
matchesWildcard("audio/*", file.mimeType)
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function processTelegramFileWithProgrammaticHandlers(
|
|
588
|
+
file: TelegramInboundHandlerFile,
|
|
589
|
+
options: {
|
|
590
|
+
cwd: string;
|
|
591
|
+
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
|
592
|
+
},
|
|
593
|
+
): Promise<string | undefined> {
|
|
594
|
+
const kind = file.kind || "*";
|
|
595
|
+
for (const handler of getTelegramInboundProgrammaticHandlers(kind)) {
|
|
596
|
+
try {
|
|
597
|
+
const output = normalizeInboundProgrammaticHandlerText(
|
|
598
|
+
await handler(
|
|
599
|
+
{
|
|
600
|
+
kind,
|
|
601
|
+
file,
|
|
602
|
+
mimeType: file.mimeType,
|
|
603
|
+
},
|
|
604
|
+
{ cwd: options.cwd },
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
if (output) return output;
|
|
608
|
+
} catch (error) {
|
|
609
|
+
options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
|
|
610
|
+
fileName: file.fileName || basename(file.path),
|
|
611
|
+
kind,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return undefined;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function transcribeTelegramVoiceFileWithProviders(
|
|
619
|
+
file: TelegramInboundHandlerFile,
|
|
620
|
+
options: {
|
|
621
|
+
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
|
622
|
+
},
|
|
623
|
+
): Promise<string | undefined> {
|
|
624
|
+
if (!isTelegramVoiceLikeFile(file)) return undefined;
|
|
625
|
+
for (const provider of getTelegramVoiceTranscriptionProviders()) {
|
|
626
|
+
try {
|
|
627
|
+
const result = await provider(file, {});
|
|
628
|
+
const text = typeof result === "string" ? result : result?.text;
|
|
629
|
+
if (text?.trim()) return text.trim();
|
|
630
|
+
} catch (error) {
|
|
631
|
+
options.recordRuntimeEvent?.("voice-transcription-provider", error, {
|
|
632
|
+
fileName: file.fileName || basename(file.path),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
|
|
473
639
|
async function readBuiltInTelegramTextAttachment(
|
|
474
640
|
file: TelegramInboundHandlerFile,
|
|
475
641
|
): Promise<string | undefined> {
|
|
@@ -564,6 +730,37 @@ export async function processTelegramInboundHandlers<
|
|
|
564
730
|
});
|
|
565
731
|
}
|
|
566
732
|
}
|
|
733
|
+
if (!hasOutput) {
|
|
734
|
+
try {
|
|
735
|
+
const output = await processTelegramFileWithProgrammaticHandlers(file, {
|
|
736
|
+
cwd: options.cwd,
|
|
737
|
+
recordRuntimeEvent: options.recordRuntimeEvent,
|
|
738
|
+
});
|
|
739
|
+
if (output) {
|
|
740
|
+
outputs.push({ file, output, handler: { type: "programmatic" } });
|
|
741
|
+
hasOutput = true;
|
|
742
|
+
}
|
|
743
|
+
} catch (error) {
|
|
744
|
+
options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
|
|
745
|
+
fileName: file.fileName || basename(file.path),
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!hasOutput) {
|
|
750
|
+
try {
|
|
751
|
+
const output = await transcribeTelegramVoiceFileWithProviders(file, {
|
|
752
|
+
recordRuntimeEvent: options.recordRuntimeEvent,
|
|
753
|
+
});
|
|
754
|
+
if (output) {
|
|
755
|
+
outputs.push({ file, output, handler: { type: "voice-provider" } });
|
|
756
|
+
hasOutput = true;
|
|
757
|
+
}
|
|
758
|
+
} catch (error) {
|
|
759
|
+
options.recordRuntimeEvent?.("voice-transcription-provider", error, {
|
|
760
|
+
fileName: file.fileName || basename(file.path),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
567
764
|
if (!hasOutput) {
|
|
568
765
|
try {
|
|
569
766
|
const output = await readBuiltInTelegramTextAttachment(file);
|
package/lib/media.ts
CHANGED
|
@@ -463,6 +463,7 @@ export function collectTelegramFileInfos(
|
|
|
463
463
|
isImage: false,
|
|
464
464
|
});
|
|
465
465
|
}
|
|
466
|
+
// Generic audio files (e.g. MP3 uploads) — can also trigger voice replies in "mirror" mode
|
|
466
467
|
if (message.audio) {
|
|
467
468
|
const fileName =
|
|
468
469
|
message.audio.file_name ||
|
|
@@ -478,6 +479,8 @@ export function collectTelegramFileInfos(
|
|
|
478
479
|
isImage: false,
|
|
479
480
|
});
|
|
480
481
|
}
|
|
482
|
+
|
|
483
|
+
// Voice messages (recorded via microphone) — primary trigger for "mirror" voice reply mode
|
|
481
484
|
if (message.voice) {
|
|
482
485
|
files.push({
|
|
483
486
|
file_id: message.voice.file_id,
|