@segmentstream/agent 0.6.10 → 0.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -8
- package/dist/_chunks/inbound-request-DgyI7oR9.mjs +333 -0
- package/dist/extension/config.d.ts +7 -8
- package/dist/extension/config.mjs +20 -2
- package/dist/extension/instructions.md +4 -0
- package/dist/extension/lib/conversations.d.ts +9 -1
- package/dist/extension/lib/conversations.mjs +5 -5
- package/dist/extension/lib/telegram/adapter.mjs +25 -7
- package/dist/extension/lib/telegram/attachments-send.mjs +1 -1
- package/dist/extension/lib/telegram/audience.d.ts +5 -1
- package/dist/extension/lib/telegram/audience.mjs +15 -2
- package/dist/extension/lib/telegram/chat-post-card.d.ts +2 -1
- package/dist/extension/lib/telegram/chat-post-card.mjs +17 -4
- package/dist/extension/lib/telegram/confirmation-text.d.ts +1 -0
- package/dist/extension/lib/telegram/confirmations.mjs +7 -6
- package/dist/extension/lib/telegram/content.d.ts +1 -1
- package/dist/extension/lib/telegram/conversations.d.ts +2 -2
- package/dist/extension/lib/telegram/conversations.mjs +110 -26
- package/dist/extension/lib/telegram/customer-text.d.ts +3 -2
- package/dist/extension/lib/telegram/customer-text.mjs +1 -217
- package/dist/extension/lib/telegram/files.mjs +1 -1
- package/dist/extension/lib/telegram/images.mjs +1 -1
- package/dist/extension/lib/telegram/inbound-request.d.ts +7 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +1 -74
- package/dist/extension/lib/telegram/inbound.mjs +15 -1
- package/dist/extension/lib/telegram/media-policy.mjs +1 -1
- package/dist/extension/lib/telegram/router.d.ts +2 -0
- package/dist/extension/lib/telegram/router.mjs +4 -3
- package/dist/extension/lib/telegram/silent.mjs +1 -1
- package/dist/extension/lib/telegram/turn-errors.d.ts +1 -1
- package/dist/extension/lib/telegram/turn-errors.mjs +9 -1
- package/dist/extension/lib/telegram/voice.mjs +1 -1
- package/package.json +2 -1
|
@@ -4,16 +4,24 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { telegramMarkdownLinks, telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
|
|
7
8
|
import { escapeMarkdownV2 } from "@chat-adapter/telegram";
|
|
8
9
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
10
|
const MAX_PREVIEW_LENGTH = 3584;
|
|
10
11
|
const exactText = new AsyncLocalStorage();
|
|
11
|
-
function withChatPostCard(text, run) {
|
|
12
|
+
function withChatPostCard(text, run, links = []) {
|
|
12
13
|
if (text.length > 4096) throw new Error("The complete chat confirmation cannot fit in one Telegram message.");
|
|
13
|
-
return exactText.run(
|
|
14
|
+
return exactText.run({
|
|
15
|
+
text,
|
|
16
|
+
links
|
|
17
|
+
}, run);
|
|
14
18
|
}
|
|
15
19
|
function isExactChatPostCard(text) {
|
|
16
|
-
return exactText.getStore() === text;
|
|
20
|
+
return exactText.getStore()?.text === text;
|
|
21
|
+
}
|
|
22
|
+
function chatPostCardLinks(text) {
|
|
23
|
+
const card = exactText.getStore();
|
|
24
|
+
return card?.text === text ? card.links : void 0;
|
|
17
25
|
}
|
|
18
26
|
function chatPostCopy(locale) {
|
|
19
27
|
return locale.startsWith("ru") ? {
|
|
@@ -43,6 +51,11 @@ function chatPostCard(preview, locale) {
|
|
|
43
51
|
if (text.length > MAX_PREVIEW_LENGTH) throw new Error("The full message and receipt cannot fit in one Telegram confirmation. Make a shorter draft.");
|
|
44
52
|
return {
|
|
45
53
|
text,
|
|
54
|
+
previewLinks: [
|
|
55
|
+
...telegramTextLinks(`${preview.title}\n${preview.replyTo ?? ""}`),
|
|
56
|
+
...telegramMarkdownLinks(preview.text),
|
|
57
|
+
...telegramTextLinks(preview.attribution)
|
|
58
|
+
],
|
|
46
59
|
confirm: russian ? "Отправить" : "Confirm",
|
|
47
60
|
cancel: russian ? "Отмена" : "Cancel",
|
|
48
61
|
executing: russian ? "Подтверждено." : "Confirmed.",
|
|
@@ -65,4 +78,4 @@ function chatPostReceipt(output, locale) {
|
|
|
65
78
|
...value.history_recorded ? [] : [copy.history]
|
|
66
79
|
].join("\n");
|
|
67
80
|
}
|
|
68
|
-
export { chatPostCard, chatPostCardStatus, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
|
|
81
|
+
export { chatPostCard, chatPostCardLinks, chatPostCardStatus, chatPostCopy, chatPostReceipt, isExactChatPostCard, withChatPostCard };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SessionAuthContext } from "eve/context";
|
|
2
2
|
import { type ConfirmationDescription } from "../confirmation-description.js";
|
|
3
3
|
export interface ConfirmationText {
|
|
4
|
+
readonly previewLinks?: readonly string[];
|
|
4
5
|
readonly text: string;
|
|
5
6
|
readonly confirm: string;
|
|
6
7
|
readonly cancel: string;
|
|
@@ -9,11 +9,11 @@ import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
|
|
|
9
9
|
import { chatPostCard, chatPostCardStatus, chatPostCopy, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
|
|
10
10
|
import { confirmations } from "../confirmations.mjs";
|
|
11
11
|
import { conversationEvent } from "../conversations.mjs";
|
|
12
|
-
import { currentInboundRequest } from "
|
|
13
|
-
import { currentLocale, failureText, reportTelegramError } from "./customer-text.mjs";
|
|
12
|
+
import { d as failureText, m as reportTelegramError, n as currentInboundRequest, o as currentLocale } from "../../../_chunks/inbound-request-DgyI7oR9.mjs";
|
|
14
13
|
import { answerMarker, isPendingQuestion, pendingQuestions, questionKey } from "../questions.mjs";
|
|
15
14
|
import { confirmationCopy, confirmationLocale, fallbackConfirmation } from "./confirmation-text.mjs";
|
|
16
15
|
import { formatAddress, scopedAddress } from "./sessions.mjs";
|
|
16
|
+
import { telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
|
|
17
17
|
import { registerTelegramFreeformPrompt, renderTelegramInputRequest } from "eve/channels/telegram";
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
const PREFIX = "ss-confirm:";
|
|
@@ -265,7 +265,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
265
265
|
}
|
|
266
266
|
]] }
|
|
267
267
|
});
|
|
268
|
-
const sent = await (exactPost ? withChatPostCard(text, send) : send());
|
|
268
|
+
const sent = await (exactPost ? withChatPostCard(text, send, copy.previewLinks) : send());
|
|
269
269
|
const state = channel.state;
|
|
270
270
|
state.confirmationMessages = {
|
|
271
271
|
...state.confirmationMessages,
|
|
@@ -275,7 +275,8 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
275
275
|
text,
|
|
276
276
|
copy,
|
|
277
277
|
locale: confirmationLocale(ctx.session.auth.current),
|
|
278
|
-
exactPost
|
|
278
|
+
exactPost,
|
|
279
|
+
previewLinks: copy.previewLinks
|
|
279
280
|
}
|
|
280
281
|
};
|
|
281
282
|
}
|
|
@@ -308,7 +309,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
308
309
|
replyMarkup: { inline_keyboard: [] },
|
|
309
310
|
text
|
|
310
311
|
});
|
|
311
|
-
await (message.exactPost ? withChatPostCard(text, edit) : edit());
|
|
312
|
+
await (message.exactPost ? withChatPostCard(text, edit, [...message.previewLinks ?? [], ...telegramTextLinks(status)]) : edit());
|
|
312
313
|
await record(callId, reason ?? resolution.outcome, text, resolution.outcome === "approved" ? "completed" : "cancelled", ctx);
|
|
313
314
|
const question = chatPostEditQuestion(resolution);
|
|
314
315
|
if (message.exactPost && reason === "edit" && question !== void 0) {
|
|
@@ -395,7 +396,7 @@ function telegramConfirmationEvents(render = async (description, locale) => fall
|
|
|
395
396
|
messageId: message.messageId,
|
|
396
397
|
replyMarkup: { inline_keyboard: [] },
|
|
397
398
|
text
|
|
398
|
-
}));
|
|
399
|
+
}), [...message.previewLinks ?? [], ...telegramTextLinks(status)]);
|
|
399
400
|
await record(data.result.callId, "receipt", text, failed ? "failed" : object(data.result.kind === "tool-result" ? data.result.output : void 0).status === "sent" ? "completed" : "unknown", ctx);
|
|
400
401
|
} else if (failed) {
|
|
401
402
|
const errorId = reportTelegramError("confirmed_action_failed", data, { callId: data.result.callId });
|
|
@@ -17,7 +17,7 @@ export declare function telegramRecord(value: unknown): Readonly<Record<string,
|
|
|
17
17
|
export declare function messageContent(raw: Readonly<Record<string, unknown>>): MessageContent;
|
|
18
18
|
/** Only shape/size diagnostics: do not retain/log raw webhook bodies, URLs or credential values. */
|
|
19
19
|
export declare function contentShape(raw: Readonly<Record<string, unknown>>, content?: MessageContent): {
|
|
20
|
-
contentSource: "
|
|
20
|
+
contentSource: "none" | "text" | "caption" | "rich_message";
|
|
21
21
|
textType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
22
22
|
captionType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
23
23
|
hasRichMessage: boolean;
|
|
@@ -10,7 +10,7 @@ export declare function captureTelegramWebhook(input: {
|
|
|
10
10
|
readonly identities: AgentIdentities;
|
|
11
11
|
readonly observed: ObservedMessages;
|
|
12
12
|
readonly chats?: AgentChats;
|
|
13
|
-
readonly next: () => Promise<Response>;
|
|
13
|
+
readonly next: (request?: Request) => Promise<Response>;
|
|
14
14
|
}): Promise<Response>;
|
|
15
15
|
/** Additive inbound hook: the router keeps ownership of the reply policy. */
|
|
16
16
|
export declare function recordGateDecision(reason: string, decision?: "reply" | "silent"): void;
|
|
@@ -27,4 +27,4 @@ export declare function telegramConversationCapture(input: {
|
|
|
27
27
|
readonly observed: ObservedMessages;
|
|
28
28
|
readonly chats: AgentChats;
|
|
29
29
|
readonly verify: (request: Request) => boolean;
|
|
30
|
-
}): (request: Request, next: () => Promise<Response>) => Promise<Response>;
|
|
30
|
+
}): (request: Request, next: (request?: Request) => Promise<Response>) => Promise<Response>;
|
|
@@ -6,12 +6,11 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { collapseConversationEvents, conversationEvent } from "../conversations.mjs";
|
|
8
8
|
import { observedChatKey } from "./observed.mjs";
|
|
9
|
-
import { currentInboundRequest } from "
|
|
9
|
+
import { n as currentInboundRequest, s as currentThreadId } from "../../../_chunks/inbound-request-DgyI7oR9.mjs";
|
|
10
10
|
import { reportMembership } from "./membership.mjs";
|
|
11
|
-
import { currentThreadId } from "./customer-text.mjs";
|
|
12
11
|
import { contentShape, messageContent } from "./content.mjs";
|
|
13
12
|
import { parseTelegramUpdate } from "eve/channels/telegram";
|
|
14
|
-
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
15
14
|
const captures = new WeakMap();
|
|
16
15
|
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
17
16
|
const id = (value) => typeof value === "number" || typeof value === "string" ? String(value) : null;
|
|
@@ -124,21 +123,37 @@ async function captureTelegramWebhook(input) {
|
|
|
124
123
|
chatId: message.chat.id,
|
|
125
124
|
...message.messageThreadId === void 0 ? {} : { messageThreadId: message.messageThreadId }
|
|
126
125
|
});
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
126
|
+
const automated = message.from === void 0 || message.from.isBot || message.chat.type === "channel";
|
|
127
|
+
const history = automated ? void 0 : await input.archive.context({
|
|
128
|
+
provider: "telegram",
|
|
129
|
+
chatId: message.chat.id,
|
|
130
|
+
...event.threadId === null ? {} : { threadId: event.threadId },
|
|
131
|
+
limit: 100,
|
|
132
|
+
includeDecisions: true,
|
|
133
|
+
...lookup?.status === "paired" ? { participantId: lookup.member.id } : {}
|
|
134
|
+
});
|
|
135
|
+
const routing = automated ? [] : (await input.archive.context({
|
|
136
|
+
provider: "telegram",
|
|
137
|
+
chatId: message.chat.id,
|
|
138
|
+
messageId: message.messageId,
|
|
139
|
+
includeDecisions: true,
|
|
140
|
+
limit: 100,
|
|
141
|
+
...lookup?.status === "paired" ? { participantId: lookup.member.id } : {}
|
|
142
|
+
})).events.filter((earlier) => earlier.kind === "decision" && earlier.senderId === event.senderId);
|
|
143
|
+
const previousClaim = routing.find((earlier) => earlier.reason === "turn_claimed");
|
|
144
|
+
const admitted = routing.some((earlier) => earlier.reason === "turn_admitted");
|
|
145
|
+
const released = previousClaim !== void 0 && routing.some((earlier) => earlier.eventId === `${previousClaim.eventId}:released`);
|
|
146
|
+
const dispatched = routing.some((earlier) => earlier.reason === "turn_dispatch_started");
|
|
147
|
+
const reserved = previousClaim !== void 0 && !released && Date.now() - Date.parse(previousClaim.at) < 6e5;
|
|
148
|
+
const alreadyHandled = admitted || dispatched || reserved;
|
|
149
|
+
if (!automated && edited === void 0 && !alreadyHandled) for (const earlier of (input.observed.recent(chat).length === 0 ? collapseConversationEvents(history?.events ?? []) : []).filter((item) => item.kind === "inbound" && item.eventId !== event.eventId && !history?.events.some((decision) => decision.kind === "decision" && decision.messageId === item.messageId && decision.reason === "turn_admitted")).slice(0, 30).reverse()) input.observed.add(chat, {
|
|
150
|
+
author: earlier.senderName ?? earlier.senderId ?? "automated post",
|
|
151
|
+
text: earlier.text ?? "[attachment or service message]",
|
|
152
|
+
at: earlier.at,
|
|
153
|
+
...earlier.messageId === null ? {} : { messageId: earlier.messageId },
|
|
154
|
+
...earlier.senderId === null ? {} : { authorId: earlier.senderId }
|
|
155
|
+
});
|
|
156
|
+
else if (automated || edited !== void 0) {
|
|
142
157
|
if (message.chat.type === "group" || message.chat.type === "supergroup") await input.chats?.seen({
|
|
143
158
|
chatId: message.chat.id,
|
|
144
159
|
title: message.chat.title ?? ""
|
|
@@ -150,13 +165,64 @@ async function captureTelegramWebhook(input) {
|
|
|
150
165
|
messageId: message.messageId
|
|
151
166
|
});
|
|
152
167
|
capture.decision = "silent";
|
|
153
|
-
capture.reason =
|
|
168
|
+
capture.reason = automated ? "automated_or_channel_message" : alreadyHandled ? "message_already_handled" : "message_edit";
|
|
169
|
+
if (alreadyHandled) capture.note = "This message already started a turn; edits and retries do not repeat its task.";
|
|
170
|
+
}
|
|
171
|
+
if (alreadyHandled) {
|
|
172
|
+
capture.decision = "silent";
|
|
173
|
+
capture.reason = "message_already_handled";
|
|
174
|
+
}
|
|
175
|
+
if (inbound !== void 0) {
|
|
176
|
+
inbound.edited = edited !== void 0;
|
|
177
|
+
const nextClaim = previousClaim === void 0 ? void 0 : createHash("sha256").update(previousClaim.eventId).digest("hex");
|
|
178
|
+
const claimId = previousClaim === void 0 ? `telegram:${message.chat.id}:${message.messageId}:turn` : `telegram:${message.chat.id}:${message.messageId}:turn:${nextClaim}`;
|
|
179
|
+
let claimed = false;
|
|
180
|
+
let accepted = false;
|
|
181
|
+
let dispatchStarted = false;
|
|
182
|
+
const claimEvent = (reason, status, suffix = "") => ({
|
|
183
|
+
...event,
|
|
184
|
+
eventId: `${claimId}${suffix}`,
|
|
185
|
+
kind: "decision",
|
|
186
|
+
decision: "reply",
|
|
187
|
+
reason,
|
|
188
|
+
status,
|
|
189
|
+
at: new Date().toISOString(),
|
|
190
|
+
text: null,
|
|
191
|
+
attachments: []
|
|
192
|
+
});
|
|
193
|
+
inbound.claimTurn = async () => {
|
|
194
|
+
if (alreadyHandled) return false;
|
|
195
|
+
claimed = await input.archive.append(claimEvent("turn_claimed", "requested")) !== false;
|
|
196
|
+
return claimed;
|
|
197
|
+
};
|
|
198
|
+
inbound.beginDispatch = async () => {
|
|
199
|
+
dispatchStarted = await input.archive.append({
|
|
200
|
+
...claimEvent("turn_dispatch_started", "requested"),
|
|
201
|
+
eventId: `telegram:${message.chat.id}:${message.messageId}:dispatch`
|
|
202
|
+
}) === true;
|
|
203
|
+
return dispatchStarted;
|
|
204
|
+
};
|
|
205
|
+
inbound.finishClaim = async (delivered) => {
|
|
206
|
+
if ((accepted || dispatchStarted) && !delivered) return;
|
|
207
|
+
accepted ||= delivered;
|
|
208
|
+
if (claimed) {
|
|
209
|
+
await input.archive.append(claimEvent(delivered ? "turn_admitted" : "turn_released", delivered ? "completed" : "failed", delivered ? ":admitted" : ":released"));
|
|
210
|
+
claimed = false;
|
|
211
|
+
}
|
|
212
|
+
};
|
|
154
213
|
}
|
|
155
214
|
let response;
|
|
156
215
|
try {
|
|
157
|
-
|
|
216
|
+
const routeEdit = edited !== void 0 && !automated && !alreadyHandled;
|
|
217
|
+
if (routeEdit) capture.reason = void 0;
|
|
218
|
+
response = await input.next(routeEdit ? new Request(input.request, { body: JSON.stringify({
|
|
219
|
+
...update,
|
|
220
|
+
edited_message: void 0,
|
|
221
|
+
message: messagePayload
|
|
222
|
+
}) }) : alreadyHandled ? new Request(input.request, { body: JSON.stringify({ update_id: update.update_id }) }) : void 0);
|
|
158
223
|
} catch (error) {
|
|
159
224
|
try {
|
|
225
|
+
await inbound?.finishClaim?.(false);
|
|
160
226
|
await input.archive.append({
|
|
161
227
|
...event,
|
|
162
228
|
eventId: `${event.eventId}:decision`,
|
|
@@ -174,6 +240,7 @@ async function captureTelegramWebhook(input) {
|
|
|
174
240
|
}
|
|
175
241
|
throw error;
|
|
176
242
|
}
|
|
243
|
+
await inbound?.finishClaim?.(false);
|
|
177
244
|
await input.archive.append({
|
|
178
245
|
...event,
|
|
179
246
|
eventId: `${event.eventId}:decision`,
|
|
@@ -192,6 +259,7 @@ function recordGateDecision(reason, decision = "silent") {
|
|
|
192
259
|
if (capture !== void 0) {
|
|
193
260
|
capture.reason = reason;
|
|
194
261
|
capture.decision = decision;
|
|
262
|
+
capture.note = void 0;
|
|
195
263
|
}
|
|
196
264
|
}
|
|
197
265
|
function recordRouteDecision(route, addressed, policy, audit) {
|
|
@@ -295,15 +363,31 @@ function conversationIdentities(identities) {
|
|
|
295
363
|
};
|
|
296
364
|
}
|
|
297
365
|
function telegramConversationCapture(input) {
|
|
366
|
+
const processing = new Map();
|
|
298
367
|
return async (request, next) => {
|
|
299
368
|
if (!input.verify(request)) return next();
|
|
300
369
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
370
|
+
const update = record(await request.clone().json().catch(() => null));
|
|
371
|
+
const payload = record(update.edited_message ?? update.message);
|
|
372
|
+
const chatId = id(record(payload.chat).id);
|
|
373
|
+
const messageId = id(payload.message_id);
|
|
374
|
+
const key = chatId === null || messageId === null ? void 0 : `${chatId}:${messageId}`;
|
|
375
|
+
const previous = key === void 0 ? void 0 : processing.get(key);
|
|
376
|
+
const task = (async () => {
|
|
377
|
+
await previous?.catch(() => void 0);
|
|
378
|
+
return captureTelegramWebhook({
|
|
379
|
+
...input,
|
|
380
|
+
request,
|
|
381
|
+
identities: input.identities(),
|
|
382
|
+
next
|
|
383
|
+
});
|
|
384
|
+
})();
|
|
385
|
+
if (key !== void 0) processing.set(key, task);
|
|
386
|
+
try {
|
|
387
|
+
return await task;
|
|
388
|
+
} finally {
|
|
389
|
+
if (key !== void 0 && processing.get(key) === task) processing.delete(key);
|
|
390
|
+
}
|
|
307
391
|
} catch (error) {
|
|
308
392
|
console.error("[segmentstream] conversation capture failed", { error });
|
|
309
393
|
return new Response("conversation storage unavailable", { status: 503 });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TelegramPreviewContext } from "@segmentstream-cloud/ontology/telegram";
|
|
1
2
|
import { type TelegramText } from "./format.js";
|
|
2
3
|
export declare function withDeliveryLocale<T>(locale: string, run: () => T, threadId?: string): T;
|
|
3
4
|
export declare function currentLocale(): string;
|
|
@@ -15,7 +16,7 @@ export declare function customerCopy(locale?: string): {
|
|
|
15
16
|
export declare function isTechnicalText(text: string): boolean;
|
|
16
17
|
/** Original failures stay in operator diagnostics, joined to the public notice by a short reference. */
|
|
17
18
|
export declare function reportTelegramError(operation: string, error: unknown, metadata?: Record<string, unknown>, secrets?: ReadonlySet<string>): string;
|
|
18
|
-
export declare function failureText(errorId: string, kind?: "failed" | "recovery" | "attachment" | "authorization" | "response", locale?: string): string;
|
|
19
|
+
export declare function failureText(errorId: string, kind?: "failed" | "cancelled" | "recovery" | "attachment" | "authorization" | "response", locale?: string): string;
|
|
19
20
|
export declare function customerText(text: string, locale?: string): string;
|
|
20
21
|
/** Check what Telegram will display, including hidden link destinations, after removing markup. */
|
|
21
22
|
export declare function customerMessage(text: string, locale?: string): TelegramText;
|
|
@@ -29,4 +30,4 @@ export declare class TelegramDeliveryError extends Error {
|
|
|
29
30
|
constructor(reference: string, delivery: "failed" | "unknown", messageIds: readonly number[]);
|
|
30
31
|
}
|
|
31
32
|
/** Covers framework defaults, edits and callback notices as well as ordinary agent replies. */
|
|
32
|
-
export declare function safeTelegramFetch(fetchImpl: typeof fetch, scope?: string): typeof fetch;
|
|
33
|
+
export declare function safeTelegramFetch(fetchImpl: typeof fetch, scope?: string, previews?: TelegramPreviewContext): typeof fetch;
|
|
@@ -4,221 +4,5 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import {
|
|
8
|
-
import { redactDiagnostic } from "../diagnostics.mjs";
|
|
9
|
-
import { currentInboundRequest } from "./inbound-request.mjs";
|
|
10
|
-
import { plainTelegramText, splitTelegramText, telegramMarkdown } from "./format.mjs";
|
|
11
|
-
import { inTelegramSendQueue } from "./send-queue.mjs";
|
|
12
|
-
import { randomBytes } from "node:crypto";
|
|
13
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
-
const deliveryLocale = new AsyncLocalStorage();
|
|
15
|
-
function withDeliveryLocale(locale, run, threadId) {
|
|
16
|
-
return deliveryLocale.run({
|
|
17
|
-
locale,
|
|
18
|
-
threadId
|
|
19
|
-
}, run);
|
|
20
|
-
}
|
|
21
|
-
function currentLocale() {
|
|
22
|
-
return deliveryLocale.getStore()?.locale ?? currentInboundRequest()?.locale ?? "en";
|
|
23
|
-
}
|
|
24
|
-
function currentThreadId() {
|
|
25
|
-
return deliveryLocale.getStore()?.threadId ?? currentInboundRequest()?.threadId;
|
|
26
|
-
}
|
|
27
|
-
function customerCopy(locale = currentLocale()) {
|
|
28
|
-
return locale.toLowerCase().split(/[-_]/)[0] === "ru" ? {
|
|
29
|
-
failed: "Не удалось выполнить запрос. Попробуйте ещё раз.",
|
|
30
|
-
recovery: "Не удалось продолжить разговор. Отправьте новое сообщение.",
|
|
31
|
-
attachment: "Не удалось обработать файл. Попробуйте ещё раз.",
|
|
32
|
-
authorization: "Не удалось подключиться. Попробуйте ещё раз.",
|
|
33
|
-
response: "Не удалось показать ответ. Попробуйте ещё раз.",
|
|
34
|
-
reference: "Код ошибки",
|
|
35
|
-
connected: "Доступ подключён.",
|
|
36
|
-
disconnected: "Подключение отменено."
|
|
37
|
-
} : {
|
|
38
|
-
failed: "I couldn't complete the request. Please try again.",
|
|
39
|
-
recovery: "I couldn't continue the conversation. Please send a new message.",
|
|
40
|
-
attachment: "I couldn't process the file. Please try again.",
|
|
41
|
-
authorization: "I couldn't connect. Please try again.",
|
|
42
|
-
response: "I couldn't display the response. Please try again.",
|
|
43
|
-
reference: "Error reference",
|
|
44
|
-
connected: "Access connected.",
|
|
45
|
-
disconnected: "Connection cancelled."
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
const TECHNICAL_PATTERNS = [
|
|
49
|
-
/[{}]|```/u,
|
|
50
|
-
/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/iu,
|
|
51
|
-
/\b[0-9a-hjkmnp-tv-z]{26}\b/iu,
|
|
52
|
-
/\b\w+__\w+/u,
|
|
53
|
-
/\b(?:Serialization|Type|Reference|Syntax|Tool|Provider|API|Workflow|InternalServer)Error\b/u,
|
|
54
|
-
/\bError:\s|(?:^|\n)\s*at\s+\S+|\b(?:stack trace|traceback|SQLSTATE|node_modules)\b/imu,
|
|
55
|
-
/\b(?:duplicate key|ECONN\w+|HTTP [45]\d\d)\b/iu,
|
|
56
|
-
/\[\s*(?:"[^"]*"|\d+|true|false|null)\s*[,\]]/u
|
|
57
|
-
];
|
|
58
|
-
function isTechnicalText(text) {
|
|
59
|
-
if (/https?:\/\/[^\s/@]+:[^\s/@]+@|[?&](?:token|access_token|api_key|secret|password)=/iu.test(text)) return true;
|
|
60
|
-
const visible = text.replace(/https?:\/\/[^\s)<>]+/giu, "link");
|
|
61
|
-
const prose = visible.replace(/(?<![\w@])@[a-z][a-z0-9_]{4,31}(?!\w)/giu, "recipient");
|
|
62
|
-
return TECHNICAL_PATTERNS.some((pattern) => pattern.test(visible)) || /\b[a-z][a-z0-9]*_[a-z][a-z0-9_]*\b(?!\.[a-z0-9])/iu.test(prose);
|
|
63
|
-
}
|
|
64
|
-
function redactCredentials(value) {
|
|
65
|
-
if (typeof value === "string") return value.replace(/\bBearer [A-Za-z0-9_.~+/-]+=*/giu, "Bearer [redacted]").replace(/(https?:\/\/)[^/@\s]+:[^/@\s]+@/gu, "$1[redacted]@").replace(/([?&](?:token|access_token|api_key|secret|password)=)[^&#\s]*/giu, "$1[redacted]").replace(/("(?:token|api_key|password|secret)"\s*:\s*)"(?:\\.|[^"\\])*"/giu, "$1\"[redacted]\"");
|
|
66
|
-
if (Array.isArray(value)) return value.map(redactCredentials);
|
|
67
|
-
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, /^(?:token|password|secret|authorization|cookie|api_?key|access_?token|refresh_?token)$/iu.test(key) && typeof item === "string" ? "[redacted]" : redactCredentials(item)]));
|
|
68
|
-
return value;
|
|
69
|
-
}
|
|
70
|
-
function reportTelegramError(operation, error, metadata = {}, secrets = new Set()) {
|
|
71
|
-
const errorId = randomBytes(4).toString("hex").toUpperCase();
|
|
72
|
-
const credentials = new Set([...secrets, ...[process.env.AI_GATEWAY_API_KEY, process.env.VERCEL_OIDC_TOKEN].filter((value) => value !== void 0)]);
|
|
73
|
-
console.error("[segmentstream] Telegram delivery diagnostic", {
|
|
74
|
-
...metadata,
|
|
75
|
-
errorId,
|
|
76
|
-
operation,
|
|
77
|
-
error: redactCredentials(redactDiagnostic(error, credentials))
|
|
78
|
-
});
|
|
79
|
-
return errorId;
|
|
80
|
-
}
|
|
81
|
-
function failureText(errorId, kind = "failed", locale = currentLocale()) {
|
|
82
|
-
const copy = customerCopy(locale);
|
|
83
|
-
return `${copy[kind]} ${copy.reference}: ${errorId}.`;
|
|
84
|
-
}
|
|
85
|
-
function customerText(text, locale = currentLocale()) {
|
|
86
|
-
if (!isTechnicalText(text)) return text;
|
|
87
|
-
return failureText(reportTelegramError("technical_text_blocked", { text }), "response", locale);
|
|
88
|
-
}
|
|
89
|
-
function customerMessage(text, locale = currentLocale()) {
|
|
90
|
-
let message;
|
|
91
|
-
try {
|
|
92
|
-
message = telegramMarkdown(text);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
reportTelegramError("markdown_render_failed", error);
|
|
95
|
-
message = {
|
|
96
|
-
text: plainTelegramText(text),
|
|
97
|
-
entities: []
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
const links = message.entities.flatMap((entity) => entity.url === void 0 ? [] : [entity.url]);
|
|
101
|
-
const visible = customerText(message.text, locale);
|
|
102
|
-
if (visible !== message.text) return {
|
|
103
|
-
text: visible,
|
|
104
|
-
entities: []
|
|
105
|
-
};
|
|
106
|
-
if (links.some(isTechnicalText)) return {
|
|
107
|
-
text: failureText(reportTelegramError("unsafe_markdown_link", new Error("Credential-bearing link refused.")), "response", locale),
|
|
108
|
-
entities: []
|
|
109
|
-
};
|
|
110
|
-
return message;
|
|
111
|
-
}
|
|
112
|
-
async function formattingRefused(response) {
|
|
113
|
-
if (response.status !== 400) return false;
|
|
114
|
-
const body = await response.clone().json().catch(() => null);
|
|
115
|
-
return body?.ok === false && typeof body.description === "string" && (/can't parse entities|cannot parse entities|entity (?:bounds|offset|length)|entities (?:are|too)/iu.test(body.description) || /unsupported (?:start tag|parse mode)/iu.test(body.description));
|
|
116
|
-
}
|
|
117
|
-
var TelegramDeliveryError = class extends Error {
|
|
118
|
-
reference;
|
|
119
|
-
delivery;
|
|
120
|
-
messageIds;
|
|
121
|
-
name = "TelegramDeliveryError";
|
|
122
|
-
constructor(reference, delivery, messageIds) {
|
|
123
|
-
super(`Telegram delivery ${delivery}; reference ${reference}. Do not automatically resend.`);
|
|
124
|
-
this.reference = reference;
|
|
125
|
-
this.delivery = delivery;
|
|
126
|
-
this.messageIds = messageIds;
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
function safeTelegramFetch(fetchImpl, scope = "telegram") {
|
|
130
|
-
const methods = new Set([
|
|
131
|
-
"sendMessage",
|
|
132
|
-
"editMessageText",
|
|
133
|
-
"editMessageCaption",
|
|
134
|
-
"answerCallbackQuery",
|
|
135
|
-
"sendPhoto",
|
|
136
|
-
"sendDocument",
|
|
137
|
-
"sendMediaGroup"
|
|
138
|
-
]);
|
|
139
|
-
return async (input, init) => {
|
|
140
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
141
|
-
if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
|
|
142
|
-
const body = JSON.parse(init.body);
|
|
143
|
-
for (const field of ["text", "caption"]) {
|
|
144
|
-
if (typeof body[field] === "string" && isExactChatPostCard(body[field])) body.parse_mode = "MarkdownV2";
|
|
145
|
-
if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) {
|
|
146
|
-
const entityField = field === "text" ? "entities" : "caption_entities";
|
|
147
|
-
const existing = body[entityField];
|
|
148
|
-
const message = Array.isArray(existing) ? {
|
|
149
|
-
text: body[field],
|
|
150
|
-
entities: existing
|
|
151
|
-
} : customerMessage(body[field]);
|
|
152
|
-
const guarded = customerText(message.text);
|
|
153
|
-
body[field] = message.entities.some((entity) => typeof entity.url === "string" && isTechnicalText(entity.url)) ? failureText(reportTelegramError("unsafe_entity_link", new Error("Credential-bearing link refused.")), "response") : guarded;
|
|
154
|
-
if (!url.endsWith("/answerCallbackQuery")) body[entityField] = body[field] === message.text ? message.entities : [];
|
|
155
|
-
delete body.parse_mode;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (Array.isArray(body.media)) body.media = body.media.map((item) => ({
|
|
159
|
-
...item,
|
|
160
|
-
...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
|
|
161
|
-
}));
|
|
162
|
-
const markup = body.reply_markup;
|
|
163
|
-
if (Array.isArray(markup?.inline_keyboard)) markup.inline_keyboard = markup.inline_keyboard.map((row) => row.map((button) => {
|
|
164
|
-
if (typeof button.text !== "string" || !isTechnicalText(button.text)) return button;
|
|
165
|
-
reportTelegramError("technical_button_label_blocked", { text: button.text });
|
|
166
|
-
return {
|
|
167
|
-
...button,
|
|
168
|
-
text: currentLocale().startsWith("ru") ? "Продолжить" : "Continue"
|
|
169
|
-
};
|
|
170
|
-
}));
|
|
171
|
-
const send = async () => {
|
|
172
|
-
const parts = url.endsWith("/sendMessage") && typeof body.text === "string" && !isExactChatPostCard(body.text) ? splitTelegramText({
|
|
173
|
-
text: body.text,
|
|
174
|
-
entities: body.entities ?? []
|
|
175
|
-
}) : [null];
|
|
176
|
-
let first;
|
|
177
|
-
const messageIds = [];
|
|
178
|
-
const attempt = async (outgoing) => {
|
|
179
|
-
try {
|
|
180
|
-
return await fetchImpl(input, {
|
|
181
|
-
...init,
|
|
182
|
-
body: JSON.stringify(outgoing)
|
|
183
|
-
});
|
|
184
|
-
} catch (error) {
|
|
185
|
-
throw new TelegramDeliveryError(reportTelegramError("telegram_send_unknown", error, { messageIds }), "unknown", messageIds);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
for (const [index, part] of parts.entries()) {
|
|
189
|
-
const outgoing = part === null ? { ...body } : {
|
|
190
|
-
...body,
|
|
191
|
-
text: part.text,
|
|
192
|
-
entities: part.entities
|
|
193
|
-
};
|
|
194
|
-
if (index < parts.length - 1) delete outgoing.reply_markup;
|
|
195
|
-
let response = await attempt(outgoing);
|
|
196
|
-
const entityField = typeof outgoing.text === "string" ? "entities" : "caption_entities";
|
|
197
|
-
if (Array.isArray(outgoing[entityField]) && outgoing[entityField].length > 0 && await formattingRefused(response)) {
|
|
198
|
-
delete outgoing[entityField];
|
|
199
|
-
delete outgoing.parse_mode;
|
|
200
|
-
response = await attempt(outgoing);
|
|
201
|
-
}
|
|
202
|
-
const result = await response.clone().json().catch(() => null);
|
|
203
|
-
if (url.endsWith("/sendMessage")) {
|
|
204
|
-
const id = result?.result?.message_id;
|
|
205
|
-
if (!response.ok || result?.ok !== true || typeof id !== "number" || !Number.isSafeInteger(id) || id < 1) {
|
|
206
|
-
const delivery = result?.ok === false ? "failed" : "unknown";
|
|
207
|
-
throw new TelegramDeliveryError(reportTelegramError("telegram_send_unacknowledged", {
|
|
208
|
-
status: response.status,
|
|
209
|
-
result,
|
|
210
|
-
messageIds,
|
|
211
|
-
delivery
|
|
212
|
-
}), delivery, messageIds);
|
|
213
|
-
}
|
|
214
|
-
messageIds.push(id);
|
|
215
|
-
} else if (!response.ok || result?.ok !== true) return response;
|
|
216
|
-
first ??= response;
|
|
217
|
-
}
|
|
218
|
-
if (first === void 0) throw new Error("Telegram message has no visible text; nothing was sent.");
|
|
219
|
-
return first;
|
|
220
|
-
};
|
|
221
|
-
return typeof body.chat_id === "string" || typeof body.chat_id === "number" ? inTelegramSendQueue(scope, String(body.chat_id), send) : send();
|
|
222
|
-
};
|
|
223
|
-
}
|
|
7
|
+
import { a as TelegramDeliveryError, c as customerCopy, d as failureText, f as formattingRefused, g as withDeliveryLocale, h as safeTelegramFetch, l as customerMessage, m as reportTelegramError, o as currentLocale, p as isTechnicalText, s as currentThreadId, u as customerText } from "../../../_chunks/inbound-request-DgyI7oR9.mjs";
|
|
224
8
|
export { TelegramDeliveryError, currentLocale, currentThreadId, customerCopy, customerMessage, customerText, failureText, formattingRefused, isTechnicalText, reportTelegramError, safeTelegramFetch, withDeliveryLocale };
|
|
@@ -6,7 +6,7 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { conversationLanguage } from "./principal.mjs";
|
|
8
8
|
import { MAXIMUM_ATTACHMENT_BYTES, attachmentUrl } from "../attachments.mjs";
|
|
9
|
-
import { failureText, reportTelegramError } from "
|
|
9
|
+
import { d as failureText, m as reportTelegramError } from "../../../_chunks/inbound-request-DgyI7oR9.mjs";
|
|
10
10
|
import { describableImage, describeReceivedImage } from "./images.mjs";
|
|
11
11
|
function telegramConversation(chatId) {
|
|
12
12
|
return `telegram:${chatId}`;
|
|
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import { reportTelegramError } from "
|
|
7
|
+
import { m as reportTelegramError } from "../../../_chunks/inbound-request-DgyI7oR9.mjs";
|
|
8
8
|
import { gateway, generateText } from "ai";
|
|
9
9
|
const MAXIMUM_IMAGE_DESCRIPTION_BYTES = 5242880;
|
|
10
10
|
const MAXIMUM_NATIVE_IMAGE_BYTES = 3145728;
|
|
@@ -25,6 +25,13 @@ export type StagedFile = {
|
|
|
25
25
|
export declare class InboundRequest {
|
|
26
26
|
/** The agent's description from the compiled manifest, for the routing decision. */
|
|
27
27
|
readonly agentDescription: () => Promise<string>;
|
|
28
|
+
edited: boolean;
|
|
29
|
+
deliveryFailed: ((error: unknown) => Promise<void>) | undefined;
|
|
30
|
+
/** Atomic per-message admission in the platform archive, after authentication and routing. */
|
|
31
|
+
claimTurn: (() => Promise<boolean>) | undefined;
|
|
32
|
+
/** Finalize the reservation only after the durable inbox acknowledges delivery. */
|
|
33
|
+
beginDispatch: (() => Promise<boolean>) | undefined;
|
|
34
|
+
finishClaim: ((accepted: boolean) => Promise<void>) | undefined;
|
|
28
35
|
locale: string | undefined;
|
|
29
36
|
threadId: string | undefined;
|
|
30
37
|
/** The text the turn should carry instead of the parsed message, once a transcript exists. */
|