kernelbot 1.0.33 → 1.0.34
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 +281 -276
- package/bin/kernel.js +51 -12
- package/package.json +2 -1
- package/src/agent.js +148 -19
- package/src/bot.js +177 -151
- package/src/conversation.js +70 -3
- package/src/prompts/persona.md +27 -0
- package/src/providers/base.js +16 -5
- package/src/providers/google-genai.js +198 -0
- package/src/providers/index.js +6 -1
- package/src/providers/models.js +6 -2
- package/src/providers/openai-compat.js +25 -11
- package/src/tools/docker.js +6 -13
- package/src/tools/monitor.js +5 -14
- package/src/tools/network.js +10 -17
- package/src/tools/os.js +37 -2
- package/src/tools/process.js +7 -14
- package/src/utils/config.js +59 -0
- package/src/utils/shell.js +31 -0
- package/src/utils/truncate.js +42 -0
- package/src/worker.js +2 -18
package/bin/kernel.js
CHANGED
|
@@ -9,7 +9,7 @@ import { readFileSync, existsSync } from 'fs';
|
|
|
9
9
|
import { join } from 'path';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import chalk from 'chalk';
|
|
12
|
-
import { loadConfig, loadConfigInteractive, changeBrainModel } from '../src/utils/config.js';
|
|
12
|
+
import { loadConfig, loadConfigInteractive, changeBrainModel, changeOrchestratorModel } from '../src/utils/config.js';
|
|
13
13
|
import { createLogger, getLogger } from '../src/utils/logger.js';
|
|
14
14
|
import {
|
|
15
15
|
showLogo,
|
|
@@ -40,11 +40,16 @@ import {
|
|
|
40
40
|
} from '../src/skills/custom.js';
|
|
41
41
|
|
|
42
42
|
function showMenu(config) {
|
|
43
|
+
const orchProviderDef = PROVIDERS[config.orchestrator.provider];
|
|
44
|
+
const orchProviderName = orchProviderDef ? orchProviderDef.name : config.orchestrator.provider;
|
|
45
|
+
const orchModelId = config.orchestrator.model;
|
|
46
|
+
|
|
43
47
|
const providerDef = PROVIDERS[config.brain.provider];
|
|
44
48
|
const providerName = providerDef ? providerDef.name : config.brain.provider;
|
|
45
49
|
const modelId = config.brain.model;
|
|
46
50
|
|
|
47
51
|
console.log('');
|
|
52
|
+
console.log(chalk.dim(` Current orchestrator: ${orchProviderName} / ${orchModelId}`));
|
|
48
53
|
console.log(chalk.dim(` Current brain: ${providerName} / ${modelId}`));
|
|
49
54
|
console.log('');
|
|
50
55
|
console.log(chalk.bold(' What would you like to do?\n'));
|
|
@@ -53,9 +58,10 @@ function showMenu(config) {
|
|
|
53
58
|
console.log(` ${chalk.cyan('3.')} View logs`);
|
|
54
59
|
console.log(` ${chalk.cyan('4.')} View audit logs`);
|
|
55
60
|
console.log(` ${chalk.cyan('5.')} Change brain model`);
|
|
56
|
-
console.log(` ${chalk.cyan('6.')}
|
|
57
|
-
console.log(` ${chalk.cyan('7.')} Manage
|
|
58
|
-
console.log(` ${chalk.cyan('8.')}
|
|
61
|
+
console.log(` ${chalk.cyan('6.')} Change orchestrator model`);
|
|
62
|
+
console.log(` ${chalk.cyan('7.')} Manage custom skills`);
|
|
63
|
+
console.log(` ${chalk.cyan('8.')} Manage automations`);
|
|
64
|
+
console.log(` ${chalk.cyan('9.')} Exit`);
|
|
59
65
|
console.log('');
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -95,23 +101,53 @@ function viewLog(filename) {
|
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
async function runCheck(config) {
|
|
104
|
+
// Orchestrator check
|
|
105
|
+
const orchProviderKey = config.orchestrator.provider || 'anthropic';
|
|
106
|
+
const orchProviderDef = PROVIDERS[orchProviderKey];
|
|
107
|
+
const orchLabel = orchProviderDef ? orchProviderDef.name : orchProviderKey;
|
|
108
|
+
const orchEnvKey = orchProviderDef ? orchProviderDef.envKey : 'ANTHROPIC_API_KEY';
|
|
109
|
+
|
|
110
|
+
await showStartupCheck(`Orchestrator ${orchEnvKey}`, async () => {
|
|
111
|
+
const orchestratorKey = config.orchestrator.api_key
|
|
112
|
+
|| (orchProviderDef && process.env[orchProviderDef.envKey])
|
|
113
|
+
|| process.env.ANTHROPIC_API_KEY;
|
|
114
|
+
if (!orchestratorKey) throw new Error('Not set');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await showStartupCheck(`Orchestrator (${orchLabel}) API connection`, async () => {
|
|
118
|
+
const orchestratorKey = config.orchestrator.api_key
|
|
119
|
+
|| (orchProviderDef && process.env[orchProviderDef.envKey])
|
|
120
|
+
|| process.env.ANTHROPIC_API_KEY;
|
|
121
|
+
const provider = createProvider({
|
|
122
|
+
brain: {
|
|
123
|
+
provider: orchProviderKey,
|
|
124
|
+
model: config.orchestrator.model,
|
|
125
|
+
max_tokens: config.orchestrator.max_tokens,
|
|
126
|
+
temperature: config.orchestrator.temperature,
|
|
127
|
+
api_key: orchestratorKey,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
await provider.ping();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Worker brain check
|
|
98
134
|
const providerDef = PROVIDERS[config.brain.provider];
|
|
99
135
|
const providerLabel = providerDef ? providerDef.name : config.brain.provider;
|
|
100
136
|
const envKeyLabel = providerDef ? providerDef.envKey : 'API_KEY';
|
|
101
137
|
|
|
102
|
-
await showStartupCheck(envKeyLabel
|
|
138
|
+
await showStartupCheck(`Worker ${envKeyLabel}`, async () => {
|
|
103
139
|
if (!config.brain.api_key) throw new Error('Not set');
|
|
104
140
|
});
|
|
105
141
|
|
|
106
|
-
await showStartupCheck(
|
|
107
|
-
if (!config.telegram.bot_token) throw new Error('Not set');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await showStartupCheck(`${providerLabel} API connection`, async () => {
|
|
142
|
+
await showStartupCheck(`Worker (${providerLabel}) API connection`, async () => {
|
|
111
143
|
const provider = createProvider(config);
|
|
112
144
|
await provider.ping();
|
|
113
145
|
});
|
|
114
146
|
|
|
147
|
+
await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
|
|
148
|
+
if (!config.telegram.bot_token) throw new Error('Not set');
|
|
149
|
+
});
|
|
150
|
+
|
|
115
151
|
await showStartupCheck('Telegram Bot API', async () => {
|
|
116
152
|
const res = await fetch(
|
|
117
153
|
`https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
|
|
@@ -429,12 +465,15 @@ async function main() {
|
|
|
429
465
|
await changeBrainModel(config, rl);
|
|
430
466
|
break;
|
|
431
467
|
case '6':
|
|
432
|
-
await
|
|
468
|
+
await changeOrchestratorModel(config, rl);
|
|
433
469
|
break;
|
|
434
470
|
case '7':
|
|
435
|
-
await
|
|
471
|
+
await manageCustomSkills(rl);
|
|
436
472
|
break;
|
|
437
473
|
case '8':
|
|
474
|
+
await manageAutomations(rl);
|
|
475
|
+
break;
|
|
476
|
+
case '9':
|
|
438
477
|
running = false;
|
|
439
478
|
break;
|
|
440
479
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kernelbot",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.34",
|
|
4
4
|
"description": "KernelBot — AI engineering agent with full OS control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Abdullah Al-Taheri <abdullah@altaheri.me>",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
34
|
+
"@google/genai": "^1.42.0",
|
|
34
35
|
"@octokit/rest": "^22.0.1",
|
|
35
36
|
"axios": "^1.13.5",
|
|
36
37
|
"boxen": "^8.0.1",
|
package/src/agent.js
CHANGED
|
@@ -9,9 +9,7 @@ import { WorkerAgent } from './worker.js';
|
|
|
9
9
|
import { getLogger } from './utils/logger.js';
|
|
10
10
|
import { getMissingCredential, saveCredential, saveProviderToYaml, saveOrchestratorToYaml, saveClaudeCodeModelToYaml, saveClaudeCodeAuth } from './utils/config.js';
|
|
11
11
|
import { resetClaudeCodeSpawner, getSpawner } from './tools/coding.js';
|
|
12
|
-
|
|
13
|
-
const MAX_RESULT_LENGTH = 3000;
|
|
14
|
-
const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
|
|
12
|
+
import { truncateToolResult } from './utils/truncate.js';
|
|
15
13
|
|
|
16
14
|
export class OrchestratorAgent {
|
|
17
15
|
constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager, memoryManager, shareQueue }) {
|
|
@@ -282,23 +280,9 @@ export class OrchestratorAgent {
|
|
|
282
280
|
}
|
|
283
281
|
}
|
|
284
282
|
|
|
285
|
-
/** Truncate a tool result. */
|
|
283
|
+
/** Truncate a tool result. Delegates to shared utility. */
|
|
286
284
|
_truncateResult(name, result) {
|
|
287
|
-
|
|
288
|
-
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
289
|
-
|
|
290
|
-
if (result && typeof result === 'object') {
|
|
291
|
-
const truncated = { ...result };
|
|
292
|
-
for (const field of LARGE_FIELDS) {
|
|
293
|
-
if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
|
|
294
|
-
truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
str = JSON.stringify(truncated);
|
|
298
|
-
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
|
|
285
|
+
return truncateToolResult(name, result);
|
|
302
286
|
}
|
|
303
287
|
|
|
304
288
|
async processMessage(chatId, userMessage, user, onUpdate, sendPhoto, opts = {}) {
|
|
@@ -1021,6 +1005,151 @@ export class OrchestratorAgent {
|
|
|
1021
1005
|
}
|
|
1022
1006
|
}
|
|
1023
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* Resume active chats after a restart.
|
|
1010
|
+
* Checks recent conversations for pending items and sends follow-up messages.
|
|
1011
|
+
* Called once from bot.js after startup.
|
|
1012
|
+
*/
|
|
1013
|
+
async resumeActiveChats(sendMessageFn) {
|
|
1014
|
+
const logger = getLogger();
|
|
1015
|
+
const now = Date.now();
|
|
1016
|
+
const MAX_AGE_MS = 24 * 60 * 60_000; // 24 hours
|
|
1017
|
+
|
|
1018
|
+
logger.info('[Orchestrator] Checking for active chats to resume...');
|
|
1019
|
+
|
|
1020
|
+
let resumeCount = 0;
|
|
1021
|
+
|
|
1022
|
+
for (const [chatId, messages] of this.conversationManager.conversations) {
|
|
1023
|
+
// Skip internal life engine chat
|
|
1024
|
+
if (chatId === '__life__') continue;
|
|
1025
|
+
|
|
1026
|
+
try {
|
|
1027
|
+
// Find the last message with a timestamp
|
|
1028
|
+
const lastMsg = [...messages].reverse().find(m => m.timestamp);
|
|
1029
|
+
if (!lastMsg || !lastMsg.timestamp) continue;
|
|
1030
|
+
|
|
1031
|
+
const ageMs = now - lastMsg.timestamp;
|
|
1032
|
+
if (ageMs > MAX_AGE_MS) continue;
|
|
1033
|
+
|
|
1034
|
+
// Calculate time gap for context
|
|
1035
|
+
const gapMinutes = Math.floor(ageMs / 60_000);
|
|
1036
|
+
const gapText = gapMinutes >= 60
|
|
1037
|
+
? `${Math.floor(gapMinutes / 60)} hour(s)`
|
|
1038
|
+
: `${gapMinutes} minute(s)`;
|
|
1039
|
+
|
|
1040
|
+
// Build summarized history
|
|
1041
|
+
const history = this.conversationManager.getSummarizedHistory(chatId);
|
|
1042
|
+
if (history.length === 0) continue;
|
|
1043
|
+
|
|
1044
|
+
// Build resume prompt
|
|
1045
|
+
const resumePrompt = `[System Restart] You just came back online after being offline for ${gapText}. Review the conversation above.\nIf there's something pending (unfinished task, follow-up, something to share), send a short natural message. If nothing's pending, respond with exactly: NONE`;
|
|
1046
|
+
|
|
1047
|
+
// Use minimal user object (private TG chats: chatId == userId)
|
|
1048
|
+
const user = { id: chatId };
|
|
1049
|
+
|
|
1050
|
+
const response = await this.orchestratorProvider.chat({
|
|
1051
|
+
system: this._getSystemPrompt(chatId, user),
|
|
1052
|
+
messages: [
|
|
1053
|
+
...history,
|
|
1054
|
+
{ role: 'user', content: resumePrompt },
|
|
1055
|
+
],
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
const reply = (response.text || '').trim();
|
|
1059
|
+
|
|
1060
|
+
if (reply && reply !== 'NONE') {
|
|
1061
|
+
await sendMessageFn(chatId, reply);
|
|
1062
|
+
this.conversationManager.addMessage(chatId, 'assistant', reply);
|
|
1063
|
+
resumeCount++;
|
|
1064
|
+
logger.info(`[Orchestrator] Resume message sent to chat ${chatId}`);
|
|
1065
|
+
} else {
|
|
1066
|
+
logger.debug(`[Orchestrator] No resume needed for chat ${chatId}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Small delay between chats to avoid rate limiting
|
|
1070
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
logger.error(`[Orchestrator] Resume failed for chat ${chatId}: ${err.message}`);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
logger.info(`[Orchestrator] Resume check complete — ${resumeCount} message(s) sent`);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Deliver pending shares from the life engine to active chats proactively.
|
|
1081
|
+
* Called periodically from bot.js.
|
|
1082
|
+
*/
|
|
1083
|
+
async deliverPendingShares(sendMessageFn) {
|
|
1084
|
+
const logger = getLogger();
|
|
1085
|
+
|
|
1086
|
+
if (!this.shareQueue) return;
|
|
1087
|
+
|
|
1088
|
+
const pending = this.shareQueue.getPending(null, 5);
|
|
1089
|
+
if (pending.length === 0) return;
|
|
1090
|
+
|
|
1091
|
+
const now = Date.now();
|
|
1092
|
+
const MAX_AGE_MS = 24 * 60 * 60_000;
|
|
1093
|
+
|
|
1094
|
+
// Find active chats (last message within 24h)
|
|
1095
|
+
const activeChats = [];
|
|
1096
|
+
for (const [chatId, messages] of this.conversationManager.conversations) {
|
|
1097
|
+
if (chatId === '__life__') continue;
|
|
1098
|
+
const lastMsg = [...messages].reverse().find(m => m.timestamp);
|
|
1099
|
+
if (lastMsg && lastMsg.timestamp && (now - lastMsg.timestamp) < MAX_AGE_MS) {
|
|
1100
|
+
activeChats.push(chatId);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (activeChats.length === 0) {
|
|
1105
|
+
logger.debug('[Orchestrator] No active chats for share delivery');
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
logger.info(`[Orchestrator] Delivering ${pending.length} pending share(s) to ${activeChats.length} active chat(s)`);
|
|
1110
|
+
|
|
1111
|
+
// Cap at 3 chats per cycle to avoid spam
|
|
1112
|
+
const targetChats = activeChats.slice(0, 3);
|
|
1113
|
+
|
|
1114
|
+
for (const chatId of targetChats) {
|
|
1115
|
+
try {
|
|
1116
|
+
const history = this.conversationManager.getSummarizedHistory(chatId);
|
|
1117
|
+
const user = { id: chatId };
|
|
1118
|
+
|
|
1119
|
+
// Build shares into a prompt
|
|
1120
|
+
const sharesText = pending.map((s, i) => `${i + 1}. [${s.source}] ${s.content}`).join('\n');
|
|
1121
|
+
|
|
1122
|
+
const sharePrompt = `[Proactive Share] You have some discoveries and thoughts you'd like to share naturally. Here they are:\n\n${sharesText}\n\nWeave one or more of these into a short, natural message. Don't be forced — pick what feels relevant to this user and conversation. If none feel right for this chat, respond with exactly: NONE`;
|
|
1123
|
+
|
|
1124
|
+
const response = await this.orchestratorProvider.chat({
|
|
1125
|
+
system: this._getSystemPrompt(chatId, user),
|
|
1126
|
+
messages: [
|
|
1127
|
+
...history,
|
|
1128
|
+
{ role: 'user', content: sharePrompt },
|
|
1129
|
+
],
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
const reply = (response.text || '').trim();
|
|
1133
|
+
|
|
1134
|
+
if (reply && reply !== 'NONE') {
|
|
1135
|
+
await sendMessageFn(chatId, reply);
|
|
1136
|
+
this.conversationManager.addMessage(chatId, 'assistant', reply);
|
|
1137
|
+
logger.info(`[Orchestrator] Proactive share delivered to chat ${chatId}`);
|
|
1138
|
+
|
|
1139
|
+
// Mark shares as delivered for this user
|
|
1140
|
+
for (const item of pending) {
|
|
1141
|
+
this.shareQueue.markShared(item.id, chatId);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// Delay between chats
|
|
1146
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
logger.error(`[Orchestrator] Share delivery failed for chat ${chatId}: ${err.message}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1024
1153
|
/** Background persona extraction. */
|
|
1025
1154
|
async _extractPersonaBackground(userMessage, reply, user) {
|
|
1026
1155
|
const logger = getLogger();
|
package/src/bot.js
CHANGED
|
@@ -16,6 +16,33 @@ import { TTSService } from './services/tts.js';
|
|
|
16
16
|
import { STTService } from './services/stt.js';
|
|
17
17
|
import { getClaudeAuthStatus, claudeLogout } from './claude-auth.js';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Simulate a human-like typing delay based on response length.
|
|
21
|
+
* Short replies (casual chat) get a brief pause; longer replies get more.
|
|
22
|
+
* Keeps the typing indicator alive during the delay so the user sees "typing...".
|
|
23
|
+
*
|
|
24
|
+
* @param {TelegramBot} bot - Telegram bot instance
|
|
25
|
+
* @param {number} chatId - Chat to show typing in
|
|
26
|
+
* @param {string} text - The reply text (used to calculate delay)
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
async function simulateTypingDelay(bot, chatId, text) {
|
|
30
|
+
const length = (text || '').length;
|
|
31
|
+
|
|
32
|
+
// ~25ms per character, clamped between 0.4s and 4s
|
|
33
|
+
// Short "hey ❤️" (~6 chars) → 0.4s | Medium reply (~120 chars) → 3s | Long reply → 4s cap
|
|
34
|
+
const delay = Math.min(4000, Math.max(400, length * 25));
|
|
35
|
+
|
|
36
|
+
// Add a small random jitter (±15%) so it doesn't feel mechanical
|
|
37
|
+
const jitter = delay * (0.85 + Math.random() * 0.3);
|
|
38
|
+
const finalDelay = Math.round(jitter);
|
|
39
|
+
|
|
40
|
+
// Keep the typing indicator alive during the delay
|
|
41
|
+
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
42
|
+
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, finalDelay));
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
function splitMessage(text, maxLength = 4096) {
|
|
20
47
|
if (text.length <= maxLength) return [text];
|
|
21
48
|
|
|
@@ -35,6 +62,83 @@ function splitMessage(text, maxLength = 4096) {
|
|
|
35
62
|
return chunks;
|
|
36
63
|
}
|
|
37
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Create an onUpdate callback that sends or edits Telegram messages.
|
|
67
|
+
* Tries Markdown first, falls back to plain text.
|
|
68
|
+
*/
|
|
69
|
+
function createOnUpdate(bot, chatId) {
|
|
70
|
+
return async (update, opts = {}) => {
|
|
71
|
+
if (opts.editMessageId) {
|
|
72
|
+
try {
|
|
73
|
+
const edited = await bot.editMessageText(update, {
|
|
74
|
+
chat_id: chatId,
|
|
75
|
+
message_id: opts.editMessageId,
|
|
76
|
+
parse_mode: 'Markdown',
|
|
77
|
+
});
|
|
78
|
+
return edited.message_id;
|
|
79
|
+
} catch {
|
|
80
|
+
try {
|
|
81
|
+
const edited = await bot.editMessageText(update, {
|
|
82
|
+
chat_id: chatId,
|
|
83
|
+
message_id: opts.editMessageId,
|
|
84
|
+
});
|
|
85
|
+
return edited.message_id;
|
|
86
|
+
} catch {
|
|
87
|
+
// Edit failed — fall through to send new message
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const parts = splitMessage(update);
|
|
92
|
+
let lastMsgId = null;
|
|
93
|
+
for (const part of parts) {
|
|
94
|
+
try {
|
|
95
|
+
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
96
|
+
lastMsgId = sent.message_id;
|
|
97
|
+
} catch {
|
|
98
|
+
const sent = await bot.sendMessage(chatId, part);
|
|
99
|
+
lastMsgId = sent.message_id;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return lastMsgId;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Create a sendPhoto callback that sends a photo with optional caption.
|
|
108
|
+
* Tries Markdown caption first, falls back to plain caption.
|
|
109
|
+
*/
|
|
110
|
+
function createSendPhoto(bot, chatId, logger) {
|
|
111
|
+
return async (filePath, caption) => {
|
|
112
|
+
const fileOpts = { contentType: 'image/png' };
|
|
113
|
+
try {
|
|
114
|
+
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
115
|
+
caption: caption || '',
|
|
116
|
+
parse_mode: 'Markdown',
|
|
117
|
+
}, fileOpts);
|
|
118
|
+
} catch {
|
|
119
|
+
try {
|
|
120
|
+
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
121
|
+
caption: caption || '',
|
|
122
|
+
}, fileOpts);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.error(`Failed to send photo: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a sendReaction callback for reacting to messages with emoji.
|
|
132
|
+
*/
|
|
133
|
+
function createSendReaction(bot) {
|
|
134
|
+
return async (targetChatId, targetMsgId, emoji, isBig = false) => {
|
|
135
|
+
await bot.setMessageReaction(targetChatId, targetMsgId, {
|
|
136
|
+
reaction: [{ type: 'emoji', emoji }],
|
|
137
|
+
is_big: isBig,
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
38
142
|
/**
|
|
39
143
|
* Simple per-chat queue to serialize agent processing.
|
|
40
144
|
* Each chat gets its own promise chain so messages are processed in order.
|
|
@@ -119,54 +223,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
119
223
|
const sendAction = (chatId, action) => bot.sendChatAction(chatId, action).catch(() => {});
|
|
120
224
|
|
|
121
225
|
const agentFactory = (chatId) => {
|
|
122
|
-
const onUpdate =
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const edited = await bot.editMessageText(update, {
|
|
126
|
-
chat_id: chatId,
|
|
127
|
-
message_id: opts.editMessageId,
|
|
128
|
-
parse_mode: 'Markdown',
|
|
129
|
-
});
|
|
130
|
-
return edited.message_id;
|
|
131
|
-
} catch {
|
|
132
|
-
try {
|
|
133
|
-
const edited = await bot.editMessageText(update, {
|
|
134
|
-
chat_id: chatId,
|
|
135
|
-
message_id: opts.editMessageId,
|
|
136
|
-
});
|
|
137
|
-
return edited.message_id;
|
|
138
|
-
} catch {
|
|
139
|
-
// Edit failed — fall through to send new message
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
const parts = splitMessage(update);
|
|
144
|
-
let lastMsgId = null;
|
|
145
|
-
for (const part of parts) {
|
|
146
|
-
try {
|
|
147
|
-
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
148
|
-
lastMsgId = sent.message_id;
|
|
149
|
-
} catch {
|
|
150
|
-
const sent = await bot.sendMessage(chatId, part);
|
|
151
|
-
lastMsgId = sent.message_id;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return lastMsgId;
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
const sendPhoto = async (filePath, caption) => {
|
|
158
|
-
const fileOpts = { contentType: 'image/png' };
|
|
159
|
-
try {
|
|
160
|
-
await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '', parse_mode: 'Markdown' }, fileOpts);
|
|
161
|
-
} catch {
|
|
162
|
-
try {
|
|
163
|
-
await bot.sendPhoto(chatId, createReadStream(filePath), { caption: caption || '' }, fileOpts);
|
|
164
|
-
} catch (err) {
|
|
165
|
-
logger.error(`[Automation] Failed to send photo: ${err.message}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
|
|
226
|
+
const onUpdate = createOnUpdate(bot, chatId);
|
|
227
|
+
const sendPhoto = createSendPhoto(bot, chatId, logger);
|
|
170
228
|
return { agent, onUpdate, sendPhoto };
|
|
171
229
|
};
|
|
172
230
|
|
|
@@ -1580,68 +1638,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1580
1638
|
bot.sendChatAction(chatId, 'typing').catch(() => {});
|
|
1581
1639
|
|
|
1582
1640
|
try {
|
|
1583
|
-
const onUpdate =
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
try {
|
|
1587
|
-
const edited = await bot.editMessageText(update, {
|
|
1588
|
-
chat_id: chatId,
|
|
1589
|
-
message_id: opts.editMessageId,
|
|
1590
|
-
parse_mode: 'Markdown',
|
|
1591
|
-
});
|
|
1592
|
-
return edited.message_id;
|
|
1593
|
-
} catch {
|
|
1594
|
-
try {
|
|
1595
|
-
const edited = await bot.editMessageText(update, {
|
|
1596
|
-
chat_id: chatId,
|
|
1597
|
-
message_id: opts.editMessageId,
|
|
1598
|
-
});
|
|
1599
|
-
return edited.message_id;
|
|
1600
|
-
} catch {
|
|
1601
|
-
// Edit failed — fall through to send new message
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
// Send new message(s) — also reached when edit fails
|
|
1607
|
-
const parts = splitMessage(update);
|
|
1608
|
-
let lastMsgId = null;
|
|
1609
|
-
for (const part of parts) {
|
|
1610
|
-
try {
|
|
1611
|
-
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
1612
|
-
lastMsgId = sent.message_id;
|
|
1613
|
-
} catch {
|
|
1614
|
-
const sent = await bot.sendMessage(chatId, part);
|
|
1615
|
-
lastMsgId = sent.message_id;
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return lastMsgId;
|
|
1619
|
-
};
|
|
1620
|
-
|
|
1621
|
-
const sendPhoto = async (filePath, caption) => {
|
|
1622
|
-
const fileOpts = { contentType: 'image/png' };
|
|
1623
|
-
try {
|
|
1624
|
-
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
1625
|
-
caption: caption || '',
|
|
1626
|
-
parse_mode: 'Markdown',
|
|
1627
|
-
}, fileOpts);
|
|
1628
|
-
} catch {
|
|
1629
|
-
try {
|
|
1630
|
-
await bot.sendPhoto(chatId, createReadStream(filePath), {
|
|
1631
|
-
caption: caption || '',
|
|
1632
|
-
}, fileOpts);
|
|
1633
|
-
} catch (err) {
|
|
1634
|
-
logger.error(`Failed to send photo: ${err.message}`);
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
};
|
|
1638
|
-
|
|
1639
|
-
const sendReaction = async (targetChatId, targetMsgId, emoji, isBig = false) => {
|
|
1640
|
-
await bot.setMessageReaction(targetChatId, targetMsgId, {
|
|
1641
|
-
reaction: [{ type: 'emoji', emoji }],
|
|
1642
|
-
is_big: isBig,
|
|
1643
|
-
});
|
|
1644
|
-
};
|
|
1641
|
+
const onUpdate = createOnUpdate(bot, chatId);
|
|
1642
|
+
const sendPhoto = createSendPhoto(bot, chatId, logger);
|
|
1643
|
+
const sendReaction = createSendReaction(bot);
|
|
1645
1644
|
|
|
1646
1645
|
logger.debug(`[Bot] Sending to orchestrator: chat ${chatId}, text="${mergedText.slice(0, 80)}"`);
|
|
1647
1646
|
const reply = await agent.processMessage(chatId, mergedText, {
|
|
@@ -1651,6 +1650,9 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1651
1650
|
|
|
1652
1651
|
clearInterval(typingInterval);
|
|
1653
1652
|
|
|
1653
|
+
// Simulate human-like typing delay before sending the reply
|
|
1654
|
+
await simulateTypingDelay(bot, chatId, reply || '');
|
|
1655
|
+
|
|
1654
1656
|
logger.info(`[Bot] Reply for chat ${chatId}: ${(reply || '').length} chars`);
|
|
1655
1657
|
const chunks = splitMessage(reply || 'Done.');
|
|
1656
1658
|
for (const chunk of chunks) {
|
|
@@ -1702,47 +1704,8 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1702
1704
|
|
|
1703
1705
|
chatQueue.enqueue(chatId, async () => {
|
|
1704
1706
|
try {
|
|
1705
|
-
const onUpdate =
|
|
1706
|
-
|
|
1707
|
-
try {
|
|
1708
|
-
const edited = await bot.editMessageText(update, {
|
|
1709
|
-
chat_id: chatId,
|
|
1710
|
-
message_id: opts.editMessageId,
|
|
1711
|
-
parse_mode: 'Markdown',
|
|
1712
|
-
});
|
|
1713
|
-
return edited.message_id;
|
|
1714
|
-
} catch {
|
|
1715
|
-
try {
|
|
1716
|
-
const edited = await bot.editMessageText(update, {
|
|
1717
|
-
chat_id: chatId,
|
|
1718
|
-
message_id: opts.editMessageId,
|
|
1719
|
-
});
|
|
1720
|
-
return edited.message_id;
|
|
1721
|
-
} catch {
|
|
1722
|
-
// fall through
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
const parts = splitMessage(update);
|
|
1727
|
-
let lastMsgId = null;
|
|
1728
|
-
for (const part of parts) {
|
|
1729
|
-
try {
|
|
1730
|
-
const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
|
|
1731
|
-
lastMsgId = sent.message_id;
|
|
1732
|
-
} catch {
|
|
1733
|
-
const sent = await bot.sendMessage(chatId, part);
|
|
1734
|
-
lastMsgId = sent.message_id;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
return lastMsgId;
|
|
1738
|
-
};
|
|
1739
|
-
|
|
1740
|
-
const sendReaction = async (targetChatId, targetMsgId, emoji, isBig = false) => {
|
|
1741
|
-
await bot.setMessageReaction(targetChatId, targetMsgId, {
|
|
1742
|
-
reaction: [{ type: 'emoji', emoji }],
|
|
1743
|
-
is_big: isBig,
|
|
1744
|
-
});
|
|
1745
|
-
};
|
|
1707
|
+
const onUpdate = createOnUpdate(bot, chatId);
|
|
1708
|
+
const sendReaction = createSendReaction(bot);
|
|
1746
1709
|
|
|
1747
1710
|
const reply = await agent.processMessage(chatId, reactionText, {
|
|
1748
1711
|
id: userId,
|
|
@@ -1769,5 +1732,68 @@ export function startBot(config, agent, conversationManager, jobManager, automat
|
|
|
1769
1732
|
logger.error(`Telegram polling error: ${err.message}`);
|
|
1770
1733
|
});
|
|
1771
1734
|
|
|
1735
|
+
// ── Resume active chats after restart ────────────────────────
|
|
1736
|
+
setTimeout(async () => {
|
|
1737
|
+
const sendMsg = async (chatId, text) => {
|
|
1738
|
+
try {
|
|
1739
|
+
await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
1740
|
+
} catch {
|
|
1741
|
+
await bot.sendMessage(chatId, text);
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
try {
|
|
1745
|
+
await agent.resumeActiveChats(sendMsg);
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
logger.error(`[Bot] Resume active chats failed: ${err.message}`);
|
|
1748
|
+
}
|
|
1749
|
+
}, 5000);
|
|
1750
|
+
|
|
1751
|
+
// ── Proactive share delivery (randomized, self-rearming) ────
|
|
1752
|
+
const lifeConfig = config.life || {};
|
|
1753
|
+
const quietStart = lifeConfig.quiet_hours?.start ?? 2;
|
|
1754
|
+
const quietEnd = lifeConfig.quiet_hours?.end ?? 6;
|
|
1755
|
+
|
|
1756
|
+
const armShareDelivery = (delivered) => {
|
|
1757
|
+
// If we just delivered something, wait longer (1–4h) before next check
|
|
1758
|
+
// If nothing was delivered, check again sooner (10–45min) in case new shares appear
|
|
1759
|
+
const minMin = delivered ? 60 : 10;
|
|
1760
|
+
const maxMin = delivered ? 240 : 45;
|
|
1761
|
+
const delayMs = (minMin + Math.random() * (maxMin - minMin)) * 60_000;
|
|
1762
|
+
|
|
1763
|
+
logger.debug(`[Bot] Next share check in ${Math.round(delayMs / 60_000)}m`);
|
|
1764
|
+
|
|
1765
|
+
setTimeout(async () => {
|
|
1766
|
+
// Respect quiet hours
|
|
1767
|
+
const hour = new Date().getHours();
|
|
1768
|
+
if (hour >= quietStart && hour < quietEnd) {
|
|
1769
|
+
armShareDelivery(false);
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
const sendMsg = async (chatId, text) => {
|
|
1774
|
+
try {
|
|
1775
|
+
await bot.sendMessage(chatId, text, { parse_mode: 'Markdown' });
|
|
1776
|
+
} catch {
|
|
1777
|
+
await bot.sendMessage(chatId, text);
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
let didDeliver = false;
|
|
1782
|
+
try {
|
|
1783
|
+
const before = shareQueue ? shareQueue.getPending(null, 1).length : 0;
|
|
1784
|
+
await agent.deliverPendingShares(sendMsg);
|
|
1785
|
+
const after = shareQueue ? shareQueue.getPending(null, 1).length : 0;
|
|
1786
|
+
didDeliver = before > 0 && after < before;
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
logger.error(`[Bot] Proactive share delivery failed: ${err.message}`);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
armShareDelivery(didDeliver);
|
|
1792
|
+
}, delayMs);
|
|
1793
|
+
};
|
|
1794
|
+
|
|
1795
|
+
// Start the first check after a random 10–30 min
|
|
1796
|
+
armShareDelivery(false);
|
|
1797
|
+
|
|
1772
1798
|
return bot;
|
|
1773
1799
|
}
|