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