@iletai/nzb 1.6.3 → 1.7.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/config.js +3 -0
- package/dist/copilot/client.js +0 -1
- package/dist/copilot/orchestrator.js +12 -5
- package/dist/copilot/tools.js +6 -11
- package/dist/daemon.js +2 -2
- package/dist/telegram/bot.js +2 -1
- package/dist/telegram/formatter.js +6 -0
- package/dist/telegram/handlers/commands.js +22 -5
- package/dist/telegram/handlers/inline.js +1 -1
- package/dist/telegram/handlers/media.js +6 -11
- package/dist/telegram/handlers/streaming.js +165 -155
- package/dist/telegram/log-channel.js +5 -2
- package/dist/telegram/menus.js +20 -4
- package/package.json +3 -3
package/dist/config.js
CHANGED
|
@@ -15,6 +15,7 @@ const configSchema = z.object({
|
|
|
15
15
|
LOG_CHANNEL_ID: z.string().optional(),
|
|
16
16
|
NODE_EXTRA_CA_CERTS: z.string().optional(),
|
|
17
17
|
OPENAI_API_KEY: z.string().optional(),
|
|
18
|
+
REASONING_EFFORT: z.string().optional(),
|
|
18
19
|
});
|
|
19
20
|
const raw = configSchema.parse(process.env);
|
|
20
21
|
// Apply NODE_EXTRA_CA_CERTS from .env if not already set via environment.
|
|
@@ -71,6 +72,8 @@ export const config = {
|
|
|
71
72
|
thinkingLevel: (process.env.THINKING_LEVEL || "off"),
|
|
72
73
|
/** Group chat: when true, bot only responds when mentioned in groups */
|
|
73
74
|
groupMentionOnly: process.env.GROUP_MENTION_ONLY !== "false",
|
|
75
|
+
/** Reasoning effort: low | medium | high */
|
|
76
|
+
reasoningEffort: (process.env.REASONING_EFFORT || "medium"),
|
|
74
77
|
};
|
|
75
78
|
/** Persist an env variable to ~/.nzb/.env */
|
|
76
79
|
export function persistEnvVar(key, value) {
|
package/dist/copilot/client.js
CHANGED
|
@@ -204,6 +204,7 @@ async function createOrResumeSession() {
|
|
|
204
204
|
model: config.copilotModel,
|
|
205
205
|
configDir: SESSIONS_DIR,
|
|
206
206
|
streaming: true,
|
|
207
|
+
reasoningEffort: config.reasoningEffort,
|
|
207
208
|
systemMessage: {
|
|
208
209
|
content: getOrchestratorSystemMessage(memorySummary || undefined, {
|
|
209
210
|
selfEditEnabled: config.selfEditEnabled,
|
|
@@ -230,6 +231,7 @@ async function createOrResumeSession() {
|
|
|
230
231
|
model: config.copilotModel,
|
|
231
232
|
configDir: SESSIONS_DIR,
|
|
232
233
|
streaming: true,
|
|
234
|
+
reasoningEffort: config.reasoningEffort,
|
|
233
235
|
systemMessage: {
|
|
234
236
|
content: getOrchestratorSystemMessage(memorySummary || undefined, {
|
|
235
237
|
selfEditEnabled: config.selfEditEnabled,
|
|
@@ -297,7 +299,7 @@ export async function initOrchestrator(client) {
|
|
|
297
299
|
}
|
|
298
300
|
}
|
|
299
301
|
/** Send a prompt on the persistent session, return the response. */
|
|
300
|
-
async function executeOnSession(prompt, callback, onToolEvent, onUsage) {
|
|
302
|
+
async function executeOnSession(prompt, callback, onToolEvent, onUsage, attachments) {
|
|
301
303
|
const session = await ensureOrchestratorSession();
|
|
302
304
|
// Wait for any in-flight context recovery injection to finish before sending
|
|
303
305
|
if (recoveryInjectionPromise) {
|
|
@@ -350,7 +352,11 @@ async function executeOnSession(prompt, callback, onToolEvent, onUsage) {
|
|
|
350
352
|
onUsage?.({ inputTokens, outputTokens, model, duration });
|
|
351
353
|
});
|
|
352
354
|
try {
|
|
353
|
-
const
|
|
355
|
+
const sendPayload = { prompt };
|
|
356
|
+
if (attachments?.length) {
|
|
357
|
+
sendPayload.attachments = attachments;
|
|
358
|
+
}
|
|
359
|
+
const result = await session.sendAndWait(sendPayload, 60_000);
|
|
354
360
|
// Allow late-arriving events (e.g. assistant.usage) to be processed
|
|
355
361
|
await new Promise((r) => setTimeout(r, 150));
|
|
356
362
|
const finalContent = result?.data?.content || accumulated || "(No response)";
|
|
@@ -394,7 +400,7 @@ async function processQueue() {
|
|
|
394
400
|
const item = messageQueue.shift();
|
|
395
401
|
currentSourceChannel = item.sourceChannel;
|
|
396
402
|
try {
|
|
397
|
-
const result = await executeOnSession(item.prompt, item.callback, item.onToolEvent, item.onUsage);
|
|
403
|
+
const result = await executeOnSession(item.prompt, item.callback, item.onToolEvent, item.onUsage, item.attachments);
|
|
398
404
|
item.resolve(result);
|
|
399
405
|
}
|
|
400
406
|
catch (err) {
|
|
@@ -409,7 +415,7 @@ function isRecoverableError(err) {
|
|
|
409
415
|
return /timeout|disconnect|connection|EPIPE|ECONNRESET|ECONNREFUSED|socket|closed|ENOENT|spawn|not found|expired|stale/i.test(msg);
|
|
410
416
|
}
|
|
411
417
|
const MAX_AUTO_CONTINUE = 3;
|
|
412
|
-
export async function sendToOrchestrator(prompt, source, callback, onToolEvent, onUsage, _autoContinueCount = 0) {
|
|
418
|
+
export async function sendToOrchestrator(prompt, source, callback, onToolEvent, onUsage, _autoContinueCount = 0, attachments) {
|
|
413
419
|
const sourceLabel = source.type === "telegram" ? "telegram" : source.type === "tui" ? "tui" : "background";
|
|
414
420
|
logMessage("in", sourceLabel, prompt);
|
|
415
421
|
// Tag the prompt with its source channel
|
|
@@ -444,6 +450,7 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
|
|
|
444
450
|
const finalContent = await new Promise((resolve, reject) => {
|
|
445
451
|
const item = {
|
|
446
452
|
prompt: taggedPrompt,
|
|
453
|
+
attachments,
|
|
447
454
|
callback,
|
|
448
455
|
onToolEvent,
|
|
449
456
|
onUsage,
|
|
@@ -572,7 +579,7 @@ export async function resetSession() {
|
|
|
572
579
|
// Destroy the existing session
|
|
573
580
|
if (orchestratorSession) {
|
|
574
581
|
try {
|
|
575
|
-
await orchestratorSession.
|
|
582
|
+
await orchestratorSession.disconnect();
|
|
576
583
|
}
|
|
577
584
|
catch { }
|
|
578
585
|
orchestratorSession = undefined;
|
package/dist/copilot/tools.js
CHANGED
|
@@ -111,7 +111,7 @@ export function createTools(deps) {
|
|
|
111
111
|
})
|
|
112
112
|
.finally(() => {
|
|
113
113
|
// Auto-destroy background workers after completion to free memory (~400MB per worker)
|
|
114
|
-
session.
|
|
114
|
+
session.disconnect().catch(() => { });
|
|
115
115
|
deps.workers.delete(args.name);
|
|
116
116
|
try {
|
|
117
117
|
getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(args.name);
|
|
@@ -162,7 +162,7 @@ export function createTools(deps) {
|
|
|
162
162
|
})
|
|
163
163
|
.finally(() => {
|
|
164
164
|
// Auto-destroy after each send_to_worker dispatch to free memory
|
|
165
|
-
worker.session.
|
|
165
|
+
worker.session.disconnect().catch(() => { });
|
|
166
166
|
deps.workers.delete(args.name);
|
|
167
167
|
try {
|
|
168
168
|
getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(args.name);
|
|
@@ -210,7 +210,7 @@ export function createTools(deps) {
|
|
|
210
210
|
return `No worker named '${args.name}'.`;
|
|
211
211
|
}
|
|
212
212
|
try {
|
|
213
|
-
await worker.session.
|
|
213
|
+
await worker.session.disconnect();
|
|
214
214
|
}
|
|
215
215
|
catch {
|
|
216
216
|
// Session may already be gone
|
|
@@ -319,7 +319,7 @@ export function createTools(deps) {
|
|
|
319
319
|
deps.onWorkerComplete(member.name, errMsg);
|
|
320
320
|
})
|
|
321
321
|
.finally(() => {
|
|
322
|
-
session.
|
|
322
|
+
session.disconnect().catch(() => { });
|
|
323
323
|
deps.workers.delete(member.name);
|
|
324
324
|
try {
|
|
325
325
|
getDb().prepare(`DELETE FROM worker_sessions WHERE name = ?`).run(member.name);
|
|
@@ -341,10 +341,7 @@ export function createTools(deps) {
|
|
|
341
341
|
defineTool("get_team_status", {
|
|
342
342
|
description: "Get the status of agent teams — shows active teams, their members, and progress.",
|
|
343
343
|
parameters: z.object({
|
|
344
|
-
team_name: z
|
|
345
|
-
.string()
|
|
346
|
-
.optional()
|
|
347
|
-
.describe("Specific team name to check. Omit to list all active teams."),
|
|
344
|
+
team_name: z.string().optional().describe("Specific team name to check. Omit to list all active teams."),
|
|
348
345
|
}),
|
|
349
346
|
handler: async (args) => {
|
|
350
347
|
if (args.team_name) {
|
|
@@ -368,9 +365,7 @@ export function createTools(deps) {
|
|
|
368
365
|
: worker?.status === "error"
|
|
369
366
|
? "❌ error"
|
|
370
367
|
: "🔄 pending";
|
|
371
|
-
const elapsed = worker?.startedAt
|
|
372
|
-
? `${Math.round((Date.now() - worker.startedAt) / 1000)}s`
|
|
373
|
-
: "";
|
|
368
|
+
const elapsed = worker?.startedAt ? `${Math.round((Date.now() - worker.startedAt) / 1000)}s` : "";
|
|
374
369
|
lines.push(` ${status} ${memberName} ${elapsed}`);
|
|
375
370
|
}
|
|
376
371
|
return lines.join("\n");
|
package/dist/daemon.js
CHANGED
|
@@ -202,7 +202,7 @@ async function shutdown() {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
// Destroy all active worker sessions to free memory
|
|
205
|
-
await Promise.allSettled(Array.from(workers.values()).map((w) => w.session.
|
|
205
|
+
await Promise.allSettled(Array.from(workers.values()).map((w) => w.session.disconnect()));
|
|
206
206
|
workers.clear();
|
|
207
207
|
try {
|
|
208
208
|
await stopClient();
|
|
@@ -234,7 +234,7 @@ export async function restartDaemon() {
|
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
// Destroy all active worker sessions to free memory
|
|
237
|
-
await Promise.allSettled(Array.from(activeWorkers.values()).map((w) => w.session.
|
|
237
|
+
await Promise.allSettled(Array.from(activeWorkers.values()).map((w) => w.session.disconnect()));
|
|
238
238
|
activeWorkers.clear();
|
|
239
239
|
try {
|
|
240
240
|
await stopClient();
|
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
|
|
@@ -85,6 +85,12 @@ function findSafeSplitIndex(text, targetIndex) {
|
|
|
85
85
|
}
|
|
86
86
|
return targetIndex;
|
|
87
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
|
+
}
|
|
88
94
|
/**
|
|
89
95
|
* Split a long message into chunks that fit within Telegram's message limit.
|
|
90
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
|
});
|
|
@@ -23,21 +23,16 @@ export function registerMediaHandlers(bot) {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
26
|
-
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
27
|
-
const { join } = await import("path");
|
|
28
|
-
const { tmpdir } = await import("os");
|
|
29
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-photo-"));
|
|
30
|
-
const ext = filePath.split(".").pop() || "jpg";
|
|
31
|
-
const localPath = join(tmpDir, `photo.${ext}`);
|
|
32
26
|
const response = await fetch(url);
|
|
33
27
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
28
|
+
const base64Data = buffer.toString("base64");
|
|
29
|
+
const ext = filePath.split(".").pop() || "jpg";
|
|
30
|
+
const mimeType = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : "image/jpeg";
|
|
31
|
+
const attachment = { type: "blob", data: base64Data, mimeType };
|
|
32
|
+
sendToOrchestrator(caption, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
38
33
|
if (done)
|
|
39
34
|
void sendFormattedReply(bot, chatId, text, { replyTo: userMessageId });
|
|
40
|
-
});
|
|
35
|
+
}, undefined, undefined, 0, [attachment]);
|
|
41
36
|
}
|
|
42
37
|
catch (err) {
|
|
43
38
|
await ctx.reply(`❌ Error processing photo: ${err instanceof Error ? err.message : String(err)}`, {
|
|
@@ -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";
|
|
@@ -75,6 +76,7 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
75
76
|
// prevents push notification spam for very short initial chunks (inspired by OpenClaw's draft-stream).
|
|
76
77
|
const MIN_INITIAL_CHARS = 80;
|
|
77
78
|
const handlerStartTime = Date.now();
|
|
79
|
+
const requestId = randomUUID();
|
|
78
80
|
const enqueueEdit = (text) => {
|
|
79
81
|
if (finalized || text === lastEditedText)
|
|
80
82
|
return;
|
|
@@ -109,6 +111,8 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
else {
|
|
114
|
+
if (finalized)
|
|
115
|
+
return;
|
|
112
116
|
try {
|
|
113
117
|
await editSafe(getBot().api, chatId, placeholderMsgId, safeText);
|
|
114
118
|
}
|
|
@@ -203,80 +207,65 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
203
207
|
void logInfo(`✅ Response done (${elapsed}s, ${toolHistory.length} tools, ${text.length} chars)`);
|
|
204
208
|
// Return the edit chain so callers can await final delivery
|
|
205
209
|
return editChain.then(async () => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
try {
|
|
211
|
+
// Format error messages with a distinct visual
|
|
212
|
+
const isError = text.startsWith("Error:");
|
|
213
|
+
if (isError) {
|
|
214
|
+
void logError(`Response error: ${text.slice(0, 200)}`);
|
|
215
|
+
const errorText = `⚠️ ${text}`;
|
|
216
|
+
const errorKb = new InlineKeyboard().text("🔄 Retry", "retry").text("📖 Explain", "explain_error");
|
|
217
|
+
if (placeholderMsgId) {
|
|
218
|
+
try {
|
|
219
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, errorText, { reply_markup: errorKb });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
/* fall through */
|
|
224
|
+
}
|
|
225
|
+
}
|
|
213
226
|
try {
|
|
214
|
-
await
|
|
215
|
-
return;
|
|
227
|
+
await ctx.reply(errorText, { reply_parameters: replyParams, reply_markup: errorKb });
|
|
216
228
|
}
|
|
217
229
|
catch {
|
|
218
|
-
/*
|
|
230
|
+
/* nothing more we can do */
|
|
219
231
|
}
|
|
232
|
+
return;
|
|
220
233
|
}
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
let textWithMeta = text;
|
|
235
|
+
if (usageInfo && config.usageMode !== "off") {
|
|
236
|
+
const fmtTokens = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n));
|
|
237
|
+
const parts = [];
|
|
238
|
+
if (config.usageMode === "full" && usageInfo.model)
|
|
239
|
+
parts.push(usageInfo.model);
|
|
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(" · ")}`;
|
|
223
246
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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) {
|
|
259
|
-
try {
|
|
260
|
-
await editSafe(getBot().api, chatId, placeholderMsgId, chunks[0], {
|
|
261
|
-
parse_mode: "HTML",
|
|
262
|
-
reply_markup: smartKb,
|
|
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,
|
|
263
255
|
});
|
|
264
|
-
|
|
265
|
-
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
266
|
-
}
|
|
267
|
-
catch { }
|
|
268
|
-
if (assistantLogId) {
|
|
269
|
-
try {
|
|
270
|
-
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
271
|
-
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
272
|
-
}
|
|
273
|
-
catch { }
|
|
274
|
-
}
|
|
275
|
-
return;
|
|
256
|
+
fullFormatted += expandable;
|
|
276
257
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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,
|
|
268
|
+
});
|
|
280
269
|
try {
|
|
281
270
|
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
282
271
|
}
|
|
@@ -290,8 +279,45 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
290
279
|
}
|
|
291
280
|
return;
|
|
292
281
|
}
|
|
293
|
-
|
|
294
|
-
|
|
282
|
+
catch (err) {
|
|
283
|
+
// "message is not modified" is harmless — placeholder already has this content
|
|
284
|
+
if (isMessageNotModifiedError(err)) {
|
|
285
|
+
try {
|
|
286
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
287
|
+
}
|
|
288
|
+
catch { }
|
|
289
|
+
if (assistantLogId) {
|
|
290
|
+
try {
|
|
291
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
292
|
+
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
293
|
+
}
|
|
294
|
+
catch { }
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// HTML parse error — try plain text fallback
|
|
299
|
+
if (isHtmlParseError(err)) {
|
|
300
|
+
try {
|
|
301
|
+
await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
|
|
302
|
+
reply_markup: smartKb,
|
|
303
|
+
});
|
|
304
|
+
try {
|
|
305
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
306
|
+
}
|
|
307
|
+
catch { }
|
|
308
|
+
if (assistantLogId) {
|
|
309
|
+
try {
|
|
310
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
311
|
+
setConversationTelegramMsgId(assistantLogId, placeholderMsgId);
|
|
312
|
+
}
|
|
313
|
+
catch { }
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
/* fall through to send new messages */
|
|
319
|
+
}
|
|
320
|
+
}
|
|
295
321
|
try {
|
|
296
322
|
await editSafe(getBot().api, chatId, placeholderMsgId, fallbackChunks[0], {
|
|
297
323
|
reply_markup: smartKb,
|
|
@@ -313,109 +339,93 @@ export function registerMessageHandler(bot, getBot) {
|
|
|
313
339
|
/* fall through to send new messages */
|
|
314
340
|
}
|
|
315
341
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
catch { }
|
|
330
|
-
}
|
|
331
|
-
return;
|
|
342
|
+
}
|
|
343
|
+
// Multi-chunk or edit fallthrough: send new chunks FIRST, then delete placeholder
|
|
344
|
+
const totalChunks = chunks.length;
|
|
345
|
+
let firstSentMsgId;
|
|
346
|
+
const sendChunk = async (chunk, fallback, index) => {
|
|
347
|
+
const isFirst = index === 0 && !placeholderMsgId;
|
|
348
|
+
const isLast = index === totalChunks - 1;
|
|
349
|
+
// Pagination header for multi-chunk messages
|
|
350
|
+
const pageTag = totalChunks > 1 ? `📄 ${index + 1}/${totalChunks}\n` : "";
|
|
351
|
+
// Trim chunk if pageTag pushes it over the limit
|
|
352
|
+
let safeChunk = chunk;
|
|
353
|
+
if (pageTag.length + safeChunk.length > TELEGRAM_MAX_LENGTH) {
|
|
354
|
+
safeChunk = safeChunk.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
332
355
|
}
|
|
333
|
-
|
|
334
|
-
|
|
356
|
+
let safeFallback = fallback;
|
|
357
|
+
if (pageTag.length + safeFallback.length > TELEGRAM_MAX_LENGTH) {
|
|
358
|
+
safeFallback = safeFallback.slice(0, TELEGRAM_MAX_LENGTH - pageTag.length - 4) + " ⋯";
|
|
335
359
|
}
|
|
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 } : {}),
|
|
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 } : {}),
|
|
368
|
+
};
|
|
369
|
+
const sent = await ctx
|
|
370
|
+
.reply(pageTag + safeChunk, opts)
|
|
371
|
+
.catch(() => ctx.reply(pageTag + safeFallback, fallbackOpts));
|
|
372
|
+
if (index === 0 && sent)
|
|
373
|
+
firstSentMsgId = sent.message_id;
|
|
363
374
|
};
|
|
364
|
-
|
|
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 {
|
|
375
|
+
let sendSucceeded = false;
|
|
380
376
|
try {
|
|
381
|
-
for (let i = 0; i <
|
|
377
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
382
378
|
if (i > 0)
|
|
383
379
|
await new Promise((r) => setTimeout(r, 300));
|
|
384
|
-
|
|
385
|
-
const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
|
|
386
|
-
if (i === 0 && sent)
|
|
387
|
-
firstSentMsgId = sent.message_id;
|
|
380
|
+
await sendChunk(chunks[i], fallbackChunks[i] ?? chunks[i], i);
|
|
388
381
|
}
|
|
389
382
|
sendSucceeded = true;
|
|
390
383
|
}
|
|
391
384
|
catch {
|
|
392
|
-
|
|
385
|
+
try {
|
|
386
|
+
for (let i = 0; i < fallbackChunks.length; i++) {
|
|
387
|
+
if (i > 0)
|
|
388
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
389
|
+
const pageTag = fallbackChunks.length > 1 ? `📄 ${i + 1}/${fallbackChunks.length}\n` : "";
|
|
390
|
+
const sent = await ctx.reply(pageTag + fallbackChunks[i], i === 0 ? { reply_parameters: replyParams } : {});
|
|
391
|
+
if (i === 0 && sent)
|
|
392
|
+
firstSentMsgId = sent.message_id;
|
|
393
|
+
}
|
|
394
|
+
sendSucceeded = true;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
/* nothing more we can do */
|
|
398
|
+
}
|
|
393
399
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
400
|
+
// Only delete placeholder AFTER new messages sent successfully
|
|
401
|
+
if (placeholderMsgId && sendSucceeded) {
|
|
402
|
+
try {
|
|
403
|
+
await getBot().api.deleteMessage(chatId, placeholderMsgId);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
/* ignore — placeholder stays but user has the real message */
|
|
407
|
+
}
|
|
399
408
|
}
|
|
400
|
-
|
|
401
|
-
|
|
409
|
+
// Track bot message ID for reply-to context lookups
|
|
410
|
+
const botMsgId = firstSentMsgId ?? placeholderMsgId;
|
|
411
|
+
if (assistantLogId && botMsgId) {
|
|
412
|
+
try {
|
|
413
|
+
const { setConversationTelegramMsgId } = await import("../../store/db.js");
|
|
414
|
+
setConversationTelegramMsgId(assistantLogId, botMsgId);
|
|
415
|
+
}
|
|
416
|
+
catch { }
|
|
402
417
|
}
|
|
403
|
-
|
|
404
|
-
// Track bot message ID for reply-to context lookups
|
|
405
|
-
const botMsgId = firstSentMsgId ?? placeholderMsgId;
|
|
406
|
-
if (assistantLogId && botMsgId) {
|
|
418
|
+
// React ✅ on the user's original message to signal completion
|
|
407
419
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
420
|
+
await getBot().api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
/* reactions may not be available */
|
|
410
424
|
}
|
|
411
|
-
catch { }
|
|
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
425
|
}
|
|
417
|
-
|
|
418
|
-
|
|
426
|
+
finally {
|
|
427
|
+
placeholderMsgId = undefined;
|
|
428
|
+
lastEditedText = "";
|
|
419
429
|
}
|
|
420
430
|
});
|
|
421
431
|
}
|
|
@@ -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" },
|
|
@@ -44,6 +44,7 @@ export function buildSettingsText(getUptimeStr) {
|
|
|
44
44
|
`⏱ Worker Timeout: ${getTimeoutLabel()}\n` +
|
|
45
45
|
`🤖 Model: ${config.copilotModel}\n` +
|
|
46
46
|
`🧠 Thinking: ${config.thinkingLevel}\n` +
|
|
47
|
+
`💡 Reasoning: ${config.reasoningEffort}\n` +
|
|
47
48
|
`📝 Verbose: ${config.verboseMode ? "✅ ON" : "❌ OFF"}\n` +
|
|
48
49
|
`📊 Usage: ${config.usageMode}\n` +
|
|
49
50
|
`🔧 Show Reasoning: ${config.showReasoning ? "✅ ON" : "❌ OFF"}\n\n` +
|
|
@@ -124,6 +125,17 @@ export function createMenus(getUptimeStr) {
|
|
|
124
125
|
ctx.menu.update();
|
|
125
126
|
await ctx.editMessageText(buildSettingsText(getUptimeStr));
|
|
126
127
|
await ctx.answerCallbackQuery(`Verbose ${config.verboseMode ? "ON" : "OFF"}`);
|
|
128
|
+
})
|
|
129
|
+
.row()
|
|
130
|
+
.text(() => `💡 Reasoning: ${config.reasoningEffort}`, async (ctx) => {
|
|
131
|
+
const efforts = ["low", "medium", "high"];
|
|
132
|
+
const idx = efforts.indexOf(config.reasoningEffort);
|
|
133
|
+
const next = efforts[(idx + 1) % efforts.length];
|
|
134
|
+
config.reasoningEffort = next;
|
|
135
|
+
persistEnvVar("REASONING_EFFORT", next);
|
|
136
|
+
ctx.menu.update();
|
|
137
|
+
await ctx.editMessageText(buildSettingsText(getUptimeStr));
|
|
138
|
+
await ctx.answerCallbackQuery(`Reasoning → ${next}`);
|
|
127
139
|
})
|
|
128
140
|
.row()
|
|
129
141
|
.text(() => `📊 Usage: ${config.usageMode}`, async (ctx) => {
|
|
@@ -174,7 +186,7 @@ export function createMenus(getUptimeStr) {
|
|
|
174
186
|
}
|
|
175
187
|
else {
|
|
176
188
|
const lines = workers.map((w) => `• ${w.name} (${w.workingDir}) — ${w.status}`);
|
|
177
|
-
await ctx.reply(lines.join("\n"));
|
|
189
|
+
await ctx.reply(truncateForTelegram(lines.join("\n")));
|
|
178
190
|
}
|
|
179
191
|
})
|
|
180
192
|
.text("🧠 Skills", async (ctx) => {
|
|
@@ -185,7 +197,7 @@ export function createMenus(getUptimeStr) {
|
|
|
185
197
|
}
|
|
186
198
|
else {
|
|
187
199
|
const lines = skills.map((s) => `• ${s.name} (${s.source}) — ${s.description}`);
|
|
188
|
-
await ctx.reply(lines.join("\n"));
|
|
200
|
+
await ctx.reply(truncateForTelegram(lines.join("\n")));
|
|
189
201
|
}
|
|
190
202
|
})
|
|
191
203
|
.row()
|
|
@@ -196,7 +208,11 @@ export function createMenus(getUptimeStr) {
|
|
|
196
208
|
await ctx.reply("No memories stored.");
|
|
197
209
|
}
|
|
198
210
|
else {
|
|
199
|
-
|
|
211
|
+
const formatted = formatMemoryList(memories);
|
|
212
|
+
const chunks = chunkMessage(formatted);
|
|
213
|
+
for (const chunk of chunks) {
|
|
214
|
+
await ctx.reply(chunk, { parse_mode: "HTML" });
|
|
215
|
+
}
|
|
200
216
|
}
|
|
201
217
|
})
|
|
202
218
|
.submenu("⚙️ Settings", "settings-menu", async (ctx) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iletai/nzb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "NZB — a personal AI assistant for developers, built on the GitHub Copilot SDK",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nzb": "dist/cli.js"
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"type": "module",
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@github/copilot-sdk": "^0.
|
|
51
|
+
"@github/copilot-sdk": "^0.2.0",
|
|
52
52
|
"@grammyjs/auto-retry": "^2.0.2",
|
|
53
53
|
"@grammyjs/menu": "^1.3.1",
|
|
54
54
|
"@grammyjs/runner": "^2.0.3",
|
|
@@ -69,4 +69,4 @@
|
|
|
69
69
|
"typescript": "^5.9.3",
|
|
70
70
|
"vitest": "^4.1.0"
|
|
71
71
|
}
|
|
72
|
-
}
|
|
72
|
+
}
|