@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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Telegram attachment
|
|
2
|
+
* Telegram outbound attachment helpers
|
|
3
3
|
* Zones: telegram outbound, pi agent tool, filesystem
|
|
4
|
-
* Owns telegram_attach registration, attachment queueing, and
|
|
4
|
+
* Owns telegram_attach registration, outbound attachment queueing, and delivery so Telegram file output stays in one domain module
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { stat } from "node:fs/promises";
|
|
@@ -16,7 +16,7 @@ const MAX_ATTACHMENTS_PER_TURN = 10;
|
|
|
16
16
|
|
|
17
17
|
export const TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
18
18
|
|
|
19
|
-
export function
|
|
19
|
+
export function getTelegramOutboundAttachmentByteLimitFromEnv(
|
|
20
20
|
env: NodeJS.ProcessEnv,
|
|
21
21
|
names: string[],
|
|
22
22
|
defaultValue = TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES,
|
|
@@ -31,17 +31,17 @@ export function getTelegramAttachmentByteLimitFromEnv(
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export const TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES =
|
|
34
|
-
|
|
34
|
+
getTelegramOutboundAttachmentByteLimitFromEnv(process.env, [
|
|
35
35
|
"PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES",
|
|
36
36
|
"TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES",
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
|
-
export interface
|
|
39
|
+
export interface TelegramOutboundAttachmentToolResult {
|
|
40
40
|
content: Array<{ type: "text"; text: string }>;
|
|
41
41
|
details: { paths: string[] };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
export interface
|
|
44
|
+
export interface TelegramOutboundAttachmentRuntimeEventRecorderPort {
|
|
45
45
|
recordRuntimeEvent?: (
|
|
46
46
|
category: string,
|
|
47
47
|
error: unknown,
|
|
@@ -49,28 +49,28 @@ export interface TelegramAttachmentRuntimeEventRecorderPort {
|
|
|
49
49
|
) => void;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export interface
|
|
52
|
+
export interface TelegramOutboundAttachmentToolRegistrationDeps extends TelegramOutboundAttachmentRuntimeEventRecorderPort {
|
|
53
53
|
maxAttachmentsPerTurn?: number;
|
|
54
54
|
maxAttachmentSizeBytes?: number;
|
|
55
|
-
getActiveTurn: () =>
|
|
55
|
+
getActiveTurn: () => TelegramOutboundAttachmentQueueTargetView | undefined;
|
|
56
56
|
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export interface
|
|
59
|
+
export interface TelegramQueuedOutboundAttachmentView {
|
|
60
60
|
path: string;
|
|
61
61
|
fileName: string;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
export interface
|
|
65
|
-
queuedAttachments:
|
|
64
|
+
export interface TelegramOutboundAttachmentQueueTargetView {
|
|
65
|
+
queuedAttachments: TelegramQueuedOutboundAttachmentView[];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export interface
|
|
68
|
+
export interface TelegramQueuedOutboundAttachmentTurnView extends TelegramOutboundAttachmentQueueTargetView {
|
|
69
69
|
chatId: number;
|
|
70
70
|
replyToMessageId: number;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function
|
|
73
|
+
function isTelegramOutboundPhotoAttachmentPath(path: string): boolean {
|
|
74
74
|
const normalized = path.toLowerCase();
|
|
75
75
|
return (
|
|
76
76
|
normalized.endsWith(".jpg") ||
|
|
@@ -81,7 +81,7 @@ function isTelegramPhotoAttachmentPath(path: string): boolean {
|
|
|
81
81
|
);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function
|
|
84
|
+
function formatTelegramOutboundAttachmentSizeLimitError(
|
|
85
85
|
size: number,
|
|
86
86
|
maxSize: number,
|
|
87
87
|
path?: string,
|
|
@@ -90,14 +90,14 @@ function formatTelegramAttachmentSizeLimitError(
|
|
|
90
90
|
return path ? `${message}: ${path}` : message;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function
|
|
93
|
+
function formatTelegramOutboundAttachmentToolResultText(count: number): string {
|
|
94
94
|
// Pi's compact tool rows need an empty first line to visually separate header and result
|
|
95
95
|
return ["", `Queued ${count} Telegram attachment(s).`].join("\n");
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
export function
|
|
98
|
+
export function registerTelegramOutboundAttachmentTool(
|
|
99
99
|
pi: ExtensionAPI,
|
|
100
|
-
deps:
|
|
100
|
+
deps: TelegramOutboundAttachmentToolRegistrationDeps,
|
|
101
101
|
): void {
|
|
102
102
|
const maxAttachmentsPerTurn =
|
|
103
103
|
deps.maxAttachmentsPerTurn ?? MAX_ATTACHMENTS_PER_TURN;
|
|
@@ -120,7 +120,7 @@ export function registerTelegramAttachmentTool(
|
|
|
120
120
|
}),
|
|
121
121
|
async execute(_toolCallId, params) {
|
|
122
122
|
try {
|
|
123
|
-
return await
|
|
123
|
+
return await queueTelegramOutboundAttachments({
|
|
124
124
|
activeTurn: deps.getActiveTurn(),
|
|
125
125
|
paths: params.paths,
|
|
126
126
|
maxAttachmentsPerTurn,
|
|
@@ -138,7 +138,7 @@ export function registerTelegramAttachmentTool(
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
export interface
|
|
141
|
+
export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
|
|
142
142
|
sendMultipart: (
|
|
143
143
|
method: string,
|
|
144
144
|
fields: Record<string, string>,
|
|
@@ -160,13 +160,13 @@ export interface TelegramQueuedAttachmentDeliveryDeps {
|
|
|
160
160
|
maxAttachmentSizeBytes?: number;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
export async function
|
|
164
|
-
activeTurn:
|
|
163
|
+
export async function queueTelegramOutboundAttachments(options: {
|
|
164
|
+
activeTurn: TelegramOutboundAttachmentQueueTargetView | undefined;
|
|
165
165
|
paths: string[];
|
|
166
166
|
maxAttachmentsPerTurn: number;
|
|
167
167
|
maxAttachmentSizeBytes?: number;
|
|
168
168
|
statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
|
|
169
|
-
}): Promise<
|
|
169
|
+
}): Promise<TelegramOutboundAttachmentToolResult> {
|
|
170
170
|
if (!options.activeTurn) {
|
|
171
171
|
throw new Error(
|
|
172
172
|
"telegram_attach can only be used while replying to an active Telegram turn",
|
|
@@ -180,7 +180,7 @@ export async function queueTelegramAttachments(options: {
|
|
|
180
180
|
`Attachment limit reached (${options.maxAttachmentsPerTurn})`,
|
|
181
181
|
);
|
|
182
182
|
}
|
|
183
|
-
const pendingAttachments:
|
|
183
|
+
const pendingAttachments: TelegramQueuedOutboundAttachmentView[] = [];
|
|
184
184
|
for (const inputPath of options.paths) {
|
|
185
185
|
const stats = await (options.statPath ?? stat)(inputPath);
|
|
186
186
|
if (!stats.isFile()) {
|
|
@@ -192,7 +192,7 @@ export async function queueTelegramAttachments(options: {
|
|
|
192
192
|
stats.size > options.maxAttachmentSizeBytes
|
|
193
193
|
) {
|
|
194
194
|
throw new Error(
|
|
195
|
-
|
|
195
|
+
formatTelegramOutboundAttachmentSizeLimitError(
|
|
196
196
|
stats.size,
|
|
197
197
|
options.maxAttachmentSizeBytes,
|
|
198
198
|
inputPath,
|
|
@@ -210,20 +210,20 @@ export async function queueTelegramAttachments(options: {
|
|
|
210
210
|
content: [
|
|
211
211
|
{
|
|
212
212
|
type: "text",
|
|
213
|
-
text:
|
|
213
|
+
text: formatTelegramOutboundAttachmentToolResultText(added.length),
|
|
214
214
|
},
|
|
215
215
|
],
|
|
216
216
|
details: { paths: added },
|
|
217
217
|
};
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
export function
|
|
221
|
-
deps:
|
|
220
|
+
export function createTelegramQueuedOutboundAttachmentSender(
|
|
221
|
+
deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
|
|
222
222
|
) {
|
|
223
223
|
return async function sendQueuedAttachments(
|
|
224
|
-
turn:
|
|
224
|
+
turn: TelegramQueuedOutboundAttachmentTurnView,
|
|
225
225
|
): Promise<void> {
|
|
226
|
-
await
|
|
226
|
+
await sendQueuedTelegramOutboundAttachments(turn, {
|
|
227
227
|
...deps,
|
|
228
228
|
maxAttachmentSizeBytes:
|
|
229
229
|
deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
|
|
@@ -231,9 +231,9 @@ export function createTelegramQueuedAttachmentSender(
|
|
|
231
231
|
};
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
export async function
|
|
235
|
-
turn:
|
|
236
|
-
deps:
|
|
234
|
+
export async function sendQueuedTelegramOutboundAttachments(
|
|
235
|
+
turn: TelegramQueuedOutboundAttachmentTurnView,
|
|
236
|
+
deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
|
|
237
237
|
): Promise<void> {
|
|
238
238
|
for (const attachment of turn.queuedAttachments) {
|
|
239
239
|
try {
|
|
@@ -241,14 +241,14 @@ export async function sendQueuedTelegramAttachments(
|
|
|
241
241
|
const stats = await (deps.statPath ?? stat)(attachment.path);
|
|
242
242
|
if (stats.size > deps.maxAttachmentSizeBytes) {
|
|
243
243
|
throw new Error(
|
|
244
|
-
|
|
244
|
+
formatTelegramOutboundAttachmentSizeLimitError(
|
|
245
245
|
stats.size,
|
|
246
246
|
deps.maxAttachmentSizeBytes,
|
|
247
247
|
),
|
|
248
248
|
);
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
-
const isPhoto =
|
|
251
|
+
const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
|
|
252
252
|
const method = isPhoto ? "sendPhoto" : "sendDocument";
|
|
253
253
|
const fieldName = isPhoto ? "photo" : "document";
|
|
254
254
|
const replyParameters = buildTelegramMultipartReplyParameters(
|
package/lib/outbound-handlers.ts
CHANGED
|
@@ -97,6 +97,55 @@ export interface TelegramVoiceReplySenderDeps {
|
|
|
97
97
|
) => void;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export interface TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup = unknown> {
|
|
101
|
+
execCommand: TelegramVoiceReplySenderDeps["execCommand"];
|
|
102
|
+
getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
|
|
103
|
+
sendTextReply: (
|
|
104
|
+
chatId: number,
|
|
105
|
+
replyToMessageId: number | undefined,
|
|
106
|
+
text: string,
|
|
107
|
+
options?: { parseMode?: "HTML" },
|
|
108
|
+
) => Promise<number | undefined>;
|
|
109
|
+
sendMarkdownReply: (
|
|
110
|
+
chatId: number,
|
|
111
|
+
replyToMessageId: number | undefined,
|
|
112
|
+
markdown: string,
|
|
113
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
114
|
+
) => Promise<number | undefined>;
|
|
115
|
+
cwd?: string;
|
|
116
|
+
recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface TelegramInlineKeyboardLike {
|
|
120
|
+
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface TelegramOutboundTextTransformOptions<TReplyMarkup = unknown> {
|
|
124
|
+
handlers?: TelegramOutboundHandlerConfig[];
|
|
125
|
+
cwd?: string;
|
|
126
|
+
execCommand: TelegramVoiceReplySenderDeps["execCommand"];
|
|
127
|
+
recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
|
|
128
|
+
replyMarkup?: TReplyMarkup;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface TelegramOutboundTextTransformResult<TReplyMarkup = unknown> {
|
|
132
|
+
text: string;
|
|
133
|
+
replyMarkup?: TReplyMarkup;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup = unknown> {
|
|
137
|
+
execCommand: TelegramVoiceReplySenderDeps["execCommand"];
|
|
138
|
+
getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
|
|
139
|
+
finalizeMarkdownPreview: (
|
|
140
|
+
chatId: number,
|
|
141
|
+
markdown: string,
|
|
142
|
+
replyToMessageId: number,
|
|
143
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
144
|
+
) => Promise<boolean>;
|
|
145
|
+
cwd?: string;
|
|
146
|
+
recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
|
|
147
|
+
}
|
|
148
|
+
|
|
100
149
|
interface TelegramTopLevelHtmlComment {
|
|
101
150
|
raw: string;
|
|
102
151
|
content: string;
|
|
@@ -665,6 +714,202 @@ export async function generateTelegramVoiceReplyFile(
|
|
|
665
714
|
});
|
|
666
715
|
}
|
|
667
716
|
|
|
717
|
+
function getOutboundTextTemplateValues(text: string): Record<string, string> {
|
|
718
|
+
return { text, type: "text" };
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function transformTelegramOutboundTextWithHandler(
|
|
722
|
+
text: string,
|
|
723
|
+
options: {
|
|
724
|
+
handler: TelegramOutboundHandlerConfig;
|
|
725
|
+
cwd: string;
|
|
726
|
+
execCommand: TelegramVoiceReplySenderDeps["execCommand"];
|
|
727
|
+
},
|
|
728
|
+
): Promise<string> {
|
|
729
|
+
const values = getOutboundTextTemplateValues(text);
|
|
730
|
+
const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
|
|
731
|
+
if (steps.length > 0) {
|
|
732
|
+
const startedAt = Date.now();
|
|
733
|
+
let stdout = text;
|
|
734
|
+
for (const [index, step] of steps.entries()) {
|
|
735
|
+
try {
|
|
736
|
+
const result = await runVoiceReplyCommand(
|
|
737
|
+
`Outbound text template step ${index + 1}`,
|
|
738
|
+
step,
|
|
739
|
+
values,
|
|
740
|
+
{
|
|
741
|
+
cwd: options.cwd,
|
|
742
|
+
timeout: getVoiceReplyCompositionStepTimeout(
|
|
743
|
+
getVoiceReplyTimeout(options.handler),
|
|
744
|
+
step,
|
|
745
|
+
startedAt,
|
|
746
|
+
),
|
|
747
|
+
execCommand: options.execCommand,
|
|
748
|
+
stdin: stdout,
|
|
749
|
+
},
|
|
750
|
+
);
|
|
751
|
+
stdout = result.stdout;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
if (typeof step === "object" && step.critical) throw error;
|
|
754
|
+
stdout = "";
|
|
755
|
+
}
|
|
756
|
+
if (!stdout) stdout = text;
|
|
757
|
+
}
|
|
758
|
+
return stdout.trim() || text;
|
|
759
|
+
}
|
|
760
|
+
const result = await runVoiceReplyCommand(
|
|
761
|
+
"Outbound text template",
|
|
762
|
+
options.handler,
|
|
763
|
+
values,
|
|
764
|
+
{
|
|
765
|
+
cwd: options.cwd,
|
|
766
|
+
timeout: getVoiceReplyTimeout(options.handler),
|
|
767
|
+
execCommand: options.execCommand,
|
|
768
|
+
stdin: text,
|
|
769
|
+
},
|
|
770
|
+
);
|
|
771
|
+
return result.stdout.trim() || text;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export async function transformTelegramOutboundText(
|
|
775
|
+
text: string,
|
|
776
|
+
options: {
|
|
777
|
+
handlers?: TelegramOutboundHandlerConfig[];
|
|
778
|
+
cwd?: string;
|
|
779
|
+
execCommand: TelegramVoiceReplySenderDeps["execCommand"];
|
|
780
|
+
recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
|
|
781
|
+
},
|
|
782
|
+
): Promise<string> {
|
|
783
|
+
let transformed = text;
|
|
784
|
+
for (const handler of findTelegramOutboundHandlers(options.handlers, "text")) {
|
|
785
|
+
try {
|
|
786
|
+
transformed = await transformTelegramOutboundTextWithHandler(
|
|
787
|
+
transformed,
|
|
788
|
+
{
|
|
789
|
+
handler,
|
|
790
|
+
cwd: options.cwd ?? process.cwd(),
|
|
791
|
+
execCommand: options.execCommand,
|
|
792
|
+
},
|
|
793
|
+
);
|
|
794
|
+
} catch (error) {
|
|
795
|
+
options.recordRuntimeEvent?.("outbound-text-handler", error, {
|
|
796
|
+
handler: outboundHandlerMatchesType(handler, "text") ? "text" : "unknown",
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return transformed;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function isTelegramInlineKeyboardLike(
|
|
804
|
+
replyMarkup: unknown,
|
|
805
|
+
): replyMarkup is TelegramInlineKeyboardLike {
|
|
806
|
+
if (!replyMarkup || typeof replyMarkup !== "object") return false;
|
|
807
|
+
const keyboard = (replyMarkup as { inline_keyboard?: unknown }).inline_keyboard;
|
|
808
|
+
return Array.isArray(keyboard);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function transformTelegramOutboundReplyMarkup<TReplyMarkup>(
|
|
812
|
+
replyMarkup: TReplyMarkup | undefined,
|
|
813
|
+
options: Omit<TelegramOutboundTextTransformOptions, "replyMarkup">,
|
|
814
|
+
): Promise<TReplyMarkup | undefined> {
|
|
815
|
+
if (!isTelegramInlineKeyboardLike(replyMarkup)) return replyMarkup;
|
|
816
|
+
const translatedRows = [];
|
|
817
|
+
for (const row of replyMarkup.inline_keyboard) {
|
|
818
|
+
const translatedRow = [];
|
|
819
|
+
for (const button of row) {
|
|
820
|
+
const text = await transformTelegramOutboundText(button.text, options);
|
|
821
|
+
translatedRow.push({ ...button, text });
|
|
822
|
+
}
|
|
823
|
+
translatedRows.push(translatedRow);
|
|
824
|
+
}
|
|
825
|
+
return { ...replyMarkup, inline_keyboard: translatedRows } as TReplyMarkup;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
export async function transformTelegramOutboundTextReply<TReplyMarkup = unknown>(
|
|
829
|
+
text: string,
|
|
830
|
+
options: TelegramOutboundTextTransformOptions<TReplyMarkup>,
|
|
831
|
+
): Promise<TelegramOutboundTextTransformResult<TReplyMarkup>> {
|
|
832
|
+
const transformOptions = {
|
|
833
|
+
handlers: options.handlers,
|
|
834
|
+
cwd: options.cwd,
|
|
835
|
+
execCommand: options.execCommand,
|
|
836
|
+
recordRuntimeEvent: options.recordRuntimeEvent,
|
|
837
|
+
};
|
|
838
|
+
const transformedText = await transformTelegramOutboundText(
|
|
839
|
+
text,
|
|
840
|
+
transformOptions,
|
|
841
|
+
);
|
|
842
|
+
const replyMarkup = await transformTelegramOutboundReplyMarkup(
|
|
843
|
+
options.replyMarkup,
|
|
844
|
+
transformOptions,
|
|
845
|
+
);
|
|
846
|
+
return { text: transformedText, ...(replyMarkup ? { replyMarkup } : {}) };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function createTelegramOutboundTextReplyRuntime<TReplyMarkup = unknown>(
|
|
850
|
+
deps: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>,
|
|
851
|
+
): Pick<
|
|
852
|
+
TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>,
|
|
853
|
+
"sendTextReply" | "sendMarkdownReply"
|
|
854
|
+
> {
|
|
855
|
+
return {
|
|
856
|
+
sendTextReply: async (chatId, replyToMessageId, text, options) => {
|
|
857
|
+
const transformed = await transformTelegramOutboundText(text, {
|
|
858
|
+
handlers: deps.getHandlers?.(),
|
|
859
|
+
cwd: deps.cwd,
|
|
860
|
+
execCommand: deps.execCommand,
|
|
861
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
862
|
+
});
|
|
863
|
+
return deps.sendTextReply(chatId, replyToMessageId, transformed, options);
|
|
864
|
+
},
|
|
865
|
+
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
866
|
+
const transformed = await transformTelegramOutboundTextReply(markdown, {
|
|
867
|
+
handlers: deps.getHandlers?.(),
|
|
868
|
+
cwd: deps.cwd,
|
|
869
|
+
execCommand: deps.execCommand,
|
|
870
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
871
|
+
replyMarkup: options?.replyMarkup,
|
|
872
|
+
});
|
|
873
|
+
return deps.sendMarkdownReply(chatId, replyToMessageId, transformed.text, {
|
|
874
|
+
...options,
|
|
875
|
+
...(transformed.replyMarkup
|
|
876
|
+
? { replyMarkup: transformed.replyMarkup }
|
|
877
|
+
: {}),
|
|
878
|
+
});
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export function createTelegramOutboundTextPreviewRuntime<TReplyMarkup = unknown>(
|
|
884
|
+
deps: TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
|
|
885
|
+
): Pick<
|
|
886
|
+
TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
|
|
887
|
+
"finalizeMarkdownPreview"
|
|
888
|
+
> {
|
|
889
|
+
return {
|
|
890
|
+
finalizeMarkdownPreview: async (chatId, markdown, replyToMessageId, options) => {
|
|
891
|
+
const transformed = await transformTelegramOutboundTextReply(markdown, {
|
|
892
|
+
handlers: deps.getHandlers?.(),
|
|
893
|
+
cwd: deps.cwd,
|
|
894
|
+
execCommand: deps.execCommand,
|
|
895
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
896
|
+
replyMarkup: options?.replyMarkup,
|
|
897
|
+
});
|
|
898
|
+
return deps.finalizeMarkdownPreview(
|
|
899
|
+
chatId,
|
|
900
|
+
transformed.text,
|
|
901
|
+
replyToMessageId,
|
|
902
|
+
{
|
|
903
|
+
...options,
|
|
904
|
+
...(transformed.replyMarkup
|
|
905
|
+
? { replyMarkup: transformed.replyMarkup }
|
|
906
|
+
: {}),
|
|
907
|
+
},
|
|
908
|
+
);
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
668
913
|
export interface TelegramOutboundReplyPlan<TReplyMarkup = unknown> {
|
|
669
914
|
markdown: string;
|
|
670
915
|
replyMarkup?: TReplyMarkup;
|
package/lib/prompts.ts
CHANGED
|
@@ -14,7 +14,7 @@ Telegram bridge extension is active.
|
|
|
14
14
|
Inbound context:
|
|
15
15
|
- \`[telegram]\` marks Telegram-originated messages.
|
|
16
16
|
- \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
|
|
17
|
-
- \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains
|
|
17
|
+
- \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains inbound-handler stdout such as transcriptions or extracted text for those attachments.
|
|
18
18
|
- Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
|
|
19
19
|
|
|
20
20
|
Telegram-visible output:
|
package/lib/routing.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import * as OutboundHandlers from "./outbound-handlers.ts";
|
|
8
8
|
import * as Commands from "./commands.ts";
|
|
9
9
|
import type { TelegramConfigStore } from "./config.ts";
|
|
10
|
-
import type {
|
|
10
|
+
import type { TelegramInboundHandlerRuntime } from "./inbound-handlers.ts";
|
|
11
11
|
import * as Media from "./media.ts";
|
|
12
12
|
import * as Menu from "./menu.ts";
|
|
13
13
|
import * as Model from "./model.ts";
|
|
@@ -60,7 +60,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
60
60
|
ctx: TContext,
|
|
61
61
|
) => Promise<boolean>;
|
|
62
62
|
buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
|
|
63
|
-
|
|
63
|
+
inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
|
|
64
64
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
65
65
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
66
66
|
answerCallbackQuery: (
|
|
@@ -205,7 +205,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
205
205
|
>({
|
|
206
206
|
allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
207
207
|
downloadFile: deps.downloadFile,
|
|
208
|
-
processAttachments: deps.
|
|
208
|
+
processAttachments: deps.inboundHandlerRuntime.process,
|
|
209
209
|
});
|
|
210
210
|
const enqueueContinueTurn = async (
|
|
211
211
|
message: TMessage,
|
package/package.json
CHANGED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# Attachment Handlers
|
|
2
|
-
|
|
3
|
-
`pi-telegram` can run ordered inbound attachment handlers after downloading files and before the Telegram turn enters the π queue.
|
|
4
|
-
|
|
5
|
-
This document is the local adaptation of the portable [Command Template Standard](./command-templates.md).
|
|
6
|
-
|
|
7
|
-
## Config Shape
|
|
8
|
-
|
|
9
|
-
`telegram.json` may define `attachmentHandlers`:
|
|
10
|
-
|
|
11
|
-
```json
|
|
12
|
-
{
|
|
13
|
-
"attachmentHandlers": [
|
|
14
|
-
{
|
|
15
|
-
"type": "voice",
|
|
16
|
-
"template": "/path/to/stt1 --file {file} --lang {lang=ru}"
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"mime": "audio/*",
|
|
20
|
-
"template": "/path/to/stt2 --file {file} --lang {lang=ru}"
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
}
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
Handlers match by `type`, `mime`, or `match`. Wildcards such as `audio/*` are accepted. Each matching handler must provide `template`; a string is one command, and an array is ordered composition. Top-level `args` and `defaults` apply to composed steps unless a step defines private values. The command-template default timeout applies automatically. Legacy configs may still use `pipe` as a local alias.
|
|
27
|
-
|
|
28
|
-
## Template Placeholders
|
|
29
|
-
|
|
30
|
-
Attachment handlers support these built-in placeholders:
|
|
31
|
-
|
|
32
|
-
| Placeholder | Value |
|
|
33
|
-
| ----------- | ---------------------------------------------------------------- |
|
|
34
|
-
| `{file}` | Full local path to the downloaded file |
|
|
35
|
-
| `{mime}` | MIME type if known |
|
|
36
|
-
| `{type}` | Attachment kind such as `voice`, `audio`, `document`, or `photo` |
|
|
37
|
-
|
|
38
|
-
`defaults` may provide additional placeholder values such as `{lang}` or `{model}`. `args` is only a string-array declaration of supported placeholders; defaults belong in `defaults` or inline placeholders such as `{lang=ru}`. Examples prefer explicit flag-style CLIs for readability, but positional forms such as `/path/to/stt {file} {lang=ru} {model=voxtral-mini-latest}` are equally valid when the target script supports them.
|
|
39
|
-
|
|
40
|
-
If a top-level one-step handler template has no `{file}` placeholder, the downloaded file path is appended as the last command arg as a one-step handler convenience. Composition steps are plain command templates and do not receive implicit file-path args; include `{file}` explicitly where needed.
|
|
41
|
-
|
|
42
|
-
## Ordered Fallbacks
|
|
43
|
-
|
|
44
|
-
A handler list is ordered. For each attachment, matching handlers run in list order and stop after the first successful handler. A composed handler counts as one handler for fallback purposes: if any step fails, the next matching handler is tried.
|
|
45
|
-
|
|
46
|
-
If a matching handler fails with a non-zero exit code, the runtime records diagnostics and tries the next matching handler. If every matching handler fails, the attachment remains visible in the prompt as a normal local file reference.
|
|
47
|
-
|
|
48
|
-
## Prompt Output
|
|
49
|
-
|
|
50
|
-
Local attachments stay in the prompt under `[attachments] <directory>` with relative file entries. Successful handler stdout is added under `[outputs]`. For composed handlers, each step receives the previous step's stdout on stdin by default, and stdout from the last successful step is used as the handler output. Empty output and failed handler output are omitted from the prompt text.
|