foliko 1.0.40 → 1.0.43
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/commands/chat.js +5 -5
- 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 +298 -537
- package/plugins/scheduler-plugin.js +1 -1
- package/plugins/subagent-plugin.js +4 -4
- package/plugins/telegram-plugin.js +314 -522
- package/plugins/think-plugin.js +2 -2
- package/plugins/web-plugin.js +542 -0
- package/plugins/weixin-plugin.js +327 -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,389 @@
|
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
**命令执行规范:**
|
|
24
|
+
- 执行 npx skills add / npx skills remove / npx skills list 命令时,必须自动添加参数:-a openclaw -y
|
|
25
|
+
- 禁止添加 -g 参数
|
|
26
|
+
- 例如:npx skills add xxx 应该执行为 npx skills add xxx -a openclaw -y`
|
|
27
|
+
|
|
28
|
+
this.config = {
|
|
29
|
+
appId: config.appId || process.env.FEISHU_APP_ID,
|
|
30
|
+
appSecret: config.appSecret || process.env.FEISHU_APP_SECRET,
|
|
31
|
+
allowedUsers: config.allowedUsers || []
|
|
41
32
|
}
|
|
42
33
|
|
|
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
|
-
}
|
|
34
|
+
this._framework = null
|
|
35
|
+
this._client = null
|
|
36
|
+
this._wsClient = null
|
|
37
|
+
this._sessionPlugin = null
|
|
38
|
+
this._sessionDeleteHandler = null
|
|
39
|
+
this._sessionAgents = new Map()
|
|
40
|
+
this._initialized = false
|
|
41
|
+
this._processedMessages = new Set()
|
|
42
|
+
this._messageCleanupInterval = null
|
|
43
|
+
this._processingLock = new Map()
|
|
44
|
+
}
|
|
57
45
|
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
install(framework) {
|
|
47
|
+
this._framework = framework
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
60
50
|
|
|
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
|
-
}
|
|
51
|
+
start(framework) {
|
|
52
|
+
if (this._initialized) return this
|
|
53
|
+
this._initialized = true
|
|
71
54
|
|
|
72
|
-
|
|
55
|
+
if (!this.config.appId || !this.config.appSecret) {
|
|
56
|
+
console.warn('[Feishu] No appId or appSecret.')
|
|
73
57
|
return this
|
|
74
58
|
}
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*/
|
|
79
|
-
_initClient() {
|
|
80
|
-
try {
|
|
81
|
-
const { Client, WSClient, EventDispatcher } = require('@larksuiteoapi/node-sdk')
|
|
60
|
+
this._framework = framework
|
|
61
|
+
this._sessionPlugin = framework.pluginManager.get('session')
|
|
82
62
|
|
|
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
|
-
})
|
|
63
|
+
if (this._sessionPlugin) {
|
|
64
|
+
this._sessionDeleteHandler = (sessionId) => {
|
|
65
|
+
if (sessionId.startsWith('feishu_')) {
|
|
66
|
+
console.log(`[Feishu] Session deleted: ${sessionId}`)
|
|
116
67
|
}
|
|
117
|
-
|
|
118
|
-
console.log('[Feishu] WebSocket client started')
|
|
119
|
-
} catch (err) {
|
|
120
|
-
console.error('[Feishu] Failed to initialize client:', err.message, err.stack)
|
|
121
68
|
}
|
|
69
|
+
this._sessionPlugin.on('session:deleted', this._sessionDeleteHandler)
|
|
122
70
|
}
|
|
123
71
|
|
|
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
|
-
}
|
|
72
|
+
this._initClient()
|
|
73
|
+
return this
|
|
74
|
+
}
|
|
171
75
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return
|
|
176
|
-
}
|
|
76
|
+
_initClient() {
|
|
77
|
+
try {
|
|
78
|
+
const { Client, WSClient, EventDispatcher } = require('@larksuiteoapi/node-sdk')
|
|
177
79
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
80
|
+
this._client = new Client({
|
|
81
|
+
appId: this.config.appId,
|
|
82
|
+
appSecret: this.config.appSecret,
|
|
83
|
+
logger: console
|
|
84
|
+
})
|
|
184
85
|
|
|
185
|
-
|
|
186
|
-
|
|
86
|
+
this._wsClient = new WSClient({
|
|
87
|
+
appId: this.config.appId,
|
|
88
|
+
appSecret: this.config.appSecret,
|
|
89
|
+
logger: console
|
|
90
|
+
})
|
|
187
91
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
await this.
|
|
191
|
-
return
|
|
92
|
+
const eventDispatcher = new EventDispatcher({}).register({
|
|
93
|
+
'im.message.receive_v1': async (data) => {
|
|
94
|
+
await this._handleMessage(data)
|
|
192
95
|
}
|
|
96
|
+
})
|
|
193
97
|
|
|
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)
|
|
98
|
+
this._wsClient.start({ eventDispatcher })
|
|
200
99
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this._processingLock.set(openId, false)
|
|
205
|
-
}
|
|
100
|
+
this._messageCleanupInterval = setInterval(() => {
|
|
101
|
+
this._processedMessages.clear()
|
|
102
|
+
}, 60000)
|
|
206
103
|
|
|
207
|
-
|
|
208
|
-
|
|
104
|
+
if (this._framework) {
|
|
105
|
+
this._framework.on('scheduler:reminder', async (data) => {
|
|
106
|
+
await this._handleScheduledReminder(data)
|
|
107
|
+
})
|
|
209
108
|
}
|
|
210
|
-
}
|
|
211
109
|
|
|
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
|
-
}
|
|
110
|
+
console.log('[Feishu] WebSocket client started')
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('[Feishu] Failed to initialize client:', err.message, err.stack)
|
|
258
113
|
}
|
|
114
|
+
}
|
|
259
115
|
|
|
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}` }
|
|
116
|
+
async _handleMessage(data) {
|
|
117
|
+
try {
|
|
118
|
+
if (!data || !data.message || Object.keys(data.message).length === 0) {
|
|
119
|
+
return
|
|
270
120
|
}
|
|
271
121
|
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
122
|
+
const message = data.message
|
|
123
|
+
const chatId = message.chat_id
|
|
124
|
+
const messageId = message.message_id
|
|
125
|
+
const openId = message.sender?.sender_id?.open_id || message.sender?.open_id || message.open_id || chatId
|
|
126
|
+
const messageType = message.message_type
|
|
127
|
+
let content = {}
|
|
278
128
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
metadata: { platform: 'feishu', openId }
|
|
284
|
-
})
|
|
129
|
+
try {
|
|
130
|
+
content = message.content ? JSON.parse(message.content) : {}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return
|
|
285
133
|
}
|
|
286
134
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
135
|
+
if (messageId && this._processedMessages.has(messageId)) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (messageId) this._processedMessages.add(messageId)
|
|
290
139
|
|
|
291
|
-
|
|
292
|
-
* 处理定时提醒
|
|
293
|
-
*/
|
|
294
|
-
async _handleScheduledReminder(data) {
|
|
295
|
-
const { taskName, message, sessionId } = data
|
|
140
|
+
if (messageType !== 'text') return
|
|
296
141
|
|
|
297
|
-
|
|
298
|
-
|
|
142
|
+
const text = content.text?.trim()
|
|
143
|
+
if (!text) return
|
|
299
144
|
|
|
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
|
-
}
|
|
145
|
+
if (!this._checkPermission(openId)) return
|
|
311
146
|
|
|
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')
|
|
147
|
+
if (text.startsWith('/')) {
|
|
148
|
+
await this._handleCommand(openId, text, message)
|
|
149
|
+
return
|
|
332
150
|
}
|
|
333
|
-
}
|
|
334
151
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*/
|
|
338
|
-
async _sendTextMessage(openId, text) {
|
|
339
|
-
if (!this._client) return
|
|
152
|
+
if (this._processingLock.get(openId)) return
|
|
153
|
+
this._processingLock.set(openId, true)
|
|
340
154
|
|
|
341
155
|
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
|
|
156
|
+
await this._processChat(openId, text, message)
|
|
157
|
+
} finally {
|
|
158
|
+
this._processingLock.set(openId, false)
|
|
355
159
|
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('[Feishu] Error handling message:', err.message)
|
|
356
162
|
}
|
|
163
|
+
}
|
|
357
164
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
165
|
+
async _handleCommand(openId, text, originalMsg) {
|
|
166
|
+
const parts = text.split(' ')
|
|
167
|
+
const command = parts[0].substring(1)
|
|
168
|
+
const args = parts.slice(1).join(' ')
|
|
169
|
+
|
|
170
|
+
switch (command.toLowerCase()) {
|
|
171
|
+
case 'start':
|
|
172
|
+
case 'help':
|
|
173
|
+
await this._sendMessage(openId,
|
|
174
|
+
'👋 欢迎使用 AI 助手!\n\n直接发送消息即可与我对话。\n\n可用命令:\n/start - 显示帮助\n/clear - 清除对话历史\n/history - 查看历史消息数',
|
|
175
|
+
originalMsg)
|
|
176
|
+
break
|
|
177
|
+
case 'clear':
|
|
178
|
+
this._clearSession(openId)
|
|
179
|
+
await this._sendMessage(openId, '✅ 对话历史已清除', originalMsg)
|
|
180
|
+
break
|
|
181
|
+
case 'history':
|
|
182
|
+
if (this._sessionPlugin) {
|
|
183
|
+
const session = this._sessionPlugin.getSession(`feishu_${openId}`)
|
|
184
|
+
const sessions = this._sessionPlugin.listSessions().filter(s => s.id.startsWith('feishu_'))
|
|
185
|
+
await this._sendMessage(openId,
|
|
186
|
+
`📊 当前状态\n\n会话数:${sessions.length}\n历史消息:${session?.messages?.length || 0}\n最后活跃:${session?.lastActive?.toLocaleString() || '无'}`,
|
|
187
|
+
originalMsg)
|
|
188
|
+
} else {
|
|
189
|
+
await this._sendMessage(openId, '❌ 会话服务不可用', originalMsg)
|
|
190
|
+
}
|
|
191
|
+
break
|
|
192
|
+
default:
|
|
193
|
+
await this._processChat(openId, text, originalMsg)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
367
196
|
|
|
368
|
-
|
|
197
|
+
_getSessionAgent(openId) {
|
|
198
|
+
if (this._sessionAgents.has(openId)) {
|
|
199
|
+
return { agent: this._sessionAgents.get(openId), sessionId: `feishu_${openId}` }
|
|
200
|
+
}
|
|
369
201
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
202
|
+
const agent = this._framework.createSessionAgent(`feishu_${openId}`, {
|
|
203
|
+
systemPrompt: this.systemPrompt,
|
|
204
|
+
sharedPrompt: `工作目录: {{WORK_DIR}}`,
|
|
205
|
+
metadata: { WORK_DIR: process.cwd() }
|
|
206
|
+
})
|
|
207
|
+
this._sessionAgents.set(openId, agent)
|
|
374
208
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
sessionId: sessionId
|
|
381
|
-
})) {
|
|
382
|
-
if (chunk.type === 'text' && chunk.text) {
|
|
383
|
-
fullResponse += chunk.text
|
|
384
|
-
}
|
|
385
|
-
}
|
|
209
|
+
if (this._sessionPlugin) {
|
|
210
|
+
this._sessionPlugin.getOrCreateSession(`feishu_${openId}`, {
|
|
211
|
+
metadata: { platform: 'feishu', openId }
|
|
212
|
+
})
|
|
213
|
+
}
|
|
386
214
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
|
|
390
|
-
}
|
|
215
|
+
return { agent, sessionId: `feishu_${openId}` }
|
|
216
|
+
}
|
|
391
217
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
console.log(`[Feishu] Reply sent (${fullResponse.length} characters)`)
|
|
396
|
-
} else {
|
|
397
|
-
await this._sendMessage(openId, '抱歉,我没有收到有效的回复。', originalMsg)
|
|
398
|
-
}
|
|
218
|
+
async _handleScheduledReminder(data) {
|
|
219
|
+
const { taskName, message, sessionId } = data
|
|
220
|
+
const reminderText = `🔔 [${taskName}]\n\n${message}`
|
|
399
221
|
|
|
222
|
+
if (sessionId && sessionId.startsWith('feishu_')) {
|
|
223
|
+
const openId = sessionId.replace('feishu_', '')
|
|
224
|
+
try {
|
|
225
|
+
await this._sendTextMessage(openId, reminderText)
|
|
400
226
|
} catch (err) {
|
|
401
|
-
console.error(
|
|
402
|
-
await this._sendMessage(openId, `发生错误:${err.message}`, originalMsg)
|
|
227
|
+
console.error(`[Feishu] Failed to send reminder:`, err.message)
|
|
403
228
|
}
|
|
229
|
+
return
|
|
404
230
|
}
|
|
405
231
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
async _sendMessage(openId, text, originalMsg) {
|
|
411
|
-
if (!this._client) return
|
|
232
|
+
if (this._sessionPlugin) {
|
|
233
|
+
const sessions = this._sessionPlugin.listSessions()
|
|
234
|
+
.filter(s => s.id.startsWith('feishu_'))
|
|
235
|
+
.sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime())
|
|
412
236
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
237
|
+
if (sessions.length > 0) {
|
|
238
|
+
const openId = sessions[0].id.replace('feishu_', '')
|
|
239
|
+
try {
|
|
240
|
+
await this._sendTextMessage(openId, reminderText)
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error(`[Feishu] Failed to send reminder:`, err.message)
|
|
419
243
|
}
|
|
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
244
|
}
|
|
435
245
|
}
|
|
246
|
+
}
|
|
436
247
|
|
|
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
|
-
}
|
|
248
|
+
async _processChat(openId, text, originalMsg) {
|
|
249
|
+
const sessionInfo = this._getSessionAgent(openId)
|
|
250
|
+
if (!sessionInfo) return
|
|
449
251
|
|
|
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
|
-
}
|
|
252
|
+
const { agent, sessionId } = sessionInfo
|
|
253
|
+
|
|
254
|
+
if (this._sessionPlugin) {
|
|
255
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'user', content: text })
|
|
256
|
+
}
|
|
508
257
|
|
|
509
|
-
|
|
510
|
-
|
|
258
|
+
try {
|
|
259
|
+
let fullResponse = ''
|
|
260
|
+
for await (const chunk of agent.chatStream(text, { sessionId })) {
|
|
261
|
+
if (chunk.type === 'text' && chunk.text) {
|
|
262
|
+
fullResponse += chunk.text
|
|
511
263
|
}
|
|
512
264
|
}
|
|
513
265
|
|
|
514
|
-
if (
|
|
515
|
-
|
|
266
|
+
if (this._sessionPlugin) {
|
|
267
|
+
this._sessionPlugin.addMessage(sessionId, { role: 'assistant', content: fullResponse })
|
|
516
268
|
}
|
|
517
269
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
content: paragraphs
|
|
523
|
-
}
|
|
524
|
-
}
|
|
270
|
+
if (fullResponse) {
|
|
271
|
+
await this._sendMessage(openId, fullResponse, originalMsg)
|
|
272
|
+
} else {
|
|
273
|
+
await this._sendMessage(openId, '抱歉,我没有收到有效的回复。', originalMsg)
|
|
525
274
|
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('[Feishu] Chat error:', err)
|
|
277
|
+
await this._sendMessage(openId, `发生错误:${err.message}`, originalMsg)
|
|
526
278
|
}
|
|
279
|
+
}
|
|
527
280
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
281
|
+
async _sendTextMessage(openId, text) {
|
|
282
|
+
if (!this._client) return
|
|
283
|
+
try {
|
|
284
|
+
await this._client.im.message.create({
|
|
285
|
+
params: { receive_id_type: 'chat_id' },
|
|
286
|
+
data: {
|
|
287
|
+
receive_id: openId,
|
|
288
|
+
content: JSON.stringify({ text }),
|
|
289
|
+
msg_type: 'text'
|
|
290
|
+
}
|
|
291
|
+
})
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error('[Feishu] Failed to send text message:', err.message)
|
|
294
|
+
throw err
|
|
536
295
|
}
|
|
296
|
+
}
|
|
537
297
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
298
|
+
async _sendMessage(openId, text, originalMsg) {
|
|
299
|
+
if (!this._client) return
|
|
300
|
+
try {
|
|
301
|
+
const chatId = originalMsg?.chat_id || originalMsg?.message?.chat_id
|
|
302
|
+
if (!chatId) return
|
|
303
|
+
|
|
304
|
+
await this._client.im.message.create({
|
|
305
|
+
params: { receive_id_type: 'chat_id' },
|
|
306
|
+
data: {
|
|
307
|
+
receive_id: chatId,
|
|
308
|
+
content: JSON.stringify({ text }),
|
|
309
|
+
msg_type: 'text'
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error('[Feishu] Failed to send message:', err.message)
|
|
547
314
|
}
|
|
315
|
+
}
|
|
548
316
|
|
|
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
|
-
}
|
|
317
|
+
_checkPermission(openId) {
|
|
318
|
+
return this.config.allowedUsers.length === 0 || this.config.allowedUsers.includes(openId)
|
|
319
|
+
}
|
|
565
320
|
|
|
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
|
-
}
|
|
321
|
+
_clearSession(openId) {
|
|
322
|
+
if (this._sessionPlugin) {
|
|
323
|
+
this._sessionPlugin.deleteSession(`feishu_${openId}`)
|
|
575
324
|
}
|
|
325
|
+
}
|
|
576
326
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
327
|
+
getStatus() {
|
|
328
|
+
let sessions = []
|
|
329
|
+
if (this._sessionPlugin) {
|
|
330
|
+
sessions = this._sessionPlugin.listSessions()
|
|
331
|
+
.filter(s => s.id.startsWith('feishu_'))
|
|
332
|
+
.map(s => ({
|
|
333
|
+
openId: s.id.replace('feishu_', ''),
|
|
334
|
+
historyLength: s.messageCount,
|
|
335
|
+
lastActive: s.lastActive
|
|
336
|
+
}))
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
connected: !!this._wsClient,
|
|
340
|
+
sessionCount: sessions.length,
|
|
341
|
+
sessions,
|
|
342
|
+
config: {
|
|
343
|
+
appId: this.config.appId ? `${this.config.appId.substring(0, 10)}...` : null,
|
|
344
|
+
allowedUsersCount: this.config.allowedUsers.length
|
|
590
345
|
}
|
|
591
346
|
}
|
|
347
|
+
}
|
|
592
348
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
this.
|
|
349
|
+
stopClient() {
|
|
350
|
+
if (this._wsClient) {
|
|
351
|
+
try { this._wsClient.close() } catch (e) { /* ignore */ }
|
|
352
|
+
this._wsClient = null
|
|
353
|
+
this._client = null
|
|
354
|
+
console.log('[Feishu] Client stopped')
|
|
598
355
|
}
|
|
356
|
+
}
|
|
599
357
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
this.stopClient()
|
|
604
|
-
this.start(framework)
|
|
605
|
-
}
|
|
358
|
+
stop() {
|
|
359
|
+
this.stopClient()
|
|
360
|
+
}
|
|
606
361
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
362
|
+
reload(framework) {
|
|
363
|
+
this._framework = framework
|
|
364
|
+
this._initialized = false
|
|
365
|
+
this.stopClient()
|
|
366
|
+
this.start(framework)
|
|
367
|
+
}
|
|
613
368
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
this._processedMessages.clear()
|
|
369
|
+
uninstall() {
|
|
370
|
+
for (const agent of this._sessionAgents.values()) {
|
|
371
|
+
agent.destroy()
|
|
372
|
+
}
|
|
373
|
+
this._sessionAgents.clear()
|
|
620
374
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
this._sessionDeleteHandler = null
|
|
625
|
-
}
|
|
626
|
-
this._sessionPlugin = null
|
|
627
|
-
this._framework = null
|
|
375
|
+
if (this._messageCleanupInterval) {
|
|
376
|
+
clearInterval(this._messageCleanupInterval)
|
|
377
|
+
this._messageCleanupInterval = null
|
|
628
378
|
}
|
|
379
|
+
this._processedMessages.clear()
|
|
380
|
+
|
|
381
|
+
this.stopClient()
|
|
382
|
+
if (this._sessionPlugin && this._sessionDeleteHandler) {
|
|
383
|
+
this._sessionPlugin.off('session:deleted', this._sessionDeleteHandler)
|
|
384
|
+
this._sessionDeleteHandler = null
|
|
385
|
+
}
|
|
386
|
+
this._sessionPlugin = null
|
|
387
|
+
this._framework = null
|
|
629
388
|
}
|
|
630
389
|
}
|
|
390
|
+
|
|
391
|
+
module.exports = { FeishuPlugin }
|