pikiclaw 0.2.35
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/LICENSE +21 -0
- package/README.md +315 -0
- package/dist/agent-driver.js +24 -0
- package/dist/bot-command-ui.js +299 -0
- package/dist/bot-commands.js +236 -0
- package/dist/bot-feishu-render.js +527 -0
- package/dist/bot-feishu.js +752 -0
- package/dist/bot-handler.js +115 -0
- package/dist/bot-menu.js +44 -0
- package/dist/bot-streaming.js +165 -0
- package/dist/bot-telegram-directory.js +74 -0
- package/dist/bot-telegram-live-preview.js +192 -0
- package/dist/bot-telegram-render.js +369 -0
- package/dist/bot-telegram.js +789 -0
- package/dist/bot.js +897 -0
- package/dist/channel-base.js +46 -0
- package/dist/channel-feishu.js +873 -0
- package/dist/channel-states.js +3 -0
- package/dist/channel-telegram.js +773 -0
- package/dist/cli-channels.js +24 -0
- package/dist/cli.js +484 -0
- package/dist/code-agent.js +1080 -0
- package/dist/config-validation.js +244 -0
- package/dist/dashboard-ui.js +31 -0
- package/dist/dashboard.js +840 -0
- package/dist/driver-claude.js +520 -0
- package/dist/driver-codex.js +1055 -0
- package/dist/driver-gemini.js +230 -0
- package/dist/mcp-bridge.js +192 -0
- package/dist/mcp-session-server.js +321 -0
- package/dist/onboarding.js +138 -0
- package/dist/process-control.js +259 -0
- package/dist/run.js +275 -0
- package/dist/session-status.js +43 -0
- package/dist/setup-wizard.js +231 -0
- package/dist/user-config.js +195 -0
- package/package.json +60 -0
|
@@ -0,0 +1,789 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot-telegram.ts - Telegram bot orchestration: commands, callbacks, artifacts, lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Rendering, workdir browsing, and live preview state live in dedicated helper modules.
|
|
5
|
+
* For a new IM (Lark, WhatsApp, ...), create a parallel bot-lark.ts / bot-whatsapp.ts
|
|
6
|
+
* that extends Bot and composes channel-specific renderer/view helpers.
|
|
7
|
+
*/
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { Bot, VERSION, fmtTokens, fmtUptime, fmtBytes, buildPrompt, parseAllowedChatIds, } from './bot.js';
|
|
13
|
+
import { stageSessionFiles, } from './code-agent.js';
|
|
14
|
+
import { shutdownAllDrivers } from './agent-driver.js';
|
|
15
|
+
import { buildDefaultMenuCommands, buildWelcomeIntro, SKILL_CMD_PREFIX, } from './bot-menu.js';
|
|
16
|
+
import { getStartData, getStatusDataAsync, getHostDataSync, getSessionTurnPreviewData, resolveSkillPrompt, summarizePromptForStatus, } from './bot-commands.js';
|
|
17
|
+
import { buildAgentsCommandView, buildModelsCommandView, buildSessionsCommandView, buildSkillsCommandView, decodeCommandAction, executeCommandAction, } from './bot-command-ui.js';
|
|
18
|
+
import { buildSwitchWorkdirView, resolveRegisteredPath } from './bot-telegram-directory.js';
|
|
19
|
+
import { LivePreview } from './bot-telegram-live-preview.js';
|
|
20
|
+
import { formatActiveTaskRestartError, getActiveTaskCount, registerProcessRuntime, buildRestartCommand, requestProcessRestart, } from './process-control.js';
|
|
21
|
+
import { buildInitialPreviewHtml, buildStreamPreviewHtml, buildFinalReplyRender, escapeHtml, formatMenuLines, formatProviderUsageLines, renderCommandNoticeHtml, renderCommandSelectionHtml, renderCommandSelectionKeyboard, renderSessionTurnHtml, truncateMiddle, } from './bot-telegram-render.js';
|
|
22
|
+
import { TelegramChannel } from './channel-telegram.js';
|
|
23
|
+
import { splitText, supportsChannelCapability } from './channel-base.js';
|
|
24
|
+
import { getActiveUserConfig } from './user-config.js';
|
|
25
|
+
/** Telegram HTML renderer for LivePreview. */
|
|
26
|
+
const telegramPreviewRenderer = {
|
|
27
|
+
renderInitial: buildInitialPreviewHtml,
|
|
28
|
+
renderStream: buildStreamPreviewHtml,
|
|
29
|
+
};
|
|
30
|
+
const SHUTDOWN_EXIT_CODE = {
|
|
31
|
+
SIGINT: 130,
|
|
32
|
+
SIGTERM: 143,
|
|
33
|
+
};
|
|
34
|
+
const SHUTDOWN_FORCE_EXIT_MS = 3_000;
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// TelegramBot
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
export class TelegramBot extends Bot {
|
|
39
|
+
token;
|
|
40
|
+
channel;
|
|
41
|
+
sessionMessages = new Map();
|
|
42
|
+
nextTaskId = 1;
|
|
43
|
+
shutdownInFlight = false;
|
|
44
|
+
shutdownExitCode = null;
|
|
45
|
+
shutdownForceExitTimer = null;
|
|
46
|
+
signalHandlers = {};
|
|
47
|
+
processRuntimeCleanup = null;
|
|
48
|
+
constructor() {
|
|
49
|
+
super();
|
|
50
|
+
const config = getActiveUserConfig();
|
|
51
|
+
// merge Telegram-specific allowed IDs into base
|
|
52
|
+
if (config.telegramAllowedChatIds) {
|
|
53
|
+
for (const id of parseAllowedChatIds(config.telegramAllowedChatIds))
|
|
54
|
+
this.allowedChatIds.add(id);
|
|
55
|
+
}
|
|
56
|
+
this.token = String(config.telegramBotToken || process.env.TELEGRAM_BOT_TOKEN || '').trim();
|
|
57
|
+
if (!this.token)
|
|
58
|
+
throw new Error('Missing Telegram token. Configure via dashboard or set TELEGRAM_BOT_TOKEN');
|
|
59
|
+
}
|
|
60
|
+
onManagedConfigChange(config, opts = {}) {
|
|
61
|
+
const nextToken = String(config.telegramBotToken || process.env.TELEGRAM_BOT_TOKEN || '').trim();
|
|
62
|
+
if (nextToken && nextToken !== this.token) {
|
|
63
|
+
this.token = nextToken;
|
|
64
|
+
if (!opts.initial)
|
|
65
|
+
this.log('telegram token reloaded from setting.json');
|
|
66
|
+
}
|
|
67
|
+
const mergedAllowed = parseAllowedChatIds(process.env.PIKICLAW_ALLOWED_IDS || '');
|
|
68
|
+
for (const id of parseAllowedChatIds(String(config.telegramAllowedChatIds || '')))
|
|
69
|
+
mergedAllowed.add(id);
|
|
70
|
+
this.allowedChatIds = mergedAllowed;
|
|
71
|
+
}
|
|
72
|
+
/** Skill command prefix used in Telegram bot commands. */
|
|
73
|
+
static SKILL_CMD_PREFIX = SKILL_CMD_PREFIX;
|
|
74
|
+
/** Register bot menu commands. Called automatically after connect. */
|
|
75
|
+
async setupMenu() {
|
|
76
|
+
if (!supportsChannelCapability(this.channel, 'commandMenu'))
|
|
77
|
+
return;
|
|
78
|
+
const { commands, skillCount } = this.getCurrentMenuState();
|
|
79
|
+
await this.channel.setMenu(commands);
|
|
80
|
+
this.log(`menu: ${commands.length} commands (${skillCount} skills)`);
|
|
81
|
+
}
|
|
82
|
+
afterSwitchWorkdir(_oldPath, _newPath) {
|
|
83
|
+
this.sessionMessages.clear();
|
|
84
|
+
if (!this.channel)
|
|
85
|
+
return;
|
|
86
|
+
void this.setupMenu().catch(err => this.log(`menu refresh failed after workdir switch: ${err}`));
|
|
87
|
+
}
|
|
88
|
+
clearShutdownForceExitTimer() {
|
|
89
|
+
if (!this.shutdownForceExitTimer)
|
|
90
|
+
return;
|
|
91
|
+
clearTimeout(this.shutdownForceExitTimer);
|
|
92
|
+
this.shutdownForceExitTimer = null;
|
|
93
|
+
}
|
|
94
|
+
removeSignalHandlers() {
|
|
95
|
+
for (const sig of Object.keys(this.signalHandlers)) {
|
|
96
|
+
const handler = this.signalHandlers[sig];
|
|
97
|
+
if (handler)
|
|
98
|
+
process.off(sig, handler);
|
|
99
|
+
}
|
|
100
|
+
this.signalHandlers = {};
|
|
101
|
+
}
|
|
102
|
+
installSignalHandlers() {
|
|
103
|
+
this.removeSignalHandlers();
|
|
104
|
+
const onSigint = () => this.beginShutdown('SIGINT');
|
|
105
|
+
const onSigterm = () => this.beginShutdown('SIGTERM');
|
|
106
|
+
const onSigusr2 = () => this.performRestart();
|
|
107
|
+
this.signalHandlers = {
|
|
108
|
+
SIGINT: onSigint,
|
|
109
|
+
SIGTERM: onSigterm,
|
|
110
|
+
SIGUSR2: onSigusr2,
|
|
111
|
+
};
|
|
112
|
+
process.once('SIGINT', onSigint);
|
|
113
|
+
process.once('SIGTERM', onSigterm);
|
|
114
|
+
process.on('SIGUSR2', onSigusr2);
|
|
115
|
+
}
|
|
116
|
+
cleanupRuntimeForExit() {
|
|
117
|
+
try {
|
|
118
|
+
this.channel.disconnect();
|
|
119
|
+
}
|
|
120
|
+
catch { }
|
|
121
|
+
this.stopKeepAlive();
|
|
122
|
+
shutdownAllDrivers();
|
|
123
|
+
}
|
|
124
|
+
buildRestartEnv() {
|
|
125
|
+
const knownIds = new Set(this.allowedChatIds);
|
|
126
|
+
const knownChats = this.channel.knownChats instanceof Set ? this.channel.knownChats : new Set();
|
|
127
|
+
for (const cid of knownChats)
|
|
128
|
+
knownIds.add(cid);
|
|
129
|
+
return knownIds.size ? { TELEGRAM_ALLOWED_CHAT_IDS: [...knownIds].join(',') } : {};
|
|
130
|
+
}
|
|
131
|
+
beginShutdown(sig) {
|
|
132
|
+
if (this.shutdownInFlight)
|
|
133
|
+
return;
|
|
134
|
+
this.shutdownInFlight = true;
|
|
135
|
+
this.shutdownExitCode = SHUTDOWN_EXIT_CODE[sig];
|
|
136
|
+
this.log(`${sig}, shutting down...`);
|
|
137
|
+
this.cleanupRuntimeForExit();
|
|
138
|
+
this.clearShutdownForceExitTimer();
|
|
139
|
+
this.shutdownForceExitTimer = setTimeout(() => {
|
|
140
|
+
this.log(`shutdown still pending after ${Math.floor(SHUTDOWN_FORCE_EXIT_MS / 1000)}s, forcing exit`);
|
|
141
|
+
process.exit(this.shutdownExitCode ?? 1);
|
|
142
|
+
}, SHUTDOWN_FORCE_EXIT_MS);
|
|
143
|
+
this.shutdownForceExitTimer.unref?.();
|
|
144
|
+
}
|
|
145
|
+
performRestart() {
|
|
146
|
+
this.cleanupRuntimeForExit();
|
|
147
|
+
const { bin, args } = buildRestartCommand(process.argv.slice(2));
|
|
148
|
+
const child = spawn(bin, args, {
|
|
149
|
+
stdio: 'inherit',
|
|
150
|
+
detached: true,
|
|
151
|
+
env: {
|
|
152
|
+
...process.env,
|
|
153
|
+
npm_config_yes: process.env.npm_config_yes || 'true',
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
child.unref();
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
getCurrentMenuState() {
|
|
160
|
+
const res = this.fetchAgents();
|
|
161
|
+
const installedCount = res.agents.filter(a => a.installed).length;
|
|
162
|
+
const skillRes = this.fetchSkills();
|
|
163
|
+
const commands = buildDefaultMenuCommands(installedCount, skillRes.skills);
|
|
164
|
+
return { commands, skillCount: skillRes.skills.length, skills: skillRes.skills };
|
|
165
|
+
}
|
|
166
|
+
welcomeIntroLines() {
|
|
167
|
+
const intro = buildWelcomeIntro(VERSION);
|
|
168
|
+
return [
|
|
169
|
+
`<b>${escapeHtml(intro.title)}</b> v${escapeHtml(intro.version)}`,
|
|
170
|
+
escapeHtml(intro.subtitle),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
createTaskId(session) {
|
|
174
|
+
const seq = this.nextTaskId++;
|
|
175
|
+
return `${session.key}:${Date.now().toString(36)}:${seq.toString(36)}`;
|
|
176
|
+
}
|
|
177
|
+
registerSessionMessage(chatId, messageId, session) {
|
|
178
|
+
if (session.workdir !== this.workdir)
|
|
179
|
+
return;
|
|
180
|
+
if (typeof messageId !== 'number' || !Number.isFinite(messageId))
|
|
181
|
+
return;
|
|
182
|
+
let messages = this.sessionMessages.get(chatId);
|
|
183
|
+
if (!messages) {
|
|
184
|
+
messages = new Map();
|
|
185
|
+
this.sessionMessages.set(chatId, messages);
|
|
186
|
+
}
|
|
187
|
+
messages.set(messageId, session.key);
|
|
188
|
+
while (messages.size > 1024) {
|
|
189
|
+
const oldest = messages.keys().next();
|
|
190
|
+
if (oldest.done)
|
|
191
|
+
break;
|
|
192
|
+
messages.delete(oldest.value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
registerSessionMessages(chatId, messageIds, session) {
|
|
196
|
+
for (const messageId of messageIds)
|
|
197
|
+
this.registerSessionMessage(chatId, messageId, session);
|
|
198
|
+
}
|
|
199
|
+
sessionFromMessage(chatId, messageId) {
|
|
200
|
+
if (typeof messageId !== 'number' || !Number.isFinite(messageId))
|
|
201
|
+
return null;
|
|
202
|
+
const sessionKey = this.sessionMessages.get(chatId)?.get(messageId) || null;
|
|
203
|
+
return this.getSessionRuntimeByKey(sessionKey);
|
|
204
|
+
}
|
|
205
|
+
ensureSession(chatId, title, files) {
|
|
206
|
+
const cs = this.chat(chatId);
|
|
207
|
+
const selected = this.getSelectedSession(cs);
|
|
208
|
+
if (selected)
|
|
209
|
+
return selected;
|
|
210
|
+
const staged = stageSessionFiles({
|
|
211
|
+
agent: cs.agent,
|
|
212
|
+
workdir: this.workdir,
|
|
213
|
+
files: [],
|
|
214
|
+
sessionId: null,
|
|
215
|
+
title: title || files[0] || 'New session',
|
|
216
|
+
});
|
|
217
|
+
const runtime = this.upsertSessionRuntime({
|
|
218
|
+
agent: cs.agent,
|
|
219
|
+
sessionId: staged.sessionId,
|
|
220
|
+
workspacePath: staged.workspacePath,
|
|
221
|
+
modelId: this.modelForAgent(cs.agent),
|
|
222
|
+
});
|
|
223
|
+
this.applySessionSelection(cs, runtime);
|
|
224
|
+
return runtime;
|
|
225
|
+
}
|
|
226
|
+
resolveIncomingSession(ctx, text, files) {
|
|
227
|
+
const cs = this.chat(ctx.chatId);
|
|
228
|
+
const replyMessageId = typeof ctx.raw?.reply_to_message?.message_id === 'number'
|
|
229
|
+
? ctx.raw.reply_to_message.message_id
|
|
230
|
+
: null;
|
|
231
|
+
const repliedSession = this.sessionFromMessage(ctx.chatId, replyMessageId);
|
|
232
|
+
if (repliedSession) {
|
|
233
|
+
this.applySessionSelection(cs, repliedSession);
|
|
234
|
+
return repliedSession;
|
|
235
|
+
}
|
|
236
|
+
const selected = this.getSelectedSession(cs);
|
|
237
|
+
if (selected)
|
|
238
|
+
return selected;
|
|
239
|
+
return this.ensureSession(ctx.chatId, text, files);
|
|
240
|
+
}
|
|
241
|
+
// ---- commands -------------------------------------------------------------
|
|
242
|
+
async cmdStart(ctx) {
|
|
243
|
+
const d = getStartData(this, ctx.chatId);
|
|
244
|
+
const lines = [
|
|
245
|
+
`<b>${escapeHtml(d.title)}</b> v${escapeHtml(d.version)}`,
|
|
246
|
+
escapeHtml(d.subtitle),
|
|
247
|
+
'',
|
|
248
|
+
`<b>Agent:</b> ${escapeHtml(d.agent)}`,
|
|
249
|
+
`<b>Workdir:</b> <code>${escapeHtml(d.workdir)}</code>`,
|
|
250
|
+
'',
|
|
251
|
+
'<b>Commands</b>',
|
|
252
|
+
...formatMenuLines(d.commands),
|
|
253
|
+
];
|
|
254
|
+
await ctx.reply(lines.join('\n'), { parseMode: 'HTML' });
|
|
255
|
+
}
|
|
256
|
+
async cmdSkills(ctx) {
|
|
257
|
+
await this.sendCommandView(ctx, buildSkillsCommandView(this, ctx.chatId));
|
|
258
|
+
}
|
|
259
|
+
async sendCommandView(ctx, view) {
|
|
260
|
+
await ctx.reply(renderCommandSelectionHtml(view), { parseMode: 'HTML', keyboard: renderCommandSelectionKeyboard(view) });
|
|
261
|
+
}
|
|
262
|
+
async replyCommandResult(ctx, result) {
|
|
263
|
+
if (result.kind === 'view') {
|
|
264
|
+
await this.sendCommandView(ctx, result.view);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (result.kind === 'skill') {
|
|
268
|
+
await this.handleMessage({ text: result.prompt, files: [] }, ctx);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (result.kind === 'notice') {
|
|
272
|
+
const sent = await ctx.reply(renderCommandNoticeHtml(result.notice), { parseMode: 'HTML' });
|
|
273
|
+
if (result.session && typeof sent === 'number')
|
|
274
|
+
this.registerSessionMessage(ctx.chatId, sent, result.session);
|
|
275
|
+
if (result.previewSession) {
|
|
276
|
+
await this.previewCurrentSessionTurn(ctx.chatId, result.previewSession.agent, result.previewSession.sessionId);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await ctx.reply(escapeHtml(result.message), { parseMode: 'HTML' });
|
|
281
|
+
}
|
|
282
|
+
async applyCommandCallbackResult(ctx, result) {
|
|
283
|
+
if (result.kind === 'noop') {
|
|
284
|
+
await ctx.answerCallback(result.message);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (result.kind === 'view') {
|
|
288
|
+
await ctx.editReply(ctx.messageId, renderCommandSelectionHtml(result.view), { parseMode: 'HTML', keyboard: renderCommandSelectionKeyboard(result.view) });
|
|
289
|
+
await ctx.answerCallback(result.callbackText ?? undefined);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (result.kind === 'skill') {
|
|
293
|
+
await ctx.answerCallback(result.callbackText ?? undefined);
|
|
294
|
+
await this.handleMessage({ text: result.prompt, files: [] }, ctx);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
await ctx.answerCallback(result.callbackText ?? undefined);
|
|
298
|
+
await ctx.editReply(ctx.messageId, renderCommandNoticeHtml(result.notice), { parseMode: 'HTML' });
|
|
299
|
+
if (result.session)
|
|
300
|
+
this.registerSessionMessage(ctx.chatId, ctx.messageId, result.session);
|
|
301
|
+
if (result.previewSession) {
|
|
302
|
+
await this.previewCurrentSessionTurn(ctx.chatId, result.previewSession.agent, result.previewSession.sessionId);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
sessionsPageSize = 5;
|
|
306
|
+
async cmdSessions(ctx) {
|
|
307
|
+
await this.sendCommandView(ctx, await buildSessionsCommandView(this, ctx.chatId, 0, this.sessionsPageSize));
|
|
308
|
+
}
|
|
309
|
+
async cmdStatus(ctx) {
|
|
310
|
+
const d = await getStatusDataAsync(this, ctx.chatId);
|
|
311
|
+
const lines = [
|
|
312
|
+
`<b>pikiclaw</b> v${d.version}\n`,
|
|
313
|
+
`<b>Uptime:</b> ${fmtUptime(d.uptime)}`,
|
|
314
|
+
`<b>Memory:</b> ${(d.memRss / 1024 / 1024).toFixed(0)}MB RSS / ${(d.memHeap / 1024 / 1024).toFixed(0)}MB heap`,
|
|
315
|
+
`<b>PID:</b> ${d.pid}`,
|
|
316
|
+
`<b>Workdir:</b> <code>${escapeHtml(d.workdir)}</code>`,
|
|
317
|
+
'',
|
|
318
|
+
`<b>Agent:</b> ${escapeHtml(d.agent)}`,
|
|
319
|
+
`<b>Model:</b> ${escapeHtml(d.model)}`,
|
|
320
|
+
`<b>Session:</b> ${d.sessionId ? `<code>${escapeHtml(d.sessionId.slice(0, 16))}</code>` : '(new)'}`,
|
|
321
|
+
`<b>Active Tasks:</b> ${d.activeTasksCount}`,
|
|
322
|
+
];
|
|
323
|
+
if (d.running) {
|
|
324
|
+
lines.push(`<b>Running:</b> ${fmtUptime(Date.now() - d.running.startedAt)} - ${escapeHtml(summarizePromptForStatus(d.running.prompt))}`);
|
|
325
|
+
}
|
|
326
|
+
lines.push(...formatProviderUsageLines(d.usage), '', '<b>Bot Usage</b>', ` Turns: ${d.stats.totalTurns}`);
|
|
327
|
+
if (d.stats.totalInputTokens || d.stats.totalOutputTokens) {
|
|
328
|
+
lines.push(` In: ${fmtTokens(d.stats.totalInputTokens)} Out: ${fmtTokens(d.stats.totalOutputTokens)}`);
|
|
329
|
+
if (d.stats.totalCachedTokens)
|
|
330
|
+
lines.push(` Cached: ${fmtTokens(d.stats.totalCachedTokens)}`);
|
|
331
|
+
}
|
|
332
|
+
await ctx.reply(lines.join('\n'), { parseMode: 'HTML' });
|
|
333
|
+
}
|
|
334
|
+
async cmdSwitch(ctx) {
|
|
335
|
+
const browsePath = path.dirname(this.workdir);
|
|
336
|
+
const view = buildSwitchWorkdirView(this.workdir, browsePath);
|
|
337
|
+
await ctx.reply(view.text, { parseMode: 'HTML', keyboard: view.keyboard });
|
|
338
|
+
}
|
|
339
|
+
async cmdHost(ctx) {
|
|
340
|
+
const d = getHostDataSync(this);
|
|
341
|
+
const lines = [
|
|
342
|
+
`<b>Host</b>\n`,
|
|
343
|
+
`<b>Name:</b> ${escapeHtml(d.hostName)}`,
|
|
344
|
+
`<b>CPU:</b> ${escapeHtml(d.cpuModel)} x${d.cpuCount}`,
|
|
345
|
+
d.cpuUsage
|
|
346
|
+
? `<b>CPU Usage:</b> ${d.cpuUsage.usedPercent.toFixed(1)}% (${d.cpuUsage.userPercent.toFixed(1)}% user, ${d.cpuUsage.sysPercent.toFixed(1)}% sys, ${d.cpuUsage.idlePercent.toFixed(1)}% idle)`
|
|
347
|
+
: '<b>CPU Usage:</b> unavailable',
|
|
348
|
+
`<b>Memory:</b> ${fmtBytes(d.memoryUsed)} / ${fmtBytes(d.totalMem)} (${d.memoryPercent.toFixed(0)}%)`,
|
|
349
|
+
`<b>Available:</b> ${fmtBytes(d.memoryAvailable)}`,
|
|
350
|
+
`<b>Battery:</b> ${d.battery ? `${escapeHtml(d.battery.percent)} (${escapeHtml(d.battery.state)})` : 'unavailable'}`,
|
|
351
|
+
];
|
|
352
|
+
if (d.disk)
|
|
353
|
+
lines.push(`<b>Disk:</b> ${escapeHtml(d.disk.used)} used / ${escapeHtml(d.disk.total)} total (${escapeHtml(d.disk.percent)})`);
|
|
354
|
+
lines.push(`\n<b>Process:</b> PID ${d.selfPid} | RSS ${fmtBytes(d.selfRss)} | Heap ${fmtBytes(d.selfHeap)}`);
|
|
355
|
+
if (d.topProcs.length > 1) {
|
|
356
|
+
lines.push(`\n<b>Top Processes:</b>`);
|
|
357
|
+
lines.push(`<pre>${d.topProcs.map(l => escapeHtml(l)).join('\n')}</pre>`);
|
|
358
|
+
}
|
|
359
|
+
await ctx.reply(lines.join('\n'), { parseMode: 'HTML' });
|
|
360
|
+
}
|
|
361
|
+
async cmdAgents(ctx) {
|
|
362
|
+
await this.sendCommandView(ctx, buildAgentsCommandView(this, ctx.chatId));
|
|
363
|
+
}
|
|
364
|
+
async cmdModels(ctx) {
|
|
365
|
+
await this.sendCommandView(ctx, await buildModelsCommandView(this, ctx.chatId));
|
|
366
|
+
}
|
|
367
|
+
async cmdRestart(ctx) {
|
|
368
|
+
const activeTasks = getActiveTaskCount();
|
|
369
|
+
if (activeTasks > 0) {
|
|
370
|
+
await ctx.reply(`⚠ ${formatActiveTaskRestartError(activeTasks)}`, { parseMode: 'HTML' });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await ctx.reply(`<b>Restarting pikiclaw...</b>\n\n` +
|
|
374
|
+
`The bot will be back shortly.`, { parseMode: 'HTML' });
|
|
375
|
+
void requestProcessRestart({ log: msg => this.log(msg) });
|
|
376
|
+
}
|
|
377
|
+
// ---- streaming bridge -----------------------------------------------------
|
|
378
|
+
async handleMessage(msg, ctx) {
|
|
379
|
+
const text = msg.text.trim();
|
|
380
|
+
if (!text && !msg.files.length)
|
|
381
|
+
return;
|
|
382
|
+
const session = this.resolveIncomingSession(ctx, text, msg.files);
|
|
383
|
+
const cs = this.chat(ctx.chatId);
|
|
384
|
+
this.applySessionSelection(cs, session);
|
|
385
|
+
const messageThreadId = typeof ctx.raw?.message_thread_id === 'number' ? ctx.raw.message_thread_id : undefined;
|
|
386
|
+
if (!text && msg.files.length) {
|
|
387
|
+
const hadPendingWork = this.sessionHasPendingWork(session);
|
|
388
|
+
const stageTask = this.queueSessionTask(session, async () => {
|
|
389
|
+
try {
|
|
390
|
+
const staged = stageSessionFiles({
|
|
391
|
+
agent: session.agent,
|
|
392
|
+
workdir: this.workdir,
|
|
393
|
+
files: msg.files,
|
|
394
|
+
sessionId: session.sessionId,
|
|
395
|
+
title: msg.files[0],
|
|
396
|
+
});
|
|
397
|
+
session.workspacePath = staged.workspacePath;
|
|
398
|
+
this.syncSelectedChats(session);
|
|
399
|
+
if (!staged.importedFiles.length)
|
|
400
|
+
throw new Error('no files persisted');
|
|
401
|
+
this.log(`[handleMessage] staged workspace files chat=${ctx.chatId} session=${staged.sessionId} files=${staged.importedFiles.length}`);
|
|
402
|
+
this.registerSessionMessage(ctx.chatId, ctx.messageId, session);
|
|
403
|
+
await this.safeSetMessageReaction(ctx.chatId, ctx.messageId, ['👌']);
|
|
404
|
+
}
|
|
405
|
+
catch (e) {
|
|
406
|
+
this.log(`[handleMessage] stage files failed: ${e?.message || e}`);
|
|
407
|
+
await this.safeSetMessageReaction(ctx.chatId, ctx.messageId, ['⚠️']);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
if (hadPendingWork) {
|
|
411
|
+
void stageTask.catch(e => this.log(`[handleMessage] stage queue failed: ${e}`));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
await stageTask.catch(e => this.log(`[handleMessage] stage queue failed: ${e}`));
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const files = msg.files;
|
|
419
|
+
const prompt = buildPrompt(text, files);
|
|
420
|
+
const start = Date.now();
|
|
421
|
+
const canEditMessages = supportsChannelCapability(this.channel, 'editMessages');
|
|
422
|
+
const canSendTyping = supportsChannelCapability(this.channel, 'typingIndicators');
|
|
423
|
+
this.log(`[handleMessage] queued chat=${ctx.chatId} agent=${session.agent} session=${session.sessionId || '(new)'} prompt="${prompt.slice(0, 100)}" files=${files.length}`);
|
|
424
|
+
let phId = null;
|
|
425
|
+
if (canEditMessages) {
|
|
426
|
+
const placeholderId = await ctx.reply(buildInitialPreviewHtml(session.agent), { parseMode: 'HTML', messageThreadId });
|
|
427
|
+
phId = typeof placeholderId === 'number' ? placeholderId : null;
|
|
428
|
+
if (phId != null) {
|
|
429
|
+
this.registerSessionMessage(ctx.chatId, phId, session);
|
|
430
|
+
this.log(`[handleMessage] placeholder sent msg_id=${phId}, task queued`);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
this.log(`[handleMessage] placeholder unavailable for chat=${ctx.chatId}; continuing without live preview`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
this.log(`[handleMessage] skipping placeholder for chat=${ctx.chatId}; channel does not support message edits`);
|
|
438
|
+
}
|
|
439
|
+
const taskId = this.createTaskId(session);
|
|
440
|
+
this.beginTask({
|
|
441
|
+
taskId,
|
|
442
|
+
chatId: ctx.chatId,
|
|
443
|
+
agent: session.agent,
|
|
444
|
+
sessionKey: session.key,
|
|
445
|
+
prompt,
|
|
446
|
+
startedAt: start,
|
|
447
|
+
sourceMessageId: ctx.messageId,
|
|
448
|
+
});
|
|
449
|
+
void this.queueSessionTask(session, async () => {
|
|
450
|
+
let livePreview = null;
|
|
451
|
+
try {
|
|
452
|
+
if (phId != null || canSendTyping) {
|
|
453
|
+
livePreview = new LivePreview({
|
|
454
|
+
agent: session.agent,
|
|
455
|
+
chatId: ctx.chatId,
|
|
456
|
+
placeholderMessageId: phId,
|
|
457
|
+
channel: this.channel,
|
|
458
|
+
renderer: telegramPreviewRenderer,
|
|
459
|
+
streamEditIntervalMs: session.agent === 'codex' ? 400 : 800,
|
|
460
|
+
startTimeMs: start,
|
|
461
|
+
canEditMessages,
|
|
462
|
+
canSendTyping,
|
|
463
|
+
messageThreadId,
|
|
464
|
+
log: (message) => this.log(message),
|
|
465
|
+
});
|
|
466
|
+
livePreview.start();
|
|
467
|
+
}
|
|
468
|
+
// MCP sendFile callback: sends files to IM in real-time during the stream
|
|
469
|
+
const mcpSendFile = this.createMcpSendFileCallback(ctx, messageThreadId);
|
|
470
|
+
const result = await this.runStream(prompt, session, files, (nextText, nextThinking, nextActivity = '', meta, plan) => {
|
|
471
|
+
livePreview?.update(nextText, nextThinking, nextActivity, meta, plan);
|
|
472
|
+
}, undefined, mcpSendFile);
|
|
473
|
+
await livePreview?.settle();
|
|
474
|
+
this.log(`[handleMessage] done agent=${session.agent} ok=${result.ok} session=${result.sessionId || '?'} elapsed=${result.elapsedS.toFixed(1)}s edits=${livePreview?.getEditCount() || 0} ` +
|
|
475
|
+
`tokens=in:${fmtTokens(result.inputTokens)}/cached:${fmtTokens(result.cachedInputTokens)}/out:${fmtTokens(result.outputTokens)}`);
|
|
476
|
+
this.log(`[handleMessage] response preview: "${result.message.slice(0, 150)}"`);
|
|
477
|
+
const finalReply = await this.sendFinalReply(ctx, phId, session.agent, result, { messageThreadId });
|
|
478
|
+
this.registerSessionMessages(ctx.chatId, finalReply.messageIds, session);
|
|
479
|
+
this.log(`[handleMessage] final reply sent to chat=${ctx.chatId}`);
|
|
480
|
+
}
|
|
481
|
+
catch (e) {
|
|
482
|
+
const msgText = String(e?.message || e || 'Unknown error');
|
|
483
|
+
this.log(`[handleMessage] task failed chat=${ctx.chatId} session=${session.sessionId} error=${msgText}`);
|
|
484
|
+
const errorHtml = `<b>Error</b>\n\n<code>${escapeHtml(msgText.slice(0, 500))}</code>`;
|
|
485
|
+
if (phId != null) {
|
|
486
|
+
try {
|
|
487
|
+
await this.channel.editMessage(ctx.chatId, phId, errorHtml, { parseMode: 'HTML' });
|
|
488
|
+
this.registerSessionMessage(ctx.chatId, phId, session);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
const sent = await this.channel.send(ctx.chatId, errorHtml, { parseMode: 'HTML', replyTo: ctx.messageId, messageThreadId }).catch(() => null);
|
|
492
|
+
this.registerSessionMessage(ctx.chatId, typeof sent === 'number' ? sent : null, session);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const sent = await this.channel.send(ctx.chatId, errorHtml, { parseMode: 'HTML', replyTo: ctx.messageId, messageThreadId }).catch(() => null);
|
|
497
|
+
this.registerSessionMessage(ctx.chatId, typeof sent === 'number' ? sent : null, session);
|
|
498
|
+
}
|
|
499
|
+
await this.safeSetMessageReaction(ctx.chatId, ctx.messageId, ['⚠️']);
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
livePreview?.dispose();
|
|
503
|
+
this.finishTask(taskId);
|
|
504
|
+
this.syncSelectedChats(session);
|
|
505
|
+
}
|
|
506
|
+
}).catch(e => {
|
|
507
|
+
this.log(`[handleMessage] queue execution failed: ${e}`);
|
|
508
|
+
this.finishTask(taskId);
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
/** Create an MCP sendFile callback bound to a Telegram chat context. */
|
|
512
|
+
createMcpSendFileCallback(ctx, messageThreadId) {
|
|
513
|
+
return async (filePath, opts) => {
|
|
514
|
+
try {
|
|
515
|
+
await this.channel.sendFile(ctx.chatId, filePath, {
|
|
516
|
+
caption: opts?.caption,
|
|
517
|
+
replyTo: ctx.messageId,
|
|
518
|
+
messageThreadId,
|
|
519
|
+
asPhoto: opts?.kind === 'photo',
|
|
520
|
+
});
|
|
521
|
+
return { ok: true };
|
|
522
|
+
}
|
|
523
|
+
catch (e) {
|
|
524
|
+
this.log(`[mcp] sendFile failed: ${filePath} error=${e?.message || e}`);
|
|
525
|
+
return { ok: false, error: e?.message || 'send failed' };
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
async safeSetMessageReaction(chatId, messageId, reactions) {
|
|
530
|
+
if (!supportsChannelCapability(this.channel, 'messageReactions'))
|
|
531
|
+
return;
|
|
532
|
+
const setReaction = this.channel?.setMessageReaction;
|
|
533
|
+
if (typeof setReaction !== 'function')
|
|
534
|
+
return;
|
|
535
|
+
try {
|
|
536
|
+
await setReaction.call(this.channel, chatId, messageId, reactions);
|
|
537
|
+
}
|
|
538
|
+
catch { }
|
|
539
|
+
}
|
|
540
|
+
async sendFinalReply(ctx, phId, agent, result, opts = {}) {
|
|
541
|
+
const rendered = buildFinalReplyRender(agent, result);
|
|
542
|
+
const messageIds = [];
|
|
543
|
+
const remember = (messageId) => {
|
|
544
|
+
if (typeof messageId === 'number' && !messageIds.includes(messageId))
|
|
545
|
+
messageIds.push(messageId);
|
|
546
|
+
return messageId;
|
|
547
|
+
};
|
|
548
|
+
const sendFinalText = (text, replyTo) => this.channel.send(ctx.chatId, text, {
|
|
549
|
+
parseMode: 'HTML',
|
|
550
|
+
replyTo: replyTo ?? ctx.messageId,
|
|
551
|
+
messageThreadId: opts.messageThreadId,
|
|
552
|
+
});
|
|
553
|
+
const replacePreview = async (text) => {
|
|
554
|
+
if (phId != null) {
|
|
555
|
+
try {
|
|
556
|
+
await this.channel.editMessage(ctx.chatId, phId, text, { parseMode: 'HTML' });
|
|
557
|
+
return remember(phId);
|
|
558
|
+
}
|
|
559
|
+
catch { }
|
|
560
|
+
}
|
|
561
|
+
return remember(await sendFinalText(text));
|
|
562
|
+
};
|
|
563
|
+
let finalMsgId = phId;
|
|
564
|
+
if (rendered.fullHtml.length <= 3900) {
|
|
565
|
+
finalMsgId = await replacePreview(rendered.fullHtml);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const maxFirst = 3900 - rendered.headerHtml.length - rendered.footerHtml.length;
|
|
569
|
+
let firstBody;
|
|
570
|
+
let remaining;
|
|
571
|
+
if (maxFirst > 200) {
|
|
572
|
+
let cut = rendered.bodyHtml.lastIndexOf('\n', maxFirst);
|
|
573
|
+
if (cut < maxFirst * 0.3)
|
|
574
|
+
cut = maxFirst;
|
|
575
|
+
firstBody = rendered.bodyHtml.slice(0, cut);
|
|
576
|
+
remaining = rendered.bodyHtml.slice(cut);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
firstBody = '';
|
|
580
|
+
remaining = rendered.bodyHtml;
|
|
581
|
+
}
|
|
582
|
+
const firstHtml = `${rendered.headerHtml}${firstBody}${rendered.footerHtml}`;
|
|
583
|
+
finalMsgId = await replacePreview(firstHtml);
|
|
584
|
+
if (remaining.trim()) {
|
|
585
|
+
const chunks = splitText(remaining, 3800);
|
|
586
|
+
for (const chunk of chunks) {
|
|
587
|
+
remember(await sendFinalText(chunk, finalMsgId ?? phId ?? ctx.messageId));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { primaryMessageId: finalMsgId, messageIds };
|
|
592
|
+
}
|
|
593
|
+
// ---- callbacks ------------------------------------------------------------
|
|
594
|
+
async handleSwitchNavigateCallback(data, ctx) {
|
|
595
|
+
if (!data.startsWith('sw:n:'))
|
|
596
|
+
return false;
|
|
597
|
+
const [pathId, pageRaw] = data.slice(5).split(':');
|
|
598
|
+
const browsePath = resolveRegisteredPath(parseInt(pathId, 10));
|
|
599
|
+
if (!browsePath) {
|
|
600
|
+
await ctx.answerCallback('Expired, use /switch again');
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
const view = buildSwitchWorkdirView(this.workdir, browsePath, parseInt(pageRaw, 10) || 0);
|
|
604
|
+
await ctx.editReply(ctx.messageId, view.text, { parseMode: 'HTML', keyboard: view.keyboard });
|
|
605
|
+
await ctx.answerCallback();
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
async handleSwitchSelectCallback(data, ctx) {
|
|
609
|
+
if (!data.startsWith('sw:s:'))
|
|
610
|
+
return false;
|
|
611
|
+
const dirPath = resolveRegisteredPath(parseInt(data.slice(5), 10));
|
|
612
|
+
if (!dirPath) {
|
|
613
|
+
await ctx.answerCallback('Expired, use /switch again');
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
616
|
+
if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
|
|
617
|
+
await ctx.answerCallback('Not a valid directory');
|
|
618
|
+
return true;
|
|
619
|
+
}
|
|
620
|
+
const oldPath = this.switchWorkdir(dirPath);
|
|
621
|
+
await ctx.answerCallback('Switched!');
|
|
622
|
+
await ctx.editReply(ctx.messageId, `<b>Workdir</b>\n● <code>${escapeHtml(truncateMiddle(oldPath, 42))}</code>\n→ <code>${escapeHtml(truncateMiddle(dirPath, 42))}</code>`, { parseMode: 'HTML' });
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
async handleSessionsPageCallback(data, ctx) {
|
|
626
|
+
const action = decodeCommandAction(data);
|
|
627
|
+
if (!action)
|
|
628
|
+
return false;
|
|
629
|
+
const result = await executeCommandAction(this, ctx.chatId, action, {
|
|
630
|
+
sessionsPageSize: this.sessionsPageSize,
|
|
631
|
+
});
|
|
632
|
+
await this.applyCommandCallbackResult(ctx, result);
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
async handleCallback(data, ctx) {
|
|
636
|
+
if (await this.handleSwitchNavigateCallback(data, ctx))
|
|
637
|
+
return;
|
|
638
|
+
if (await this.handleSwitchSelectCallback(data, ctx))
|
|
639
|
+
return;
|
|
640
|
+
if (await this.handleSessionsPageCallback(data, ctx))
|
|
641
|
+
return;
|
|
642
|
+
await ctx.answerCallback();
|
|
643
|
+
}
|
|
644
|
+
async previewCurrentSessionTurn(chatId, agent, sessionId) {
|
|
645
|
+
try {
|
|
646
|
+
const preview = await getSessionTurnPreviewData(this, agent, sessionId, 50);
|
|
647
|
+
if (!preview)
|
|
648
|
+
return;
|
|
649
|
+
const previewHtml = renderSessionTurnHtml(preview.userText, preview.assistantText);
|
|
650
|
+
if (!previewHtml)
|
|
651
|
+
return;
|
|
652
|
+
const sent = await this.channel.send(chatId, previewHtml, { parseMode: 'HTML' });
|
|
653
|
+
if (sessionId) {
|
|
654
|
+
const runtime = this.getSessionRuntimeByKey(this.sessionKey(agent, sessionId));
|
|
655
|
+
if (runtime && typeof sent === 'number')
|
|
656
|
+
this.registerSessionMessage(chatId, sent, runtime);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch {
|
|
660
|
+
// non-critical
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
// ---- command router -------------------------------------------------------
|
|
664
|
+
async handleCommand(cmd, args, ctx) {
|
|
665
|
+
try {
|
|
666
|
+
switch (cmd) {
|
|
667
|
+
case 'start':
|
|
668
|
+
await this.cmdStart(ctx);
|
|
669
|
+
return;
|
|
670
|
+
case 'sessions':
|
|
671
|
+
await this.cmdSessions(ctx);
|
|
672
|
+
return;
|
|
673
|
+
case 'agents':
|
|
674
|
+
await this.cmdAgents(ctx);
|
|
675
|
+
return;
|
|
676
|
+
case 'models':
|
|
677
|
+
await this.cmdModels(ctx);
|
|
678
|
+
return;
|
|
679
|
+
case 'skills':
|
|
680
|
+
await this.cmdSkills(ctx);
|
|
681
|
+
return;
|
|
682
|
+
case 'status':
|
|
683
|
+
await this.cmdStatus(ctx);
|
|
684
|
+
return;
|
|
685
|
+
case 'host':
|
|
686
|
+
await this.cmdHost(ctx);
|
|
687
|
+
return;
|
|
688
|
+
case 'switch':
|
|
689
|
+
await this.cmdSwitch(ctx);
|
|
690
|
+
return;
|
|
691
|
+
case 'restart':
|
|
692
|
+
await this.cmdRestart(ctx);
|
|
693
|
+
return;
|
|
694
|
+
default:
|
|
695
|
+
// Intercept skill commands (sk_<name>) and route to agent
|
|
696
|
+
if (cmd.startsWith(TelegramBot.SKILL_CMD_PREFIX)) {
|
|
697
|
+
await this.cmdSkill(cmd, args, ctx);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
await this.handleMessage({ text: `/${cmd}${args ? ' ' + args : ''}`, files: [] }, ctx);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch (e) {
|
|
704
|
+
this.log(`cmd error: ${e}`);
|
|
705
|
+
await ctx.reply(`Error: ${String(e).slice(0, 200)}`);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/** Execute a project-defined skill by routing it to the current agent. */
|
|
709
|
+
async cmdSkill(cmd, args, ctx) {
|
|
710
|
+
const resolved = resolveSkillPrompt(this, ctx.chatId, cmd, args);
|
|
711
|
+
if (!resolved) {
|
|
712
|
+
await ctx.reply(`Skill not found for command /${cmd} in:\n<code>${escapeHtml(this.workdir)}</code>`, { parseMode: 'HTML' });
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
this.log(`skill: ${resolved.skillName} agent=${this.chat(ctx.chatId).agent}${args.trim() ? ` args="${args.trim()}"` : ''}`);
|
|
716
|
+
await this.handleMessage({ text: resolved.prompt, files: [] }, ctx);
|
|
717
|
+
}
|
|
718
|
+
// ---- lifecycle ------------------------------------------------------------
|
|
719
|
+
async run() {
|
|
720
|
+
const tmpDir = path.join(os.tmpdir(), 'pikiclaw');
|
|
721
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
722
|
+
this.channel = new TelegramChannel({
|
|
723
|
+
token: this.token,
|
|
724
|
+
workdir: tmpDir,
|
|
725
|
+
allowedChatIds: this.allowedChatIds.size ? this.allowedChatIds : undefined,
|
|
726
|
+
});
|
|
727
|
+
this.processRuntimeCleanup?.();
|
|
728
|
+
this.processRuntimeCleanup = registerProcessRuntime({
|
|
729
|
+
label: 'telegram',
|
|
730
|
+
getActiveTaskCount: () => this.activeTasks.size,
|
|
731
|
+
prepareForRestart: () => this.cleanupRuntimeForExit(),
|
|
732
|
+
buildRestartEnv: () => this.buildRestartEnv(),
|
|
733
|
+
});
|
|
734
|
+
this.installSignalHandlers();
|
|
735
|
+
try {
|
|
736
|
+
const bot = await this.channel.connect();
|
|
737
|
+
this.connected = true;
|
|
738
|
+
this.log(`bot: @${bot.username} (id=${bot.id})`);
|
|
739
|
+
this.channel.skipPendingUpdatesOnNextListen();
|
|
740
|
+
// Seed knownChats so setupMenu applies per-chat commands
|
|
741
|
+
for (const cid of this.allowedChatIds)
|
|
742
|
+
if (typeof cid === 'number')
|
|
743
|
+
this.channel.knownChats.add(cid);
|
|
744
|
+
for (const ag of this.fetchAgents().agents) {
|
|
745
|
+
this.log(`agent ${ag.agent}: ${ag.path || 'NOT FOUND'}`);
|
|
746
|
+
}
|
|
747
|
+
this.log(`config: agent=${this.defaultAgent} workdir=${this.workdir} timeout=${this.runTimeout}s`);
|
|
748
|
+
this.channel.onCommand((cmd, args, ctx) => this.handleCommand(cmd, args, ctx));
|
|
749
|
+
this.channel.onMessage((msg, ctx) => this.handleMessage(msg, ctx));
|
|
750
|
+
this.channel.onCallback((data, ctx) => this.handleCallback(data, ctx));
|
|
751
|
+
this.channel.onError(err => this.log(`error: ${err}`));
|
|
752
|
+
this.startKeepAlive();
|
|
753
|
+
void this.setupMenu().catch(err => this.log(`menu setup failed: ${err}`));
|
|
754
|
+
void this.sendStartupNotice().catch(err => this.log(`startup notice failed: ${err}`));
|
|
755
|
+
this.log('✓ Telegram connected, polling started — ready to receive messages');
|
|
756
|
+
await this.channel.listen();
|
|
757
|
+
this.stopKeepAlive();
|
|
758
|
+
this.log('stopped');
|
|
759
|
+
}
|
|
760
|
+
finally {
|
|
761
|
+
this.stopKeepAlive();
|
|
762
|
+
this.clearShutdownForceExitTimer();
|
|
763
|
+
this.removeSignalHandlers();
|
|
764
|
+
this.processRuntimeCleanup?.();
|
|
765
|
+
this.processRuntimeCleanup = null;
|
|
766
|
+
if (this.shutdownInFlight)
|
|
767
|
+
process.exit(this.shutdownExitCode ?? 1);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async sendStartupNotice() {
|
|
771
|
+
const targets = new Set(this.allowedChatIds);
|
|
772
|
+
for (const cid of this.channel.knownChats)
|
|
773
|
+
targets.add(cid);
|
|
774
|
+
if (!targets.size) {
|
|
775
|
+
this.log('no known chats for startup notice');
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const text = this.welcomeIntroLines().join('\n');
|
|
779
|
+
for (const cid of targets) {
|
|
780
|
+
try {
|
|
781
|
+
await this.channel.send(cid, text, { parseMode: 'HTML' });
|
|
782
|
+
this.log(`startup notice sent to chat=${cid}`);
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
this.log(`startup notice failed for chat=${cid}: ${e}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|