kernelbot 1.0.26 → 1.0.28
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/README.md +198 -124
- package/bin/kernel.js +201 -4
- package/package.json +1 -1
- package/src/agent.js +397 -222
- package/src/automation/automation-manager.js +377 -0
- package/src/automation/automation.js +79 -0
- package/src/automation/index.js +2 -0
- package/src/automation/scheduler.js +141 -0
- package/src/bot.js +667 -21
- package/src/conversation.js +33 -0
- package/src/intents/detector.js +50 -0
- package/src/intents/index.js +2 -0
- package/src/intents/planner.js +58 -0
- package/src/persona.js +68 -0
- package/src/prompts/orchestrator.js +76 -0
- package/src/prompts/persona.md +21 -0
- package/src/prompts/system.js +59 -6
- package/src/prompts/workers.js +89 -0
- package/src/providers/anthropic.js +23 -16
- package/src/providers/base.js +76 -2
- package/src/providers/index.js +1 -0
- package/src/providers/models.js +2 -1
- package/src/providers/openai-compat.js +5 -3
- package/src/security/confirm.js +7 -2
- package/src/skills/catalog.js +506 -0
- package/src/skills/custom.js +128 -0
- package/src/swarm/job-manager.js +169 -0
- package/src/swarm/job.js +67 -0
- package/src/swarm/worker-registry.js +74 -0
- package/src/tools/browser.js +458 -335
- package/src/tools/categories.js +3 -3
- package/src/tools/index.js +3 -0
- package/src/tools/orchestrator-tools.js +371 -0
- package/src/tools/persona.js +32 -0
- package/src/utils/config.js +50 -15
- package/src/worker.js +305 -0
- package/.agents/skills/interface-design/SKILL.md +0 -391
- package/.agents/skills/interface-design/references/critique.md +0 -67
- package/.agents/skills/interface-design/references/example.md +0 -86
- package/.agents/skills/interface-design/references/principles.md +0 -235
- package/.agents/skills/interface-design/references/validation.md +0 -48
package/src/bot.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import TelegramBot from 'node-telegram-bot-api';
|
|
2
|
-
import { createReadStream } from 'fs';
|
|
2
|
+
import { createReadStream, readFileSync } from 'fs';
|
|
3
3
|
import { isAllowedUser, getUnauthorizedMessage } from './security/auth.js';
|
|
4
4
|
import { getLogger } from './utils/logger.js';
|
|
5
5
|
import { PROVIDERS } from './providers/models.js';
|
|
6
|
+
import {
|
|
7
|
+
getUnifiedSkillById,
|
|
8
|
+
getUnifiedCategoryList,
|
|
9
|
+
getUnifiedSkillsByCategory,
|
|
10
|
+
loadCustomSkills,
|
|
11
|
+
addCustomSkill,
|
|
12
|
+
deleteCustomSkill,
|
|
13
|
+
getCustomSkills,
|
|
14
|
+
} from './skills/custom.js';
|
|
6
15
|
|
|
7
16
|
function splitMessage(text, maxLength = 4096) {
|
|
8
17
|
if (text.length <= maxLength) return [text];
|
|
@@ -41,7 +50,7 @@ class ChatQueue {
|
|
|
41
50
|
}
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
export function startBot(config, agent, conversationManager) {
|
|
53
|
+
export function startBot(config, agent, conversationManager, jobManager, automationManager) {
|
|
45
54
|
const logger = getLogger();
|
|
46
55
|
const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
|
|
47
56
|
const chatQueue = new ChatQueue();
|
|
@@ -56,11 +65,86 @@ export function startBot(config, agent, conversationManager) {
|
|
|
56
65
|
logger.info('Loaded previous conversations from disk');
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
// Load custom skills from disk
|
|
69
|
+
loadCustomSkills();
|
|
70
|
+
|
|
59
71
|
logger.info('Telegram bot started with polling');
|
|
60
72
|
|
|
73
|
+
// Initialize automation manager with bot context
|
|
74
|
+
if (automationManager) {
|
|
75
|
+
const sendMsg = async (chatId, text) => {
|
|
76
|
+
try {
|
|
77
|
+
await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
78
|
+
} catch {
|
|
79
|
+
await bot.sendMessage(chatId, text);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const sendAction = (chatId, action) => bot.sendChatAction(chatId, action).catch(() => {});
|
|
84
|
+
|
|
85
|
+
const agentFactory = (chatId) => {
|
|
86
|
+
const onUpdate = async (update, opts = {}) => {
|
|
87
|
+
if (opts.editMessageId) {
|
|
88
|
+
try {
|
|
89
|
+
const edited = await bot.editMessageText(update, {
|
|
90
|
+
chat_id: chatId,
|
|
91
|
+
message_id: opts.editMessageId,
|
|
92
|
+
parse_mode: 'Markdown',
|
|
93
|
+
});
|
|
94
|
+
return edited.message_id;
|
|
95
|
+
} catch {
|
|
96
|
+
try {
|
|
97
|
+
const edited = await bot.editMessageText(update, {
|
|
98
|
+
chat_id: chatId,
|
|
99
|
+
message_id: opts.editMessageId,
|
|
100
|
+
});
|
|
101
|
+
return edited.message_id;
|
|
102
|
+
} catch {
|
|
103
|
+
return opts.editMessageId;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const parts = splitMessage(update);
|
|
108
|
+
let lastMsgId = null;
|
|
109
|
+
for (const part of parts) {
|
|
110
|
+
try {
|
|
111
|
+
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
112
|
+
lastMsgId = sent.message_id;
|
|
113
|
+
} catch {
|
|
114
|
+
const sent = await bot.sendMessage(chatId, part);
|
|
115
|
+
lastMsgId = sent.message_id;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return lastMsgId;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const sendPhoto = async (filePath, caption) => {
|
|
122
|
+
const fileOpts = { contentType: 'image/png' };
|
|
123
|
+
try {
|
|
124
|
+
await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '', parse_mode: 'Markdown' }, fileOpts);
|
|
125
|
+
} catch {
|
|
126
|
+
try {
|
|
127
|
+
await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '' }, fileOpts);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
logger.error(`[Automation] Failed to send photo: ${err.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return { agent, onUpdate, sendPhoto };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
automationManager.init({ sendMessage: sendMsg, sendChatAction: sendAction, agentFactory, config });
|
|
138
|
+
automationManager.startAll();
|
|
139
|
+
logger.info('[Bot] Automation manager initialized and started');
|
|
140
|
+
}
|
|
141
|
+
|
|
61
142
|
// Track pending brain API key input: chatId -> { providerKey, modelId }
|
|
62
143
|
const pendingBrainKey = new Map();
|
|
63
144
|
|
|
145
|
+
// Track pending custom skill creation: chatId -> { step: 'name' | 'prompt', name?: string }
|
|
146
|
+
const pendingCustomSkill = new Map();
|
|
147
|
+
|
|
64
148
|
// Handle inline keyboard callbacks for /brain
|
|
65
149
|
bot.on('callback_query', async (query) => {
|
|
66
150
|
const chatId = query.message.chat.id;
|
|
@@ -72,6 +156,8 @@ export function startBot(config, agent, conversationManager) {
|
|
|
72
156
|
}
|
|
73
157
|
|
|
74
158
|
try {
|
|
159
|
+
logger.info(`[Bot] Callback query from chat ${chatId}: ${data}`);
|
|
160
|
+
|
|
75
161
|
if (data.startsWith('brain_provider:')) {
|
|
76
162
|
// User picked a provider — show model list
|
|
77
163
|
const providerKey = data.split(':')[1];
|
|
@@ -102,12 +188,35 @@ export function startBot(config, agent, conversationManager) {
|
|
|
102
188
|
const modelEntry = providerDef?.models.find((m) => m.id === modelId);
|
|
103
189
|
const modelLabel = modelEntry ? modelEntry.label : modelId;
|
|
104
190
|
|
|
105
|
-
|
|
106
|
-
|
|
191
|
+
await bot.editMessageText(
|
|
192
|
+
`⏳ Verifying *${providerDef.name}* / *${modelLabel}*...`,
|
|
193
|
+
{
|
|
194
|
+
chat_id: chatId,
|
|
195
|
+
message_id: query.message.message_id,
|
|
196
|
+
parse_mode: 'Markdown',
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
logger.info(`[Bot] Brain switch request: ${providerKey}/${modelId} from chat ${chatId}`);
|
|
201
|
+
const result = await agent.switchBrain(providerKey, modelId);
|
|
202
|
+
if (result && typeof result === 'object' && result.error) {
|
|
203
|
+
// Validation failed — keep current model
|
|
204
|
+
logger.warn(`[Bot] Brain switch failed: ${result.error}`);
|
|
205
|
+
const current = agent.getBrainInfo();
|
|
206
|
+
await bot.editMessageText(
|
|
207
|
+
`❌ Failed to switch: ${result.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
|
|
208
|
+
{
|
|
209
|
+
chat_id: chatId,
|
|
210
|
+
message_id: query.message.message_id,
|
|
211
|
+
parse_mode: 'Markdown',
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
} else if (result) {
|
|
107
215
|
// API key missing — ask for it
|
|
216
|
+
logger.info(`[Bot] Brain switch needs API key: ${result} for ${providerKey}/${modelId}`);
|
|
108
217
|
pendingBrainKey.set(chatId, { providerKey, modelId });
|
|
109
218
|
await bot.editMessageText(
|
|
110
|
-
`🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${
|
|
219
|
+
`🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${result}\` now.\n\nOr send *cancel* to abort.`,
|
|
111
220
|
{
|
|
112
221
|
chat_id: chatId,
|
|
113
222
|
message_id: query.message.message_id,
|
|
@@ -116,6 +225,7 @@ export function startBot(config, agent, conversationManager) {
|
|
|
116
225
|
);
|
|
117
226
|
} else {
|
|
118
227
|
const info = agent.getBrainInfo();
|
|
228
|
+
logger.info(`[Bot] Brain switched successfully to ${info.providerName}/${info.modelLabel}`);
|
|
119
229
|
await bot.editMessageText(
|
|
120
230
|
`🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*`,
|
|
121
231
|
{
|
|
@@ -134,9 +244,216 @@ export function startBot(config, agent, conversationManager) {
|
|
|
134
244
|
message_id: query.message.message_id,
|
|
135
245
|
});
|
|
136
246
|
await bot.answerCallbackQuery(query.id);
|
|
247
|
+
|
|
248
|
+
// ── Skill callbacks ──────────────────────────────────────────
|
|
249
|
+
} else if (data.startsWith('skill_category:')) {
|
|
250
|
+
const categoryKey = data.split(':')[1];
|
|
251
|
+
const skills = getUnifiedSkillsByCategory(categoryKey);
|
|
252
|
+
const categories = getUnifiedCategoryList();
|
|
253
|
+
const cat = categories.find((c) => c.key === categoryKey);
|
|
254
|
+
if (!skills.length) {
|
|
255
|
+
await bot.answerCallbackQuery(query.id, { text: 'No skills in this category' });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
260
|
+
const buttons = skills.map((s) => ([{
|
|
261
|
+
text: `${s.emoji} ${s.name}${activeSkill && activeSkill.id === s.id ? ' ✓' : ''}`,
|
|
262
|
+
callback_data: `skill_select:${s.id}`,
|
|
263
|
+
}]));
|
|
264
|
+
buttons.push([
|
|
265
|
+
{ text: '« Back', callback_data: 'skill_back' },
|
|
266
|
+
{ text: 'Cancel', callback_data: 'skill_cancel' },
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
await bot.editMessageText(
|
|
270
|
+
`${cat ? cat.emoji : ''} *${cat ? cat.name : categoryKey}* — select a skill:`,
|
|
271
|
+
{
|
|
272
|
+
chat_id: chatId,
|
|
273
|
+
message_id: query.message.message_id,
|
|
274
|
+
parse_mode: 'Markdown',
|
|
275
|
+
reply_markup: { inline_keyboard: buttons },
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
await bot.answerCallbackQuery(query.id);
|
|
279
|
+
|
|
280
|
+
} else if (data.startsWith('skill_select:')) {
|
|
281
|
+
const skillId = data.split(':')[1];
|
|
282
|
+
const skill = getUnifiedSkillById(skillId);
|
|
283
|
+
if (!skill) {
|
|
284
|
+
logger.warn(`[Bot] Unknown skill selected: ${skillId}`);
|
|
285
|
+
await bot.answerCallbackQuery(query.id, { text: 'Unknown skill' });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
logger.info(`[Bot] Skill activated: ${skill.name} (${skillId}) for chat ${chatId}`);
|
|
290
|
+
agent.setSkill(chatId, skillId);
|
|
291
|
+
await bot.editMessageText(
|
|
292
|
+
`${skill.emoji} *${skill.name}* activated!\n\n_${skill.description}_\n\nThe agent will now respond as this persona. Use /skills reset to return to default.`,
|
|
293
|
+
{
|
|
294
|
+
chat_id: chatId,
|
|
295
|
+
message_id: query.message.message_id,
|
|
296
|
+
parse_mode: 'Markdown',
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
await bot.answerCallbackQuery(query.id);
|
|
300
|
+
|
|
301
|
+
} else if (data === 'skill_reset') {
|
|
302
|
+
logger.info(`[Bot] Skill reset for chat ${chatId}`);
|
|
303
|
+
agent.clearSkill(chatId);
|
|
304
|
+
await bot.editMessageText('🔄 Skill cleared — back to default persona.', {
|
|
305
|
+
chat_id: chatId,
|
|
306
|
+
message_id: query.message.message_id,
|
|
307
|
+
});
|
|
308
|
+
await bot.answerCallbackQuery(query.id);
|
|
309
|
+
|
|
310
|
+
} else if (data === 'skill_custom_add') {
|
|
311
|
+
pendingCustomSkill.set(chatId, { step: 'name' });
|
|
312
|
+
await bot.editMessageText(
|
|
313
|
+
'✏️ Send me a *name* for your custom skill:',
|
|
314
|
+
{
|
|
315
|
+
chat_id: chatId,
|
|
316
|
+
message_id: query.message.message_id,
|
|
317
|
+
parse_mode: 'Markdown',
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
await bot.answerCallbackQuery(query.id);
|
|
321
|
+
|
|
322
|
+
} else if (data === 'skill_custom_manage') {
|
|
323
|
+
const customs = getCustomSkills();
|
|
324
|
+
if (!customs.length) {
|
|
325
|
+
await bot.answerCallbackQuery(query.id, { text: 'No custom skills yet' });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const buttons = customs.map((s) => ([{
|
|
329
|
+
text: `🗑️ ${s.name}`,
|
|
330
|
+
callback_data: `skill_custom_delete:${s.id}`,
|
|
331
|
+
}]));
|
|
332
|
+
buttons.push([{ text: '« Back', callback_data: 'skill_back' }]);
|
|
333
|
+
|
|
334
|
+
await bot.editMessageText('🛠️ *Custom Skills* — tap to delete:', {
|
|
335
|
+
chat_id: chatId,
|
|
336
|
+
message_id: query.message.message_id,
|
|
337
|
+
parse_mode: 'Markdown',
|
|
338
|
+
reply_markup: { inline_keyboard: buttons },
|
|
339
|
+
});
|
|
340
|
+
await bot.answerCallbackQuery(query.id);
|
|
341
|
+
|
|
342
|
+
} else if (data.startsWith('skill_custom_delete:')) {
|
|
343
|
+
const skillId = data.slice('skill_custom_delete:'.length);
|
|
344
|
+
logger.info(`[Bot] Custom skill delete request: ${skillId} from chat ${chatId}`);
|
|
345
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
346
|
+
if (activeSkill && activeSkill.id === skillId) {
|
|
347
|
+
logger.info(`[Bot] Clearing active skill before deletion: ${skillId}`);
|
|
348
|
+
agent.clearSkill(chatId);
|
|
349
|
+
}
|
|
350
|
+
const deleted = deleteCustomSkill(skillId);
|
|
351
|
+
const msg = deleted ? '🗑️ Custom skill deleted.' : 'Skill not found.';
|
|
352
|
+
await bot.editMessageText(msg, {
|
|
353
|
+
chat_id: chatId,
|
|
354
|
+
message_id: query.message.message_id,
|
|
355
|
+
});
|
|
356
|
+
await bot.answerCallbackQuery(query.id);
|
|
357
|
+
|
|
358
|
+
} else if (data === 'skill_back') {
|
|
359
|
+
// Re-show category list
|
|
360
|
+
const categories = getUnifiedCategoryList();
|
|
361
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
362
|
+
const buttons = categories.map((cat) => ([{
|
|
363
|
+
text: `${cat.emoji} ${cat.name} (${cat.count})`,
|
|
364
|
+
callback_data: `skill_category:${cat.key}`,
|
|
365
|
+
}]));
|
|
366
|
+
// Custom skill management row
|
|
367
|
+
const customRow = [{ text: '➕ Add Custom', callback_data: 'skill_custom_add' }];
|
|
368
|
+
if (getCustomSkills().length > 0) {
|
|
369
|
+
customRow.push({ text: '🗑️ Manage Custom', callback_data: 'skill_custom_manage' });
|
|
370
|
+
}
|
|
371
|
+
buttons.push(customRow);
|
|
372
|
+
const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
|
|
373
|
+
if (activeSkill) {
|
|
374
|
+
footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
|
|
375
|
+
}
|
|
376
|
+
buttons.push(footerRow);
|
|
377
|
+
|
|
378
|
+
const header = activeSkill
|
|
379
|
+
? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
|
|
380
|
+
: '🎭 *Skills* — select a category:';
|
|
381
|
+
|
|
382
|
+
await bot.editMessageText(header, {
|
|
383
|
+
chat_id: chatId,
|
|
384
|
+
message_id: query.message.message_id,
|
|
385
|
+
parse_mode: 'Markdown',
|
|
386
|
+
reply_markup: { inline_keyboard: buttons },
|
|
387
|
+
});
|
|
388
|
+
await bot.answerCallbackQuery(query.id);
|
|
389
|
+
|
|
390
|
+
} else if (data === 'skill_cancel') {
|
|
391
|
+
await bot.editMessageText('Skill selection cancelled.', {
|
|
392
|
+
chat_id: chatId,
|
|
393
|
+
message_id: query.message.message_id,
|
|
394
|
+
});
|
|
395
|
+
await bot.answerCallbackQuery(query.id);
|
|
396
|
+
|
|
397
|
+
// ── Job cancellation callbacks ────────────────────────────────
|
|
398
|
+
} else if (data.startsWith('cancel_job:')) {
|
|
399
|
+
const jobId = data.slice('cancel_job:'.length);
|
|
400
|
+
logger.info(`[Bot] Job cancel request via callback: ${jobId} from chat ${chatId}`);
|
|
401
|
+
const job = jobManager.cancelJob(jobId);
|
|
402
|
+
if (job) {
|
|
403
|
+
logger.info(`[Bot] Job cancelled via callback: ${jobId} [${job.workerType}]`);
|
|
404
|
+
await bot.editMessageText(`🚫 Cancelled job \`${jobId}\` (${job.workerType})`, {
|
|
405
|
+
chat_id: chatId,
|
|
406
|
+
message_id: query.message.message_id,
|
|
407
|
+
parse_mode: 'Markdown',
|
|
408
|
+
});
|
|
409
|
+
} else {
|
|
410
|
+
await bot.editMessageText(`Job \`${jobId}\` not found or already finished.`, {
|
|
411
|
+
chat_id: chatId,
|
|
412
|
+
message_id: query.message.message_id,
|
|
413
|
+
parse_mode: 'Markdown',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
await bot.answerCallbackQuery(query.id);
|
|
417
|
+
|
|
418
|
+
} else if (data === 'cancel_all_jobs') {
|
|
419
|
+
logger.info(`[Bot] Cancel all jobs request via callback from chat ${chatId}`);
|
|
420
|
+
const cancelled = jobManager.cancelAllForChat(chatId);
|
|
421
|
+
const msg = cancelled.length > 0
|
|
422
|
+
? `🚫 Cancelled ${cancelled.length} job(s).`
|
|
423
|
+
: 'No running jobs to cancel.';
|
|
424
|
+
await bot.editMessageText(msg, {
|
|
425
|
+
chat_id: chatId,
|
|
426
|
+
message_id: query.message.message_id,
|
|
427
|
+
});
|
|
428
|
+
await bot.answerCallbackQuery(query.id);
|
|
429
|
+
|
|
430
|
+
// ── Automation callbacks ─────────────────────────────────────────
|
|
431
|
+
} else if (data.startsWith('auto_pause:')) {
|
|
432
|
+
const autoId = data.slice('auto_pause:'.length);
|
|
433
|
+
logger.info(`[Bot] Automation pause request: ${autoId} from chat ${chatId}`);
|
|
434
|
+
const auto = automationManager?.update(autoId, { enabled: false });
|
|
435
|
+
const msg = auto ? `⏸️ Paused automation \`${autoId}\` (${auto.name})` : `Automation \`${autoId}\` not found.`;
|
|
436
|
+
await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
|
|
437
|
+
await bot.answerCallbackQuery(query.id);
|
|
438
|
+
|
|
439
|
+
} else if (data.startsWith('auto_resume:')) {
|
|
440
|
+
const autoId = data.slice('auto_resume:'.length);
|
|
441
|
+
logger.info(`[Bot] Automation resume request: ${autoId} from chat ${chatId}`);
|
|
442
|
+
const auto = automationManager?.update(autoId, { enabled: true });
|
|
443
|
+
const msg = auto ? `▶️ Resumed automation \`${autoId}\` (${auto.name})` : `Automation \`${autoId}\` not found.`;
|
|
444
|
+
await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
|
|
445
|
+
await bot.answerCallbackQuery(query.id);
|
|
446
|
+
|
|
447
|
+
} else if (data.startsWith('auto_delete:')) {
|
|
448
|
+
const autoId = data.slice('auto_delete:'.length);
|
|
449
|
+
logger.info(`[Bot] Automation delete request: ${autoId} from chat ${chatId}`);
|
|
450
|
+
const deleted = automationManager?.delete(autoId);
|
|
451
|
+
const msg = deleted ? `🗑️ Deleted automation \`${autoId}\`` : `Automation \`${autoId}\` not found.`;
|
|
452
|
+
await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
|
|
453
|
+
await bot.answerCallbackQuery(query.id);
|
|
137
454
|
}
|
|
138
455
|
} catch (err) {
|
|
139
|
-
logger.error(`Callback query error: ${err.message}`);
|
|
456
|
+
logger.error(`[Bot] Callback query error for "${data}" in chat ${chatId}: ${err.message}`);
|
|
140
457
|
await bot.answerCallbackQuery(query.id, { text: 'Error' });
|
|
141
458
|
}
|
|
142
459
|
});
|
|
@@ -167,6 +484,10 @@ export function startBot(config, agent, conversationManager) {
|
|
|
167
484
|
? batch.messages[0]
|
|
168
485
|
: batch.messages.map((m, i) => `[${i + 1}]: ${m}`).join('\n\n');
|
|
169
486
|
|
|
487
|
+
if (batch.messages.length > 1) {
|
|
488
|
+
logger.info(`[Bot] Batch merged ${batch.messages.length} messages for chat ${key}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
170
491
|
// First resolver gets the merged text, rest get null (skip)
|
|
171
492
|
batch.resolvers[0](merged);
|
|
172
493
|
for (let i = 1; i < batch.resolvers.length; i++) {
|
|
@@ -177,19 +498,56 @@ export function startBot(config, agent, conversationManager) {
|
|
|
177
498
|
}
|
|
178
499
|
|
|
179
500
|
bot.on('message', async (msg) => {
|
|
180
|
-
if (!msg.text) return; // ignore non-text
|
|
181
|
-
|
|
182
501
|
const chatId = msg.chat.id;
|
|
183
502
|
const userId = msg.from.id;
|
|
184
503
|
const username = msg.from.username || msg.from.first_name || 'unknown';
|
|
185
504
|
|
|
186
505
|
// Auth check
|
|
187
506
|
if (!isAllowedUser(userId, config)) {
|
|
188
|
-
|
|
189
|
-
|
|
507
|
+
if (msg.text || msg.document) {
|
|
508
|
+
logger.warn(`Unauthorized access attempt from ${username} (${userId})`);
|
|
509
|
+
await bot.sendMessage(chatId, getUnauthorizedMessage());
|
|
510
|
+
}
|
|
190
511
|
return;
|
|
191
512
|
}
|
|
192
513
|
|
|
514
|
+
// Handle file upload for pending custom skill prompt step
|
|
515
|
+
if (msg.document && pendingCustomSkill.has(chatId)) {
|
|
516
|
+
const pending = pendingCustomSkill.get(chatId);
|
|
517
|
+
if (pending.step === 'prompt') {
|
|
518
|
+
const doc = msg.document;
|
|
519
|
+
const mime = doc.mime_type || '';
|
|
520
|
+
const fname = doc.file_name || '';
|
|
521
|
+
if (!fname.endsWith('.md') && mime !== 'text/markdown' && mime !== 'text/plain') {
|
|
522
|
+
await bot.sendMessage(chatId, 'Please upload a `.md` or plain text file, or type the prompt directly.');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
const filePath = await bot.downloadFile(doc.file_id, '/tmp');
|
|
527
|
+
const content = readFileSync(filePath, 'utf-8').trim();
|
|
528
|
+
if (!content) {
|
|
529
|
+
await bot.sendMessage(chatId, 'The file appears to be empty. Please try again.');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
pendingCustomSkill.delete(chatId);
|
|
533
|
+
const skill = addCustomSkill({ name: pending.name, systemPrompt: content });
|
|
534
|
+
logger.info(`[Bot] Custom skill created from file: "${skill.name}" (${skill.id}) — ${content.length} chars, by ${username} in chat ${chatId}`);
|
|
535
|
+
agent.setSkill(chatId, skill.id);
|
|
536
|
+
await bot.sendMessage(
|
|
537
|
+
chatId,
|
|
538
|
+
`✅ Custom skill *${skill.name}* created and activated!\n\n_Prompt loaded from file (${content.length} chars)_`,
|
|
539
|
+
{ parse_mode: 'Markdown' },
|
|
540
|
+
);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
logger.error(`Custom skill file upload error: ${err.message}`);
|
|
543
|
+
await bot.sendMessage(chatId, `Failed to read file: ${err.message}`);
|
|
544
|
+
}
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!msg.text) return; // ignore non-text (and non-document) messages
|
|
550
|
+
|
|
193
551
|
let text = msg.text.trim();
|
|
194
552
|
|
|
195
553
|
// Handle pending brain API key input
|
|
@@ -198,22 +556,71 @@ export function startBot(config, agent, conversationManager) {
|
|
|
198
556
|
pendingBrainKey.delete(chatId);
|
|
199
557
|
|
|
200
558
|
if (text.toLowerCase() === 'cancel') {
|
|
559
|
+
logger.info(`[Bot] Brain key input cancelled by ${username} in chat ${chatId}`);
|
|
201
560
|
await bot.sendMessage(chatId, 'Brain change cancelled.');
|
|
202
561
|
return;
|
|
203
562
|
}
|
|
204
563
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
564
|
+
logger.info(`[Bot] Brain key received for ${pending.providerKey}/${pending.modelId} from ${username} in chat ${chatId}`);
|
|
565
|
+
await bot.sendMessage(chatId, '⏳ Verifying API key...');
|
|
566
|
+
const switchResult = await agent.switchBrainWithKey(pending.providerKey, pending.modelId, text);
|
|
567
|
+
if (switchResult && switchResult.error) {
|
|
568
|
+
const current = agent.getBrainInfo();
|
|
569
|
+
await bot.sendMessage(
|
|
570
|
+
chatId,
|
|
571
|
+
`❌ Failed to switch: ${switchResult.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
|
|
572
|
+
{ parse_mode: 'Markdown' },
|
|
573
|
+
);
|
|
574
|
+
} else {
|
|
575
|
+
const info = agent.getBrainInfo();
|
|
576
|
+
await bot.sendMessage(
|
|
577
|
+
chatId,
|
|
578
|
+
`🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*\n\nAPI key saved.`,
|
|
579
|
+
{ parse_mode: 'Markdown' },
|
|
580
|
+
);
|
|
581
|
+
}
|
|
212
582
|
return;
|
|
213
583
|
}
|
|
214
584
|
|
|
585
|
+
// Handle pending custom skill creation (text input for name or prompt)
|
|
586
|
+
if (pendingCustomSkill.has(chatId)) {
|
|
587
|
+
const pending = pendingCustomSkill.get(chatId);
|
|
588
|
+
|
|
589
|
+
if (text.toLowerCase() === 'cancel') {
|
|
590
|
+
pendingCustomSkill.delete(chatId);
|
|
591
|
+
await bot.sendMessage(chatId, 'Custom skill creation cancelled.');
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (pending.step === 'name') {
|
|
596
|
+
pending.name = text;
|
|
597
|
+
pending.step = 'prompt';
|
|
598
|
+
pendingCustomSkill.set(chatId, pending);
|
|
599
|
+
await bot.sendMessage(
|
|
600
|
+
chatId,
|
|
601
|
+
`Got it: *${text}*\n\nNow send the system prompt — type it out or upload a \`.md\` file:`,
|
|
602
|
+
{ parse_mode: 'Markdown' },
|
|
603
|
+
);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (pending.step === 'prompt') {
|
|
608
|
+
pendingCustomSkill.delete(chatId);
|
|
609
|
+
const skill = addCustomSkill({ name: pending.name, systemPrompt: text });
|
|
610
|
+
logger.info(`[Bot] Custom skill created: "${skill.name}" (${skill.id}) by ${username} in chat ${chatId}`);
|
|
611
|
+
agent.setSkill(chatId, skill.id);
|
|
612
|
+
await bot.sendMessage(
|
|
613
|
+
chatId,
|
|
614
|
+
`✅ Custom skill *${skill.name}* created and activated!`,
|
|
615
|
+
{ parse_mode: 'Markdown' },
|
|
616
|
+
);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
215
621
|
// Handle commands — these bypass batching entirely
|
|
216
622
|
if (text === '/brain') {
|
|
623
|
+
logger.info(`[Bot] /brain command from ${username} (${userId}) in chat ${chatId}`);
|
|
217
624
|
const info = agent.getBrainInfo();
|
|
218
625
|
const providerKeys = Object.keys(PROVIDERS);
|
|
219
626
|
const buttons = providerKeys.map((key) => ([{
|
|
@@ -233,6 +640,44 @@ export function startBot(config, agent, conversationManager) {
|
|
|
233
640
|
return;
|
|
234
641
|
}
|
|
235
642
|
|
|
643
|
+
if (text === '/skills reset' || text === '/skill reset') {
|
|
644
|
+
logger.info(`[Bot] /skills reset from ${username} (${userId}) in chat ${chatId}`);
|
|
645
|
+
agent.clearSkill(chatId);
|
|
646
|
+
await bot.sendMessage(chatId, '🔄 Skill cleared — back to default persona.');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (text === '/skills' || text === '/skill') {
|
|
651
|
+
logger.info(`[Bot] /skills command from ${username} (${userId}) in chat ${chatId}`);
|
|
652
|
+
const categories = getUnifiedCategoryList();
|
|
653
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
654
|
+
const buttons = categories.map((cat) => ([{
|
|
655
|
+
text: `${cat.emoji} ${cat.name} (${cat.count})`,
|
|
656
|
+
callback_data: `skill_category:${cat.key}`,
|
|
657
|
+
}]));
|
|
658
|
+
// Custom skill management row
|
|
659
|
+
const customRow = [{ text: '➕ Add Custom', callback_data: 'skill_custom_add' }];
|
|
660
|
+
if (getCustomSkills().length > 0) {
|
|
661
|
+
customRow.push({ text: '🗑️ Manage Custom', callback_data: 'skill_custom_manage' });
|
|
662
|
+
}
|
|
663
|
+
buttons.push(customRow);
|
|
664
|
+
const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
|
|
665
|
+
if (activeSkill) {
|
|
666
|
+
footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
|
|
667
|
+
}
|
|
668
|
+
buttons.push(footerRow);
|
|
669
|
+
|
|
670
|
+
const header = activeSkill
|
|
671
|
+
? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
|
|
672
|
+
: '🎭 *Skills* — select a category:';
|
|
673
|
+
|
|
674
|
+
await bot.sendMessage(chatId, header, {
|
|
675
|
+
parse_mode: 'Markdown',
|
|
676
|
+
reply_markup: { inline_keyboard: buttons },
|
|
677
|
+
});
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
236
681
|
if (text === '/clean' || text === '/clear' || text === '/reset') {
|
|
237
682
|
conversationManager.clear(chatId);
|
|
238
683
|
logger.info(`Conversation cleared for chat ${chatId} by ${username}`);
|
|
@@ -246,11 +691,108 @@ export function startBot(config, agent, conversationManager) {
|
|
|
246
691
|
return;
|
|
247
692
|
}
|
|
248
693
|
|
|
694
|
+
if (text === '/context') {
|
|
695
|
+
const info = agent.getBrainInfo();
|
|
696
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
697
|
+
const msgCount = conversationManager.getMessageCount(chatId);
|
|
698
|
+
const history = conversationManager.getHistory(chatId);
|
|
699
|
+
const maxHistory = conversationManager.maxHistory;
|
|
700
|
+
const recentWindow = conversationManager.recentWindow;
|
|
701
|
+
|
|
702
|
+
// Build recent topics from last few user messages
|
|
703
|
+
const recentUserMsgs = history
|
|
704
|
+
.filter((m) => m.role === 'user')
|
|
705
|
+
.slice(-5)
|
|
706
|
+
.map((m) => {
|
|
707
|
+
const txt = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
708
|
+
return txt.length > 80 ? txt.slice(0, 80) + '…' : txt;
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const lines = [
|
|
712
|
+
'📋 *Conversation Context*',
|
|
713
|
+
'',
|
|
714
|
+
`🧠 *Brain:* ${info.providerName} / ${info.modelLabel}`,
|
|
715
|
+
activeSkill
|
|
716
|
+
? `🎭 *Skill:* ${activeSkill.emoji} ${activeSkill.name}`
|
|
717
|
+
: '🎭 *Skill:* Default persona',
|
|
718
|
+
`💬 *Messages in memory:* ${msgCount} / ${maxHistory}`,
|
|
719
|
+
`📌 *Recent window:* ${recentWindow} messages`,
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
if (recentUserMsgs.length > 0) {
|
|
723
|
+
lines.push('', '🕐 *Recent topics:*');
|
|
724
|
+
recentUserMsgs.forEach((msg) => lines.push(` • ${msg}`));
|
|
725
|
+
} else {
|
|
726
|
+
lines.push('', '_No messages yet — start chatting!_');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (text === '/jobs') {
|
|
734
|
+
logger.info(`[Bot] /jobs command from ${username} (${userId}) in chat ${chatId}`);
|
|
735
|
+
const jobs = jobManager.getJobsForChat(chatId);
|
|
736
|
+
if (jobs.length === 0) {
|
|
737
|
+
await bot.sendMessage(chatId, 'No jobs for this chat.');
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const lines = ['*Jobs*', ''];
|
|
741
|
+
for (const job of jobs.slice(0, 15)) {
|
|
742
|
+
lines.push(job.toSummary());
|
|
743
|
+
}
|
|
744
|
+
if (jobs.length > 15) {
|
|
745
|
+
lines.push(`\n_... and ${jobs.length - 15} more_`);
|
|
746
|
+
}
|
|
747
|
+
await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (text === '/cancel') {
|
|
752
|
+
logger.info(`[Bot] /cancel command from ${username} (${userId}) in chat ${chatId}`);
|
|
753
|
+
const running = jobManager.getRunningJobsForChat(chatId);
|
|
754
|
+
if (running.length === 0) {
|
|
755
|
+
logger.debug(`[Bot] /cancel — no running jobs for chat ${chatId}`);
|
|
756
|
+
await bot.sendMessage(chatId, 'No running jobs to cancel.');
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
if (running.length === 1) {
|
|
760
|
+
logger.info(`[Bot] /cancel — single job ${running[0].id}, cancelling directly`);
|
|
761
|
+
const job = jobManager.cancelJob(running[0].id);
|
|
762
|
+
if (job) {
|
|
763
|
+
await bot.sendMessage(chatId, `🚫 Cancelled \`${job.id}\` (${job.workerType})`, { parse_mode: 'Markdown' });
|
|
764
|
+
}
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
// Multiple running — show inline keyboard
|
|
768
|
+
logger.info(`[Bot] /cancel — ${running.length} running jobs, showing picker`);
|
|
769
|
+
const buttons = running.map((j) => ([{
|
|
770
|
+
text: `🚫 ${j.workerType} (${j.id})`,
|
|
771
|
+
callback_data: `cancel_job:${j.id}`,
|
|
772
|
+
}]));
|
|
773
|
+
buttons.push([{ text: '🚫 Cancel All', callback_data: 'cancel_all_jobs' }]);
|
|
774
|
+
await bot.sendMessage(chatId, `*${running.length} running jobs* — select one to cancel:`, {
|
|
775
|
+
parse_mode: 'Markdown',
|
|
776
|
+
reply_markup: { inline_keyboard: buttons },
|
|
777
|
+
});
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
249
781
|
if (text === '/help') {
|
|
782
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
783
|
+
const skillLine = activeSkill
|
|
784
|
+
? `\n🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n`
|
|
785
|
+
: '';
|
|
250
786
|
await bot.sendMessage(chatId, [
|
|
251
787
|
'*KernelBot Commands*',
|
|
252
|
-
|
|
788
|
+
skillLine,
|
|
253
789
|
'/brain — Show current AI model and switch provider/model',
|
|
790
|
+
'/skills — Browse and activate persona skills',
|
|
791
|
+
'/skills reset — Clear active skill back to default',
|
|
792
|
+
'/jobs — List running and recent jobs',
|
|
793
|
+
'/cancel — Cancel running job(s)',
|
|
794
|
+
'/auto — Manage recurring automations',
|
|
795
|
+
'/context — Show current conversation context and brain info',
|
|
254
796
|
'/clean — Clear conversation and start fresh',
|
|
255
797
|
'/history — Show message count in memory',
|
|
256
798
|
'/browse <url> — Browse a website and get a summary',
|
|
@@ -263,6 +805,107 @@ export function startBot(config, agent, conversationManager) {
|
|
|
263
805
|
return;
|
|
264
806
|
}
|
|
265
807
|
|
|
808
|
+
// ── /auto command ──────────────────────────────────────────────
|
|
809
|
+
if (text === '/auto' || text.startsWith('/auto ')) {
|
|
810
|
+
logger.info(`[Bot] /auto command from ${username} (${userId}) in chat ${chatId}`);
|
|
811
|
+
const args = text.slice('/auto'.length).trim();
|
|
812
|
+
|
|
813
|
+
if (!automationManager) {
|
|
814
|
+
await bot.sendMessage(chatId, 'Automation system not available.');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// /auto (no args) — list automations
|
|
819
|
+
if (!args) {
|
|
820
|
+
const autos = automationManager.listForChat(chatId);
|
|
821
|
+
if (autos.length === 0) {
|
|
822
|
+
await bot.sendMessage(chatId, [
|
|
823
|
+
'⏰ *No automations set up yet.*',
|
|
824
|
+
'',
|
|
825
|
+
'Tell me what to automate in natural language, e.g.:',
|
|
826
|
+
' "check my server health every hour"',
|
|
827
|
+
' "send me a news summary every morning at 9am"',
|
|
828
|
+
'',
|
|
829
|
+
'Or use `/auto` subcommands:',
|
|
830
|
+
' `/auto pause <id>` — pause an automation',
|
|
831
|
+
' `/auto resume <id>` — resume an automation',
|
|
832
|
+
' `/auto delete <id>` — delete an automation',
|
|
833
|
+
' `/auto run <id>` — trigger immediately',
|
|
834
|
+
].join('\n'), { parse_mode: 'Markdown' });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const lines = ['⏰ *Automations*', ''];
|
|
839
|
+
for (const auto of autos) {
|
|
840
|
+
lines.push(auto.toSummary());
|
|
841
|
+
}
|
|
842
|
+
lines.push('', '_Use `/auto pause|resume|delete|run <id>` to manage._');
|
|
843
|
+
|
|
844
|
+
// Build inline keyboard for quick actions
|
|
845
|
+
const buttons = autos.map((a) => {
|
|
846
|
+
const row = [];
|
|
847
|
+
if (a.enabled) {
|
|
848
|
+
row.push({ text: `⏸️ Pause ${a.id}`, callback_data: `auto_pause:${a.id}` });
|
|
849
|
+
} else {
|
|
850
|
+
row.push({ text: `▶️ Resume ${a.id}`, callback_data: `auto_resume:${a.id}` });
|
|
851
|
+
}
|
|
852
|
+
row.push({ text: `🗑️ Delete ${a.id}`, callback_data: `auto_delete:${a.id}` });
|
|
853
|
+
return row;
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
await bot.sendMessage(chatId, lines.join('\n'), {
|
|
857
|
+
parse_mode: 'Markdown',
|
|
858
|
+
reply_markup: { inline_keyboard: buttons },
|
|
859
|
+
});
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// /auto pause <id>
|
|
864
|
+
if (args.startsWith('pause ')) {
|
|
865
|
+
const autoId = args.slice('pause '.length).trim();
|
|
866
|
+
const auto = automationManager.update(autoId, { enabled: false });
|
|
867
|
+
await bot.sendMessage(chatId, auto
|
|
868
|
+
? `⏸️ Paused automation \`${autoId}\` (${auto.name})`
|
|
869
|
+
: `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// /auto resume <id>
|
|
874
|
+
if (args.startsWith('resume ')) {
|
|
875
|
+
const autoId = args.slice('resume '.length).trim();
|
|
876
|
+
const auto = automationManager.update(autoId, { enabled: true });
|
|
877
|
+
await bot.sendMessage(chatId, auto
|
|
878
|
+
? `▶️ Resumed automation \`${autoId}\` (${auto.name})`
|
|
879
|
+
: `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// /auto delete <id>
|
|
884
|
+
if (args.startsWith('delete ')) {
|
|
885
|
+
const autoId = args.slice('delete '.length).trim();
|
|
886
|
+
const deleted = automationManager.delete(autoId);
|
|
887
|
+
await bot.sendMessage(chatId, deleted
|
|
888
|
+
? `🗑️ Deleted automation \`${autoId}\``
|
|
889
|
+
: `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// /auto run <id> — trigger immediately
|
|
894
|
+
if (args.startsWith('run ')) {
|
|
895
|
+
const autoId = args.slice('run '.length).trim();
|
|
896
|
+
try {
|
|
897
|
+
await automationManager.runNow(autoId);
|
|
898
|
+
} catch (err) {
|
|
899
|
+
await bot.sendMessage(chatId, `Failed: ${err.message}`);
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// /auto <anything else> — treat as natural language automation request
|
|
905
|
+
text = `Set up an automation: ${args}`;
|
|
906
|
+
// Fall through to normal message processing below
|
|
907
|
+
}
|
|
908
|
+
|
|
266
909
|
// Web browsing shortcut commands — rewrite as natural language for the agent
|
|
267
910
|
if (text.startsWith('/browse ')) {
|
|
268
911
|
const browseUrl = text.slice('/browse '.length).trim();
|
|
@@ -346,22 +989,24 @@ export function startBot(config, agent, conversationManager) {
|
|
|
346
989
|
};
|
|
347
990
|
|
|
348
991
|
const sendPhoto = async (filePath, caption) => {
|
|
992
|
+
const fileOpts = { contentType: 'image/png' };
|
|
349
993
|
try {
|
|
350
994
|
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
351
995
|
caption: caption || '',
|
|
352
996
|
parse_mode: 'Markdown',
|
|
353
|
-
});
|
|
997
|
+
}, fileOpts);
|
|
354
998
|
} catch {
|
|
355
999
|
try {
|
|
356
1000
|
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
357
1001
|
caption: caption || '',
|
|
358
|
-
});
|
|
1002
|
+
}, fileOpts);
|
|
359
1003
|
} catch (err) {
|
|
360
1004
|
logger.error(`Failed to send photo: ${err.message}`);
|
|
361
1005
|
}
|
|
362
1006
|
}
|
|
363
1007
|
};
|
|
364
1008
|
|
|
1009
|
+
logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
|
|
365
1010
|
const reply = await agent.processMessage(chatId, mergedText, {
|
|
366
1011
|
id: userId,
|
|
367
1012
|
username,
|
|
@@ -369,6 +1014,7 @@ export function startBot(config, agent, conversationManager) {
|
|
|
369
1014
|
|
|
370
1015
|
clearInterval(typingInterval);
|
|
371
1016
|
|
|
1017
|
+
logger.info(`[Bot] Reply for chat ${chatId}: ${(reply || '').length} chars`);
|
|
372
1018
|
const chunks = splitMessage(reply || 'Done.');
|
|
373
1019
|
for (const chunk of chunks) {
|
|
374
1020
|
try {
|
|
@@ -380,7 +1026,7 @@ export function startBot(config, agent, conversationManager) {
|
|
|
380
1026
|
}
|
|
381
1027
|
} catch (err) {
|
|
382
1028
|
clearInterval(typingInterval);
|
|
383
|
-
logger.error(`Error processing message: ${err.message}`);
|
|
1029
|
+
logger.error(`[Bot] Error processing message in chat ${chatId}: ${err.message}`);
|
|
384
1030
|
await bot.sendMessage(chatId, `Error: ${err.message}`);
|
|
385
1031
|
}
|
|
386
1032
|
});
|