@llblab/pi-telegram 0.4.0 → 0.5.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/README.md +21 -12
- package/docs/README.md +2 -0
- package/docs/architecture.md +39 -42
- package/docs/attachment-handlers.md +60 -0
- package/docs/command-templates.md +75 -0
- package/index.ts +9 -7
- package/lib/attachments.ts +70 -2
- package/lib/commands.ts +116 -48
- package/lib/handlers.ts +118 -192
- package/lib/lifecycle.ts +140 -0
- package/lib/locks.ts +43 -13
- package/lib/menu.ts +0 -4
- package/lib/prompts.ts +44 -0
- package/lib/queue.ts +9 -6
- package/lib/routing.ts +4 -2
- package/lib/runtime.ts +9 -6
- package/lib/setup.ts +5 -1
- package/lib/status.ts +29 -4
- package/lib/turns.ts +12 -7
- package/package.json +1 -1
- package/lib/registration.ts +0 -346
package/lib/commands.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram command routing helpers
|
|
3
|
-
* Owns slash-command normalization
|
|
3
|
+
* Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { pairTelegramUserIfNeeded } from "./config.ts";
|
|
7
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
|
|
7
8
|
import {
|
|
8
9
|
createTelegramControlItemBuilder,
|
|
9
10
|
createTelegramControlQueueController,
|
|
@@ -52,22 +53,115 @@ export function createTelegramBotCommandRegistrar(
|
|
|
52
53
|
return () => registerTelegramBotCommands(deps);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
export interface TelegramBridgeCommandStartPollingOptions {
|
|
57
|
+
force?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface TelegramBridgeCommandStartPollingResult {
|
|
61
|
+
ok: boolean;
|
|
62
|
+
message?: string;
|
|
63
|
+
canTakeover?: boolean;
|
|
64
|
+
owner?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TelegramBridgeCommandRegistrationDeps {
|
|
68
|
+
promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
69
|
+
getStatusLines: () => string[];
|
|
70
|
+
reloadConfig: () => Promise<void>;
|
|
71
|
+
hasBotToken: () => boolean;
|
|
72
|
+
startPolling: (
|
|
73
|
+
ctx: ExtensionCommandContext,
|
|
74
|
+
options?: TelegramBridgeCommandStartPollingOptions,
|
|
75
|
+
) =>
|
|
76
|
+
| void
|
|
77
|
+
| Promise<void | TelegramBridgeCommandStartPollingResult>
|
|
78
|
+
| TelegramBridgeCommandStartPollingResult;
|
|
79
|
+
stopPolling: () => Promise<void | string>;
|
|
80
|
+
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
|
|
84
|
+
return ctx.ui.theme.fg("accent", "pi-telegram");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatTelegramTakeoverPrompt(
|
|
88
|
+
ctx: ExtensionCommandContext,
|
|
89
|
+
owner?: string,
|
|
90
|
+
): string {
|
|
91
|
+
const theme = ctx.ui.theme;
|
|
92
|
+
const action = theme.fg("warning", "move singleton lock here?");
|
|
93
|
+
const from = theme.fg("muted", "from:");
|
|
94
|
+
const to = theme.fg("muted", "to:");
|
|
95
|
+
const source = owner ?? "another pi instance";
|
|
96
|
+
return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function registerTelegramBridgeCommands(
|
|
100
|
+
pi: ExtensionAPI,
|
|
101
|
+
deps: TelegramBridgeCommandRegistrationDeps,
|
|
102
|
+
): void {
|
|
103
|
+
pi.registerCommand("telegram-setup", {
|
|
104
|
+
description: "Configure Telegram bot token",
|
|
105
|
+
handler: async (_args, ctx) => {
|
|
106
|
+
await deps.promptForConfig(ctx);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
pi.registerCommand("telegram-status", {
|
|
110
|
+
description: "Show Telegram bridge status",
|
|
111
|
+
handler: async (_args, ctx) => {
|
|
112
|
+
ctx.ui.notify(deps.getStatusLines().join("\n"), "info");
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
pi.registerCommand("telegram-connect", {
|
|
116
|
+
description: "Start the Telegram bridge in this pi session",
|
|
117
|
+
handler: async (_args, ctx) => {
|
|
118
|
+
await deps.reloadConfig();
|
|
119
|
+
if (!deps.hasBotToken()) {
|
|
120
|
+
await deps.promptForConfig(ctx);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let result = await deps.startPolling(ctx);
|
|
124
|
+
if (result && !result.ok && result.canTakeover) {
|
|
125
|
+
const confirmed = await ctx.ui.confirm(
|
|
126
|
+
formatTelegramTakeoverTitle(ctx),
|
|
127
|
+
formatTelegramTakeoverPrompt(ctx, result.owner),
|
|
128
|
+
);
|
|
129
|
+
if (!confirmed) {
|
|
130
|
+
ctx.ui.notify("Telegram bridge takeover cancelled.", "info");
|
|
131
|
+
deps.updateStatus(ctx);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
result = await deps.startPolling(ctx, { force: true });
|
|
135
|
+
}
|
|
136
|
+
if (result?.message) {
|
|
137
|
+
ctx.ui.notify(result.message, result.ok ? "info" : "warning");
|
|
138
|
+
}
|
|
139
|
+
deps.updateStatus(ctx);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
pi.registerCommand("telegram-disconnect", {
|
|
143
|
+
description: "Stop the Telegram bridge in this pi session",
|
|
144
|
+
handler: async (_args, ctx) => {
|
|
145
|
+
const message = await deps.stopPolling();
|
|
146
|
+
if (message) ctx.ui.notify(message, "info");
|
|
147
|
+
deps.updateStatus(ctx);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
55
152
|
export type TelegramCommandAction =
|
|
56
153
|
| { kind: "ignore"; executionMode: "ignored" }
|
|
57
154
|
| { kind: "stop"; executionMode: "immediate" }
|
|
58
155
|
| { kind: "compact"; executionMode: "immediate" }
|
|
59
|
-
| { kind: "status"; executionMode: "
|
|
60
|
-
| { kind: "model"; executionMode: "
|
|
156
|
+
| { kind: "status"; executionMode: "immediate" }
|
|
157
|
+
| { kind: "model"; executionMode: "immediate" }
|
|
61
158
|
| {
|
|
62
159
|
kind: "help";
|
|
63
160
|
commandName: "help" | "start";
|
|
64
161
|
executionMode: "immediate";
|
|
65
162
|
};
|
|
66
163
|
|
|
67
|
-
export type TelegramCommandExecutionMode =
|
|
68
|
-
| "ignored"
|
|
69
|
-
| "immediate"
|
|
70
|
-
| "control-queue";
|
|
164
|
+
export type TelegramCommandExecutionMode = "ignored" | "immediate";
|
|
71
165
|
|
|
72
166
|
export interface TelegramCommandActionDeps<TMessage, TContext> {
|
|
73
167
|
handleStop: (message: TMessage, ctx: TContext) => Promise<void>;
|
|
@@ -99,8 +193,7 @@ export interface TelegramRuntimeEventRecorderPort {
|
|
|
99
193
|
) => void;
|
|
100
194
|
}
|
|
101
195
|
|
|
102
|
-
export interface TelegramCompactCommandDeps
|
|
103
|
-
extends TelegramRuntimeEventRecorderPort {
|
|
196
|
+
export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort {
|
|
104
197
|
isIdle: () => boolean;
|
|
105
198
|
hasPendingMessages: () => boolean;
|
|
106
199
|
hasActiveTelegramTurn: () => boolean;
|
|
@@ -130,14 +223,6 @@ export interface TelegramHelpCommandDeps {
|
|
|
130
223
|
export type TelegramControlCommandType =
|
|
131
224
|
PendingTelegramControlItem<unknown>["controlType"];
|
|
132
225
|
|
|
133
|
-
export interface TelegramQueuedControlCommandDeps<TContext> {
|
|
134
|
-
enqueueControlItem: (
|
|
135
|
-
controlType: TelegramControlCommandType,
|
|
136
|
-
statusSummary: string,
|
|
137
|
-
execute: (ctx: TContext) => Promise<void>,
|
|
138
|
-
) => void;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
226
|
export interface TelegramCommandRuntimeMessage {
|
|
142
227
|
chat: { id: number };
|
|
143
228
|
message_id: number;
|
|
@@ -380,9 +465,9 @@ export function buildTelegramCommandAction(
|
|
|
380
465
|
case "compact":
|
|
381
466
|
return { kind: "compact", executionMode: "immediate" };
|
|
382
467
|
case "status":
|
|
383
|
-
return { kind: "status", executionMode: "
|
|
468
|
+
return { kind: "status", executionMode: "immediate" };
|
|
384
469
|
case "model":
|
|
385
|
-
return { kind: "model", executionMode: "
|
|
470
|
+
return { kind: "model", executionMode: "immediate" };
|
|
386
471
|
case "help":
|
|
387
472
|
case "start":
|
|
388
473
|
return { kind: "help", commandName, executionMode: "immediate" };
|
|
@@ -483,20 +568,18 @@ export async function handleTelegramHelpCommand(
|
|
|
483
568
|
});
|
|
484
569
|
}
|
|
485
570
|
|
|
486
|
-
export async function handleTelegramStatusCommand<TContext>(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
)
|
|
491
|
-
deps.enqueueControlItem("status", "⚡ status", deps.showStatus);
|
|
571
|
+
export async function handleTelegramStatusCommand<TContext>(deps: {
|
|
572
|
+
ctx: TContext;
|
|
573
|
+
showStatus: (ctx: TContext) => Promise<void>;
|
|
574
|
+
}): Promise<void> {
|
|
575
|
+
await deps.showStatus(deps.ctx);
|
|
492
576
|
}
|
|
493
577
|
|
|
494
|
-
export async function handleTelegramModelCommand<TContext>(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
499
|
-
deps.enqueueControlItem("model", "⚡ model", deps.openModelMenu);
|
|
578
|
+
export async function handleTelegramModelCommand<TContext>(deps: {
|
|
579
|
+
ctx: TContext;
|
|
580
|
+
openModelMenu: (ctx: TContext) => Promise<void>;
|
|
581
|
+
}): Promise<void> {
|
|
582
|
+
await deps.openModelMenu(deps.ctx);
|
|
500
583
|
}
|
|
501
584
|
|
|
502
585
|
export async function executeTelegramCommandAction<TMessage, TContext>(
|
|
@@ -644,21 +727,6 @@ async function handleTelegramCommandRuntime<
|
|
|
644
727
|
deps.sendTextReply(nextMessage, text);
|
|
645
728
|
const updateStatusFor = (commandCtx: TContext) => () =>
|
|
646
729
|
deps.updateStatus(commandCtx);
|
|
647
|
-
const enqueueControlFor =
|
|
648
|
-
(nextMessage: TMessage, commandCtx: TContext) =>
|
|
649
|
-
(
|
|
650
|
-
controlType: TelegramControlCommandType,
|
|
651
|
-
statusSummary: string,
|
|
652
|
-
execute: (ctx: TContext) => Promise<void>,
|
|
653
|
-
) => {
|
|
654
|
-
deps.enqueueControlItem(
|
|
655
|
-
nextMessage,
|
|
656
|
-
commandCtx,
|
|
657
|
-
controlType,
|
|
658
|
-
statusSummary,
|
|
659
|
-
execute,
|
|
660
|
-
);
|
|
661
|
-
};
|
|
662
730
|
return executeTelegramCommandAction(
|
|
663
731
|
buildTelegramCommandAction(commandName),
|
|
664
732
|
message,
|
|
@@ -694,13 +762,13 @@ async function handleTelegramCommandRuntime<
|
|
|
694
762
|
},
|
|
695
763
|
handleStatus: async (nextMessage, commandCtx) => {
|
|
696
764
|
await handleTelegramStatusCommand<TContext>({
|
|
697
|
-
|
|
765
|
+
ctx: commandCtx,
|
|
698
766
|
showStatus: (controlCtx) => deps.showStatus(nextMessage, controlCtx),
|
|
699
767
|
});
|
|
700
768
|
},
|
|
701
769
|
handleModel: async (nextMessage, commandCtx) => {
|
|
702
770
|
await handleTelegramModelCommand<TContext>({
|
|
703
|
-
|
|
771
|
+
ctx: commandCtx,
|
|
704
772
|
openModelMenu: (controlCtx) =>
|
|
705
773
|
deps.openModelMenu(nextMessage, controlCtx),
|
|
706
774
|
});
|
package/lib/handlers.ts
CHANGED
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram inbound attachment handler pipeline
|
|
3
|
-
* Owns MIME/type matching
|
|
3
|
+
* Owns MIME/type matching, command-template execution, fallback handling, and prompt injection before prompt enqueueing
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFile } from "node:fs/promises";
|
|
7
6
|
import { homedir } from "node:os";
|
|
8
|
-
import { basename, isAbsolute,
|
|
7
|
+
import { basename, isAbsolute, resolve } from "node:path";
|
|
9
8
|
|
|
10
9
|
const DEFAULT_ATTACHMENT_HANDLER_TIMEOUT_MS = 120_000;
|
|
11
10
|
|
|
12
|
-
function getDefaultAgentDir(): string {
|
|
13
|
-
return process.env.PI_CODING_AGENT_DIR
|
|
14
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
15
|
-
: join(homedir(), ".pi", "agent");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function getDefaultAutoToolsPath(): string {
|
|
19
|
-
return join(getDefaultAgentDir(), "auto-tools.json");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
11
|
export interface TelegramAttachmentHandlerConfig {
|
|
23
12
|
match?: string | string[];
|
|
24
13
|
mime?: string | string[];
|
|
25
14
|
type?: string | string[];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
template?: string;
|
|
16
|
+
args?: string | string[];
|
|
17
|
+
defaults?: Record<string, unknown>;
|
|
29
18
|
timeoutMs?: number;
|
|
30
19
|
}
|
|
31
20
|
|
|
@@ -77,8 +66,6 @@ export interface TelegramAttachmentHandlerRuntimeDeps<TContext> {
|
|
|
77
66
|
options?: TelegramAttachmentHandlerExecOptions,
|
|
78
67
|
) => Promise<TelegramAttachmentHandlerExecResult>;
|
|
79
68
|
getCwd: (ctx: TContext) => string;
|
|
80
|
-
readTextFile?: (path: string) => Promise<string>;
|
|
81
|
-
autoToolsPath?: string;
|
|
82
69
|
recordRuntimeEvent?: (
|
|
83
70
|
category: string,
|
|
84
71
|
error: unknown,
|
|
@@ -99,15 +86,12 @@ interface AttachmentHandlerInvocation {
|
|
|
99
86
|
args: string[];
|
|
100
87
|
}
|
|
101
88
|
|
|
102
|
-
interface AutoToolConfig {
|
|
103
|
-
name: string;
|
|
104
|
-
script: string;
|
|
105
|
-
args: string[];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
89
|
function normalizeStringList(value: string | string[] | undefined): string[] {
|
|
109
90
|
if (Array.isArray(value)) {
|
|
110
|
-
return value
|
|
91
|
+
return value
|
|
92
|
+
.map(String)
|
|
93
|
+
.map((item) => item.trim())
|
|
94
|
+
.filter(Boolean);
|
|
111
95
|
}
|
|
112
96
|
if (typeof value === "string" && value.trim()) return [value.trim()];
|
|
113
97
|
return [];
|
|
@@ -124,7 +108,9 @@ function matchesWildcard(pattern: string, value: string | undefined): boolean {
|
|
|
124
108
|
return new RegExp(`^${escaped}$`).test(normalizedValue);
|
|
125
109
|
}
|
|
126
110
|
|
|
127
|
-
function handlerHasSelectors(
|
|
111
|
+
function handlerHasSelectors(
|
|
112
|
+
handler: TelegramAttachmentHandlerConfig,
|
|
113
|
+
): boolean {
|
|
128
114
|
return (
|
|
129
115
|
normalizeStringList(handler.match).length > 0 ||
|
|
130
116
|
normalizeStringList(handler.mime).length > 0 ||
|
|
@@ -132,7 +118,10 @@ function handlerHasSelectors(handler: TelegramAttachmentHandlerConfig): boolean
|
|
|
132
118
|
);
|
|
133
119
|
}
|
|
134
120
|
|
|
135
|
-
function matchesAnyPattern(
|
|
121
|
+
function matchesAnyPattern(
|
|
122
|
+
patterns: string[],
|
|
123
|
+
value: string | undefined,
|
|
124
|
+
): boolean {
|
|
136
125
|
return patterns.some((pattern) => matchesWildcard(pattern, value));
|
|
137
126
|
}
|
|
138
127
|
|
|
@@ -150,12 +139,12 @@ export function telegramAttachmentHandlerMatchesFile(
|
|
|
150
139
|
return matchesAnyPattern(matchPatterns, file.kind);
|
|
151
140
|
}
|
|
152
141
|
|
|
153
|
-
export function
|
|
142
|
+
export function findTelegramAttachmentHandlers(
|
|
154
143
|
handlers: TelegramAttachmentHandlerConfig[] | undefined,
|
|
155
144
|
file: TelegramAttachmentHandlerFile,
|
|
156
|
-
): TelegramAttachmentHandlerConfig
|
|
157
|
-
if (!Array.isArray(handlers)) return
|
|
158
|
-
return handlers.
|
|
145
|
+
): TelegramAttachmentHandlerConfig[] {
|
|
146
|
+
if (!Array.isArray(handlers)) return [];
|
|
147
|
+
return handlers.filter(
|
|
159
148
|
(handler) =>
|
|
160
149
|
!!handler &&
|
|
161
150
|
typeof handler === "object" &&
|
|
@@ -163,29 +152,64 @@ export function findTelegramAttachmentHandler(
|
|
|
163
152
|
);
|
|
164
153
|
}
|
|
165
154
|
|
|
166
|
-
function
|
|
167
|
-
|
|
155
|
+
export function findTelegramAttachmentHandler(
|
|
156
|
+
handlers: TelegramAttachmentHandlerConfig[] | undefined,
|
|
157
|
+
file: TelegramAttachmentHandlerFile,
|
|
158
|
+
): TelegramAttachmentHandlerConfig | undefined {
|
|
159
|
+
return findTelegramAttachmentHandlers(handlers, file)[0];
|
|
168
160
|
}
|
|
169
161
|
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
function hasAttachmentFilePlaceholder(value: string): boolean {
|
|
163
|
+
return /\{file\}/.test(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeTelegramAttachmentHandlerArgs(
|
|
167
|
+
value: string | string[] | undefined,
|
|
168
|
+
): string[] {
|
|
169
|
+
if (Array.isArray(value)) return value.map(String).map((item) => item.trim());
|
|
170
|
+
if (typeof value !== "string") return [];
|
|
171
|
+
return value.split(",").map((item) => item.trim());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getTelegramAttachmentHandlerArgDefaults(
|
|
175
|
+
handler: TelegramAttachmentHandlerConfig,
|
|
176
|
+
): Record<string, string> {
|
|
177
|
+
const defaults: Record<string, string> = {};
|
|
178
|
+
for (const item of normalizeTelegramAttachmentHandlerArgs(handler.args)) {
|
|
179
|
+
if (!item) continue;
|
|
180
|
+
const [name, ...defaultParts] = item.split("=");
|
|
181
|
+
if (!name || defaultParts.length === 0) continue;
|
|
182
|
+
defaults[name.trim()] = defaultParts.join("=").trim();
|
|
183
|
+
}
|
|
184
|
+
for (const [key, value] of Object.entries(handler.defaults ?? {})) {
|
|
185
|
+
defaults[key] = value === undefined || value === null ? "" : String(value);
|
|
186
|
+
}
|
|
187
|
+
return defaults;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getTelegramAttachmentHandlerTemplateValues(
|
|
191
|
+
handler: TelegramAttachmentHandlerConfig,
|
|
172
192
|
file: TelegramAttachmentHandlerFile,
|
|
173
|
-
): string {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"{type}": file.kind ?? "",
|
|
193
|
+
): Record<string, string> {
|
|
194
|
+
return {
|
|
195
|
+
...getTelegramAttachmentHandlerArgDefaults(handler),
|
|
196
|
+
file: file.path,
|
|
197
|
+
mime: file.mimeType ?? "",
|
|
198
|
+
type: file.kind ?? "",
|
|
180
199
|
};
|
|
181
|
-
let result = token;
|
|
182
|
-
for (const [key, value] of Object.entries(replacements)) {
|
|
183
|
-
result = result.split(key).join(value);
|
|
184
|
-
}
|
|
185
|
-
return result;
|
|
186
200
|
}
|
|
187
201
|
|
|
188
|
-
|
|
202
|
+
function substituteTelegramAttachmentHandlerTemplateToken(
|
|
203
|
+
token: string,
|
|
204
|
+
values: Record<string, string>,
|
|
205
|
+
): string {
|
|
206
|
+
return token.replace(/\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (_match, name) => {
|
|
207
|
+
if (Object.hasOwn(values, name)) return values[name] ?? "";
|
|
208
|
+
throw new Error(`Missing attachment handler template value: ${name}`);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function splitTelegramAttachmentHandlerTemplate(input: string): string[] {
|
|
189
213
|
const words: string[] = [];
|
|
190
214
|
let current = "";
|
|
191
215
|
let quote: "'" | '"' | undefined;
|
|
@@ -237,135 +261,38 @@ function expandExecutablePath(command: string, cwd: string): string {
|
|
|
237
261
|
return command;
|
|
238
262
|
}
|
|
239
263
|
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
function buildTelegramAttachmentTemplateInvocation(
|
|
265
|
+
template: string,
|
|
266
|
+
handler: TelegramAttachmentHandlerConfig,
|
|
242
267
|
file: TelegramAttachmentHandlerFile,
|
|
243
268
|
cwd: string,
|
|
244
269
|
): AttachmentHandlerInvocation {
|
|
245
|
-
const parts =
|
|
270
|
+
const parts = splitTelegramAttachmentHandlerTemplate(template);
|
|
246
271
|
const commandPart = parts[0];
|
|
247
|
-
if (!commandPart) throw new Error("Attachment handler
|
|
248
|
-
const
|
|
272
|
+
if (!commandPart) throw new Error("Attachment handler template is empty");
|
|
273
|
+
const values = getTelegramAttachmentHandlerTemplateValues(handler, file);
|
|
274
|
+
const hadFilePlaceholder = parts.some(hasAttachmentFilePlaceholder);
|
|
249
275
|
const command = expandExecutablePath(
|
|
250
|
-
|
|
276
|
+
substituteTelegramAttachmentHandlerTemplateToken(commandPart, values),
|
|
251
277
|
cwd,
|
|
252
278
|
);
|
|
253
279
|
const args = parts
|
|
254
280
|
.slice(1)
|
|
255
|
-
.map((part) =>
|
|
256
|
-
|
|
257
|
-
return { command, args };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function normalizeAutoToolName(name: string): string {
|
|
261
|
-
return name
|
|
262
|
-
.trim()
|
|
263
|
-
.toLowerCase()
|
|
264
|
-
.replace(/[^a-z0-9_]/g, "_")
|
|
265
|
-
.replace(/_+/g, "_")
|
|
266
|
-
.replace(/^_+|_+$/g, "");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function normalizeAutoToolArgs(value: unknown): string[] {
|
|
270
|
-
const source = Array.isArray(value)
|
|
271
|
-
? value
|
|
272
|
-
: typeof value === "string"
|
|
273
|
-
? value.split(",")
|
|
274
|
-
: [];
|
|
275
|
-
const args: string[] = [];
|
|
276
|
-
const seen = new Set<string>();
|
|
277
|
-
for (const item of source) {
|
|
278
|
-
const arg = normalizeAutoToolName(String(item));
|
|
279
|
-
if (!arg || seen.has(arg)) continue;
|
|
280
|
-
seen.add(arg);
|
|
281
|
-
args.push(arg);
|
|
282
|
-
}
|
|
283
|
-
return args;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function parseTelegramAutoToolsRegistry(
|
|
287
|
-
content: string,
|
|
288
|
-
): Map<string, AutoToolConfig> {
|
|
289
|
-
const raw = JSON.parse(content) as unknown;
|
|
290
|
-
const entries = Array.isArray(raw)
|
|
291
|
-
? raw.map((value) => [undefined, value] as const)
|
|
292
|
-
: raw && typeof raw === "object"
|
|
293
|
-
? Object.entries(raw as Record<string, unknown>)
|
|
294
|
-
: [];
|
|
295
|
-
const tools = new Map<string, AutoToolConfig>();
|
|
296
|
-
for (const [key, value] of entries) {
|
|
297
|
-
if (!value || typeof value !== "object") continue;
|
|
298
|
-
const record = value as Record<string, unknown>;
|
|
299
|
-
const name = normalizeAutoToolName(
|
|
300
|
-
typeof record.name === "string" ? record.name : (key ?? ""),
|
|
281
|
+
.map((part) =>
|
|
282
|
+
substituteTelegramAttachmentHandlerTemplateToken(part, values),
|
|
301
283
|
);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
tools.set(name, { name, script, args: normalizeAutoToolArgs(record.args) });
|
|
305
|
-
}
|
|
306
|
-
return tools;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
async function readTelegramAutoToolsRegistry(
|
|
310
|
-
path: string,
|
|
311
|
-
readTextFile: (path: string) => Promise<string>,
|
|
312
|
-
): Promise<Map<string, AutoToolConfig>> {
|
|
313
|
-
try {
|
|
314
|
-
return parseTelegramAutoToolsRegistry(await readTextFile(path));
|
|
315
|
-
} catch {
|
|
316
|
-
return new Map();
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function getConfiguredToolArgValue(
|
|
321
|
-
value: unknown,
|
|
322
|
-
file: TelegramAttachmentHandlerFile,
|
|
323
|
-
): string | undefined {
|
|
324
|
-
if (value === undefined || value === null) return undefined;
|
|
325
|
-
return substituteTelegramAttachmentHandlerToken(String(value), file);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function getDefaultToolArgValue(
|
|
329
|
-
arg: string,
|
|
330
|
-
file: TelegramAttachmentHandlerFile,
|
|
331
|
-
): string {
|
|
332
|
-
if (["file", "filename", "path"].includes(arg)) return file.path;
|
|
333
|
-
if (["basename", "name"].includes(arg)) {
|
|
334
|
-
return file.fileName || basename(file.path);
|
|
335
|
-
}
|
|
336
|
-
if (["mime", "mime_type", "mimetype"].includes(arg)) return file.mimeType ?? "";
|
|
337
|
-
if (["type", "kind"].includes(arg)) return file.kind ?? "";
|
|
338
|
-
return "";
|
|
284
|
+
if (!hadFilePlaceholder) args.push(file.path);
|
|
285
|
+
return { command, args };
|
|
339
286
|
}
|
|
340
287
|
|
|
341
|
-
|
|
288
|
+
export function buildTelegramAttachmentHandlerInvocation(
|
|
342
289
|
handler: TelegramAttachmentHandlerConfig,
|
|
343
290
|
file: TelegramAttachmentHandlerFile,
|
|
344
291
|
cwd: string,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
): Promise<AttachmentHandlerInvocation> {
|
|
350
|
-
const toolName = normalizeAutoToolName(handler.tool ?? "");
|
|
351
|
-
if (!toolName) throw new Error("Attachment handler tool is empty");
|
|
352
|
-
const readRegistryFile =
|
|
353
|
-
deps.readTextFile ?? ((path: string) => readFile(path, "utf8"));
|
|
354
|
-
const registry = await readTelegramAutoToolsRegistry(
|
|
355
|
-
deps.autoToolsPath ?? getDefaultAutoToolsPath(),
|
|
356
|
-
readRegistryFile,
|
|
357
|
-
);
|
|
358
|
-
const tool = registry.get(toolName);
|
|
359
|
-
if (!tool) {
|
|
360
|
-
throw new Error(`Attachment handler tool not found in auto-tools: ${toolName}`);
|
|
361
|
-
}
|
|
362
|
-
const script = expandExecutablePath(tool.script, cwd);
|
|
363
|
-
const args = tool.args.map(
|
|
364
|
-
(arg) =>
|
|
365
|
-
getConfiguredToolArgValue(handler.args?.[arg], file) ??
|
|
366
|
-
getDefaultToolArgValue(arg, file),
|
|
367
|
-
);
|
|
368
|
-
return { command: script, args };
|
|
292
|
+
): AttachmentHandlerInvocation {
|
|
293
|
+
const { template } = handler;
|
|
294
|
+
if (!template) throw new Error("Attachment handler template is required");
|
|
295
|
+
return buildTelegramAttachmentTemplateInvocation(template, handler, file, cwd);
|
|
369
296
|
}
|
|
370
297
|
|
|
371
298
|
function getTelegramAttachmentHandlerTimeout(
|
|
@@ -378,9 +305,10 @@ function getTelegramAttachmentHandlerTimeout(
|
|
|
378
305
|
: DEFAULT_ATTACHMENT_HANDLER_TIMEOUT_MS;
|
|
379
306
|
}
|
|
380
307
|
|
|
381
|
-
function getTelegramAttachmentHandlerKind(
|
|
382
|
-
|
|
383
|
-
|
|
308
|
+
function getTelegramAttachmentHandlerKind(
|
|
309
|
+
handler: TelegramAttachmentHandlerConfig,
|
|
310
|
+
): string {
|
|
311
|
+
if (handler.template) return "template";
|
|
384
312
|
return "unknown";
|
|
385
313
|
}
|
|
386
314
|
|
|
@@ -399,19 +327,19 @@ async function executeTelegramAttachmentHandler(
|
|
|
399
327
|
handler: TelegramAttachmentHandlerConfig,
|
|
400
328
|
file: TelegramAttachmentHandlerFile,
|
|
401
329
|
cwd: string,
|
|
402
|
-
deps: Pick<
|
|
403
|
-
TelegramAttachmentHandlerRuntimeDeps<unknown>,
|
|
404
|
-
"execCommand" | "readTextFile" | "autoToolsPath"
|
|
405
|
-
>,
|
|
330
|
+
deps: Pick<TelegramAttachmentHandlerRuntimeDeps<unknown>, "execCommand">,
|
|
406
331
|
): Promise<string> {
|
|
407
|
-
const invocation =
|
|
408
|
-
|
|
409
|
-
|
|
332
|
+
const invocation = buildTelegramAttachmentHandlerInvocation(
|
|
333
|
+
handler,
|
|
334
|
+
file,
|
|
335
|
+
cwd,
|
|
336
|
+
);
|
|
410
337
|
const result = await deps.execCommand(invocation.command, invocation.args, {
|
|
411
338
|
cwd,
|
|
412
339
|
timeout: getTelegramAttachmentHandlerTimeout(handler),
|
|
413
340
|
});
|
|
414
|
-
if (result.code !== 0)
|
|
341
|
+
if (result.code !== 0)
|
|
342
|
+
throw new Error(formatTelegramAttachmentHandlerFailure(result));
|
|
415
343
|
return result.stdout.trim();
|
|
416
344
|
}
|
|
417
345
|
|
|
@@ -423,28 +351,28 @@ export async function processTelegramAttachmentHandlers<
|
|
|
423
351
|
handlers?: TelegramAttachmentHandlerConfig[];
|
|
424
352
|
cwd: string;
|
|
425
353
|
execCommand: TelegramAttachmentHandlerRuntimeDeps<unknown>["execCommand"];
|
|
426
|
-
readTextFile?: (path: string) => Promise<string>;
|
|
427
|
-
autoToolsPath?: string;
|
|
428
354
|
recordRuntimeEvent?: TelegramAttachmentHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
|
429
355
|
}): Promise<TelegramAttachmentHandlerProcessResult<TFile>> {
|
|
430
356
|
const promptFiles: TFile[] = [...options.files];
|
|
431
357
|
const outputs: TelegramAttachmentHandlerOutput[] = [];
|
|
432
358
|
for (const file of options.files) {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
359
|
+
const handlers = findTelegramAttachmentHandlers(options.handlers, file);
|
|
360
|
+
for (const handler of handlers) {
|
|
361
|
+
try {
|
|
362
|
+
const output = await executeTelegramAttachmentHandler(
|
|
363
|
+
handler,
|
|
364
|
+
file,
|
|
365
|
+
options.cwd,
|
|
366
|
+
options,
|
|
367
|
+
);
|
|
368
|
+
if (output) outputs.push({ file, output, handler });
|
|
369
|
+
break;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
options.recordRuntimeEvent?.("attachment-handler", error, {
|
|
372
|
+
fileName: file.fileName || basename(file.path),
|
|
373
|
+
handler: getTelegramAttachmentHandlerKind(handler),
|
|
374
|
+
});
|
|
375
|
+
}
|
|
448
376
|
}
|
|
449
377
|
}
|
|
450
378
|
return {
|
|
@@ -466,8 +394,6 @@ export function createTelegramAttachmentHandlerRuntime<TContext>(
|
|
|
466
394
|
handlers: deps.getHandlers(),
|
|
467
395
|
cwd: deps.getCwd(ctx),
|
|
468
396
|
execCommand: deps.execCommand,
|
|
469
|
-
readTextFile: deps.readTextFile,
|
|
470
|
-
autoToolsPath: deps.autoToolsPath,
|
|
471
397
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
472
398
|
}),
|
|
473
399
|
};
|