kernelbot 1.0.26 → 1.0.30
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/.env.example +4 -0
- package/README.md +198 -124
- package/bin/kernel.js +208 -4
- package/config.example.yaml +14 -1
- package/package.json +1 -1
- package/src/agent.js +839 -209
- 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 +1001 -18
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- 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 +124 -0
- package/src/prompts/persona.md +21 -0
- package/src/prompts/system.js +59 -6
- package/src/prompts/workers.js +148 -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/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +7 -2
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/skills/catalog.js +506 -0
- package/src/skills/custom.js +128 -0
- package/src/swarm/job-manager.js +216 -0
- package/src/swarm/job.js +85 -0
- package/src/swarm/worker-registry.js +79 -0
- package/src/tools/browser.js +458 -335
- package/src/tools/categories.js +3 -3
- package/src/tools/coding.js +5 -0
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +3 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +428 -0
- package/src/tools/os.js +14 -1
- package/src/tools/persona.js +32 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +153 -15
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/worker.js +396 -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,20 @@
|
|
|
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';
|
|
15
|
+
import { TTSService } from './services/tts.js';
|
|
16
|
+
import { STTService } from './services/stt.js';
|
|
17
|
+
import { getClaudeAuthStatus, claudeLogout } from './claude-auth.js';
|
|
6
18
|
|
|
7
19
|
function splitMessage(text, maxLength = 4096) {
|
|
8
20
|
if (text.length <= maxLength) return [text];
|
|
@@ -41,12 +53,18 @@ class ChatQueue {
|
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
export function startBot(config, agent, conversationManager) {
|
|
56
|
+
export function startBot(config, agent, conversationManager, jobManager, automationManager) {
|
|
45
57
|
const logger = getLogger();
|
|
46
58
|
const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
|
|
47
59
|
const chatQueue = new ChatQueue();
|
|
48
60
|
const batchWindowMs = config.telegram.batch_window_ms || 3000;
|
|
49
61
|
|
|
62
|
+
// Initialize voice services
|
|
63
|
+
const ttsService = new TTSService(config);
|
|
64
|
+
const sttService = new STTService(config);
|
|
65
|
+
if (ttsService.isAvailable()) logger.info('[Bot] TTS service enabled (ElevenLabs)');
|
|
66
|
+
if (sttService.isAvailable()) logger.info('[Bot] STT service enabled');
|
|
67
|
+
|
|
50
68
|
// Per-chat message batching: chatId -> { messages[], timer, resolve }
|
|
51
69
|
const chatBatches = new Map();
|
|
52
70
|
|
|
@@ -56,11 +74,108 @@ export function startBot(config, agent, conversationManager) {
|
|
|
56
74
|
logger.info('Loaded previous conversations from disk');
|
|
57
75
|
}
|
|
58
76
|
|
|
77
|
+
// Load custom skills from disk
|
|
78
|
+
loadCustomSkills();
|
|
79
|
+
|
|
80
|
+
// Register commands in Telegram's menu button
|
|
81
|
+
bot.setMyCommands([
|
|
82
|
+
{ command: 'brain', description: 'Switch worker AI model/provider' },
|
|
83
|
+
{ command: 'orchestrator', description: 'Switch orchestrator AI model/provider' },
|
|
84
|
+
{ command: 'claudemodel', description: 'Switch Claude Code model' },
|
|
85
|
+
{ command: 'claude', description: 'Manage Claude Code authentication' },
|
|
86
|
+
{ command: 'skills', description: 'Browse and activate persona skills' },
|
|
87
|
+
{ command: 'jobs', description: 'List running and recent jobs' },
|
|
88
|
+
{ command: 'cancel', description: 'Cancel running job(s)' },
|
|
89
|
+
{ command: 'auto', description: 'Manage recurring automations' },
|
|
90
|
+
{ command: 'context', description: 'Show all models, auth, and context info' },
|
|
91
|
+
{ command: 'clean', description: 'Clear conversation and start fresh' },
|
|
92
|
+
{ command: 'history', description: 'Show message count in memory' },
|
|
93
|
+
{ command: 'help', description: 'Show all available commands' },
|
|
94
|
+
]).catch((err) => logger.warn(`Failed to set bot commands menu: ${err.message}`));
|
|
95
|
+
|
|
59
96
|
logger.info('Telegram bot started with polling');
|
|
60
97
|
|
|
98
|
+
// Initialize automation manager with bot context
|
|
99
|
+
if (automationManager) {
|
|
100
|
+
const sendMsg = async (chatId, text) => {
|
|
101
|
+
try {
|
|
102
|
+
await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
103
|
+
} catch {
|
|
104
|
+
await bot.sendMessage(chatId, text);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const sendAction = (chatId, action) => bot.sendChatAction(chatId, action).catch(() => {});
|
|
109
|
+
|
|
110
|
+
const agentFactory = (chatId) => {
|
|
111
|
+
const onUpdate = async (update, opts = {}) => {
|
|
112
|
+
if (opts.editMessageId) {
|
|
113
|
+
try {
|
|
114
|
+
const edited = await bot.editMessageText(update, {
|
|
115
|
+
chat_id: chatId,
|
|
116
|
+
message_id: opts.editMessageId,
|
|
117
|
+
parse_mode: 'Markdown',
|
|
118
|
+
});
|
|
119
|
+
return edited.message_id;
|
|
120
|
+
} catch {
|
|
121
|
+
try {
|
|
122
|
+
const edited = await bot.editMessageText(update, {
|
|
123
|
+
chat_id: chatId,
|
|
124
|
+
message_id: opts.editMessageId,
|
|
125
|
+
});
|
|
126
|
+
return edited.message_id;
|
|
127
|
+
} catch {
|
|
128
|
+
return opts.editMessageId;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const parts = splitMessage(update);
|
|
133
|
+
let lastMsgId = null;
|
|
134
|
+
for (const part of parts) {
|
|
135
|
+
try {
|
|
136
|
+
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
137
|
+
lastMsgId = sent.message_id;
|
|
138
|
+
} catch {
|
|
139
|
+
const sent = await bot.sendMessage(chatId, part);
|
|
140
|
+
lastMsgId = sent.message_id;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return lastMsgId;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const sendPhoto = async (filePath, caption) => {
|
|
147
|
+
const fileOpts = { contentType: 'image/png' };
|
|
148
|
+
try {
|
|
149
|
+
await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '', parse_mode: 'Markdown' }, fileOpts);
|
|
150
|
+
} catch {
|
|
151
|
+
try {
|
|
152
|
+
await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '' }, fileOpts);
|
|
153
|
+
} catch (err) {
|
|
154
|
+
logger.error(`[Automation] Failed to send photo: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return { agent, onUpdate, sendPhoto };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
automationManager.init({ sendMessage: sendMsg, sendChatAction: sendAction, agentFactory, config });
|
|
163
|
+
automationManager.startAll();
|
|
164
|
+
logger.info('[Bot] Automation manager initialized and started');
|
|
165
|
+
}
|
|
166
|
+
|
|
61
167
|
// Track pending brain API key input: chatId -> { providerKey, modelId }
|
|
62
168
|
const pendingBrainKey = new Map();
|
|
63
169
|
|
|
170
|
+
// Track pending orchestrator API key input: chatId -> { providerKey, modelId }
|
|
171
|
+
const pendingOrchKey = new Map();
|
|
172
|
+
|
|
173
|
+
// Track pending Claude Code auth input: chatId -> { type: 'api_key' | 'oauth_token' }
|
|
174
|
+
const pendingClaudeAuth = new Map();
|
|
175
|
+
|
|
176
|
+
// Track pending custom skill creation: chatId -> { step: 'name' | 'prompt', name?: string }
|
|
177
|
+
const pendingCustomSkill = new Map();
|
|
178
|
+
|
|
64
179
|
// Handle inline keyboard callbacks for /brain
|
|
65
180
|
bot.on('callback_query', async (query) => {
|
|
66
181
|
const chatId = query.message.chat.id;
|
|
@@ -72,6 +187,8 @@ export function startBot(config, agent, conversationManager) {
|
|
|
72
187
|
}
|
|
73
188
|
|
|
74
189
|
try {
|
|
190
|
+
logger.info(`[Bot] Callback query from chat ${chatId}: ${data}`);
|
|
191
|
+
|
|
75
192
|
if (data.startsWith('brain_provider:')) {
|
|
76
193
|
// User picked a provider — show model list
|
|
77
194
|
const providerKey = data.split(':')[1];
|
|
@@ -102,12 +219,35 @@ export function startBot(config, agent, conversationManager) {
|
|
|
102
219
|
const modelEntry = providerDef?.models.find((m) => m.id === modelId);
|
|
103
220
|
const modelLabel = modelEntry ? modelEntry.label : modelId;
|
|
104
221
|
|
|
105
|
-
|
|
106
|
-
|
|
222
|
+
await bot.editMessageText(
|
|
223
|
+
`⏳ Verifying *${providerDef.name}* / *${modelLabel}*...`,
|
|
224
|
+
{
|
|
225
|
+
chat_id: chatId,
|
|
226
|
+
message_id: query.message.message_id,
|
|
227
|
+
parse_mode: 'Markdown',
|
|
228
|
+
},
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
logger.info(`[Bot] Brain switch request: ${providerKey}/${modelId} from chat ${chatId}`);
|
|
232
|
+
const result = await agent.switchBrain(providerKey, modelId);
|
|
233
|
+
if (result && typeof result === 'object' && result.error) {
|
|
234
|
+
// Validation failed — keep current model
|
|
235
|
+
logger.warn(`[Bot] Brain switch failed: ${result.error}`);
|
|
236
|
+
const current = agent.getBrainInfo();
|
|
237
|
+
await bot.editMessageText(
|
|
238
|
+
`❌ Failed to switch: ${result.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
|
|
239
|
+
{
|
|
240
|
+
chat_id: chatId,
|
|
241
|
+
message_id: query.message.message_id,
|
|
242
|
+
parse_mode: 'Markdown',
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
} else if (result) {
|
|
107
246
|
// API key missing — ask for it
|
|
247
|
+
logger.info(`[Bot] Brain switch needs API key: ${result} for ${providerKey}/${modelId}`);
|
|
108
248
|
pendingBrainKey.set(chatId, { providerKey, modelId });
|
|
109
249
|
await bot.editMessageText(
|
|
110
|
-
`🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${
|
|
250
|
+
`🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${result}\` now.\n\nOr send *cancel* to abort.`,
|
|
111
251
|
{
|
|
112
252
|
chat_id: chatId,
|
|
113
253
|
message_id: query.message.message_id,
|
|
@@ -116,6 +256,7 @@ export function startBot(config, agent, conversationManager) {
|
|
|
116
256
|
);
|
|
117
257
|
} else {
|
|
118
258
|
const info = agent.getBrainInfo();
|
|
259
|
+
logger.info(`[Bot] Brain switched successfully to ${info.providerName}/${info.modelLabel}`);
|
|
119
260
|
await bot.editMessageText(
|
|
120
261
|
`🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*`,
|
|
121
262
|
{
|
|
@@ -134,9 +275,350 @@ export function startBot(config, agent, conversationManager) {
|
|
|
134
275
|
message_id: query.message.message_id,
|
|
135
276
|
});
|
|
136
277
|
await bot.answerCallbackQuery(query.id);
|
|
278
|
+
|
|
279
|
+
// ── Skill callbacks ──────────────────────────────────────────
|
|
280
|
+
} else if (data.startsWith('skill_category:')) {
|
|
281
|
+
const categoryKey = data.split(':')[1];
|
|
282
|
+
const skills = getUnifiedSkillsByCategory(categoryKey);
|
|
283
|
+
const categories = getUnifiedCategoryList();
|
|
284
|
+
const cat = categories.find((c) => c.key === categoryKey);
|
|
285
|
+
if (!skills.length) {
|
|
286
|
+
await bot.answerCallbackQuery(query.id, { text: 'No skills in this category' });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
291
|
+
const buttons = skills.map((s) => ([{
|
|
292
|
+
text: `${s.emoji} ${s.name}${activeSkill && activeSkill.id === s.id ? ' ✓' : ''}`,
|
|
293
|
+
callback_data: `skill_select:${s.id}`,
|
|
294
|
+
}]));
|
|
295
|
+
buttons.push([
|
|
296
|
+
{ text: '« Back', callback_data: 'skill_back' },
|
|
297
|
+
{ text: 'Cancel', callback_data: 'skill_cancel' },
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
await bot.editMessageText(
|
|
301
|
+
`${cat ? cat.emoji : ''} *${cat ? cat.name : categoryKey}* — select a skill:`,
|
|
302
|
+
{
|
|
303
|
+
chat_id: chatId,
|
|
304
|
+
message_id: query.message.message_id,
|
|
305
|
+
parse_mode: 'Markdown',
|
|
306
|
+
reply_markup: { inline_keyboard: buttons },
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
await bot.answerCallbackQuery(query.id);
|
|
310
|
+
|
|
311
|
+
} else if (data.startsWith('skill_select:')) {
|
|
312
|
+
const skillId = data.split(':')[1];
|
|
313
|
+
const skill = getUnifiedSkillById(skillId);
|
|
314
|
+
if (!skill) {
|
|
315
|
+
logger.warn(`[Bot] Unknown skill selected: ${skillId}`);
|
|
316
|
+
await bot.answerCallbackQuery(query.id, { text: 'Unknown skill' });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
logger.info(`[Bot] Skill activated: ${skill.name} (${skillId}) for chat ${chatId}`);
|
|
321
|
+
agent.setSkill(chatId, skillId);
|
|
322
|
+
await bot.editMessageText(
|
|
323
|
+
`${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.`,
|
|
324
|
+
{
|
|
325
|
+
chat_id: chatId,
|
|
326
|
+
message_id: query.message.message_id,
|
|
327
|
+
parse_mode: 'Markdown',
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
await bot.answerCallbackQuery(query.id);
|
|
331
|
+
|
|
332
|
+
} else if (data === 'skill_reset') {
|
|
333
|
+
logger.info(`[Bot] Skill reset for chat ${chatId}`);
|
|
334
|
+
agent.clearSkill(chatId);
|
|
335
|
+
await bot.editMessageText('🔄 Skill cleared — back to default persona.', {
|
|
336
|
+
chat_id: chatId,
|
|
337
|
+
message_id: query.message.message_id,
|
|
338
|
+
});
|
|
339
|
+
await bot.answerCallbackQuery(query.id);
|
|
340
|
+
|
|
341
|
+
} else if (data === 'skill_custom_add') {
|
|
342
|
+
pendingCustomSkill.set(chatId, { step: 'name' });
|
|
343
|
+
await bot.editMessageText(
|
|
344
|
+
'✏️ Send me a *name* for your custom skill:',
|
|
345
|
+
{
|
|
346
|
+
chat_id: chatId,
|
|
347
|
+
message_id: query.message.message_id,
|
|
348
|
+
parse_mode: 'Markdown',
|
|
349
|
+
},
|
|
350
|
+
);
|
|
351
|
+
await bot.answerCallbackQuery(query.id);
|
|
352
|
+
|
|
353
|
+
} else if (data === 'skill_custom_manage') {
|
|
354
|
+
const customs = getCustomSkills();
|
|
355
|
+
if (!customs.length) {
|
|
356
|
+
await bot.answerCallbackQuery(query.id, { text: 'No custom skills yet' });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const buttons = customs.map((s) => ([{
|
|
360
|
+
text: `🗑️ ${s.name}`,
|
|
361
|
+
callback_data: `skill_custom_delete:${s.id}`,
|
|
362
|
+
}]));
|
|
363
|
+
buttons.push([{ text: '« Back', callback_data: 'skill_back' }]);
|
|
364
|
+
|
|
365
|
+
await bot.editMessageText('🛠️ *Custom Skills* — tap to delete:', {
|
|
366
|
+
chat_id: chatId,
|
|
367
|
+
message_id: query.message.message_id,
|
|
368
|
+
parse_mode: 'Markdown',
|
|
369
|
+
reply_markup: { inline_keyboard: buttons },
|
|
370
|
+
});
|
|
371
|
+
await bot.answerCallbackQuery(query.id);
|
|
372
|
+
|
|
373
|
+
} else if (data.startsWith('skill_custom_delete:')) {
|
|
374
|
+
const skillId = data.slice('skill_custom_delete:'.length);
|
|
375
|
+
logger.info(`[Bot] Custom skill delete request: ${skillId} from chat ${chatId}`);
|
|
376
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
377
|
+
if (activeSkill && activeSkill.id === skillId) {
|
|
378
|
+
logger.info(`[Bot] Clearing active skill before deletion: ${skillId}`);
|
|
379
|
+
agent.clearSkill(chatId);
|
|
380
|
+
}
|
|
381
|
+
const deleted = deleteCustomSkill(skillId);
|
|
382
|
+
const msg = deleted ? '🗑️ Custom skill deleted.' : 'Skill not found.';
|
|
383
|
+
await bot.editMessageText(msg, {
|
|
384
|
+
chat_id: chatId,
|
|
385
|
+
message_id: query.message.message_id,
|
|
386
|
+
});
|
|
387
|
+
await bot.answerCallbackQuery(query.id);
|
|
388
|
+
|
|
389
|
+
} else if (data === 'skill_back') {
|
|
390
|
+
// Re-show category list
|
|
391
|
+
const categories = getUnifiedCategoryList();
|
|
392
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
393
|
+
const buttons = categories.map((cat) => ([{
|
|
394
|
+
text: `${cat.emoji} ${cat.name} (${cat.count})`,
|
|
395
|
+
callback_data: `skill_category:${cat.key}`,
|
|
396
|
+
}]));
|
|
397
|
+
// Custom skill management row
|
|
398
|
+
const customRow = [{ text: '➕ Add Custom', callback_data: 'skill_custom_add' }];
|
|
399
|
+
if (getCustomSkills().length > 0) {
|
|
400
|
+
customRow.push({ text: '🗑️ Manage Custom', callback_data: 'skill_custom_manage' });
|
|
401
|
+
}
|
|
402
|
+
buttons.push(customRow);
|
|
403
|
+
const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
|
|
404
|
+
if (activeSkill) {
|
|
405
|
+
footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
|
|
406
|
+
}
|
|
407
|
+
buttons.push(footerRow);
|
|
408
|
+
|
|
409
|
+
const header = activeSkill
|
|
410
|
+
? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
|
|
411
|
+
: '🎭 *Skills* — select a category:';
|
|
412
|
+
|
|
413
|
+
await bot.editMessageText(header, {
|
|
414
|
+
chat_id: chatId,
|
|
415
|
+
message_id: query.message.message_id,
|
|
416
|
+
parse_mode: 'Markdown',
|
|
417
|
+
reply_markup: { inline_keyboard: buttons },
|
|
418
|
+
});
|
|
419
|
+
await bot.answerCallbackQuery(query.id);
|
|
420
|
+
|
|
421
|
+
} else if (data === 'skill_cancel') {
|
|
422
|
+
await bot.editMessageText('Skill selection cancelled.', {
|
|
423
|
+
chat_id: chatId,
|
|
424
|
+
message_id: query.message.message_id,
|
|
425
|
+
});
|
|
426
|
+
await bot.answerCallbackQuery(query.id);
|
|
427
|
+
|
|
428
|
+
// ── Job cancellation callbacks ────────────────────────────────
|
|
429
|
+
} else if (data.startsWith('cancel_job:')) {
|
|
430
|
+
const jobId = data.slice('cancel_job:'.length);
|
|
431
|
+
logger.info(`[Bot] Job cancel request via callback: ${jobId} from chat ${chatId}`);
|
|
432
|
+
const job = jobManager.cancelJob(jobId);
|
|
433
|
+
if (job) {
|
|
434
|
+
logger.info(`[Bot] Job cancelled via callback: ${jobId} [${job.workerType}]`);
|
|
435
|
+
await bot.editMessageText(`🚫 Cancelled job \`${jobId}\` (${job.workerType})`, {
|
|
436
|
+
chat_id: chatId,
|
|
437
|
+
message_id: query.message.message_id,
|
|
438
|
+
parse_mode: 'Markdown',
|
|
439
|
+
});
|
|
440
|
+
} else {
|
|
441
|
+
await bot.editMessageText(`Job \`${jobId}\` not found or already finished.`, {
|
|
442
|
+
chat_id: chatId,
|
|
443
|
+
message_id: query.message.message_id,
|
|
444
|
+
parse_mode: 'Markdown',
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
await bot.answerCallbackQuery(query.id);
|
|
448
|
+
|
|
449
|
+
} else if (data === 'cancel_all_jobs') {
|
|
450
|
+
logger.info(`[Bot] Cancel all jobs request via callback from chat ${chatId}`);
|
|
451
|
+
const cancelled = jobManager.cancelAllForChat(chatId);
|
|
452
|
+
const msg = cancelled.length > 0
|
|
453
|
+
? `🚫 Cancelled ${cancelled.length} job(s).`
|
|
454
|
+
: 'No running jobs to cancel.';
|
|
455
|
+
await bot.editMessageText(msg, {
|
|
456
|
+
chat_id: chatId,
|
|
457
|
+
message_id: query.message.message_id,
|
|
458
|
+
});
|
|
459
|
+
await bot.answerCallbackQuery(query.id);
|
|
460
|
+
|
|
461
|
+
// ── Automation callbacks ─────────────────────────────────────────
|
|
462
|
+
} else if (data.startsWith('auto_pause:')) {
|
|
463
|
+
const autoId = data.slice('auto_pause:'.length);
|
|
464
|
+
logger.info(`[Bot] Automation pause request: ${autoId} from chat ${chatId}`);
|
|
465
|
+
const auto = automationManager?.update(autoId, { enabled: false });
|
|
466
|
+
const msg = auto ? `⏸️ Paused automation \`${autoId}\` (${auto.name})` : `Automation \`${autoId}\` not found.`;
|
|
467
|
+
await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
|
|
468
|
+
await bot.answerCallbackQuery(query.id);
|
|
469
|
+
|
|
470
|
+
} else if (data.startsWith('auto_resume:')) {
|
|
471
|
+
const autoId = data.slice('auto_resume:'.length);
|
|
472
|
+
logger.info(`[Bot] Automation resume request: ${autoId} from chat ${chatId}`);
|
|
473
|
+
const auto = automationManager?.update(autoId, { enabled: true });
|
|
474
|
+
const msg = auto ? `▶️ Resumed automation \`${autoId}\` (${auto.name})` : `Automation \`${autoId}\` not found.`;
|
|
475
|
+
await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
|
|
476
|
+
await bot.answerCallbackQuery(query.id);
|
|
477
|
+
|
|
478
|
+
} else if (data.startsWith('auto_delete:')) {
|
|
479
|
+
const autoId = data.slice('auto_delete:'.length);
|
|
480
|
+
logger.info(`[Bot] Automation delete request: ${autoId} from chat ${chatId}`);
|
|
481
|
+
const deleted = automationManager?.delete(autoId);
|
|
482
|
+
const msg = deleted ? `🗑️ Deleted automation \`${autoId}\`` : `Automation \`${autoId}\` not found.`;
|
|
483
|
+
await bot.editMessageText(msg, { chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' });
|
|
484
|
+
await bot.answerCallbackQuery(query.id);
|
|
485
|
+
|
|
486
|
+
// ── Orchestrator callbacks ─────────────────────────────────────
|
|
487
|
+
} else if (data.startsWith('orch_provider:')) {
|
|
488
|
+
const providerKey = data.split(':')[1];
|
|
489
|
+
const providerDef = PROVIDERS[providerKey];
|
|
490
|
+
if (!providerDef) {
|
|
491
|
+
await bot.answerCallbackQuery(query.id, { text: 'Unknown provider' });
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const modelButtons = providerDef.models.map((m) => ([{
|
|
496
|
+
text: m.label,
|
|
497
|
+
callback_data: `orch_model:${providerKey}:${m.id}`,
|
|
498
|
+
}]));
|
|
499
|
+
modelButtons.push([{ text: 'Cancel', callback_data: 'orch_cancel' }]);
|
|
500
|
+
|
|
501
|
+
await bot.editMessageText(`Select a *${providerDef.name}* model for orchestrator:`, {
|
|
502
|
+
chat_id: chatId,
|
|
503
|
+
message_id: query.message.message_id,
|
|
504
|
+
parse_mode: 'Markdown',
|
|
505
|
+
reply_markup: { inline_keyboard: modelButtons },
|
|
506
|
+
});
|
|
507
|
+
await bot.answerCallbackQuery(query.id);
|
|
508
|
+
|
|
509
|
+
} else if (data.startsWith('orch_model:')) {
|
|
510
|
+
const [, providerKey, modelId] = data.split(':');
|
|
511
|
+
const providerDef = PROVIDERS[providerKey];
|
|
512
|
+
const modelEntry = providerDef?.models.find((m) => m.id === modelId);
|
|
513
|
+
const modelLabel = modelEntry ? modelEntry.label : modelId;
|
|
514
|
+
|
|
515
|
+
await bot.editMessageText(
|
|
516
|
+
`⏳ Verifying *${providerDef.name}* / *${modelLabel}* for orchestrator...`,
|
|
517
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
logger.info(`[Bot] Orchestrator switch request: ${providerKey}/${modelId} from chat ${chatId}`);
|
|
521
|
+
const result = await agent.switchOrchestrator(providerKey, modelId);
|
|
522
|
+
if (result && typeof result === 'object' && result.error) {
|
|
523
|
+
const current = agent.getOrchestratorInfo();
|
|
524
|
+
await bot.editMessageText(
|
|
525
|
+
`❌ Failed to switch: ${result.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
|
|
526
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
527
|
+
);
|
|
528
|
+
} else if (result) {
|
|
529
|
+
// API key missing
|
|
530
|
+
logger.info(`[Bot] Orchestrator switch needs API key: ${result} for ${providerKey}/${modelId}`);
|
|
531
|
+
pendingOrchKey.set(chatId, { providerKey, modelId });
|
|
532
|
+
await bot.editMessageText(
|
|
533
|
+
`🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${result}\` now.\n\nOr send *cancel* to abort.`,
|
|
534
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
535
|
+
);
|
|
536
|
+
} else {
|
|
537
|
+
const info = agent.getOrchestratorInfo();
|
|
538
|
+
await bot.editMessageText(
|
|
539
|
+
`🎛️ Orchestrator switched to *${info.providerName}* / *${info.modelLabel}*`,
|
|
540
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
await bot.answerCallbackQuery(query.id);
|
|
544
|
+
|
|
545
|
+
} else if (data === 'orch_cancel') {
|
|
546
|
+
pendingOrchKey.delete(chatId);
|
|
547
|
+
await bot.editMessageText('Orchestrator change cancelled.', {
|
|
548
|
+
chat_id: chatId, message_id: query.message.message_id,
|
|
549
|
+
});
|
|
550
|
+
await bot.answerCallbackQuery(query.id);
|
|
551
|
+
|
|
552
|
+
// ── Claude Code model callbacks ────────────────────────────────
|
|
553
|
+
} else if (data.startsWith('ccmodel:')) {
|
|
554
|
+
const modelId = data.slice('ccmodel:'.length);
|
|
555
|
+
agent.switchClaudeCodeModel(modelId);
|
|
556
|
+
const info = agent.getClaudeCodeInfo();
|
|
557
|
+
logger.info(`[Bot] Claude Code model switched to ${info.modelLabel} from chat ${chatId}`);
|
|
558
|
+
await bot.editMessageText(
|
|
559
|
+
`💻 Claude Code model switched to *${info.modelLabel}*`,
|
|
560
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
561
|
+
);
|
|
562
|
+
await bot.answerCallbackQuery(query.id);
|
|
563
|
+
|
|
564
|
+
} else if (data === 'ccmodel_cancel') {
|
|
565
|
+
await bot.editMessageText('Claude Code model change cancelled.', {
|
|
566
|
+
chat_id: chatId, message_id: query.message.message_id,
|
|
567
|
+
});
|
|
568
|
+
await bot.answerCallbackQuery(query.id);
|
|
569
|
+
|
|
570
|
+
// ── Claude Code auth callbacks ─────────────────────────────────
|
|
571
|
+
} else if (data === 'claude_apikey') {
|
|
572
|
+
pendingClaudeAuth.set(chatId, { type: 'api_key' });
|
|
573
|
+
await bot.editMessageText(
|
|
574
|
+
'🔑 Send your *Anthropic API key* for Claude Code.\n\nOr send *cancel* to abort.',
|
|
575
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
576
|
+
);
|
|
577
|
+
await bot.answerCallbackQuery(query.id);
|
|
578
|
+
|
|
579
|
+
} else if (data === 'claude_oauth') {
|
|
580
|
+
pendingClaudeAuth.set(chatId, { type: 'oauth_token' });
|
|
581
|
+
await bot.editMessageText(
|
|
582
|
+
'🔑 Run `claude setup-token` locally and paste the *OAuth token* here.\n\nThis uses your Pro/Max subscription instead of an API key.\n\nOr send *cancel* to abort.',
|
|
583
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
584
|
+
);
|
|
585
|
+
await bot.answerCallbackQuery(query.id);
|
|
586
|
+
|
|
587
|
+
} else if (data === 'claude_system') {
|
|
588
|
+
agent.setClaudeCodeAuth('system', null);
|
|
589
|
+
logger.info(`[Bot] Claude Code auth set to system from chat ${chatId}`);
|
|
590
|
+
await bot.editMessageText(
|
|
591
|
+
'🔓 Claude Code set to *system auth* — using host machine credentials.',
|
|
592
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
593
|
+
);
|
|
594
|
+
await bot.answerCallbackQuery(query.id);
|
|
595
|
+
|
|
596
|
+
} else if (data === 'claude_status') {
|
|
597
|
+
await bot.answerCallbackQuery(query.id, { text: 'Checking...' });
|
|
598
|
+
const status = await getClaudeAuthStatus();
|
|
599
|
+
const authConfig = agent.getClaudeAuthConfig();
|
|
600
|
+
await bot.editMessageText(
|
|
601
|
+
`🔐 *Claude Code Auth*\n\n*Mode:* ${authConfig.mode}\n*Credential:* ${authConfig.credential}\n\n*CLI Status:*\n\`\`\`\n${status.output.slice(0, 500)}\n\`\`\``,
|
|
602
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
} else if (data === 'claude_logout') {
|
|
606
|
+
await bot.answerCallbackQuery(query.id, { text: 'Logging out...' });
|
|
607
|
+
const result = await claudeLogout();
|
|
608
|
+
await bot.editMessageText(
|
|
609
|
+
`🚪 Claude Code logout: ${result.output || 'Done.'}`,
|
|
610
|
+
{ chat_id: chatId, message_id: query.message.message_id, parse_mode: 'Markdown' },
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
} else if (data === 'claude_cancel') {
|
|
614
|
+
pendingClaudeAuth.delete(chatId);
|
|
615
|
+
await bot.editMessageText('Claude Code auth management dismissed.', {
|
|
616
|
+
chat_id: chatId, message_id: query.message.message_id,
|
|
617
|
+
});
|
|
618
|
+
await bot.answerCallbackQuery(query.id);
|
|
137
619
|
}
|
|
138
620
|
} catch (err) {
|
|
139
|
-
logger.error(`Callback query error: ${err.message}`);
|
|
621
|
+
logger.error(`[Bot] Callback query error for "${data}" in chat ${chatId}: ${err.message}`);
|
|
140
622
|
await bot.answerCallbackQuery(query.id, { text: 'Error' });
|
|
141
623
|
}
|
|
142
624
|
});
|
|
@@ -167,6 +649,10 @@ export function startBot(config, agent, conversationManager) {
|
|
|
167
649
|
? batch.messages[0]
|
|
168
650
|
: batch.messages.map((m, i) => `[${i + 1}]: ${m}`).join('\n\n');
|
|
169
651
|
|
|
652
|
+
if (batch.messages.length > 1) {
|
|
653
|
+
logger.info(`[Bot] Batch merged ${batch.messages.length} messages for chat ${key}`);
|
|
654
|
+
}
|
|
655
|
+
|
|
170
656
|
// First resolver gets the merged text, rest get null (skip)
|
|
171
657
|
batch.resolvers[0](merged);
|
|
172
658
|
for (let i = 1; i < batch.resolvers.length; i++) {
|
|
@@ -177,19 +663,82 @@ export function startBot(config, agent, conversationManager) {
|
|
|
177
663
|
}
|
|
178
664
|
|
|
179
665
|
bot.on('message', async (msg) => {
|
|
180
|
-
if (!msg.text) return; // ignore non-text
|
|
181
|
-
|
|
182
666
|
const chatId = msg.chat.id;
|
|
183
667
|
const userId = msg.from.id;
|
|
184
668
|
const username = msg.from.username || msg.from.first_name || 'unknown';
|
|
185
669
|
|
|
186
670
|
// Auth check
|
|
187
671
|
if (!isAllowedUser(userId, config)) {
|
|
188
|
-
|
|
189
|
-
|
|
672
|
+
if (msg.text || msg.document) {
|
|
673
|
+
logger.warn(`Unauthorized access attempt from ${username} (${userId})`);
|
|
674
|
+
await bot.sendMessage(chatId, getUnauthorizedMessage());
|
|
675
|
+
}
|
|
190
676
|
return;
|
|
191
677
|
}
|
|
192
678
|
|
|
679
|
+
// Handle file upload for pending custom skill prompt step
|
|
680
|
+
if (msg.document && pendingCustomSkill.has(chatId)) {
|
|
681
|
+
const pending = pendingCustomSkill.get(chatId);
|
|
682
|
+
if (pending.step === 'prompt') {
|
|
683
|
+
const doc = msg.document;
|
|
684
|
+
const mime = doc.mime_type || '';
|
|
685
|
+
const fname = doc.file_name || '';
|
|
686
|
+
if (!fname.endsWith('.md') && mime !== 'text/markdown' && mime !== 'text/plain') {
|
|
687
|
+
await bot.sendMessage(chatId, 'Please upload a `.md` or plain text file, or type the prompt directly.');
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const filePath = await bot.downloadFile(doc.file_id, '/tmp');
|
|
692
|
+
const content = readFileSync(filePath, 'utf-8').trim();
|
|
693
|
+
if (!content) {
|
|
694
|
+
await bot.sendMessage(chatId, 'The file appears to be empty. Please try again.');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
pendingCustomSkill.delete(chatId);
|
|
698
|
+
const skill = addCustomSkill({ name: pending.name, systemPrompt: content });
|
|
699
|
+
logger.info(`[Bot] Custom skill created from file: "${skill.name}" (${skill.id}) — ${content.length} chars, by ${username} in chat ${chatId}`);
|
|
700
|
+
agent.setSkill(chatId, skill.id);
|
|
701
|
+
await bot.sendMessage(
|
|
702
|
+
chatId,
|
|
703
|
+
`✅ Custom skill *${skill.name}* created and activated!\n\n_Prompt loaded from file (${content.length} chars)_`,
|
|
704
|
+
{ parse_mode: 'Markdown' },
|
|
705
|
+
);
|
|
706
|
+
} catch (err) {
|
|
707
|
+
logger.error(`Custom skill file upload error: ${err.message}`);
|
|
708
|
+
await bot.sendMessage(chatId, `Failed to read file: ${err.message}`);
|
|
709
|
+
}
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Handle voice messages — transcribe and process as text
|
|
715
|
+
if (msg.voice && sttService.isAvailable()) {
|
|
716
|
+
logger.info(`[Bot] Voice message from ${username} (${userId}) in chat ${chatId}, duration: ${msg.voice.duration}s`);
|
|
717
|
+
let tmpPath = null;
|
|
718
|
+
try {
|
|
719
|
+
const fileUrl = await bot.getFileLink(msg.voice.file_id);
|
|
720
|
+
tmpPath = await sttService.downloadAudio(fileUrl);
|
|
721
|
+
const transcribed = await sttService.transcribe(tmpPath);
|
|
722
|
+
if (!transcribed) {
|
|
723
|
+
await bot.sendMessage(chatId, 'Could not transcribe the voice message. Please try again or send text.');
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
logger.info(`[Bot] Transcribed voice: "${transcribed.slice(0, 100)}" from ${username} in chat ${chatId}`);
|
|
727
|
+
// Show the user what was heard
|
|
728
|
+
await bot.sendMessage(chatId, `🎤 _"${transcribed}"_`, { parse_mode: 'Markdown' });
|
|
729
|
+
// Process as a normal text message (fall through below)
|
|
730
|
+
msg.text = transcribed;
|
|
731
|
+
} catch (err) {
|
|
732
|
+
logger.error(`[Bot] Voice transcription failed: ${err.message}`);
|
|
733
|
+
await bot.sendMessage(chatId, 'Failed to process voice message. Please try sending text instead.');
|
|
734
|
+
return;
|
|
735
|
+
} finally {
|
|
736
|
+
if (tmpPath) sttService.cleanup(tmpPath);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (!msg.text) return; // ignore non-text (and non-document) messages
|
|
741
|
+
|
|
193
742
|
let text = msg.text.trim();
|
|
194
743
|
|
|
195
744
|
// Handle pending brain API key input
|
|
@@ -198,22 +747,125 @@ export function startBot(config, agent, conversationManager) {
|
|
|
198
747
|
pendingBrainKey.delete(chatId);
|
|
199
748
|
|
|
200
749
|
if (text.toLowerCase() === 'cancel') {
|
|
750
|
+
logger.info(`[Bot] Brain key input cancelled by ${username} in chat ${chatId}`);
|
|
201
751
|
await bot.sendMessage(chatId, 'Brain change cancelled.');
|
|
202
752
|
return;
|
|
203
753
|
}
|
|
204
754
|
|
|
205
|
-
|
|
206
|
-
|
|
755
|
+
logger.info(`[Bot] Brain key received for ${pending.providerKey}/${pending.modelId} from ${username} in chat ${chatId}`);
|
|
756
|
+
await bot.sendMessage(chatId, '⏳ Verifying API key...');
|
|
757
|
+
const switchResult = await agent.switchBrainWithKey(pending.providerKey, pending.modelId, text);
|
|
758
|
+
if (switchResult && switchResult.error) {
|
|
759
|
+
const current = agent.getBrainInfo();
|
|
760
|
+
await bot.sendMessage(
|
|
761
|
+
chatId,
|
|
762
|
+
`❌ Failed to switch: ${switchResult.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
|
|
763
|
+
{ parse_mode: 'Markdown' },
|
|
764
|
+
);
|
|
765
|
+
} else {
|
|
766
|
+
const info = agent.getBrainInfo();
|
|
767
|
+
await bot.sendMessage(
|
|
768
|
+
chatId,
|
|
769
|
+
`🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*\n\nAPI key saved.`,
|
|
770
|
+
{ parse_mode: 'Markdown' },
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Handle pending orchestrator API key input
|
|
777
|
+
if (pendingOrchKey.has(chatId)) {
|
|
778
|
+
const pending = pendingOrchKey.get(chatId);
|
|
779
|
+
pendingOrchKey.delete(chatId);
|
|
780
|
+
|
|
781
|
+
if (text.toLowerCase() === 'cancel') {
|
|
782
|
+
logger.info(`[Bot] Orchestrator key input cancelled by ${username} in chat ${chatId}`);
|
|
783
|
+
await bot.sendMessage(chatId, 'Orchestrator change cancelled.');
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
logger.info(`[Bot] Orchestrator key received for ${pending.providerKey}/${pending.modelId} from ${username} in chat ${chatId}`);
|
|
788
|
+
await bot.sendMessage(chatId, '⏳ Verifying API key...');
|
|
789
|
+
const switchResult = await agent.switchOrchestratorWithKey(pending.providerKey, pending.modelId, text);
|
|
790
|
+
if (switchResult && switchResult.error) {
|
|
791
|
+
const current = agent.getOrchestratorInfo();
|
|
792
|
+
await bot.sendMessage(
|
|
793
|
+
chatId,
|
|
794
|
+
`❌ Failed to switch: ${switchResult.error}\n\nKeeping *${current.providerName}* / *${current.modelLabel}*`,
|
|
795
|
+
{ parse_mode: 'Markdown' },
|
|
796
|
+
);
|
|
797
|
+
} else {
|
|
798
|
+
const info = agent.getOrchestratorInfo();
|
|
799
|
+
await bot.sendMessage(
|
|
800
|
+
chatId,
|
|
801
|
+
`🎛️ Orchestrator switched to *${info.providerName}* / *${info.modelLabel}*\n\nAPI key saved.`,
|
|
802
|
+
{ parse_mode: 'Markdown' },
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Handle pending Claude Code auth input
|
|
809
|
+
if (pendingClaudeAuth.has(chatId)) {
|
|
810
|
+
const pending = pendingClaudeAuth.get(chatId);
|
|
811
|
+
pendingClaudeAuth.delete(chatId);
|
|
812
|
+
|
|
813
|
+
if (text.toLowerCase() === 'cancel') {
|
|
814
|
+
logger.info(`[Bot] Claude Code auth input cancelled by ${username} in chat ${chatId}`);
|
|
815
|
+
await bot.sendMessage(chatId, 'Claude Code auth setup cancelled.');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
agent.setClaudeCodeAuth(pending.type, text);
|
|
820
|
+
const label = pending.type === 'api_key' ? 'API Key' : 'OAuth Token';
|
|
821
|
+
logger.info(`[Bot] Claude Code ${label} saved from ${username} in chat ${chatId}`);
|
|
207
822
|
await bot.sendMessage(
|
|
208
823
|
chatId,
|
|
209
|
-
|
|
824
|
+
`🔐 Claude Code *${label}* saved and activated.\n\nNext Claude Code spawn will use this credential.`,
|
|
210
825
|
{ parse_mode: 'Markdown' },
|
|
211
826
|
);
|
|
212
827
|
return;
|
|
213
828
|
}
|
|
214
829
|
|
|
830
|
+
// Handle pending custom skill creation (text input for name or prompt)
|
|
831
|
+
if (pendingCustomSkill.has(chatId)) {
|
|
832
|
+
const pending = pendingCustomSkill.get(chatId);
|
|
833
|
+
|
|
834
|
+
if (text.toLowerCase() === 'cancel') {
|
|
835
|
+
pendingCustomSkill.delete(chatId);
|
|
836
|
+
await bot.sendMessage(chatId, 'Custom skill creation cancelled.');
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (pending.step === 'name') {
|
|
841
|
+
pending.name = text;
|
|
842
|
+
pending.step = 'prompt';
|
|
843
|
+
pendingCustomSkill.set(chatId, pending);
|
|
844
|
+
await bot.sendMessage(
|
|
845
|
+
chatId,
|
|
846
|
+
`Got it: *${text}*\n\nNow send the system prompt — type it out or upload a \`.md\` file:`,
|
|
847
|
+
{ parse_mode: 'Markdown' },
|
|
848
|
+
);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (pending.step === 'prompt') {
|
|
853
|
+
pendingCustomSkill.delete(chatId);
|
|
854
|
+
const skill = addCustomSkill({ name: pending.name, systemPrompt: text });
|
|
855
|
+
logger.info(`[Bot] Custom skill created: "${skill.name}" (${skill.id}) by ${username} in chat ${chatId}`);
|
|
856
|
+
agent.setSkill(chatId, skill.id);
|
|
857
|
+
await bot.sendMessage(
|
|
858
|
+
chatId,
|
|
859
|
+
`✅ Custom skill *${skill.name}* created and activated!`,
|
|
860
|
+
{ parse_mode: 'Markdown' },
|
|
861
|
+
);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
215
866
|
// Handle commands — these bypass batching entirely
|
|
216
867
|
if (text === '/brain') {
|
|
868
|
+
logger.info(`[Bot] /brain command from ${username} (${userId}) in chat ${chatId}`);
|
|
217
869
|
const info = agent.getBrainInfo();
|
|
218
870
|
const providerKeys = Object.keys(PROVIDERS);
|
|
219
871
|
const buttons = providerKeys.map((key) => ([{
|
|
@@ -233,6 +885,116 @@ export function startBot(config, agent, conversationManager) {
|
|
|
233
885
|
return;
|
|
234
886
|
}
|
|
235
887
|
|
|
888
|
+
if (text === '/orchestrator') {
|
|
889
|
+
logger.info(`[Bot] /orchestrator command from ${username} (${userId}) in chat ${chatId}`);
|
|
890
|
+
const info = agent.getOrchestratorInfo();
|
|
891
|
+
const providerKeys = Object.keys(PROVIDERS);
|
|
892
|
+
const buttons = providerKeys.map((key) => ([{
|
|
893
|
+
text: `${PROVIDERS[key].name}${key === info.provider ? ' ✓' : ''}`,
|
|
894
|
+
callback_data: `orch_provider:${key}`,
|
|
895
|
+
}]));
|
|
896
|
+
buttons.push([{ text: 'Cancel', callback_data: 'orch_cancel' }]);
|
|
897
|
+
|
|
898
|
+
await bot.sendMessage(
|
|
899
|
+
chatId,
|
|
900
|
+
`🎛️ *Current orchestrator:* ${info.providerName} / ${info.modelLabel}\n\nSelect a provider to switch:`,
|
|
901
|
+
{
|
|
902
|
+
parse_mode: 'Markdown',
|
|
903
|
+
reply_markup: { inline_keyboard: buttons },
|
|
904
|
+
},
|
|
905
|
+
);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (text === '/claudemodel') {
|
|
910
|
+
logger.info(`[Bot] /claudemodel command from ${username} (${userId}) in chat ${chatId}`);
|
|
911
|
+
const info = agent.getClaudeCodeInfo();
|
|
912
|
+
const anthropicModels = PROVIDERS.anthropic.models;
|
|
913
|
+
const buttons = anthropicModels.map((m) => ([{
|
|
914
|
+
text: `${m.label}${m.id === info.model ? ' ✓' : ''}`,
|
|
915
|
+
callback_data: `ccmodel:${m.id}`,
|
|
916
|
+
}]));
|
|
917
|
+
buttons.push([{ text: 'Cancel', callback_data: 'ccmodel_cancel' }]);
|
|
918
|
+
|
|
919
|
+
await bot.sendMessage(
|
|
920
|
+
chatId,
|
|
921
|
+
`💻 *Current Claude Code model:* ${info.modelLabel}\n\nSelect a model:`,
|
|
922
|
+
{
|
|
923
|
+
parse_mode: 'Markdown',
|
|
924
|
+
reply_markup: { inline_keyboard: buttons },
|
|
925
|
+
},
|
|
926
|
+
);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (text === '/claude') {
|
|
931
|
+
logger.info(`[Bot] /claude command from ${username} (${userId}) in chat ${chatId}`);
|
|
932
|
+
const authConfig = agent.getClaudeAuthConfig();
|
|
933
|
+
const ccInfo = agent.getClaudeCodeInfo();
|
|
934
|
+
|
|
935
|
+
const modeLabels = { system: '🔓 System Login', api_key: '🔑 API Key', oauth_token: '🎫 OAuth Token (Pro/Max)' };
|
|
936
|
+
const modeLabel = modeLabels[authConfig.mode] || authConfig.mode;
|
|
937
|
+
|
|
938
|
+
const buttons = [
|
|
939
|
+
[{ text: '🔑 Set API Key', callback_data: 'claude_apikey' }],
|
|
940
|
+
[{ text: '🎫 Set OAuth Token (Pro/Max)', callback_data: 'claude_oauth' }],
|
|
941
|
+
[{ text: '🔓 Use System Auth', callback_data: 'claude_system' }],
|
|
942
|
+
[
|
|
943
|
+
{ text: '🔄 Refresh Status', callback_data: 'claude_status' },
|
|
944
|
+
{ text: '🚪 Logout', callback_data: 'claude_logout' },
|
|
945
|
+
],
|
|
946
|
+
[{ text: 'Cancel', callback_data: 'claude_cancel' }],
|
|
947
|
+
];
|
|
948
|
+
|
|
949
|
+
await bot.sendMessage(
|
|
950
|
+
chatId,
|
|
951
|
+
`🔐 *Claude Code Auth*\n\n*Auth Mode:* ${modeLabel}\n*Credential:* ${authConfig.credential}\n*Model:* ${ccInfo.modelLabel}\n\nSelect an action:`,
|
|
952
|
+
{
|
|
953
|
+
parse_mode: 'Markdown',
|
|
954
|
+
reply_markup: { inline_keyboard: buttons },
|
|
955
|
+
},
|
|
956
|
+
);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (text === '/skills reset' || text === '/skill reset') {
|
|
961
|
+
logger.info(`[Bot] /skills reset from ${username} (${userId}) in chat ${chatId}`);
|
|
962
|
+
agent.clearSkill(chatId);
|
|
963
|
+
await bot.sendMessage(chatId, '🔄 Skill cleared — back to default persona.');
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (text === '/skills' || text === '/skill') {
|
|
968
|
+
logger.info(`[Bot] /skills command from ${username} (${userId}) in chat ${chatId}`);
|
|
969
|
+
const categories = getUnifiedCategoryList();
|
|
970
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
971
|
+
const buttons = categories.map((cat) => ([{
|
|
972
|
+
text: `${cat.emoji} ${cat.name} (${cat.count})`,
|
|
973
|
+
callback_data: `skill_category:${cat.key}`,
|
|
974
|
+
}]));
|
|
975
|
+
// Custom skill management row
|
|
976
|
+
const customRow = [{ text: '➕ Add Custom', callback_data: 'skill_custom_add' }];
|
|
977
|
+
if (getCustomSkills().length > 0) {
|
|
978
|
+
customRow.push({ text: '🗑️ Manage Custom', callback_data: 'skill_custom_manage' });
|
|
979
|
+
}
|
|
980
|
+
buttons.push(customRow);
|
|
981
|
+
const footerRow = [{ text: 'Cancel', callback_data: 'skill_cancel' }];
|
|
982
|
+
if (activeSkill) {
|
|
983
|
+
footerRow.unshift({ text: '🔄 Reset to Default', callback_data: 'skill_reset' });
|
|
984
|
+
}
|
|
985
|
+
buttons.push(footerRow);
|
|
986
|
+
|
|
987
|
+
const header = activeSkill
|
|
988
|
+
? `🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n\nSelect a category:`
|
|
989
|
+
: '🎭 *Skills* — select a category:';
|
|
990
|
+
|
|
991
|
+
await bot.sendMessage(chatId, header, {
|
|
992
|
+
parse_mode: 'Markdown',
|
|
993
|
+
reply_markup: { inline_keyboard: buttons },
|
|
994
|
+
});
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
236
998
|
if (text === '/clean' || text === '/clear' || text === '/reset') {
|
|
237
999
|
conversationManager.clear(chatId);
|
|
238
1000
|
logger.info(`Conversation cleared for chat ${chatId} by ${username}`);
|
|
@@ -246,11 +1008,116 @@ export function startBot(config, agent, conversationManager) {
|
|
|
246
1008
|
return;
|
|
247
1009
|
}
|
|
248
1010
|
|
|
1011
|
+
if (text === '/context') {
|
|
1012
|
+
const info = agent.getBrainInfo();
|
|
1013
|
+
const orchInfo = agent.getOrchestratorInfo();
|
|
1014
|
+
const ccInfo = agent.getClaudeCodeInfo();
|
|
1015
|
+
const authConfig = agent.getClaudeAuthConfig();
|
|
1016
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
1017
|
+
const msgCount = conversationManager.getMessageCount(chatId);
|
|
1018
|
+
const history = conversationManager.getHistory(chatId);
|
|
1019
|
+
const maxHistory = conversationManager.maxHistory;
|
|
1020
|
+
const recentWindow = conversationManager.recentWindow;
|
|
1021
|
+
|
|
1022
|
+
// Build recent topics from last few user messages
|
|
1023
|
+
const recentUserMsgs = history
|
|
1024
|
+
.filter((m) => m.role === 'user')
|
|
1025
|
+
.slice(-5)
|
|
1026
|
+
.map((m) => {
|
|
1027
|
+
const txt = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
|
|
1028
|
+
return txt.length > 80 ? txt.slice(0, 80) + '…' : txt;
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
const lines = [
|
|
1032
|
+
'📋 *Conversation Context*',
|
|
1033
|
+
'',
|
|
1034
|
+
`🎛️ *Orchestrator:* ${orchInfo.providerName} / ${orchInfo.modelLabel}`,
|
|
1035
|
+
`🧠 *Brain (Workers):* ${info.providerName} / ${info.modelLabel}`,
|
|
1036
|
+
`💻 *Claude Code:* ${ccInfo.modelLabel} (auth: ${authConfig.mode})`,
|
|
1037
|
+
activeSkill
|
|
1038
|
+
? `🎭 *Skill:* ${activeSkill.emoji} ${activeSkill.name}`
|
|
1039
|
+
: '🎭 *Skill:* Default persona',
|
|
1040
|
+
`💬 *Messages in memory:* ${msgCount} / ${maxHistory}`,
|
|
1041
|
+
`📌 *Recent window:* ${recentWindow} messages`,
|
|
1042
|
+
];
|
|
1043
|
+
|
|
1044
|
+
if (recentUserMsgs.length > 0) {
|
|
1045
|
+
lines.push('', '🕐 *Recent topics:*');
|
|
1046
|
+
recentUserMsgs.forEach((msg) => lines.push(` • ${msg}`));
|
|
1047
|
+
} else {
|
|
1048
|
+
lines.push('', '_No messages yet — start chatting!_');
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (text === '/jobs') {
|
|
1056
|
+
logger.info(`[Bot] /jobs command from ${username} (${userId}) in chat ${chatId}`);
|
|
1057
|
+
const jobs = jobManager.getJobsForChat(chatId);
|
|
1058
|
+
if (jobs.length === 0) {
|
|
1059
|
+
await bot.sendMessage(chatId, 'No jobs for this chat.');
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const lines = ['*Jobs*', ''];
|
|
1063
|
+
for (const job of jobs.slice(0, 15)) {
|
|
1064
|
+
lines.push(job.toSummary());
|
|
1065
|
+
}
|
|
1066
|
+
if (jobs.length > 15) {
|
|
1067
|
+
lines.push(`\n_... and ${jobs.length - 15} more_`);
|
|
1068
|
+
}
|
|
1069
|
+
await bot.sendMessage(chatId, lines.join('\n'), { parse_mode: 'Markdown' });
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (text === '/cancel') {
|
|
1074
|
+
logger.info(`[Bot] /cancel command from ${username} (${userId}) in chat ${chatId}`);
|
|
1075
|
+
const running = jobManager.getRunningJobsForChat(chatId);
|
|
1076
|
+
if (running.length === 0) {
|
|
1077
|
+
logger.debug(`[Bot] /cancel — no running jobs for chat ${chatId}`);
|
|
1078
|
+
await bot.sendMessage(chatId, 'No running jobs to cancel.');
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (running.length === 1) {
|
|
1082
|
+
logger.info(`[Bot] /cancel — single job ${running[0].id}, cancelling directly`);
|
|
1083
|
+
const job = jobManager.cancelJob(running[0].id);
|
|
1084
|
+
if (job) {
|
|
1085
|
+
await bot.sendMessage(chatId, `🚫 Cancelled \`${job.id}\` (${job.workerType})`, { parse_mode: 'Markdown' });
|
|
1086
|
+
}
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
// Multiple running — show inline keyboard
|
|
1090
|
+
logger.info(`[Bot] /cancel — ${running.length} running jobs, showing picker`);
|
|
1091
|
+
const buttons = running.map((j) => ([{
|
|
1092
|
+
text: `🚫 ${j.workerType} (${j.id})`,
|
|
1093
|
+
callback_data: `cancel_job:${j.id}`,
|
|
1094
|
+
}]));
|
|
1095
|
+
buttons.push([{ text: '🚫 Cancel All', callback_data: 'cancel_all_jobs' }]);
|
|
1096
|
+
await bot.sendMessage(chatId, `*${running.length} running jobs* — select one to cancel:`, {
|
|
1097
|
+
parse_mode: 'Markdown',
|
|
1098
|
+
reply_markup: { inline_keyboard: buttons },
|
|
1099
|
+
});
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
249
1103
|
if (text === '/help') {
|
|
1104
|
+
const activeSkill = agent.getActiveSkill(chatId);
|
|
1105
|
+
const skillLine = activeSkill
|
|
1106
|
+
? `\n🎭 *Active skill:* ${activeSkill.emoji} ${activeSkill.name}\n`
|
|
1107
|
+
: '';
|
|
250
1108
|
await bot.sendMessage(chatId, [
|
|
251
1109
|
'*KernelBot Commands*',
|
|
252
|
-
|
|
253
|
-
'/brain —
|
|
1110
|
+
skillLine,
|
|
1111
|
+
'/brain — Switch worker AI model/provider',
|
|
1112
|
+
'/orchestrator — Switch orchestrator AI model/provider',
|
|
1113
|
+
'/claudemodel — Switch Claude Code model',
|
|
1114
|
+
'/claude — Manage Claude Code authentication',
|
|
1115
|
+
'/skills — Browse and activate persona skills',
|
|
1116
|
+
'/skills reset — Clear active skill back to default',
|
|
1117
|
+
'/jobs — List running and recent jobs',
|
|
1118
|
+
'/cancel — Cancel running job(s)',
|
|
1119
|
+
'/auto — Manage recurring automations',
|
|
1120
|
+
'/context — Show all models, auth, and context info',
|
|
254
1121
|
'/clean — Clear conversation and start fresh',
|
|
255
1122
|
'/history — Show message count in memory',
|
|
256
1123
|
'/browse <url> — Browse a website and get a summary',
|
|
@@ -263,6 +1130,107 @@ export function startBot(config, agent, conversationManager) {
|
|
|
263
1130
|
return;
|
|
264
1131
|
}
|
|
265
1132
|
|
|
1133
|
+
// ── /auto command ──────────────────────────────────────────────
|
|
1134
|
+
if (text === '/auto' || text.startsWith('/auto ')) {
|
|
1135
|
+
logger.info(`[Bot] /auto command from ${username} (${userId}) in chat ${chatId}`);
|
|
1136
|
+
const args = text.slice('/auto'.length).trim();
|
|
1137
|
+
|
|
1138
|
+
if (!automationManager) {
|
|
1139
|
+
await bot.sendMessage(chatId, 'Automation system not available.');
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// /auto (no args) — list automations
|
|
1144
|
+
if (!args) {
|
|
1145
|
+
const autos = automationManager.listForChat(chatId);
|
|
1146
|
+
if (autos.length === 0) {
|
|
1147
|
+
await bot.sendMessage(chatId, [
|
|
1148
|
+
'⏰ *No automations set up yet.*',
|
|
1149
|
+
'',
|
|
1150
|
+
'Tell me what to automate in natural language, e.g.:',
|
|
1151
|
+
' "check my server health every hour"',
|
|
1152
|
+
' "send me a news summary every morning at 9am"',
|
|
1153
|
+
'',
|
|
1154
|
+
'Or use `/auto` subcommands:',
|
|
1155
|
+
' `/auto pause <id>` — pause an automation',
|
|
1156
|
+
' `/auto resume <id>` — resume an automation',
|
|
1157
|
+
' `/auto delete <id>` — delete an automation',
|
|
1158
|
+
' `/auto run <id>` — trigger immediately',
|
|
1159
|
+
].join('\n'), { parse_mode: 'Markdown' });
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const lines = ['⏰ *Automations*', ''];
|
|
1164
|
+
for (const auto of autos) {
|
|
1165
|
+
lines.push(auto.toSummary());
|
|
1166
|
+
}
|
|
1167
|
+
lines.push('', '_Use `/auto pause|resume|delete|run <id>` to manage._');
|
|
1168
|
+
|
|
1169
|
+
// Build inline keyboard for quick actions
|
|
1170
|
+
const buttons = autos.map((a) => {
|
|
1171
|
+
const row = [];
|
|
1172
|
+
if (a.enabled) {
|
|
1173
|
+
row.push({ text: `⏸️ Pause ${a.id}`, callback_data: `auto_pause:${a.id}` });
|
|
1174
|
+
} else {
|
|
1175
|
+
row.push({ text: `▶️ Resume ${a.id}`, callback_data: `auto_resume:${a.id}` });
|
|
1176
|
+
}
|
|
1177
|
+
row.push({ text: `🗑️ Delete ${a.id}`, callback_data: `auto_delete:${a.id}` });
|
|
1178
|
+
return row;
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
await bot.sendMessage(chatId, lines.join('\n'), {
|
|
1182
|
+
parse_mode: 'Markdown',
|
|
1183
|
+
reply_markup: { inline_keyboard: buttons },
|
|
1184
|
+
});
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// /auto pause <id>
|
|
1189
|
+
if (args.startsWith('pause ')) {
|
|
1190
|
+
const autoId = args.slice('pause '.length).trim();
|
|
1191
|
+
const auto = automationManager.update(autoId, { enabled: false });
|
|
1192
|
+
await bot.sendMessage(chatId, auto
|
|
1193
|
+
? `⏸️ Paused automation \`${autoId}\` (${auto.name})`
|
|
1194
|
+
: `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// /auto resume <id>
|
|
1199
|
+
if (args.startsWith('resume ')) {
|
|
1200
|
+
const autoId = args.slice('resume '.length).trim();
|
|
1201
|
+
const auto = automationManager.update(autoId, { enabled: true });
|
|
1202
|
+
await bot.sendMessage(chatId, auto
|
|
1203
|
+
? `▶️ Resumed automation \`${autoId}\` (${auto.name})`
|
|
1204
|
+
: `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// /auto delete <id>
|
|
1209
|
+
if (args.startsWith('delete ')) {
|
|
1210
|
+
const autoId = args.slice('delete '.length).trim();
|
|
1211
|
+
const deleted = automationManager.delete(autoId);
|
|
1212
|
+
await bot.sendMessage(chatId, deleted
|
|
1213
|
+
? `🗑️ Deleted automation \`${autoId}\``
|
|
1214
|
+
: `Automation \`${autoId}\` not found.`, { parse_mode: 'Markdown' });
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// /auto run <id> — trigger immediately
|
|
1219
|
+
if (args.startsWith('run ')) {
|
|
1220
|
+
const autoId = args.slice('run '.length).trim();
|
|
1221
|
+
try {
|
|
1222
|
+
await automationManager.runNow(autoId);
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
await bot.sendMessage(chatId, `Failed: ${err.message}`);
|
|
1225
|
+
}
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// /auto <anything else> — treat as natural language automation request
|
|
1230
|
+
text = `Set up an automation: ${args}`;
|
|
1231
|
+
// Fall through to normal message processing below
|
|
1232
|
+
}
|
|
1233
|
+
|
|
266
1234
|
// Web browsing shortcut commands — rewrite as natural language for the agent
|
|
267
1235
|
if (text.startsWith('/browse ')) {
|
|
268
1236
|
const browseUrl = text.slice('/browse '.length).trim();
|
|
@@ -346,22 +1314,24 @@ export function startBot(config, agent, conversationManager) {
|
|
|
346
1314
|
};
|
|
347
1315
|
|
|
348
1316
|
const sendPhoto = async (filePath, caption) => {
|
|
1317
|
+
const fileOpts = { contentType: 'image/png' };
|
|
349
1318
|
try {
|
|
350
1319
|
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
351
1320
|
caption: caption || '',
|
|
352
1321
|
parse_mode: 'Markdown',
|
|
353
|
-
});
|
|
1322
|
+
}, fileOpts);
|
|
354
1323
|
} catch {
|
|
355
1324
|
try {
|
|
356
1325
|
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
357
1326
|
caption: caption || '',
|
|
358
|
-
});
|
|
1327
|
+
}, fileOpts);
|
|
359
1328
|
} catch (err) {
|
|
360
1329
|
logger.error(`Failed to send photo: ${err.message}`);
|
|
361
1330
|
}
|
|
362
1331
|
}
|
|
363
1332
|
};
|
|
364
1333
|
|
|
1334
|
+
logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
|
|
365
1335
|
const reply = await agent.processMessage(chatId, mergedText, {
|
|
366
1336
|
id: userId,
|
|
367
1337
|
username,
|
|
@@ -369,6 +1339,7 @@ export function startBot(config, agent, conversationManager) {
|
|
|
369
1339
|
|
|
370
1340
|
clearInterval(typingInterval);
|
|
371
1341
|
|
|
1342
|
+
logger.info(`[Bot] Reply for chat ${chatId}: ${(reply || '').length} chars`);
|
|
372
1343
|
const chunks = splitMessage(reply || 'Done.');
|
|
373
1344
|
for (const chunk of chunks) {
|
|
374
1345
|
try {
|
|
@@ -378,9 +1349,21 @@ export function startBot(config, agent, conversationManager) {
|
|
|
378
1349
|
await bot.sendMessage(chatId, chunk);
|
|
379
1350
|
}
|
|
380
1351
|
}
|
|
1352
|
+
|
|
1353
|
+
// Send voice reply if TTS is available and the reply isn't too short
|
|
1354
|
+
if (ttsService.isAvailable() && reply && reply.length > 5) {
|
|
1355
|
+
try {
|
|
1356
|
+
const audioPath = await ttsService.synthesize(reply);
|
|
1357
|
+
if (audioPath) {
|
|
1358
|
+
await bot.sendVoice(chatId, createReadStream(audioPath));
|
|
1359
|
+
}
|
|
1360
|
+
} catch (err) {
|
|
1361
|
+
logger.warn(`[Bot] TTS voice reply failed: ${err.message}`);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
381
1364
|
} catch (err) {
|
|
382
1365
|
clearInterval(typingInterval);
|
|
383
|
-
logger.error(`Error processing message: ${err.message}`);
|
|
1366
|
+
logger.error(`[Bot] Error processing message in chat ${chatId}: ${err.message}`);
|
|
384
1367
|
await bot.sendMessage(chatId, `Error: ${err.message}`);
|
|
385
1368
|
}
|
|
386
1369
|
});
|