@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
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telegram inbound attachment 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 { basename } from "node:path";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
buildCommandTemplateInvocation,
|
|
11
|
-
expandCommandTemplateConfigs,
|
|
12
|
-
normalizeCommandTemplateConfig,
|
|
13
|
-
type CommandTemplateConfig,
|
|
14
|
-
type CommandTemplateObjectConfig,
|
|
15
|
-
} from "./command-templates.ts";
|
|
16
|
-
|
|
17
|
-
const DEFAULT_ATTACHMENT_HANDLER_TIMEOUT_MS = 120_000;
|
|
18
|
-
|
|
19
|
-
type TelegramAttachmentCommandTemplateConfig =
|
|
20
|
-
| string
|
|
21
|
-
| CommandTemplateObjectConfig;
|
|
22
|
-
|
|
23
|
-
export interface TelegramAttachmentHandlerConfig {
|
|
24
|
-
match?: string | string[];
|
|
25
|
-
mime?: string | string[];
|
|
26
|
-
type?: string | string[];
|
|
27
|
-
template?: string | TelegramAttachmentCommandTemplateConfig[];
|
|
28
|
-
pipe?: TelegramAttachmentCommandTemplateConfig[];
|
|
29
|
-
args?: string[];
|
|
30
|
-
defaults?: Record<string, unknown>;
|
|
31
|
-
timeout?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface TelegramAttachmentHandlerFile {
|
|
35
|
-
path: string;
|
|
36
|
-
fileName?: string;
|
|
37
|
-
mimeType?: string;
|
|
38
|
-
kind?: string;
|
|
39
|
-
isImage?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface TelegramAttachmentHandlerOutput {
|
|
43
|
-
file: TelegramAttachmentHandlerFile;
|
|
44
|
-
output: string;
|
|
45
|
-
handler: TelegramAttachmentHandlerConfig;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface TelegramAttachmentHandlerProcessResult<
|
|
49
|
-
TFile extends TelegramAttachmentHandlerFile = TelegramAttachmentHandlerFile,
|
|
50
|
-
> {
|
|
51
|
-
rawText: string;
|
|
52
|
-
promptFiles: TFile[];
|
|
53
|
-
handlerOutputs: string[];
|
|
54
|
-
handledFiles: TelegramAttachmentHandlerOutput[];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface TelegramAttachmentHandlerExecOptions {
|
|
58
|
-
cwd?: string;
|
|
59
|
-
timeout?: number;
|
|
60
|
-
signal?: AbortSignal;
|
|
61
|
-
stdin?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface TelegramAttachmentHandlerExecResult {
|
|
65
|
-
stdout: string;
|
|
66
|
-
stderr: string;
|
|
67
|
-
code: number;
|
|
68
|
-
killed: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface TelegramAttachmentHandlerRuntimeContext {
|
|
72
|
-
cwd: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface TelegramAttachmentHandlerRuntimeDeps<TContext> {
|
|
76
|
-
getHandlers: () => TelegramAttachmentHandlerConfig[] | undefined;
|
|
77
|
-
execCommand: (
|
|
78
|
-
command: string,
|
|
79
|
-
args: string[],
|
|
80
|
-
options?: TelegramAttachmentHandlerExecOptions,
|
|
81
|
-
) => Promise<TelegramAttachmentHandlerExecResult>;
|
|
82
|
-
getCwd: (ctx: TContext) => string;
|
|
83
|
-
recordRuntimeEvent?: (
|
|
84
|
-
category: string,
|
|
85
|
-
error: unknown,
|
|
86
|
-
details?: Record<string, unknown>,
|
|
87
|
-
) => void;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export interface TelegramAttachmentHandlerRuntime<TContext> {
|
|
91
|
-
process: <TFile extends TelegramAttachmentHandlerFile>(
|
|
92
|
-
files: TFile[],
|
|
93
|
-
rawText: string,
|
|
94
|
-
ctx: TContext,
|
|
95
|
-
) => Promise<TelegramAttachmentHandlerProcessResult<TFile>>;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface AttachmentHandlerInvocation {
|
|
99
|
-
command: string;
|
|
100
|
-
args: string[];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function normalizeStringList(value: string | string[] | undefined): string[] {
|
|
104
|
-
if (Array.isArray(value)) {
|
|
105
|
-
return value
|
|
106
|
-
.map(String)
|
|
107
|
-
.map((item) => item.trim())
|
|
108
|
-
.filter(Boolean);
|
|
109
|
-
}
|
|
110
|
-
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function matchesWildcard(pattern: string, value: string | undefined): boolean {
|
|
115
|
-
if (!value) return false;
|
|
116
|
-
const normalizedPattern = pattern.toLowerCase();
|
|
117
|
-
const normalizedValue = value.toLowerCase();
|
|
118
|
-
if (normalizedPattern === "*") return true;
|
|
119
|
-
const escaped = normalizedPattern
|
|
120
|
-
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
121
|
-
.replace(/\\\*/g, ".*");
|
|
122
|
-
return new RegExp(`^${escaped}$`).test(normalizedValue);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function handlerHasSelectors(
|
|
126
|
-
handler: TelegramAttachmentHandlerConfig,
|
|
127
|
-
): boolean {
|
|
128
|
-
return (
|
|
129
|
-
normalizeStringList(handler.match).length > 0 ||
|
|
130
|
-
normalizeStringList(handler.mime).length > 0 ||
|
|
131
|
-
normalizeStringList(handler.type).length > 0
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function matchesAnyPattern(
|
|
136
|
-
patterns: string[],
|
|
137
|
-
value: string | undefined,
|
|
138
|
-
): boolean {
|
|
139
|
-
return patterns.some((pattern) => matchesWildcard(pattern, value));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function telegramAttachmentHandlerMatchesFile(
|
|
143
|
-
handler: TelegramAttachmentHandlerConfig,
|
|
144
|
-
file: TelegramAttachmentHandlerFile,
|
|
145
|
-
): boolean {
|
|
146
|
-
if (!handlerHasSelectors(handler)) return true;
|
|
147
|
-
const matchPatterns = normalizeStringList(handler.match);
|
|
148
|
-
const mimePatterns = normalizeStringList(handler.mime);
|
|
149
|
-
const typePatterns = normalizeStringList(handler.type);
|
|
150
|
-
if (matchesAnyPattern(mimePatterns, file.mimeType)) return true;
|
|
151
|
-
if (matchesAnyPattern(typePatterns, file.kind)) return true;
|
|
152
|
-
if (matchesAnyPattern(matchPatterns, file.mimeType)) return true;
|
|
153
|
-
return matchesAnyPattern(matchPatterns, file.kind);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function findTelegramAttachmentHandlers(
|
|
157
|
-
handlers: TelegramAttachmentHandlerConfig[] | undefined,
|
|
158
|
-
file: TelegramAttachmentHandlerFile,
|
|
159
|
-
): TelegramAttachmentHandlerConfig[] {
|
|
160
|
-
if (!Array.isArray(handlers)) return [];
|
|
161
|
-
return handlers.filter(
|
|
162
|
-
(handler) =>
|
|
163
|
-
!!handler &&
|
|
164
|
-
typeof handler === "object" &&
|
|
165
|
-
telegramAttachmentHandlerMatchesFile(handler, file),
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function findTelegramAttachmentHandler(
|
|
170
|
-
handlers: TelegramAttachmentHandlerConfig[] | undefined,
|
|
171
|
-
file: TelegramAttachmentHandlerFile,
|
|
172
|
-
): TelegramAttachmentHandlerConfig | undefined {
|
|
173
|
-
return findTelegramAttachmentHandlers(handlers, file)[0];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function hasAttachmentFilePlaceholder(value: string): boolean {
|
|
177
|
-
return /\{file\}/.test(value);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function getTelegramAttachmentHandlerTemplateValues(
|
|
181
|
-
file: TelegramAttachmentHandlerFile,
|
|
182
|
-
): Record<string, string> {
|
|
183
|
-
return {
|
|
184
|
-
file: file.path,
|
|
185
|
-
mime: file.mimeType ?? "",
|
|
186
|
-
type: file.kind ?? "",
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function buildTelegramAttachmentTemplateInvocation(
|
|
191
|
-
handler: CommandTemplateConfig,
|
|
192
|
-
file: TelegramAttachmentHandlerFile,
|
|
193
|
-
cwd: string,
|
|
194
|
-
appendFileIfMissing = true,
|
|
195
|
-
): AttachmentHandlerInvocation {
|
|
196
|
-
const values = getTelegramAttachmentHandlerTemplateValues(file);
|
|
197
|
-
const templateConfig = normalizeCommandTemplateConfig(handler);
|
|
198
|
-
const hadFilePlaceholder =
|
|
199
|
-
typeof templateConfig.template === "string"
|
|
200
|
-
? hasAttachmentFilePlaceholder(templateConfig.template)
|
|
201
|
-
: false;
|
|
202
|
-
const invocation = buildCommandTemplateInvocation(handler, values, cwd, {
|
|
203
|
-
emptyMessage: "Attachment handler template is empty",
|
|
204
|
-
missingLabel: "attachment handler template",
|
|
205
|
-
});
|
|
206
|
-
if (appendFileIfMissing && !hadFilePlaceholder)
|
|
207
|
-
invocation.args.push(file.path);
|
|
208
|
-
return invocation;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function buildTelegramAttachmentHandlerInvocation(
|
|
212
|
-
handler: CommandTemplateConfig,
|
|
213
|
-
file: TelegramAttachmentHandlerFile,
|
|
214
|
-
cwd: string,
|
|
215
|
-
appendFileIfMissing = true,
|
|
216
|
-
): AttachmentHandlerInvocation {
|
|
217
|
-
const { template } = normalizeCommandTemplateConfig(handler);
|
|
218
|
-
if (!template) throw new Error("Attachment handler template is required");
|
|
219
|
-
return buildTelegramAttachmentTemplateInvocation(
|
|
220
|
-
handler,
|
|
221
|
-
file,
|
|
222
|
-
cwd,
|
|
223
|
-
appendFileIfMissing,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function getTelegramAttachmentHandlerConfiguredTimeout(
|
|
228
|
-
handler: TelegramAttachmentCommandTemplateConfig,
|
|
229
|
-
): number | undefined {
|
|
230
|
-
const timeout = typeof handler === "string" ? undefined : handler.timeout;
|
|
231
|
-
return typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
|
|
232
|
-
? timeout
|
|
233
|
-
: undefined;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function getTelegramAttachmentHandlerTimeout(
|
|
237
|
-
handler: TelegramAttachmentCommandTemplateConfig,
|
|
238
|
-
): number {
|
|
239
|
-
return (
|
|
240
|
-
getTelegramAttachmentHandlerConfiguredTimeout(handler) ??
|
|
241
|
-
DEFAULT_ATTACHMENT_HANDLER_TIMEOUT_MS
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function getRemainingTelegramAttachmentTimeout(
|
|
246
|
-
timeout: number,
|
|
247
|
-
startedAt: number,
|
|
248
|
-
): number {
|
|
249
|
-
return Math.max(1, timeout - (Date.now() - startedAt));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function getTelegramAttachmentCompositionStepTimeout(
|
|
253
|
-
handler: TelegramAttachmentHandlerConfig,
|
|
254
|
-
step: TelegramAttachmentCommandTemplateConfig,
|
|
255
|
-
startedAt: number,
|
|
256
|
-
): number {
|
|
257
|
-
const remaining = getRemainingTelegramAttachmentTimeout(
|
|
258
|
-
getTelegramAttachmentHandlerTimeout(handler),
|
|
259
|
-
startedAt,
|
|
260
|
-
);
|
|
261
|
-
const stepTimeout = getTelegramAttachmentHandlerConfiguredTimeout(step);
|
|
262
|
-
return stepTimeout === undefined
|
|
263
|
-
? remaining
|
|
264
|
-
: Math.min(stepTimeout, remaining);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function getTelegramAttachmentHandlerKind(
|
|
268
|
-
handler: TelegramAttachmentHandlerConfig,
|
|
269
|
-
): string {
|
|
270
|
-
if (Array.isArray(handler.template) || handler.pipe?.length)
|
|
271
|
-
return "composition";
|
|
272
|
-
if (handler.template) return "template";
|
|
273
|
-
return "unknown";
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function formatTelegramAttachmentHandlerFailure(
|
|
277
|
-
result: TelegramAttachmentHandlerExecResult,
|
|
278
|
-
): string {
|
|
279
|
-
const parts = [
|
|
280
|
-
`Attachment handler exited with code ${result.code}${result.killed ? " (killed)" : ""}`,
|
|
281
|
-
];
|
|
282
|
-
if (result.stderr.trim()) parts.push(`stderr:\n${result.stderr.trimEnd()}`);
|
|
283
|
-
if (result.stdout.trim()) parts.push(`stdout:\n${result.stdout.trimEnd()}`);
|
|
284
|
-
return parts.join("\n\n");
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function executeTelegramAttachmentHandlerInvocation(
|
|
288
|
-
handler: TelegramAttachmentCommandTemplateConfig,
|
|
289
|
-
file: TelegramAttachmentHandlerFile,
|
|
290
|
-
cwd: string,
|
|
291
|
-
deps: Pick<TelegramAttachmentHandlerRuntimeDeps<unknown>, "execCommand">,
|
|
292
|
-
appendFileIfMissing = true,
|
|
293
|
-
timeout = getTelegramAttachmentHandlerTimeout(handler),
|
|
294
|
-
stdin?: string,
|
|
295
|
-
): Promise<string> {
|
|
296
|
-
const invocation = buildTelegramAttachmentHandlerInvocation(
|
|
297
|
-
handler,
|
|
298
|
-
file,
|
|
299
|
-
cwd,
|
|
300
|
-
appendFileIfMissing,
|
|
301
|
-
);
|
|
302
|
-
const result = await deps.execCommand(invocation.command, invocation.args, {
|
|
303
|
-
cwd,
|
|
304
|
-
timeout,
|
|
305
|
-
...(typeof handler === "object" && handler.retry !== undefined
|
|
306
|
-
? { retry: handler.retry }
|
|
307
|
-
: {}),
|
|
308
|
-
...(stdin !== undefined ? { stdin } : {}),
|
|
309
|
-
});
|
|
310
|
-
if (result.code !== 0)
|
|
311
|
-
throw new Error(formatTelegramAttachmentHandlerFailure(result));
|
|
312
|
-
return result.stdout;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function getTelegramAttachmentHandlerCompositionSteps(
|
|
316
|
-
handler: TelegramAttachmentHandlerConfig,
|
|
317
|
-
): TelegramAttachmentCommandTemplateConfig[] {
|
|
318
|
-
if (Array.isArray(handler.template)) {
|
|
319
|
-
return expandCommandTemplateConfigs(
|
|
320
|
-
handler,
|
|
321
|
-
) as TelegramAttachmentCommandTemplateConfig[];
|
|
322
|
-
}
|
|
323
|
-
if (handler.pipe?.length) {
|
|
324
|
-
return expandCommandTemplateConfigs({
|
|
325
|
-
...handler,
|
|
326
|
-
template: handler.pipe,
|
|
327
|
-
}) as TelegramAttachmentCommandTemplateConfig[];
|
|
328
|
-
}
|
|
329
|
-
return [];
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function executeTelegramAttachmentHandler(
|
|
333
|
-
handler: TelegramAttachmentHandlerConfig,
|
|
334
|
-
file: TelegramAttachmentHandlerFile,
|
|
335
|
-
cwd: string,
|
|
336
|
-
deps: Pick<TelegramAttachmentHandlerRuntimeDeps<unknown>, "execCommand">,
|
|
337
|
-
): Promise<string> {
|
|
338
|
-
const steps = getTelegramAttachmentHandlerCompositionSteps(handler);
|
|
339
|
-
if (steps.length === 0) {
|
|
340
|
-
const output = await executeTelegramAttachmentHandlerInvocation(
|
|
341
|
-
handler,
|
|
342
|
-
file,
|
|
343
|
-
cwd,
|
|
344
|
-
deps,
|
|
345
|
-
);
|
|
346
|
-
return output.trim();
|
|
347
|
-
}
|
|
348
|
-
const startedAt = Date.now();
|
|
349
|
-
let output = "";
|
|
350
|
-
for (const [index, step] of steps.entries()) {
|
|
351
|
-
try {
|
|
352
|
-
output = await executeTelegramAttachmentHandlerInvocation(
|
|
353
|
-
step,
|
|
354
|
-
file,
|
|
355
|
-
cwd,
|
|
356
|
-
deps,
|
|
357
|
-
false,
|
|
358
|
-
getTelegramAttachmentCompositionStepTimeout(handler, step, startedAt),
|
|
359
|
-
index === 0 ? undefined : output,
|
|
360
|
-
);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
if (typeof step === "object" && step.critical) throw error;
|
|
363
|
-
output = "";
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return output.trim();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
export async function processTelegramAttachmentHandlers<
|
|
370
|
-
TFile extends TelegramAttachmentHandlerFile,
|
|
371
|
-
>(options: {
|
|
372
|
-
files: TFile[];
|
|
373
|
-
rawText: string;
|
|
374
|
-
handlers?: TelegramAttachmentHandlerConfig[];
|
|
375
|
-
cwd: string;
|
|
376
|
-
execCommand: TelegramAttachmentHandlerRuntimeDeps<unknown>["execCommand"];
|
|
377
|
-
recordRuntimeEvent?: TelegramAttachmentHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
|
378
|
-
}): Promise<TelegramAttachmentHandlerProcessResult<TFile>> {
|
|
379
|
-
const promptFiles: TFile[] = [...options.files];
|
|
380
|
-
const outputs: TelegramAttachmentHandlerOutput[] = [];
|
|
381
|
-
for (const file of options.files) {
|
|
382
|
-
const handlers = findTelegramAttachmentHandlers(options.handlers, file);
|
|
383
|
-
for (const handler of handlers) {
|
|
384
|
-
try {
|
|
385
|
-
const output = await executeTelegramAttachmentHandler(
|
|
386
|
-
handler,
|
|
387
|
-
file,
|
|
388
|
-
options.cwd,
|
|
389
|
-
options,
|
|
390
|
-
);
|
|
391
|
-
if (output) outputs.push({ file, output, handler });
|
|
392
|
-
break;
|
|
393
|
-
} catch (error) {
|
|
394
|
-
options.recordRuntimeEvent?.("attachment-handler", error, {
|
|
395
|
-
fileName: file.fileName || basename(file.path),
|
|
396
|
-
handler: getTelegramAttachmentHandlerKind(handler),
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
return {
|
|
402
|
-
rawText: options.rawText,
|
|
403
|
-
promptFiles,
|
|
404
|
-
handlerOutputs: outputs.map((output) => output.output),
|
|
405
|
-
handledFiles: outputs,
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export function createTelegramAttachmentHandlerRuntime<TContext>(
|
|
410
|
-
deps: TelegramAttachmentHandlerRuntimeDeps<TContext>,
|
|
411
|
-
): TelegramAttachmentHandlerRuntime<TContext> {
|
|
412
|
-
return {
|
|
413
|
-
process: (files, rawText, ctx) =>
|
|
414
|
-
processTelegramAttachmentHandlers({
|
|
415
|
-
files,
|
|
416
|
-
rawText,
|
|
417
|
-
handlers: deps.getHandlers(),
|
|
418
|
-
cwd: deps.getCwd(ctx),
|
|
419
|
-
execCommand: deps.execCommand,
|
|
420
|
-
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
421
|
-
}),
|
|
422
|
-
};
|
|
423
|
-
}
|