@iletai/nzb 1.3.2 → 1.3.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/config.js +2 -0
- package/dist/copilot/orchestrator.js +8 -0
- package/dist/setup.js +16 -0
- package/dist/telegram/bot.js +156 -9
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -14,6 +14,7 @@ const configSchema = z.object({
|
|
|
14
14
|
SHOW_REASONING: z.string().optional(),
|
|
15
15
|
LOG_CHANNEL_ID: z.string().optional(),
|
|
16
16
|
NODE_EXTRA_CA_CERTS: z.string().optional(),
|
|
17
|
+
OPENAI_API_KEY: z.string().optional(),
|
|
17
18
|
});
|
|
18
19
|
const raw = configSchema.parse(process.env);
|
|
19
20
|
// Apply NODE_EXTRA_CA_CERTS from .env if not already set via environment.
|
|
@@ -43,6 +44,7 @@ export const config = {
|
|
|
43
44
|
apiPort: parsedPort,
|
|
44
45
|
logChannelId: parsedLogChannelId,
|
|
45
46
|
workerTimeoutMs: parsedWorkerTimeout,
|
|
47
|
+
openaiApiKey: raw.OPENAI_API_KEY,
|
|
46
48
|
get copilotModel() {
|
|
47
49
|
return _copilotModel;
|
|
48
50
|
},
|
|
@@ -419,6 +419,14 @@ export async function sendToOrchestrator(prompt, source, callback, onToolEvent,
|
|
|
419
419
|
// send a follow-up "Continue" message so the user doesn't have to
|
|
420
420
|
if (finalContent.includes("⏱ Response was cut short (timeout)")) {
|
|
421
421
|
console.log("[nzb] Auto-continuing after timeout…");
|
|
422
|
+
// Notify user that auto-continue is happening
|
|
423
|
+
if (source.type === "telegram") {
|
|
424
|
+
try {
|
|
425
|
+
const { sendProactiveMessage } = await import("../telegram/bot.js");
|
|
426
|
+
await sendProactiveMessage("🔄 Auto-continuing...");
|
|
427
|
+
}
|
|
428
|
+
catch { }
|
|
429
|
+
}
|
|
422
430
|
await sleep(1000);
|
|
423
431
|
void sendToOrchestrator("Continue from where you left off. Do not repeat what was already said.", source, callback, onToolEvent, onUsage);
|
|
424
432
|
}
|
package/dist/setup.js
CHANGED
|
@@ -215,6 +215,20 @@ ${BOLD}╔═══════════════════════
|
|
|
215
215
|
else {
|
|
216
216
|
console.log(`\n${DIM} Skipping Google. You can always set it up later with: nzb setup${RESET}\n`);
|
|
217
217
|
}
|
|
218
|
+
// ── Voice / Whisper Setup ────────────────────────────────
|
|
219
|
+
console.log(`${BOLD}━━━ Voice Message Setup (optional) ━━━${RESET}\n`);
|
|
220
|
+
console.log(`NZB can transcribe voice messages using OpenAI's Whisper API.`);
|
|
221
|
+
console.log(`You need an OpenAI API key from ${CYAN}https://platform.openai.com/api-keys${RESET}`);
|
|
222
|
+
console.log();
|
|
223
|
+
const existingOpenaiKey = existing.OPENAI_API_KEY;
|
|
224
|
+
const openaiKey = await ask(rl, ` OpenAI API Key ${existingOpenaiKey ? `${DIM}(Enter to keep existing)${RESET}` : `${DIM}(Enter to skip)${RESET}`}: `);
|
|
225
|
+
const finalOpenaiKey = openaiKey.trim() || existingOpenaiKey || "";
|
|
226
|
+
if (finalOpenaiKey) {
|
|
227
|
+
console.log(`\n${GREEN} ✓ Whisper transcription enabled${RESET}\n`);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
console.log(`\n${DIM} Skipping voice. Voice messages will be saved but not transcribed.${RESET}\n`);
|
|
231
|
+
}
|
|
218
232
|
// ── Model picker ─────────────────────────────────────────
|
|
219
233
|
console.log(`\n${BOLD}━━━ Default Model ━━━${RESET}\n`);
|
|
220
234
|
console.log(`${DIM}Fetching available models from Copilot...${RESET}`);
|
|
@@ -241,6 +255,8 @@ ${BOLD}╔═══════════════════════
|
|
|
241
255
|
lines.push(`AUTHORIZED_USER_ID=${userId}`);
|
|
242
256
|
lines.push(`API_PORT=${apiPort}`);
|
|
243
257
|
lines.push(`COPILOT_MODEL=${model}`);
|
|
258
|
+
if (finalOpenaiKey)
|
|
259
|
+
lines.push(`OPENAI_API_KEY=${finalOpenaiKey}`);
|
|
244
260
|
writeFileSync(ENV_PATH, lines.join("\n") + "\n");
|
|
245
261
|
// ── Done ─────────────────────────────────────────────────
|
|
246
262
|
console.log(`
|
package/dist/telegram/bot.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { autoRetry } from "@grammyjs/auto-retry";
|
|
2
2
|
import { Menu } from "@grammyjs/menu";
|
|
3
|
-
import { Bot } from "grammy";
|
|
3
|
+
import { Bot, 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";
|
|
@@ -80,7 +80,7 @@ const mainMenu = new Menu("main-menu")
|
|
|
80
80
|
await ctx.reply("No memories stored.");
|
|
81
81
|
}
|
|
82
82
|
else {
|
|
83
|
-
await ctx.reply(formatMemoryList(memories));
|
|
83
|
+
await ctx.reply(formatMemoryList(memories), { parse_mode: "HTML" });
|
|
84
84
|
}
|
|
85
85
|
})
|
|
86
86
|
.submenu("⚙️ Settings", "settings-menu", async (ctx) => {
|
|
@@ -110,18 +110,23 @@ const CATEGORY_ICONS = {
|
|
|
110
110
|
routine: "🔄",
|
|
111
111
|
};
|
|
112
112
|
function formatMemoryList(memories) {
|
|
113
|
-
// Group by category
|
|
114
113
|
const groups = {};
|
|
115
114
|
for (const m of memories) {
|
|
116
115
|
(groups[m.category] ??= []).push(m);
|
|
117
116
|
}
|
|
117
|
+
for (const items of Object.values(groups)) {
|
|
118
|
+
items.sort((a, b) => a.id - b.id);
|
|
119
|
+
}
|
|
118
120
|
const sections = Object.entries(groups).map(([cat, items]) => {
|
|
119
121
|
const icon = CATEGORY_ICONS[cat] || "📝";
|
|
120
|
-
const header = `${icon}
|
|
121
|
-
const lines = items.map((m) =>
|
|
122
|
+
const header = `${icon} <b>${escapeHtml(cat.charAt(0).toUpperCase() + cat.slice(1))}</b>`;
|
|
123
|
+
const lines = items.map((m) => `${m.id}. ${escapeHtml(m.content)}`);
|
|
122
124
|
return `${header}\n${lines.join("\n")}`;
|
|
123
125
|
});
|
|
124
|
-
return `🧠
|
|
126
|
+
return `🧠 <b>${memories.length} memories</b>\n\n${sections.join("\n\n")}`;
|
|
127
|
+
}
|
|
128
|
+
function escapeHtml(text) {
|
|
129
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
125
130
|
}
|
|
126
131
|
export function createBot() {
|
|
127
132
|
if (!config.telegramBotToken) {
|
|
@@ -152,8 +157,17 @@ export function createBot() {
|
|
|
152
157
|
});
|
|
153
158
|
// Register interactive menu plugin
|
|
154
159
|
bot.use(mainMenu);
|
|
155
|
-
//
|
|
156
|
-
|
|
160
|
+
// Persistent reply keyboard — quick actions always visible below chat input
|
|
161
|
+
const replyKeyboard = new Keyboard()
|
|
162
|
+
.text("📊 Status").text("❌ Cancel").row()
|
|
163
|
+
.text("🧠 Memory").text("🔄 Restart")
|
|
164
|
+
.resized()
|
|
165
|
+
.persistent();
|
|
166
|
+
// /start and /help — with inline menu + reply keyboard
|
|
167
|
+
bot.command("start", async (ctx) => {
|
|
168
|
+
await ctx.reply("NZB is online. Quick actions below ⬇️", { reply_markup: replyKeyboard });
|
|
169
|
+
await ctx.reply("Or use the menu:", { reply_markup: mainMenu });
|
|
170
|
+
});
|
|
157
171
|
bot.command("help", (ctx) => ctx.reply("I'm NZB, your AI daemon.\n\n" +
|
|
158
172
|
"Just send me a message and I'll handle it.\n\n" +
|
|
159
173
|
"Commands:\n" +
|
|
@@ -207,7 +221,7 @@ export function createBot() {
|
|
|
207
221
|
await ctx.reply("No memories stored.");
|
|
208
222
|
}
|
|
209
223
|
else {
|
|
210
|
-
await ctx.reply(formatMemoryList(memories));
|
|
224
|
+
await ctx.reply(formatMemoryList(memories), { parse_mode: "HTML" });
|
|
211
225
|
}
|
|
212
226
|
});
|
|
213
227
|
bot.command("skills", async (ctx) => {
|
|
@@ -261,6 +275,35 @@ export function createBot() {
|
|
|
261
275
|
`🤖 Model: ${config.copilotModel}\n` +
|
|
262
276
|
` └ Dùng /model <name> để đổi`, { reply_markup: settingsMenu });
|
|
263
277
|
});
|
|
278
|
+
// Reply keyboard button handlers — intercept before general text handler
|
|
279
|
+
bot.hears("📊 Status", async (ctx) => {
|
|
280
|
+
const workers = Array.from(getWorkers().values());
|
|
281
|
+
const lines = [
|
|
282
|
+
"📊 NZB Status",
|
|
283
|
+
`Model: ${config.copilotModel}`,
|
|
284
|
+
`Uptime: ${getUptimeStr()}`,
|
|
285
|
+
`Workers: ${workers.length} active`,
|
|
286
|
+
`Queue: ${getQueueSize()} pending`,
|
|
287
|
+
];
|
|
288
|
+
await ctx.reply(lines.join("\n"));
|
|
289
|
+
});
|
|
290
|
+
bot.hears("❌ Cancel", async (ctx) => {
|
|
291
|
+
const cancelled = await cancelCurrentMessage();
|
|
292
|
+
await ctx.reply(cancelled ? "Cancelled." : "Nothing to cancel.");
|
|
293
|
+
});
|
|
294
|
+
bot.hears("🧠 Memory", async (ctx) => {
|
|
295
|
+
const memories = searchMemories(undefined, undefined, 50);
|
|
296
|
+
if (memories.length === 0) {
|
|
297
|
+
await ctx.reply("No memories stored.");
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
await ctx.reply(formatMemoryList(memories), { parse_mode: "HTML" });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
bot.hears("🔄 Restart", async (ctx) => {
|
|
304
|
+
await ctx.reply("Restarting NZB...");
|
|
305
|
+
setTimeout(() => { restartDaemon().catch(console.error); }, 500);
|
|
306
|
+
});
|
|
264
307
|
// Handle all text messages — progressive streaming with tool event feedback
|
|
265
308
|
bot.on("message:text", async (ctx) => {
|
|
266
309
|
const chatId = ctx.chat.id;
|
|
@@ -693,6 +736,110 @@ export function createBot() {
|
|
|
693
736
|
});
|
|
694
737
|
}
|
|
695
738
|
});
|
|
739
|
+
// Handle voice messages — download, transcribe via Whisper, send to AI
|
|
740
|
+
bot.on("message:voice", async (ctx) => {
|
|
741
|
+
const chatId = ctx.chat.id;
|
|
742
|
+
const userMessageId = ctx.message.message_id;
|
|
743
|
+
const duration = ctx.message.voice.duration;
|
|
744
|
+
void logInfo(`🎤 Voice received: ${duration}s`);
|
|
745
|
+
try {
|
|
746
|
+
await ctx.react("👀");
|
|
747
|
+
}
|
|
748
|
+
catch { }
|
|
749
|
+
// Limit voice duration to 5 minutes
|
|
750
|
+
if (duration > 300) {
|
|
751
|
+
await ctx.reply("❌ Voice too long (max 5 min).", { reply_parameters: { message_id: userMessageId } });
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const file = await ctx.api.getFile(ctx.message.voice.file_id);
|
|
756
|
+
const filePath = file.file_path;
|
|
757
|
+
if (!filePath) {
|
|
758
|
+
await ctx.reply("❌ Could not download voice.", { reply_parameters: { message_id: userMessageId } });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const url = `https://api.telegram.org/file/bot${config.telegramBotToken}/${filePath}`;
|
|
762
|
+
const { mkdtempSync, writeFileSync } = await import("fs");
|
|
763
|
+
const { join } = await import("path");
|
|
764
|
+
const { tmpdir } = await import("os");
|
|
765
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "nzb-voice-"));
|
|
766
|
+
const ext = filePath.split(".").pop() || "oga";
|
|
767
|
+
const localPath = join(tmpDir, `voice.${ext}`);
|
|
768
|
+
const response = await fetch(url);
|
|
769
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
770
|
+
writeFileSync(localPath, buffer);
|
|
771
|
+
let prompt;
|
|
772
|
+
if (config.openaiApiKey) {
|
|
773
|
+
// Transcribe using OpenAI Whisper API
|
|
774
|
+
try {
|
|
775
|
+
const formData = new FormData();
|
|
776
|
+
formData.append("file", new Blob([buffer], { type: "audio/ogg" }), `voice.${ext}`);
|
|
777
|
+
formData.append("model", "whisper-1");
|
|
778
|
+
const whisperResp = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
779
|
+
method: "POST",
|
|
780
|
+
headers: { Authorization: `Bearer ${config.openaiApiKey}` },
|
|
781
|
+
body: formData,
|
|
782
|
+
});
|
|
783
|
+
if (!whisperResp.ok) {
|
|
784
|
+
const errText = await whisperResp.text();
|
|
785
|
+
throw new Error(`Whisper API ${whisperResp.status}: ${errText.slice(0, 200)}`);
|
|
786
|
+
}
|
|
787
|
+
const result = (await whisperResp.json());
|
|
788
|
+
const transcript = result.text?.trim();
|
|
789
|
+
if (!transcript) {
|
|
790
|
+
prompt = `[User sent a voice message (${duration}s) but transcription was empty. File saved at: ${localPath}]`;
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
prompt = `[Voice message transcribed (${duration}s)]: ${transcript}`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch (whisperErr) {
|
|
797
|
+
console.error("[nzb] Whisper transcription failed:", whisperErr instanceof Error ? whisperErr.message : whisperErr);
|
|
798
|
+
prompt = `[User sent a voice message (${duration}s), saved at: ${localPath}. Transcription failed: ${whisperErr instanceof Error ? whisperErr.message : String(whisperErr)}]`;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
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]`;
|
|
803
|
+
}
|
|
804
|
+
sendToOrchestrator(prompt, { type: "telegram", chatId, messageId: userMessageId }, (text, done) => {
|
|
805
|
+
if (done) {
|
|
806
|
+
const formatted = toTelegramMarkdown(text);
|
|
807
|
+
const chunks = chunkMessage(formatted);
|
|
808
|
+
const fallbackChunks = chunkMessage(text);
|
|
809
|
+
void (async () => {
|
|
810
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
811
|
+
if (i > 0)
|
|
812
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
813
|
+
const pageTag = chunks.length > 1 ? `📄 ${i + 1}/${chunks.length}\n` : "";
|
|
814
|
+
try {
|
|
815
|
+
await ctx.api.sendMessage(chatId, pageTag + chunks[i], {
|
|
816
|
+
parse_mode: "MarkdownV2",
|
|
817
|
+
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
catch {
|
|
821
|
+
try {
|
|
822
|
+
await ctx.api.sendMessage(chatId, pageTag + (fallbackChunks[i] ?? chunks[i]), {
|
|
823
|
+
reply_parameters: i === 0 ? { message_id: userMessageId } : undefined,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
catch { }
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
await ctx.api.setMessageReaction(chatId, userMessageId, [{ type: "emoji", emoji: "👍" }]);
|
|
831
|
+
}
|
|
832
|
+
catch { }
|
|
833
|
+
})();
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
catch (err) {
|
|
838
|
+
await ctx.reply(`❌ Error processing voice: ${err instanceof Error ? err.message : String(err)}`, {
|
|
839
|
+
reply_parameters: { message_id: userMessageId },
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
});
|
|
696
843
|
return bot;
|
|
697
844
|
}
|
|
698
845
|
export async function startBot() {
|