@ebowwa/channel-telegram 1.12.5 → 1.13.0
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 +78 -44
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +3 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/restart.d.ts +7 -0
- package/dist/commands/restart.d.ts.map +1 -0
- package/dist/commands/restart.js +29 -0
- package/dist/commands/restart.js.map +1 -0
- package/dist/commands/settings.d.ts +8 -0
- package/dist/commands/settings.d.ts.map +1 -0
- package/dist/commands/settings.js +16 -0
- package/dist/commands/settings.js.map +1 -0
- package/dist/index.d.ts +83 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +350 -712
- package/dist/index.js.map +1 -1
- package/package.json +9 -13
- package/src/commands/index.ts +3 -0
- package/src/commands/restart.ts +41 -0
- package/src/commands/settings.ts +24 -0
- package/src/index.ts +415 -823
- package/dist/mcp/client.d.ts +0 -50
- package/dist/mcp/client.d.ts.map +0 -1
- package/dist/mcp/client.js +0 -150
- package/dist/mcp/client.js.map +0 -1
- package/dist/mcp/index.d.ts +0 -5
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js +0 -5
- package/dist/mcp/index.js.map +0 -1
- package/src/api/fetch-retry.js +0 -96
- package/src/api/keys.js +0 -25
- package/src/commands/cancel.js +0 -120
- package/src/commands/clear.js +0 -59
- package/src/commands/doppler.js +0 -118
- package/src/commands/git.js +0 -126
- package/src/commands/help.js +0 -74
- package/src/commands/index.js +0 -65
- package/src/commands/logs.js +0 -81
- package/src/commands/pause.js +0 -133
- package/src/commands/resources.js +0 -87
- package/src/commands/resume.js +0 -95
- package/src/commands/start.js +0 -68
- package/src/commands/status.js +0 -62
- package/src/commands/tools.js +0 -67
- package/src/commands/toolsoutput.js +0 -85
- package/src/commands/types.js +0 -5
- package/src/mcp/client.ts +0 -188
- package/src/mcp/index.ts +0 -5
package/src/index.ts
CHANGED
|
@@ -1,357 +1,241 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @ebowwa/channel-telegram
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Telegram channel adapter implementing ChannelConnector.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
6
|
+
* This package handles:
|
|
7
|
+
* - Telegram protocol operations (polling, message delivery)
|
|
8
|
+
* - Built-in commands (/start, /help, /status, etc.)
|
|
9
|
+
* - Typing indicators, message chunking, access control
|
|
10
|
+
* - Message normalization to ChannelMessage format
|
|
11
|
+
*
|
|
12
|
+
* Intelligence (GLM, tools, memory) can be provided by:
|
|
13
|
+
* 1. This package (standalone mode with built-in tools)
|
|
14
|
+
* 2. External daemon/consumer (adapter mode via onMessage handler)
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
|
-
import TelegramBot from
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
import TelegramBot from "node-telegram-bot-api";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
type ChannelId,
|
|
21
|
+
type ChannelMessage,
|
|
22
|
+
type ChannelResponse,
|
|
23
|
+
type ChannelCapabilities,
|
|
24
|
+
type MessageSender,
|
|
25
|
+
type MessageContext,
|
|
26
|
+
type MessageHandler,
|
|
27
|
+
createChannelId,
|
|
28
|
+
} from "@ebowwa/channel-types";
|
|
29
|
+
|
|
30
|
+
import { ConversationMemory } from "./conversation-memory.js";
|
|
31
|
+
import { registerAllCommands, isQuiet } from "./commands/index.js";
|
|
32
|
+
import type { Tool } from "./types.js";
|
|
33
|
+
|
|
34
|
+
// Re-export for external use
|
|
35
|
+
export { ConversationMemory } from "./conversation-memory.js";
|
|
36
|
+
export { registerAllCommands, isQuiet } from "./commands/index.js";
|
|
37
|
+
export type { Tool } from "./types.js";
|
|
38
|
+
|
|
39
|
+
// ============================================================
|
|
40
|
+
// CONFIG
|
|
41
|
+
// ============================================================
|
|
42
|
+
|
|
43
|
+
export interface TelegramConfig {
|
|
44
|
+
botToken: string;
|
|
45
|
+
testChatId?: string;
|
|
46
|
+
allowedUsers?: number[];
|
|
47
|
+
allowedChats?: number[];
|
|
48
|
+
/** Optional tools for command handlers (e.g., /tools command) */
|
|
49
|
+
tools?: Tool[];
|
|
50
|
+
/** Optional conversations file path */
|
|
51
|
+
conversationsFile?: string;
|
|
52
|
+
}
|
|
34
53
|
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
export function createTelegramConfigFromEnv(): TelegramConfig {
|
|
55
|
+
const allowedUsers = process.env.TELEGRAM_ALLOWED_USERS
|
|
56
|
+
?.split(",")
|
|
57
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
58
|
+
.filter((n) => !isNaN(n));
|
|
59
|
+
|
|
60
|
+
const allowedChats = process.env.TELEGRAM_ALLOWED_CHATS
|
|
61
|
+
?.split(",")
|
|
62
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
63
|
+
.filter((n) => !isNaN(n));
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
botToken: process.env.TELEGRAM_BOT_TOKEN || "",
|
|
67
|
+
testChatId: process.env.TELEGRAM_TEST_CHAT_ID,
|
|
68
|
+
allowedUsers,
|
|
69
|
+
allowedChats,
|
|
70
|
+
conversationsFile: process.env.CONVERSATIONS_FILE,
|
|
71
|
+
};
|
|
40
72
|
}
|
|
41
73
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return content.length > 4000 ? content.slice(0, 4000) + '\n...[truncated]' : content;
|
|
59
|
-
} catch (e) {
|
|
60
|
-
return `Error reading file: ${(e as Error).message}`;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: 'write_file',
|
|
66
|
-
description: 'Write content to a file. Creates the file if it does not exist, overwrites if it does.',
|
|
67
|
-
parameters: {
|
|
68
|
-
type: 'object',
|
|
69
|
-
properties: {
|
|
70
|
-
path: { type: 'string', description: 'Absolute file path to write' },
|
|
71
|
-
content: { type: 'string', description: 'Content to write to the file' }
|
|
72
|
-
},
|
|
73
|
-
required: ['path', 'content']
|
|
74
|
-
},
|
|
75
|
-
handler: async (args) => {
|
|
76
|
-
const path = args.path as string;
|
|
77
|
-
const content = args.content as string;
|
|
78
|
-
try {
|
|
79
|
-
writeFileSync(path, content, 'utf-8');
|
|
80
|
-
return `Successfully wrote ${content.length} bytes to ${path}`;
|
|
81
|
-
} catch (e) {
|
|
82
|
-
return `Error writing file: ${(e as Error).message}`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: 'edit_file',
|
|
88
|
-
description: 'Edit a file by replacing a specific string with new content.',
|
|
89
|
-
parameters: {
|
|
90
|
-
type: 'object',
|
|
91
|
-
properties: {
|
|
92
|
-
path: { type: 'string', description: 'Absolute file path to edit' },
|
|
93
|
-
old_string: { type: 'string', description: 'The exact text to find and replace' },
|
|
94
|
-
new_string: { type: 'string', description: 'The text to replace it with' }
|
|
95
|
-
},
|
|
96
|
-
required: ['path', 'old_string', 'new_string']
|
|
97
|
-
},
|
|
98
|
-
handler: async (args) => {
|
|
99
|
-
const path = args.path as string;
|
|
100
|
-
const oldStr = args.old_string as string;
|
|
101
|
-
const newStr = args.new_string as string;
|
|
102
|
-
try {
|
|
103
|
-
if (!existsSync(path)) return `File not found: ${path}`;
|
|
104
|
-
const content = readFileSync(path, 'utf-8');
|
|
105
|
-
if (!content.includes(oldStr)) {
|
|
106
|
-
return `String not found in file. First 500 chars:\n${content.slice(0, 500)}`;
|
|
107
|
-
}
|
|
108
|
-
const newContent = content.replace(oldStr, newStr);
|
|
109
|
-
writeFileSync(path, newContent, 'utf-8');
|
|
110
|
-
return `Successfully edited ${path}`;
|
|
111
|
-
} catch (e) {
|
|
112
|
-
return `Error editing file: ${(e as Error).message}`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: 'append_file',
|
|
118
|
-
description: 'Append content to the end of a file. Creates the file if it does not exist.',
|
|
119
|
-
parameters: {
|
|
120
|
-
type: 'object',
|
|
121
|
-
properties: {
|
|
122
|
-
path: { type: 'string', description: 'Absolute file path to append to' },
|
|
123
|
-
content: { type: 'string', description: 'Content to append to the file' }
|
|
124
|
-
},
|
|
125
|
-
required: ['path', 'content']
|
|
126
|
-
},
|
|
127
|
-
handler: async (args) => {
|
|
128
|
-
const path = args.path as string;
|
|
129
|
-
const content = args.content as string;
|
|
130
|
-
try {
|
|
131
|
-
appendFileSync(path, content, 'utf-8');
|
|
132
|
-
return `Successfully appended ${content.length} bytes to ${path}`;
|
|
133
|
-
} catch (e) {
|
|
134
|
-
return `Error appending to file: ${(e as Error).message}`;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: 'list_dir',
|
|
140
|
-
description: 'List contents of a directory. Shows files and subdirectories.',
|
|
141
|
-
parameters: {
|
|
142
|
-
type: 'object',
|
|
143
|
-
properties: {
|
|
144
|
-
path: { type: 'string', description: 'Directory path to list (default: current working directory)' }
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
handler: async (args) => {
|
|
148
|
-
const path = (args.path as string) || process.cwd();
|
|
149
|
-
try {
|
|
150
|
-
const items = execSync(`ls -la "${path}" 2>&1`).toString();
|
|
151
|
-
return items;
|
|
152
|
-
} catch (e) {
|
|
153
|
-
return `Error listing directory: ${(e as Error).message}`;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
name: 'run_command',
|
|
159
|
-
description: 'Execute a shell command. Use for git, system info, etc. BE CAREFUL with destructive commands.',
|
|
160
|
-
parameters: {
|
|
161
|
-
type: 'object',
|
|
162
|
-
properties: {
|
|
163
|
-
command: { type: 'string', description: 'Shell command to execute' },
|
|
164
|
-
cwd: { type: 'string', description: 'Working directory (optional)' }
|
|
165
|
-
},
|
|
166
|
-
required: ['command']
|
|
74
|
+
// ============================================================
|
|
75
|
+
// TELEGRAM CHANNEL
|
|
76
|
+
// ============================================================
|
|
77
|
+
|
|
78
|
+
export class TelegramChannel {
|
|
79
|
+
readonly id: ChannelId;
|
|
80
|
+
readonly label = "Telegram";
|
|
81
|
+
readonly capabilities: ChannelCapabilities = {
|
|
82
|
+
supports: {
|
|
83
|
+
text: true,
|
|
84
|
+
media: true,
|
|
85
|
+
replies: true,
|
|
86
|
+
threads: false,
|
|
87
|
+
reactions: true,
|
|
88
|
+
editing: true,
|
|
89
|
+
streaming: false,
|
|
167
90
|
},
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// Safety check - block dangerous commands
|
|
172
|
-
const blocked = ['rm -rf', 'mkfs', 'dd if=', '> /dev/', 'chmod 777'];
|
|
173
|
-
if (blocked.some(b => cmd.includes(b))) {
|
|
174
|
-
return `Blocked: command contains dangerous pattern`;
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
const result = execSync(cmd, { timeout: 10000, cwd }).toString();
|
|
178
|
-
return result || '(no output)';
|
|
179
|
-
} catch (e: any) {
|
|
180
|
-
return e.stdout?.toString() || e.message;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
name: 'git_status',
|
|
186
|
-
description: 'Check git repository status',
|
|
187
|
-
parameters: {
|
|
188
|
-
type: 'object',
|
|
189
|
-
properties: {
|
|
190
|
-
cwd: { type: 'string', description: 'Repository path (optional)' }
|
|
191
|
-
}
|
|
91
|
+
media: {
|
|
92
|
+
maxFileSize: 50 * 1024 * 1024, // 50MB
|
|
93
|
+
supportedMimeTypes: ["image/*", "video/*", "audio/*", "application/pdf"],
|
|
192
94
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const status = execSync('git status 2>&1', { cwd }).toString();
|
|
197
|
-
const branch = execSync('git branch --show-current 2>&1', { cwd }).toString();
|
|
198
|
-
return `Branch: ${branch}\n\n${status}`;
|
|
199
|
-
} catch (e) {
|
|
200
|
-
return `Error: ${(e as Error).message}`;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
name: 'get_prompt',
|
|
206
|
-
description: 'Get a prompt template from the prompt store',
|
|
207
|
-
parameters: {
|
|
208
|
-
type: 'object',
|
|
209
|
-
properties: {
|
|
210
|
-
id: { type: 'string', description: 'Prompt ID to retrieve' }
|
|
211
|
-
},
|
|
212
|
-
required: ['id']
|
|
95
|
+
rateLimits: {
|
|
96
|
+
messagesPerMinute: 30,
|
|
97
|
+
charactersPerMessage: 4096,
|
|
213
98
|
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
{
|
|
231
|
-
name: 'system_info',
|
|
232
|
-
description: 'Get system resource info (CPU, memory, disk, uptime)',
|
|
233
|
-
parameters: { type: 'object', properties: {} },
|
|
234
|
-
handler: async () => {
|
|
235
|
-
try {
|
|
236
|
-
const cpu = execSync('nproc 2>/dev/null || echo "unknown"').toString().trim();
|
|
237
|
-
const mem = execSync('free -h 2>/dev/null | grep Mem || echo "unknown"').toString().trim();
|
|
238
|
-
const disk = execSync('df -h / 2>/dev/null | tail -1 || echo "unknown"').toString().trim();
|
|
239
|
-
const uptime = execSync('uptime -p 2>/dev/null || uptime 2>/dev/null || echo "unknown"').toString().trim();
|
|
240
|
-
return `CPU cores: ${cpu}\nMemory: ${mem}\nDisk: ${disk}\nUptime: ${uptime}`;
|
|
241
|
-
} catch (e) {
|
|
242
|
-
return `Error: ${(e as Error).message}`;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
private bot: TelegramBot;
|
|
102
|
+
private config: TelegramConfig;
|
|
103
|
+
private memory: ConversationMemory;
|
|
104
|
+
private tools: Tool[];
|
|
105
|
+
private messageHandler?: MessageHandler;
|
|
106
|
+
private typingIntervals: Map<number, NodeJS.Timeout> = new Map();
|
|
107
|
+
private connected = false;
|
|
108
|
+
|
|
109
|
+
constructor(config: TelegramConfig) {
|
|
110
|
+
this.config = config;
|
|
111
|
+
this.id = createChannelId("telegram", "default");
|
|
112
|
+
this.bot = new TelegramBot(config.botToken, { polling: false });
|
|
113
|
+
this.memory = new ConversationMemory(config.conversationsFile);
|
|
114
|
+
this.tools = config.tools || [];
|
|
245
115
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
116
|
+
|
|
117
|
+
// ============================================================
|
|
118
|
+
// ChannelConnector Implementation
|
|
119
|
+
// ============================================================
|
|
120
|
+
|
|
121
|
+
async start(): Promise<void> {
|
|
122
|
+
console.log("[TelegramChannel] Starting...");
|
|
123
|
+
|
|
124
|
+
// Start polling
|
|
125
|
+
this.bot = new TelegramBot(this.config.botToken, {
|
|
126
|
+
polling: {
|
|
127
|
+
interval: 300,
|
|
128
|
+
autoStart: true,
|
|
129
|
+
params: {
|
|
130
|
+
allowed_updates: ["message", "edited_message", "message_reaction", "callback_query"],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Register built-in commands
|
|
136
|
+
registerAllCommands(this.bot, this.memory, this.tools);
|
|
137
|
+
|
|
138
|
+
// Setup message routing
|
|
139
|
+
this.setupMessageHandlers();
|
|
140
|
+
|
|
141
|
+
// Register Telegram commands menu
|
|
142
|
+
await this.registerCommands();
|
|
143
|
+
|
|
144
|
+
this.connected = true;
|
|
145
|
+
console.log("[TelegramChannel] Connected and polling");
|
|
146
|
+
|
|
147
|
+
// Send startup notification if configured
|
|
148
|
+
if (this.config.testChatId) {
|
|
149
|
+
await this.sendMessage(Number(this.config.testChatId), "Telegram channel adapter online");
|
|
271
150
|
}
|
|
272
151
|
}
|
|
273
152
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const GLM_TOOLS = TOOLS.map(t => ({
|
|
279
|
-
type: 'function',
|
|
280
|
-
function: {
|
|
281
|
-
name: t.name,
|
|
282
|
-
description: t.description,
|
|
283
|
-
parameters: t.parameters
|
|
153
|
+
async stop(): Promise<void> {
|
|
154
|
+
console.log("[TelegramChannel] Stopping...");
|
|
155
|
+
await this.bot.stopPolling();
|
|
156
|
+
this.connected = false;
|
|
284
157
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (tool) {
|
|
292
|
-
return tool.handler(args);
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Set the message handler. The daemon/consumer provides intelligence.
|
|
161
|
+
*/
|
|
162
|
+
onMessage(handler: MessageHandler): void {
|
|
163
|
+
this.messageHandler = handler;
|
|
293
164
|
}
|
|
294
165
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Send a response back to Telegram.
|
|
168
|
+
*/
|
|
169
|
+
async send(response: ChannelResponse): Promise<void> {
|
|
170
|
+
const chatId = this.extractChatId(response.replyTo);
|
|
171
|
+
if (!chatId) {
|
|
172
|
+
console.error("[TelegramChannel] Cannot send: no chat ID");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const options: TelegramBot.SendMessageOptions = {};
|
|
177
|
+
if (response.content.replyToOriginal) {
|
|
178
|
+
options.reply_to_message_id = parseInt(response.replyTo.messageId, 10);
|
|
305
179
|
}
|
|
180
|
+
if (response.content.options?.parseMode) {
|
|
181
|
+
options.parse_mode = response.content.options.parseMode as "Markdown" | "HTML";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await this.sendLongMessage(chatId, response.content.text, options);
|
|
306
185
|
}
|
|
307
186
|
|
|
308
|
-
|
|
309
|
-
|
|
187
|
+
isConnected(): boolean {
|
|
188
|
+
return this.connected;
|
|
189
|
+
}
|
|
310
190
|
|
|
311
|
-
|
|
191
|
+
// ============================================================
|
|
192
|
+
// Public Helpers
|
|
193
|
+
// ============================================================
|
|
312
194
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Get the underlying TelegramBot instance for advanced operations.
|
|
197
|
+
*/
|
|
198
|
+
getBot(): TelegramBot {
|
|
199
|
+
return this.bot;
|
|
200
|
+
}
|
|
317
201
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
autoStart: true,
|
|
324
|
-
params: {
|
|
325
|
-
allowed_updates: ['message', 'edited_message', 'message_reaction', 'callback_query']
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
this.memory = new ConversationMemory(process.env.CONVERSATIONS_FILE);
|
|
202
|
+
/**
|
|
203
|
+
* Get the conversation memory.
|
|
204
|
+
*/
|
|
205
|
+
getMemory(): ConversationMemory {
|
|
206
|
+
return this.memory;
|
|
330
207
|
}
|
|
331
208
|
|
|
332
209
|
/**
|
|
333
|
-
*
|
|
334
|
-
* Telegram typing indicator lasts ~5 seconds, so refresh every 3 seconds
|
|
210
|
+
* Send a simple text message.
|
|
335
211
|
*/
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
212
|
+
async sendMessage(chatId: number, text: string, options?: TelegramBot.SendMessageOptions): Promise<void> {
|
|
213
|
+
await this.bot.sendMessage(chatId, text, options);
|
|
214
|
+
}
|
|
339
215
|
|
|
340
|
-
|
|
341
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Send typing indicator.
|
|
218
|
+
*/
|
|
219
|
+
async sendTyping(chatId: number): Promise<void> {
|
|
220
|
+
await this.bot.sendChatAction(chatId, "typing");
|
|
221
|
+
}
|
|
342
222
|
|
|
343
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Start continuous typing indicator (until response is ready).
|
|
225
|
+
*/
|
|
226
|
+
startTypingIndicator(chatId: number): void {
|
|
227
|
+
this.stopTypingIndicator(chatId);
|
|
228
|
+
this.bot.sendChatAction(chatId, "typing").catch(() => {});
|
|
344
229
|
const interval = setInterval(() => {
|
|
345
|
-
this.bot.sendChatAction(chatId,
|
|
230
|
+
this.bot.sendChatAction(chatId, "typing").catch(() => {});
|
|
346
231
|
}, 3000);
|
|
347
|
-
|
|
348
232
|
this.typingIntervals.set(chatId, interval);
|
|
349
233
|
}
|
|
350
234
|
|
|
351
235
|
/**
|
|
352
|
-
* Stop
|
|
236
|
+
* Stop typing indicator.
|
|
353
237
|
*/
|
|
354
|
-
|
|
238
|
+
stopTypingIndicator(chatId: number): void {
|
|
355
239
|
const interval = this.typingIntervals.get(chatId);
|
|
356
240
|
if (interval) {
|
|
357
241
|
clearInterval(interval);
|
|
@@ -360,572 +244,280 @@ export class TelegramGLMBot {
|
|
|
360
244
|
}
|
|
361
245
|
|
|
362
246
|
/**
|
|
363
|
-
*
|
|
247
|
+
* Check if user/chat is allowed.
|
|
364
248
|
*/
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
if (text.length <= MAX_LENGTH) {
|
|
369
|
-
await this.bot.sendMessage(chatId, text);
|
|
370
|
-
return;
|
|
249
|
+
isAllowed(userId?: number, chatId?: number): boolean {
|
|
250
|
+
if (!this.config.allowedUsers?.length && !this.config.allowedChats?.length) {
|
|
251
|
+
return true;
|
|
371
252
|
}
|
|
372
253
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
let remaining = text;
|
|
376
|
-
|
|
377
|
-
while (remaining.length > 0) {
|
|
378
|
-
if (remaining.length <= MAX_LENGTH) {
|
|
379
|
-
chunks.push(remaining);
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Try to find a good break point (newline or space)
|
|
384
|
-
let breakPoint = remaining.lastIndexOf('\n', MAX_LENGTH);
|
|
385
|
-
if (breakPoint < 1000) {
|
|
386
|
-
breakPoint = remaining.lastIndexOf(' ', MAX_LENGTH);
|
|
387
|
-
}
|
|
388
|
-
if (breakPoint < 1000) {
|
|
389
|
-
breakPoint = MAX_LENGTH;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
chunks.push(remaining.slice(0, breakPoint));
|
|
393
|
-
remaining = remaining.slice(breakPoint).trim();
|
|
254
|
+
if (this.config.allowedUsers?.length && userId) {
|
|
255
|
+
if (this.config.allowedUsers.includes(userId)) return true;
|
|
394
256
|
}
|
|
395
257
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const prefix = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : '';
|
|
399
|
-
await this.bot.sendMessage(chatId, prefix + chunks[i]);
|
|
258
|
+
if (this.config.allowedChats?.length && chatId) {
|
|
259
|
+
if (this.config.allowedChats.includes(chatId)) return true;
|
|
400
260
|
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async start(): Promise<void> {
|
|
404
|
-
console.log('🤖 GLM Daemon Telegram Bot starting with GLM-4.7...');
|
|
405
|
-
|
|
406
|
-
// Initialize MCP client for external tool access
|
|
407
|
-
mcpClient = new MCPClient();
|
|
408
|
-
try {
|
|
409
|
-
await mcpClient.connect();
|
|
410
|
-
} catch (error) {
|
|
411
|
-
console.error('[MCP] Failed to initialize MCP client:', error);
|
|
412
|
-
// Continue without MCP - local tools still work
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Register all commands from commands/ directory
|
|
416
|
-
registerAllCommands(this.bot, this.memory, TOOLS);
|
|
417
|
-
|
|
418
|
-
// Register commands with Telegram for autocomplete
|
|
419
|
-
await this.bot.setMyCommands([
|
|
420
|
-
{ command: 'start', description: 'Start the bot' },
|
|
421
|
-
{ command: 'help', description: 'Show available commands' },
|
|
422
|
-
{ command: 'status', description: 'Check bot and API status' },
|
|
423
|
-
{ command: 'git', description: 'Check git status and GitHub auth' },
|
|
424
|
-
{ command: 'doppler', description: 'Check Doppler secrets status' },
|
|
425
|
-
{ command: 'logs', description: 'View recent bot logs' },
|
|
426
|
-
{ command: 'clear', description: 'Clear conversation history' },
|
|
427
|
-
{ command: 'tools', description: 'List available tools' },
|
|
428
|
-
{ command: 'cancel', description: 'Stop current task immediately' },
|
|
429
|
-
{ command: 'pause', description: 'Pause execution' },
|
|
430
|
-
{ command: 'resume', description: 'Resume after pause/cancel' },
|
|
431
|
-
{ command: 'quiet', description: 'Hide tool logging' },
|
|
432
|
-
{ command: 'verbose', description: 'Show tool logging' },
|
|
433
|
-
]);
|
|
434
|
-
|
|
435
|
-
// Handle all text messages with GLM-4.7
|
|
436
|
-
this.bot.on('message', async (msg: TelegramBot.Message) => {
|
|
437
|
-
if (!msg.text) return;
|
|
438
|
-
if (msg.text.startsWith('/')) return;
|
|
439
|
-
|
|
440
|
-
const chatId = msg.chat.id;
|
|
441
|
-
const userName = msg.from?.first_name || msg.from?.username || 'User';
|
|
442
|
-
const messageId = msg.message_id;
|
|
443
|
-
|
|
444
|
-
// Check if this is a reply to another message
|
|
445
|
-
const replyContext = msg.reply_to_message ? {
|
|
446
|
-
isReply: true,
|
|
447
|
-
originalMessage: msg.reply_to_message.text || '[non-text message]',
|
|
448
|
-
originalFrom: msg.reply_to_message.from?.first_name || 'User',
|
|
449
|
-
originalMessageId: msg.reply_to_message.message_id
|
|
450
|
-
} : null;
|
|
451
|
-
|
|
452
|
-
if (replyContext) {
|
|
453
|
-
console.log(`[Telegram] ${userName} (replying to ${replyContext.originalFrom}): ${msg.text}`);
|
|
454
|
-
} else {
|
|
455
|
-
console.log(`[Telegram] ${userName}: ${msg.text}`);
|
|
456
|
-
}
|
|
457
261
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
if (replyContext) {
|
|
461
|
-
messageWithContext = `[Replying to ${replyContext.originalFrom}'s message: "${replyContext.originalMessage.slice(0, 200)}${replyContext.originalMessage.length > 200 ? '...' : ''}"]\n\n${msg.text}`;
|
|
462
|
-
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
463
264
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
this.
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
|
|
265
|
+
// ============================================================
|
|
266
|
+
// Message Routing (Internal)
|
|
267
|
+
// ============================================================
|
|
268
|
+
|
|
269
|
+
private setupMessageHandlers(): void {
|
|
270
|
+
// Text messages (skip commands - handled by onText handlers)
|
|
271
|
+
this.bot.on("message", async (msg) => {
|
|
272
|
+
if (!msg.text || msg.text.startsWith("/")) return;
|
|
273
|
+
if (!this.isAllowed(msg.from?.id, msg.chat.id)) return;
|
|
274
|
+
|
|
275
|
+
const message = this.createChannelMessage(msg);
|
|
276
|
+
console.log(`[TelegramChannel] Message from ${message.sender.displayName}: ${message.text.slice(0, 50)}...`);
|
|
277
|
+
|
|
278
|
+
// Route to handler if set
|
|
279
|
+
if (this.messageHandler) {
|
|
280
|
+
try {
|
|
281
|
+
const response = await this.messageHandler(message);
|
|
282
|
+
if (response) {
|
|
283
|
+
await this.send(response);
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error("[TelegramChannel] Handler error:", error);
|
|
287
|
+
await this.sendMessage(msg.chat.id, `Error: ${(error as Error).message}`);
|
|
288
|
+
}
|
|
487
289
|
}
|
|
488
290
|
});
|
|
489
291
|
|
|
490
|
-
//
|
|
491
|
-
this.bot.on(
|
|
492
|
-
console.error('[Telegram] Polling error:', error.message);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
// ====================================================================
|
|
496
|
-
// Message Edit Handler
|
|
497
|
-
// ====================================================================
|
|
498
|
-
this.bot.on('edited_message', async (msg: TelegramBot.Message) => {
|
|
292
|
+
// Edited messages
|
|
293
|
+
this.bot.on("edited_message", async (msg) => {
|
|
499
294
|
if (!msg.text) return;
|
|
295
|
+
if (!this.isAllowed(msg.from?.id, msg.chat.id)) return;
|
|
500
296
|
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const editDate = msg.edit_date || Date.now();
|
|
505
|
-
|
|
506
|
-
console.log(`[Telegram] EDIT ${userName}: ${msg.text.slice(0, 50)}...`);
|
|
507
|
-
|
|
508
|
-
// Update the message in memory
|
|
509
|
-
const updated = this.memory.updateByMessageId(chatId, messageId, msg.text, editDate);
|
|
510
|
-
|
|
511
|
-
if (updated) {
|
|
512
|
-
// Message was found and updated - optionally re-process with AI
|
|
513
|
-
const response = await this.getGLMResponse(
|
|
514
|
-
chatId,
|
|
515
|
-
`[User edited their previous message to: "${msg.text}"]`,
|
|
516
|
-
userName
|
|
517
|
-
);
|
|
518
|
-
this.memory.add(chatId, 'assistant', response);
|
|
519
|
-
await this.sendLongMessage(chatId, `📝 *Edit noted:*\n\n${response}`);
|
|
520
|
-
} else {
|
|
521
|
-
// Message not in memory (too old or new) - just acknowledge
|
|
522
|
-
console.log(`[Telegram] Edit for unknown message ${messageId}`);
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
// ====================================================================
|
|
527
|
-
// Reaction Handler (Likes/Emojis)
|
|
528
|
-
// ====================================================================
|
|
529
|
-
// Note: message_reaction requires Telegram Bot API 5.0+
|
|
530
|
-
// The type definitions may not include this yet, so we use any
|
|
531
|
-
(this.bot as any).on('message_reaction', async (reaction: {
|
|
532
|
-
chat: { id: number };
|
|
533
|
-
message_id: number;
|
|
534
|
-
user: { id: number; first_name?: string };
|
|
535
|
-
old_reaction: Array<{ type: string; emoji?: string; custom_emoji_id?: string }>;
|
|
536
|
-
new_reaction: Array<{ type: string; emoji?: string; custom_emoji_id?: string }>;
|
|
537
|
-
}) => {
|
|
538
|
-
const chatId = reaction.chat.id;
|
|
539
|
-
const messageId = reaction.message_id;
|
|
540
|
-
const userId = reaction.user.id;
|
|
541
|
-
const userName = reaction.user.first_name || 'User';
|
|
542
|
-
|
|
543
|
-
// Extract emojis from reactions
|
|
544
|
-
const newEmojis = reaction.new_reaction
|
|
545
|
-
.filter(r => r.type === 'emoji' && r.emoji)
|
|
546
|
-
.map(r => r.emoji!);
|
|
547
|
-
|
|
548
|
-
const oldEmojis = reaction.old_reaction
|
|
549
|
-
.filter(r => r.type === 'emoji' && r.emoji)
|
|
550
|
-
.map(r => r.emoji!);
|
|
551
|
-
|
|
552
|
-
// Find added reactions
|
|
553
|
-
const added = newEmojis.filter(e => !oldEmojis.includes(e));
|
|
554
|
-
const removed = oldEmojis.filter(e => !newEmojis.includes(e));
|
|
555
|
-
|
|
556
|
-
console.log(`[Telegram] REACTION by ${userName}: +${added.join(',')} -${removed.join(',')} on msg ${messageId}`);
|
|
557
|
-
|
|
558
|
-
// Update memory for each added reaction
|
|
559
|
-
for (const emoji of added) {
|
|
560
|
-
this.memory.addReaction(chatId, messageId, emoji, userId);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Update memory for each removed reaction
|
|
564
|
-
for (const emoji of removed) {
|
|
565
|
-
this.memory.removeReaction(chatId, messageId, emoji, userId);
|
|
566
|
-
}
|
|
297
|
+
const message = this.createChannelMessage(msg);
|
|
298
|
+
// Mark as edited in text
|
|
299
|
+
(message as any).text = `[Edited] ${msg.text}`;
|
|
567
300
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
301
|
+
if (this.messageHandler) {
|
|
302
|
+
try {
|
|
303
|
+
const response = await this.messageHandler(message);
|
|
304
|
+
if (response) {
|
|
305
|
+
await this.send(response);
|
|
306
|
+
}
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.error("[TelegramChannel] Handler error:", error);
|
|
309
|
+
}
|
|
572
310
|
}
|
|
311
|
+
});
|
|
573
312
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
313
|
+
// Reactions
|
|
314
|
+
(this.bot as any).on("message_reaction", async (reaction: any) => {
|
|
315
|
+
const message = {
|
|
316
|
+
messageId: reaction.message_id?.toString(),
|
|
317
|
+
channelId: this.id,
|
|
318
|
+
timestamp: new Date(),
|
|
319
|
+
sender: {
|
|
320
|
+
id: reaction.user?.id?.toString(),
|
|
321
|
+
displayName: reaction.user?.first_name || "User",
|
|
322
|
+
},
|
|
323
|
+
text: "",
|
|
324
|
+
context: { isDM: reaction.chat?.type === "private" },
|
|
325
|
+
metadata: { type: "reaction", reaction: reaction.new_reaction },
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (this.messageHandler) {
|
|
329
|
+
await this.messageHandler(message as any);
|
|
577
330
|
}
|
|
578
331
|
});
|
|
579
332
|
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
// ====================================================================
|
|
583
|
-
this.bot.on('callback_query', async (query: TelegramBot.CallbackQuery) => {
|
|
333
|
+
// Callback queries (button clicks)
|
|
334
|
+
this.bot.on("callback_query", async (query) => {
|
|
584
335
|
const chatId = query.message?.chat.id;
|
|
585
|
-
|
|
586
|
-
const data = query.data;
|
|
587
|
-
const userName = query.from.first_name || 'User';
|
|
588
|
-
|
|
589
|
-
if (!chatId || !data) {
|
|
336
|
+
if (!chatId || !query.data) {
|
|
590
337
|
await this.bot.answerCallbackQuery(query.id);
|
|
591
338
|
return;
|
|
592
339
|
}
|
|
593
340
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
case 'approve': {
|
|
616
|
-
// Approval action (useful for workflows)
|
|
617
|
-
await this.bot.sendMessage(chatId, `✅ Approved by ${userName}`);
|
|
618
|
-
// Could trigger follow-up actions here
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
case 'reject': {
|
|
623
|
-
// Rejection action
|
|
624
|
-
await this.bot.sendMessage(chatId, `❌ Rejected by ${userName}`);
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
case 'cancel': {
|
|
629
|
-
// Cancel current task
|
|
630
|
-
await this.bot.sendMessage(chatId, `🛑 Task cancelled by ${userName}`);
|
|
631
|
-
break;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
default: {
|
|
635
|
-
// Unknown callback - let AI handle it
|
|
636
|
-
const response = await this.getGLMResponse(
|
|
637
|
-
chatId,
|
|
638
|
-
`[User clicked button: "${data}". Respond appropriately.]`,
|
|
639
|
-
userName
|
|
640
|
-
);
|
|
641
|
-
await this.bot.sendMessage(chatId, response);
|
|
341
|
+
const message = {
|
|
342
|
+
messageId: query.id,
|
|
343
|
+
channelId: this.id,
|
|
344
|
+
timestamp: new Date(),
|
|
345
|
+
sender: {
|
|
346
|
+
id: query.from?.id?.toString(),
|
|
347
|
+
displayName: query.from?.first_name || "User",
|
|
348
|
+
},
|
|
349
|
+
text: query.data,
|
|
350
|
+
context: { isDM: query.message?.chat.type === "private" },
|
|
351
|
+
metadata: { type: "callback" },
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (this.messageHandler) {
|
|
355
|
+
try {
|
|
356
|
+
const response = await this.messageHandler(message as any);
|
|
357
|
+
if (response) {
|
|
358
|
+
await this.send(response);
|
|
359
|
+
}
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.error("[TelegramChannel] Callback handler error:", error);
|
|
642
362
|
}
|
|
643
363
|
}
|
|
644
364
|
|
|
645
|
-
// Always acknowledge the callback to remove loading state
|
|
646
365
|
await this.bot.answerCallbackQuery(query.id);
|
|
647
366
|
});
|
|
648
367
|
|
|
649
|
-
|
|
368
|
+
// Polling errors
|
|
369
|
+
this.bot.on("polling_error", (error) => {
|
|
370
|
+
console.error("[TelegramChannel] Polling error:", error.message);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
650
373
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Register Telegram command menu
|
|
376
|
+
*/
|
|
377
|
+
private async registerCommands(): Promise<void> {
|
|
378
|
+
await this.bot.setMyCommands([
|
|
379
|
+
{ command: "start", description: "Start the bot" },
|
|
380
|
+
{ command: "help", description: "Show available commands" },
|
|
381
|
+
{ command: "status", description: "Check bot status" },
|
|
382
|
+
{ command: "git", description: "Check git status" },
|
|
383
|
+
{ command: "doppler", description: "Check Doppler secrets" },
|
|
384
|
+
{ command: "logs", description: "View recent logs" },
|
|
385
|
+
{ command: "clear", description: "Clear conversation history" },
|
|
386
|
+
{ command: "tools", description: "List available tools" },
|
|
387
|
+
{ command: "restart", description: "Restart the bot service" },
|
|
388
|
+
{ command: "cancel", description: "Stop current task" },
|
|
389
|
+
{ command: "quiet", description: "Hide tool logging" },
|
|
390
|
+
{ command: "verbose", description: "Show tool logging" },
|
|
391
|
+
]);
|
|
658
392
|
}
|
|
659
393
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
394
|
+
/**
|
|
395
|
+
* Normalize Telegram message to ChannelMessage format.
|
|
396
|
+
*/
|
|
397
|
+
private createChannelMessage(msg: TelegramBot.Message): ChannelMessage {
|
|
398
|
+
const sender: MessageSender = {
|
|
399
|
+
id: msg.from?.id?.toString() || msg.chat.id.toString(),
|
|
400
|
+
username: msg.from?.username,
|
|
401
|
+
displayName: msg.from?.first_name || msg.from?.username || "User",
|
|
402
|
+
isBot: msg.from?.is_bot || false,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const context: MessageContext = {
|
|
406
|
+
isDM: msg.chat.type === "private",
|
|
407
|
+
groupName: msg.chat.type !== "private" ? msg.chat.title : undefined,
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Build reply context
|
|
411
|
+
let replyText: string | undefined;
|
|
412
|
+
if (msg.reply_to_message?.text) {
|
|
413
|
+
const replyFrom = msg.reply_to_message.from?.first_name || "User";
|
|
414
|
+
replyText = `[Replying to ${replyFrom}: "${msg.reply_to_message.text.slice(0, 100)}"]`;
|
|
664
415
|
}
|
|
665
416
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const history = this.memory.get(chatId);
|
|
678
|
-
const messages: Array<{ role: string; content?: string; tool_calls?: unknown[]; name?: string }> = [
|
|
679
|
-
{ role: 'system', content: systemContent },
|
|
680
|
-
...history,
|
|
681
|
-
{ role: 'user', content: userMessage }
|
|
682
|
-
];
|
|
683
|
-
|
|
684
|
-
try {
|
|
685
|
-
console.log(`🔄 Calling GLM-4.7 API for: ${userMessage} (${history.length} history)`);
|
|
686
|
-
|
|
687
|
-
// Loop to handle tool calls
|
|
688
|
-
let iterations = 0;
|
|
689
|
-
const maxIterations = 50; // Allow more tool calls for complex tasks
|
|
690
|
-
|
|
691
|
-
while (iterations < maxIterations) {
|
|
692
|
-
iterations++;
|
|
693
|
-
|
|
694
|
-
const response = await fetchWithRetry(ZAI_API_ENDPOINT, {
|
|
695
|
-
method: 'POST',
|
|
696
|
-
headers: {
|
|
697
|
-
'Content-Type': 'application/json',
|
|
698
|
-
'Authorization': `Bearer ${apiKey}`
|
|
699
|
-
},
|
|
700
|
-
body: JSON.stringify({
|
|
701
|
-
model: 'glm-4.7',
|
|
702
|
-
messages,
|
|
703
|
-
tools: getGLMTools(),
|
|
704
|
-
temperature,
|
|
705
|
-
max_tokens: maxTokens
|
|
706
|
-
})
|
|
707
|
-
}, 3, 1000);
|
|
708
|
-
|
|
709
|
-
const data = await response.json() as {
|
|
710
|
-
choices?: Array<{
|
|
711
|
-
message?: {
|
|
712
|
-
content?: string;
|
|
713
|
-
tool_calls?: Array<{
|
|
714
|
-
id: string;
|
|
715
|
-
function: { name: string; arguments: string };
|
|
716
|
-
}>;
|
|
717
|
-
}
|
|
718
|
-
}>
|
|
719
|
-
};
|
|
720
|
-
|
|
721
|
-
const message = data.choices?.[0]?.message;
|
|
722
|
-
if (!message) {
|
|
723
|
-
console.error('❌ Unexpected API response format:', data);
|
|
724
|
-
return '❌ Unexpected response from AI API.';
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Check if LLM wants to call tools
|
|
728
|
-
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
729
|
-
console.log(`🔧 LLM calling ${message.tool_calls.length} tool(s)`);
|
|
730
|
-
|
|
731
|
-
// Add assistant message with tool calls to history
|
|
732
|
-
messages.push({
|
|
733
|
-
role: 'assistant',
|
|
734
|
-
tool_calls: message.tool_calls
|
|
735
|
-
});
|
|
736
|
-
|
|
737
|
-
// Check quiet mode for Telegram messages
|
|
738
|
-
const quiet = isQuiet();
|
|
739
|
-
|
|
740
|
-
// Execute each tool and add results
|
|
741
|
-
for (const toolCall of message.tool_calls) {
|
|
742
|
-
const toolName = toolCall.function.name;
|
|
743
|
-
const args = JSON.parse(toolCall.function.arguments);
|
|
744
|
-
|
|
745
|
-
console.log(` → ${toolName}(${JSON.stringify(args)})`);
|
|
746
|
-
|
|
747
|
-
// Send tool call notification to Telegram (skip if quiet)
|
|
748
|
-
if (!quiet) {
|
|
749
|
-
// Sanitize to prevent entity parsing errors - strip Markdown special chars
|
|
750
|
-
const sanitizeForTelegram = (str: string): string => {
|
|
751
|
-
return str
|
|
752
|
-
.replace(/[_*[\]()~`>#+=|{}.!-]/g, '') // Remove Markdown special chars
|
|
753
|
-
.replace(/\\/g, '') // Remove backslashes
|
|
754
|
-
.slice(0, 100); // Truncate to prevent long messages
|
|
755
|
-
};
|
|
756
|
-
const argsPreview = Object.keys(args).length > 0
|
|
757
|
-
? sanitizeForTelegram(JSON.stringify(args))
|
|
758
|
-
: '';
|
|
759
|
-
await this.bot.sendMessage(chatId, `🔧 ${toolName}${argsPreview ? ' ' + argsPreview : ''}`);
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Execute tool locally
|
|
763
|
-
const result = await executeTool(toolName, args);
|
|
764
|
-
|
|
765
|
-
console.log(` ← ${result.slice(0, 100)}...`);
|
|
766
|
-
|
|
767
|
-
// Send result to Telegram (skip if quiet)
|
|
768
|
-
if (!quiet) {
|
|
769
|
-
const sanitizedResult = result
|
|
770
|
-
.replace(/[_*[\]()~`>#+=|{}.!-]/g, '')
|
|
771
|
-
.replace(/\\/g, '');
|
|
772
|
-
const resultPreview = sanitizedResult.length > 500 ? sanitizedResult.slice(0, 500) + ' [truncated]' : sanitizedResult;
|
|
773
|
-
await this.bot.sendMessage(chatId, `📥 ${resultPreview}`);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
// Add tool result to messages
|
|
777
|
-
messages.push({
|
|
778
|
-
role: 'tool',
|
|
779
|
-
name: toolName,
|
|
780
|
-
content: result
|
|
781
|
-
});
|
|
417
|
+
return {
|
|
418
|
+
messageId: msg.message_id.toString(),
|
|
419
|
+
channelId: this.id,
|
|
420
|
+
timestamp: new Date(msg.date * 1000),
|
|
421
|
+
sender,
|
|
422
|
+
text: replyText ? `${replyText}\n\n${msg.text || ""}` : (msg.text || ""),
|
|
423
|
+
context,
|
|
424
|
+
replyTo: msg.reply_to_message
|
|
425
|
+
? {
|
|
426
|
+
messageId: msg.reply_to_message.message_id.toString(),
|
|
427
|
+
channelId: this.id,
|
|
782
428
|
}
|
|
429
|
+
: undefined,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
783
432
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
433
|
+
private extractChatId(replyTo: { messageId: string; channelId: ChannelId }): number | null {
|
|
434
|
+
const accountId = replyTo.channelId.accountId;
|
|
435
|
+
const parsed = parseInt(accountId, 10);
|
|
436
|
+
if (!isNaN(parsed)) return parsed;
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
787
439
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
440
|
+
/**
|
|
441
|
+
* Send long message by splitting into chunks.
|
|
442
|
+
*/
|
|
443
|
+
private async sendLongMessage(
|
|
444
|
+
chatId: number,
|
|
445
|
+
text: string,
|
|
446
|
+
options?: TelegramBot.SendMessageOptions
|
|
447
|
+
): Promise<void> {
|
|
448
|
+
const MAX_LENGTH = 4000;
|
|
793
449
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
450
|
+
if (text.length <= MAX_LENGTH) {
|
|
451
|
+
await this.bot.sendMessage(chatId, text, options);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
797
454
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
console.error('❌ Error calling GLM-4.7:', (error as Error).message);
|
|
455
|
+
const chunks: string[] = [];
|
|
456
|
+
let remaining = text;
|
|
801
457
|
|
|
802
|
-
|
|
803
|
-
|
|
458
|
+
while (remaining.length > 0) {
|
|
459
|
+
if (remaining.length <= MAX_LENGTH) {
|
|
460
|
+
chunks.push(remaining);
|
|
461
|
+
break;
|
|
804
462
|
}
|
|
805
|
-
return `❌ Error: ${(error as Error).message}`;
|
|
806
|
-
} finally {
|
|
807
|
-
// Always stop typing indicator when done
|
|
808
|
-
this.stopTypingIndicator(chatId);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
463
|
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
chatId,
|
|
816
|
-
'✅ GLM Daemon Telegram Bot is NOW ONLINE!\n\n' +
|
|
817
|
-
'🎉 Connected from seed-node-prod!\n\n' +
|
|
818
|
-
`🧠 AI: ${apiKey ? 'GLM-4.7 via Z.AI' : 'Not configured'}\n\n` +
|
|
819
|
-
'Commands:\n' +
|
|
820
|
-
'/start - Show welcome message\n' +
|
|
821
|
-
'/status - Check API status\n' +
|
|
822
|
-
'/git - Git & GitHub status\n' +
|
|
823
|
-
'/doppler - Doppler config\n' +
|
|
824
|
-
'/help - Show all commands\n' +
|
|
825
|
-
'Any message - Chat with GLM-4.7 AI'
|
|
826
|
-
);
|
|
827
|
-
console.log(`✅ Test message sent to chat ${chatId}`);
|
|
828
|
-
}
|
|
464
|
+
let breakPoint = remaining.lastIndexOf("\n", MAX_LENGTH);
|
|
465
|
+
if (breakPoint < 1000) breakPoint = remaining.lastIndexOf(" ", MAX_LENGTH);
|
|
466
|
+
if (breakPoint < 1000) breakPoint = MAX_LENGTH;
|
|
829
467
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
468
|
+
chunks.push(remaining.slice(0, breakPoint));
|
|
469
|
+
remaining = remaining.slice(breakPoint).trim();
|
|
470
|
+
}
|
|
834
471
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
*/
|
|
839
|
-
async checkCrashAndReport(chatId: number): Promise<void> {
|
|
840
|
-
try {
|
|
841
|
-
// Get previous boot/session logs to check for crash
|
|
842
|
-
const crashLog = execSync(
|
|
843
|
-
'journalctl -u telegram-bot -b -1 -n 50 --no-pager 2>/dev/null || journalctl -u telegram-bot --since "5 minutes ago" -n 50 --no-pager 2>/dev/null',
|
|
844
|
-
{ encoding: 'utf-8', timeout: 5000 }
|
|
845
|
-
).trim();
|
|
846
|
-
|
|
847
|
-
// Look for crash indicators
|
|
848
|
-
const crashIndicators = ['Error:', 'error', 'crashed', 'killed', 'ETELEGRAM', '400 Bad Request', 'exception'];
|
|
849
|
-
const hasCrash = crashLog && crashIndicators.some(ind => crashLog.toLowerCase().includes(ind.toLowerCase()));
|
|
850
|
-
|
|
851
|
-
if (hasCrash) {
|
|
852
|
-
// Crash detected - analyze and report
|
|
853
|
-
console.log('🔍 Crash detected, analyzing...');
|
|
854
|
-
await this.bot.sendMessage(chatId, '🔄 I detected a crash in my previous session. Analyzing...');
|
|
855
|
-
|
|
856
|
-
// Extract error lines
|
|
857
|
-
const errorLines = crashLog.split('\n')
|
|
858
|
-
.filter(line => crashIndicators.some(ind => line.toLowerCase().includes(ind.toLowerCase())))
|
|
859
|
-
.slice(-10)
|
|
860
|
-
.join('\n');
|
|
861
|
-
|
|
862
|
-
if (errorLines) {
|
|
863
|
-
// Ask LLM to diagnose
|
|
864
|
-
const diagnosis = await this.getGLMResponse(
|
|
865
|
-
chatId,
|
|
866
|
-
`Analyze this crash log and tell me what went wrong in one short paragraph, then suggest a fix:
|
|
867
|
-
|
|
868
|
-
\`\`\`
|
|
869
|
-
${errorLines.slice(0, 1000)}
|
|
870
|
-
\`\`\`
|
|
871
|
-
|
|
872
|
-
Format: "Issue: [what happened]. Fix: [how to fix it]"`,
|
|
873
|
-
'CrashAnalyzer'
|
|
874
|
-
);
|
|
875
|
-
|
|
876
|
-
await this.sendLongMessage(chatId, `🏥 Crash Diagnosis:\n\n${diagnosis}`);
|
|
877
|
-
}
|
|
878
|
-
} else {
|
|
879
|
-
// Normal start - no crash detected
|
|
880
|
-
await this.bot.sendMessage(
|
|
881
|
-
chatId,
|
|
882
|
-
'✅ Bot started normally.\n\n' +
|
|
883
|
-
'Previous session ended cleanly. Ready to assist!'
|
|
884
|
-
);
|
|
885
|
-
}
|
|
886
|
-
} catch (e) {
|
|
887
|
-
// If we can't check logs, still notify of normal start
|
|
888
|
-
console.log('Crash check skipped:', (e as Error).message);
|
|
889
|
-
await this.bot.sendMessage(
|
|
890
|
-
chatId,
|
|
891
|
-
'✅ Bot started fresh.\n\n' +
|
|
892
|
-
'No previous session data available. Ready to assist!'
|
|
893
|
-
);
|
|
472
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
473
|
+
const prefix = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
|
|
474
|
+
await this.bot.sendMessage(chatId, prefix + chunks[i], options);
|
|
894
475
|
}
|
|
895
476
|
}
|
|
896
477
|
}
|
|
897
478
|
|
|
898
|
-
//
|
|
479
|
+
// ============================================================
|
|
480
|
+
// FACTORY
|
|
481
|
+
// ============================================================
|
|
482
|
+
|
|
483
|
+
export function createTelegramChannel(config: TelegramConfig): TelegramChannel {
|
|
484
|
+
return new TelegramChannel(config);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ============================================================
|
|
488
|
+
// MAIN ENTRY POINT (for standalone testing)
|
|
489
|
+
// ============================================================
|
|
490
|
+
|
|
899
491
|
async function main() {
|
|
900
|
-
|
|
901
|
-
console.error('❌ TELEGRAM_BOT_TOKEN not found in Doppler secrets');
|
|
902
|
-
console.error('Make sure to set in your seed/prd Doppler project');
|
|
903
|
-
process.exit(1);
|
|
904
|
-
}
|
|
492
|
+
const config = createTelegramConfigFromEnv();
|
|
905
493
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
if (!store.get('glm-daemon-system')) {
|
|
910
|
-
console.log('📝 Seeding prompts...');
|
|
911
|
-
seedPrompts(promptsFile);
|
|
494
|
+
if (!config.botToken) {
|
|
495
|
+
console.error("TELEGRAM_BOT_TOKEN not found in environment");
|
|
496
|
+
process.exit(1);
|
|
912
497
|
}
|
|
913
498
|
|
|
914
|
-
|
|
915
|
-
console.log(`🤖 Bot name: @SimulationapiBot`);
|
|
499
|
+
const channel = createTelegramChannel(config);
|
|
916
500
|
|
|
917
|
-
|
|
918
|
-
|
|
501
|
+
// Example: Echo handler for testing
|
|
502
|
+
channel.onMessage(async (msg) => {
|
|
503
|
+
console.log(`Received: ${msg.text}`);
|
|
504
|
+
return {
|
|
505
|
+
content: { text: `Echo: ${msg.text}` },
|
|
506
|
+
replyTo: { messageId: msg.messageId, channelId: msg.channelId },
|
|
507
|
+
};
|
|
508
|
+
});
|
|
919
509
|
|
|
920
|
-
process.on(
|
|
921
|
-
await
|
|
510
|
+
process.on("SIGINT", async () => {
|
|
511
|
+
await channel.stop();
|
|
922
512
|
process.exit(0);
|
|
923
513
|
});
|
|
924
514
|
|
|
925
|
-
process.on(
|
|
926
|
-
await
|
|
515
|
+
process.on("SIGTERM", async () => {
|
|
516
|
+
await channel.stop();
|
|
927
517
|
process.exit(0);
|
|
928
518
|
});
|
|
519
|
+
|
|
520
|
+
await channel.start();
|
|
929
521
|
}
|
|
930
522
|
|
|
931
523
|
main().catch(console.error);
|