foliko 1.0.31 → 1.0.32
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/.claude/settings.local.json +3 -1
- package/package.json +44 -43
- package/plugins/default-plugins.js +16 -1
- package/plugins/email.js +22 -15
- package/plugins/feishu-plugin.js +612 -0
- package/plugins/telegram-plugin.js +1 -1
- package/plugins/weixin-plugin.js +4 -2
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
"Bash(node -c plugins/email.js 2>&1)",
|
|
74
74
|
"Bash(npm list:*)",
|
|
75
75
|
"Bash(node -e \"const {EmailPlugin} = require\\('./plugins/email'\\); const p = new EmailPlugin\\(\\); console.log\\('email plugin loaded ok'\\); console.log\\('enabled:', p.enabled\\); console.log\\('version:', p.version\\);\" 2>&1)",
|
|
76
|
-
"Bash(cd D:/Code/vb-agent && node -e \"console.log\\('IMAP_HOST:', process.env.IMAP_HOST\\); console.log\\('IMAP_USER:', process.env.IMAP_USER\\); console.log\\('IMAP_PORT:', process.env.IMAP_PORT\\);\")"
|
|
76
|
+
"Bash(cd D:/Code/vb-agent && node -e \"console.log\\('IMAP_HOST:', process.env.IMAP_HOST\\); console.log\\('IMAP_USER:', process.env.IMAP_USER\\); console.log\\('IMAP_PORT:', process.env.IMAP_PORT\\);\")",
|
|
77
|
+
"Bash(node -e \"const sdk = require\\('@larksuiteoapi/node-sdk'\\); console.log\\(Object.keys\\(sdk\\)\\);\")",
|
|
78
|
+
"Bash(node -e \"const { WSClient } = require\\('@larksuiteoapi/node-sdk'\\); const sdk = new WSClient\\({}\\); console.log\\(Object.getOwnPropertyNames\\(Object.getPrototypeOf\\(sdk\\)\\)\\);\")"
|
|
77
79
|
]
|
|
78
80
|
}
|
|
79
81
|
}
|
package/package.json
CHANGED
|
@@ -1,43 +1,44 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "foliko",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "简约的插件化 Agent 框架",
|
|
5
|
-
"main": "src/index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"foliko": "./cli/bin/foliko.js"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"start": "node examples/basic.js",
|
|
11
|
-
"chat": "node cli/bin/foliko.js chat",
|
|
12
|
-
"test": "echo \"No tests yet\""
|
|
13
|
-
},
|
|
14
|
-
"keywords": [
|
|
15
|
-
"agent",
|
|
16
|
-
"ai",
|
|
17
|
-
"framework",
|
|
18
|
-
"plugin"
|
|
19
|
-
],
|
|
20
|
-
"author": "",
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@ai-sdk/anthropic": "^3.0.58",
|
|
24
|
-
"@ai-sdk/mcp": "^1.0.25",
|
|
25
|
-
"@ai-sdk/openai": "^3.0.41",
|
|
26
|
-
"@ai-sdk/openai-compatible": "^2.0.35",
|
|
27
|
-
"@anthropic-ai/sdk": "^0.39.0",
|
|
28
|
-
"@chnak/weixin-bot": "^1.2.0",
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"imap
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"marked
|
|
37
|
-
"
|
|
38
|
-
"node-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "foliko",
|
|
3
|
+
"version": "1.0.32",
|
|
4
|
+
"description": "简约的插件化 Agent 框架",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"foliko": "./cli/bin/foliko.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node examples/basic.js",
|
|
11
|
+
"chat": "node cli/bin/foliko.js chat",
|
|
12
|
+
"test": "echo \"No tests yet\""
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"agent",
|
|
16
|
+
"ai",
|
|
17
|
+
"framework",
|
|
18
|
+
"plugin"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@ai-sdk/anthropic": "^3.0.58",
|
|
24
|
+
"@ai-sdk/mcp": "^1.0.25",
|
|
25
|
+
"@ai-sdk/openai": "^3.0.41",
|
|
26
|
+
"@ai-sdk/openai-compatible": "^2.0.35",
|
|
27
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
28
|
+
"@chnak/weixin-bot": "^1.2.0",
|
|
29
|
+
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
31
|
+
"ai": "^6.0.116",
|
|
32
|
+
"dotenv": "^17.3.1",
|
|
33
|
+
"imap": "^0.8.19",
|
|
34
|
+
"imap-mkl": "^1.0.2",
|
|
35
|
+
"mailparser": "^3.7.2",
|
|
36
|
+
"marked": "^11.2.0",
|
|
37
|
+
"marked-terminal": "6",
|
|
38
|
+
"node-cron": "^4.2.1",
|
|
39
|
+
"node-telegram-bot-api": "^0.67.0",
|
|
40
|
+
"nodemailer": "^6.10.0",
|
|
41
|
+
"qrcode-terminal": "^0.12.0",
|
|
42
|
+
"zod": "^3.24.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -443,7 +443,22 @@ async function bootstrapDefaults(framework, config = {}) {
|
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
// 12.9
|
|
446
|
+
// 12.9 Feishu 插件(默认禁用,需要在 plugins.json 中设置 enabled: true)
|
|
447
|
+
if (shouldLoad('feishu')) {
|
|
448
|
+
try {
|
|
449
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
450
|
+
const createFeishuPlugin = require('./feishu-plugin')
|
|
451
|
+
const FeishuPlugin = createFeishuPlugin(Plugin)
|
|
452
|
+
const feishuConfig = {
|
|
453
|
+
allowedUsers: agentConfig.feishu?.allowedUsers || []
|
|
454
|
+
}
|
|
455
|
+
await framework.loadPlugin(new FeishuPlugin(feishuConfig))
|
|
456
|
+
} catch (err) {
|
|
457
|
+
console.warn('[Bootstrap] Feishu Plugin not available:', err.message)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// 12.10 SubAgent 管理器
|
|
447
462
|
if (shouldLoad('subagent-manager')) {
|
|
448
463
|
try {
|
|
449
464
|
const { SubAgentManagerPlugin } = require('./subagent-plugin')
|
package/plugins/email.js
CHANGED
|
@@ -307,13 +307,7 @@ class EmailPlugin extends Plugin {
|
|
|
307
307
|
|
|
308
308
|
f.on('message', (msg) => {
|
|
309
309
|
let email = {}
|
|
310
|
-
let
|
|
311
|
-
|
|
312
|
-
const finishEmail = () => {
|
|
313
|
-
if (parserDone) {
|
|
314
|
-
emails.push(email)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
310
|
+
let bodyParsed = false
|
|
317
311
|
|
|
318
312
|
msg.on('body', (stream) => {
|
|
319
313
|
simpleParser(stream).then(mail => {
|
|
@@ -327,12 +321,10 @@ class EmailPlugin extends Plugin {
|
|
|
327
321
|
filename: a.filename,
|
|
328
322
|
contentType: a.contentType
|
|
329
323
|
})) || []
|
|
330
|
-
|
|
331
|
-
finishEmail()
|
|
324
|
+
bodyParsed = true
|
|
332
325
|
}).catch(err => {
|
|
333
326
|
email.error = err.message
|
|
334
|
-
|
|
335
|
-
finishEmail()
|
|
327
|
+
bodyParsed = true
|
|
336
328
|
})
|
|
337
329
|
})
|
|
338
330
|
|
|
@@ -343,8 +335,15 @@ class EmailPlugin extends Plugin {
|
|
|
343
335
|
})
|
|
344
336
|
|
|
345
337
|
msg.on('end', () => {
|
|
346
|
-
//
|
|
347
|
-
|
|
338
|
+
// 等待 body 解析完成后再加入列表
|
|
339
|
+
const checkDone = () => {
|
|
340
|
+
if (bodyParsed) {
|
|
341
|
+
emails.push(email)
|
|
342
|
+
} else {
|
|
343
|
+
setTimeout(checkDone, 10)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
checkDone()
|
|
348
347
|
})
|
|
349
348
|
})
|
|
350
349
|
|
|
@@ -354,8 +353,16 @@ class EmailPlugin extends Plugin {
|
|
|
354
353
|
})
|
|
355
354
|
|
|
356
355
|
f.on('end', () => {
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
// 等待所有邮件解析完成
|
|
357
|
+
const waitForAll = () => {
|
|
358
|
+
if (emails.length === fetchIds.length) {
|
|
359
|
+
cleanup()
|
|
360
|
+
resolve(emails)
|
|
361
|
+
} else {
|
|
362
|
+
setTimeout(waitForAll, 10)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
waitForAll()
|
|
359
366
|
})
|
|
360
367
|
})
|
|
361
368
|
})
|
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 飞书插件
|
|
3
|
+
* 使用 @larksuiteoapi/node-sdk 的 WebSocket 长连接模式
|
|
4
|
+
*
|
|
5
|
+
* 配置:
|
|
6
|
+
* - appId: 飞书应用 ID (默认从 FEISHU_APP_ID 环境变量获取)
|
|
7
|
+
* - appSecret: 飞书应用密钥 (默认从 FEISHU_APP_SECRET 环境变量获取)
|
|
8
|
+
* - allowedUsers: 允许的用户 openId 数组,为空则允许所有用户
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { Plugin } = require('../src/core/plugin-base')
|
|
12
|
+
|
|
13
|
+
module.exports = function(Plugin) {
|
|
14
|
+
return class FeishuPlugin extends Plugin {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super()
|
|
17
|
+
this.name = 'feishu'
|
|
18
|
+
this.version = '1.0.0'
|
|
19
|
+
this.description = '飞书对话插件,使用 WebSocket 长连接接收消息'
|
|
20
|
+
this.priority = 80
|
|
21
|
+
// 默认不启用,需要在 plugins.json 中设置 enabled: true
|
|
22
|
+
this.enabled = false
|
|
23
|
+
|
|
24
|
+
this.config = {
|
|
25
|
+
appId: config.appId || process.env.FEISHU_APP_ID,
|
|
26
|
+
appSecret: config.appSecret || process.env.FEISHU_APP_SECRET,
|
|
27
|
+
allowedUsers: config.allowedUsers || [],
|
|
28
|
+
systemPrompt: '你是一个飞书助手。回复内容不要使用markdown格式文本。'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this._framework = null
|
|
32
|
+
this._client = null
|
|
33
|
+
this._wsClient = null
|
|
34
|
+
this._sessionPlugin = null
|
|
35
|
+
this._sessionDeleteHandler = null
|
|
36
|
+
this._sessionAgents = new Map() // openId -> Agent
|
|
37
|
+
this._initialized = false
|
|
38
|
+
this._processedMessages = new Set() // 消息去重
|
|
39
|
+
this._messageCleanupInterval = null
|
|
40
|
+
this._processingLock = new Map() // openId -> boolean (是否正在处理)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
install(framework) {
|
|
44
|
+
this._framework = framework
|
|
45
|
+
return this
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
start(framework) {
|
|
49
|
+
// 防止重复初始化
|
|
50
|
+
if (this._initialized) return this
|
|
51
|
+
this._initialized = true
|
|
52
|
+
|
|
53
|
+
if (!this.config.appId || !this.config.appSecret) {
|
|
54
|
+
console.warn('[Feishu] No appId or appSecret. Set FEISHU_APP_ID and FEISHU_APP_SECRET env vars.')
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 获取 SessionPlugin 引用
|
|
59
|
+
this._sessionPlugin = framework.pluginManager.get('session')
|
|
60
|
+
|
|
61
|
+
// 监听 SessionPlugin 的会话删除事件
|
|
62
|
+
if (this._sessionPlugin) {
|
|
63
|
+
this._sessionDeleteHandler = (sessionId) => {
|
|
64
|
+
// 只处理 feishu 会话的删除
|
|
65
|
+
if (sessionId.startsWith('feishu_')) {
|
|
66
|
+
console.log(`[Feishu] Session deleted: ${sessionId}`)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
this._sessionPlugin.on('session:deleted', this._sessionDeleteHandler)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this._initClient()
|
|
73
|
+
return this
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 初始化飞书 WebSocket 客户端
|
|
78
|
+
*/
|
|
79
|
+
_initClient() {
|
|
80
|
+
try {
|
|
81
|
+
const { Client, WSClient, EventDispatcher } = require('@larksuiteoapi/node-sdk')
|
|
82
|
+
|
|
83
|
+
this._client = new Client({
|
|
84
|
+
appId: this.config.appId,
|
|
85
|
+
appSecret: this.config.appSecret,
|
|
86
|
+
logger: console
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
this._wsClient = new WSClient({
|
|
90
|
+
appId: this.config.appId,
|
|
91
|
+
appSecret: this.config.appSecret,
|
|
92
|
+
logger: console
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// 创建事件分发器并注册处理器
|
|
96
|
+
const eventDispatcher = new EventDispatcher({}).register({
|
|
97
|
+
'im.message.receive_v1': async (data) => {
|
|
98
|
+
await this._handleMessage(data)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// 启动 WebSocket 连接
|
|
103
|
+
this._wsClient.start({ eventDispatcher })
|
|
104
|
+
|
|
105
|
+
// 启动消息去重清理定时器(每分钟清理一次)
|
|
106
|
+
this._messageCleanupInterval = setInterval(() => {
|
|
107
|
+
this._processedMessages.clear()
|
|
108
|
+
}, 60000)
|
|
109
|
+
|
|
110
|
+
// 监听定时提醒事件
|
|
111
|
+
if (this._framework) {
|
|
112
|
+
this._framework.on('scheduler:reminder', async (data) => {
|
|
113
|
+
console.log('[Feishu] Received scheduler reminder:', data)
|
|
114
|
+
await this._handleScheduledReminder(data)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('[Feishu] WebSocket client started')
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error('[Feishu] Failed to initialize client:', err.message, err.stack)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 处理飞书消息
|
|
126
|
+
*/
|
|
127
|
+
async _handleMessage(data) {
|
|
128
|
+
try {
|
|
129
|
+
// 解析消息数据 - SDK 事件格式
|
|
130
|
+
// data 结构: { message: { chat_id, content, sender: { sender_id: { open_id }, ... }, message_type } }
|
|
131
|
+
if (!data || !data.message) {
|
|
132
|
+
console.log('[Feishu] Invalid message data:', JSON.stringify(data).substring(0, 500))
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const message = data.message
|
|
137
|
+
const chatId = message.chat_id
|
|
138
|
+
const messageId = message.message_id
|
|
139
|
+
// 飞书消息结构: sender.sender_id.open_id 或 sender.open_id
|
|
140
|
+
// 注意: 群组消息可能没有 sender 字段,此时使用 chat_id 作为会话标识
|
|
141
|
+
const openId = message.sender?.sender_id?.open_id || message.sender?.open_id || message.open_id || chatId
|
|
142
|
+
const messageType = message.message_type
|
|
143
|
+
const content = message.content ? JSON.parse(message.content) : {}
|
|
144
|
+
|
|
145
|
+
// 消息去重
|
|
146
|
+
if (messageId && this._processedMessages.has(messageId)) {
|
|
147
|
+
console.log(`[Feishu] Duplicate message ignored: ${messageId}`)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
if (messageId) {
|
|
151
|
+
this._processedMessages.add(messageId)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 忽略非文本消息
|
|
155
|
+
if (messageType !== 'text') {
|
|
156
|
+
console.log(`[Feishu] Unsupported message type: ${messageType}`)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const text = content.text?.trim()
|
|
161
|
+
if (!text) {
|
|
162
|
+
console.log('[Feishu] Empty text message')
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 权限检查
|
|
167
|
+
if (!this._checkPermission(openId)) {
|
|
168
|
+
console.log(`[Feishu] User ${openId} not allowed`)
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 获取历史消息数量
|
|
173
|
+
let messageCount = 0
|
|
174
|
+
if (this._sessionPlugin) {
|
|
175
|
+
const session = this._sessionPlugin.getSession(`feishu_${openId}`)
|
|
176
|
+
messageCount = session?.messages?.length || 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log(`[Feishu] #${messageCount + 1} | Chat: ${chatId} | User: ${openId}`)
|
|
180
|
+
console.log(`[Feishu] Content: ${text}`)
|
|
181
|
+
|
|
182
|
+
// 命令处理
|
|
183
|
+
if (text.startsWith('/')) {
|
|
184
|
+
await this._handleCommand(openId, text, message)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 检查是否正在处理,防止并发
|
|
189
|
+
if (this._processingLock.get(openId)) {
|
|
190
|
+
console.log(`[Feishu] Session ${openId} is busy, ignoring message`)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
this._processingLock.set(openId, true)
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await this._processChat(openId, text, message)
|
|
197
|
+
} finally {
|
|
198
|
+
this._processingLock.set(openId, false)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error('[Feishu] Error handling message:', err)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 处理命令
|
|
208
|
+
*/
|
|
209
|
+
async _handleCommand(openId, text, originalMsg) {
|
|
210
|
+
const parts = text.split(' ')
|
|
211
|
+
const command = parts[0].substring(1)
|
|
212
|
+
const args = parts.slice(1).join(' ')
|
|
213
|
+
|
|
214
|
+
switch (command.toLowerCase()) {
|
|
215
|
+
case 'start':
|
|
216
|
+
case 'help':
|
|
217
|
+
await this._sendMessage(openId,
|
|
218
|
+
'👋 欢迎使用 AI 助手!\n\n' +
|
|
219
|
+
'直接发送消息即可与我对话。\n\n' +
|
|
220
|
+
'可用命令:\n' +
|
|
221
|
+
'/start - 显示帮助\n' +
|
|
222
|
+
'/clear - 清除对话历史\n' +
|
|
223
|
+
'/history - 查看历史消息数',
|
|
224
|
+
originalMsg)
|
|
225
|
+
break
|
|
226
|
+
|
|
227
|
+
case 'clear':
|
|
228
|
+
this._clearSession(openId)
|
|
229
|
+
await this._sendMessage(openId, '✅ 对话历史已清除', originalMsg)
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
case 'history':
|
|
233
|
+
if (this._sessionPlugin) {
|
|
234
|
+
const sessionId = `feishu_${openId}`
|
|
235
|
+
const session = this._sessionPlugin.getSession(sessionId)
|
|
236
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
237
|
+
const feishuSessionCount = allSessions.filter(s => s.id.startsWith('feishu_')).length
|
|
238
|
+
await this._sendMessage(openId,
|
|
239
|
+
`📊 当前状态\n\n会话数:${feishuSessionCount}\n` +
|
|
240
|
+
`历史消息:${session?.messages?.length || 0}\n` +
|
|
241
|
+
`最后活跃:${session?.lastActive?.toLocaleString() || '无'}`,
|
|
242
|
+
originalMsg)
|
|
243
|
+
} else {
|
|
244
|
+
await this._sendMessage(openId, '❌ 会话服务不可用', originalMsg)
|
|
245
|
+
}
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
default:
|
|
249
|
+
// 未识别命令,作为普通消息处理
|
|
250
|
+
await this._processChat(openId, text, originalMsg)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 获取或创建会话 Agent
|
|
256
|
+
* 使用 SessionPlugin 统一管理会话历史
|
|
257
|
+
*/
|
|
258
|
+
_getSessionAgent(openId) {
|
|
259
|
+
// 检查缓存
|
|
260
|
+
if (this._sessionAgents.has(openId)) {
|
|
261
|
+
const agent = this._sessionAgents.get(openId)
|
|
262
|
+
console.log('[Feishu] Reusing cached session agent for openId:', openId)
|
|
263
|
+
return { agent, sessionId: `feishu_${openId}` }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 创建新 agent
|
|
267
|
+
const agent = this._framework.createSessionAgent(`feishu_${openId}`, {
|
|
268
|
+
systemPrompt: this.config.systemPrompt
|
|
269
|
+
})
|
|
270
|
+
this._sessionAgents.set(openId, agent)
|
|
271
|
+
console.log('[Feishu] Created new session agent for openId:', openId)
|
|
272
|
+
|
|
273
|
+
// 使用 SessionPlugin 管理会话历史
|
|
274
|
+
if (this._sessionPlugin) {
|
|
275
|
+
const sessionId = `feishu_${openId}`
|
|
276
|
+
this._sessionPlugin.getOrCreateSession(sessionId, {
|
|
277
|
+
metadata: { platform: 'feishu', openId }
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`[Feishu] Session ready for openId: ${openId}`)
|
|
282
|
+
return { agent, sessionId: `feishu_${openId}` }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 处理定时提醒
|
|
287
|
+
*/
|
|
288
|
+
async _handleScheduledReminder(data) {
|
|
289
|
+
const { taskName, message, sessionId } = data
|
|
290
|
+
|
|
291
|
+
// 构建提醒消息
|
|
292
|
+
const reminderText = `🔔 [${taskName}]\n\n${message}`
|
|
293
|
+
|
|
294
|
+
// 如果有 sessionId 是 feishu 类型的,发送到对应用户
|
|
295
|
+
if (sessionId && sessionId.startsWith('feishu_')) {
|
|
296
|
+
const openId = sessionId.replace('feishu_', '')
|
|
297
|
+
try {
|
|
298
|
+
await this._sendTextMessage(openId, reminderText)
|
|
299
|
+
console.log(`[Feishu] Reminder sent to ${openId}`)
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.error(`[Feishu] Failed to send reminder:`, err.message)
|
|
302
|
+
}
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 其他情况,发送到最近的 Feishu 会话
|
|
307
|
+
if (this._sessionPlugin) {
|
|
308
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
309
|
+
const feishuSessions = allSessions
|
|
310
|
+
.filter(s => s.id.startsWith('feishu_'))
|
|
311
|
+
.sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime())
|
|
312
|
+
|
|
313
|
+
if (feishuSessions.length > 0) {
|
|
314
|
+
const openId = feishuSessions[0].id.replace('feishu_', '')
|
|
315
|
+
try {
|
|
316
|
+
await this._sendTextMessage(openId, reminderText)
|
|
317
|
+
console.log(`[Feishu] Reminder sent to ${openId}`)
|
|
318
|
+
} catch (err) {
|
|
319
|
+
console.error(`[Feishu] Failed to send reminder to ${openId}:`, err.message)
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
console.log('[Feishu] No active Feishu sessions to send reminder')
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
console.log('[Feishu] No active Feishu sessions to send reminder')
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 发送纯文本消息(内部使用,不经过 markdown 处理)
|
|
331
|
+
*/
|
|
332
|
+
async _sendTextMessage(openId, text) {
|
|
333
|
+
if (!this._client) return
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
await this._client.im.message.create({
|
|
337
|
+
params: {
|
|
338
|
+
receive_id_type: 'chat_id'
|
|
339
|
+
},
|
|
340
|
+
data: {
|
|
341
|
+
receive_id: openId,
|
|
342
|
+
content: JSON.stringify({ text: text }),
|
|
343
|
+
msg_type: 'text'
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.error('[Feishu] Failed to send text message:', err.message)
|
|
348
|
+
throw err
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* 处理对话
|
|
354
|
+
*/
|
|
355
|
+
async _processChat(openId, text, originalMsg) {
|
|
356
|
+
const sessionInfo = this._getSessionAgent(openId)
|
|
357
|
+
if (!sessionInfo) {
|
|
358
|
+
console.error('[Feishu] No session agent available')
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { agent, sessionId } = sessionInfo
|
|
363
|
+
|
|
364
|
+
// 使用 SessionPlugin 添加用户消息到历史
|
|
365
|
+
if (this._sessionPlugin) {
|
|
366
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'user', content: text })
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
let fullResponse = ''
|
|
371
|
+
|
|
372
|
+
// 使用流式响应
|
|
373
|
+
for await (const chunk of agent.chatStream(text, {
|
|
374
|
+
sessionId: sessionId
|
|
375
|
+
})) {
|
|
376
|
+
if (chunk.type === 'text' && chunk.text) {
|
|
377
|
+
fullResponse += chunk.text
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 保存助手回复到历史(使用 SessionPlugin)
|
|
382
|
+
if (this._sessionPlugin) {
|
|
383
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 发送回复
|
|
387
|
+
if (fullResponse) {
|
|
388
|
+
await this._sendMessage(openId, fullResponse, originalMsg)
|
|
389
|
+
console.log(`[Feishu] Reply sent (${fullResponse.length} characters)`)
|
|
390
|
+
} else {
|
|
391
|
+
await this._sendMessage(openId, '抱歉,我没有收到有效的回复。', originalMsg)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.error('[Feishu] Chat error:', err)
|
|
396
|
+
await this._sendMessage(openId, `发生错误:${err.message}`, originalMsg)
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 发送消息到飞书
|
|
402
|
+
* 使用 post 富文本类型
|
|
403
|
+
*/
|
|
404
|
+
async _sendMessage(openId, text, originalMsg) {
|
|
405
|
+
if (!this._client) return
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
// 获取用户所在的聊天 ID(回复到原消息所在的聊天)
|
|
409
|
+
const chatId = originalMsg?.chat_id || originalMsg?.message?.chat_id
|
|
410
|
+
if (!chatId) {
|
|
411
|
+
console.error('[Feishu] No chat_id found for reply')
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 暂时用 text 类型,等解决 post 权限问题后再切换
|
|
416
|
+
await this._client.im.message.create({
|
|
417
|
+
params: {
|
|
418
|
+
receive_id_type: 'chat_id'
|
|
419
|
+
},
|
|
420
|
+
data: {
|
|
421
|
+
receive_id: chatId,
|
|
422
|
+
content: JSON.stringify({ text: text }),
|
|
423
|
+
msg_type: 'text'
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.error('[Feishu] Failed to send message:', err.message, err.response?.data)
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 构建飞书富文本内容
|
|
433
|
+
*/
|
|
434
|
+
_buildPostContent(text) {
|
|
435
|
+
const lines = text.split('\n')
|
|
436
|
+
const paragraphs = []
|
|
437
|
+
|
|
438
|
+
for (const line of lines) {
|
|
439
|
+
if (line.trim() === '') {
|
|
440
|
+
paragraphs.push([{ tag: 'text', text: ' ' }])
|
|
441
|
+
continue
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const segments = []
|
|
445
|
+
let remaining = line
|
|
446
|
+
|
|
447
|
+
while (remaining.length > 0) {
|
|
448
|
+
// 匹配加粗 **text**
|
|
449
|
+
const boldMatch = remaining.match(/^\*\*(.+?)\*\*/)
|
|
450
|
+
if (boldMatch) {
|
|
451
|
+
segments.push({ tag: 'text', text: boldMatch[1], bold: true })
|
|
452
|
+
remaining = remaining.slice(boldMatch[0].length)
|
|
453
|
+
continue
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// 匹配斜体 *text* 或 _text_
|
|
457
|
+
const italicMatch = remaining.match(/^\*([^*]+?)\*|^_([^_]+?)_/)
|
|
458
|
+
if (italicMatch) {
|
|
459
|
+
const italicText = italicMatch[1] !== undefined ? italicMatch[1] : italicMatch[2]
|
|
460
|
+
segments.push({ tag: 'text', text: italicText, italic: true })
|
|
461
|
+
remaining = remaining.slice(italicMatch[0].length)
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 匹配行内代码 `code`
|
|
466
|
+
const codeMatch = remaining.match(/^`([^`]+?)`/)
|
|
467
|
+
if (codeMatch) {
|
|
468
|
+
segments.push({ tag: 'text', text: codeMatch[1], code: true })
|
|
469
|
+
remaining = remaining.slice(codeMatch[0].length)
|
|
470
|
+
continue
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 匹配删除线 ~~text~~
|
|
474
|
+
const delMatch = remaining.match(/^~~(.+?)~~/)
|
|
475
|
+
if (delMatch) {
|
|
476
|
+
segments.push({ tag: 'text', text: delMatch[1], strikethrough: true })
|
|
477
|
+
remaining = remaining.slice(delMatch[0].length)
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 匹配链接 [text](url)
|
|
482
|
+
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/)
|
|
483
|
+
if (linkMatch) {
|
|
484
|
+
segments.push({ tag: 'a', text: linkMatch[1], href: linkMatch[2] })
|
|
485
|
+
remaining = remaining.slice(linkMatch[0].length)
|
|
486
|
+
continue
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 普通文本
|
|
490
|
+
const nextSpecial = remaining.search(/(\*|_|`|~~|\[)/)
|
|
491
|
+
if (nextSpecial === -1) {
|
|
492
|
+
segments.push({ tag: 'text', text: remaining })
|
|
493
|
+
break
|
|
494
|
+
} else if (nextSpecial === 0) {
|
|
495
|
+
segments.push({ tag: 'text', text: remaining[0] })
|
|
496
|
+
remaining = remaining.slice(1)
|
|
497
|
+
} else {
|
|
498
|
+
segments.push({ tag: 'text', text: remaining.slice(0, nextSpecial) })
|
|
499
|
+
remaining = remaining.slice(nextSpecial)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (segments.length > 0) {
|
|
504
|
+
paragraphs.push(segments)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (paragraphs.length === 0) {
|
|
509
|
+
paragraphs.push([{ tag: 'text', text: ' ' }])
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
post: {
|
|
514
|
+
zh_cn: {
|
|
515
|
+
title: '',
|
|
516
|
+
content: paragraphs
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* 权限检查
|
|
524
|
+
*/
|
|
525
|
+
_checkPermission(openId) {
|
|
526
|
+
if (!this.config.allowedUsers || this.config.allowedUsers.length === 0) {
|
|
527
|
+
return true
|
|
528
|
+
}
|
|
529
|
+
return this.config.allowedUsers.includes(openId)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 清除会话
|
|
534
|
+
*/
|
|
535
|
+
_clearSession(openId) {
|
|
536
|
+
// 清除 SessionPlugin 中的会话(会触发 session:deleted 事件)
|
|
537
|
+
if (this._sessionPlugin) {
|
|
538
|
+
const sessionId = `feishu_${openId}`
|
|
539
|
+
this._sessionPlugin.deleteSession(sessionId)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* 获取插件状态
|
|
545
|
+
*/
|
|
546
|
+
getStatus() {
|
|
547
|
+
// 从 SessionPlugin 获取 Feishu 相关会话
|
|
548
|
+
let sessions = []
|
|
549
|
+
if (this._sessionPlugin) {
|
|
550
|
+
const allSessions = this._sessionPlugin.listSessions()
|
|
551
|
+
sessions = allSessions
|
|
552
|
+
.filter(s => s.id.startsWith('feishu_'))
|
|
553
|
+
.map(s => ({
|
|
554
|
+
openId: s.id.replace('feishu_', ''),
|
|
555
|
+
historyLength: s.messageCount,
|
|
556
|
+
lastActive: s.lastActive
|
|
557
|
+
}))
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
connected: !!this._wsClient,
|
|
562
|
+
sessionCount: sessions.length,
|
|
563
|
+
sessions,
|
|
564
|
+
config: {
|
|
565
|
+
appId: this.config.appId ? `${this.config.appId.substring(0, 10)}...` : null,
|
|
566
|
+
allowedUsersCount: this.config.allowedUsers.length
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* 停止客户端
|
|
573
|
+
*/
|
|
574
|
+
stopClient() {
|
|
575
|
+
if (this._wsClient) {
|
|
576
|
+
this._wsClient = null
|
|
577
|
+
this._client = null
|
|
578
|
+
console.log('[Feishu] Client stopped')
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
reload(framework) {
|
|
583
|
+
this._framework = framework
|
|
584
|
+
this._initialized = false
|
|
585
|
+
this.stopClient()
|
|
586
|
+
this.start(framework)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
uninstall(framework) {
|
|
590
|
+
// 销毁所有 session agents
|
|
591
|
+
for (const agent of this._sessionAgents.values()) {
|
|
592
|
+
agent.destroy()
|
|
593
|
+
}
|
|
594
|
+
this._sessionAgents.clear()
|
|
595
|
+
|
|
596
|
+
// 清理去重定时器
|
|
597
|
+
if (this._messageCleanupInterval) {
|
|
598
|
+
clearInterval(this._messageCleanupInterval)
|
|
599
|
+
this._messageCleanupInterval = null
|
|
600
|
+
}
|
|
601
|
+
this._processedMessages.clear()
|
|
602
|
+
|
|
603
|
+
this.stopClient()
|
|
604
|
+
if (this._sessionPlugin && this._sessionDeleteHandler) {
|
|
605
|
+
this._sessionPlugin.off('session:deleted', this._sessionDeleteHandler)
|
|
606
|
+
this._sessionDeleteHandler = null
|
|
607
|
+
}
|
|
608
|
+
this._sessionPlugin = null
|
|
609
|
+
this._framework = null
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
@@ -35,7 +35,7 @@ module.exports = function(Plugin) {
|
|
|
35
35
|
allowedChats: config.allowedChats || [],
|
|
36
36
|
groupMode: config.groupMode || false,
|
|
37
37
|
prefix: config.prefix || '/',
|
|
38
|
-
systemPrompt: config.systemPrompt || '你是一个有帮助的AI
|
|
38
|
+
systemPrompt: config.systemPrompt || '你是一个有帮助的AI助手。回复内容不要使用markdown格式文本。'
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
this._framework = null
|
package/plugins/weixin-plugin.js
CHANGED
|
@@ -24,7 +24,7 @@ module.exports = function(Plugin) {
|
|
|
24
24
|
this.config = {
|
|
25
25
|
forceLogin: config.forceLogin || process.env.WEIXIN_FORCE_LOGIN === 'true',
|
|
26
26
|
qrcodeTerminal: config.qrcodeTerminal !== false && process.env.WEIXIN_QRCODE_TERMINAL !== 'false',
|
|
27
|
-
systemPrompt:
|
|
27
|
+
systemPrompt: '你是一个微信助手。回复内容不要使用markdown格式文本',
|
|
28
28
|
allowedUsers: config.allowedUsers || []
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -159,7 +159,9 @@ module.exports = function(Plugin) {
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// 创建新 agent
|
|
162
|
-
const agent = this._framework.createSessionAgent(`weixin_${userId}
|
|
162
|
+
const agent = this._framework.createSessionAgent(`weixin_${userId}`, {
|
|
163
|
+
systemPrompt: this.config.systemPrompt
|
|
164
|
+
})
|
|
163
165
|
this._sessionAgents.set(userId, agent)
|
|
164
166
|
console.log('[WeChat] Created new session agent for userId:', userId)
|
|
165
167
|
|