@iletai/nzb 1.3.6 → 1.4.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/dist/telegram/bot.js +79 -21
- package/dist/telegram/formatter.js +60 -85
- package/package.json +1 -1
package/dist/telegram/bot.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { autoRetry } from "@grammyjs/auto-retry";
|
|
2
2
|
import { Menu } from "@grammyjs/menu";
|
|
3
|
-
import { Bot, Keyboard } from "grammy";
|
|
3
|
+
import { Bot, InlineKeyboard, Keyboard } from "grammy";
|
|
4
4
|
import { Agent as HttpsAgent } from "https";
|
|
5
5
|
import { config, persistEnvVar, persistModel } from "../config.js";
|
|
6
6
|
import { cancelCurrentMessage, getQueueSize, getWorkers, sendToOrchestrator } from "../copilot/orchestrator.js";
|
|
7
7
|
import { listSkills } from "../copilot/skills.js";
|
|
8
8
|
import { restartDaemon } from "../daemon.js";
|
|
9
9
|
import { searchMemories } from "../store/db.js";
|
|
10
|
-
import { chunkMessage, formatToolSummaryExpandable,
|
|
10
|
+
import { chunkMessage, escapeHtml, formatToolSummaryExpandable, toTelegramHTML } from "./formatter.js";
|
|
11
11
|
import { initLogChannel, logDebug, logError, logInfo } from "./log-channel.js";
|
|
12
12
|
let bot;
|
|
13
13
|
const startedAt = Date.now();
|
|
@@ -125,9 +125,7 @@ function formatMemoryList(memories) {
|
|
|
125
125
|
});
|
|
126
126
|
return `🧠 <b>${memories.length} memories</b>\n\n${sections.join("\n\n")}`;
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
130
|
-
}
|
|
128
|
+
// escapeHtml is imported from formatter.ts
|
|
131
129
|
export function createBot() {
|
|
132
130
|
if (!config.telegramBotToken) {
|
|
133
131
|
throw new Error("Telegram bot token is missing. Run 'nzb setup' and enter the bot token from @BotFather.");
|
|
@@ -157,6 +155,56 @@ export function createBot() {
|
|
|
157
155
|
});
|
|
158
156
|
// Register interactive menu plugin
|
|
159
157
|
bot.use(mainMenu);
|
|
158
|
+
// Callback handlers for contextual inline buttons
|
|
159
|
+
bot.callbackQuery("retry", async (ctx) => {
|
|
160
|
+
await ctx.answerCallbackQuery({ text: "Retrying..." });
|
|
161
|
+
const originalMsg = ctx.callbackQuery.message;
|
|
162
|
+
if (originalMsg?.reply_to_message && "text" in originalMsg.reply_to_message && originalMsg.reply_to_message.text) {
|
|
163
|
+
const retryPrompt = originalMsg.reply_to_message.text;
|
|
164
|
+
const chatId = ctx.chat.id;
|
|
165
|
+
sendToOrchestrator(retryPrompt, { type: "telegram", chatId, messageId: originalMsg.message_id }, (text, done) => {
|
|
166
|
+
if (done) {
|
|
167
|
+
const formatted = toTelegramHTML(text);
|
|
168
|
+
const chunks = chunkMessage(formatted);
|
|
169
|
+
void (async () => {
|
|
170
|
+
try {
|
|
171
|
+
await bot.api.editMessageText(chatId, originalMsg.message_id, chunks[0], { parse_mode: "HTML" });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
try {
|
|
175
|
+
await ctx.reply(text);
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
bot.callbackQuery("explain_error", async (ctx) => {
|
|
185
|
+
await ctx.answerCallbackQuery({ text: "Explaining..." });
|
|
186
|
+
const originalMsg = ctx.callbackQuery.message;
|
|
187
|
+
if (originalMsg && "text" in originalMsg && originalMsg.text) {
|
|
188
|
+
const chatId = ctx.chat.id;
|
|
189
|
+
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
|
+
const formatted = toTelegramHTML(text);
|
|
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
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
160
208
|
// Persistent reply keyboard — quick actions always visible below chat input
|
|
161
209
|
const replyKeyboard = new Keyboard()
|
|
162
210
|
.text("📊 Status").text("❌ Cancel").row()
|
|
@@ -402,8 +450,9 @@ export function createBot() {
|
|
|
402
450
|
void logDebug(`🔧 Tool start: ${event.toolName}${event.detail ? ` — ${event.detail}` : ""}`);
|
|
403
451
|
currentToolName = event.toolName;
|
|
404
452
|
toolHistory.push({ name: event.toolName, startTime: Date.now(), detail: event.detail });
|
|
453
|
+
const elapsed = ((Date.now() - handlerStartTime) / 1000).toFixed(1);
|
|
405
454
|
const existingText = lastEditedText.replace(/^🔧 .*\n\n/, "");
|
|
406
|
-
enqueueEdit(`🔧 ${event.toolName}\n\n${existingText}`.trim() || `🔧 ${event.toolName}`);
|
|
455
|
+
enqueueEdit(`🔧 ${event.toolName} (${elapsed}s...)\n\n${existingText}`.trim() || `🔧 ${event.toolName}`);
|
|
407
456
|
}
|
|
408
457
|
else if (event.type === "tool_complete") {
|
|
409
458
|
for (let i = toolHistory.length - 1; i >= 0; i--) {
|
|
@@ -412,14 +461,22 @@ export function createBot() {
|
|
|
412
461
|
break;
|
|
413
462
|
}
|
|
414
463
|
}
|
|
464
|
+
// Show completion with checkmark
|
|
465
|
+
const completedTool = toolHistory.find((t) => t.name === event.toolName && t.durationMs !== undefined);
|
|
466
|
+
if (completedTool) {
|
|
467
|
+
const dur = (completedTool.durationMs / 1000).toFixed(1);
|
|
468
|
+
const existingText = lastEditedText.replace(/^🔧 .*\n\n/, "").replace(/^✅ .*\n\n/, "");
|
|
469
|
+
enqueueEdit(`✅ ${event.toolName} (${dur}s)\n\n${existingText}`.trim());
|
|
470
|
+
}
|
|
415
471
|
currentToolName = undefined;
|
|
416
472
|
}
|
|
417
473
|
else if (event.type === "tool_partial_result" && event.detail) {
|
|
418
474
|
const now = Date.now();
|
|
419
475
|
if (now - lastEditTime >= EDIT_INTERVAL_MS) {
|
|
420
476
|
lastEditTime = now;
|
|
477
|
+
const elapsed = ((now - handlerStartTime) / 1000).toFixed(1);
|
|
421
478
|
const truncated = event.detail.length > 500 ? "⋯\n" + event.detail.slice(-500) : event.detail;
|
|
422
|
-
const toolLine = `🔧 ${currentToolName || event.toolName}\n
|
|
479
|
+
const toolLine = `🔧 ${currentToolName || event.toolName} (${elapsed}s...)\n<pre>${escapeHtml(truncated)}</pre>`;
|
|
423
480
|
enqueueEdit(toolLine);
|
|
424
481
|
}
|
|
425
482
|
}
|
|
@@ -468,9 +525,10 @@ export function createBot() {
|
|
|
468
525
|
if (isError) {
|
|
469
526
|
void logError(`Response error: ${text.slice(0, 200)}`);
|
|
470
527
|
const errorText = `⚠️ ${text}`;
|
|
528
|
+
const errorKb = new InlineKeyboard().text("🔄 Retry", "retry").text("📖 Explain", "explain_error");
|
|
471
529
|
if (placeholderMsgId) {
|
|
472
530
|
try {
|
|
473
|
-
await bot.api.editMessageText(chatId, placeholderMsgId, errorText);
|
|
531
|
+
await bot.api.editMessageText(chatId, placeholderMsgId, errorText, { reply_markup: errorKb });
|
|
474
532
|
return;
|
|
475
533
|
}
|
|
476
534
|
catch {
|
|
@@ -478,7 +536,7 @@ export function createBot() {
|
|
|
478
536
|
}
|
|
479
537
|
}
|
|
480
538
|
try {
|
|
481
|
-
await ctx.reply(errorText, { reply_parameters: replyParams });
|
|
539
|
+
await ctx.reply(errorText, { reply_parameters: replyParams, reply_markup: errorKb });
|
|
482
540
|
}
|
|
483
541
|
catch {
|
|
484
542
|
/* nothing more we can do */
|
|
@@ -498,7 +556,7 @@ export function createBot() {
|
|
|
498
556
|
parts.push(`${(usageInfo.duration / 1000).toFixed(1)}s`);
|
|
499
557
|
textWithMeta += `\n\n📊 ${parts.join(" · ")}`;
|
|
500
558
|
}
|
|
501
|
-
const formatted =
|
|
559
|
+
const formatted = toTelegramHTML(textWithMeta);
|
|
502
560
|
let fullFormatted = formatted;
|
|
503
561
|
if (config.showReasoning && toolHistory.length > 0) {
|
|
504
562
|
const expandable = formatToolSummaryExpandable(toolHistory.map((t) => ({ name: t.name, durationMs: t.durationMs, detail: t.detail })));
|
|
@@ -509,7 +567,7 @@ export function createBot() {
|
|
|
509
567
|
// Single chunk: edit placeholder in place
|
|
510
568
|
if (placeholderMsgId && chunks.length === 1) {
|
|
511
569
|
try {
|
|
512
|
-
await bot.api.editMessageText(chatId, placeholderMsgId, chunks[0], { parse_mode: "
|
|
570
|
+
await bot.api.editMessageText(chatId, placeholderMsgId, chunks[0], { parse_mode: "HTML" });
|
|
513
571
|
try {
|
|
514
572
|
await bot.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
515
573
|
}
|
|
@@ -552,8 +610,8 @@ export function createBot() {
|
|
|
552
610
|
// Pagination header for multi-chunk messages
|
|
553
611
|
const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
|
|
554
612
|
const opts = isFirst
|
|
555
|
-
? { parse_mode: "
|
|
556
|
-
: { parse_mode: "
|
|
613
|
+
? { parse_mode: "HTML", reply_parameters: replyParams }
|
|
614
|
+
: { parse_mode: "HTML" };
|
|
557
615
|
const sent = await ctx
|
|
558
616
|
.reply(pageTag + chunk, opts)
|
|
559
617
|
.catch(() => ctx.reply(pageTag + fallback, isFirst ? { reply_parameters: replyParams } : {}));
|
|
@@ -664,7 +722,7 @@ export function createBot() {
|
|
|
664
722
|
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.`;
|
|
665
723
|
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
666
724
|
if (done) {
|
|
667
|
-
const formatted =
|
|
725
|
+
const formatted = toTelegramHTML(text);
|
|
668
726
|
const chunks = chunkMessage(formatted);
|
|
669
727
|
const fallbackChunks = chunkMessage(text);
|
|
670
728
|
void (async () => {
|
|
@@ -674,7 +732,7 @@ export function createBot() {
|
|
|
674
732
|
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
675
733
|
try {
|
|
676
734
|
await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
677
|
-
parse_mode: "
|
|
735
|
+
parse_mode: "HTML",
|
|
678
736
|
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
679
737
|
});
|
|
680
738
|
}
|
|
@@ -736,7 +794,7 @@ export function createBot() {
|
|
|
736
794
|
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.`;
|
|
737
795
|
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
738
796
|
if (done) {
|
|
739
|
-
const formatted =
|
|
797
|
+
const formatted = toTelegramHTML(text);
|
|
740
798
|
const chunks = chunkMessage(formatted);
|
|
741
799
|
const fallbackChunks = chunkMessage(text);
|
|
742
800
|
void (async () => {
|
|
@@ -746,7 +804,7 @@ export function createBot() {
|
|
|
746
804
|
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
747
805
|
try {
|
|
748
806
|
await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
749
|
-
parse_mode: "
|
|
807
|
+
parse_mode: "HTML",
|
|
750
808
|
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
751
809
|
});
|
|
752
810
|
}
|
|
@@ -855,7 +913,7 @@ export function createBot() {
|
|
|
855
913
|
sendToOrchestrator(voiceReplyContext + prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done, meta) => {
|
|
856
914
|
if (done) {
|
|
857
915
|
const assistantLogId = meta?.assistantLogId;
|
|
858
|
-
const formatted =
|
|
916
|
+
const formatted = toTelegramHTML(text);
|
|
859
917
|
const chunks = chunkMessage(formatted);
|
|
860
918
|
const fallbackChunks = chunkMessage(text);
|
|
861
919
|
void (async () => {
|
|
@@ -866,7 +924,7 @@ export function createBot() {
|
|
|
866
924
|
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
867
925
|
try {
|
|
868
926
|
const sent = await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
869
|
-
parse_mode: "
|
|
927
|
+
parse_mode: "HTML",
|
|
870
928
|
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
871
929
|
});
|
|
872
930
|
if (i === 0)
|
|
@@ -957,7 +1015,7 @@ export async function stopBot() {
|
|
|
957
1015
|
export async function sendProactiveMessage(text) {
|
|
958
1016
|
if (!bot || config.authorizedUserId === undefined)
|
|
959
1017
|
return;
|
|
960
|
-
const formatted =
|
|
1018
|
+
const formatted = toTelegramHTML(text);
|
|
961
1019
|
const chunks = chunkMessage(formatted);
|
|
962
1020
|
const fallbackChunks = chunkMessage(text);
|
|
963
1021
|
for (let i = 0; i < chunks.length; i++) {
|
|
@@ -965,7 +1023,7 @@ export async function sendProactiveMessage(text) {
|
|
|
965
1023
|
await new Promise((r) => setTimeout(r, 300));
|
|
966
1024
|
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
967
1025
|
try {
|
|
968
|
-
await bot.api.sendMessage(config.authorizedUserId, pageTag + chunks[i], { parse_mode: "
|
|
1026
|
+
await bot.api.sendMessage(config.authorizedUserId, pageTag + chunks[i], { parse_mode: "HTML" });
|
|
969
1027
|
}
|
|
970
1028
|
catch {
|
|
971
1029
|
try {
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
const TELEGRAM_MAX_LENGTH = 4096;
|
|
2
|
-
// Reserve space for
|
|
3
|
-
const CHUNK_TARGET = TELEGRAM_MAX_LENGTH -
|
|
2
|
+
// Reserve space for tag closure and pagination prefix
|
|
3
|
+
const CHUNK_TARGET = TELEGRAM_MAX_LENGTH - 40;
|
|
4
|
+
/** Escape HTML special characters. */
|
|
5
|
+
export function escapeHtml(text) {
|
|
6
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
7
|
+
}
|
|
4
8
|
/**
|
|
5
9
|
* Split a long message into chunks that fit within Telegram's message limit.
|
|
6
|
-
*
|
|
7
|
-
* closed at the split and reopened in the next chunk so MarkdownV2 stays valid.
|
|
10
|
+
* HTML-aware: if a split falls inside a <pre> block, close and reopen it.
|
|
8
11
|
*/
|
|
9
12
|
export function chunkMessage(text) {
|
|
10
13
|
if (text.length <= TELEGRAM_MAX_LENGTH) {
|
|
@@ -25,12 +28,13 @@ export function chunkMessage(text) {
|
|
|
25
28
|
splitAt = CHUNK_TARGET;
|
|
26
29
|
}
|
|
27
30
|
const segment = remaining.slice(0, splitAt);
|
|
28
|
-
// Count
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
+
// Count <pre> vs </pre> — mismatch means we're inside a code block
|
|
32
|
+
const opens = (segment.match(/<pre/g) || []).length;
|
|
33
|
+
const closes = (segment.match(/<\/pre>/g) || []).length;
|
|
34
|
+
const insideCodeBlock = opens > closes;
|
|
31
35
|
if (insideCodeBlock) {
|
|
32
|
-
chunks.push(segment + "\n
|
|
33
|
-
remaining = "
|
|
36
|
+
chunks.push(segment + "\n</pre>");
|
|
37
|
+
remaining = "<pre>" + remaining.slice(splitAt).trimStart();
|
|
34
38
|
}
|
|
35
39
|
else {
|
|
36
40
|
chunks.push(segment);
|
|
@@ -40,20 +44,7 @@ export function chunkMessage(text) {
|
|
|
40
44
|
return chunks;
|
|
41
45
|
}
|
|
42
46
|
/**
|
|
43
|
-
*
|
|
44
|
-
*/
|
|
45
|
-
export function escapeSegment(text) {
|
|
46
|
-
return text.replace(/([_*\[\]()~`>#+\-=|{}.!\\])/g, "\\$1");
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Escape only characters needed inside a MarkdownV2 link URL.
|
|
50
|
-
*/
|
|
51
|
-
function escapeLinkUrl(url) {
|
|
52
|
-
return url.replace(/([)\\])/g, "\\$1");
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Convert a markdown table into a readable mobile-friendly list.
|
|
56
|
-
* Returns already-escaped MarkdownV2 text ready to be stashed.
|
|
47
|
+
* Convert a markdown table into a readable mobile-friendly HTML list.
|
|
57
48
|
*/
|
|
58
49
|
function convertTable(table) {
|
|
59
50
|
const rows = table
|
|
@@ -71,100 +62,84 @@ function convertTable(table) {
|
|
|
71
62
|
.map((cols) => {
|
|
72
63
|
if (cols.length === 0)
|
|
73
64
|
return "";
|
|
74
|
-
const first =
|
|
65
|
+
const first = `<b>${escapeHtml(cols[0])}</b>`;
|
|
75
66
|
const rest = cols
|
|
76
67
|
.slice(1)
|
|
77
|
-
.map((c) =>
|
|
68
|
+
.map((c) => escapeHtml(c))
|
|
78
69
|
.join(" · ");
|
|
79
70
|
return rest ? `${first} — ${rest}` : first;
|
|
80
71
|
})
|
|
81
72
|
.join("\n");
|
|
82
73
|
}
|
|
83
74
|
/**
|
|
84
|
-
* Convert standard markdown from the AI into Telegram
|
|
75
|
+
* Convert standard markdown from the AI into Telegram HTML.
|
|
85
76
|
* Handles bold, italic, strikethrough, links, lists, blockquotes,
|
|
86
77
|
* code blocks, headers, tables, and horizontal rules.
|
|
87
78
|
*/
|
|
88
|
-
export function
|
|
79
|
+
export function toTelegramHTML(text) {
|
|
89
80
|
const stash = [];
|
|
90
81
|
const stashToken = (s) => {
|
|
91
82
|
stash.push(s);
|
|
92
|
-
return `\
|
|
83
|
+
return `\x00S${stash.length - 1}\x00`;
|
|
93
84
|
};
|
|
94
85
|
let out = text;
|
|
95
|
-
// 1. Stash fenced code blocks
|
|
96
|
-
out = out.replace(/```([a-z]*)\n?([\s\S]*?)```/g, (_m, lang, code) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
86
|
+
// 1. Stash fenced code blocks → <pre><code>
|
|
87
|
+
out = out.replace(/```([a-z]*)\n?([\s\S]*?)```/g, (_m, lang, code) => {
|
|
88
|
+
const cls = lang ? ` class="language-${escapeHtml(lang)}"` : "";
|
|
89
|
+
return stashToken(`<pre><code${cls}>${escapeHtml(code.trim())}</code></pre>`);
|
|
90
|
+
});
|
|
91
|
+
// 2. Stash inline code → <code>
|
|
92
|
+
out = out.replace(/`([^`\n]+)`/g, (_m, code) => stashToken(`<code>${escapeHtml(code)}</code>`));
|
|
93
|
+
// 3. Stash markdown links → <a href>
|
|
94
|
+
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, linkText, url) => stashToken(`<a href="${escapeHtml(url)}">${escapeHtml(linkText)}</a>`));
|
|
95
|
+
// 4. Convert tables
|
|
102
96
|
out = out.replace(/(?:^\|.+\|[ \t]*$\n?)+/gm, (table) => stashToken(convertTable(table) + "\n"));
|
|
103
97
|
// 5. Convert headers → bold
|
|
104
98
|
out = out.replace(/^#{1,6}\s+(.+)$/gm, (_m, title) => `**${title.trim()}**`);
|
|
105
99
|
// 6. Remove horizontal rules
|
|
106
100
|
out = out.replace(/^[-*_]{3,}\s*$/gm, "");
|
|
107
|
-
// 7.
|
|
108
|
-
out = out.replace(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// 9. Convert ordered lists: 1. item → 1\) item (stash \) to avoid double-escaping)
|
|
112
|
-
out = out.replace(/^(\s*)(\d+)\.\s+/gm, (_m, spaces, num) => spaces + num + stashToken("\\) "));
|
|
113
|
-
// 10. Extract strikethrough before escaping
|
|
114
|
-
const strikeParts = [];
|
|
115
|
-
out = out.replace(/~~(.+?)~~/g, (_m, inner) => {
|
|
116
|
-
strikeParts.push(inner);
|
|
117
|
-
return `\x00STRIKE${strikeParts.length - 1}\x00`;
|
|
118
|
-
});
|
|
119
|
-
// 11. Extract bold markers before escaping
|
|
120
|
-
const boldParts = [];
|
|
121
|
-
out = out.replace(/\*\*(.+?)\*\*/g, (_m, inner) => {
|
|
122
|
-
boldParts.push(inner);
|
|
123
|
-
return `\x00BOLD${boldParts.length - 1}\x00`;
|
|
101
|
+
// 7. Blockquotes → <blockquote>
|
|
102
|
+
out = out.replace(/(?:^>\s?(.*)$\n?)+/gm, (block) => {
|
|
103
|
+
const content = block.replace(/^>\s?/gm, "").trim();
|
|
104
|
+
return stashToken(`<blockquote>${escapeHtml(content)}</blockquote>`);
|
|
124
105
|
});
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
out = out.replace(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
out = out.replace(/\
|
|
142
|
-
out = out.replace(/\x00ITALIC(\d+)\x00/g, (_m, i) => `_${escapeSegment(italicParts[+i])}_`);
|
|
143
|
-
out = out.replace(/\x00UNDERLINE(\d+)\x00/g, (_m, i) => `__${escapeSegment(underlineParts[+i])}__`);
|
|
144
|
-
// 16. Restore stashed code blocks, inline code, links, tables
|
|
145
|
-
out = out.replace(/\x00STASH(\d+)\x00/g, (_m, i) => stash[+i]);
|
|
106
|
+
// 8. Unordered lists: - item or * item → • item
|
|
107
|
+
out = out.replace(/^(\s*)[-*]\s+/gm, "$1• ");
|
|
108
|
+
// 9. Ordered lists: keep as-is (1. 2. 3.)
|
|
109
|
+
// 10. Strikethrough ~~text~~ → <s>
|
|
110
|
+
out = out.replace(/~~(.+?)~~/g, (_m, inner) => stashToken(`<s>\x00ESC${inner}\x00ESC</s>`));
|
|
111
|
+
// 11. Bold **text** → <b>
|
|
112
|
+
out = out.replace(/\*\*(.+?)\*\*/g, (_m, inner) => stashToken(`<b>\x00ESC${inner}\x00ESC</b>`));
|
|
113
|
+
// 12. Italic *text* → <i>
|
|
114
|
+
out = out.replace(/\*(.+?)\*/g, (_m, inner) => stashToken(`<i>\x00ESC${inner}\x00ESC</i>`));
|
|
115
|
+
// 13. Underline __text__ → <u>
|
|
116
|
+
out = out.replace(/__(.+?)__/g, (_m, inner) => stashToken(`<u>\x00ESC${inner}\x00ESC</u>`));
|
|
117
|
+
// 14. Escape remaining plain text
|
|
118
|
+
out = escapeHtml(out);
|
|
119
|
+
// 15. Restore stashed tokens
|
|
120
|
+
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));
|
|
146
123
|
// 17. Clean up excessive blank lines
|
|
147
124
|
out = out.replace(/\n{3,}/g, "\n\n");
|
|
148
125
|
return out.trim();
|
|
149
126
|
}
|
|
127
|
+
/** @deprecated Use toTelegramHTML instead. Kept for backward compatibility. */
|
|
128
|
+
export const toTelegramMarkdown = toTelegramHTML;
|
|
129
|
+
export const escapeSegment = escapeHtml;
|
|
150
130
|
/**
|
|
151
|
-
* Format tool call info as
|
|
152
|
-
* First line
|
|
131
|
+
* Format tool call info as Telegram HTML expandable blockquote.
|
|
132
|
+
* First line visible, tool list expands on tap.
|
|
153
133
|
*/
|
|
154
134
|
export function formatToolSummaryExpandable(toolCalls) {
|
|
155
135
|
if (toolCalls.length === 0)
|
|
156
136
|
return "";
|
|
157
137
|
const lines = toolCalls.map((t) => {
|
|
158
|
-
const name =
|
|
159
|
-
const dur = t.durationMs !== undefined
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const detail = t.detail ? `\n> _${escapeSegment(t.detail.slice(0, 60))}_` : "";
|
|
163
|
-
return `${escapeSegment("• ")}${name}${dur}${detail}`;
|
|
138
|
+
const name = escapeHtml(t.name);
|
|
139
|
+
const dur = t.durationMs !== undefined ? ` (${(t.durationMs / 1000).toFixed(1)}s)` : "";
|
|
140
|
+
const detail = t.detail ? `\n <i>${escapeHtml(t.detail.slice(0, 60))}</i>` : "";
|
|
141
|
+
return `• ${name}${dur}${detail}`;
|
|
164
142
|
});
|
|
165
|
-
|
|
166
|
-
const toolList = lines.join(`\n>`);
|
|
167
|
-
// Expandable: header visible, tool list hidden until tapped
|
|
168
|
-
return `\n\n**>${header}\n>${toolList}||`;
|
|
143
|
+
return `\n\n<blockquote expandable>🔧 Tools used:\n${lines.join("\n")}</blockquote>`;
|
|
169
144
|
}
|
|
170
145
|
//# sourceMappingURL=formatter.js.map
|