@iletai/nzb 1.6.4 → 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 +14 -2
- package/dist/copilot/client.js +17 -17
- package/dist/copilot/mcp-config.js +2 -0
- package/dist/copilot/orchestrator.js +192 -55
- package/dist/copilot/skills.js +4 -2
- package/dist/copilot/tools.js +39 -16
- package/dist/copilot/types.js +2 -0
- package/dist/daemon.js +13 -12
- 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 +62 -10
- package/dist/telegram/handlers/streaming.js +252 -207
- package/dist/telegram/handlers/suggestions.js +22 -1
- package/dist/telegram/log-channel.js +2 -2
- package/dist/telegram/menus.js +247 -91
- 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 +3 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { InlineKeyboard } from "grammy";
|
|
2
3
|
import { config } from "../../config.js";
|
|
3
4
|
import { getQueueSize, sendToOrchestrator } from "../../copilot/orchestrator.js";
|
|
@@ -56,6 +57,18 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
56
57
|
}
|
|
57
58
|
};
|
|
58
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
|
+
};
|
|
59
72
|
// Progressive streaming state — all Telegram API calls are serialized through editChain
|
|
60
73
|
// to prevent duplicate placeholder messages and race conditions
|
|
61
74
|
let placeholderMsgId;
|
|
@@ -75,6 +88,7 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
75
88
|
// prevents push notification spam for very short initial chunks (inspired by OpenClaw's draft-stream).
|
|
76
89
|
const MIN_INITIAL_CHARS = 80;
|
|
77
90
|
const handlerStartTime = Date.now();
|
|
91
|
+
const requestId = randomUUID();
|
|
78
92
|
const enqueueEdit = (text) => {
|
|
79
93
|
if (finalized || text === lastEditedText)
|
|
80
94
|
return;
|
|
@@ -102,13 +116,15 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
102
116
|
const msg = await ctx.reply(safeText, { reply_parameters: replyParams });
|
|
103
117
|
placeholderMsgId = msg.message_id;
|
|
104
118
|
// Stop typing once placeholder is visible — edits serve as the indicator now
|
|
105
|
-
|
|
119
|
+
stopTypingAndClearTimeout();
|
|
106
120
|
}
|
|
107
121
|
catch {
|
|
108
122
|
return;
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
else {
|
|
126
|
+
if (finalized)
|
|
127
|
+
return;
|
|
112
128
|
try {
|
|
113
129
|
await editSafe(getBot().api, chatId, placeholderMsgId, safeText);
|
|
114
130
|
}
|
|
@@ -121,7 +137,9 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
121
137
|
}
|
|
122
138
|
lastEditedText = safeText;
|
|
123
139
|
})
|
|
124
|
-
.catch(() => {
|
|
140
|
+
.catch((err) => {
|
|
141
|
+
console.error("[nzb] Edit chain error:", err instanceof Error ? err.message : err);
|
|
142
|
+
});
|
|
125
143
|
};
|
|
126
144
|
const onToolEvent = (event) => {
|
|
127
145
|
console.log(`[nzb] Bot received tool event: ${event.type} ${event.toolName}`);
|
|
@@ -194,106 +212,73 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
194
212
|
userPrompt = `[Replying to: "${quoted}"]\n\n${userPrompt}`;
|
|
195
213
|
}
|
|
196
214
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const isError = text.startsWith("Error:");
|
|
208
|
-
if (isError) {
|
|
209
|
-
void logError(`Response error: ${text.slice(0, 200)}`);
|
|
210
|
-
const errorText = `⚠️ ${text}`;
|
|
211
|
-
const errorKb = new InlineKeyboard().text("🔄 Retry", "retry").text("📖 Explain", "explain_error");
|
|
212
|
-
if (placeholderMsgId) {
|
|
213
|
-
try {
|
|
214
|
-
await editSafe(getBot().api, chatId, placeholderMsgId, errorText, { reply_markup: errorKb });
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
/* fall through */
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
try {
|
|
222
|
-
await ctx.reply(errorText, { reply_parameters: replyParams, reply_markup: errorKb });
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
/* nothing more we can do */
|
|
226
|
-
}
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
let textWithMeta = text;
|
|
230
|
-
if (usageInfo && config.usageMode !== "off") {
|
|
231
|
-
const fmtTokens = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n));
|
|
232
|
-
const parts = [];
|
|
233
|
-
if (config.usageMode === "full" && usageInfo.model)
|
|
234
|
-
parts.push(usageInfo.model);
|
|
235
|
-
parts.push(`⬆${fmtTokens(usageInfo.inputTokens)} ⬇${fmtTokens(usageInfo.outputTokens)}`);
|
|
236
|
-
const totalTokens = usageInfo.inputTokens + usageInfo.outputTokens;
|
|
237
|
-
parts.push(`Σ${fmtTokens(totalTokens)}`);
|
|
238
|
-
if (config.usageMode === "full" && usageInfo.duration)
|
|
239
|
-
parts.push(`${(usageInfo.duration / 1000).toFixed(1)}s`);
|
|
240
|
-
textWithMeta += `\n\n📊 ${parts.join(" · ")}`;
|
|
241
|
-
}
|
|
242
|
-
const formatted = toTelegramHTML(textWithMeta);
|
|
243
|
-
let fullFormatted = formatted;
|
|
244
|
-
if (config.showReasoning && toolHistory.length > 0) {
|
|
245
|
-
const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs, detail: t.detail })), {
|
|
246
|
-
elapsedMs: Date.now() - handlerStartTime,
|
|
247
|
-
model: usageInfo?.model,
|
|
248
|
-
inputTokens: usageInfo?.inputTokens,
|
|
249
|
-
outputTokens: usageInfo?.outputTokens,
|
|
250
|
-
});
|
|
251
|
-
fullFormatted += expandable;
|
|
252
|
-
}
|
|
253
|
-
const chunks = chunkMessage(fullFormatted);
|
|
254
|
-
const fallbackChunks = chunkMessage(textWithMeta);
|
|
255
|
-
// Build smart suggestion buttons based on response content
|
|
256
|
-
const smartKb = createSmartSuggestionsWithContext(text, ctx.message.text, 4);
|
|
257
|
-
// Single chunk: edit placeholder in place
|
|
258
|
-
if (placeholderMsgId && chunks.length === 1) {
|
|
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 () => {
|
|
259
225
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
}
|
|
272
240
|
}
|
|
273
|
-
catch { }
|
|
274
|
-
}
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
catch (err) {
|
|
278
|
-
// "message is not modified" is harmless — placeholder already has this content
|
|
279
|
-
if (isMessageNotModifiedError(err)) {
|
|
280
241
|
try {
|
|
281
|
-
await
|
|
242
|
+
await ctx.reply(errorText, { reply_parameters: replyParams, reply_markup: errorKb });
|
|
282
243
|
}
|
|
283
|
-
catch {
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
287
|
-
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
288
|
-
}
|
|
289
|
-
catch { }
|
|
244
|
+
catch {
|
|
245
|
+
/* nothing more we can do */
|
|
290
246
|
}
|
|
291
247
|
return;
|
|
292
248
|
}
|
|
293
|
-
|
|
294
|
-
if (
|
|
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(" · ")}`;
|
|
261
|
+
}
|
|
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,
|
|
270
|
+
});
|
|
271
|
+
fullFormatted += expandable;
|
|
272
|
+
}
|
|
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) {
|
|
295
279
|
try {
|
|
296
|
-
await editSafe(getBot().api, chatId, placeholderMsgId,
|
|
280
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, chunks[0], {
|
|
281
|
+
parse_mode: "HTML",
|
|
297
282
|
reply_markup: smartKb,
|
|
298
283
|
});
|
|
299
284
|
try {
|
|
@@ -309,135 +294,195 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
309
294
|
}
|
|
310
295
|
return;
|
|
311
296
|
}
|
|
312
|
-
catch {
|
|
313
|
-
|
|
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
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
|
|
338
|
+
reply_markup: smartKb,
|
|
339
|
+
});
|
|
340
|
+
try {
|
|
341
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
342
|
+
}
|
|
343
|
+
catch { }
|
|
344
|
+
if (assistantLogId) {
|
|
345
|
+
try {
|
|
346
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
347
|
+
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
/* fall through to send new messages */
|
|
355
|
+
}
|
|
314
356
|
}
|
|
315
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) + " ⋯";
|
|
370
|
+
}
|
|
371
|
+
let safeFallback = fallback;
|
|
372
|
+
if (pageTag.length + safeFallback.length > TELEGRAM_MAX_LENGTH) {
|
|
373
|
+
safeFallback = safeFallback.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
374
|
+
}
|
|
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;
|
|
389
|
+
};
|
|
390
|
+
let sendSucceeded = false;
|
|
316
391
|
try {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
392
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
393
|
+
if (i > 0)
|
|
394
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
395
|
+
await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
|
|
322
396
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
397
|
+
sendSucceeded = true;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
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;
|
|
328
408
|
}
|
|
329
|
-
|
|
409
|
+
sendSucceeded = true;
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
/* nothing more we can do */
|
|
330
413
|
}
|
|
331
|
-
return;
|
|
332
414
|
}
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
}
|
|
335
423
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
let safeFallback = fallback;
|
|
352
|
-
if (pageTag.length + safeFallback.length > TELEGRAM_MAX_LENGTH) {
|
|
353
|
-
safeFallback = safeFallback.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
354
|
-
}
|
|
355
|
-
const opts = {
|
|
356
|
-
parse_mode: "HTML",
|
|
357
|
-
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
358
|
-
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
359
|
-
};
|
|
360
|
-
const fallbackOpts = {
|
|
361
|
-
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
362
|
-
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
363
|
-
};
|
|
364
|
-
const sent = await ctx
|
|
365
|
-
.reply(pageTag + safeChunk, opts)
|
|
366
|
-
.catch(() => ctx.reply(pageTag + safeFallback, fallbackOpts));
|
|
367
|
-
if (index === 0 && sent)
|
|
368
|
-
firstSentMsgId = sent.message_id;
|
|
369
|
-
};
|
|
370
|
-
let sendSucceeded = false;
|
|
371
|
-
try {
|
|
372
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
373
|
-
if (i > 0)
|
|
374
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
375
|
-
await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
|
|
376
|
-
}
|
|
377
|
-
sendSucceeded = true;
|
|
378
|
-
}
|
|
379
|
-
catch {
|
|
380
|
-
try {
|
|
381
|
-
for (let i = 0; i < fallbackChunks.length; i++) {
|
|
382
|
-
if (i > 0)
|
|
383
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
384
|
-
const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
|
|
385
|
-
const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
|
|
386
|
-
if (i === 0 && sent)
|
|
387
|
-
firstSentMsgId = sent.message_id;
|
|
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
|
|
434
|
+
try {
|
|
435
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
/* reactions may not be available */
|
|
388
439
|
}
|
|
389
|
-
sendSucceeded = true;
|
|
390
|
-
}
|
|
391
|
-
catch {
|
|
392
|
-
/* nothing more we can do */
|
|
393
440
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
await getBot().api.deleteMessage(chatId, placeholderMsgId);
|
|
441
|
+
finally {
|
|
442
|
+
placeholderMsgId = undefined;
|
|
443
|
+
lastEditedText = "";
|
|
399
444
|
}
|
|
400
|
-
|
|
401
|
-
|
|
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);
|
|
402
457
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
const botMsgId = firstSentMsgId ?? placeholderMsgId;
|
|
406
|
-
if (assistantLogId && botMsgId) {
|
|
407
|
-
try {
|
|
408
|
-
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
409
|
-
setConversationTelegramMsgId(assistantLogId, botMsgId);
|
|
458
|
+
else {
|
|
459
|
+
preview = text;
|
|
410
460
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
// React ✅ on the user's original message to signal completion
|
|
414
|
-
try {
|
|
415
|
-
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
/* reactions may not be available */
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
// Progressive streaming: update placeholder periodically with delta threshold
|
|
424
|
-
const now = Date.now();
|
|
425
|
-
const textDelta = Math.abs(text.length - lastEditedText.length);
|
|
426
|
-
if (now - lastEditTime >= EDIT_INTERVAL_MS && textDelta >= MIN_EDIT_DELTA) {
|
|
427
|
-
lastEditTime = now;
|
|
428
|
-
// Show beginning + end for context instead of just the tail
|
|
429
|
-
let preview;
|
|
430
|
-
if (text.length > 4000) {
|
|
431
|
-
preview = text.slice(0, 1800) + "\n\n⋯\n\n" + text.slice(-1800);
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
preview = text;
|
|
461
|
+
const statusLine = currentToolName ? `🔧 ${currentToolName}\n\n` : "";
|
|
462
|
+
enqueueEdit(statusLine + preview);
|
|
435
463
|
}
|
|
436
|
-
|
|
437
|
-
|
|
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 });
|
|
438
477
|
}
|
|
439
478
|
}
|
|
440
|
-
|
|
479
|
+
catch {
|
|
480
|
+
/* nothing more we can do */
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
stopTypingAndClearTimeout();
|
|
485
|
+
}
|
|
441
486
|
});
|
|
442
487
|
}
|
|
443
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 */
|