foliko 1.0.75 → 1.0.76

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.
Files changed (88) hide show
  1. package/.claude/settings.local.json +159 -157
  2. package/cli/bin/foliko.js +12 -12
  3. package/cli/src/commands/chat.js +143 -143
  4. package/cli/src/commands/list.js +93 -93
  5. package/cli/src/index.js +75 -75
  6. package/cli/src/ui/chat-ui.js +201 -201
  7. package/cli/src/utils/ansi.js +40 -40
  8. package/cli/src/utils/markdown.js +292 -292
  9. package/examples/ambient-example.js +194 -194
  10. package/examples/basic.js +115 -115
  11. package/examples/bootstrap.js +121 -121
  12. package/examples/mcp-example.js +56 -56
  13. package/examples/skill-example.js +49 -49
  14. package/examples/test-chat.js +137 -137
  15. package/examples/test-mcp.js +85 -85
  16. package/examples/test-reload.js +59 -59
  17. package/examples/test-telegram.js +50 -50
  18. package/examples/test-tg-bot.js +45 -45
  19. package/examples/test-tg-simple.js +47 -47
  20. package/examples/test-tg.js +62 -62
  21. package/examples/test-think.js +43 -43
  22. package/examples/test-web-plugin.js +103 -103
  23. package/examples/test-weixin-feishu.js +103 -103
  24. package/examples/workflow.js +158 -158
  25. package/package.json +1 -1
  26. package/plugins/ai-plugin.js +102 -102
  27. package/plugins/ambient-agent/EventWatcher.js +113 -113
  28. package/plugins/ambient-agent/ExplorerLoop.js +640 -640
  29. package/plugins/ambient-agent/GoalManager.js +197 -197
  30. package/plugins/ambient-agent/Reflector.js +95 -95
  31. package/plugins/ambient-agent/StateStore.js +90 -90
  32. package/plugins/ambient-agent/constants.js +101 -101
  33. package/plugins/ambient-agent/index.js +579 -579
  34. package/plugins/audit-plugin.js +187 -187
  35. package/plugins/default-plugins.js +662 -662
  36. package/plugins/email/constants.js +64 -64
  37. package/plugins/email/handlers.js +461 -461
  38. package/plugins/email/index.js +278 -278
  39. package/plugins/email/monitor.js +269 -269
  40. package/plugins/email/parser.js +138 -138
  41. package/plugins/email/reply.js +151 -151
  42. package/plugins/email/utils.js +124 -124
  43. package/plugins/feishu-plugin.js +481 -481
  44. package/plugins/file-system-plugin.js +826 -826
  45. package/plugins/install-plugin.js +199 -199
  46. package/plugins/python-executor-plugin.js +367 -367
  47. package/plugins/python-plugin-loader.js +481 -481
  48. package/plugins/rules-plugin.js +294 -294
  49. package/plugins/scheduler-plugin.js +691 -691
  50. package/plugins/session-plugin.js +369 -369
  51. package/plugins/shell-executor-plugin.js +197 -197
  52. package/plugins/storage-plugin.js +240 -240
  53. package/plugins/subagent-plugin.js +845 -845
  54. package/plugins/telegram-plugin.js +482 -482
  55. package/plugins/think-plugin.js +345 -345
  56. package/plugins/tools-plugin.js +196 -196
  57. package/plugins/web-plugin.js +606 -606
  58. package/plugins/weixin-plugin.js +545 -545
  59. package/src/capabilities/index.js +11 -11
  60. package/src/capabilities/skill-manager.js +609 -609
  61. package/src/capabilities/workflow-engine.js +1109 -1109
  62. package/src/core/agent-chat.js +882 -882
  63. package/src/core/agent.js +892 -892
  64. package/src/core/framework.js +465 -465
  65. package/src/core/index.js +19 -19
  66. package/src/core/plugin-base.js +219 -219
  67. package/src/core/plugin-manager.js +863 -863
  68. package/src/core/provider.js +114 -114
  69. package/src/core/sub-agent-config.js +264 -264
  70. package/src/core/system-prompt-builder.js +120 -120
  71. package/src/core/tool-registry.js +517 -517
  72. package/src/core/tool-router.js +297 -297
  73. package/src/executors/executor-base.js +58 -58
  74. package/src/executors/mcp-executor.js +741 -741
  75. package/src/index.js +25 -25
  76. package/src/utils/circuit-breaker.js +301 -301
  77. package/src/utils/error-boundary.js +363 -363
  78. package/src/utils/error.js +374 -374
  79. package/src/utils/event-emitter.js +97 -97
  80. package/src/utils/id.js +133 -133
  81. package/src/utils/index.js +217 -217
  82. package/src/utils/logger.js +181 -181
  83. package/src/utils/plugin-helpers.js +90 -90
  84. package/src/utils/retry.js +122 -122
  85. package/src/utils/sandbox.js +292 -292
  86. package/test/tool-registry-validation.test.js +218 -218
  87. package/website/script.js +136 -136
  88. package/foliko-1.0.75.tgz +0 -0
@@ -1,482 +1,482 @@
1
- /**
2
- * Telegram 插件
3
- * 支持绑定主Agent进行持续对话
4
- *
5
- * 配置:
6
- * - botToken: Telegram Bot Token (必需)
7
- * - allowedChats: 允许的聊天ID数组
8
- * - groupMode: 是否启用群组模式
9
- * - prefix: 命令前缀
10
- */
11
-
12
- const { Plugin } = require('../src/core/plugin-base')
13
- const { logger } = require('../src/utils/logger')
14
- const log = logger.child('Telegram')
15
- const { z } = require('zod')
16
-
17
- // 转义 MarkdownV2 特殊字符
18
- function escapeMarkdown(text) {
19
- if (!text) return ''
20
- return text.replace(/([\_*\[\]()~`>#+\-=|{}.!])/g, '\\$1')
21
- }
22
-
23
- class TelegramPlugin extends Plugin {
24
- constructor(config = {}) {
25
- super()
26
- this.name = 'telegram'
27
- this.version = '2.1.0'
28
- this.description = 'Telegram 对话插件,绑定主Agent进行持续对话'
29
- this.priority = 80
30
- this.enabled = false
31
- this.systemPrompt = `你是一个有帮助的AI助手。回复内容不要使用markdown格式文本。
32
-
33
- **重要:** 子Agent 匹配规则必须遵守:
34
- - 根据【子 Agent 匹配表】,将任务委托给最匹配的子Agent处理
35
- - 使用 subagent_call 工具并指定 agentName 来委托任务
36
- - 只有当没有匹配的子Agent时,才直接调用工具
37
-
38
- **命令执行规范:**
39
- - 执行 npx skills add / npx skills remove / npx skills list 命令时,必须自动添加参数:-a openclaw -y
40
- - 禁止添加 -g 参数
41
- - 例如:npx skills add xxx 应该执行为 npx skills add xxx -a openclaw -y`
42
-
43
- this.config = {
44
- botToken: config.botToken || process.env.TELEGRAM_BOT_TOKEN,
45
- allowedChats: config.allowedChats || [],
46
- groupMode: config.groupMode || false,
47
- prefix: config.prefix || '/'
48
- }
49
-
50
- this._framework = null
51
- this._bot = null
52
- this._sessionPlugin = null
53
- this._sessionDeleteHandler = null
54
- this._sessionAgents = new Map()
55
- }
56
-
57
- install(framework) {
58
- this._framework = framework
59
- return this
60
- }
61
-
62
- start(framework) {
63
- if (!this.config.botToken) {
64
- log.warn(' No bot token. Set TELEGRAM_BOT_TOKEN env var.')
65
- return this
66
- }
67
-
68
- this._framework = framework
69
- this._sessionPlugin = framework.pluginManager.get('session')
70
-
71
- if (this._sessionPlugin) {
72
- this._sessionDeleteHandler = (sessionId) => {
73
- if (sessionId.startsWith('telegram_')) {
74
- log.info(` Session deleted: ${sessionId}`)
75
- }
76
- }
77
- this._sessionPlugin.on('session:deleted', this._sessionDeleteHandler)
78
- }
79
-
80
- this._initBot()
81
- return this
82
- }
83
-
84
- _initBot() {
85
- try {
86
- const TelegramBot = require('node-telegram-bot-api')
87
- this._bot = new TelegramBot(this.config.botToken, { polling: true })
88
-
89
- log.info(' Bot started successfully')
90
-
91
- this._bot.setMyCommands([
92
- { command: 'start', description: '显示帮助信息' },
93
- { command: 'clear', description: '清除对话历史' },
94
- { command: 'history', description: '查看对话状态' }
95
- ]).catch((err) => {
96
- log.error(' Failed to register commands:', err.message)
97
- })
98
-
99
- this._bot.on('message', (msg) => this._handleMessage(msg))
100
- this._bot.on('edit_message', (msg) => this._handleEdit(msg))
101
- this._bot.on('callback_query', (query) => this._handleCallback(query))
102
- this._bot.on('polling_error', (err) => log.error(' Polling error:', err.message))
103
- this._bot.on('error', (err) => log.error(' Bot error:', err.message))
104
-
105
- if (this._framework) {
106
- this._framework.on('agent:created', (agent) => {
107
- log.info(' New agent created:', agent.name)
108
- })
109
-
110
- // 监听统一通知事件
111
- this._framework.on('notification', async (data) => {
112
- await this._handleNotification(data)
113
- })
114
-
115
- // 监听 webhook 事件
116
- this._framework.on('webhook:received', async (data) => {
117
- await this._handleWebhookNotification(data)
118
- })
119
- }
120
- } catch (err) {
121
- log.error(' Failed to initialize bot:', err.message)
122
- }
123
- }
124
-
125
- /**
126
- * 处理统一通知
127
- */
128
- async _handleNotification(data) {
129
- const { title, message, source, level } = data
130
-
131
- if (!this._bot) {
132
- log.warn(' Bot not ready, cannot send notification')
133
- return
134
- }
135
-
136
- // 确定 chatId
137
- let chatId = null
138
- let effectiveSessionId = data.sessionId
139
-
140
- // 如果没有 sessionId,尝试从执行上下文获取
141
- if (!effectiveSessionId) {
142
- const ctx = this._framework.getExecutionContext()
143
- if (ctx?.sessionId) {
144
- effectiveSessionId = ctx.sessionId
145
- }
146
- }
147
-
148
- if (effectiveSessionId && effectiveSessionId.startsWith('telegram_')) {
149
- chatId = effectiveSessionId.replace('telegram_', '')
150
- } else if (this._sessionPlugin) {
151
- // 获取最近的 telegram 会话
152
- const sessions = this._sessionPlugin.listSessions()
153
- .filter(s => s.id.startsWith('telegram_'))
154
- .sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime())
155
- if (sessions.length > 0) {
156
- chatId = sessions[0].id.replace('telegram_', '')
157
- }
158
- }
159
-
160
- if (!chatId) {
161
- log.warn(' No telegram session found for notification')
162
- return
163
- }
164
-
165
- // 格式化通知消息
166
- const levelEmoji = {
167
- info: 'ℹ️',
168
- warning: '⚠️',
169
- success: '✅',
170
- error: '❌'
171
- }
172
- const emoji = levelEmoji[level] || 'ℹ️'
173
- const notificationText = `${emoji} [${source}] ${title}\n\n${message}`
174
-
175
- try {
176
- await this._bot.sendMessage(chatId, notificationText)
177
- log.info(` Notification sent to chat ${chatId}`)
178
- } catch (err) {
179
- log.error(` Failed to send notification:`, err.message)
180
- }
181
- }
182
-
183
- /**
184
- * 处理 webhook 通知
185
- */
186
- async _handleWebhookNotification(data) {
187
- const { data: webhookData, response, sessionId } = data
188
-
189
- if (!sessionId || !sessionId.startsWith('telegram_')) {
190
- return
191
- }
192
-
193
- const chatId = sessionId.replace('telegram_', '')
194
- const notificationText = `📥 [Webhook 接收]\n\n路径: ${webhookData.path}\n方法: ${webhookData.method}\n\n处理结果: ${response || '处理中...'}`
195
-
196
- try {
197
- await this._bot.sendMessage(chatId, notificationText)
198
- log.info(` Webhook notification sent to chat ${chatId}`)
199
- } catch (err) {
200
- log.error(` Failed to send webhook notification:`, err.message)
201
- }
202
- }
203
-
204
- _getMainAgent() {
205
- if (this._framework._mainAgent) return this._framework._mainAgent
206
- const agents = this._framework._agents || []
207
- return agents.length > 0 ? agents[agents.length - 1] : null
208
- }
209
-
210
- _getSessionAgent(chatId) {
211
- if (this._sessionAgents.has(chatId)) {
212
- return { agent: this._sessionAgents.get(chatId), sessionId: `telegram_${chatId}` }
213
- }
214
-
215
- const agent = this._framework.createSessionAgent(`telegram_${chatId}`, {
216
- systemPrompt: this.systemPrompt,
217
- sharedPrompt: `工作目录: {{WORK_DIR}}`,
218
- metadata: { WORK_DIR: process.cwd() }
219
- })
220
- this._sessionAgents.set(chatId, agent)
221
-
222
- if (this._sessionPlugin) {
223
- this._sessionPlugin.getOrCreateSession(`telegram_${chatId}`, {
224
- metadata: { platform: 'telegram', chatId }
225
- })
226
- }
227
-
228
- return { agent, sessionId: `telegram_${chatId}` }
229
- }
230
-
231
- async _handleMessage(msg) {
232
- if (!msg || !msg.chat) return
233
- const chatId = msg.chat.id.toString()
234
-
235
- if (msg.photo) { await this._handlePhoto(msg); return }
236
- if (msg.document) { await this._handleDocument(msg); return }
237
- if (!msg.text) return
238
-
239
- const text = msg.text.trim()
240
- if (text.startsWith(this.config.prefix)) {
241
- await this._handleCommand(msg)
242
- return
243
- }
244
-
245
- if (msg.chat.type === 'group' || msg.chat.type === 'supergroup') {
246
- if (!this.config.groupMode) return
247
- }
248
-
249
- if (!this._checkPermission(chatId)) {
250
- await this._sendMessage(chatId, '抱歉,您没有权限使用此 Bot。', msg.message_id)
251
- return
252
- }
253
-
254
- await this._processChat(chatId, text, msg.message_id)
255
- }
256
-
257
- async _handleCommand(msg) {
258
- const chatId = msg.chat.id.toString()
259
- const text = msg.text.trim()
260
- const parts = text.split(' ')
261
- const command = parts[0].substring(1)
262
- const args = parts.slice(1).join(' ')
263
-
264
- switch (command.toLowerCase()) {
265
- case 'start':
266
- await this._sendMessage(chatId,
267
- '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/start - 显示帮助\n/clear - 清除对话历史\n/history - 查看历史消息数',
268
- msg.message_id)
269
- break
270
- case 'clear':
271
- this._clearSession(chatId)
272
- await this._sendMessage(chatId, '✅ 对话历史已清除', msg.message_id)
273
- break
274
- case 'history':
275
- if (this._sessionPlugin) {
276
- const session = this._sessionPlugin.getSession(`telegram_${chatId}`)
277
- const sessions = this._sessionPlugin.listSessions().filter(s => s.id.startsWith('telegram_'))
278
- await this._sendMessage(chatId,
279
- `📊 当前状态\n\n会话数:${sessions.length}\n历史消息:${session?.messages?.length || 0}\n最后活跃:${session?.lastActive?.toLocaleString() || '无'}`,
280
- msg.message_id)
281
- } else {
282
- await this._sendMessage(chatId, '❌ 会话服务不可用', msg.message_id)
283
- }
284
- break
285
- default:
286
- await this._processChat(chatId, text, msg.message_id)
287
- }
288
- }
289
-
290
- async _processChat(chatId, text, replyToMessageId) {
291
- const sessionInfo = this._getSessionAgent(chatId)
292
- if (!sessionInfo) {
293
- await this._sendMessage(chatId, '❌ AI 服务未初始化', replyToMessageId)
294
- return
295
- }
296
-
297
- const { agent, sessionId } = sessionInfo
298
-
299
- if (this._sessionPlugin) {
300
- this._sessionPlugin.addMessage(sessionId, { role: 'user', content: text })
301
- }
302
-
303
- let thinkingMsg
304
- try {
305
- thinkingMsg = await this._bot.sendMessage(chatId, '🤔 思考中...', {
306
- reply_to_message_id: replyToMessageId
307
- })
308
- } catch (err) {
309
- log.error(' Send thinking error:', err.message)
310
- return
311
- }
312
-
313
- try {
314
- let fullResponse = ''
315
- for await (const chunk of agent.chatStream(text, { sessionId })) {
316
- if (chunk.type === 'text' && chunk.text) {
317
- fullResponse += chunk.text
318
- if (fullResponse.length % 100 === 0) {
319
- try {
320
- await this._bot.editMessageText(`📝 ${escapeMarkdown(fullResponse)}▌`, {
321
- chat_id: chatId,
322
- message_id: thinkingMsg.message_id
323
- })
324
- } catch (e) { /* ignore */ }
325
- }
326
- }
327
- }
328
-
329
- if (this._sessionPlugin) {
330
- this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
331
- }
332
-
333
- const safeResponse = escapeMarkdown(fullResponse) || '抱歉,我没有收到有效的回复。'
334
- await this._bot.editMessageText(safeResponse, {
335
- chat_id: chatId,
336
- message_id: thinkingMsg.message_id,
337
- parse_mode: 'MarkdownV2'
338
- })
339
- } catch (err) {
340
- log.error(' Chat error:', err)
341
- await this._bot.editMessageText(escapeMarkdown(`❌ 发生错误:${err.message}`), {
342
- chat_id: chatId,
343
- message_id: thinkingMsg.message_id
344
- })
345
- }
346
- }
347
-
348
- async _handleEdit(msg) { /* 支持编辑后重新处理 */ }
349
-
350
- async _handleCallback(query) {
351
- await this._bot.answerCallbackQuery(query.id, { text: '处理中...' })
352
- }
353
-
354
- _checkPermission(chatId) {
355
- return this.config.allowedChats.length === 0 || this.config.allowedChats.includes(chatId)
356
- }
357
-
358
- async _handlePhoto(msg) {
359
- const chatId = msg.chat.id.toString()
360
- if (!this._checkPermission(chatId)) {
361
- await this._sendMessage(chatId, '抱歉,您没有权限使用此 Bot。', msg.message_id)
362
- return
363
- }
364
-
365
- const caption = msg.caption?.trim() || ''
366
- const photo = msg.photo[msg.photo.length - 1]
367
-
368
- try {
369
- const filePath = await this._downloadFile(photo.file_id, '.jpg', 'telegram_images')
370
- await this._sendMessage(chatId, `收到图片${caption ? ': ' + caption : ''}\n已保存至: ${filePath}`, msg.message_id)
371
- if (caption) {
372
- await this._processChat(chatId, `图片:${filePath}, ${caption}`, msg.message_id)
373
- }
374
- } catch (err) {
375
- log.error(' Failed to save photo:', err.message)
376
- await this._sendMessage(chatId, '图片保存失败: ' + err.message, msg.message_id)
377
- }
378
- }
379
-
380
- async _handleDocument(msg) {
381
- const chatId = msg.chat.id.toString()
382
- if (!this._checkPermission(chatId)) {
383
- await this._sendMessage(chatId, '抱歉,您没有权限使用此 Bot。', msg.message_id)
384
- return
385
- }
386
-
387
- const fileName = msg.document.file_name || '未命名文件'
388
- const caption = msg.caption?.trim() || ''
389
- const ext = fileName.includes('.') ? fileName.split('.').pop() : 'bin'
390
-
391
- try {
392
- const filePath = await this._downloadFile(msg.document.file_id, ext, 'telegram_documents')
393
- await this._sendMessage(chatId, `收到文件: ${fileName}\n已保存至: ${filePath}${caption ? '\n\n说明: ' + caption : ''}`, msg.message_id)
394
- if (caption) {
395
- await this._processChat(chatId, `文件:${filePath}, ${caption}`, msg.message_id)
396
- }
397
- } catch (err) {
398
- log.error(' Failed to save document:', err.message)
399
- await this._sendMessage(chatId, '文件保存失败: ' + err.message, msg.message_id)
400
- }
401
- }
402
-
403
- async _downloadFile(fileId, ext, subDir) {
404
- const path = require('path')
405
- const fs = require('fs')
406
- const saveDir = path.join(process.cwd(), '.agent', 'data', subDir)
407
- if (!fs.existsSync(saveDir)) fs.mkdirSync(saveDir, { recursive: true })
408
-
409
- const fileName = `${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`
410
- const filePath = path.join(saveDir, fileName)
411
-
412
- const savedPath = await this._bot.downloadFile(fileId, saveDir)
413
- if (savedPath !== filePath) fs.renameSync(savedPath, filePath)
414
- return filePath
415
- }
416
-
417
- _clearSession(chatId) {
418
- if (this._sessionPlugin) {
419
- this._sessionPlugin.deleteSession(`telegram_${chatId}`)
420
- }
421
- }
422
-
423
- async _sendMessage(chatId, text, replyToMessageId = null) {
424
- if (!this._bot) return
425
- return this._bot.sendMessage(chatId, text, { reply_to_message_id: replyToMessageId })
426
- }
427
-
428
- getStatus() {
429
- let sessions = []
430
- if (this._sessionPlugin) {
431
- sessions = this._sessionPlugin.listSessions()
432
- .filter(s => s.id.startsWith('telegram_'))
433
- .map(s => ({
434
- chatId: s.id.replace('telegram_', ''),
435
- historyLength: s.messageCount,
436
- lastActive: s.lastActive
437
- }))
438
- }
439
- return {
440
- connected: !!this._bot,
441
- sessionCount: sessions.length,
442
- sessions,
443
- config: {
444
- groupMode: this.config.groupMode,
445
- prefix: this.config.prefix,
446
- allowedChatsCount: this.config.allowedChats.length
447
- }
448
- }
449
- }
450
-
451
- stopBot() {
452
- if (this._bot) {
453
- this._bot.stopPolling()
454
- this._bot = null
455
- log.info(' Bot stopped')
456
- }
457
- }
458
-
459
- reload(framework) {
460
- this._framework = framework
461
- if (this.config.botToken) {
462
- this.stopBot()
463
- this._initBot()
464
- }
465
- }
466
-
467
- uninstall() {
468
- for (const agent of this._sessionAgents.values()) {
469
- agent.destroy()
470
- }
471
- this._sessionAgents.clear()
472
- this.stopBot()
473
- if (this._sessionPlugin && this._sessionDeleteHandler) {
474
- this._sessionPlugin.off('session:deleted', this._sessionDeleteHandler)
475
- this._sessionDeleteHandler = null
476
- }
477
- this._sessionPlugin = null
478
- this._framework = null
479
- }
480
- }
481
-
482
- module.exports = { TelegramPlugin }
1
+ /**
2
+ * Telegram 插件
3
+ * 支持绑定主Agent进行持续对话
4
+ *
5
+ * 配置:
6
+ * - botToken: Telegram Bot Token (必需)
7
+ * - allowedChats: 允许的聊天ID数组
8
+ * - groupMode: 是否启用群组模式
9
+ * - prefix: 命令前缀
10
+ */
11
+
12
+ const { Plugin } = require('../src/core/plugin-base')
13
+ const { logger } = require('../src/utils/logger')
14
+ const log = logger.child('Telegram')
15
+ const { z } = require('zod')
16
+
17
+ // 转义 MarkdownV2 特殊字符
18
+ function escapeMarkdown(text) {
19
+ if (!text) return ''
20
+ return text.replace(/([\_*\[\]()~`>#+\-=|{}.!])/g, '\\$1')
21
+ }
22
+
23
+ class TelegramPlugin extends Plugin {
24
+ constructor(config = {}) {
25
+ super()
26
+ this.name = 'telegram'
27
+ this.version = '2.1.0'
28
+ this.description = 'Telegram 对话插件,绑定主Agent进行持续对话'
29
+ this.priority = 80
30
+ this.enabled = false
31
+ this.systemPrompt = `你是一个有帮助的AI助手。回复内容不要使用markdown格式文本。
32
+
33
+ **重要:** 子Agent 匹配规则必须遵守:
34
+ - 根据【子 Agent 匹配表】,将任务委托给最匹配的子Agent处理
35
+ - 使用 subagent_call 工具并指定 agentName 来委托任务
36
+ - 只有当没有匹配的子Agent时,才直接调用工具
37
+
38
+ **命令执行规范:**
39
+ - 执行 npx skills add / npx skills remove / npx skills list 命令时,必须自动添加参数:-a openclaw -y
40
+ - 禁止添加 -g 参数
41
+ - 例如:npx skills add xxx 应该执行为 npx skills add xxx -a openclaw -y`
42
+
43
+ this.config = {
44
+ botToken: config.botToken || process.env.TELEGRAM_BOT_TOKEN,
45
+ allowedChats: config.allowedChats || [],
46
+ groupMode: config.groupMode || false,
47
+ prefix: config.prefix || '/'
48
+ }
49
+
50
+ this._framework = null
51
+ this._bot = null
52
+ this._sessionPlugin = null
53
+ this._sessionDeleteHandler = null
54
+ this._sessionAgents = new Map()
55
+ }
56
+
57
+ install(framework) {
58
+ this._framework = framework
59
+ return this
60
+ }
61
+
62
+ start(framework) {
63
+ if (!this.config.botToken) {
64
+ log.warn(' No bot token. Set TELEGRAM_BOT_TOKEN env var.')
65
+ return this
66
+ }
67
+
68
+ this._framework = framework
69
+ this._sessionPlugin = framework.pluginManager.get('session')
70
+
71
+ if (this._sessionPlugin) {
72
+ this._sessionDeleteHandler = (sessionId) => {
73
+ if (sessionId.startsWith('telegram_')) {
74
+ log.info(` Session deleted: ${sessionId}`)
75
+ }
76
+ }
77
+ this._sessionPlugin.on('session:deleted', this._sessionDeleteHandler)
78
+ }
79
+
80
+ this._initBot()
81
+ return this
82
+ }
83
+
84
+ _initBot() {
85
+ try {
86
+ const TelegramBot = require('node-telegram-bot-api')
87
+ this._bot = new TelegramBot(this.config.botToken, { polling: true })
88
+
89
+ log.info(' Bot started successfully')
90
+
91
+ this._bot.setMyCommands([
92
+ { command: 'start', description: '显示帮助信息' },
93
+ { command: 'clear', description: '清除对话历史' },
94
+ { command: 'history', description: '查看对话状态' }
95
+ ]).catch((err) => {
96
+ log.error(' Failed to register commands:', err.message)
97
+ })
98
+
99
+ this._bot.on('message', (msg) => this._handleMessage(msg))
100
+ this._bot.on('edit_message', (msg) => this._handleEdit(msg))
101
+ this._bot.on('callback_query', (query) => this._handleCallback(query))
102
+ this._bot.on('polling_error', (err) => log.error(' Polling error:', err.message))
103
+ this._bot.on('error', (err) => log.error(' Bot error:', err.message))
104
+
105
+ if (this._framework) {
106
+ this._framework.on('agent:created', (agent) => {
107
+ log.info(' New agent created:', agent.name)
108
+ })
109
+
110
+ // 监听统一通知事件
111
+ this._framework.on('notification', async (data) => {
112
+ await this._handleNotification(data)
113
+ })
114
+
115
+ // 监听 webhook 事件
116
+ this._framework.on('webhook:received', async (data) => {
117
+ await this._handleWebhookNotification(data)
118
+ })
119
+ }
120
+ } catch (err) {
121
+ log.error(' Failed to initialize bot:', err.message)
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 处理统一通知
127
+ */
128
+ async _handleNotification(data) {
129
+ const { title, message, source, level } = data
130
+
131
+ if (!this._bot) {
132
+ log.warn(' Bot not ready, cannot send notification')
133
+ return
134
+ }
135
+
136
+ // 确定 chatId
137
+ let chatId = null
138
+ let effectiveSessionId = data.sessionId
139
+
140
+ // 如果没有 sessionId,尝试从执行上下文获取
141
+ if (!effectiveSessionId) {
142
+ const ctx = this._framework.getExecutionContext()
143
+ if (ctx?.sessionId) {
144
+ effectiveSessionId = ctx.sessionId
145
+ }
146
+ }
147
+
148
+ if (effectiveSessionId && effectiveSessionId.startsWith('telegram_')) {
149
+ chatId = effectiveSessionId.replace('telegram_', '')
150
+ } else if (this._sessionPlugin) {
151
+ // 获取最近的 telegram 会话
152
+ const sessions = this._sessionPlugin.listSessions()
153
+ .filter(s => s.id.startsWith('telegram_'))
154
+ .sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime())
155
+ if (sessions.length > 0) {
156
+ chatId = sessions[0].id.replace('telegram_', '')
157
+ }
158
+ }
159
+
160
+ if (!chatId) {
161
+ log.warn(' No telegram session found for notification')
162
+ return
163
+ }
164
+
165
+ // 格式化通知消息
166
+ const levelEmoji = {
167
+ info: 'ℹ️',
168
+ warning: '⚠️',
169
+ success: '✅',
170
+ error: '❌'
171
+ }
172
+ const emoji = levelEmoji[level] || 'ℹ️'
173
+ const notificationText = `${emoji} [${source}] ${title}\n\n${message}`
174
+
175
+ try {
176
+ await this._bot.sendMessage(chatId, notificationText)
177
+ log.info(` Notification sent to chat ${chatId}`)
178
+ } catch (err) {
179
+ log.error(` Failed to send notification:`, err.message)
180
+ }
181
+ }
182
+
183
+ /**
184
+ * 处理 webhook 通知
185
+ */
186
+ async _handleWebhookNotification(data) {
187
+ const { data: webhookData, response, sessionId } = data
188
+
189
+ if (!sessionId || !sessionId.startsWith('telegram_')) {
190
+ return
191
+ }
192
+
193
+ const chatId = sessionId.replace('telegram_', '')
194
+ const notificationText = `📥 [Webhook 接收]\n\n路径: ${webhookData.path}\n方法: ${webhookData.method}\n\n处理结果: ${response || '处理中...'}`
195
+
196
+ try {
197
+ await this._bot.sendMessage(chatId, notificationText)
198
+ log.info(` Webhook notification sent to chat ${chatId}`)
199
+ } catch (err) {
200
+ log.error(` Failed to send webhook notification:`, err.message)
201
+ }
202
+ }
203
+
204
+ _getMainAgent() {
205
+ if (this._framework._mainAgent) return this._framework._mainAgent
206
+ const agents = this._framework._agents || []
207
+ return agents.length > 0 ? agents[agents.length - 1] : null
208
+ }
209
+
210
+ _getSessionAgent(chatId) {
211
+ if (this._sessionAgents.has(chatId)) {
212
+ return { agent: this._sessionAgents.get(chatId), sessionId: `telegram_${chatId}` }
213
+ }
214
+
215
+ const agent = this._framework.createSessionAgent(`telegram_${chatId}`, {
216
+ systemPrompt: this.systemPrompt,
217
+ sharedPrompt: `工作目录: {{WORK_DIR}}`,
218
+ metadata: { WORK_DIR: process.cwd() }
219
+ })
220
+ this._sessionAgents.set(chatId, agent)
221
+
222
+ if (this._sessionPlugin) {
223
+ this._sessionPlugin.getOrCreateSession(`telegram_${chatId}`, {
224
+ metadata: { platform: 'telegram', chatId }
225
+ })
226
+ }
227
+
228
+ return { agent, sessionId: `telegram_${chatId}` }
229
+ }
230
+
231
+ async _handleMessage(msg) {
232
+ if (!msg || !msg.chat) return
233
+ const chatId = msg.chat.id.toString()
234
+
235
+ if (msg.photo) { await this._handlePhoto(msg); return }
236
+ if (msg.document) { await this._handleDocument(msg); return }
237
+ if (!msg.text) return
238
+
239
+ const text = msg.text.trim()
240
+ if (text.startsWith(this.config.prefix)) {
241
+ await this._handleCommand(msg)
242
+ return
243
+ }
244
+
245
+ if (msg.chat.type === 'group' || msg.chat.type === 'supergroup') {
246
+ if (!this.config.groupMode) return
247
+ }
248
+
249
+ if (!this._checkPermission(chatId)) {
250
+ await this._sendMessage(chatId, '抱歉,您没有权限使用此 Bot。', msg.message_id)
251
+ return
252
+ }
253
+
254
+ await this._processChat(chatId, text, msg.message_id)
255
+ }
256
+
257
+ async _handleCommand(msg) {
258
+ const chatId = msg.chat.id.toString()
259
+ const text = msg.text.trim()
260
+ const parts = text.split(' ')
261
+ const command = parts[0].substring(1)
262
+ const args = parts.slice(1).join(' ')
263
+
264
+ switch (command.toLowerCase()) {
265
+ case 'start':
266
+ await this._sendMessage(chatId,
267
+ '👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/start - 显示帮助\n/clear - 清除对话历史\n/history - 查看历史消息数',
268
+ msg.message_id)
269
+ break
270
+ case 'clear':
271
+ this._clearSession(chatId)
272
+ await this._sendMessage(chatId, '✅ 对话历史已清除', msg.message_id)
273
+ break
274
+ case 'history':
275
+ if (this._sessionPlugin) {
276
+ const session = this._sessionPlugin.getSession(`telegram_${chatId}`)
277
+ const sessions = this._sessionPlugin.listSessions().filter(s => s.id.startsWith('telegram_'))
278
+ await this._sendMessage(chatId,
279
+ `📊 当前状态\n\n会话数:${sessions.length}\n历史消息:${session?.messages?.length || 0}\n最后活跃:${session?.lastActive?.toLocaleString() || '无'}`,
280
+ msg.message_id)
281
+ } else {
282
+ await this._sendMessage(chatId, '❌ 会话服务不可用', msg.message_id)
283
+ }
284
+ break
285
+ default:
286
+ await this._processChat(chatId, text, msg.message_id)
287
+ }
288
+ }
289
+
290
+ async _processChat(chatId, text, replyToMessageId) {
291
+ const sessionInfo = this._getSessionAgent(chatId)
292
+ if (!sessionInfo) {
293
+ await this._sendMessage(chatId, '❌ AI 服务未初始化', replyToMessageId)
294
+ return
295
+ }
296
+
297
+ const { agent, sessionId } = sessionInfo
298
+
299
+ if (this._sessionPlugin) {
300
+ this._sessionPlugin.addMessage(sessionId, { role: 'user', content: text })
301
+ }
302
+
303
+ let thinkingMsg
304
+ try {
305
+ thinkingMsg = await this._bot.sendMessage(chatId, '🤔 思考中...', {
306
+ reply_to_message_id: replyToMessageId
307
+ })
308
+ } catch (err) {
309
+ log.error(' Send thinking error:', err.message)
310
+ return
311
+ }
312
+
313
+ try {
314
+ let fullResponse = ''
315
+ for await (const chunk of agent.chatStream(text, { sessionId })) {
316
+ if (chunk.type === 'text' && chunk.text) {
317
+ fullResponse += chunk.text
318
+ if (fullResponse.length % 100 === 0) {
319
+ try {
320
+ await this._bot.editMessageText(`📝 ${escapeMarkdown(fullResponse)}▌`, {
321
+ chat_id: chatId,
322
+ message_id: thinkingMsg.message_id
323
+ })
324
+ } catch (e) { /* ignore */ }
325
+ }
326
+ }
327
+ }
328
+
329
+ if (this._sessionPlugin) {
330
+ this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
331
+ }
332
+
333
+ const safeResponse = escapeMarkdown(fullResponse) || '抱歉,我没有收到有效的回复。'
334
+ await this._bot.editMessageText(safeResponse, {
335
+ chat_id: chatId,
336
+ message_id: thinkingMsg.message_id,
337
+ parse_mode: 'MarkdownV2'
338
+ })
339
+ } catch (err) {
340
+ log.error(' Chat error:', err)
341
+ await this._bot.editMessageText(escapeMarkdown(`❌ 发生错误:${err.message}`), {
342
+ chat_id: chatId,
343
+ message_id: thinkingMsg.message_id
344
+ })
345
+ }
346
+ }
347
+
348
+ async _handleEdit(msg) { /* 支持编辑后重新处理 */ }
349
+
350
+ async _handleCallback(query) {
351
+ await this._bot.answerCallbackQuery(query.id, { text: '处理中...' })
352
+ }
353
+
354
+ _checkPermission(chatId) {
355
+ return this.config.allowedChats.length === 0 || this.config.allowedChats.includes(chatId)
356
+ }
357
+
358
+ async _handlePhoto(msg) {
359
+ const chatId = msg.chat.id.toString()
360
+ if (!this._checkPermission(chatId)) {
361
+ await this._sendMessage(chatId, '抱歉,您没有权限使用此 Bot。', msg.message_id)
362
+ return
363
+ }
364
+
365
+ const caption = msg.caption?.trim() || ''
366
+ const photo = msg.photo[msg.photo.length - 1]
367
+
368
+ try {
369
+ const filePath = await this._downloadFile(photo.file_id, '.jpg', 'telegram_images')
370
+ await this._sendMessage(chatId, `收到图片${caption ? ': ' + caption : ''}\n已保存至: ${filePath}`, msg.message_id)
371
+ if (caption) {
372
+ await this._processChat(chatId, `图片:${filePath}, ${caption}`, msg.message_id)
373
+ }
374
+ } catch (err) {
375
+ log.error(' Failed to save photo:', err.message)
376
+ await this._sendMessage(chatId, '图片保存失败: ' + err.message, msg.message_id)
377
+ }
378
+ }
379
+
380
+ async _handleDocument(msg) {
381
+ const chatId = msg.chat.id.toString()
382
+ if (!this._checkPermission(chatId)) {
383
+ await this._sendMessage(chatId, '抱歉,您没有权限使用此 Bot。', msg.message_id)
384
+ return
385
+ }
386
+
387
+ const fileName = msg.document.file_name || '未命名文件'
388
+ const caption = msg.caption?.trim() || ''
389
+ const ext = fileName.includes('.') ? fileName.split('.').pop() : 'bin'
390
+
391
+ try {
392
+ const filePath = await this._downloadFile(msg.document.file_id, ext, 'telegram_documents')
393
+ await this._sendMessage(chatId, `收到文件: ${fileName}\n已保存至: ${filePath}${caption ? '\n\n说明: ' + caption : ''}`, msg.message_id)
394
+ if (caption) {
395
+ await this._processChat(chatId, `文件:${filePath}, ${caption}`, msg.message_id)
396
+ }
397
+ } catch (err) {
398
+ log.error(' Failed to save document:', err.message)
399
+ await this._sendMessage(chatId, '文件保存失败: ' + err.message, msg.message_id)
400
+ }
401
+ }
402
+
403
+ async _downloadFile(fileId, ext, subDir) {
404
+ const path = require('path')
405
+ const fs = require('fs')
406
+ const saveDir = path.join(process.cwd(), '.agent', 'data', subDir)
407
+ if (!fs.existsSync(saveDir)) fs.mkdirSync(saveDir, { recursive: true })
408
+
409
+ const fileName = `${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`
410
+ const filePath = path.join(saveDir, fileName)
411
+
412
+ const savedPath = await this._bot.downloadFile(fileId, saveDir)
413
+ if (savedPath !== filePath) fs.renameSync(savedPath, filePath)
414
+ return filePath
415
+ }
416
+
417
+ _clearSession(chatId) {
418
+ if (this._sessionPlugin) {
419
+ this._sessionPlugin.deleteSession(`telegram_${chatId}`)
420
+ }
421
+ }
422
+
423
+ async _sendMessage(chatId, text, replyToMessageId = null) {
424
+ if (!this._bot) return
425
+ return this._bot.sendMessage(chatId, text, { reply_to_message_id: replyToMessageId })
426
+ }
427
+
428
+ getStatus() {
429
+ let sessions = []
430
+ if (this._sessionPlugin) {
431
+ sessions = this._sessionPlugin.listSessions()
432
+ .filter(s => s.id.startsWith('telegram_'))
433
+ .map(s => ({
434
+ chatId: s.id.replace('telegram_', ''),
435
+ historyLength: s.messageCount,
436
+ lastActive: s.lastActive
437
+ }))
438
+ }
439
+ return {
440
+ connected: !!this._bot,
441
+ sessionCount: sessions.length,
442
+ sessions,
443
+ config: {
444
+ groupMode: this.config.groupMode,
445
+ prefix: this.config.prefix,
446
+ allowedChatsCount: this.config.allowedChats.length
447
+ }
448
+ }
449
+ }
450
+
451
+ stopBot() {
452
+ if (this._bot) {
453
+ this._bot.stopPolling()
454
+ this._bot = null
455
+ log.info(' Bot stopped')
456
+ }
457
+ }
458
+
459
+ reload(framework) {
460
+ this._framework = framework
461
+ if (this.config.botToken) {
462
+ this.stopBot()
463
+ this._initBot()
464
+ }
465
+ }
466
+
467
+ uninstall() {
468
+ for (const agent of this._sessionAgents.values()) {
469
+ agent.destroy()
470
+ }
471
+ this._sessionAgents.clear()
472
+ this.stopBot()
473
+ if (this._sessionPlugin && this._sessionDeleteHandler) {
474
+ this._sessionPlugin.off('session:deleted', this._sessionDeleteHandler)
475
+ this._sessionDeleteHandler = null
476
+ }
477
+ this._sessionPlugin = null
478
+ this._framework = null
479
+ }
480
+ }
481
+
482
+ module.exports = { TelegramPlugin }