@llblab/pi-telegram 0.7.2 → 0.8.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 +5 -5
- package/CHANGELOG.md +13 -1
- package/README.md +34 -10
- package/docs/README.md +2 -2
- package/docs/architecture.md +6 -6
- package/docs/inbound-handlers.md +93 -0
- package/docs/outbound-handlers.md +40 -4
- package/index.ts +42 -31
- package/lib/config.ts +9 -3
- package/lib/inbound-handlers.ts +588 -0
- package/lib/{attachments.ts → outbound-attachments.ts} +34 -34
- package/lib/outbound-handlers.ts +245 -0
- package/lib/prompts.ts +1 -1
- package/lib/routing.ts +3 -3
- package/package.json +1 -1
- package/docs/attachment-handlers.md +0 -50
- package/lib/attachment-handlers.ts +0 -423
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram inbound handler pipeline
|
|
3
|
+
* Zones: telegram inbound, command templates, prompt preparation
|
|
4
|
+
* Owns MIME/type matching, command-template execution, fallback handling, and prompt injection before prompt enqueueing
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { basename } from "node:path";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
buildCommandTemplateInvocation,
|
|
12
|
+
expandCommandTemplateConfigs,
|
|
13
|
+
normalizeCommandTemplateConfig,
|
|
14
|
+
type CommandTemplateConfig,
|
|
15
|
+
type CommandTemplateObjectConfig,
|
|
16
|
+
} from "./command-templates.ts";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_INBOUND_HANDLER_TIMEOUT_MS = 120_000;
|
|
19
|
+
|
|
20
|
+
type TelegramInboundCommandTemplateConfig =
|
|
21
|
+
| string
|
|
22
|
+
| CommandTemplateObjectConfig;
|
|
23
|
+
|
|
24
|
+
export interface TelegramInboundHandlerConfig {
|
|
25
|
+
match?: string | string[];
|
|
26
|
+
mime?: string | string[];
|
|
27
|
+
type?: string | string[];
|
|
28
|
+
template?: string | TelegramInboundCommandTemplateConfig[];
|
|
29
|
+
pipe?: TelegramInboundCommandTemplateConfig[];
|
|
30
|
+
args?: string[];
|
|
31
|
+
defaults?: Record<string, unknown>;
|
|
32
|
+
timeout?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TelegramInboundHandlerFile {
|
|
36
|
+
path: string;
|
|
37
|
+
fileName?: string;
|
|
38
|
+
mimeType?: string;
|
|
39
|
+
kind?: string;
|
|
40
|
+
isImage?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TelegramInboundHandlerOutput {
|
|
44
|
+
file: TelegramInboundHandlerFile;
|
|
45
|
+
output: string;
|
|
46
|
+
handler: TelegramInboundHandlerConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TelegramInboundHandlerProcessResult<
|
|
50
|
+
TFile extends TelegramInboundHandlerFile = TelegramInboundHandlerFile,
|
|
51
|
+
> {
|
|
52
|
+
rawText: string;
|
|
53
|
+
promptFiles: TFile[];
|
|
54
|
+
handlerOutputs: string[];
|
|
55
|
+
handledFiles: TelegramInboundHandlerOutput[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface TelegramInboundHandlerExecOptions {
|
|
59
|
+
cwd?: string;
|
|
60
|
+
timeout?: number;
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
stdin?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TelegramInboundHandlerExecResult {
|
|
66
|
+
stdout: string;
|
|
67
|
+
stderr: string;
|
|
68
|
+
code: number;
|
|
69
|
+
killed: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TelegramInboundHandlerRuntimeContext {
|
|
73
|
+
cwd: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface TelegramInboundHandlerRuntimeDeps<TContext> {
|
|
77
|
+
getHandlers: () => TelegramInboundHandlerConfig[] | undefined;
|
|
78
|
+
execCommand: (
|
|
79
|
+
command: string,
|
|
80
|
+
args: string[],
|
|
81
|
+
options?: TelegramInboundHandlerExecOptions,
|
|
82
|
+
) => Promise<TelegramInboundHandlerExecResult>;
|
|
83
|
+
getCwd: (ctx: TContext) => string;
|
|
84
|
+
recordRuntimeEvent?: (
|
|
85
|
+
category: string,
|
|
86
|
+
error: unknown,
|
|
87
|
+
details?: Record<string, unknown>,
|
|
88
|
+
) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TelegramInboundHandlerRuntime<TContext> {
|
|
92
|
+
process: <TFile extends TelegramInboundHandlerFile>(
|
|
93
|
+
files: TFile[],
|
|
94
|
+
rawText: string,
|
|
95
|
+
ctx: TContext,
|
|
96
|
+
) => Promise<TelegramInboundHandlerProcessResult<TFile>>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface InboundHandlerInvocation {
|
|
100
|
+
command: string;
|
|
101
|
+
args: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES = 1_000_000;
|
|
105
|
+
|
|
106
|
+
function normalizeStringList(value: string | string[] | undefined): string[] {
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
return value
|
|
109
|
+
.map(String)
|
|
110
|
+
.map((item) => item.trim())
|
|
111
|
+
.filter(Boolean);
|
|
112
|
+
}
|
|
113
|
+
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function matchesWildcard(pattern: string, value: string | undefined): boolean {
|
|
118
|
+
if (!value) return false;
|
|
119
|
+
const normalizedPattern = pattern.toLowerCase();
|
|
120
|
+
const normalizedValue = value.toLowerCase();
|
|
121
|
+
if (normalizedPattern === "*") return true;
|
|
122
|
+
const escaped = normalizedPattern
|
|
123
|
+
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
124
|
+
.replace(/\\\*/g, ".*");
|
|
125
|
+
return new RegExp(`^${escaped}$`).test(normalizedValue);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handlerHasSelectors(
|
|
129
|
+
handler: TelegramInboundHandlerConfig,
|
|
130
|
+
): boolean {
|
|
131
|
+
return (
|
|
132
|
+
normalizeStringList(handler.match).length > 0 ||
|
|
133
|
+
normalizeStringList(handler.mime).length > 0 ||
|
|
134
|
+
normalizeStringList(handler.type).length > 0
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function matchesAnyPattern(
|
|
139
|
+
patterns: string[],
|
|
140
|
+
value: string | undefined,
|
|
141
|
+
): boolean {
|
|
142
|
+
return patterns.some((pattern) => matchesWildcard(pattern, value));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isTelegramTextMimeType(mimeType: string | undefined): boolean {
|
|
146
|
+
return matchesWildcard("text/*", mimeType);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function telegramInboundHandlerMatchesFile(
|
|
150
|
+
handler: TelegramInboundHandlerConfig,
|
|
151
|
+
file: TelegramInboundHandlerFile,
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!handlerHasSelectors(handler)) return true;
|
|
154
|
+
const matchPatterns = normalizeStringList(handler.match);
|
|
155
|
+
const mimePatterns = normalizeStringList(handler.mime);
|
|
156
|
+
const typePatterns = normalizeStringList(handler.type);
|
|
157
|
+
if (matchesAnyPattern(mimePatterns, file.mimeType)) return true;
|
|
158
|
+
if (matchesAnyPattern(typePatterns, file.kind)) return true;
|
|
159
|
+
if (matchesAnyPattern(matchPatterns, file.mimeType)) return true;
|
|
160
|
+
return matchesAnyPattern(matchPatterns, file.kind);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function findTelegramInboundHandlers(
|
|
164
|
+
handlers: TelegramInboundHandlerConfig[] | undefined,
|
|
165
|
+
file: TelegramInboundHandlerFile,
|
|
166
|
+
): TelegramInboundHandlerConfig[] {
|
|
167
|
+
if (!Array.isArray(handlers)) return [];
|
|
168
|
+
return handlers.filter(
|
|
169
|
+
(handler) =>
|
|
170
|
+
!!handler &&
|
|
171
|
+
typeof handler === "object" &&
|
|
172
|
+
telegramInboundHandlerMatchesFile(handler, file),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function findTelegramInboundHandler(
|
|
177
|
+
handlers: TelegramInboundHandlerConfig[] | undefined,
|
|
178
|
+
file: TelegramInboundHandlerFile,
|
|
179
|
+
): TelegramInboundHandlerConfig | undefined {
|
|
180
|
+
return findTelegramInboundHandlers(handlers, file)[0];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function hasInboundFilePlaceholder(value: string): boolean {
|
|
184
|
+
return /\{file\}/.test(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getTelegramInboundHandlerTemplateValues(
|
|
188
|
+
file: TelegramInboundHandlerFile,
|
|
189
|
+
text = "",
|
|
190
|
+
): Record<string, string> {
|
|
191
|
+
return {
|
|
192
|
+
file: file.path,
|
|
193
|
+
mime: file.mimeType ?? "",
|
|
194
|
+
text,
|
|
195
|
+
type: file.kind ?? "",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildTelegramInboundTemplateInvocation(
|
|
200
|
+
handler: CommandTemplateConfig,
|
|
201
|
+
file: TelegramInboundHandlerFile,
|
|
202
|
+
cwd: string,
|
|
203
|
+
appendFileIfMissing = true,
|
|
204
|
+
): InboundHandlerInvocation {
|
|
205
|
+
const values = getTelegramInboundHandlerTemplateValues(file);
|
|
206
|
+
const templateConfig = normalizeCommandTemplateConfig(handler);
|
|
207
|
+
const hadFilePlaceholder =
|
|
208
|
+
typeof templateConfig.template === "string"
|
|
209
|
+
? hasInboundFilePlaceholder(templateConfig.template)
|
|
210
|
+
: false;
|
|
211
|
+
const invocation = buildCommandTemplateInvocation(handler, values, cwd, {
|
|
212
|
+
emptyMessage: "Inbound handler template is empty",
|
|
213
|
+
missingLabel: "inbound handler template",
|
|
214
|
+
});
|
|
215
|
+
if (appendFileIfMissing && !hadFilePlaceholder)
|
|
216
|
+
invocation.args.push(file.path);
|
|
217
|
+
return invocation;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function buildTelegramInboundHandlerInvocation(
|
|
221
|
+
handler: CommandTemplateConfig,
|
|
222
|
+
file: TelegramInboundHandlerFile,
|
|
223
|
+
cwd: string,
|
|
224
|
+
appendFileIfMissing = true,
|
|
225
|
+
): InboundHandlerInvocation {
|
|
226
|
+
const { template } = normalizeCommandTemplateConfig(handler);
|
|
227
|
+
if (!template) throw new Error("Inbound handler template is required");
|
|
228
|
+
return buildTelegramInboundTemplateInvocation(
|
|
229
|
+
handler,
|
|
230
|
+
file,
|
|
231
|
+
cwd,
|
|
232
|
+
appendFileIfMissing,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function getTelegramInboundHandlerConfiguredTimeout(
|
|
237
|
+
handler: TelegramInboundCommandTemplateConfig,
|
|
238
|
+
): number | undefined {
|
|
239
|
+
const timeout = typeof handler === "string" ? undefined : handler.timeout;
|
|
240
|
+
return typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
|
|
241
|
+
? timeout
|
|
242
|
+
: undefined;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function getTelegramInboundHandlerTimeout(
|
|
246
|
+
handler: TelegramInboundCommandTemplateConfig,
|
|
247
|
+
): number {
|
|
248
|
+
return (
|
|
249
|
+
getTelegramInboundHandlerConfiguredTimeout(handler) ??
|
|
250
|
+
DEFAULT_INBOUND_HANDLER_TIMEOUT_MS
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getRemainingTelegramInboundTimeout(
|
|
255
|
+
timeout: number,
|
|
256
|
+
startedAt: number,
|
|
257
|
+
): number {
|
|
258
|
+
return Math.max(1, timeout - (Date.now() - startedAt));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getTelegramInboundCompositionStepTimeout(
|
|
262
|
+
handler: TelegramInboundHandlerConfig,
|
|
263
|
+
step: TelegramInboundCommandTemplateConfig,
|
|
264
|
+
startedAt: number,
|
|
265
|
+
): number {
|
|
266
|
+
const remaining = getRemainingTelegramInboundTimeout(
|
|
267
|
+
getTelegramInboundHandlerTimeout(handler),
|
|
268
|
+
startedAt,
|
|
269
|
+
);
|
|
270
|
+
const stepTimeout = getTelegramInboundHandlerConfiguredTimeout(step);
|
|
271
|
+
return stepTimeout === undefined
|
|
272
|
+
? remaining
|
|
273
|
+
: Math.min(stepTimeout, remaining);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getTelegramInboundHandlerKind(
|
|
277
|
+
handler: TelegramInboundHandlerConfig,
|
|
278
|
+
): string {
|
|
279
|
+
if (Array.isArray(handler.template) || handler.pipe?.length)
|
|
280
|
+
return "composition";
|
|
281
|
+
if (handler.template) return "template";
|
|
282
|
+
return "unknown";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function formatTelegramInboundHandlerFailure(
|
|
286
|
+
result: TelegramInboundHandlerExecResult,
|
|
287
|
+
): string {
|
|
288
|
+
const parts = [
|
|
289
|
+
`Inbound handler exited with code ${result.code}${result.killed ? " (killed)" : ""}`,
|
|
290
|
+
];
|
|
291
|
+
if (result.stderr.trim()) parts.push(`stderr:\n${result.stderr.trimEnd()}`);
|
|
292
|
+
if (result.stdout.trim()) parts.push(`stdout:\n${result.stdout.trimEnd()}`);
|
|
293
|
+
return parts.join("\n\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function executeTelegramInboundHandlerInvocation(
|
|
297
|
+
handler: TelegramInboundCommandTemplateConfig,
|
|
298
|
+
file: TelegramInboundHandlerFile,
|
|
299
|
+
cwd: string,
|
|
300
|
+
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
|
301
|
+
appendFileIfMissing = true,
|
|
302
|
+
timeout = getTelegramInboundHandlerTimeout(handler),
|
|
303
|
+
stdin?: string,
|
|
304
|
+
): Promise<string> {
|
|
305
|
+
const invocation = buildTelegramInboundHandlerInvocation(
|
|
306
|
+
handler,
|
|
307
|
+
file,
|
|
308
|
+
cwd,
|
|
309
|
+
appendFileIfMissing,
|
|
310
|
+
);
|
|
311
|
+
const result = await deps.execCommand(invocation.command, invocation.args, {
|
|
312
|
+
cwd,
|
|
313
|
+
timeout,
|
|
314
|
+
...(typeof handler === "object" && handler.retry !== undefined
|
|
315
|
+
? { retry: handler.retry }
|
|
316
|
+
: {}),
|
|
317
|
+
...(stdin !== undefined ? { stdin } : {}),
|
|
318
|
+
});
|
|
319
|
+
if (result.code !== 0)
|
|
320
|
+
throw new Error(formatTelegramInboundHandlerFailure(result));
|
|
321
|
+
return result.stdout;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getTelegramInboundHandlerCompositionSteps(
|
|
325
|
+
handler: TelegramInboundHandlerConfig,
|
|
326
|
+
): TelegramInboundCommandTemplateConfig[] {
|
|
327
|
+
if (Array.isArray(handler.template)) {
|
|
328
|
+
return expandCommandTemplateConfigs(
|
|
329
|
+
handler,
|
|
330
|
+
) as TelegramInboundCommandTemplateConfig[];
|
|
331
|
+
}
|
|
332
|
+
if (handler.pipe?.length) {
|
|
333
|
+
return expandCommandTemplateConfigs({
|
|
334
|
+
...handler,
|
|
335
|
+
template: handler.pipe,
|
|
336
|
+
}) as TelegramInboundCommandTemplateConfig[];
|
|
337
|
+
}
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function getTelegramTextHandlerFile(): TelegramInboundHandlerFile {
|
|
342
|
+
return {
|
|
343
|
+
path: "",
|
|
344
|
+
fileName: "message.txt",
|
|
345
|
+
mimeType: "text/plain",
|
|
346
|
+
kind: "text",
|
|
347
|
+
isImage: false,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function findTelegramTextHandlers(
|
|
352
|
+
handlers: TelegramInboundHandlerConfig[] | undefined,
|
|
353
|
+
): TelegramInboundHandlerConfig[] {
|
|
354
|
+
if (!Array.isArray(handlers)) return [];
|
|
355
|
+
const textFile = getTelegramTextHandlerFile();
|
|
356
|
+
return handlers.filter(
|
|
357
|
+
(handler) =>
|
|
358
|
+
!!handler &&
|
|
359
|
+
typeof handler === "object" &&
|
|
360
|
+
handlerHasSelectors(handler) &&
|
|
361
|
+
telegramInboundHandlerMatchesFile(handler, textFile),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function buildTelegramTextHandlerInvocation(
|
|
366
|
+
handler: CommandTemplateConfig,
|
|
367
|
+
text: string,
|
|
368
|
+
cwd: string,
|
|
369
|
+
): InboundHandlerInvocation {
|
|
370
|
+
const values = getTelegramInboundHandlerTemplateValues(
|
|
371
|
+
getTelegramTextHandlerFile(),
|
|
372
|
+
text,
|
|
373
|
+
);
|
|
374
|
+
const { template } = normalizeCommandTemplateConfig(handler);
|
|
375
|
+
if (!template) throw new Error("Text handler template is required");
|
|
376
|
+
return buildCommandTemplateInvocation(handler, values, cwd, {
|
|
377
|
+
emptyMessage: "Text handler template is empty",
|
|
378
|
+
missingLabel: "text handler template",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function executeTelegramTextHandlerInvocation(
|
|
383
|
+
handler: TelegramInboundCommandTemplateConfig,
|
|
384
|
+
text: string,
|
|
385
|
+
cwd: string,
|
|
386
|
+
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
|
387
|
+
timeout = getTelegramInboundHandlerTimeout(handler),
|
|
388
|
+
): Promise<string> {
|
|
389
|
+
const invocation = buildTelegramTextHandlerInvocation(handler, text, cwd);
|
|
390
|
+
const result = await deps.execCommand(invocation.command, invocation.args, {
|
|
391
|
+
cwd,
|
|
392
|
+
timeout,
|
|
393
|
+
stdin: text,
|
|
394
|
+
...(typeof handler === "object" && handler.retry !== undefined
|
|
395
|
+
? { retry: handler.retry }
|
|
396
|
+
: {}),
|
|
397
|
+
});
|
|
398
|
+
if (result.code !== 0)
|
|
399
|
+
throw new Error(formatTelegramInboundHandlerFailure(result));
|
|
400
|
+
return result.stdout;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function executeTelegramTextHandler(
|
|
404
|
+
handler: TelegramInboundHandlerConfig,
|
|
405
|
+
text: string,
|
|
406
|
+
cwd: string,
|
|
407
|
+
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
|
408
|
+
): Promise<string> {
|
|
409
|
+
const steps = getTelegramInboundHandlerCompositionSteps(handler);
|
|
410
|
+
if (steps.length === 0) {
|
|
411
|
+
return (
|
|
412
|
+
await executeTelegramTextHandlerInvocation(handler, text, cwd, deps)
|
|
413
|
+
).trim();
|
|
414
|
+
}
|
|
415
|
+
const startedAt = Date.now();
|
|
416
|
+
let output = text;
|
|
417
|
+
for (const [index, step] of steps.entries()) {
|
|
418
|
+
try {
|
|
419
|
+
output = await executeTelegramTextHandlerInvocation(
|
|
420
|
+
step,
|
|
421
|
+
output,
|
|
422
|
+
cwd,
|
|
423
|
+
deps,
|
|
424
|
+
getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
|
|
425
|
+
);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (typeof step === "object" && step.critical) throw error;
|
|
428
|
+
output = "";
|
|
429
|
+
}
|
|
430
|
+
if (index > 0 && !output) output = text;
|
|
431
|
+
}
|
|
432
|
+
return output.trim();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function processTelegramTextHandlers(options: {
|
|
436
|
+
rawText: string;
|
|
437
|
+
handlers?: TelegramInboundHandlerConfig[];
|
|
438
|
+
cwd: string;
|
|
439
|
+
execCommand: TelegramInboundHandlerRuntimeDeps<unknown>["execCommand"];
|
|
440
|
+
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
|
441
|
+
}): Promise<string> {
|
|
442
|
+
if (!options.rawText) return options.rawText;
|
|
443
|
+
let text = options.rawText;
|
|
444
|
+
for (const handler of findTelegramTextHandlers(options.handlers)) {
|
|
445
|
+
try {
|
|
446
|
+
const output = await executeTelegramTextHandler(
|
|
447
|
+
handler,
|
|
448
|
+
text,
|
|
449
|
+
options.cwd,
|
|
450
|
+
options,
|
|
451
|
+
);
|
|
452
|
+
if (output) text = output;
|
|
453
|
+
} catch (error) {
|
|
454
|
+
options.recordRuntimeEvent?.("inbound-text-handler", error, {
|
|
455
|
+
handler: getTelegramInboundHandlerKind(handler),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return text;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function readBuiltInTelegramTextAttachment(
|
|
463
|
+
file: TelegramInboundHandlerFile,
|
|
464
|
+
): Promise<string | undefined> {
|
|
465
|
+
if (!isTelegramTextMimeType(file.mimeType)) return undefined;
|
|
466
|
+
const content = await readFile(file.path, "utf8");
|
|
467
|
+
const normalized = content.trim();
|
|
468
|
+
if (!normalized || Buffer.byteLength(normalized, "utf8") > BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES) {
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
const name = file.fileName || basename(file.path);
|
|
472
|
+
return `[${name}]\n${normalized}`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function executeTelegramInboundHandler(
|
|
476
|
+
handler: TelegramInboundHandlerConfig,
|
|
477
|
+
file: TelegramInboundHandlerFile,
|
|
478
|
+
cwd: string,
|
|
479
|
+
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
|
480
|
+
): Promise<string> {
|
|
481
|
+
const steps = getTelegramInboundHandlerCompositionSteps(handler);
|
|
482
|
+
if (steps.length === 0) {
|
|
483
|
+
const output = await executeTelegramInboundHandlerInvocation(
|
|
484
|
+
handler,
|
|
485
|
+
file,
|
|
486
|
+
cwd,
|
|
487
|
+
deps,
|
|
488
|
+
);
|
|
489
|
+
return output.trim();
|
|
490
|
+
}
|
|
491
|
+
const startedAt = Date.now();
|
|
492
|
+
let output = "";
|
|
493
|
+
for (const [index, step] of steps.entries()) {
|
|
494
|
+
try {
|
|
495
|
+
output = await executeTelegramInboundHandlerInvocation(
|
|
496
|
+
step,
|
|
497
|
+
file,
|
|
498
|
+
cwd,
|
|
499
|
+
deps,
|
|
500
|
+
false,
|
|
501
|
+
getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
|
|
502
|
+
index === 0 ? undefined : output,
|
|
503
|
+
);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
if (typeof step === "object" && step.critical) throw error;
|
|
506
|
+
output = "";
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return output.trim();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export async function processTelegramInboundHandlers<
|
|
513
|
+
TFile extends TelegramInboundHandlerFile,
|
|
514
|
+
>(options: {
|
|
515
|
+
files: TFile[];
|
|
516
|
+
rawText: string;
|
|
517
|
+
handlers?: TelegramInboundHandlerConfig[];
|
|
518
|
+
cwd: string;
|
|
519
|
+
execCommand: TelegramInboundHandlerRuntimeDeps<unknown>["execCommand"];
|
|
520
|
+
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
|
521
|
+
}): Promise<TelegramInboundHandlerProcessResult<TFile>> {
|
|
522
|
+
const rawText = await processTelegramTextHandlers({
|
|
523
|
+
rawText: options.rawText,
|
|
524
|
+
handlers: options.handlers,
|
|
525
|
+
cwd: options.cwd,
|
|
526
|
+
execCommand: options.execCommand,
|
|
527
|
+
recordRuntimeEvent: options.recordRuntimeEvent,
|
|
528
|
+
});
|
|
529
|
+
const promptFiles: TFile[] = [...options.files];
|
|
530
|
+
const outputs: TelegramInboundHandlerOutput[] = [];
|
|
531
|
+
for (const file of options.files) {
|
|
532
|
+
let hasOutput = false;
|
|
533
|
+
const handlers = findTelegramInboundHandlers(options.handlers, file);
|
|
534
|
+
for (const handler of handlers) {
|
|
535
|
+
try {
|
|
536
|
+
const output = await executeTelegramInboundHandler(
|
|
537
|
+
handler,
|
|
538
|
+
file,
|
|
539
|
+
options.cwd,
|
|
540
|
+
options,
|
|
541
|
+
);
|
|
542
|
+
if (output) {
|
|
543
|
+
outputs.push({ file, output, handler });
|
|
544
|
+
hasOutput = true;
|
|
545
|
+
}
|
|
546
|
+
break;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
options.recordRuntimeEvent?.("inbound-handler", error, {
|
|
549
|
+
fileName: file.fileName || basename(file.path),
|
|
550
|
+
handler: getTelegramInboundHandlerKind(handler),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (!hasOutput) {
|
|
555
|
+
try {
|
|
556
|
+
const output = await readBuiltInTelegramTextAttachment(file);
|
|
557
|
+
if (output) outputs.push({ file, output, handler: { type: "text" } });
|
|
558
|
+
} catch (error) {
|
|
559
|
+
options.recordRuntimeEvent?.("inbound-handler", error, {
|
|
560
|
+
fileName: file.fileName || basename(file.path),
|
|
561
|
+
handler: "built-in-text",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
rawText,
|
|
568
|
+
promptFiles,
|
|
569
|
+
handlerOutputs: outputs.map((output) => output.output),
|
|
570
|
+
handledFiles: outputs,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export function createTelegramInboundHandlerRuntime<TContext>(
|
|
575
|
+
deps: TelegramInboundHandlerRuntimeDeps<TContext>,
|
|
576
|
+
): TelegramInboundHandlerRuntime<TContext> {
|
|
577
|
+
return {
|
|
578
|
+
process: (files, rawText, ctx) =>
|
|
579
|
+
processTelegramInboundHandlers({
|
|
580
|
+
files,
|
|
581
|
+
rawText,
|
|
582
|
+
handlers: deps.getHandlers(),
|
|
583
|
+
cwd: deps.getCwd(ctx),
|
|
584
|
+
execCommand: deps.execCommand,
|
|
585
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
586
|
+
}),
|
|
587
|
+
};
|
|
588
|
+
}
|