@iletai/nzb 1.4.0 → 1.4.2
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 +78 -142
- package/dist/telegram/formatter.js +19 -11
- package/package.json +1 -1
package/dist/telegram/bot.js
CHANGED
|
@@ -2,6 +2,7 @@ import { autoRetry } from "@grammyjs/auto-retry";
|
|
|
2
2
|
import { Menu } from "@grammyjs/menu";
|
|
3
3
|
import { Bot, InlineKeyboard, Keyboard } from "grammy";
|
|
4
4
|
import { Agent as HttpsAgent } from "https";
|
|
5
|
+
import { rmSync } from "fs";
|
|
5
6
|
import { config, persistEnvVar, persistModel } from "../config.js";
|
|
6
7
|
import { cancelCurrentMessage, getQueueSize, getWorkers, sendToOrchestrator } from "../copilot/orchestrator.js";
|
|
7
8
|
import { listSkills } from "../copilot/skills.js";
|
|
@@ -19,6 +20,61 @@ function getUptimeStr() {
|
|
|
19
20
|
const seconds = uptime % 60;
|
|
20
21
|
return hours > 0 ? `${hours}h ${minutes}m ${seconds}s` : minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Send a formatted HTML reply with multi-chunk support and fallback to plain text.
|
|
25
|
+
* Consolidates the repeated toTelegramHTML → chunkMessage → fallback → send pattern.
|
|
26
|
+
*/
|
|
27
|
+
async function sendFormattedReply(botInstance, chatId, text, opts) {
|
|
28
|
+
const formatted = toTelegramHTML(text);
|
|
29
|
+
const chunks = chunkMessage(formatted);
|
|
30
|
+
const fallbackChunks = chunkMessage(text);
|
|
31
|
+
let firstMsgId;
|
|
32
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
33
|
+
if (i > 0)
|
|
34
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
35
|
+
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
36
|
+
const replyParams = i === 0 && opts?.replyTo ? { message_id: opts.replyTo } : undefined;
|
|
37
|
+
try {
|
|
38
|
+
const sent = await botInstance.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
39
|
+
parse_mode: "HTML",
|
|
40
|
+
reply_parameters: replyParams,
|
|
41
|
+
});
|
|
42
|
+
if (i === 0)
|
|
43
|
+
firstMsgId = sent.message_id;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
try {
|
|
47
|
+
const sent = await botInstance.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
48
|
+
reply_parameters: replyParams,
|
|
49
|
+
});
|
|
50
|
+
if (i === 0)
|
|
51
|
+
firstMsgId = sent.message_id;
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (opts?.assistantLogId && firstMsgId) {
|
|
57
|
+
try {
|
|
58
|
+
const { setConversationTelegramMsgId } = await import("../store/db.js");
|
|
59
|
+
setConversationTelegramMsgId(opts.assistantLogId, firstMsgId);
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await botInstance.api.setMessageReaction(chatId, opts?.replyTo ?? 0, [{ type: "emoji", emoji: "👍" }]);
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
return firstMsgId;
|
|
68
|
+
}
|
|
69
|
+
/** Remove a temp directory after a delay (gives orchestrator time to use the file). */
|
|
70
|
+
function scheduleTempCleanup(dirPath, delayMs = 5 * 60_000) {
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
try {
|
|
73
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
catch { }
|
|
76
|
+
}, delayMs);
|
|
77
|
+
}
|
|
22
78
|
// Settings sub-menu
|
|
23
79
|
const settingsMenu = new Menu("settings-menu")
|
|
24
80
|
.text((ctx) => `${config.showReasoning ? "✅" : "❌"} Show Reasoning`, async (ctx) => {
|
|
@@ -164,10 +220,10 @@ export function createBot() {
|
|
|
164
220
|
const chatId = ctx.chat.id;
|
|
165
221
|
sendToOrchestrator(retryPrompt, { type: "telegram", chatId, messageId: originalMsg.message_id }, (text, done) => {
|
|
166
222
|
if (done) {
|
|
167
|
-
const formatted = toTelegramHTML(text);
|
|
168
|
-
const chunks = chunkMessage(formatted);
|
|
169
223
|
void (async () => {
|
|
170
224
|
try {
|
|
225
|
+
const formatted = toTelegramHTML(text);
|
|
226
|
+
const chunks = chunkMessage(formatted);
|
|
171
227
|
await bot.api.editMessageText(chatId, originalMsg.message_id, chunks[0], { parse_mode: "HTML" });
|
|
172
228
|
}
|
|
173
229
|
catch {
|
|
@@ -187,21 +243,8 @@ export function createBot() {
|
|
|
187
243
|
if (originalMsg && "text" in originalMsg && originalMsg.text) {
|
|
188
244
|
const chatId = ctx.chat.id;
|
|
189
245
|
sendToOrchestrator(`Explain this error in simple terms and suggest a fix:\n${originalMsg.text}`, { type: "telegram", chatId, messageId: originalMsg.message_id }, (text, done) => {
|
|
190
|
-
if (done)
|
|
191
|
-
|
|
192
|
-
const chunks = chunkMessage(formatted);
|
|
193
|
-
void (async () => {
|
|
194
|
-
try {
|
|
195
|
-
await ctx.reply(chunks[0], { parse_mode: "HTML" });
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
try {
|
|
199
|
-
await ctx.reply(text);
|
|
200
|
-
}
|
|
201
|
-
catch { }
|
|
202
|
-
}
|
|
203
|
-
})();
|
|
204
|
-
}
|
|
246
|
+
if (done)
|
|
247
|
+
void sendFormattedReply(bot, chatId, text);
|
|
205
248
|
});
|
|
206
249
|
}
|
|
207
250
|
});
|
|
@@ -698,7 +741,6 @@ export function createBot() {
|
|
|
698
741
|
await ctx.react("👀");
|
|
699
742
|
}
|
|
700
743
|
catch { }
|
|
701
|
-
// Get the largest photo (last in array)
|
|
702
744
|
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
|
703
745
|
try {
|
|
704
746
|
const file = await ctx.api.getFile(photo.file_id);
|
|
@@ -708,7 +750,6 @@ export function createBot() {
|
|
|
708
750
|
return;
|
|
709
751
|
}
|
|
710
752
|
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
711
|
-
// Download to temp file
|
|
712
753
|
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
713
754
|
const { join } = await import("path");
|
|
714
755
|
const { tmpdir } = await import("os");
|
|
@@ -718,39 +759,11 @@ export function createBot() {
|
|
|
718
759
|
const response = await fetch(url);
|
|
719
760
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
720
761
|
writeFileSync(localPath, buffer);
|
|
721
|
-
|
|
762
|
+
scheduleTempCleanup(tmpDir);
|
|
722
763
|
const prompt = `[User sent a photo saved at: ${localPath}]\n\nCaption: ${caption}\n\nPlease analyze this image. The file is at ${localPath} — you can use bash to view it with tools if needed.`;
|
|
723
764
|
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
724
|
-
if (done)
|
|
725
|
-
|
|
726
|
-
const chunks = chunkMessage(formatted);
|
|
727
|
-
const fallbackChunks = chunkMessage(text);
|
|
728
|
-
void (async () => {
|
|
729
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
730
|
-
if (i > 0)
|
|
731
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
732
|
-
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
733
|
-
try {
|
|
734
|
-
await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
735
|
-
parse_mode: "HTML",
|
|
736
|
-
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
try {
|
|
741
|
-
await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
742
|
-
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
743
|
-
});
|
|
744
|
-
}
|
|
745
|
-
catch { }
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
try {
|
|
749
|
-
await ctx.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
750
|
-
}
|
|
751
|
-
catch { }
|
|
752
|
-
})();
|
|
753
|
-
}
|
|
765
|
+
if (done)
|
|
766
|
+
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
|
|
754
767
|
});
|
|
755
768
|
}
|
|
756
769
|
catch (err) {
|
|
@@ -791,38 +804,11 @@ export function createBot() {
|
|
|
791
804
|
const response = await fetch(url);
|
|
792
805
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
793
806
|
writeFileSync(localPath, buffer);
|
|
807
|
+
scheduleTempCleanup(tmpDir);
|
|
794
808
|
const prompt = `[User sent a file: ${doc.file_name || "unknown"} (${doc.file_size || 0} bytes), saved at: ${localPath}]\n\nCaption: ${caption}\n\nPlease analyze this file. You can read it with bash tools.`;
|
|
795
809
|
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
796
|
-
if (done)
|
|
797
|
-
|
|
798
|
-
const chunks = chunkMessage(formatted);
|
|
799
|
-
const fallbackChunks = chunkMessage(text);
|
|
800
|
-
void (async () => {
|
|
801
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
802
|
-
if (i > 0)
|
|
803
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
804
|
-
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
805
|
-
try {
|
|
806
|
-
await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
807
|
-
parse_mode: "HTML",
|
|
808
|
-
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
catch {
|
|
812
|
-
try {
|
|
813
|
-
await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
814
|
-
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
catch { }
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
try {
|
|
821
|
-
await ctx.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
822
|
-
}
|
|
823
|
-
catch { }
|
|
824
|
-
})();
|
|
825
|
-
}
|
|
810
|
+
if (done)
|
|
811
|
+
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
|
|
826
812
|
});
|
|
827
813
|
}
|
|
828
814
|
catch (err) {
|
|
@@ -877,6 +863,7 @@ export function createBot() {
|
|
|
877
863
|
const response = await fetch(url);
|
|
878
864
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
879
865
|
writeFileSync(localPath, buffer);
|
|
866
|
+
scheduleTempCleanup(tmpDir);
|
|
880
867
|
let prompt;
|
|
881
868
|
if (config.openaiApiKey) {
|
|
882
869
|
// Transcribe using OpenAI Whisper API
|
|
@@ -911,49 +898,8 @@ export function createBot() {
|
|
|
911
898
|
prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. No OPENAI_API_KEY configured for transcription. You can tell the user to set it up in ~/.nzb/.env]`;
|
|
912
899
|
}
|
|
913
900
|
sendToOrchestrator(voiceReplyContext + prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
|
|
914
|
-
if (done)
|
|
915
|
-
|
|
916
|
-
const formatted = toTelegramHTML(text);
|
|
917
|
-
const chunks = chunkMessage(formatted);
|
|
918
|
-
const fallbackChunks = chunkMessage(text);
|
|
919
|
-
void (async () => {
|
|
920
|
-
let firstMsgId;
|
|
921
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
922
|
-
if (i > 0)
|
|
923
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
924
|
-
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
925
|
-
try {
|
|
926
|
-
const sent = await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
927
|
-
parse_mode: "HTML",
|
|
928
|
-
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
929
|
-
});
|
|
930
|
-
if (i === 0)
|
|
931
|
-
firstMsgId = sent.message_id;
|
|
932
|
-
}
|
|
933
|
-
catch {
|
|
934
|
-
try {
|
|
935
|
-
const sent = await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
936
|
-
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
937
|
-
});
|
|
938
|
-
if (i === 0)
|
|
939
|
-
firstMsgId = sent.message_id;
|
|
940
|
-
}
|
|
941
|
-
catch { }
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
if (assistantLogId && firstMsgId) {
|
|
945
|
-
try {
|
|
946
|
-
const { setConversationTelegramMsgId } = await import("../store/db.js");
|
|
947
|
-
setConversationTelegramMsgId(assistantLogId, firstMsgId);
|
|
948
|
-
}
|
|
949
|
-
catch { }
|
|
950
|
-
}
|
|
951
|
-
try {
|
|
952
|
-
await ctx.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
953
|
-
}
|
|
954
|
-
catch { }
|
|
955
|
-
})();
|
|
956
|
-
}
|
|
901
|
+
if (done)
|
|
902
|
+
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId, assistantLogId: meta?.assistantLogId });
|
|
957
903
|
});
|
|
958
904
|
}
|
|
959
905
|
catch (err) {
|
|
@@ -962,6 +908,14 @@ export function createBot() {
|
|
|
962
908
|
});
|
|
963
909
|
}
|
|
964
910
|
});
|
|
911
|
+
// Global error handler — prevents unhandled errors from crashing the bot
|
|
912
|
+
bot.catch((err) => {
|
|
913
|
+
const ctx = err.ctx;
|
|
914
|
+
const e = err.error;
|
|
915
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
916
|
+
console.error(`[nzb] Bot error for ${ctx?.update?.update_id}: ${msg}`);
|
|
917
|
+
void logError(`Bot error: ${msg.slice(0, 200)}`);
|
|
918
|
+
});
|
|
965
919
|
return bot;
|
|
966
920
|
}
|
|
967
921
|
export async function startBot() {
|
|
@@ -1015,25 +969,7 @@ export async function stopBot() {
|
|
|
1015
969
|
export async function sendProactiveMessage(text) {
|
|
1016
970
|
if (!bot || config.authorizedUserId === undefined)
|
|
1017
971
|
return;
|
|
1018
|
-
|
|
1019
|
-
const chunks = chunkMessage(formatted);
|
|
1020
|
-
const fallbackChunks = chunkMessage(text);
|
|
1021
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
1022
|
-
if (i > 0)
|
|
1023
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
1024
|
-
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
1025
|
-
try {
|
|
1026
|
-
await bot.api.sendMessage(config.authorizedUserId, pageTag + chunks[i], { parse_mode: "HTML" });
|
|
1027
|
-
}
|
|
1028
|
-
catch {
|
|
1029
|
-
try {
|
|
1030
|
-
await bot.api.sendMessage(config.authorizedUserId, pageTag + (fallbackChunks[i] ?? chunks[i]));
|
|
1031
|
-
}
|
|
1032
|
-
catch {
|
|
1033
|
-
// Bot may not be connected yet
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
972
|
+
await sendFormattedReply(bot, config.authorizedUserId, text);
|
|
1037
973
|
}
|
|
1038
974
|
/** Send a worker lifecycle notification to the authorized user. */
|
|
1039
975
|
export async function sendWorkerNotification(message) {
|
|
@@ -107,19 +107,27 @@ export function toTelegramHTML(text) {
|
|
|
107
107
|
out = out.replace(/^(\s*)[-*]\s+/gm, "$1• ");
|
|
108
108
|
// 9. Ordered lists: keep as-is (1. 2. 3.)
|
|
109
109
|
// 10. Strikethrough ~~text~~ → <s>
|
|
110
|
-
out = out.replace(/~~(.+?)~~/g, (_m, inner) => stashToken(`<s
|
|
111
|
-
// 11. Bold
|
|
112
|
-
out = out.replace(
|
|
113
|
-
// 12.
|
|
114
|
-
out = out.replace(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
out = out.replace(/~~(.+?)~~/g, (_m, inner) => stashToken(`<s>${escapeHtml(inner)}</s>`));
|
|
111
|
+
// 11. Bold+italic ***text*** → <b><i>text</i></b>
|
|
112
|
+
out = out.replace(/\*\*\*(.+?)\*\*\*/g, (_m, inner) => stashToken(`<b><i>${escapeHtml(inner)}</i></b>`));
|
|
113
|
+
// 12. Bold **text** → <b> (inner may contain stash tokens, preserve them)
|
|
114
|
+
out = out.replace(/\*\*(.+?)\*\*/g, (_m, inner) => {
|
|
115
|
+
const escaped = escapeHtml(inner.replace(/\x00S\d+\x00/g, (tok) => `\x00KEEP${tok}\x00KEEP`));
|
|
116
|
+
const restored = escaped.replace(/\x00KEEP\x00S(\d+)\x00\x00KEEP/g, (_m2, i) => stash[+i]);
|
|
117
|
+
return stashToken(`<b>${restored}</b>`);
|
|
118
|
+
});
|
|
119
|
+
// 13. Italic *text* → <i>
|
|
120
|
+
out = out.replace(/\*(.+?)\*/g, (_m, inner) => {
|
|
121
|
+
const escaped = escapeHtml(inner.replace(/\x00S\d+\x00/g, (tok) => `\x00KEEP${tok}\x00KEEP`));
|
|
122
|
+
const restored = escaped.replace(/\x00KEEP\x00S(\d+)\x00\x00KEEP/g, (_m2, i) => stash[+i]);
|
|
123
|
+
return stashToken(`<i>${restored}</i>`);
|
|
124
|
+
});
|
|
125
|
+
// 14. Underline __text__ → <u>
|
|
126
|
+
out = out.replace(/__(.+?)__/g, (_m, inner) => stashToken(`<u>${escapeHtml(inner)}</u>`));
|
|
127
|
+
// 15. Escape remaining plain text
|
|
118
128
|
out = escapeHtml(out);
|
|
119
|
-
//
|
|
129
|
+
// 16. Restore stashed tokens
|
|
120
130
|
out = out.replace(/\x00S(\d+)\x00/g, (_m, i) => stash[+i]);
|
|
121
|
-
// 16. Escape inner text of formatting tags (marked with ESC)
|
|
122
|
-
out = out.replace(/\x00ESC([\s\S]*?)\x00ESC/g, (_m, inner) => escapeHtml(inner));
|
|
123
131
|
// 17. Clean up excessive blank lines
|
|
124
132
|
out = out.replace(/\n{3,}/g, "\n\n");
|
|
125
133
|
return out.trim();
|