@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
package/README.md
CHANGED
|
@@ -79,14 +79,16 @@ Both entry points export the same handle: `@segmentstream/agent` and
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
| Field
|
|
83
|
-
|
|
|
84
|
-
| `channels.telegram.connection`
|
|
85
|
-
| `channels.telegram.policy`
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `models.
|
|
89
|
-
| `
|
|
82
|
+
| Field | Default | Meaning |
|
|
83
|
+
| ---------------------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `channels.telegram.connection` | none | The Connect key whose credential is the bot token. Without `channels.telegram`, no Telegram. |
|
|
85
|
+
| `channels.telegram.policy` | `"smart"` | Unaddressed group text: `smart` asks the decision model, `mention` observes; attachments always trigger a turn |
|
|
86
|
+
| `channels.telegram.linkPreviews` | `all` | Keep native first-link previews; opt into `external` to preview external links only, or `none` to disable previews. Caller options take precedence. |
|
|
87
|
+
| `channels.telegram.additionalOwnOrigins` | `[]` | Additional exact HTTP(S) workspace origins, including local ports; no paths or credentials. |
|
|
88
|
+
| `models.stt` | `"spacexai/grok-stt"` | The AI Gateway transcription model for voice messages; `false` leaves them untranscribed |
|
|
89
|
+
| `models.imageDescription` | automatic | A separate vision model for ingestion descriptions; omitted prefers native images, `false` disables description fallback |
|
|
90
|
+
| `models.decision` | `"typesafe-ai/jev"` | The AI Gateway decision model of the inbound router |
|
|
91
|
+
| `ontology.connection` | `true` | Whether the agent reaches the ontology over MCP |
|
|
90
92
|
|
|
91
93
|
An unknown field or an invalid value fails the mount with `SegmentStreamAgentConfigError`, naming
|
|
92
94
|
each path. `manifestOf(config)` returns the normalized configuration, defaults applied, for the
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { chatPostCardLinks, isExactChatPostCard } from "../extension/lib/telegram/chat-post-card.mjs";
|
|
8
|
+
import { redactDiagnostic } from "../extension/lib/diagnostics.mjs";
|
|
9
|
+
import { plainTelegramText, splitTelegramText, telegramMarkdown } from "../extension/lib/telegram/format.mjs";
|
|
10
|
+
import { inTelegramSendQueue } from "../extension/lib/telegram/send-queue.mjs";
|
|
11
|
+
import { telegramPreviewOptions, telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
|
|
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
|
+
if (kind === "cancelled") return `⏹ ${errorId}`;
|
|
83
|
+
const copy = customerCopy(locale);
|
|
84
|
+
return `${copy[kind]} ${copy.reference}: ${errorId}.`;
|
|
85
|
+
}
|
|
86
|
+
function customerText(text, locale = currentLocale()) {
|
|
87
|
+
if (!isTechnicalText(text)) return text;
|
|
88
|
+
return failureText(reportTelegramError("technical_text_blocked", { text }), "response", locale);
|
|
89
|
+
}
|
|
90
|
+
function customerMessage(text, locale = currentLocale()) {
|
|
91
|
+
let message;
|
|
92
|
+
try {
|
|
93
|
+
message = telegramMarkdown(text);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
reportTelegramError("markdown_render_failed", error);
|
|
96
|
+
message = {
|
|
97
|
+
text: plainTelegramText(text),
|
|
98
|
+
entities: []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const links = message.entities.flatMap((entity) => entity.url === void 0 ? [] : [entity.url]);
|
|
102
|
+
const visible = customerText(message.text, locale);
|
|
103
|
+
if (visible !== message.text) return {
|
|
104
|
+
text: visible,
|
|
105
|
+
entities: []
|
|
106
|
+
};
|
|
107
|
+
if (links.some(isTechnicalText)) return {
|
|
108
|
+
text: failureText(reportTelegramError("unsafe_markdown_link", new Error("Credential-bearing link refused.")), "response", locale),
|
|
109
|
+
entities: []
|
|
110
|
+
};
|
|
111
|
+
return message;
|
|
112
|
+
}
|
|
113
|
+
async function formattingRefused(response) {
|
|
114
|
+
if (response.status !== 400) return false;
|
|
115
|
+
const body = await response.clone().json().catch(() => null);
|
|
116
|
+
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));
|
|
117
|
+
}
|
|
118
|
+
var TelegramDeliveryError = class extends Error {
|
|
119
|
+
reference;
|
|
120
|
+
delivery;
|
|
121
|
+
messageIds;
|
|
122
|
+
name = "TelegramDeliveryError";
|
|
123
|
+
constructor(reference, delivery, messageIds) {
|
|
124
|
+
super(`Telegram delivery ${delivery}; reference ${reference}. Do not automatically resend.`);
|
|
125
|
+
this.reference = reference;
|
|
126
|
+
this.delivery = delivery;
|
|
127
|
+
this.messageIds = messageIds;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
function safeTelegramFetch(fetchImpl, scope = "telegram", previews = {}) {
|
|
131
|
+
const methods = new Set([
|
|
132
|
+
"sendMessage",
|
|
133
|
+
"editMessageText",
|
|
134
|
+
"editMessageCaption",
|
|
135
|
+
"answerCallbackQuery",
|
|
136
|
+
"sendPhoto",
|
|
137
|
+
"sendDocument",
|
|
138
|
+
"sendMediaGroup"
|
|
139
|
+
]);
|
|
140
|
+
return async (input, init) => {
|
|
141
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
142
|
+
if (!methods.has(url.split("/").at(-1) ?? "") || typeof init?.body !== "string") return fetchImpl(input, init);
|
|
143
|
+
const body = JSON.parse(init.body);
|
|
144
|
+
for (const field of ["text", "caption"]) {
|
|
145
|
+
if (typeof body[field] === "string" && isExactChatPostCard(body[field])) body.parse_mode = "MarkdownV2";
|
|
146
|
+
if (typeof body[field] === "string" && !isExactChatPostCard(body[field])) {
|
|
147
|
+
const entityField = field === "text" ? "entities" : "caption_entities";
|
|
148
|
+
const existing = body[entityField];
|
|
149
|
+
const message = Array.isArray(existing) ? {
|
|
150
|
+
text: body[field],
|
|
151
|
+
entities: existing
|
|
152
|
+
} : customerMessage(body[field]);
|
|
153
|
+
const guarded = customerText(message.text);
|
|
154
|
+
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;
|
|
155
|
+
if (!url.endsWith("/answerCallbackQuery")) body[entityField] = body[field] === message.text ? message.entities : [];
|
|
156
|
+
delete body.parse_mode;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (Array.isArray(body.media)) body.media = body.media.map((item) => ({
|
|
160
|
+
...item,
|
|
161
|
+
...typeof item.caption === "string" ? { caption: customerText(item.caption) } : {}
|
|
162
|
+
}));
|
|
163
|
+
const markup = body.reply_markup;
|
|
164
|
+
if (Array.isArray(markup?.inline_keyboard)) markup.inline_keyboard = markup.inline_keyboard.map((row) => row.map((button) => {
|
|
165
|
+
if (typeof button.text !== "string" || !isTechnicalText(button.text)) return button;
|
|
166
|
+
reportTelegramError("technical_button_label_blocked", { text: button.text });
|
|
167
|
+
return {
|
|
168
|
+
...button,
|
|
169
|
+
text: currentLocale().startsWith("ru") ? "Продолжить" : "Continue"
|
|
170
|
+
};
|
|
171
|
+
}));
|
|
172
|
+
const send = async () => {
|
|
173
|
+
const parts = url.endsWith("/sendMessage") && typeof body.text === "string" && !isExactChatPostCard(body.text) ? splitTelegramText({
|
|
174
|
+
text: body.text,
|
|
175
|
+
entities: body.entities ?? []
|
|
176
|
+
}) : [null];
|
|
177
|
+
let first;
|
|
178
|
+
const messageIds = [];
|
|
179
|
+
const attempt = async (outgoing) => {
|
|
180
|
+
try {
|
|
181
|
+
const previewOptions = (url.endsWith("/sendMessage") || url.endsWith("/editMessageText")) && typeof outgoing.text === "string" ? telegramPreviewOptions(body, chatPostCardLinks(outgoing.text) ?? telegramTextLinks(outgoing.text, outgoing.entities ?? []), previews) : {};
|
|
182
|
+
const payload = { ...outgoing };
|
|
183
|
+
if (url.endsWith("/sendMessage") || url.endsWith("/editMessageText")) {
|
|
184
|
+
delete payload.link_preview_options;
|
|
185
|
+
delete payload.disable_web_page_preview;
|
|
186
|
+
Object.assign(payload, previewOptions);
|
|
187
|
+
}
|
|
188
|
+
return await fetchImpl(input, {
|
|
189
|
+
...init,
|
|
190
|
+
body: JSON.stringify(payload)
|
|
191
|
+
});
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new TelegramDeliveryError(reportTelegramError("telegram_send_unknown", error, { messageIds }), "unknown", messageIds);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
for (const [index, part] of parts.entries()) {
|
|
197
|
+
const outgoing = part === null ? { ...body } : {
|
|
198
|
+
...body,
|
|
199
|
+
text: part.text,
|
|
200
|
+
entities: part.entities
|
|
201
|
+
};
|
|
202
|
+
if (index < parts.length - 1) delete outgoing.reply_markup;
|
|
203
|
+
let response = await attempt(outgoing);
|
|
204
|
+
const entityField = typeof outgoing.text === "string" ? "entities" : "caption_entities";
|
|
205
|
+
if (Array.isArray(outgoing[entityField]) && outgoing[entityField].length > 0 && await formattingRefused(response)) {
|
|
206
|
+
delete outgoing[entityField];
|
|
207
|
+
delete outgoing.parse_mode;
|
|
208
|
+
response = await attempt(outgoing);
|
|
209
|
+
}
|
|
210
|
+
const result = await response.clone().json().catch(() => null);
|
|
211
|
+
if (url.endsWith("/sendMessage")) {
|
|
212
|
+
const id = result?.result?.message_id;
|
|
213
|
+
if (!response.ok || result?.ok !== true || typeof id !== "number" || !Number.isSafeInteger(id) || id < 1) {
|
|
214
|
+
const delivery = result?.ok === false ? "failed" : "unknown";
|
|
215
|
+
throw new TelegramDeliveryError(reportTelegramError("telegram_send_unacknowledged", {
|
|
216
|
+
status: response.status,
|
|
217
|
+
result,
|
|
218
|
+
messageIds,
|
|
219
|
+
delivery
|
|
220
|
+
}), delivery, messageIds);
|
|
221
|
+
}
|
|
222
|
+
messageIds.push(id);
|
|
223
|
+
} else if (!response.ok || result?.ok !== true) return response;
|
|
224
|
+
first ??= response;
|
|
225
|
+
}
|
|
226
|
+
if (first === void 0) throw new Error("Telegram message has no visible text; nothing was sent.");
|
|
227
|
+
return first;
|
|
228
|
+
};
|
|
229
|
+
return typeof body.chat_id === "string" || typeof body.chat_id === "number" ? inTelegramSendQueue(scope, String(body.chat_id), send) : send();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
var InboundRequest = class {
|
|
233
|
+
agentDescription;
|
|
234
|
+
edited = false;
|
|
235
|
+
deliveryFailed;
|
|
236
|
+
claimTurn;
|
|
237
|
+
beginDispatch;
|
|
238
|
+
finishClaim;
|
|
239
|
+
locale;
|
|
240
|
+
threadId;
|
|
241
|
+
text;
|
|
242
|
+
files;
|
|
243
|
+
extraFiles;
|
|
244
|
+
sessions;
|
|
245
|
+
waitUntil;
|
|
246
|
+
address;
|
|
247
|
+
respond;
|
|
248
|
+
constructor(agentDescription) {
|
|
249
|
+
this.agentDescription = agentDescription;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const storage = new AsyncLocalStorage();
|
|
253
|
+
function runInboundRequest(request, run) {
|
|
254
|
+
return storage.run(request, run);
|
|
255
|
+
}
|
|
256
|
+
function currentInboundRequest() {
|
|
257
|
+
return storage.getStore();
|
|
258
|
+
}
|
|
259
|
+
function replaceText(message, text) {
|
|
260
|
+
if (typeof message === "string") return text;
|
|
261
|
+
return [{
|
|
262
|
+
type: "text",
|
|
263
|
+
text
|
|
264
|
+
}, ...message.filter((part) => part.type !== "text")];
|
|
265
|
+
}
|
|
266
|
+
function telegramFileId(data) {
|
|
267
|
+
const href = data instanceof URL ? data.href : typeof data === "string" ? data : void 0;
|
|
268
|
+
if (href === void 0 || !href.startsWith("telegram-file:")) return;
|
|
269
|
+
try {
|
|
270
|
+
return decodeURIComponent(new URL(href).pathname);
|
|
271
|
+
} catch {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function replaceFiles(message, files) {
|
|
276
|
+
if (typeof message === "string") return message;
|
|
277
|
+
return message.map((part) => {
|
|
278
|
+
if (part.type !== "file") return part;
|
|
279
|
+
const fileId = telegramFileId(part.data);
|
|
280
|
+
const staged = fileId === void 0 ? void 0 : files.get(fileId);
|
|
281
|
+
if (staged === void 0) return part;
|
|
282
|
+
if (staged.status === "refused") return {
|
|
283
|
+
type: "text",
|
|
284
|
+
text: `[file ${staged.filename} was not received: ${staged.reason}]`
|
|
285
|
+
};
|
|
286
|
+
if (staged.imageInput === "text") return {
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `[image ${staged.attachment.filename}: ${staged.description ?? "see attachment context"}]`
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
...part,
|
|
292
|
+
data: staged.url
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function withReplacedText(source, request) {
|
|
297
|
+
return {
|
|
298
|
+
...source,
|
|
299
|
+
async send(message, options) {
|
|
300
|
+
const text = request.text === void 0 ? message : replaceText(message, request.text);
|
|
301
|
+
const files = request.files === void 0 ? text : replaceFiles(text, request.files);
|
|
302
|
+
const content = request.extraFiles === void 0 || request.extraFiles.length === 0 ? files : [...typeof files === "string" ? [{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: files
|
|
305
|
+
}] : files, ...request.extraFiles];
|
|
306
|
+
let result;
|
|
307
|
+
try {
|
|
308
|
+
result = await source.send(content, {
|
|
309
|
+
...options,
|
|
310
|
+
turnPolicy: "queue"
|
|
311
|
+
});
|
|
312
|
+
} catch (error) {
|
|
313
|
+
try {
|
|
314
|
+
await request.finishClaim?.(false);
|
|
315
|
+
await request.deliveryFailed?.(error);
|
|
316
|
+
} catch (cleanupError) {
|
|
317
|
+
console.error("[segmentstream] inbound delivery cleanup failed", {
|
|
318
|
+
cleanupError,
|
|
319
|
+
cause: error
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
await request.finishClaim?.(true);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
reportTelegramError("turn_admission_recording_failed", error);
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export { TelegramDeliveryError as a, customerCopy as c, failureText as d, formattingRefused as f, withDeliveryLocale as g, safeTelegramFetch as h, withReplacedText as i, customerMessage as l, reportTelegramError as m, currentInboundRequest as n, currentLocale as o, isTechnicalText as p, runInboundRequest as r, currentThreadId as s, InboundRequest as t, customerText as u };
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* The extension's configuration: `agent/segmentstream.json`, mounted by
|
|
3
|
-
* `agent/agent/extensions/segmentstream.ts` and read by the platform at deploy.
|
|
4
|
-
*
|
|
5
|
-
* eve validates a mount's configuration with a Standard Schema. The schema below is written by
|
|
6
|
-
* hand, so the package adds no second schema library next to the Effect Schema the rest of the
|
|
7
|
-
* repository uses.
|
|
8
|
-
*/
|
|
1
|
+
import { type TelegramLinkPreviews } from "@segmentstream-cloud/ontology/telegram";
|
|
9
2
|
export declare const TELEGRAM_POLICIES: readonly ["mention", "smart"];
|
|
10
3
|
/**
|
|
11
4
|
* How the Telegram channel treats group messages that do not address the bot: `smart` asks the
|
|
@@ -28,6 +21,10 @@ export interface SegmentStreamAgentConfigInput {
|
|
|
28
21
|
readonly connection: string;
|
|
29
22
|
/** `smart` (default) or `mention`. */
|
|
30
23
|
readonly policy?: string;
|
|
24
|
+
/** Telegram’s native first link (default), external links only, or no previews. */
|
|
25
|
+
readonly linkPreviews?: string;
|
|
26
|
+
/** Additional exact HTTP(S) origins belonging to this workspace. */
|
|
27
|
+
readonly additionalOwnOrigins?: readonly string[];
|
|
31
28
|
};
|
|
32
29
|
};
|
|
33
30
|
readonly models?: {
|
|
@@ -72,6 +69,8 @@ export interface SegmentStreamAgentConfigInput {
|
|
|
72
69
|
export interface TelegramSettings {
|
|
73
70
|
readonly connection: string;
|
|
74
71
|
readonly policy: TelegramPolicy;
|
|
72
|
+
readonly linkPreviews?: TelegramLinkPreviews;
|
|
73
|
+
readonly additionalOwnOrigins?: readonly string[];
|
|
75
74
|
}
|
|
76
75
|
/** The chat models `segmentstream.model()` chooses between, as AI Gateway model ids. */
|
|
77
76
|
export interface ChatModelChoice {
|
|
@@ -4,6 +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 { TELEGRAM_LINK_PREVIEWS, isHttpOrigin } from "@segmentstream-cloud/ontology/telegram";
|
|
7
8
|
const CONNECTION_KEY = /^[a-z][a-z0-9_]{0,63}$/;
|
|
8
9
|
const TELEGRAM_POLICIES = ["mention", "smart"];
|
|
9
10
|
const DEFAULT_STT_MODEL = "spacexai/grok-stt";
|
|
@@ -44,7 +45,12 @@ function telegramSettings(value, issues) {
|
|
|
44
45
|
const telegram = section({
|
|
45
46
|
value,
|
|
46
47
|
path,
|
|
47
|
-
allowed: [
|
|
48
|
+
allowed: [
|
|
49
|
+
"connection",
|
|
50
|
+
"policy",
|
|
51
|
+
"linkPreviews",
|
|
52
|
+
"additionalOwnOrigins"
|
|
53
|
+
],
|
|
48
54
|
issues
|
|
49
55
|
});
|
|
50
56
|
if (telegram === void 0) return null;
|
|
@@ -58,9 +64,21 @@ function telegramSettings(value, issues) {
|
|
|
58
64
|
message: "must be \"smart\" or \"mention\"",
|
|
59
65
|
path: [...path, "policy"]
|
|
60
66
|
});
|
|
67
|
+
const linkPreviews = telegram.linkPreviews === void 0 ? "all" : telegram.linkPreviews;
|
|
68
|
+
const additionalOwnOrigins = telegram.additionalOwnOrigins === void 0 ? [] : telegram.additionalOwnOrigins;
|
|
69
|
+
if (!TELEGRAM_LINK_PREVIEWS.some((mode) => mode === linkPreviews)) issues.push({
|
|
70
|
+
message: "must be \"external\", \"none\" or \"all\"",
|
|
71
|
+
path: [...path, "linkPreviews"]
|
|
72
|
+
});
|
|
73
|
+
if (!Array.isArray(additionalOwnOrigins) || !additionalOwnOrigins.every(isHttpOrigin)) issues.push({
|
|
74
|
+
message: "must be an array of HTTP(S) origins without paths, credentials, queries or fragments",
|
|
75
|
+
path: [...path, "additionalOwnOrigins"]
|
|
76
|
+
});
|
|
61
77
|
return typeof connection === "string" && isTelegramPolicy(policy) ? {
|
|
62
78
|
connection,
|
|
63
|
-
policy
|
|
79
|
+
policy,
|
|
80
|
+
linkPreviews,
|
|
81
|
+
additionalOwnOrigins
|
|
64
82
|
} : null;
|
|
65
83
|
}
|
|
66
84
|
function modelId(input) {
|
|
@@ -19,6 +19,10 @@ talking to you, and you act with exactly their permissions.
|
|
|
19
19
|
- Ask only when information is missing or ambiguous. In Telegram, use
|
|
20
20
|
`segmentstream__questions_ask` with the question and pending task so the session keeps its intent,
|
|
21
21
|
member and chat/topic. An answer to a pending question is never chatter or `[SILENT]`.
|
|
22
|
+
- New messages and uploads may arrive together or while you are working. Complete or explicitly
|
|
23
|
+
report the status of every request and every supplied file. A later message adds work unless
|
|
24
|
+
the member explicitly cancels or replaces the earlier task. If work is interrupted or blocked,
|
|
25
|
+
say what remains unfinished; never silently replace it with the latest question.
|
|
22
26
|
- Destructive or irreversible actions, and legacy custom actions without safety labels, require
|
|
23
27
|
the tool boundary's Confirm / Cancel buttons. Call the action to start that flow; never ask for
|
|
24
28
|
approval in prose or treat a text acknowledgement as approval. Wait for the button outcome.
|
|
@@ -55,12 +55,20 @@ export interface ConversationPage {
|
|
|
55
55
|
readonly nextCursor: string | null;
|
|
56
56
|
}
|
|
57
57
|
export interface Conversations {
|
|
58
|
-
|
|
58
|
+
/** True only for the first insert of this event ID. Legacy custom archives may omit the result, but Telegram dispatch requires explicit true. */
|
|
59
|
+
append(event: ConversationEvent): Promise<boolean | void>;
|
|
59
60
|
context(input: {
|
|
60
61
|
readonly provider: string;
|
|
61
62
|
readonly chatId: string;
|
|
62
63
|
readonly threadId?: string;
|
|
63
64
|
readonly limit?: number;
|
|
65
|
+
/** Read routing audit records as this chat's reader, rather than model transcript only. */
|
|
66
|
+
readonly includeDecisions?: boolean;
|
|
67
|
+
readonly participantId?: string;
|
|
68
|
+
readonly before?: string;
|
|
69
|
+
readonly from?: string;
|
|
70
|
+
readonly senderId?: string;
|
|
71
|
+
readonly messageId?: string;
|
|
64
72
|
}): Promise<ConversationPage>;
|
|
65
73
|
}
|
|
66
74
|
/** Collapse delivery snapshots from older archives before building a user/model transcript. */
|
|
@@ -66,14 +66,14 @@ function conversations(input = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
return {
|
|
68
68
|
append: async (event) => {
|
|
69
|
-
await call("events", event);
|
|
69
|
+
return (await call("events", event)).stored;
|
|
70
70
|
},
|
|
71
|
-
context: async (search) => await call("search", {
|
|
71
|
+
context: async ({ includeDecisions, participantId, ...search }) => await call("search", {
|
|
72
72
|
search,
|
|
73
|
-
context:
|
|
73
|
+
context: !includeDecisions,
|
|
74
74
|
reader: {
|
|
75
|
-
participantId: null,
|
|
76
|
-
groups:
|
|
75
|
+
participantId: participantId ?? null,
|
|
76
|
+
groups: includeDecisions === true,
|
|
77
77
|
admin: false
|
|
78
78
|
}
|
|
79
79
|
})
|
|
@@ -13,10 +13,9 @@ import { agentAttachments, parseAttachmentUrl } from "../attachments.mjs";
|
|
|
13
13
|
import { agentIdentities } from "../identities.mjs";
|
|
14
14
|
import { conversations } from "../conversations.mjs";
|
|
15
15
|
import { observedMessages } from "./observed.mjs";
|
|
16
|
-
import { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText } from "./inbound-request.mjs";
|
|
17
16
|
import { telegramSendScope } from "./send-queue.mjs";
|
|
18
|
-
import { failureText,
|
|
19
|
-
import { captureRouteDecider, captureTelegramFetch, conversationIdentities, telegramConversationCapture } from "./conversations.mjs";
|
|
17
|
+
import { d as failureText, g as withDeliveryLocale, h as safeTelegramFetch, i as withReplacedText, m as reportTelegramError, n as currentInboundRequest, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-DgyI7oR9.mjs";
|
|
18
|
+
import { captureRouteDecider, captureTelegramFetch, conversationIdentities, recordGateDecision, telegramConversationCapture } from "./conversations.mjs";
|
|
20
19
|
import { runtimeTokenVerifier } from "../runtime-identity.mjs";
|
|
21
20
|
import { botIdentityCache } from "./bot.mjs";
|
|
22
21
|
import { confirmationLocale, confirmationRenderer } from "./confirmation-text.mjs";
|
|
@@ -33,6 +32,7 @@ import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch }
|
|
|
33
32
|
import { healthRoute } from "./health.mjs";
|
|
34
33
|
import { telegramFailureEvents } from "./turn-errors.mjs";
|
|
35
34
|
import { telegramWebhookVerifier } from "./webhook.mjs";
|
|
35
|
+
import { telegramPreviewEnvironment } from "@segmentstream-cloud/ontology/telegram";
|
|
36
36
|
import { telegramChannel } from "eve/channels/telegram";
|
|
37
37
|
const eveHistory = async (sessionId) => {
|
|
38
38
|
const { sessions } = await import("eve/server");
|
|
@@ -125,10 +125,14 @@ function segmentstreamTelegram(options) {
|
|
|
125
125
|
return store;
|
|
126
126
|
}
|
|
127
127
|
const channel = telegramChannel({
|
|
128
|
+
turnPolicy: "queue",
|
|
128
129
|
api: {
|
|
129
130
|
apiBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
130
131
|
fileBaseUrl: TELEGRAM_PROXY_ORIGIN,
|
|
131
|
-
fetch: (input, init) => safeTelegramFetch(captureTelegramFetch(telegramProxyFetch(proxy(), telegramSendScope(environment, settings().connection)), archive, pairing), telegramSendScope(environment, settings().connection)
|
|
132
|
+
fetch: (input, init) => safeTelegramFetch(captureTelegramFetch(telegramProxyFetch(proxy(), telegramSendScope(environment, settings().connection)), archive, pairing), telegramSendScope(environment, settings().connection), {
|
|
133
|
+
settings: settings(),
|
|
134
|
+
ownOrigins: telegramPreviewEnvironment(environment).appOrigins
|
|
135
|
+
})(input, init)
|
|
132
136
|
},
|
|
133
137
|
credentials: {
|
|
134
138
|
botToken: TELEGRAM_PLACEHOLDER_TOKEN,
|
|
@@ -201,7 +205,21 @@ function segmentstreamTelegram(options) {
|
|
|
201
205
|
connectUrl: (code) => `${appOrigin(environment)}/agent/connect?code=${encodeURIComponent(code)}`
|
|
202
206
|
}
|
|
203
207
|
});
|
|
204
|
-
if (inbound !== void 0 && result !== null)
|
|
208
|
+
if (inbound !== void 0 && result !== null) {
|
|
209
|
+
inbound.locale = confirmationLocale(result.auth);
|
|
210
|
+
inbound.deliveryFailed = async (error) => {
|
|
211
|
+
recordGateDecision("inbound_delivery_failed");
|
|
212
|
+
const errorId = reportTelegramError("inbound_delivery_failed", error, {
|
|
213
|
+
chatId: message.chat.id,
|
|
214
|
+
messageId: message.messageId
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
await context.telegram.post(failureText(errorId));
|
|
218
|
+
} catch (noticeError) {
|
|
219
|
+
reportTelegramError("inbound_failure_notice_failed", noticeError, { errorId });
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
205
223
|
return result;
|
|
206
224
|
} catch (error) {
|
|
207
225
|
const errorId = reportTelegramError("inbound_message_failed", error, { chatId: message.chat.id });
|
|
@@ -305,8 +323,8 @@ function gatedRoute(input) {
|
|
|
305
323
|
args.waitUntil(task);
|
|
306
324
|
};
|
|
307
325
|
inbound.waitUntil = waitUntil;
|
|
308
|
-
return runInboundRequest(inbound, () => input.capture(request, async () => {
|
|
309
|
-
const response = await route.handler(
|
|
326
|
+
return runInboundRequest(inbound, () => input.capture(request, async (routedRequest = request) => {
|
|
327
|
+
const response = await route.handler(routedRequest, {
|
|
310
328
|
...args,
|
|
311
329
|
waitUntil,
|
|
312
330
|
from: (address) => withReplacedText(args.from(inbound.address ?? scoped(address)), inbound)
|
|
@@ -12,7 +12,7 @@ import { runtimeToken } from "../runtime-token.mjs";
|
|
|
12
12
|
import { ACT_AS_HEADER } from "../ontology-connection.mjs";
|
|
13
13
|
import { AttachmentError, CONVERSATION_HEADER, SESSION_HEADER } from "../attachments.mjs";
|
|
14
14
|
import { inTelegramSendQueue, telegramSendScope } from "./send-queue.mjs";
|
|
15
|
-
import { customerCopy,
|
|
15
|
+
import { c as customerCopy, f as formattingRefused, l as customerMessage, m as reportTelegramError } from "../../../_chunks/inbound-request-DgyI7oR9.mjs";
|
|
16
16
|
import { confirmationLocale } from "./confirmation-text.mjs";
|
|
17
17
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
18
18
|
import { attachmentOwner } from "../attachment-tools.mjs";
|
|
@@ -17,10 +17,14 @@ export interface MessageAudience {
|
|
|
17
17
|
readonly replyTo: (Recipient & {
|
|
18
18
|
readonly messageId: string;
|
|
19
19
|
readonly text: string;
|
|
20
|
+
/** A reply can quote evidence instead of addressing its uploader. */
|
|
21
|
+
readonly hasAttachments?: boolean;
|
|
22
|
+
readonly filename?: string;
|
|
23
|
+
readonly self?: boolean;
|
|
20
24
|
}) | null;
|
|
21
25
|
}
|
|
22
26
|
/** Current-message entities only; mentions inside a quoted/code range are not addressees. */
|
|
23
27
|
export declare function messageAudience(message: TelegramMessage, bot: BotIdentity | undefined): MessageAudience;
|
|
24
|
-
/**
|
|
28
|
+
/** Explicit recipients win; quoting an upload or one's own message goes to smart inference. */
|
|
25
29
|
export declare function otherRecipient(audience: MessageAudience | undefined): string | undefined;
|
|
26
30
|
export {};
|
|
@@ -53,6 +53,7 @@ function messageAudience(message, bot) {
|
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
const rawReply = telegramRecord(message.raw.reply_to_message);
|
|
56
|
+
const documentName = telegramRecord(rawReply.document).file_name;
|
|
56
57
|
const author = message.replyToMessage?.from;
|
|
57
58
|
const replyId = message.replyToMessage?.messageId ?? rawReply.message_id;
|
|
58
59
|
const replyTo = replyId === void 0 ? null : {
|
|
@@ -63,7 +64,19 @@ function messageAudience(message, bot) {
|
|
|
63
64
|
first_name: author?.firstName
|
|
64
65
|
}, bot),
|
|
65
66
|
messageId: String(replyId),
|
|
66
|
-
text: messageContent(rawReply).text.slice(0, 1e3)
|
|
67
|
+
text: messageContent(rawReply).text.slice(0, 1e3),
|
|
68
|
+
...typeof documentName === "string" ? { filename: documentName } : {},
|
|
69
|
+
hasAttachments: [
|
|
70
|
+
"document",
|
|
71
|
+
"photo",
|
|
72
|
+
"voice",
|
|
73
|
+
"audio",
|
|
74
|
+
"video",
|
|
75
|
+
"video_note",
|
|
76
|
+
"sticker",
|
|
77
|
+
"animation"
|
|
78
|
+
].some((kind) => rawReply[kind] !== void 0),
|
|
79
|
+
self: message.from !== void 0 && String(telegramRecord(rawReply.from).id ?? author?.id) === message.from.id
|
|
67
80
|
};
|
|
68
81
|
return {
|
|
69
82
|
originalText: text,
|
|
@@ -74,7 +87,7 @@ function messageAudience(message, bot) {
|
|
|
74
87
|
}
|
|
75
88
|
function otherRecipient(audience) {
|
|
76
89
|
if (audience === void 0 || audience.botTagged || audience.replyTo?.kind === "bot") return;
|
|
77
|
-
if (audience.replyTo?.kind === "human" || audience.replyTo?.kind === "other_bot") return "Reply targets another person or bot; this bot was not addressed.";
|
|
78
90
|
if (audience.mentions.some((item) => item.kind !== "bot")) return "Message tags another recipient; this bot was not addressed.";
|
|
91
|
+
if (audience.replyTo?.kind === "other_bot" || audience.replyTo?.kind === "human" && !audience.replyTo.hasAttachments && !audience.replyTo.self) return "Reply targets another person or bot; this bot was not addressed.";
|
|
79
92
|
}
|
|
80
93
|
export { messageAudience, otherRecipient };
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ChatPostPreview } from "../chat-post-confirmation.js";
|
|
2
2
|
import type { ConfirmationText } from "./confirmation-text.js";
|
|
3
3
|
/** Only the frozen deterministic card may bypass the generic technical-prose filter. */
|
|
4
|
-
export declare function withChatPostCard<T>(text: string, run: () => T): T;
|
|
4
|
+
export declare function withChatPostCard<T>(text: string, run: () => T, links?: readonly string[]): T;
|
|
5
5
|
export declare function isExactChatPostCard(text: string): boolean;
|
|
6
|
+
export declare function chatPostCardLinks(text: string): readonly string[] | undefined;
|
|
6
7
|
export declare function chatPostCopy(locale: string): {
|
|
7
8
|
edit: string;
|
|
8
9
|
changes: string;
|