@llblab/pi-telegram 0.45.11 → 0.47.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/BACKLOG.md +5 -1
- package/CHANGELOG.md +12 -0
- package/README.md +6 -6
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1608 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +116 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1006 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +17 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +11 -3
- package/lib/bus-leader.ts +57 -20
- package/lib/bus.ts +12 -2
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +26 -1
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +21 -5
- package/lib/outbound-markup.ts +16 -7
- package/lib/pi.ts +4 -0
- package/lib/sync.ts +3 -0
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-cleanup-manager.ts +36 -6
- package/lib/thread-display.ts +96 -18
- package/lib/threads.ts +150 -30
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram outbound attachment helpers
|
|
3
|
+
* Zones: telegram outbound, pi agent tool, filesystem
|
|
4
|
+
* Owns telegram_attach registration, outbound attachment queueing, and delivery so Telegram file output stays in one domain module
|
|
5
|
+
*/
|
|
6
|
+
import { stat } from "node:fs/promises";
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
import { isTelegramChannelPostValidationError, TelegramChannelPostValidationError, } from "./channel-posts.js";
|
|
10
|
+
import { TELEGRAM_ATTACH_PROMPT_GUIDELINES, TELEGRAM_ATTACH_PROMPT_SNIPPET, TELEGRAM_MESSAGE_PROMPT_GUIDELINES, TELEGRAM_MESSAGE_PROMPT_SNIPPET, } from "./prompts.js";
|
|
11
|
+
import { withTelegramReplyParameters, normalizeTelegramNativeMarkdown, } from "./replies.js";
|
|
12
|
+
import { getTelegramTargetThreadParams, } from "./target.js";
|
|
13
|
+
const MAX_ATTACHMENTS_PER_TURN = 10;
|
|
14
|
+
export const TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
15
|
+
export function getTelegramOutboundAttachmentByteLimitFromEnv(env, names, defaultValue = TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES) {
|
|
16
|
+
for (const name of names) {
|
|
17
|
+
const rawValue = env[name]?.trim();
|
|
18
|
+
if (!rawValue)
|
|
19
|
+
continue;
|
|
20
|
+
const parsed = Number(rawValue);
|
|
21
|
+
if (Number.isSafeInteger(parsed) && parsed > 0)
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
return defaultValue;
|
|
25
|
+
}
|
|
26
|
+
export const TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES = getTelegramOutboundAttachmentByteLimitFromEnv(process.env, [
|
|
27
|
+
"PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES",
|
|
28
|
+
"TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES",
|
|
29
|
+
]);
|
|
30
|
+
class TelegramRichAttachmentCommitUnknownError extends Error {
|
|
31
|
+
kind = "commit-unknown";
|
|
32
|
+
constructor(cause) {
|
|
33
|
+
super("Telegram Rich media upload may have committed without a message id.");
|
|
34
|
+
this.name = "TelegramRichAttachmentCommitUnknownError";
|
|
35
|
+
this.cause = cause;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isTelegramRichAttachmentCommitUnknownError(error) {
|
|
39
|
+
return (error instanceof TelegramRichAttachmentCommitUnknownError ||
|
|
40
|
+
(typeof error === "object" &&
|
|
41
|
+
error !== null &&
|
|
42
|
+
error.kind === "commit-unknown"));
|
|
43
|
+
}
|
|
44
|
+
export function planTelegramRichOutboundAttachment(options) {
|
|
45
|
+
if (options.renderingMode !== "rich")
|
|
46
|
+
return undefined;
|
|
47
|
+
if (!options.markdown.trim())
|
|
48
|
+
return undefined;
|
|
49
|
+
if (options.turn.queuedAttachments.length !== 1)
|
|
50
|
+
return undefined;
|
|
51
|
+
const attachment = options.turn.queuedAttachments[0];
|
|
52
|
+
const normalizedPath = attachment.path.toLowerCase();
|
|
53
|
+
const mediaType = normalizedPath.endsWith(".jpg") ||
|
|
54
|
+
normalizedPath.endsWith(".jpeg") ||
|
|
55
|
+
normalizedPath.endsWith(".png")
|
|
56
|
+
? "photo"
|
|
57
|
+
: normalizedPath.endsWith(".mp4")
|
|
58
|
+
? "video"
|
|
59
|
+
: normalizedPath.endsWith(".mp3")
|
|
60
|
+
? "audio"
|
|
61
|
+
: undefined;
|
|
62
|
+
if (!mediaType)
|
|
63
|
+
return undefined;
|
|
64
|
+
const mediaId = "artifact";
|
|
65
|
+
const richMessage = {
|
|
66
|
+
markdown: `\n\n${normalizeTelegramNativeMarkdown(options.markdown)}`,
|
|
67
|
+
media: [
|
|
68
|
+
{
|
|
69
|
+
id: mediaId,
|
|
70
|
+
media: {
|
|
71
|
+
type: mediaType,
|
|
72
|
+
media: "attach://rich_media_upload",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
method: "sendRichMessage",
|
|
79
|
+
fields: {
|
|
80
|
+
chat_id: String(options.turn.chatId),
|
|
81
|
+
...getTelegramMultipartTargetFields(options.turn.target),
|
|
82
|
+
rich_message: JSON.stringify(richMessage),
|
|
83
|
+
...(options.replyMarkup
|
|
84
|
+
? { reply_markup: JSON.stringify(options.replyMarkup) }
|
|
85
|
+
: {}),
|
|
86
|
+
},
|
|
87
|
+
fileField: "rich_media_upload",
|
|
88
|
+
filePath: attachment.path,
|
|
89
|
+
fileName: attachment.fileName,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function createTelegramRichOutboundAttachmentSender(deps) {
|
|
93
|
+
return async (turn, markdown, options) => {
|
|
94
|
+
if (options?.isDeliveryActive?.() === false)
|
|
95
|
+
return false;
|
|
96
|
+
const plan = planTelegramRichOutboundAttachment({
|
|
97
|
+
turn,
|
|
98
|
+
markdown,
|
|
99
|
+
renderingMode: deps.getRenderingMode(),
|
|
100
|
+
replyMarkup: options?.replyMarkup,
|
|
101
|
+
});
|
|
102
|
+
if (!plan)
|
|
103
|
+
return false;
|
|
104
|
+
try {
|
|
105
|
+
const result = await withTelegramReplyParameters(turn.chatId, turn.replyToMessageId, turn.target, (replyParameters) => deps.sendMultipart(plan.method, { ...plan.fields, ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}) }, plan.fileField, plan.filePath, plan.fileName));
|
|
106
|
+
const messageId = result && typeof result === "object" &&
|
|
107
|
+
Number.isInteger(result.message_id)
|
|
108
|
+
? result.message_id
|
|
109
|
+
: undefined;
|
|
110
|
+
if (messageId === undefined) {
|
|
111
|
+
throw new TelegramRichAttachmentCommitUnknownError(new Error("Successful Rich media upload omitted message_id."));
|
|
112
|
+
}
|
|
113
|
+
if (options?.isDeliveryActive?.() !== false) {
|
|
114
|
+
deps.recordOwnership?.({
|
|
115
|
+
chatId: turn.chatId,
|
|
116
|
+
messageId,
|
|
117
|
+
target: turn.target,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (isTelegramRichAttachmentCommitUnknownError(error))
|
|
124
|
+
throw error;
|
|
125
|
+
deps.recordRuntimeEvent?.("attachment", error, {
|
|
126
|
+
phase: "rich-media-known-failure",
|
|
127
|
+
fileName: plan.fileName,
|
|
128
|
+
});
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function isTelegramOutboundPhotoAttachmentPath(path) {
|
|
134
|
+
const normalized = path.toLowerCase();
|
|
135
|
+
return (normalized.endsWith(".jpg") ||
|
|
136
|
+
normalized.endsWith(".jpeg") ||
|
|
137
|
+
normalized.endsWith(".png") ||
|
|
138
|
+
normalized.endsWith(".webp") ||
|
|
139
|
+
normalized.endsWith(".gif"));
|
|
140
|
+
}
|
|
141
|
+
function getTelegramGuestAttachmentTransport(path) {
|
|
142
|
+
const normalized = path.toLowerCase();
|
|
143
|
+
if (normalized.endsWith(".jpg") ||
|
|
144
|
+
normalized.endsWith(".jpeg") ||
|
|
145
|
+
normalized.endsWith(".png")) {
|
|
146
|
+
return { method: "sendPhoto", fileField: "photo" };
|
|
147
|
+
}
|
|
148
|
+
if (normalized.endsWith(".ogg") || normalized.endsWith(".opus")) {
|
|
149
|
+
return { method: "sendVoice", fileField: "voice" };
|
|
150
|
+
}
|
|
151
|
+
if (normalized.endsWith(".mp3")) {
|
|
152
|
+
return { method: "sendAudio", fileField: "audio" };
|
|
153
|
+
}
|
|
154
|
+
return { method: "sendDocument", fileField: "document" };
|
|
155
|
+
}
|
|
156
|
+
function formatTelegramOutboundAttachmentSizeLimitError(size, maxSize, path) {
|
|
157
|
+
const message = `Attachment exceeds size limit (${size} bytes > ${maxSize} bytes)`;
|
|
158
|
+
return path ? `${message}: ${path}` : message;
|
|
159
|
+
}
|
|
160
|
+
function formatTelegramOutboundAttachmentToolResultText(count, mode = "queued") {
|
|
161
|
+
// Pi's compact tool rows need one leading newline to visually separate header and result.
|
|
162
|
+
const verb = mode === "queued" ? "Queued" : "Sent";
|
|
163
|
+
return ["", `${verb} ${count} Telegram attachment(s).`].join("\n");
|
|
164
|
+
}
|
|
165
|
+
function formatTelegramOutboundMessageToolResultText(chatId) {
|
|
166
|
+
return ["", `Sent Telegram message to ${chatId}.`].join("\n");
|
|
167
|
+
}
|
|
168
|
+
function getTelegramMultipartTargetFields(target) {
|
|
169
|
+
if (!target)
|
|
170
|
+
return {};
|
|
171
|
+
return Object.fromEntries(Object.entries(getTelegramTargetThreadParams(target)).map(([key, value]) => [key, String(value)]));
|
|
172
|
+
}
|
|
173
|
+
function assertTelegramDirectDeliveryAllowed(canSendDirect) {
|
|
174
|
+
if (canSendDirect?.())
|
|
175
|
+
return;
|
|
176
|
+
throw new Error("Telegram direct delivery requires this Pi instance to own /telegram-connect or be registered with the Telegram multi-instance bus");
|
|
177
|
+
}
|
|
178
|
+
function resolveTelegramOutboundTarget(options) {
|
|
179
|
+
if (options.target)
|
|
180
|
+
return { chatId: options.target.chatId, target: options.target };
|
|
181
|
+
if (options.chatId !== undefined) {
|
|
182
|
+
return {
|
|
183
|
+
chatId: options.chatId,
|
|
184
|
+
target: options.threadId !== undefined
|
|
185
|
+
? { chatId: options.chatId, threadId: options.threadId }
|
|
186
|
+
: undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const defaultTarget = options.getDefaultTarget?.();
|
|
190
|
+
if (defaultTarget) {
|
|
191
|
+
return { chatId: defaultTarget.chatId, target: defaultTarget };
|
|
192
|
+
}
|
|
193
|
+
const defaultChatId = options.getDefaultChatId?.();
|
|
194
|
+
if (defaultChatId === undefined) {
|
|
195
|
+
throw new Error("Telegram chat_id is required when no paired/default Telegram chat is available");
|
|
196
|
+
}
|
|
197
|
+
return { chatId: defaultChatId };
|
|
198
|
+
}
|
|
199
|
+
async function buildTelegramOutboundAttachmentViews(options) {
|
|
200
|
+
const pendingAttachments = [];
|
|
201
|
+
for (const inputPath of options.paths) {
|
|
202
|
+
const stats = await (options.statPath ?? stat)(inputPath);
|
|
203
|
+
if (!stats.isFile()) {
|
|
204
|
+
throw new Error(`Not a file: ${inputPath}`);
|
|
205
|
+
}
|
|
206
|
+
if (options.maxAttachmentSizeBytes !== undefined &&
|
|
207
|
+
stats.size !== undefined &&
|
|
208
|
+
stats.size > options.maxAttachmentSizeBytes) {
|
|
209
|
+
throw new Error(formatTelegramOutboundAttachmentSizeLimitError(stats.size, options.maxAttachmentSizeBytes, inputPath));
|
|
210
|
+
}
|
|
211
|
+
pendingAttachments.push({
|
|
212
|
+
path: inputPath,
|
|
213
|
+
fileName: basename(inputPath),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return pendingAttachments;
|
|
217
|
+
}
|
|
218
|
+
function formatTelegramOutboundToolError(error) {
|
|
219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
220
|
+
return new Error(`\n${message.replace(/^\n+/u, "") || "Telegram outbound operation failed."}`);
|
|
221
|
+
}
|
|
222
|
+
export function registerTelegramOutboundAttachmentTool(pi, deps) {
|
|
223
|
+
const maxAttachmentsPerTurn = deps.maxAttachmentsPerTurn ?? MAX_ATTACHMENTS_PER_TURN;
|
|
224
|
+
const maxAttachmentSizeBytes = deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES;
|
|
225
|
+
pi.registerTool({
|
|
226
|
+
name: "telegram_attach",
|
|
227
|
+
label: "Telegram Attach",
|
|
228
|
+
description: "Queue one or more local files for the active Telegram reply, or send them immediately to Telegram when no Telegram turn is active.",
|
|
229
|
+
promptSnippet: TELEGRAM_ATTACH_PROMPT_SNIPPET,
|
|
230
|
+
promptGuidelines: [...TELEGRAM_ATTACH_PROMPT_GUIDELINES],
|
|
231
|
+
parameters: Type.Object({
|
|
232
|
+
paths: Type.Array(Type.String({ description: "Local file path to attach" }), { minItems: 1, maxItems: maxAttachmentsPerTurn }),
|
|
233
|
+
chat_id: Type.Optional(Type.Number({
|
|
234
|
+
description: "Optional Telegram chat id for immediate delivery when no Telegram turn is active",
|
|
235
|
+
})),
|
|
236
|
+
thread_id: Type.Optional(Type.Number({
|
|
237
|
+
description: "Optional Telegram topic thread id for immediate delivery with chat_id",
|
|
238
|
+
})),
|
|
239
|
+
caption: Type.Optional(Type.String({
|
|
240
|
+
description: "Optional caption for immediate delivery; ignored when queued for an active turn",
|
|
241
|
+
})),
|
|
242
|
+
}),
|
|
243
|
+
async execute(_toolCallId, params) {
|
|
244
|
+
try {
|
|
245
|
+
return await queueTelegramOutboundAttachments({
|
|
246
|
+
activeTurn: deps.getActiveTurn(),
|
|
247
|
+
paths: params.paths,
|
|
248
|
+
chatId: params.chat_id,
|
|
249
|
+
threadId: params.thread_id,
|
|
250
|
+
caption: params.caption,
|
|
251
|
+
maxAttachmentsPerTurn,
|
|
252
|
+
maxAttachmentSizeBytes,
|
|
253
|
+
sendMultipart: deps.sendMultipart,
|
|
254
|
+
getDefaultChatId: deps.getDefaultChatId,
|
|
255
|
+
getDefaultTarget: deps.getDefaultTarget,
|
|
256
|
+
canSendDirect: deps.canSendDirect,
|
|
257
|
+
statPath: deps.statPath,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
deps.recordRuntimeEvent?.("attachment", error, {
|
|
262
|
+
phase: "queue",
|
|
263
|
+
count: params.paths.length,
|
|
264
|
+
});
|
|
265
|
+
throw formatTelegramOutboundToolError(error);
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
export function registerTelegramOutboundMessageTool(pi, deps) {
|
|
271
|
+
pi.registerTool({
|
|
272
|
+
name: "telegram_message",
|
|
273
|
+
label: "Telegram Message",
|
|
274
|
+
description: "Send Markdown text directly to the paired/default Telegram chat, an exact channel chat_id, or an explicit live target. Channel delivery supports one optional local photo or video upload with the text as its caption. Channel posting requires Telegram-granted bot permission. Hidden telegram_button comments become inline prompt buttons.",
|
|
275
|
+
promptSnippet: TELEGRAM_MESSAGE_PROMPT_SNIPPET,
|
|
276
|
+
promptGuidelines: [...TELEGRAM_MESSAGE_PROMPT_GUIDELINES],
|
|
277
|
+
parameters: Type.Object({
|
|
278
|
+
text: Type.String({ description: "Message text to send" }),
|
|
279
|
+
media: Type.Optional(Type.String({
|
|
280
|
+
minLength: 1,
|
|
281
|
+
description: "Local single-file channel upload: .jpg/.jpeg/.png/.webp photo or .mp4 video; the text becomes its caption (max 1024 characters). Albums and other media types are rejected.",
|
|
282
|
+
})),
|
|
283
|
+
chat_id: Type.Optional(Type.Union([
|
|
284
|
+
Type.Number(),
|
|
285
|
+
Type.String({ pattern: "^@[A-Za-z0-9_]{5,32}$" }),
|
|
286
|
+
], { description: "Optional exact Telegram chat id or public channel @username" })),
|
|
287
|
+
thread_id: Type.Optional(Type.Number({
|
|
288
|
+
description: "Optional Telegram topic thread id with chat_id",
|
|
289
|
+
})),
|
|
290
|
+
channel: Type.Optional(Type.Boolean({
|
|
291
|
+
description: "Set true only when a numeric chat_id is explicitly intended as a channel",
|
|
292
|
+
})),
|
|
293
|
+
thread: Type.Optional(Type.Union([Type.String(), Type.Number()], {
|
|
294
|
+
description: "Optional live Pi thread name or numeric id for visible delivery plus a target-agent turn",
|
|
295
|
+
})),
|
|
296
|
+
}),
|
|
297
|
+
async execute(toolCallId, params) {
|
|
298
|
+
try {
|
|
299
|
+
return await sendTelegramOutboundMessage({
|
|
300
|
+
text: params.text,
|
|
301
|
+
media: params.media,
|
|
302
|
+
operationId: toolCallId,
|
|
303
|
+
channel: params.channel,
|
|
304
|
+
chatId: params.chat_id,
|
|
305
|
+
threadId: params.thread_id,
|
|
306
|
+
agentThread: params.thread,
|
|
307
|
+
getDefaultChatId: deps.getDefaultChatId,
|
|
308
|
+
getDefaultTarget: deps.getDefaultTarget,
|
|
309
|
+
getActiveTurn: deps.getActiveTurn,
|
|
310
|
+
resolveAgentTarget: deps.resolveAgentTarget,
|
|
311
|
+
routeAgentMessage: deps.routeAgentMessage,
|
|
312
|
+
canSendDirect: deps.canSendDirect,
|
|
313
|
+
planMessage: deps.planMessage,
|
|
314
|
+
sendMarkdownMessage: deps.sendMarkdownMessage,
|
|
315
|
+
sendChannelMarkdownMessage: deps.sendChannelMarkdownMessage,
|
|
316
|
+
sendChannelMediaMessage: deps.sendChannelMediaMessage,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
const isChannelTarget = typeof params.chat_id === "string" || params.channel === true;
|
|
321
|
+
const reportableError = isChannelTarget &&
|
|
322
|
+
!isTelegramChannelPostValidationError(error)
|
|
323
|
+
? new Error("Telegram channel publication failed; inspect the retained local record before retrying.")
|
|
324
|
+
: error;
|
|
325
|
+
deps.recordRuntimeEvent?.("message", reportableError, { phase: "direct" });
|
|
326
|
+
throw formatTelegramOutboundToolError(reportableError);
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
export async function queueTelegramOutboundAttachments(options) {
|
|
332
|
+
if (!options.activeTurn) {
|
|
333
|
+
if (!options.sendMultipart) {
|
|
334
|
+
throw new Error("telegram_attach can only queue files while replying to an active Telegram turn; provide Telegram send ports for immediate delivery");
|
|
335
|
+
}
|
|
336
|
+
return sendTelegramOutboundFiles({
|
|
337
|
+
paths: options.paths,
|
|
338
|
+
chatId: options.chatId,
|
|
339
|
+
threadId: options.threadId,
|
|
340
|
+
caption: options.caption,
|
|
341
|
+
maxAttachmentsPerTurn: options.maxAttachmentsPerTurn,
|
|
342
|
+
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
343
|
+
sendMultipart: options.sendMultipart,
|
|
344
|
+
getDefaultChatId: options.getDefaultChatId,
|
|
345
|
+
getDefaultTarget: options.getDefaultTarget,
|
|
346
|
+
canSendDirect: options.canSendDirect,
|
|
347
|
+
statPath: options.statPath,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (options.activeTurn.guestQueryId &&
|
|
351
|
+
options.activeTurn.queuedAttachments.length + options.paths.length > 1) {
|
|
352
|
+
throw new Error("Telegram Guest Mode supports one attachment per reply; no attachment was queued");
|
|
353
|
+
}
|
|
354
|
+
if (options.activeTurn.queuedAttachments.length + options.paths.length >
|
|
355
|
+
options.maxAttachmentsPerTurn) {
|
|
356
|
+
throw new Error(`Attachment limit reached (${options.maxAttachmentsPerTurn})`);
|
|
357
|
+
}
|
|
358
|
+
const pendingAttachments = await buildTelegramOutboundAttachmentViews({
|
|
359
|
+
paths: options.paths,
|
|
360
|
+
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
361
|
+
statPath: options.statPath,
|
|
362
|
+
});
|
|
363
|
+
options.activeTurn.queuedAttachments.push(...pendingAttachments);
|
|
364
|
+
const added = pendingAttachments.map((attachment) => attachment.path);
|
|
365
|
+
return {
|
|
366
|
+
content: [
|
|
367
|
+
{
|
|
368
|
+
type: "text",
|
|
369
|
+
text: formatTelegramOutboundAttachmentToolResultText(added.length),
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
details: { paths: added },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
export async function deliverTelegramGuestCachedAttachment(options) {
|
|
376
|
+
const transport = getTelegramGuestAttachmentTransport(options.attachment.path);
|
|
377
|
+
let stagingMessageId;
|
|
378
|
+
let answerAttempted = false;
|
|
379
|
+
try {
|
|
380
|
+
const message = (await options.sendMultipart(transport.method, {
|
|
381
|
+
chat_id: String(options.stagingChatId),
|
|
382
|
+
...getTelegramMultipartTargetFields(options.stagingTarget),
|
|
383
|
+
}, transport.fileField, options.attachment.path, options.attachment.fileName));
|
|
384
|
+
stagingMessageId = message.message_id;
|
|
385
|
+
const caption = options.caption
|
|
386
|
+
? Array.from(options.caption).slice(0, 1024).join("")
|
|
387
|
+
: undefined;
|
|
388
|
+
let result;
|
|
389
|
+
if (transport.fileField === "photo") {
|
|
390
|
+
const photo = [...(message.photo ?? [])]
|
|
391
|
+
.sort((left, right) => (left.file_size ?? 0) - (right.file_size ?? 0))
|
|
392
|
+
.at(-1);
|
|
393
|
+
if (!photo?.file_id)
|
|
394
|
+
throw new Error("Guest staging upload returned no photo file_id");
|
|
395
|
+
result = {
|
|
396
|
+
type: "photo",
|
|
397
|
+
id: "attachment-1",
|
|
398
|
+
photo_file_id: photo.file_id,
|
|
399
|
+
...(caption ? { caption } : {}),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
else if (transport.fileField === "audio") {
|
|
403
|
+
if (!message.audio?.file_id)
|
|
404
|
+
throw new Error("Guest staging upload returned no audio file_id");
|
|
405
|
+
result = {
|
|
406
|
+
type: "audio",
|
|
407
|
+
id: "attachment-1",
|
|
408
|
+
audio_file_id: message.audio.file_id,
|
|
409
|
+
...(caption ? { caption } : {}),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
else if (transport.fileField === "voice") {
|
|
413
|
+
if (!message.voice?.file_id)
|
|
414
|
+
throw new Error("Guest staging upload returned no voice file_id");
|
|
415
|
+
result = {
|
|
416
|
+
type: "voice",
|
|
417
|
+
id: "attachment-1",
|
|
418
|
+
voice_file_id: message.voice.file_id,
|
|
419
|
+
title: options.attachment.fileName,
|
|
420
|
+
...(caption ? { caption } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
if (!message.document?.file_id)
|
|
425
|
+
throw new Error("Guest staging upload returned no document file_id");
|
|
426
|
+
result = {
|
|
427
|
+
type: "document",
|
|
428
|
+
id: "attachment-1",
|
|
429
|
+
title: options.attachment.fileName,
|
|
430
|
+
document_file_id: message.document.file_id,
|
|
431
|
+
...(caption ? { caption } : {}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
answerAttempted = true;
|
|
435
|
+
await options.answerGuestQuery(options.guestQueryId, result);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
if (!answerAttempted &&
|
|
439
|
+
options.answerGuestText &&
|
|
440
|
+
options.fallbackText) {
|
|
441
|
+
answerAttempted = true;
|
|
442
|
+
await options.answerGuestText(options.guestQueryId, options.fallbackText);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
if (stagingMessageId !== undefined) {
|
|
450
|
+
try {
|
|
451
|
+
await options.deleteMessage(options.stagingChatId, stagingMessageId);
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
options.recordRuntimeEvent?.("attachment", error, {
|
|
455
|
+
phase: "guest-staging-cleanup",
|
|
456
|
+
chatId: options.stagingChatId,
|
|
457
|
+
messageId: stagingMessageId,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
export async function sendTelegramOutboundMessage(options) {
|
|
464
|
+
assertTelegramDirectDeliveryAllowed(options.canSendDirect);
|
|
465
|
+
const activeTurn = options.getActiveTurn?.();
|
|
466
|
+
if (typeof options.chatId === "string" || options.channel === true) {
|
|
467
|
+
if (!((typeof options.chatId === "string" && /^@[A-Za-z0-9_]{5,32}$/u.test(options.chatId)) ||
|
|
468
|
+
(typeof options.chatId === "number" && Number.isSafeInteger(options.chatId) && options.chatId < 0)) ||
|
|
469
|
+
options.threadId !== undefined ||
|
|
470
|
+
options.target !== undefined || options.agentThread !== undefined) {
|
|
471
|
+
throw new Error("Telegram channel delivery requires one exact @username or negative numeric channel ID without a thread target.");
|
|
472
|
+
}
|
|
473
|
+
const plan = options.planMessage(options.text);
|
|
474
|
+
if (options.media !== undefined) {
|
|
475
|
+
if (!options.sendChannelMediaMessage || !options.operationId) {
|
|
476
|
+
throw new TelegramChannelPostValidationError("Telegram channel media delivery requires direct leader transport ownership and operation identity.");
|
|
477
|
+
}
|
|
478
|
+
const messageId = await options.sendChannelMediaMessage(options.chatId, options.media, plan.markdown, { operationId: options.operationId, replyMarkup: plan.replyMarkup });
|
|
479
|
+
return { content: [{ type: "text",
|
|
480
|
+
text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
|
|
481
|
+
details: { chatId: options.chatId, messageId } };
|
|
482
|
+
}
|
|
483
|
+
if (!options.sendChannelMarkdownMessage || !options.operationId) {
|
|
484
|
+
throw new Error("Telegram channel delivery requires direct leader transport ownership and operation identity.");
|
|
485
|
+
}
|
|
486
|
+
const messageId = await options.sendChannelMarkdownMessage(options.chatId, plan.markdown, { operationId: options.operationId,
|
|
487
|
+
replyMarkup: plan.replyMarkup });
|
|
488
|
+
return { content: [{ type: "text",
|
|
489
|
+
text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
|
|
490
|
+
details: { chatId: options.chatId, messageId } };
|
|
491
|
+
}
|
|
492
|
+
if (options.media !== undefined) {
|
|
493
|
+
throw new TelegramChannelPostValidationError("telegram_message media uploads require channel delivery with an exact @username or negative numeric channel ID and channel: true.");
|
|
494
|
+
}
|
|
495
|
+
const requestedAgentSelector = options.agentThread !== undefined
|
|
496
|
+
? typeof options.agentThread === "number"
|
|
497
|
+
? { chatId: options.chatId, threadId: options.agentThread }
|
|
498
|
+
: { chatId: options.chatId, threadName: options.agentThread }
|
|
499
|
+
: activeTurn &&
|
|
500
|
+
options.resolveAgentTarget &&
|
|
501
|
+
options.routeAgentMessage &&
|
|
502
|
+
((options.target?.threadId !== undefined) ||
|
|
503
|
+
(options.chatId !== undefined && options.threadId !== undefined))
|
|
504
|
+
? {
|
|
505
|
+
chatId: options.target?.chatId ?? options.chatId,
|
|
506
|
+
threadId: options.target?.threadId ?? options.threadId,
|
|
507
|
+
}
|
|
508
|
+
: undefined;
|
|
509
|
+
let agentTarget;
|
|
510
|
+
if (requestedAgentSelector) {
|
|
511
|
+
if (!options.resolveAgentTarget || !options.routeAgentMessage) {
|
|
512
|
+
throw new Error("Telegram agent turn routing is unavailable.");
|
|
513
|
+
}
|
|
514
|
+
agentTarget = await options.resolveAgentTarget(requestedAgentSelector);
|
|
515
|
+
}
|
|
516
|
+
const { chatId, target } = resolveTelegramOutboundTarget({
|
|
517
|
+
chatId: agentTarget?.chatId ?? options.chatId,
|
|
518
|
+
threadId: agentTarget?.threadId ?? options.threadId,
|
|
519
|
+
target: agentTarget ?? options.target,
|
|
520
|
+
getDefaultChatId: options.getDefaultChatId,
|
|
521
|
+
getDefaultTarget: options.getDefaultTarget,
|
|
522
|
+
});
|
|
523
|
+
if (activeTurn) {
|
|
524
|
+
const hasExplicitTarget = options.chatId !== undefined ||
|
|
525
|
+
options.target !== undefined ||
|
|
526
|
+
options.agentThread !== undefined;
|
|
527
|
+
const activeTarget = activeTurn.target ?? { chatId: activeTurn.chatId };
|
|
528
|
+
const requestedTarget = target ?? { chatId };
|
|
529
|
+
const targetsMatch = activeTarget.chatId === requestedTarget.chatId &&
|
|
530
|
+
activeTarget.threadId === requestedTarget.threadId;
|
|
531
|
+
if (!hasExplicitTarget || targetsMatch) {
|
|
532
|
+
throw new Error("telegram_message cannot send directly to the active Telegram turn target; return the text normally so the bridge can deliver it once");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const plan = options.planMessage(options.text);
|
|
536
|
+
const messageId = await options.sendMarkdownMessage(chatId, plan.markdown, {
|
|
537
|
+
replyMarkup: plan.replyMarkup,
|
|
538
|
+
target,
|
|
539
|
+
});
|
|
540
|
+
if (agentTarget) {
|
|
541
|
+
if (messageId === undefined) {
|
|
542
|
+
throw new Error("Telegram message was sent but no message id was returned for agent routing.");
|
|
543
|
+
}
|
|
544
|
+
await options.routeAgentMessage({
|
|
545
|
+
target: agentTarget,
|
|
546
|
+
messageId,
|
|
547
|
+
text: options.text,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return {
|
|
551
|
+
content: [
|
|
552
|
+
{
|
|
553
|
+
type: "text",
|
|
554
|
+
text: formatTelegramOutboundMessageToolResultText(chatId),
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
details: { chatId, messageId },
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
export async function sendTelegramOutboundFiles(options) {
|
|
561
|
+
assertTelegramDirectDeliveryAllowed(options.canSendDirect);
|
|
562
|
+
if (options.paths.length > options.maxAttachmentsPerTurn) {
|
|
563
|
+
throw new Error(`Attachment limit reached (${options.maxAttachmentsPerTurn})`);
|
|
564
|
+
}
|
|
565
|
+
const { chatId, target } = resolveTelegramOutboundTarget({
|
|
566
|
+
chatId: options.chatId,
|
|
567
|
+
threadId: options.threadId,
|
|
568
|
+
target: options.target,
|
|
569
|
+
getDefaultChatId: options.getDefaultChatId,
|
|
570
|
+
getDefaultTarget: options.getDefaultTarget,
|
|
571
|
+
});
|
|
572
|
+
const pendingAttachments = await buildTelegramOutboundAttachmentViews({
|
|
573
|
+
paths: options.paths,
|
|
574
|
+
maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
|
|
575
|
+
statPath: options.statPath,
|
|
576
|
+
});
|
|
577
|
+
for (const [index, attachment] of pendingAttachments.entries()) {
|
|
578
|
+
const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
|
|
579
|
+
const method = isPhoto ? "sendPhoto" : "sendDocument";
|
|
580
|
+
const fieldName = isPhoto ? "photo" : "document";
|
|
581
|
+
await options.sendMultipart(method, {
|
|
582
|
+
chat_id: String(chatId),
|
|
583
|
+
...(options.caption && index === 0 ? { caption: options.caption } : {}),
|
|
584
|
+
...getTelegramMultipartTargetFields(target),
|
|
585
|
+
}, fieldName, attachment.path, attachment.fileName);
|
|
586
|
+
}
|
|
587
|
+
const added = pendingAttachments.map((attachment) => attachment.path);
|
|
588
|
+
return {
|
|
589
|
+
content: [
|
|
590
|
+
{
|
|
591
|
+
type: "text",
|
|
592
|
+
text: formatTelegramOutboundAttachmentToolResultText(added.length, "sent"),
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
details: { paths: added, chatId },
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
export function createTelegramQueuedOutboundAttachmentSender(deps) {
|
|
599
|
+
return async (turn, options) => {
|
|
600
|
+
await sendQueuedTelegramOutboundAttachments(turn, {
|
|
601
|
+
...deps,
|
|
602
|
+
isDeliveryActive: () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false,
|
|
603
|
+
maxAttachmentSizeBytes: deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
|
|
604
|
+
});
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
export async function sendQueuedTelegramOutboundAttachments(turn, deps) {
|
|
608
|
+
for (const attachment of turn.queuedAttachments) {
|
|
609
|
+
if (deps.isDeliveryActive?.() === false)
|
|
610
|
+
return;
|
|
611
|
+
try {
|
|
612
|
+
if (deps.maxAttachmentSizeBytes !== undefined) {
|
|
613
|
+
const stats = await (deps.statPath ?? stat)(attachment.path);
|
|
614
|
+
if (deps.isDeliveryActive?.() === false)
|
|
615
|
+
return;
|
|
616
|
+
if (stats.size > deps.maxAttachmentSizeBytes) {
|
|
617
|
+
throw new Error(formatTelegramOutboundAttachmentSizeLimitError(stats.size, deps.maxAttachmentSizeBytes));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
|
|
621
|
+
const method = isPhoto ? "sendPhoto" : "sendDocument";
|
|
622
|
+
const fieldName = isPhoto ? "photo" : "document";
|
|
623
|
+
await withTelegramReplyParameters(turn.chatId, turn.replyToMessageId, turn.target, (replyParameters) => deps.sendMultipart(method, {
|
|
624
|
+
chat_id: String(turn.chatId),
|
|
625
|
+
...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
|
|
626
|
+
...getTelegramMultipartTargetFields(turn.target),
|
|
627
|
+
}, fieldName, attachment.path, attachment.fileName));
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
if (deps.isDeliveryActive?.() === false)
|
|
631
|
+
return;
|
|
632
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
+
deps.recordRuntimeEvent?.("attachment", error, {
|
|
634
|
+
fileName: attachment.fileName,
|
|
635
|
+
});
|
|
636
|
+
await deps.sendTextReply(turn.chatId, turn.replyToMessageId, `Failed to send attachment ${attachment.fileName}: ${message}`, { target: turn.target });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|