@iletai/nzb 1.6.2 → 1.6.4
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/telegram/bot.js +2 -1
- package/dist/telegram/formatter.js +12 -1
- package/dist/telegram/handlers/commands.js +22 -5
- package/dist/telegram/handlers/inline.js +1 -1
- package/dist/telegram/handlers/streaming.js +24 -10
- package/dist/telegram/log-channel.js +5 -2
- package/dist/telegram/menus.js +8 -4
- package/package.json +1 -1
package/dist/telegram/bot.js
CHANGED
|
@@ -212,7 +212,8 @@ export async function sendWorkerNotification(message) {
|
|
|
212
212
|
if (!bot || config.authorizedUserId === undefined)
|
|
213
213
|
return;
|
|
214
214
|
try {
|
|
215
|
-
await
|
|
215
|
+
const { truncateForTelegram } = await import("./formatter.js");
|
|
216
|
+
await bot.api.sendMessage(config.authorizedUserId, truncateForTelegram(message));
|
|
216
217
|
}
|
|
217
218
|
catch {
|
|
218
219
|
// best-effort — don't crash if notification fails
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const TELEGRAM_MAX_LENGTH = 4096;
|
|
1
|
+
export const TELEGRAM_MAX_LENGTH = 4096;
|
|
2
2
|
// Reserve space for tag closure and pagination prefix
|
|
3
3
|
const CHUNK_TARGET = TELEGRAM_MAX_LENGTH - 40;
|
|
4
4
|
// Telegram HTML tags we track for proper tag closure across chunks
|
|
@@ -27,6 +27,11 @@ export function isChatNotFoundError(err) {
|
|
|
27
27
|
const msg = err instanceof Error ? err.message : String(err);
|
|
28
28
|
return /chat not found/i.test(msg);
|
|
29
29
|
}
|
|
30
|
+
/** Detect Telegram "message is too long" errors. */
|
|
31
|
+
export function isMessageTooLongError(err) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
33
|
+
return /message is too long/i.test(msg);
|
|
34
|
+
}
|
|
30
35
|
/**
|
|
31
36
|
* Track open HTML tags in a segment for proper closure/reopening across chunks.
|
|
32
37
|
* Returns the stack of currently open tag names (outermost first).
|
|
@@ -80,6 +85,12 @@ function findSafeSplitIndex(text, targetIndex) {
|
|
|
80
85
|
}
|
|
81
86
|
return targetIndex;
|
|
82
87
|
}
|
|
88
|
+
/** Truncate text to fit within Telegram's message length limit. */
|
|
89
|
+
export function truncateForTelegram(text, limit = TELEGRAM_MAX_LENGTH) {
|
|
90
|
+
if (text.length <= limit)
|
|
91
|
+
return text;
|
|
92
|
+
return text.slice(0, limit - 4) + " ⋯";
|
|
93
|
+
}
|
|
83
94
|
/**
|
|
84
95
|
* Split a long message into chunks that fit within Telegram's message limit.
|
|
85
96
|
* Full HTML-aware: tracks all open tags (pre, code, blockquote, b, i, s, u, a, tg-spoiler)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { config, persistEnvVar, persistModel } from "../../config.js";
|
|
2
|
-
import { cancelCurrentMessage, compactSession, getQueueSize, getWorkers, resetSession } from "../../copilot/orchestrator.js";
|
|
2
|
+
import { cancelCurrentMessage, compactSession, getQueueSize, getWorkers, resetSession, } from "../../copilot/orchestrator.js";
|
|
3
3
|
import { listSkills } from "../../copilot/skills.js";
|
|
4
4
|
import { restartDaemon } from "../../daemon.js";
|
|
5
5
|
import { searchMemories } from "../../store/db.js";
|
|
6
|
+
import { chunkMessage } from "../formatter.js";
|
|
6
7
|
import { buildSettingsText, formatMemoryList } from "../menus.js";
|
|
7
8
|
import { getReactionHelpText } from "./reactions.js";
|
|
8
9
|
export function registerCommandHandlers(bot, deps) {
|
|
@@ -133,7 +134,11 @@ export function registerCommandHandlers(bot, deps) {
|
|
|
133
134
|
await ctx.reply("No memories stored.");
|
|
134
135
|
}
|
|
135
136
|
else {
|
|
136
|
-
|
|
137
|
+
const formatted = formatMemoryList(memories);
|
|
138
|
+
const chunks = chunkMessage(formatted);
|
|
139
|
+
for (const chunk of chunks) {
|
|
140
|
+
await ctx.reply(chunk, { parse_mode: "HTML" });
|
|
141
|
+
}
|
|
137
142
|
}
|
|
138
143
|
});
|
|
139
144
|
bot.command("skills", async (ctx) => {
|
|
@@ -143,7 +148,11 @@ export function registerCommandHandlers(bot, deps) {
|
|
|
143
148
|
}
|
|
144
149
|
else {
|
|
145
150
|
const lines = skills.map((s) => `• ${s.name} (${s.source}) — ${s.description}`);
|
|
146
|
-
|
|
151
|
+
const text = lines.join("\n");
|
|
152
|
+
const chunks = chunkMessage(text);
|
|
153
|
+
for (const chunk of chunks) {
|
|
154
|
+
await ctx.reply(chunk);
|
|
155
|
+
}
|
|
147
156
|
}
|
|
148
157
|
});
|
|
149
158
|
bot.command("workers", async (ctx) => {
|
|
@@ -153,7 +162,11 @@ export function registerCommandHandlers(bot, deps) {
|
|
|
153
162
|
}
|
|
154
163
|
else {
|
|
155
164
|
const lines = workers.map((w) => `• ${w.name} (${w.workingDir}) — ${w.status}`);
|
|
156
|
-
|
|
165
|
+
const text = lines.join("\n");
|
|
166
|
+
const chunks = chunkMessage(text);
|
|
167
|
+
for (const chunk of chunks) {
|
|
168
|
+
await ctx.reply(chunk);
|
|
169
|
+
}
|
|
157
170
|
}
|
|
158
171
|
});
|
|
159
172
|
bot.command("status", async (ctx) => {
|
|
@@ -206,7 +219,11 @@ export function registerCommandHandlers(bot, deps) {
|
|
|
206
219
|
await ctx.reply("No memories stored.");
|
|
207
220
|
}
|
|
208
221
|
else {
|
|
209
|
-
|
|
222
|
+
const formatted = formatMemoryList(memories);
|
|
223
|
+
const chunks = chunkMessage(formatted);
|
|
224
|
+
for (const chunk of chunks) {
|
|
225
|
+
await ctx.reply(chunk, { parse_mode: "HTML" });
|
|
226
|
+
}
|
|
210
227
|
}
|
|
211
228
|
});
|
|
212
229
|
bot.hears("🔄 Restart", async (ctx) => {
|
|
@@ -67,7 +67,7 @@ export function registerInlineQueryHandler(bot) {
|
|
|
67
67
|
try {
|
|
68
68
|
const chatId = ctx.chat?.id;
|
|
69
69
|
if (chatId) {
|
|
70
|
-
const truncated = text.length >
|
|
70
|
+
const truncated = text.length > 3900 ? text.slice(0, 3900) + "\n\n⋯" : text;
|
|
71
71
|
await bot.api.sendMessage(chatId, `🔍 <b>Detailed Answer:</b>\n\n${escapeHtml(truncated)}`, {
|
|
72
72
|
parse_mode: "HTML",
|
|
73
73
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
2
|
import { config } from "../../config.js";
|
|
3
3
|
import { getQueueSize, sendToOrchestrator } from "../../copilot/orchestrator.js";
|
|
4
|
-
import { chunkMessage, escapeHtml, formatToolSummaryExpandable, isHtmlParseError, isMessageNotModifiedError, toTelegramHTML, } from "../formatter.js";
|
|
4
|
+
import { chunkMessage, escapeHtml, formatToolSummaryExpandable, isHtmlParseError, isMessageNotModifiedError, isMessageTooLongError, TELEGRAM_MAX_LENGTH, toTelegramHTML, } from "../formatter.js";
|
|
5
5
|
import { logDebug, logError, logInfo } from "../log-channel.js";
|
|
6
6
|
import { editSafe } from "../safe-api.js";
|
|
7
7
|
import { createSmartSuggestionsWithContext } from "./suggestions.js";
|
|
@@ -78,13 +78,18 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
78
78
|
const enqueueEdit = (text) => {
|
|
79
79
|
if (finalized || text === lastEditedText)
|
|
80
80
|
return;
|
|
81
|
+
// Clamp streaming previews to Telegram's limit — these are ephemeral
|
|
82
|
+
let safeText = text;
|
|
83
|
+
if (safeText.length > TELEGRAM_MAX_LENGTH) {
|
|
84
|
+
safeText = safeText.slice(0, TELEGRAM_MAX_LENGTH - 4) + " ⋯";
|
|
85
|
+
}
|
|
81
86
|
editChain = editChain
|
|
82
87
|
.then(async () => {
|
|
83
|
-
if (finalized ||
|
|
88
|
+
if (finalized || safeText === lastEditedText)
|
|
84
89
|
return;
|
|
85
90
|
if (!placeholderMsgId) {
|
|
86
91
|
// Don't create a placeholder for tiny initial chunks — wait for meaningful content
|
|
87
|
-
if (
|
|
92
|
+
if (safeText.length < MIN_INITIAL_CHARS && !safeText.startsWith("🔧") && !safeText.startsWith("✅"))
|
|
88
93
|
return;
|
|
89
94
|
// Let the typing indicator show for at least a short period
|
|
90
95
|
const elapsed = Date.now() - handlerStartTime;
|
|
@@ -94,7 +99,7 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
94
99
|
if (finalized)
|
|
95
100
|
return;
|
|
96
101
|
try {
|
|
97
|
-
const msg = await ctx.reply(
|
|
102
|
+
const msg = await ctx.reply(safeText, { reply_parameters: replyParams });
|
|
98
103
|
placeholderMsgId = msg.message_id;
|
|
99
104
|
// Stop typing once placeholder is visible — edits serve as the indicator now
|
|
100
105
|
stopTyping();
|
|
@@ -105,16 +110,16 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
105
110
|
}
|
|
106
111
|
else {
|
|
107
112
|
try {
|
|
108
|
-
await editSafe(getBot().api, chatId, placeholderMsgId,
|
|
113
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, safeText);
|
|
109
114
|
}
|
|
110
115
|
catch (err) {
|
|
111
|
-
// Silently ignore "message is not modified"
|
|
112
|
-
if (isMessageNotModifiedError(err))
|
|
116
|
+
// Silently ignore "message is not modified" or "too long" during streaming
|
|
117
|
+
if (isMessageNotModifiedError(err) || isMessageTooLongError(err))
|
|
113
118
|
return;
|
|
114
119
|
return;
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
|
-
lastEditedText =
|
|
122
|
+
lastEditedText = safeText;
|
|
118
123
|
})
|
|
119
124
|
.catch(() => { });
|
|
120
125
|
};
|
|
@@ -338,6 +343,15 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
338
343
|
const isLast = index === totalChunks - 1;
|
|
339
344
|
// Pagination header for multi-chunk messages
|
|
340
345
|
const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
|
|
346
|
+
// Trim chunk if pageTag pushes it over the limit
|
|
347
|
+
let safeChunk = chunk;
|
|
348
|
+
if (pageTag.length + safeChunk.length > TELEGRAM_MAX_LENGTH) {
|
|
349
|
+
safeChunk = safeChunk.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
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
|
+
}
|
|
341
355
|
const opts = {
|
|
342
356
|
parse_mode: "HTML",
|
|
343
357
|
...(isFirst ? { reply_parameters: replyParams } : {}),
|
|
@@ -348,8 +362,8 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
348
362
|
...(isLast && smartKb ? { reply_markup: smartKb } : {}),
|
|
349
363
|
};
|
|
350
364
|
const sent = await ctx
|
|
351
|
-
.reply(pageTag +
|
|
352
|
-
.catch(() => ctx.reply(pageTag +
|
|
365
|
+
.reply(pageTag + safeChunk, opts)
|
|
366
|
+
.catch(() => ctx.reply(pageTag + safeFallback, fallbackOpts));
|
|
353
367
|
if (index === 0 && sent)
|
|
354
368
|
firstSentMsgId = sent.message_id;
|
|
355
369
|
};
|
|
@@ -10,15 +10,18 @@ const ICONS = {
|
|
|
10
10
|
error: "🔴",
|
|
11
11
|
debug: "🔍",
|
|
12
12
|
};
|
|
13
|
+
const MAX_LOG_LENGTH = 4096;
|
|
13
14
|
/** Send a log message to the configured Telegram channel */
|
|
14
15
|
export async function sendLog(level, message) {
|
|
15
16
|
if (!botRef || !config.logChannelId)
|
|
16
17
|
return;
|
|
17
18
|
const icon = ICONS[level];
|
|
18
19
|
const timestamp = new Date().toISOString().replace("T", " ").slice(0, 19);
|
|
19
|
-
const
|
|
20
|
+
const header = `${icon} <b>[${level.toUpperCase()}]</b> <code>${timestamp}</code>\n`;
|
|
21
|
+
const maxBody = MAX_LOG_LENGTH - header.length - 4;
|
|
22
|
+
const body = message.length > maxBody ? escapeHtml(message.slice(0, maxBody)) + " ⋯" : escapeHtml(message);
|
|
20
23
|
try {
|
|
21
|
-
await botRef.api.sendMessage(config.logChannelId,
|
|
24
|
+
await botRef.api.sendMessage(config.logChannelId, header + body, { parse_mode: "HTML" });
|
|
22
25
|
}
|
|
23
26
|
catch {
|
|
24
27
|
// best-effort — don't crash if log channel is unreachable
|
package/dist/telegram/menus.js
CHANGED
|
@@ -3,7 +3,7 @@ import { config, persistEnvVar, persistModel } from "../config.js";
|
|
|
3
3
|
import { cancelCurrentMessage, getQueueSize, getWorkers } from "../copilot/orchestrator.js";
|
|
4
4
|
import { listSkills } from "../copilot/skills.js";
|
|
5
5
|
import { searchMemories } from "../store/db.js";
|
|
6
|
-
import { escapeHtml } from "./formatter.js";
|
|
6
|
+
import { chunkMessage, escapeHtml, truncateForTelegram } from "./formatter.js";
|
|
7
7
|
// Worker timeout presets (ms → display label)
|
|
8
8
|
export const TIMEOUT_PRESETS = [
|
|
9
9
|
{ ms: 600_000, label: "10min" },
|
|
@@ -174,7 +174,7 @@ export function createMenus(getUptimeStr) {
|
|
|
174
174
|
}
|
|
175
175
|
else {
|
|
176
176
|
const lines = workers.map((w) => `• ${w.name} (${w.workingDir}) — ${w.status}`);
|
|
177
|
-
await ctx.reply(lines.join("\n"));
|
|
177
|
+
await ctx.reply(truncateForTelegram(lines.join("\n")));
|
|
178
178
|
}
|
|
179
179
|
})
|
|
180
180
|
.text("🧠 Skills", async (ctx) => {
|
|
@@ -185,7 +185,7 @@ export function createMenus(getUptimeStr) {
|
|
|
185
185
|
}
|
|
186
186
|
else {
|
|
187
187
|
const lines = skills.map((s) => `• ${s.name} (${s.source}) — ${s.description}`);
|
|
188
|
-
await ctx.reply(lines.join("\n"));
|
|
188
|
+
await ctx.reply(truncateForTelegram(lines.join("\n")));
|
|
189
189
|
}
|
|
190
190
|
})
|
|
191
191
|
.row()
|
|
@@ -196,7 +196,11 @@ export function createMenus(getUptimeStr) {
|
|
|
196
196
|
await ctx.reply("No memories stored.");
|
|
197
197
|
}
|
|
198
198
|
else {
|
|
199
|
-
|
|
199
|
+
const formatted = formatMemoryList(memories);
|
|
200
|
+
const chunks = chunkMessage(formatted);
|
|
201
|
+
for (const chunk of chunks) {
|
|
202
|
+
await ctx.reply(chunk, { parse_mode: "HTML" });
|
|
203
|
+
}
|
|
200
204
|
}
|
|
201
205
|
})
|
|
202
206
|
.submenu("⚙️ Settings", "settings-menu", async (ctx) => {
|