@iletai/nzb 1.7.0 → 1.7.3
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/dist/api/server.js +14 -5
- package/dist/cli.js +1 -0
- package/dist/config.js +12 -3
- package/dist/copilot/client.js +17 -16
- package/dist/copilot/mcp-config.js +2 -0
- package/dist/copilot/orchestrator.js +181 -51
- package/dist/copilot/skills.js +4 -2
- package/dist/copilot/tools.js +33 -5
- package/dist/copilot/types.js +2 -0
- package/dist/daemon.js +11 -10
- package/dist/setup.js +3 -2
- package/dist/store/conversation.js +96 -0
- package/dist/store/db.js +6 -206
- package/dist/store/memory.js +90 -0
- package/dist/store/team-store.js +51 -0
- package/dist/telegram/bot.js +77 -8
- package/dist/telegram/handlers/commands.js +1 -1
- package/dist/telegram/handlers/media.js +63 -6
- package/dist/telegram/handlers/streaming.js +223 -188
- package/dist/telegram/handlers/suggestions.js +22 -1
- package/dist/telegram/log-channel.js +2 -2
- package/dist/telegram/menus.js +243 -99
- package/dist/tui/ansi.js +19 -0
- package/dist/tui/api-client.js +158 -0
- package/dist/tui/debug.js +27 -0
- package/dist/tui/renderer.js +59 -0
- package/dist/tui/stream.js +163 -0
- package/dist/update.js +2 -0
- package/dist/utils.js +102 -0
- package/package.json +1 -1
|
@@ -57,6 +57,18 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
await startTyping();
|
|
60
|
+
// Safety timeout — force-stop typing if orchestrator hangs
|
|
61
|
+
const MAX_TYPING_MS = 120_000; // 2 minutes
|
|
62
|
+
const typingTimeout = setTimeout(() => {
|
|
63
|
+
if (!typingStopped) {
|
|
64
|
+
console.log("[nzb] Typing indicator timeout, force stopping");
|
|
65
|
+
stopTyping();
|
|
66
|
+
}
|
|
67
|
+
}, MAX_TYPING_MS);
|
|
68
|
+
const stopTypingAndClearTimeout = () => {
|
|
69
|
+
stopTyping();
|
|
70
|
+
clearTimeout(typingTimeout);
|
|
71
|
+
};
|
|
60
72
|
// Progressive streaming state — all Telegram API calls are serialized through editChain
|
|
61
73
|
// to prevent duplicate placeholder messages and race conditions
|
|
62
74
|
let placeholderMsgId;
|
|
@@ -104,7 +116,7 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
104
116
|
const msg = await ctx.reply(safeText, { reply_parameters: replyParams });
|
|
105
117
|
placeholderMsgId = msg.message_id;
|
|
106
118
|
// Stop typing once placeholder is visible — edits serve as the indicator now
|
|
107
|
-
|
|
119
|
+
stopTypingAndClearTimeout();
|
|
108
120
|
}
|
|
109
121
|
catch {
|
|
110
122
|
return;
|
|
@@ -125,7 +137,9 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
125
137
|
}
|
|
126
138
|
lastEditedText = safeText;
|
|
127
139
|
})
|
|
128
|
-
.catch(() => {
|
|
140
|
+
.catch((err) => {
|
|
141
|
+
console.error("[nzb] Edit chain error:", err instanceof Error ? err.message : err);
|
|
142
|
+
});
|
|
129
143
|
};
|
|
130
144
|
const onToolEvent = (event) => {
|
|
131
145
|
console.log(`[nzb] Bot received tool event: ${event.type} ${event.toolName}`);
|
|
@@ -198,90 +212,75 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
198
212
|
userPrompt = `[Replying to: "${quoted}"]\n\n${userPrompt}`;
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
try {
|
|
216
|
+
sendToOrchestrator(userPrompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
|
|
217
|
+
if (done) {
|
|
218
|
+
finalized = true;
|
|
219
|
+
stopTypingAndClearTimeout();
|
|
220
|
+
const assistantLogId = meta?.assistantLogId;
|
|
221
|
+
const elapsed = ((Date.now() - handlerStartTime) / 1000).toFixed(1);
|
|
222
|
+
void logInfo(`✅ Response done (${elapsed}s, ${toolHistory.length} tools, ${text.length} chars)`);
|
|
223
|
+
// Return the edit chain so callers can await final delivery
|
|
224
|
+
return editChain.then(async () => {
|
|
225
|
+
try {
|
|
226
|
+
// Format error messages with a distinct visual
|
|
227
|
+
const isError = text.startsWith("Error:");
|
|
228
|
+
if (isError) {
|
|
229
|
+
void logError(`Response error: ${text.slice(0, 200)}`);
|
|
230
|
+
const errorText = `⚠️ ${text}`;
|
|
231
|
+
const errorKb = new InlineKeyboard().text("🔄 Retry", "retry").text("📖 Explain", "explain_error");
|
|
232
|
+
if (placeholderMsgId) {
|
|
233
|
+
try {
|
|
234
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, errorText, { reply_markup: errorKb });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
/* fall through */
|
|
239
|
+
}
|
|
240
|
+
}
|
|
218
241
|
try {
|
|
219
|
-
await
|
|
220
|
-
return;
|
|
242
|
+
await ctx.reply(errorText, { reply_parameters: replyParams, reply_markup: errorKb });
|
|
221
243
|
}
|
|
222
244
|
catch {
|
|
223
|
-
/*
|
|
245
|
+
/* nothing more we can do */
|
|
224
246
|
}
|
|
247
|
+
return;
|
|
225
248
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
let textWithMeta = text;
|
|
250
|
+
if (usageInfo && config.usageMode !== "off") {
|
|
251
|
+
const fmtTokens = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n));
|
|
252
|
+
const parts = [];
|
|
253
|
+
if (config.usageMode === "full" && usageInfo.model)
|
|
254
|
+
parts.push(usageInfo.model);
|
|
255
|
+
parts.push(`⬆${fmtTokens(usageInfo.inputTokens)} ⬇${fmtTokens(usageInfo.outputTokens)}`);
|
|
256
|
+
const totalTokens = usageInfo.inputTokens + usageInfo.outputTokens;
|
|
257
|
+
parts.push(`Σ${fmtTokens(totalTokens)}`);
|
|
258
|
+
if (config.usageMode === "full" && usageInfo.duration)
|
|
259
|
+
parts.push(`${(usageInfo.duration / 1000).toFixed(1)}s`);
|
|
260
|
+
textWithMeta += `\n\n📊 ${parts.join(" · ")}`;
|
|
231
261
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
parts.push(`⬆${fmtTokens(usageInfo.inputTokens)} ⬇${fmtTokens(usageInfo.outputTokens)}`);
|
|
241
|
-
const totalTokens = usageInfo.inputTokens + usageInfo.outputTokens;
|
|
242
|
-
parts.push(`Σ${fmtTokens(totalTokens)}`);
|
|
243
|
-
if (config.usageMode === "full" && usageInfo.duration)
|
|
244
|
-
parts.push(`${(usageInfo.duration / 1000).toFixed(1)}s`);
|
|
245
|
-
textWithMeta += `\n\n📊 ${parts.join(" · ")}`;
|
|
246
|
-
}
|
|
247
|
-
const formatted = toTelegramHTML(textWithMeta);
|
|
248
|
-
let fullFormatted = formatted;
|
|
249
|
-
if (config.showReasoning && toolHistory.length > 0) {
|
|
250
|
-
const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs, detail: t.detail })), {
|
|
251
|
-
elapsedMs: Date.now() - handlerStartTime,
|
|
252
|
-
model: usageInfo?.model,
|
|
253
|
-
inputTokens: usageInfo?.inputTokens,
|
|
254
|
-
outputTokens: usageInfo?.outputTokens,
|
|
255
|
-
});
|
|
256
|
-
fullFormatted += expandable;
|
|
257
|
-
}
|
|
258
|
-
const chunks = chunkMessage(fullFormatted);
|
|
259
|
-
const fallbackChunks = chunkMessage(textWithMeta);
|
|
260
|
-
// Build smart suggestion buttons based on response content
|
|
261
|
-
const smartKb = createSmartSuggestionsWithContext(text, ctx.message.text, 4);
|
|
262
|
-
// Single chunk: edit placeholder in place
|
|
263
|
-
if (placeholderMsgId && chunks.length === 1) {
|
|
264
|
-
try {
|
|
265
|
-
await editSafe(getBot().api, chatId, placeholderMsgId, chunks[0], {
|
|
266
|
-
parse_mode: "HTML",
|
|
267
|
-
reply_markup: smartKb,
|
|
262
|
+
const formatted = toTelegramHTML(textWithMeta);
|
|
263
|
+
let fullFormatted = formatted;
|
|
264
|
+
if (config.showReasoning && toolHistory.length > 0) {
|
|
265
|
+
const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs, detail: t.detail })), {
|
|
266
|
+
elapsedMs: Date.now() - handlerStartTime,
|
|
267
|
+
model: usageInfo?.model,
|
|
268
|
+
inputTokens: usageInfo?.inputTokens,
|
|
269
|
+
outputTokens: usageInfo?.outputTokens,
|
|
268
270
|
});
|
|
269
|
-
|
|
270
|
-
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
271
|
-
}
|
|
272
|
-
catch { }
|
|
273
|
-
if (assistantLogId) {
|
|
274
|
-
try {
|
|
275
|
-
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
276
|
-
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
277
|
-
}
|
|
278
|
-
catch { }
|
|
279
|
-
}
|
|
280
|
-
return;
|
|
271
|
+
fullFormatted += expandable;
|
|
281
272
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
273
|
+
const chunks = chunkMessage(fullFormatted);
|
|
274
|
+
const fallbackChunks = chunkMessage(textWithMeta);
|
|
275
|
+
// Build smart suggestion buttons based on response content
|
|
276
|
+
const smartKb = createSmartSuggestionsWithContext(text, ctx.message.text, 4);
|
|
277
|
+
// Single chunk: edit placeholder in place
|
|
278
|
+
if (placeholderMsgId && chunks.length === 1) {
|
|
279
|
+
try {
|
|
280
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, chunks[0], {
|
|
281
|
+
parse_mode: "HTML",
|
|
282
|
+
reply_markup: smartKb,
|
|
283
|
+
});
|
|
285
284
|
try {
|
|
286
285
|
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
287
286
|
}
|
|
@@ -295,8 +294,45 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
295
294
|
}
|
|
296
295
|
return;
|
|
297
296
|
}
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
catch (err) {
|
|
298
|
+
// "message is not modified" is harmless — placeholder already has this content
|
|
299
|
+
if (isMessageNotModifiedError(err)) {
|
|
300
|
+
try {
|
|
301
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
302
|
+
}
|
|
303
|
+
catch { }
|
|
304
|
+
if (assistantLogId) {
|
|
305
|
+
try {
|
|
306
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
307
|
+
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
}
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// HTML parse error — try plain text fallback
|
|
314
|
+
if (isHtmlParseError(err)) {
|
|
315
|
+
try {
|
|
316
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
|
|
317
|
+
reply_markup: smartKb,
|
|
318
|
+
});
|
|
319
|
+
try {
|
|
320
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
321
|
+
}
|
|
322
|
+
catch { }
|
|
323
|
+
if (assistantLogId) {
|
|
324
|
+
try {
|
|
325
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
326
|
+
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
327
|
+
}
|
|
328
|
+
catch { }
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
/* fall through to send new messages */
|
|
334
|
+
}
|
|
335
|
+
}
|
|
300
336
|
try {
|
|
301
337
|
await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
|
|
302
338
|
reply_markup: smartKb,
|
|
@@ -318,136 +354,135 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
318
354
|
/* fall through to send new messages */
|
|
319
355
|
}
|
|
320
356
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
catch { }
|
|
335
|
-
}
|
|
336
|
-
return;
|
|
357
|
+
}
|
|
358
|
+
// Multi-chunk or edit fallthrough: send new chunks FIRST, then delete placeholder
|
|
359
|
+
const totalChunks = chunks.length;
|
|
360
|
+
let firstSentMsgId;
|
|
361
|
+
const sendChunk = async (chunk, fallback, index) => {
|
|
362
|
+
const isFirst = index === 0 && !placeholderMsgId;
|
|
363
|
+
const isLast = index === totalChunks - 1;
|
|
364
|
+
// Pagination header for multi-chunk messages
|
|
365
|
+
const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
|
|
366
|
+
// Trim chunk if pageTag pushes it over the limit
|
|
367
|
+
let safeChunk = chunk;
|
|
368
|
+
if (pageTag.length + safeChunk.length > TELEGRAM_MAX_LENGTH) {
|
|
369
|
+
safeChunk = safeChunk.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
337
370
|
}
|
|
338
|
-
|
|
339
|
-
|
|
371
|
+
let safeFallback = fallback;
|
|
372
|
+
if (pageTag.length + safeFallback.length > TELEGRAM_MAX_LENGTH) {
|
|
373
|
+
safeFallback = safeFallback.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
340
374
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
let safeFallback = fallback;
|
|
357
|
-
if (pageTag.length + safeFallback.length > TELEGRAM_MAX_LENGTH) {
|
|
358
|
-
safeFallback = safeFallback.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
359
|
-
}
|
|
360
|
-
const opts = {
|
|
361
|
-
parse_mode: "HTML",
|
|
362
|
-
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
363
|
-
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
364
|
-
};
|
|
365
|
-
const fallbackOpts = {
|
|
366
|
-
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
367
|
-
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
375
|
+
const opts = {
|
|
376
|
+
parse_mode: "HTML",
|
|
377
|
+
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
378
|
+
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
379
|
+
};
|
|
380
|
+
const fallbackOpts = {
|
|
381
|
+
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
382
|
+
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
383
|
+
};
|
|
384
|
+
const sent = await ctx
|
|
385
|
+
.reply(pageTag + safeChunk, opts)
|
|
386
|
+
.catch(() => ctx.reply(pageTag + safeFallback, fallbackOpts));
|
|
387
|
+
if (index === 0 && sent)
|
|
388
|
+
firstSentMsgId = sent.message_id;
|
|
368
389
|
};
|
|
369
|
-
|
|
370
|
-
.reply(pageTag + safeChunk, opts)
|
|
371
|
-
.catch(() => ctx.reply(pageTag + safeFallback, fallbackOpts));
|
|
372
|
-
if (index === 0 && sent)
|
|
373
|
-
firstSentMsgId = sent.message_id;
|
|
374
|
-
};
|
|
375
|
-
let sendSucceeded = false;
|
|
376
|
-
try {
|
|
377
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
378
|
-
if (i > 0)
|
|
379
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
380
|
-
await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
|
|
381
|
-
}
|
|
382
|
-
sendSucceeded = true;
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
390
|
+
let sendSucceeded = false;
|
|
385
391
|
try {
|
|
386
|
-
for (let i = 0; i <
|
|
392
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
387
393
|
if (i > 0)
|
|
388
394
|
await new Promise((r) => setTimeout(r, 300));
|
|
389
|
-
|
|
390
|
-
const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
|
|
391
|
-
if (i === 0 && sent)
|
|
392
|
-
firstSentMsgId = sent.message_id;
|
|
395
|
+
await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
|
|
393
396
|
}
|
|
394
397
|
sendSucceeded = true;
|
|
395
398
|
}
|
|
396
399
|
catch {
|
|
397
|
-
|
|
400
|
+
try {
|
|
401
|
+
for (let i = 0; i < fallbackChunks.length; i++) {
|
|
402
|
+
if (i > 0)
|
|
403
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
404
|
+
const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
|
|
405
|
+
const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
|
|
406
|
+
if (i === 0 && sent)
|
|
407
|
+
firstSentMsgId = sent.message_id;
|
|
408
|
+
}
|
|
409
|
+
sendSucceeded = true;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
/* nothing more we can do */
|
|
413
|
+
}
|
|
398
414
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
415
|
+
// Only delete placeholder AFTER new messages sent successfully
|
|
416
|
+
if (placeholderMsgId && sendSucceeded) {
|
|
417
|
+
try {
|
|
418
|
+
await getBot().api.deleteMessage(chatId, placeholderMsgId);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
/* ignore — placeholder stays but user has the real message */
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Track bot message ID for reply-to context lookups
|
|
425
|
+
const botMsgId = firstSentMsgId ?? placeholderMsgId;
|
|
426
|
+
if (assistantLogId && botMsgId) {
|
|
427
|
+
try {
|
|
428
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
429
|
+
setConversationTelegramMsgId(assistantLogId, botMsgId);
|
|
430
|
+
}
|
|
431
|
+
catch { }
|
|
432
|
+
}
|
|
433
|
+
// React ✅ on the user's original message to signal completion
|
|
402
434
|
try {
|
|
403
|
-
await getBot().api.
|
|
435
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
404
436
|
}
|
|
405
437
|
catch {
|
|
406
|
-
/*
|
|
438
|
+
/* reactions may not be available */
|
|
407
439
|
}
|
|
408
440
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
414
|
-
setConversationTelegramMsgId(assistantLogId, botMsgId);
|
|
415
|
-
}
|
|
416
|
-
catch { }
|
|
441
|
+
finally {
|
|
442
|
+
placeholderMsgId = undefined;
|
|
443
|
+
lastEditedText = "";
|
|
417
444
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
// Progressive streaming: update placeholder periodically with delta threshold
|
|
449
|
+
const now = Date.now();
|
|
450
|
+
const textDelta = Math.abs(text.length - lastEditedText.length);
|
|
451
|
+
if (now - lastEditTime >= EDIT_INTERVAL_MS && textDelta >= MIN_EDIT_DELTA) {
|
|
452
|
+
lastEditTime = now;
|
|
453
|
+
// Show beginning + end for context instead of just the tail
|
|
454
|
+
let preview;
|
|
455
|
+
if (text.length > 4000) {
|
|
456
|
+
preview = text.slice(0, 1800) + "\n\n⋯\n\n" + text.slice(-1800);
|
|
421
457
|
}
|
|
422
|
-
|
|
423
|
-
|
|
458
|
+
else {
|
|
459
|
+
preview = text;
|
|
424
460
|
}
|
|
461
|
+
const statusLine = currentToolName ? `🔧 ${currentToolName}\n\n` : "";
|
|
462
|
+
enqueueEdit(statusLine + preview);
|
|
425
463
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
let preview;
|
|
440
|
-
if (text.length > 4000) {
|
|
441
|
-
preview = text.slice(0, 1800) + "\n\n⋯\n\n" + text.slice(-1800);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
preview = text;
|
|
445
|
-
}
|
|
446
|
-
const statusLine = currentToolName ? `🔧 ${currentToolName}\n\n` : "";
|
|
447
|
-
enqueueEdit(statusLine + preview);
|
|
464
|
+
}
|
|
465
|
+
}, onToolEvent, onUsage);
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
console.error("[nzb] sendToOrchestrator threw:", err instanceof Error ? err.message : err);
|
|
469
|
+
// Attempt to notify user of the failure
|
|
470
|
+
try {
|
|
471
|
+
const errorText = `⚠️ Error: Failed to process message. Please try again.`;
|
|
472
|
+
if (placeholderMsgId) {
|
|
473
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, errorText);
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
await ctx.reply(errorText, { reply_parameters: replyParams });
|
|
448
477
|
}
|
|
449
478
|
}
|
|
450
|
-
|
|
479
|
+
catch {
|
|
480
|
+
/* nothing more we can do */
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
stopTypingAndClearTimeout();
|
|
485
|
+
}
|
|
451
486
|
});
|
|
452
487
|
}
|
|
453
488
|
//# sourceMappingURL=streaming.js.map
|
|
@@ -82,16 +82,37 @@ export function buildSmartSuggestions(response, prompt, maxButtons = 4) {
|
|
|
82
82
|
}
|
|
83
83
|
// In-memory store for pending smart suggestion prompts (TTL: 5 minutes)
|
|
84
84
|
const pendingPrompts = new Map();
|
|
85
|
+
const SUGGESTION_MAX_AGE_MS = 5 * 60_000;
|
|
86
|
+
const SUGGESTION_CLEANUP_INTERVAL_MS = 60_000;
|
|
87
|
+
const MAX_PENDING_PROMPTS = 1000;
|
|
88
|
+
// Periodic cleanup to prevent unbounded memory growth
|
|
89
|
+
setInterval(() => {
|
|
90
|
+
const cutoff = Date.now() - SUGGESTION_MAX_AGE_MS;
|
|
91
|
+
for (const [key, value] of pendingPrompts) {
|
|
92
|
+
if (value.timestamp < cutoff) {
|
|
93
|
+
pendingPrompts.delete(key);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}, SUGGESTION_CLEANUP_INTERVAL_MS).unref();
|
|
85
97
|
/** Store the context for smart suggestion callbacks. */
|
|
86
98
|
export function storeSuggestionContext(callbackPrefix, timeKey, prompt, response) {
|
|
87
99
|
const key = `${callbackPrefix}:${timeKey}`;
|
|
88
100
|
pendingPrompts.set(key, { prompt, response, timestamp: Date.now() });
|
|
89
101
|
// Cleanup old entries (older than 5 minutes)
|
|
90
|
-
const cutoff = Date.now() -
|
|
102
|
+
const cutoff = Date.now() - SUGGESTION_MAX_AGE_MS;
|
|
91
103
|
for (const [k, v] of pendingPrompts) {
|
|
92
104
|
if (v.timestamp < cutoff)
|
|
93
105
|
pendingPrompts.delete(k);
|
|
94
106
|
}
|
|
107
|
+
// Enforce max size to prevent unbounded growth
|
|
108
|
+
if (pendingPrompts.size > MAX_PENDING_PROMPTS) {
|
|
109
|
+
const entries = Array.from(pendingPrompts.entries())
|
|
110
|
+
.sort((a, b) => a[1].timestamp - b[1].timestamp);
|
|
111
|
+
const toRemove = entries.slice(0, pendingPrompts.size - MAX_PENDING_PROMPTS);
|
|
112
|
+
for (const [rmKey] of toRemove) {
|
|
113
|
+
pendingPrompts.delete(rmKey);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
95
116
|
}
|
|
96
117
|
/** Register callback handlers for smart suggestion buttons. */
|
|
97
118
|
export function registerSmartSuggestionHandlers(bot) {
|
|
@@ -23,8 +23,8 @@ export async function sendLog(level, message) {
|
|
|
23
23
|
try {
|
|
24
24
|
await botRef.api.sendMessage(config.logChannelId, header + body, { parse_mode: "HTML" });
|
|
25
25
|
}
|
|
26
|
-
catch {
|
|
27
|
-
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error("[nzb] Log channel send failed:", err instanceof Error ? err.message : err);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
/** Convenience wrappers */
|