@llblab/pi-telegram 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -5
- package/CHANGELOG.md +25 -1
- package/README.md +40 -10
- package/docs/README.md +2 -2
- package/docs/architecture.md +6 -6
- package/docs/command-templates.md +21 -5
- package/docs/inbound-handlers.md +93 -0
- package/docs/locks.md +13 -11
- package/docs/outbound-handlers.md +41 -5
- package/index.ts +42 -31
- package/lib/config.ts +9 -3
- package/lib/inbound-handlers.ts +588 -0
- package/lib/menu-queue.ts +161 -20
- package/lib/menu-status.ts +1 -1
- package/lib/{attachments.ts → outbound-attachments.ts} +34 -34
- package/lib/outbound-handlers.ts +247 -2
- package/lib/prompts.ts +1 -1
- package/lib/queue.ts +18 -4
- package/lib/routing.ts +3 -3
- package/lib/updates.ts +34 -21
- package/package.json +1 -1
- package/docs/attachment-handlers.md +0 -50
- package/lib/attachment-handlers.ts +0 -423
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;
|
|
@@ -601,7 +650,7 @@ async function generateTelegramVoiceReplyFileWithHandler(
|
|
|
601
650
|
const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
|
|
602
651
|
if (steps.length > 0) {
|
|
603
652
|
const startedAt = Date.now();
|
|
604
|
-
let stdout =
|
|
653
|
+
let stdout = text;
|
|
605
654
|
for (const [index, step] of steps.entries()) {
|
|
606
655
|
try {
|
|
607
656
|
const result = await runVoiceReplyCommand(
|
|
@@ -616,7 +665,7 @@ async function generateTelegramVoiceReplyFileWithHandler(
|
|
|
616
665
|
startedAt,
|
|
617
666
|
),
|
|
618
667
|
execCommand: options.execCommand,
|
|
619
|
-
|
|
668
|
+
stdin: stdout,
|
|
620
669
|
},
|
|
621
670
|
);
|
|
622
671
|
stdout = result.stdout;
|
|
@@ -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/queue.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
78
78
|
queuedAttachments: QueuedAttachment[];
|
|
79
79
|
content: TelegramPromptContent[];
|
|
80
80
|
historyText: string;
|
|
81
|
+
priorityEmoji?: string;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export interface PendingTelegramControlItem<
|
|
@@ -304,6 +305,7 @@ export function clearTelegramQueuePromptPriority<TContext = unknown>(
|
|
|
304
305
|
...item,
|
|
305
306
|
queueLane: "default" as const,
|
|
306
307
|
laneOrder: item.queueOrder,
|
|
308
|
+
priorityEmoji: undefined,
|
|
307
309
|
};
|
|
308
310
|
});
|
|
309
311
|
return { items: nextItems, changed };
|
|
@@ -313,6 +315,7 @@ export function prioritizeTelegramQueuePrompt<TContext = unknown>(
|
|
|
313
315
|
items: TelegramQueueItem<TContext>[],
|
|
314
316
|
messageId: number,
|
|
315
317
|
laneOrder: number,
|
|
318
|
+
priorityEmoji = "⚡",
|
|
316
319
|
): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
|
|
317
320
|
let changed = false;
|
|
318
321
|
const nextItems = items.map((item) => {
|
|
@@ -327,6 +330,7 @@ export function prioritizeTelegramQueuePrompt<TContext = unknown>(
|
|
|
327
330
|
...item,
|
|
328
331
|
queueLane: "priority" as const,
|
|
329
332
|
laneOrder,
|
|
333
|
+
priorityEmoji,
|
|
330
334
|
};
|
|
331
335
|
});
|
|
332
336
|
return { items: nextItems, changed };
|
|
@@ -353,7 +357,7 @@ function formatTelegramQueueItemStatusSummary<TContext = unknown>(
|
|
|
353
357
|
item: TelegramQueueItem<TContext>,
|
|
354
358
|
): string {
|
|
355
359
|
if (item.queueLane === "priority") {
|
|
356
|
-
return
|
|
360
|
+
return `${item.kind === "prompt" ? item.priorityEmoji ?? "⚡" : "⚡"} ${item.statusSummary}`;
|
|
357
361
|
}
|
|
358
362
|
return item.statusSummary;
|
|
359
363
|
}
|
|
@@ -1161,7 +1165,11 @@ export interface TelegramQueueMutationController<TContext> {
|
|
|
1161
1165
|
clear: (ctx: TContext) => number;
|
|
1162
1166
|
removeByMessageIds: (messageIds: number[], ctx: TContext) => number;
|
|
1163
1167
|
clearPriorityByMessageId: (messageId: number, ctx: TContext) => boolean;
|
|
1164
|
-
prioritizeByMessageId: (
|
|
1168
|
+
prioritizeByMessageId: (
|
|
1169
|
+
messageId: number,
|
|
1170
|
+
ctx: TContext,
|
|
1171
|
+
priorityEmoji?: string,
|
|
1172
|
+
) => boolean;
|
|
1165
1173
|
}
|
|
1166
1174
|
|
|
1167
1175
|
export interface TelegramControlQueueControllerDeps<TContext> {
|
|
@@ -1375,8 +1383,12 @@ export function createTelegramQueueMutationController<TContext>(
|
|
|
1375
1383
|
),
|
|
1376
1384
|
clearPriorityByMessageId: (messageId, ctx) =>
|
|
1377
1385
|
clearTelegramQueuePromptPriorityRuntime(messageId, buildRuntimeDeps(ctx)),
|
|
1378
|
-
prioritizeByMessageId: (messageId, ctx) =>
|
|
1379
|
-
prioritizeTelegramQueuePromptRuntime(
|
|
1386
|
+
prioritizeByMessageId: (messageId, ctx, priorityEmoji) =>
|
|
1387
|
+
prioritizeTelegramQueuePromptRuntime(
|
|
1388
|
+
messageId,
|
|
1389
|
+
buildRuntimeDeps(ctx),
|
|
1390
|
+
priorityEmoji,
|
|
1391
|
+
),
|
|
1380
1392
|
};
|
|
1381
1393
|
}
|
|
1382
1394
|
|
|
@@ -1438,6 +1450,7 @@ export function clearTelegramQueuePromptPriorityRuntime<TContext>(
|
|
|
1438
1450
|
export function prioritizeTelegramQueuePromptRuntime<TContext>(
|
|
1439
1451
|
messageId: number,
|
|
1440
1452
|
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
1453
|
+
priorityEmoji?: string,
|
|
1441
1454
|
): boolean {
|
|
1442
1455
|
const nextPriorityReactionOrder = deps.getNextPriorityReactionOrder?.();
|
|
1443
1456
|
if (nextPriorityReactionOrder === undefined) return false;
|
|
@@ -1445,6 +1458,7 @@ export function prioritizeTelegramQueuePromptRuntime<TContext>(
|
|
|
1445
1458
|
deps.getQueuedItems(),
|
|
1446
1459
|
messageId,
|
|
1447
1460
|
nextPriorityReactionOrder,
|
|
1461
|
+
priorityEmoji,
|
|
1448
1462
|
);
|
|
1449
1463
|
if (!changed) return false;
|
|
1450
1464
|
deps.setQueuedItems(items);
|
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/lib/updates.ts
CHANGED
|
@@ -26,18 +26,23 @@ export type TelegramReactionType =
|
|
|
26
26
|
| TelegramReactionTypeEmoji
|
|
27
27
|
| TelegramReactionTypeNonEmoji;
|
|
28
28
|
|
|
29
|
-
export const
|
|
30
|
-
"👍",
|
|
31
|
-
"⚡",
|
|
32
|
-
"❤",
|
|
33
|
-
"🕊",
|
|
29
|
+
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
30
|
+
{ id: 10, name: "like", emoji: "👍" },
|
|
31
|
+
{ id: 11, name: "lightning", emoji: "⚡" },
|
|
32
|
+
{ id: 12, name: "heart", emoji: "❤" },
|
|
33
|
+
{ id: 13, name: "dove", emoji: "🕊" },
|
|
34
34
|
] as const;
|
|
35
|
-
export const
|
|
36
|
-
"👎",
|
|
37
|
-
"👻",
|
|
38
|
-
"💔",
|
|
39
|
-
"💩",
|
|
35
|
+
export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
36
|
+
{ id: 20, name: "dislike", emoji: "👎" },
|
|
37
|
+
{ id: 21, name: "ghost", emoji: "👻" },
|
|
38
|
+
{ id: 22, name: "broken-heart", emoji: "💔" },
|
|
39
|
+
{ id: 23, name: "poop", emoji: "💩" },
|
|
40
40
|
] as const;
|
|
41
|
+
export const TELEGRAM_PRIORITY_REACTION_EMOJIS =
|
|
42
|
+
TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
|
|
43
|
+
export const TELEGRAM_REMOVAL_REACTION_EMOJIS = TELEGRAM_REMOVAL_REACTIONS.map(
|
|
44
|
+
(reaction) => reaction.emoji,
|
|
45
|
+
);
|
|
41
46
|
|
|
42
47
|
export interface TelegramUpdateDeletion {
|
|
43
48
|
deleted_business_messages?: { message_ids?: unknown };
|
|
@@ -71,15 +76,22 @@ function hasAnyTelegramReactionEmoji(
|
|
|
71
76
|
return candidates.some((emoji) => emojis.has(emoji));
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
function
|
|
79
|
+
function getAddedTelegramReactionEmoji(
|
|
75
80
|
oldEmojis: Set<string>,
|
|
76
81
|
newEmojis: Set<string>,
|
|
77
82
|
candidates: readonly string[],
|
|
78
|
-
):
|
|
79
|
-
return candidates.
|
|
83
|
+
): string | undefined {
|
|
84
|
+
return candidates.find(
|
|
80
85
|
(emoji) => !oldEmojis.has(emoji) && newEmojis.has(emoji),
|
|
81
86
|
);
|
|
82
87
|
}
|
|
88
|
+
function hasAddedTelegramReactionEmoji(
|
|
89
|
+
oldEmojis: Set<string>,
|
|
90
|
+
newEmojis: Set<string>,
|
|
91
|
+
candidates: readonly string[],
|
|
92
|
+
): boolean {
|
|
93
|
+
return !!getAddedTelegramReactionEmoji(oldEmojis, newEmojis, candidates);
|
|
94
|
+
}
|
|
83
95
|
|
|
84
96
|
export function extractDeletedTelegramMessageIds(
|
|
85
97
|
update: TelegramUpdateDeletion,
|
|
@@ -410,6 +422,7 @@ export interface TelegramUpdateRuntimeControllerDeps<
|
|
|
410
422
|
prioritizeQueuedTelegramTurnByMessageId: (
|
|
411
423
|
messageId: number,
|
|
412
424
|
ctx: TContext,
|
|
425
|
+
priorityEmoji?: string,
|
|
413
426
|
) => boolean;
|
|
414
427
|
pairTelegramUserIfNeeded: (userId: number, ctx: TContext) => Promise<boolean>;
|
|
415
428
|
answerCallbackQuery: (
|
|
@@ -592,6 +605,7 @@ export interface AuthorizedTelegramReactionUpdateDeps<TContext> {
|
|
|
592
605
|
prioritizeQueuedTelegramTurnByMessageId: (
|
|
593
606
|
messageId: number,
|
|
594
607
|
ctx: TContext,
|
|
608
|
+
priorityEmoji?: string,
|
|
595
609
|
) => boolean;
|
|
596
610
|
}
|
|
597
611
|
|
|
@@ -638,17 +652,16 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
638
652
|
deps.ctx,
|
|
639
653
|
);
|
|
640
654
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
)
|
|
648
|
-
return;
|
|
655
|
+
const addedPriorityEmoji = getAddedTelegramReactionEmoji(
|
|
656
|
+
oldEmojis,
|
|
657
|
+
newEmojis,
|
|
658
|
+
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
659
|
+
);
|
|
660
|
+
if (!addedPriorityEmoji) return;
|
|
649
661
|
deps.prioritizeQueuedTelegramTurnByMessageId(
|
|
650
662
|
reactionUpdate.message_id,
|
|
651
663
|
deps.ctx,
|
|
664
|
+
addedPriorityEmoji,
|
|
652
665
|
);
|
|
653
666
|
}
|
|
654
667
|
|
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.
|