kernelbot 1.0.25 → 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 -123
- package/bin/kernel.js +201 -4
- package/package.json +1 -1
- package/src/agent.js +447 -174
- 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 +908 -69
- package/src/conversation.js +69 -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 +74 -35
- 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 +101 -0
- 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 +53 -16
- 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,7 +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
|
+
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';
|
|
5
15
|
|
|
6
16
|
function splitMessage(text, maxLength = 4096) {
|
|
7
17
|
if (text.length <= maxLength) return [text];
|
|
@@ -22,9 +32,32 @@ function splitMessage(text, maxLength = 4096) {
|
|
|
22
32
|
return chunks;
|
|
23
33
|
}
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Simple per-chat queue to serialize agent processing.
|
|
37
|
+
* Each chat gets its own promise chain so messages are processed in order.
|
|
38
|
+
*/
|
|
39
|
+
class ChatQueue {
|
|
40
|
+
constructor() {
|
|
41
|
+
this.queues = new Map();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
enqueue(chatId, fn) {
|
|
45
|
+
const key = String(chatId);
|
|
46
|
+
const prev = this.queues.get(key) || Promise.resolve();
|
|
47
|
+
const next = prev.then(() => fn()).catch(() => {});
|
|
48
|
+
this.queues.set(key, next);
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function startBot(config, agent, conversationManager, jobManager, automationManager) {
|
|
26
54
|
const logger = getLogger();
|
|
27
55
|
const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
|
|
56
|
+
const chatQueue = new ChatQueue();
|
|
57
|
+
const batchWindowMs = config.telegram.batch_window_ms || 3000;
|
|
58
|
+
|
|
59
|
+
// Per-chat message batching: chatId -> { messages[], timer, resolve }
|
|
60
|
+
const chatBatches = new Map();
|
|
28
61
|
|
|
29
62
|
// Load previous conversations from disk
|
|
30
63
|
const loaded = conversationManager.load();
|
|
@@ -32,25 +65,619 @@ export function startBot(config, agent, conversationManager) {
|
|
|
32
65
|
logger.info('Loaded previous conversations from disk');
|
|
33
66
|
}
|
|
34
67
|
|
|
68
|
+
// Load custom skills from disk
|
|
69
|
+
loadCustomSkills();
|
|
70
|
+
|
|
35
71
|
logger.info('Telegram bot started with polling');
|
|
36
72
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
|
|
142
|
+
// Track pending brain API key input: chatId -> { providerKey, modelId }
|
|
143
|
+
const pendingBrainKey = new Map();
|
|
144
|
+
|
|
145
|
+
// Track pending custom skill creation: chatId -> { step: 'name' | 'prompt', name?: string }
|
|
146
|
+
const pendingCustomSkill = new Map();
|
|
147
|
+
|
|
148
|
+
// Handle inline keyboard callbacks for /brain
|
|
149
|
+
bot.on('callback_query', async (query) => {
|
|
150
|
+
const chatId = query.message.chat.id;
|
|
151
|
+
const data = query.data;
|
|
152
|
+
|
|
153
|
+
if (!isAllowedUser(query.from.id, config)) {
|
|
154
|
+
await bot.answerCallbackQuery(query.id, { text: 'Unauthorized' });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
logger.info(`[Bot] Callback query from chat ${chatId}: ${data}`);
|
|
160
|
+
|
|
161
|
+
if (data.startsWith('brain_provider:')) {
|
|
162
|
+
// User picked a provider — show model list
|
|
163
|
+
const providerKey = data.split(':')[1];
|
|
164
|
+
const providerDef = PROVIDERS[providerKey];
|
|
165
|
+
if (!providerDef) {
|
|
166
|
+
await bot.answerCallbackQuery(query.id, { text: 'Unknown provider' });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const modelButtons = providerDef.models.map((m) => ([{
|
|
171
|
+
text: m.label,
|
|
172
|
+
callback_data: `brain_model:${providerKey}:${m.id}`,
|
|
173
|
+
}]));
|
|
174
|
+
modelButtons.push([{ text: 'Cancel', callback_data: 'brain_cancel' }]);
|
|
175
|
+
|
|
176
|
+
await bot.editMessageText(`Select a *${providerDef.name}* model:`, {
|
|
177
|
+
chat_id: chatId,
|
|
178
|
+
message_id: query.message.message_id,
|
|
179
|
+
parse_mode: 'Markdown',
|
|
180
|
+
reply_markup: { inline_keyboard: modelButtons },
|
|
181
|
+
});
|
|
182
|
+
await bot.answerCallbackQuery(query.id);
|
|
183
|
+
|
|
184
|
+
} else if (data.startsWith('brain_model:')) {
|
|
185
|
+
// User picked a model — attempt switch
|
|
186
|
+
const [, providerKey, modelId] = data.split(':');
|
|
187
|
+
const providerDef = PROVIDERS[providerKey];
|
|
188
|
+
const modelEntry = providerDef?.models.find((m) => m.id === modelId);
|
|
189
|
+
const modelLabel = modelEntry ? modelEntry.label : modelId;
|
|
39
190
|
|
|
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) {
|
|
215
|
+
// API key missing — ask for it
|
|
216
|
+
logger.info(`[Bot] Brain switch needs API key: ${result} for ${providerKey}/${modelId}`);
|
|
217
|
+
pendingBrainKey.set(chatId, { providerKey, modelId });
|
|
218
|
+
await bot.editMessageText(
|
|
219
|
+
`🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${result}\` now.\n\nOr send *cancel* to abort.`,
|
|
220
|
+
{
|
|
221
|
+
chat_id: chatId,
|
|
222
|
+
message_id: query.message.message_id,
|
|
223
|
+
parse_mode: 'Markdown',
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
} else {
|
|
227
|
+
const info = agent.getBrainInfo();
|
|
228
|
+
logger.info(`[Bot] Brain switched successfully to ${info.providerName}/${info.modelLabel}`);
|
|
229
|
+
await bot.editMessageText(
|
|
230
|
+
`🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*`,
|
|
231
|
+
{
|
|
232
|
+
chat_id: chatId,
|
|
233
|
+
message_id: query.message.message_id,
|
|
234
|
+
parse_mode: 'Markdown',
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
await bot.answerCallbackQuery(query.id);
|
|
239
|
+
|
|
240
|
+
} else if (data === 'brain_cancel') {
|
|
241
|
+
pendingBrainKey.delete(chatId);
|
|
242
|
+
await bot.editMessageText('Brain change cancelled.', {
|
|
243
|
+
chat_id: chatId,
|
|
244
|
+
message_id: query.message.message_id,
|
|
245
|
+
});
|
|
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);
|
|
454
|
+
}
|
|
455
|
+
} catch (err) {
|
|
456
|
+
logger.error(`[Bot] Callback query error for "${data}" in chat ${chatId}: ${err.message}`);
|
|
457
|
+
await bot.answerCallbackQuery(query.id, { text: 'Error' });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Batch messages for a chat. Returns the merged text for the first message,
|
|
463
|
+
* or null for subsequent messages (they get merged into the first).
|
|
464
|
+
*/
|
|
465
|
+
function batchMessage(chatId, text) {
|
|
466
|
+
return new Promise((resolve) => {
|
|
467
|
+
const key = String(chatId);
|
|
468
|
+
let batch = chatBatches.get(key);
|
|
469
|
+
|
|
470
|
+
if (!batch) {
|
|
471
|
+
batch = { messages: [], timer: null, resolvers: [] };
|
|
472
|
+
chatBatches.set(key, batch);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
batch.messages.push(text);
|
|
476
|
+
batch.resolvers.push(resolve);
|
|
477
|
+
|
|
478
|
+
// Reset timer on each new message
|
|
479
|
+
if (batch.timer) clearTimeout(batch.timer);
|
|
480
|
+
|
|
481
|
+
batch.timer = setTimeout(() => {
|
|
482
|
+
chatBatches.delete(key);
|
|
483
|
+
const merged = batch.messages.length === 1
|
|
484
|
+
? batch.messages[0]
|
|
485
|
+
: batch.messages.map((m, i) => `[${i + 1}]: ${m}`).join('\n\n');
|
|
486
|
+
|
|
487
|
+
if (batch.messages.length > 1) {
|
|
488
|
+
logger.info(`[Bot] Batch merged ${batch.messages.length} messages for chat ${key}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// First resolver gets the merged text, rest get null (skip)
|
|
492
|
+
batch.resolvers[0](merged);
|
|
493
|
+
for (let i = 1; i < batch.resolvers.length; i++) {
|
|
494
|
+
batch.resolvers[i](null);
|
|
495
|
+
}
|
|
496
|
+
}, batchWindowMs);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
bot.on('message', async (msg) => {
|
|
40
501
|
const chatId = msg.chat.id;
|
|
41
502
|
const userId = msg.from.id;
|
|
42
503
|
const username = msg.from.username || msg.from.first_name || 'unknown';
|
|
43
504
|
|
|
44
505
|
// Auth check
|
|
45
506
|
if (!isAllowedUser(userId, config)) {
|
|
46
|
-
|
|
47
|
-
|
|
507
|
+
if (msg.text || msg.document) {
|
|
508
|
+
logger.warn(`Unauthorized access attempt from ${username} (${userId})`);
|
|
509
|
+
await bot.sendMessage(chatId, getUnauthorizedMessage());
|
|
510
|
+
}
|
|
48
511
|
return;
|
|
49
512
|
}
|
|
50
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
|
+
|
|
51
551
|
let text = msg.text.trim();
|
|
52
552
|
|
|
53
|
-
// Handle
|
|
553
|
+
// Handle pending brain API key input
|
|
554
|
+
if (pendingBrainKey.has(chatId)) {
|
|
555
|
+
const pending = pendingBrainKey.get(chatId);
|
|
556
|
+
pendingBrainKey.delete(chatId);
|
|
557
|
+
|
|
558
|
+
if (text.toLowerCase() === 'cancel') {
|
|
559
|
+
logger.info(`[Bot] Brain key input cancelled by ${username} in chat ${chatId}`);
|
|
560
|
+
await bot.sendMessage(chatId, 'Brain change cancelled.');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
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
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
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
|
+
|
|
621
|
+
// Handle commands — these bypass batching entirely
|
|
622
|
+
if (text === '/brain') {
|
|
623
|
+
logger.info(`[Bot] /brain command from ${username} (${userId}) in chat ${chatId}`);
|
|
624
|
+
const info = agent.getBrainInfo();
|
|
625
|
+
const providerKeys = Object.keys(PROVIDERS);
|
|
626
|
+
const buttons = providerKeys.map((key) => ([{
|
|
627
|
+
text: `${PROVIDERS[key].name}${key === info.provider ? ' ✓' : ''}`,
|
|
628
|
+
callback_data: `brain_provider:${key}`,
|
|
629
|
+
}]));
|
|
630
|
+
buttons.push([{ text: 'Cancel', callback_data: 'brain_cancel' }]);
|
|
631
|
+
|
|
632
|
+
await bot.sendMessage(
|
|
633
|
+
chatId,
|
|
634
|
+
`🧠 *Current brain:* ${info.providerName} / ${info.modelLabel}\n\nSelect a provider to switch:`,
|
|
635
|
+
{
|
|
636
|
+
parse_mode: 'Markdown',
|
|
637
|
+
reply_markup: { inline_keyboard: buttons },
|
|
638
|
+
},
|
|
639
|
+
);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
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
|
+
|
|
54
681
|
if (text === '/clean' || text === '/clear' || text === '/reset') {
|
|
55
682
|
conversationManager.clear(chatId);
|
|
56
683
|
logger.info(`Conversation cleared for chat ${chatId} by ${username}`);
|
|
@@ -64,10 +691,108 @@ export function startBot(config, agent, conversationManager) {
|
|
|
64
691
|
return;
|
|
65
692
|
}
|
|
66
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
|
+
|
|
67
781
|
if (text === '/help') {
|
|
782
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
783
|
+
const skillLine = activeSkill
|
|
784
|
+
? `\n🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n`
|
|
785
|
+
: '';
|
|
68
786
|
await bot.sendMessage(chatId, [
|
|
69
787
|
'*KernelBot Commands*',
|
|
70
|
-
|
|
788
|
+
skillLine,
|
|
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',
|
|
71
796
|
'/clean — Clear conversation and start fresh',
|
|
72
797
|
'/history — Show message count in memory',
|
|
73
798
|
'/browse <url> — Browse a website and get a summary',
|
|
@@ -80,6 +805,107 @@ export function startBot(config, agent, conversationManager) {
|
|
|
80
805
|
return;
|
|
81
806
|
}
|
|
82
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
|
+
|
|
83
909
|
// Web browsing shortcut commands — rewrite as natural language for the agent
|
|
84
910
|
if (text.startsWith('/browse ')) {
|
|
85
911
|
const browseUrl = text.slice('/browse '.length).trim();
|
|
@@ -106,91 +932,104 @@ export function startBot(config, agent, conversationManager) {
|
|
|
106
932
|
text = `Extract content from ${extractUrl} using the CSS selector: ${extractSelector}`;
|
|
107
933
|
}
|
|
108
934
|
|
|
109
|
-
|
|
935
|
+
// Batch messages — wait for the batch window to close
|
|
936
|
+
const mergedText = await batchMessage(chatId, text);
|
|
937
|
+
if (mergedText === null) {
|
|
938
|
+
// This message was merged into another batch — skip
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
logger.info(`Message from ${username} (${userId}): ${mergedText.slice(0, 100)}`);
|
|
110
943
|
|
|
111
|
-
//
|
|
112
|
-
|
|
944
|
+
// Enqueue into per-chat queue for serialized processing
|
|
945
|
+
chatQueue.enqueue(chatId, async () => {
|
|
946
|
+
// Show typing and keep refreshing it
|
|
947
|
+
const typingInterval = setInterval(() => {
|
|
948
|
+
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
949
|
+
}, 4000);
|
|
113
950
|
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
114
|
-
}, 4000);
|
|
115
|
-
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
116
951
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const edited = await bot.editMessageText(update, {
|
|
123
|
-
chat_id: chatId,
|
|
124
|
-
message_id: opts.editMessageId,
|
|
125
|
-
parse_mode: 'Markdown',
|
|
126
|
-
});
|
|
127
|
-
return edited.message_id;
|
|
128
|
-
} catch {
|
|
952
|
+
try {
|
|
953
|
+
const onUpdate = async (update, opts = {}) => {
|
|
954
|
+
// Edit an existing message instead of sending a new one
|
|
955
|
+
if (opts.editMessageId) {
|
|
129
956
|
try {
|
|
130
957
|
const edited = await bot.editMessageText(update, {
|
|
131
958
|
chat_id: chatId,
|
|
132
959
|
message_id: opts.editMessageId,
|
|
960
|
+
parse_mode: 'Markdown',
|
|
133
961
|
});
|
|
134
962
|
return edited.message_id;
|
|
135
963
|
} catch {
|
|
136
|
-
|
|
964
|
+
try {
|
|
965
|
+
const edited = await bot.editMessageText(update, {
|
|
966
|
+
chat_id: chatId,
|
|
967
|
+
message_id: opts.editMessageId,
|
|
968
|
+
});
|
|
969
|
+
return edited.message_id;
|
|
970
|
+
} catch {
|
|
971
|
+
return opts.editMessageId;
|
|
972
|
+
}
|
|
137
973
|
}
|
|
138
974
|
}
|
|
139
|
-
}
|
|
140
975
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
976
|
+
// Send new message(s)
|
|
977
|
+
const parts = splitMessage(update);
|
|
978
|
+
let lastMsgId = null;
|
|
979
|
+
for (const part of parts) {
|
|
980
|
+
try {
|
|
981
|
+
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
982
|
+
lastMsgId = sent.message_id;
|
|
983
|
+
} catch {
|
|
984
|
+
const sent = await bot.sendMessage(chatId, part);
|
|
985
|
+
lastMsgId = sent.message_id;
|
|
986
|
+
}
|
|
151
987
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
988
|
+
return lastMsgId;
|
|
989
|
+
};
|
|
155
990
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
159
|
-
caption: caption || '',
|
|
160
|
-
parse_mode: 'Markdown',
|
|
161
|
-
});
|
|
162
|
-
} catch {
|
|
991
|
+
const sendPhoto = async (filePath, caption) => {
|
|
992
|
+
const fileOpts = { contentType: 'image/png' };
|
|
163
993
|
try {
|
|
164
994
|
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
165
995
|
caption: caption || '',
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
996
|
+
parse_mode: 'Markdown',
|
|
997
|
+
}, fileOpts);
|
|
998
|
+
} catch {
|
|
999
|
+
try {
|
|
1000
|
+
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
1001
|
+
caption: caption || '',
|
|
1002
|
+
}, fileOpts);
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
logger.error(`Failed to send photo: ${err.message}`);
|
|
1005
|
+
}
|
|
169
1006
|
}
|
|
170
|
-
}
|
|
171
|
-
};
|
|
1007
|
+
};
|
|
172
1008
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
1009
|
+
logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
|
|
1010
|
+
const reply = await agent.processMessage(chatId, mergedText, {
|
|
1011
|
+
id: userId,
|
|
1012
|
+
username,
|
|
1013
|
+
}, onUpdate, sendPhoto);
|
|
177
1014
|
|
|
178
|
-
|
|
1015
|
+
clearInterval(typingInterval);
|
|
179
1016
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
1017
|
+
logger.info(`[Bot] Reply for chat ${chatId}: ${(reply || '').length} chars`);
|
|
1018
|
+
const chunks = splitMessage(reply || 'Done.');
|
|
1019
|
+
for (const chunk of chunks) {
|
|
1020
|
+
try {
|
|
1021
|
+
await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
|
|
1022
|
+
} catch {
|
|
1023
|
+
// Fallback to plain text if Markdown fails
|
|
1024
|
+
await bot.sendMessage(chatId, chunk);
|
|
1025
|
+
}
|
|
187
1026
|
}
|
|
1027
|
+
} catch (err) {
|
|
1028
|
+
clearInterval(typingInterval);
|
|
1029
|
+
logger.error(`[Bot] Error processing message in chat ${chatId}: ${err.message}`);
|
|
1030
|
+
await bot.sendMessage(chatId, `Error: ${err.message}`);
|
|
188
1031
|
}
|
|
189
|
-
}
|
|
190
|
-
clearInterval(typingInterval);
|
|
191
|
-
logger.error(`Error processing message: ${err.message}`);
|
|
192
|
-
await bot.sendMessage(chatId, `Error: ${err.message}`);
|
|
193
|
-
}
|
|
1032
|
+
});
|
|
194
1033
|
});
|
|
195
1034
|
|
|
196
1035
|
bot.on('polling_error', (err) => {
|