@diegoaltoworks/chatter 0.54.2 → 0.56.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/README.md +2 -1
- package/dist/channels/index.js +18 -1
- package/dist/channels/index.mjs +18 -1
- package/dist/channels/pipeline.d.ts +11 -1
- package/dist/channels/pipeline.d.ts.map +1 -1
- package/dist/channels/telegram/api.d.ts +122 -0
- package/dist/channels/telegram/api.d.ts.map +1 -0
- package/dist/channels/telegram/channel.d.ts +82 -0
- package/dist/channels/telegram/channel.d.ts.map +1 -0
- package/dist/channels/telegram/index.d.ts +26 -0
- package/dist/channels/telegram/index.d.ts.map +1 -0
- package/dist/channels/telegram/index.js +710 -0
- package/dist/channels/telegram/index.mjs +667 -0
- package/dist/channels/telegram/poll.d.ts +47 -0
- package/dist/channels/telegram/poll.d.ts.map +1 -0
- package/dist/channels/telegram/updates.d.ts +63 -0
- package/dist/channels/telegram/updates.d.ts.map +1 -0
- package/dist/channels/whatsapp/inbound.d.ts +4 -1
- package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +22 -2
- package/dist/channels/whatsapp/index.mjs +22 -2
- package/dist/core/answer.d.ts +31 -0
- package/dist/core/answer.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -10
- package/dist/index.mjs +53 -10
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +26 -4
- package/dist/mcp-server.mjs +26 -4
- package/dist/routes/demo.d.ts.map +1 -1
- package/dist/routes/openai.d.ts.map +1 -1
- package/dist/routes/private.d.ts.map +1 -1
- package/dist/routes/public.d.ts.map +1 -1
- package/dist/server.js +38 -6
- package/dist/server.mjs +38 -6
- package/dist/types.d.ts +17 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -2
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/channels/telegram/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
TELEGRAM_API_BASE: () => TELEGRAM_API_BASE,
|
|
24
|
+
TELEGRAM_TEXT_LIMIT: () => TELEGRAM_TEXT_LIMIT,
|
|
25
|
+
TelegramApiError: () => TelegramApiError,
|
|
26
|
+
createTelegramApi: () => createTelegramApi,
|
|
27
|
+
createTelegramChannel: () => createTelegramChannel,
|
|
28
|
+
mentionsBot: () => mentionsBot,
|
|
29
|
+
messageEntities: () => messageEntities,
|
|
30
|
+
messageText: () => messageText,
|
|
31
|
+
nextOffset: () => nextOffset,
|
|
32
|
+
pollBackoffMs: () => pollBackoffMs,
|
|
33
|
+
redactToken: () => redactToken,
|
|
34
|
+
retryDelayMs: () => retryDelayMs,
|
|
35
|
+
runLongPoll: () => runLongPoll,
|
|
36
|
+
splitTelegramText: () => splitTelegramText,
|
|
37
|
+
telegramSenderKey: () => telegramSenderKey,
|
|
38
|
+
toChannelMessage: () => toChannelMessage,
|
|
39
|
+
toTelegramMediaRequest: () => toTelegramMediaRequest
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(index_exports);
|
|
42
|
+
|
|
43
|
+
// src/channels/telegram/api.ts
|
|
44
|
+
var TELEGRAM_API_BASE = "https://api.telegram.org";
|
|
45
|
+
var TELEGRAM_TEXT_LIMIT = 4096;
|
|
46
|
+
var TelegramApiError = class extends Error {
|
|
47
|
+
errorCode;
|
|
48
|
+
retryAfterMs;
|
|
49
|
+
constructor(message, options) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "TelegramApiError";
|
|
52
|
+
this.errorCode = options?.errorCode;
|
|
53
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
function redactToken(text, token) {
|
|
57
|
+
if (!token) return text;
|
|
58
|
+
return text.split(token).join("***");
|
|
59
|
+
}
|
|
60
|
+
function splitTelegramText(text, limit = TELEGRAM_TEXT_LIMIT) {
|
|
61
|
+
if (!text.trim()) return [];
|
|
62
|
+
if (text.length <= limit) return [text];
|
|
63
|
+
const chunks = [];
|
|
64
|
+
let rest = text;
|
|
65
|
+
while (rest.length > limit) {
|
|
66
|
+
const window = rest.slice(0, limit);
|
|
67
|
+
const boundary = ["\n\n", "\n", " "].map((separator) => window.lastIndexOf(separator)).find((index) => index > 0);
|
|
68
|
+
const cut = boundary ?? limit;
|
|
69
|
+
chunks.push(rest.slice(0, cut).trimEnd());
|
|
70
|
+
rest = rest.slice(cut).trimStart();
|
|
71
|
+
}
|
|
72
|
+
if (rest) chunks.push(rest);
|
|
73
|
+
return chunks;
|
|
74
|
+
}
|
|
75
|
+
var MEDIA_METHODS = {
|
|
76
|
+
photo: { method: "sendPhoto", field: "photo" },
|
|
77
|
+
document: { method: "sendDocument", field: "document" },
|
|
78
|
+
video: { method: "sendVideo", field: "video" },
|
|
79
|
+
audio: { method: "sendAudio", field: "audio" }
|
|
80
|
+
};
|
|
81
|
+
function toTelegramMediaRequest(chatId, payload) {
|
|
82
|
+
const media = typeof payload === "string" ? { url: payload } : payload;
|
|
83
|
+
if (!media || typeof media !== "object" || typeof media.url !== "string" || !media.url) {
|
|
84
|
+
throw new TypeError(
|
|
85
|
+
"Telegram sendMedia payload must be a URL string or { url, kind?, caption? } \u2014 got: " + typeof payload
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const kind = media.kind ?? "photo";
|
|
89
|
+
const target = MEDIA_METHODS[kind];
|
|
90
|
+
if (!target) {
|
|
91
|
+
throw new TypeError(
|
|
92
|
+
`Telegram sendMedia kind must be one of ${Object.keys(MEDIA_METHODS).join(", ")} \u2014 got "${kind}"`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
method: target.method,
|
|
97
|
+
body: {
|
|
98
|
+
chat_id: chatId,
|
|
99
|
+
[target.field]: media.url,
|
|
100
|
+
...media.caption ? { caption: media.caption } : {}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function errorText(error) {
|
|
105
|
+
return error instanceof Error ? error.message : String(error);
|
|
106
|
+
}
|
|
107
|
+
function createTelegramApi(config) {
|
|
108
|
+
const token = config.botToken;
|
|
109
|
+
if (!token.trim()) throw new Error("Telegram botToken is required (e.g. TELEGRAM_BOT_TOKEN)");
|
|
110
|
+
const baseUrl = (config.baseUrl ?? TELEGRAM_API_BASE).replace(/\/+$/, "");
|
|
111
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
112
|
+
async function call(method, body, signal) {
|
|
113
|
+
let response;
|
|
114
|
+
try {
|
|
115
|
+
response = await doFetch(`${baseUrl}/bot${token}/${method}`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "content-type": "application/json" },
|
|
118
|
+
body: JSON.stringify(body ?? {}),
|
|
119
|
+
signal
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new TelegramApiError(
|
|
123
|
+
`${method} request failed: ${redactToken(errorText(error), token)}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
let envelope;
|
|
127
|
+
try {
|
|
128
|
+
envelope = await response.json();
|
|
129
|
+
} catch {
|
|
130
|
+
throw new TelegramApiError(
|
|
131
|
+
`${method} returned a non-JSON response (HTTP ${response.status})`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
if (!envelope?.ok) {
|
|
135
|
+
const retryAfter = envelope?.parameters?.retry_after;
|
|
136
|
+
throw new TelegramApiError(
|
|
137
|
+
`${method} failed (HTTP ${response.status}): ${redactToken(envelope?.description ?? "unknown error", token)}`,
|
|
138
|
+
{
|
|
139
|
+
errorCode: envelope?.error_code,
|
|
140
|
+
retryAfterMs: typeof retryAfter === "number" ? retryAfter * 1e3 : void 0
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return envelope.result;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
call,
|
|
148
|
+
getMe: () => call("getMe"),
|
|
149
|
+
getUpdates: ({ offset, timeoutSeconds, signal }) => call(
|
|
150
|
+
"getUpdates",
|
|
151
|
+
{
|
|
152
|
+
...offset === void 0 ? {} : { offset },
|
|
153
|
+
timeout: timeoutSeconds,
|
|
154
|
+
// Only what this channel interprets: asking for every update type
|
|
155
|
+
// would have Telegram queue edits, reactions and channel posts this
|
|
156
|
+
// channel then silently discards.
|
|
157
|
+
allowed_updates: ["message"]
|
|
158
|
+
},
|
|
159
|
+
signal
|
|
160
|
+
),
|
|
161
|
+
async sendMessage(chatId, text, options) {
|
|
162
|
+
const chunks = splitTelegramText(text);
|
|
163
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
164
|
+
await call("sendMessage", {
|
|
165
|
+
chat_id: chatId,
|
|
166
|
+
text: chunk,
|
|
167
|
+
// Threading belongs on the first chunk only — every chunk quoting
|
|
168
|
+
// the same incoming message reads as several separate replies.
|
|
169
|
+
...index === 0 && options?.replyToMessageId ? {
|
|
170
|
+
reply_parameters: {
|
|
171
|
+
message_id: options.replyToMessageId,
|
|
172
|
+
// The quoted message can be gone by the time we answer
|
|
173
|
+
// (deleted, or an old message in a cleared chat); sending
|
|
174
|
+
// unthreaded beats failing the whole reply.
|
|
175
|
+
allow_sending_without_reply: true
|
|
176
|
+
}
|
|
177
|
+
} : {}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
async sendMedia(chatId, payload) {
|
|
182
|
+
const { method, body } = toTelegramMediaRequest(chatId, payload);
|
|
183
|
+
await call(method, body);
|
|
184
|
+
},
|
|
185
|
+
async setMessageReaction(chatId, messageId, emoji) {
|
|
186
|
+
await call("setMessageReaction", {
|
|
187
|
+
chat_id: chatId,
|
|
188
|
+
message_id: messageId,
|
|
189
|
+
reaction: [{ type: "emoji", emoji }]
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/core/logger.ts
|
|
196
|
+
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
197
|
+
function createConsoleLogger(level = "info") {
|
|
198
|
+
const threshold = LEVEL_ORDER[level];
|
|
199
|
+
const write = (at) => (...args) => {
|
|
200
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
201
|
+
console.error(`[${at}]`, ...args);
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
debug: write("debug"),
|
|
205
|
+
info: write("info"),
|
|
206
|
+
warn: write("warn"),
|
|
207
|
+
error: write("error")
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/channels/gates.ts
|
|
212
|
+
function matches(re, text) {
|
|
213
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
214
|
+
}
|
|
215
|
+
function isBlockedByAllowlist(msg, config) {
|
|
216
|
+
return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
|
|
217
|
+
}
|
|
218
|
+
function decideChannelAction(msg, config) {
|
|
219
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
220
|
+
if (!msg.isDirectMessage) {
|
|
221
|
+
if (isBlockedByAllowlist(msg, config)) return "ignore";
|
|
222
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
223
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
224
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
225
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
226
|
+
}
|
|
227
|
+
return "reply";
|
|
228
|
+
}
|
|
229
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
230
|
+
const log = /* @__PURE__ */ new Map();
|
|
231
|
+
let lastSweep = now();
|
|
232
|
+
function sweep(current) {
|
|
233
|
+
if (current - lastSweep < windowMs) return;
|
|
234
|
+
lastSweep = current;
|
|
235
|
+
const cutoff = current - windowMs;
|
|
236
|
+
for (const [key, entries] of log) {
|
|
237
|
+
if (!entries.some((t) => t > cutoff)) log.delete(key);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function allow(key) {
|
|
241
|
+
const current = now();
|
|
242
|
+
sweep(current);
|
|
243
|
+
const cutoff = current - windowMs;
|
|
244
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
245
|
+
if (entries.length >= maxPerWindow) {
|
|
246
|
+
if (entries.length > 0) log.set(key, entries);
|
|
247
|
+
else log.delete(key);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
entries.push(current);
|
|
251
|
+
log.set(key, entries);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
allow.size = () => log.size;
|
|
255
|
+
return allow;
|
|
256
|
+
}
|
|
257
|
+
function underReplyRateLimit(msg, limiters) {
|
|
258
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/core/guardrails.ts
|
|
262
|
+
function detectLeakage(text) {
|
|
263
|
+
const flags = [
|
|
264
|
+
/system prompt/i,
|
|
265
|
+
/hidden instruction/i,
|
|
266
|
+
/here (are|is) (my|the) rules/i,
|
|
267
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
268
|
+
/tool instructions/i
|
|
269
|
+
];
|
|
270
|
+
return flags.some((rx) => rx.test(text));
|
|
271
|
+
}
|
|
272
|
+
function scrubOutput(text) {
|
|
273
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
274
|
+
}
|
|
275
|
+
function guardOutput(text) {
|
|
276
|
+
if (detectLeakage(text)) {
|
|
277
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
278
|
+
}
|
|
279
|
+
return scrubOutput(text);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/core/llm.ts
|
|
283
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
284
|
+
async function completeOnce({
|
|
285
|
+
client,
|
|
286
|
+
system,
|
|
287
|
+
messages,
|
|
288
|
+
temperature = 0.2,
|
|
289
|
+
model = DEFAULT_MODEL
|
|
290
|
+
}) {
|
|
291
|
+
const res = await client.chat.completions.create({
|
|
292
|
+
model,
|
|
293
|
+
temperature,
|
|
294
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
295
|
+
});
|
|
296
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
297
|
+
return {
|
|
298
|
+
content: guardOutput(text),
|
|
299
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/core/answer.ts
|
|
304
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
305
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
|
|
306
|
+
const result = await answerFn({ system, messages, mode, sender, conversationId });
|
|
307
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
308
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
309
|
+
return {
|
|
310
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
311
|
+
usage: usage ?? noUsage()
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
async function answerOnce({
|
|
315
|
+
answerFn,
|
|
316
|
+
client,
|
|
317
|
+
system,
|
|
318
|
+
messages,
|
|
319
|
+
mode,
|
|
320
|
+
sender,
|
|
321
|
+
conversationId,
|
|
322
|
+
temperature,
|
|
323
|
+
model
|
|
324
|
+
}) {
|
|
325
|
+
if (answerFn) {
|
|
326
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
|
|
327
|
+
}
|
|
328
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
329
|
+
}
|
|
330
|
+
async function applyTransformReply(transformReply, input, logger) {
|
|
331
|
+
if (!transformReply) return input.text;
|
|
332
|
+
try {
|
|
333
|
+
return await transformReply(input);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
logger?.error(
|
|
336
|
+
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
337
|
+
error
|
|
338
|
+
);
|
|
339
|
+
return input.text;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/core/buckets.ts
|
|
344
|
+
function defaultBuckets(mode) {
|
|
345
|
+
return ["base", mode];
|
|
346
|
+
}
|
|
347
|
+
function isIdentified(sender) {
|
|
348
|
+
return typeof sender === "string" && sender.trim().length > 0;
|
|
349
|
+
}
|
|
350
|
+
function normalize(buckets) {
|
|
351
|
+
const out = [];
|
|
352
|
+
for (const raw of buckets) {
|
|
353
|
+
if (typeof raw !== "string") continue;
|
|
354
|
+
const bucket = raw.trim();
|
|
355
|
+
if (bucket && !out.includes(bucket)) out.push(bucket);
|
|
356
|
+
}
|
|
357
|
+
return out;
|
|
358
|
+
}
|
|
359
|
+
async function resolveBuckets({
|
|
360
|
+
mode,
|
|
361
|
+
sender,
|
|
362
|
+
bucketsFor
|
|
363
|
+
}) {
|
|
364
|
+
if (!bucketsFor) return void 0;
|
|
365
|
+
const identified = isIdentified(sender);
|
|
366
|
+
const requested = await bucketsFor(identified ? { mode, sender } : { mode });
|
|
367
|
+
if (!Array.isArray(requested)) return void 0;
|
|
368
|
+
const cleaned = normalize(requested);
|
|
369
|
+
if (identified) return cleaned;
|
|
370
|
+
const ceiling = new Set(defaultBuckets(mode));
|
|
371
|
+
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/core/messages.ts
|
|
375
|
+
function lastUserMessage(messages) {
|
|
376
|
+
return [...messages].reverse().find((m) => m.role === "user");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/core/pipeline.ts
|
|
380
|
+
var MODE_SETTINGS = {
|
|
381
|
+
public: { topK: 6, label: "Context" },
|
|
382
|
+
private: { topK: 8, label: "Internal Context" }
|
|
383
|
+
};
|
|
384
|
+
async function prepareChat({
|
|
385
|
+
store,
|
|
386
|
+
prompts,
|
|
387
|
+
mode,
|
|
388
|
+
messages,
|
|
389
|
+
personaLayer,
|
|
390
|
+
channelHint,
|
|
391
|
+
buckets
|
|
392
|
+
}) {
|
|
393
|
+
const lastUserMsg = lastUserMessage(messages);
|
|
394
|
+
if (!lastUserMsg) {
|
|
395
|
+
throw new Error("no user message found in conversation");
|
|
396
|
+
}
|
|
397
|
+
const { topK, label } = MODE_SETTINGS[mode];
|
|
398
|
+
const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
|
|
399
|
+
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
400
|
+
const hint = channelHint?.trim();
|
|
401
|
+
const system = [
|
|
402
|
+
prompts.baseSystemRules,
|
|
403
|
+
persona,
|
|
404
|
+
...hint ? [hint] : [],
|
|
405
|
+
`${label}:
|
|
406
|
+
${ctx.join("\n\n")}`
|
|
407
|
+
].join("\n\n");
|
|
408
|
+
return { system, messages, context: ctx };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// src/channels/pipeline.ts
|
|
412
|
+
var DEFAULT_HISTORY_LIMIT = 20;
|
|
413
|
+
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
414
|
+
var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
|
|
415
|
+
async function resolvePersonaLayer(config, ctx) {
|
|
416
|
+
if (!config.personaResolver) return void 0;
|
|
417
|
+
try {
|
|
418
|
+
return await config.personaResolver(ctx);
|
|
419
|
+
} catch {
|
|
420
|
+
return void 0;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function createInboundPipeline(deps, config) {
|
|
424
|
+
const mutedChats = /* @__PURE__ */ new Set();
|
|
425
|
+
const now = config.now ?? Date.now;
|
|
426
|
+
const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
|
|
427
|
+
const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
|
|
428
|
+
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
429
|
+
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
430
|
+
const mode = "public";
|
|
431
|
+
return async function handleInboundMessage(msg, turn) {
|
|
432
|
+
const allowedChats = config.allowedChats ?? [];
|
|
433
|
+
const action = decideChannelAction(msg, {
|
|
434
|
+
allowedChats,
|
|
435
|
+
mutedChats,
|
|
436
|
+
muteRegex: config.muteRegex,
|
|
437
|
+
unmuteRegex: config.unmuteRegex
|
|
438
|
+
});
|
|
439
|
+
if (action === "mute") {
|
|
440
|
+
mutedChats.add(msg.chatId);
|
|
441
|
+
if (config.muteReply) await turn.reply.sendGateReply(msg.chatId, config.muteReply);
|
|
442
|
+
return { action: "mute" };
|
|
443
|
+
}
|
|
444
|
+
if (action === "unmute") {
|
|
445
|
+
mutedChats.delete(msg.chatId);
|
|
446
|
+
if (config.unmuteReply) await turn.reply.sendGateReply(msg.chatId, config.unmuteReply);
|
|
447
|
+
return { action: "unmute" };
|
|
448
|
+
}
|
|
449
|
+
if (action === "ignore") {
|
|
450
|
+
return { action: "ignore" };
|
|
451
|
+
}
|
|
452
|
+
if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
|
|
453
|
+
return { action: "rate-limited" };
|
|
454
|
+
}
|
|
455
|
+
const sender = typeof turn.sender === "function" ? await turn.sender() : turn.sender ?? msg.senderId;
|
|
456
|
+
const conversationId = turn.conversationId ?? msg.chatId;
|
|
457
|
+
if (turn.intercept && await turn.intercept(sender)) {
|
|
458
|
+
return { action: "intercepted" };
|
|
459
|
+
}
|
|
460
|
+
const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
|
|
461
|
+
const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
|
|
462
|
+
const priorTurns = config.history ? await config.history.store.load(
|
|
463
|
+
conversationId,
|
|
464
|
+
config.history.limit ?? DEFAULT_HISTORY_LIMIT
|
|
465
|
+
) : [];
|
|
466
|
+
const messages = [...priorTurns, { role: "user", content: msg.text }];
|
|
467
|
+
await config.history?.store.append(conversationId, { role: "user", content: msg.text });
|
|
468
|
+
const { system } = await prepareChat({
|
|
469
|
+
store: deps.store,
|
|
470
|
+
prompts: deps.prompts,
|
|
471
|
+
mode,
|
|
472
|
+
messages,
|
|
473
|
+
personaLayer,
|
|
474
|
+
channelHint: config.channelHint,
|
|
475
|
+
buckets
|
|
476
|
+
});
|
|
477
|
+
const { content: produced } = await answerOnce({
|
|
478
|
+
answerFn: config.answerFn,
|
|
479
|
+
client: deps.client,
|
|
480
|
+
system,
|
|
481
|
+
messages,
|
|
482
|
+
mode,
|
|
483
|
+
sender,
|
|
484
|
+
conversationId,
|
|
485
|
+
model: config.model
|
|
486
|
+
});
|
|
487
|
+
const content = await applyTransformReply(
|
|
488
|
+
config.transformReply,
|
|
489
|
+
{ channel: config.channel ?? "channel", sender, conversationId, text: produced },
|
|
490
|
+
config.logger
|
|
491
|
+
);
|
|
492
|
+
if (!content) {
|
|
493
|
+
return { action: "ignore" };
|
|
494
|
+
}
|
|
495
|
+
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
496
|
+
await turn.reply.sendAnswer(msg.chatId, content);
|
|
497
|
+
return { action: "reply", content };
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/channels/telegram/updates.ts
|
|
502
|
+
function messageText(message) {
|
|
503
|
+
return message.text ?? message.caption ?? "";
|
|
504
|
+
}
|
|
505
|
+
function messageEntities(message) {
|
|
506
|
+
return (message.text !== void 0 ? message.entities : message.caption_entities) ?? [];
|
|
507
|
+
}
|
|
508
|
+
function mentionsBot(message, me) {
|
|
509
|
+
const text = messageText(message);
|
|
510
|
+
const handle = me.username ? `@${me.username.toLowerCase()}` : void 0;
|
|
511
|
+
return messageEntities(message).some((entity) => {
|
|
512
|
+
if (entity.type === "text_mention") return entity.user?.id === me.id;
|
|
513
|
+
if (!handle) return false;
|
|
514
|
+
const token = text.slice(entity.offset, entity.offset + entity.length).toLowerCase();
|
|
515
|
+
if (entity.type === "mention") return token === handle;
|
|
516
|
+
if (entity.type === "bot_command") return token.endsWith(handle);
|
|
517
|
+
return false;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function toChannelMessage(update, me) {
|
|
521
|
+
const message = update.message;
|
|
522
|
+
if (!message?.from) return void 0;
|
|
523
|
+
return {
|
|
524
|
+
chatId: String(message.chat.id),
|
|
525
|
+
senderId: String(message.from.id),
|
|
526
|
+
text: messageText(message),
|
|
527
|
+
isDirectMessage: message.chat.type === "private",
|
|
528
|
+
mentionsBot: mentionsBot(message, me),
|
|
529
|
+
isReplyToBot: message.reply_to_message?.from?.id === me.id,
|
|
530
|
+
// A single bot token is a single identity, so unlike WhatsApp's
|
|
531
|
+
// multi-session loop guard this is a plain equality check.
|
|
532
|
+
fromBot: message.from.id === me.id,
|
|
533
|
+
// What `setMessageReaction` targets — see `ChannelSenderRegistry.sendReaction`.
|
|
534
|
+
messageRef: message.message_id
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function nextOffset(update) {
|
|
538
|
+
return update.update_id + 1;
|
|
539
|
+
}
|
|
540
|
+
function telegramSenderKey(senderId) {
|
|
541
|
+
return `tg:${senderId}`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/channels/telegram/poll.ts
|
|
545
|
+
var BASE_DELAY_MS = 1e3;
|
|
546
|
+
var MAX_DELAY_MS = 6e4;
|
|
547
|
+
function pollBackoffMs(failures) {
|
|
548
|
+
return Math.min(BASE_DELAY_MS * 2 ** Math.min(failures, 20), MAX_DELAY_MS);
|
|
549
|
+
}
|
|
550
|
+
function retryDelayMs(error, failures) {
|
|
551
|
+
const retryAfterMs = error instanceof TelegramApiError ? error.retryAfterMs : void 0;
|
|
552
|
+
return retryAfterMs ?? pollBackoffMs(failures);
|
|
553
|
+
}
|
|
554
|
+
async function runLongPoll(deps) {
|
|
555
|
+
const logger = deps.logger ?? createConsoleLogger();
|
|
556
|
+
const label = deps.label ?? "Telegram";
|
|
557
|
+
let offset = deps.initialOffset;
|
|
558
|
+
let failures = 0;
|
|
559
|
+
while (!deps.isStopped()) {
|
|
560
|
+
let updates;
|
|
561
|
+
try {
|
|
562
|
+
updates = await deps.getUpdates(offset);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
if (deps.isStopped()) break;
|
|
565
|
+
failures += 1;
|
|
566
|
+
const delay = retryDelayMs(error, failures);
|
|
567
|
+
logger.warn(`${label}: getUpdates failed, retrying in ${Math.round(delay / 1e3)}s:`, error);
|
|
568
|
+
await deps.sleep(delay);
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
failures = 0;
|
|
572
|
+
for (const update of updates) {
|
|
573
|
+
offset = nextOffset(update);
|
|
574
|
+
deps.onOffset?.(offset);
|
|
575
|
+
if (deps.isStopped()) break;
|
|
576
|
+
try {
|
|
577
|
+
await deps.handleUpdate(update);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
logger.warn(`${label}: update ${update.update_id} handling failed:`, error);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return offset;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/channels/telegram/channel.ts
|
|
587
|
+
var DEFAULT_POLL_TIMEOUT_SECONDS = 30;
|
|
588
|
+
function defaultSleep(ms) {
|
|
589
|
+
return new Promise((resolve) => {
|
|
590
|
+
const timer = setTimeout(resolve, ms);
|
|
591
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
function createTelegramChannel(config) {
|
|
595
|
+
const channelName = config.name ?? "telegram";
|
|
596
|
+
const allowedChats = config.allowedChats ?? [];
|
|
597
|
+
const sleep = config.sleep ?? defaultSleep;
|
|
598
|
+
const pollTimeoutSeconds = config.pollTimeoutSeconds ?? DEFAULT_POLL_TIMEOUT_SECONDS;
|
|
599
|
+
let stopped = false;
|
|
600
|
+
let registered = false;
|
|
601
|
+
let abort;
|
|
602
|
+
let senders;
|
|
603
|
+
return {
|
|
604
|
+
name: channelName,
|
|
605
|
+
async start(deps) {
|
|
606
|
+
const log = config.logger ?? deps.logger ?? createConsoleLogger();
|
|
607
|
+
const api = config.api ?? createTelegramApi({
|
|
608
|
+
botToken: config.botToken,
|
|
609
|
+
fetch: config.fetch,
|
|
610
|
+
baseUrl: config.apiBaseUrl
|
|
611
|
+
});
|
|
612
|
+
const me = await api.getMe();
|
|
613
|
+
const label = `Telegram[${me.username ?? me.id}]`;
|
|
614
|
+
const sender = {
|
|
615
|
+
sendText: (chatId, text) => api.sendMessage(chatId, text),
|
|
616
|
+
sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
|
|
617
|
+
sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
|
|
618
|
+
};
|
|
619
|
+
deps.senders.register(channelName, sender);
|
|
620
|
+
senders = deps.senders;
|
|
621
|
+
registered = true;
|
|
622
|
+
const pipeline = createInboundPipeline(
|
|
623
|
+
{ client: deps.client, store: deps.store, prompts: deps.prompts },
|
|
624
|
+
{
|
|
625
|
+
channel: channelName,
|
|
626
|
+
answerFn: config.answerFn ?? deps.config.answerFn,
|
|
627
|
+
bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
|
|
628
|
+
transformReply: config.transformReply ?? deps.config.transformReply,
|
|
629
|
+
model: config.model,
|
|
630
|
+
channelHint: config.channelHint ?? "Channel: Telegram.",
|
|
631
|
+
personaResolver: config.personaResolver,
|
|
632
|
+
history: config.history,
|
|
633
|
+
allowedChats,
|
|
634
|
+
muteRegex: config.muteRegex,
|
|
635
|
+
unmuteRegex: config.unmuteRegex,
|
|
636
|
+
muteReply: config.muteReply,
|
|
637
|
+
unmuteReply: config.unmuteReply,
|
|
638
|
+
dmRateLimit: config.dmRateLimit,
|
|
639
|
+
groupRateLimit: config.groupRateLimit,
|
|
640
|
+
now: config.now,
|
|
641
|
+
logger: log
|
|
642
|
+
}
|
|
643
|
+
);
|
|
644
|
+
const loggedUnallowedChats = /* @__PURE__ */ new Set();
|
|
645
|
+
async function handleUpdate(update) {
|
|
646
|
+
const message = update.message;
|
|
647
|
+
const msg = toChannelMessage(update, me);
|
|
648
|
+
if (!msg || !message) return;
|
|
649
|
+
if (isBlockedByAllowlist(msg, { allowedChats })) {
|
|
650
|
+
if (!loggedUnallowedChats.has(msg.chatId)) {
|
|
651
|
+
loggedUnallowedChats.add(msg.chatId);
|
|
652
|
+
log.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const reply = {
|
|
656
|
+
// Threaded onto the incoming message: in a busy group an untethered
|
|
657
|
+
// answer reads as a non-sequitur.
|
|
658
|
+
sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
|
|
659
|
+
sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
|
|
660
|
+
};
|
|
661
|
+
await pipeline(msg, {
|
|
662
|
+
reply,
|
|
663
|
+
conversationId: msg.chatId,
|
|
664
|
+
sender: telegramSenderKey(msg.senderId)
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
abort = new AbortController();
|
|
668
|
+
const signal = abort.signal;
|
|
669
|
+
void runLongPoll({
|
|
670
|
+
getUpdates: (offset) => api.getUpdates({ offset, timeoutSeconds: pollTimeoutSeconds, signal }),
|
|
671
|
+
handleUpdate,
|
|
672
|
+
isStopped: () => stopped,
|
|
673
|
+
sleep,
|
|
674
|
+
initialOffset: config.initialOffset,
|
|
675
|
+
onOffset: config.onOffset,
|
|
676
|
+
logger: log,
|
|
677
|
+
label
|
|
678
|
+
}).catch((error) => log.error(`${label}: poll loop ended unexpectedly:`, error));
|
|
679
|
+
log.info(`${label}: polling for updates as @${me.username ?? me.id}`);
|
|
680
|
+
},
|
|
681
|
+
stop() {
|
|
682
|
+
stopped = true;
|
|
683
|
+
abort?.abort();
|
|
684
|
+
if (registered) {
|
|
685
|
+
senders?.unregister(channelName);
|
|
686
|
+
registered = false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
692
|
+
0 && (module.exports = {
|
|
693
|
+
TELEGRAM_API_BASE,
|
|
694
|
+
TELEGRAM_TEXT_LIMIT,
|
|
695
|
+
TelegramApiError,
|
|
696
|
+
createTelegramApi,
|
|
697
|
+
createTelegramChannel,
|
|
698
|
+
mentionsBot,
|
|
699
|
+
messageEntities,
|
|
700
|
+
messageText,
|
|
701
|
+
nextOffset,
|
|
702
|
+
pollBackoffMs,
|
|
703
|
+
redactToken,
|
|
704
|
+
retryDelayMs,
|
|
705
|
+
runLongPoll,
|
|
706
|
+
splitTelegramText,
|
|
707
|
+
telegramSenderKey,
|
|
708
|
+
toChannelMessage,
|
|
709
|
+
toTelegramMediaRequest
|
|
710
|
+
});
|