@wenbin_wb/dsh-bridge 2.3.3 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +85 -8
- package/README.md +85 -8
- package/client/client.js +832 -57
- package/client/index.js +718 -61
- package/docs/telegram-usage.md +89 -0
- package/lib/auth/login-template.js +378 -0
- package/lib/auth/manager.js +425 -0
- package/lib/bridge-rpc-constants.js +6 -0
- package/lib/bridge-rpc.js +34 -1
- package/lib/feishu/gateway.js +166 -7
- package/lib/feishu/node.js +55 -10
- package/lib/index.js +258 -14
- package/lib/platform/base.js +13 -0
- package/lib/platform/conversation-bridge.js +32 -4
- package/lib/qq/gateway.js +94 -14
- package/lib/qq/node.js +49 -9
- package/lib/telegram/gateway.js +619 -0
- package/lib/telegram/index.js +210 -0
- package/lib/telegram/node.js +342 -0
- package/lib/wechat/gateway.js +54 -0
- package/lib/wechat/node.js +1 -0
- package/package.json +4 -3
package/lib/feishu/gateway.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Official SDK: @larksuiteoapi/node-sdk
|
|
3
3
|
// Reference: https://open.feishu.cn/document/home/event-subscription-via-websocket/overview
|
|
4
4
|
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
5
7
|
import { Service } from '@deepseek-ai/cordis'
|
|
6
8
|
import * as Lark from '@larksuiteoapi/node-sdk'
|
|
7
9
|
|
|
@@ -144,6 +146,7 @@ export class FeishuGateway extends Service {
|
|
|
144
146
|
// 5. 启动 WebSocket 长连接
|
|
145
147
|
await this.wsClient.start({ eventDispatcher: this.eventDispatcher })
|
|
146
148
|
this.setStatus('online')
|
|
149
|
+
this._startWatchdog()
|
|
147
150
|
this.logger.info?.('[dsh-bridge feishu] WebSocket connected to Feishu Open Platform')
|
|
148
151
|
return true
|
|
149
152
|
} catch (err) {
|
|
@@ -158,8 +161,28 @@ export class FeishuGateway extends Service {
|
|
|
158
161
|
return this._startingPromise
|
|
159
162
|
}
|
|
160
163
|
|
|
164
|
+
_startWatchdog() {
|
|
165
|
+
this._stopWatchdog()
|
|
166
|
+
this._watchdogTimer = setInterval(async () => {
|
|
167
|
+
if (this.status === 'online' && !this._closing && this.configured && this.client) {
|
|
168
|
+
try {
|
|
169
|
+
await this.client.bot.v3.bot.get({}).catch(() => null)
|
|
170
|
+
} catch (err) {
|
|
171
|
+
this.logger?.warn?.('[dsh-bridge feishu] liveness probe warning: %s', err?.message ?? err)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}, 60_000)
|
|
175
|
+
if (typeof this._watchdogTimer?.unref === 'function') this._watchdogTimer.unref()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_stopWatchdog() {
|
|
179
|
+
if (this._watchdogTimer) clearInterval(this._watchdogTimer)
|
|
180
|
+
this._watchdogTimer = null
|
|
181
|
+
}
|
|
182
|
+
|
|
161
183
|
async stop() {
|
|
162
184
|
this._closing = true
|
|
185
|
+
this._stopWatchdog()
|
|
163
186
|
try {
|
|
164
187
|
if (this.wsClient) {
|
|
165
188
|
const wsInstance = this.wsClient.wsConfig?.getWSInstance?.()
|
|
@@ -199,15 +222,16 @@ export class FeishuGateway extends Service {
|
|
|
199
222
|
const chatType = message.chat_type || 'p2p' // 'p2p' or 'group'
|
|
200
223
|
const isGroup = chatType === 'group'
|
|
201
224
|
|
|
202
|
-
//
|
|
225
|
+
// 解析消息体(文本 / 图片 / 文件 / 音频 / 视频)
|
|
203
226
|
let text = ''
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
text = message.content || ''
|
|
227
|
+
let contentObj = {}
|
|
228
|
+
try {
|
|
229
|
+
contentObj = JSON.parse(message.content || '{}')
|
|
230
|
+
if (message.message_type === 'text') {
|
|
231
|
+
text = contentObj.text || ''
|
|
210
232
|
}
|
|
233
|
+
} catch {
|
|
234
|
+
text = message.content || ''
|
|
211
235
|
}
|
|
212
236
|
|
|
213
237
|
// 群聊中剥离机器人 @ 占位符(例如 @_user_1 等)
|
|
@@ -226,12 +250,54 @@ export class FeishuGateway extends Service {
|
|
|
226
250
|
isGroup,
|
|
227
251
|
messageId,
|
|
228
252
|
text,
|
|
253
|
+
contentObj,
|
|
229
254
|
messageType: message.message_type,
|
|
230
255
|
raw: data,
|
|
231
256
|
createTime: Number(message.create_time || Date.now()),
|
|
232
257
|
})
|
|
233
258
|
}
|
|
234
259
|
|
|
260
|
+
/**
|
|
261
|
+
* 下载消息中的资源文件(图片/文件/音频/媒体)
|
|
262
|
+
* @param {object} opts
|
|
263
|
+
* @param {string} opts.messageId 消息 ID
|
|
264
|
+
* @param {string} opts.fileKey 资源 Key
|
|
265
|
+
* @param {string} [opts.type] 'file' | 'image'
|
|
266
|
+
* @returns {Promise<Buffer|null>}
|
|
267
|
+
*/
|
|
268
|
+
async downloadMessageResource({ messageId, fileKey, type = 'file' }) {
|
|
269
|
+
if (!this.client) throw new Error('Feishu client not initialized')
|
|
270
|
+
try {
|
|
271
|
+
const resp = await this.client.im.messageResource.get({
|
|
272
|
+
path: {
|
|
273
|
+
message_id: messageId,
|
|
274
|
+
file_key: fileKey,
|
|
275
|
+
},
|
|
276
|
+
params: {
|
|
277
|
+
type: type === 'image' ? 'image' : 'file',
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
if (Buffer.isBuffer(resp)) return resp
|
|
281
|
+
if (resp && typeof resp.pipe === 'function') {
|
|
282
|
+
const chunks = []
|
|
283
|
+
for await (const chunk of resp) chunks.push(Buffer.from(chunk))
|
|
284
|
+
return Buffer.concat(chunks)
|
|
285
|
+
}
|
|
286
|
+
if (resp?.data) {
|
|
287
|
+
if (Buffer.isBuffer(resp.data)) return resp.data
|
|
288
|
+
if (resp.data.pipe) {
|
|
289
|
+
const chunks = []
|
|
290
|
+
for await (const chunk of resp.data) chunks.push(Buffer.from(chunk))
|
|
291
|
+
return Buffer.concat(chunks)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return null
|
|
295
|
+
} catch (err) {
|
|
296
|
+
this.logger?.warn?.('[dsh-bridge feishu] downloadMessageResource error: %s', err?.message ?? err)
|
|
297
|
+
return null
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
235
301
|
async _handleCardActionTrigger(data) {
|
|
236
302
|
const action = data?.action || {}
|
|
237
303
|
const operator = data?.operator || {}
|
|
@@ -382,6 +448,99 @@ export class FeishuGateway extends Service {
|
|
|
382
448
|
}
|
|
383
449
|
}
|
|
384
450
|
|
|
451
|
+
/**
|
|
452
|
+
* 上传并发送本地图片
|
|
453
|
+
*/
|
|
454
|
+
async sendLocalImage(receiveId, filePath, opts = {}) {
|
|
455
|
+
if (!this.client || !receiveId || !filePath) return null
|
|
456
|
+
let receiveIdType = opts.receiveIdType
|
|
457
|
+
if (!receiveIdType) {
|
|
458
|
+
if (receiveId.startsWith('ou_')) receiveIdType = 'open_id'
|
|
459
|
+
else if (receiveId.startsWith('oc_')) receiveIdType = 'chat_id'
|
|
460
|
+
else if (opts.isGroup) receiveIdType = 'chat_id'
|
|
461
|
+
else receiveIdType = 'open_id'
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
try {
|
|
465
|
+
const imageStream = fs.createReadStream(filePath)
|
|
466
|
+
const uploadRes = await this.client.im.v1.image.create({
|
|
467
|
+
data: {
|
|
468
|
+
image_type: 'message',
|
|
469
|
+
image: imageStream,
|
|
470
|
+
},
|
|
471
|
+
})
|
|
472
|
+
const imageKey = uploadRes?.image_key
|
|
473
|
+
if (!imageKey) throw new Error('Feishu image upload returned no image_key')
|
|
474
|
+
|
|
475
|
+
const res = await this.client.im.v1.message.create({
|
|
476
|
+
params: { receive_id_type: receiveIdType },
|
|
477
|
+
data: {
|
|
478
|
+
receive_id: receiveId,
|
|
479
|
+
msg_type: 'image',
|
|
480
|
+
content: JSON.stringify({ image_key: imageKey }),
|
|
481
|
+
},
|
|
482
|
+
})
|
|
483
|
+
return res?.data
|
|
484
|
+
} catch (err) {
|
|
485
|
+
this.logger.warn?.(`[dsh-bridge feishu] sendLocalImage error (${receiveId}, ${filePath}):`, err?.message ?? err)
|
|
486
|
+
return null
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 上传并发送本地文件
|
|
492
|
+
*/
|
|
493
|
+
async sendLocalFile(receiveId, filePath, opts = {}) {
|
|
494
|
+
if (!this.client || !receiveId || !filePath) return null
|
|
495
|
+
let receiveIdType = opts.receiveIdType
|
|
496
|
+
if (!receiveIdType) {
|
|
497
|
+
if (receiveId.startsWith('ou_')) receiveIdType = 'open_id'
|
|
498
|
+
else if (receiveId.startsWith('oc_')) receiveIdType = 'chat_id'
|
|
499
|
+
else if (opts.isGroup) receiveIdType = 'chat_id'
|
|
500
|
+
else receiveIdType = 'open_id'
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
const fileName = path.basename(filePath)
|
|
505
|
+
const fileStream = fs.createReadStream(filePath)
|
|
506
|
+
const uploadRes = await this.client.im.v1.file.create({
|
|
507
|
+
data: {
|
|
508
|
+
file_type: 'stream',
|
|
509
|
+
file_name: fileName,
|
|
510
|
+
file: fileStream,
|
|
511
|
+
},
|
|
512
|
+
})
|
|
513
|
+
const fileKey = uploadRes?.file_key
|
|
514
|
+
if (!fileKey) throw new Error('Feishu file upload returned no file_key')
|
|
515
|
+
|
|
516
|
+
const res = await this.client.im.v1.message.create({
|
|
517
|
+
params: { receive_id_type: receiveIdType },
|
|
518
|
+
data: {
|
|
519
|
+
receive_id: receiveId,
|
|
520
|
+
msg_type: 'file',
|
|
521
|
+
content: JSON.stringify({ file_key: fileKey }),
|
|
522
|
+
},
|
|
523
|
+
})
|
|
524
|
+
return res?.data
|
|
525
|
+
} catch (err) {
|
|
526
|
+
this.logger.warn?.(`[dsh-bridge feishu] sendLocalFile error (${receiveId}, ${filePath}):`, err?.message ?? err)
|
|
527
|
+
return null
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 发送本地媒体文件(自动识别图片/文档)
|
|
533
|
+
*/
|
|
534
|
+
async sendMediaFile(receiveId, filePath, opts = {}) {
|
|
535
|
+
if (!fs.existsSync(filePath)) return null
|
|
536
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
537
|
+
const isImage = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(ext)
|
|
538
|
+
if (isImage) {
|
|
539
|
+
return this.sendLocalImage(receiveId, filePath, opts)
|
|
540
|
+
}
|
|
541
|
+
return this.sendLocalFile(receiveId, filePath, opts)
|
|
542
|
+
}
|
|
543
|
+
|
|
385
544
|
// 释放资源
|
|
386
545
|
dispose() {
|
|
387
546
|
void this.stop()
|
package/lib/feishu/node.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// 把飞书 OpenAPI / WebSocket 长连接入站事件解析后交给平台无关的
|
|
3
3
|
// ConversationBridge 处理,出站通过 FeishuGateway 发送文本 / 卡片。
|
|
4
4
|
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
5
7
|
import { ConversationBridge, conversationBridgeHelpers } from '../platform/conversation-bridge.js'
|
|
6
8
|
|
|
7
9
|
function makePlatform(gateway) {
|
|
@@ -12,6 +14,9 @@ function makePlatform(gateway) {
|
|
|
12
14
|
async sendText(peerId, text, opts = {}) {
|
|
13
15
|
return gateway?.sendMarkdownCard(peerId, text, opts)
|
|
14
16
|
},
|
|
17
|
+
async sendMediaFile(peerId, filePath, opts = {}) {
|
|
18
|
+
return gateway?.sendMediaFile(peerId, filePath, opts)
|
|
19
|
+
},
|
|
15
20
|
async sendTyping() {},
|
|
16
21
|
dispose() {},
|
|
17
22
|
}
|
|
@@ -70,14 +75,10 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
70
75
|
this._inTurn = false
|
|
71
76
|
|
|
72
77
|
// 订阅网关入站消息事件
|
|
73
|
-
this.ctx.on('feishu/message', (event) =>
|
|
74
|
-
void this._handleInbound(event)
|
|
75
|
-
})
|
|
78
|
+
this.ctx.on('feishu/message', (event) => this._handleInbound(event))
|
|
76
79
|
|
|
77
80
|
// 订阅卡片交互事件(审批按钮点击)
|
|
78
|
-
this.ctx.on('feishu/action', (event) =>
|
|
79
|
-
void this._handleAction(event)
|
|
80
|
-
})
|
|
81
|
+
this.ctx.on('feishu/action', (event) => this._handleAction(event))
|
|
81
82
|
|
|
82
83
|
// 监听轮次事件:turn/start 开启流式会话,turn/end 最终刷新并重置
|
|
83
84
|
this.ctx.on('session/event', (session, event) => {
|
|
@@ -106,10 +107,26 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
async _handleInbound(event) {
|
|
109
|
-
const { peerId, senderId, isGroup, text } = event
|
|
110
|
+
const { peerId, senderId, isGroup, text, messageId, messageType, contentObj } = event
|
|
110
111
|
this._lastPeer = { peerId, senderId, isGroup }
|
|
111
112
|
|
|
112
|
-
|
|
113
|
+
let mediaFiles = []
|
|
114
|
+
if (messageType && messageType !== 'text') {
|
|
115
|
+
mediaFiles = await this._processMedia(messageId, messageType, contentObj, this.config.cwd)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!text && mediaFiles.length === 0) {
|
|
119
|
+
this.logger.debug?.(`[dsh-bridge feishu] ignore empty message from ${peerId}`)
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let fullText = text || ''
|
|
124
|
+
if (mediaFiles.length > 0) {
|
|
125
|
+
const mediaDesc = mediaFiles.map((f) => `[文件: ${f.path}]`).join('\n')
|
|
126
|
+
fullText = fullText ? `${fullText}\n\n${mediaDesc}` : mediaDesc
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.logger.debug?.(`[dsh-bridge feishu] handling inbound message from ${peerId} (group=${isGroup}): ${fullText}`)
|
|
113
130
|
|
|
114
131
|
// 如果还没有活动会话,且是首条消息,自动尝试选择最新已有会话
|
|
115
132
|
if (!this.activeSessionId) {
|
|
@@ -117,14 +134,42 @@ export class FeishuConversationNode extends ConversationBridge {
|
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
// 转交通用 ConversationBridge 路由
|
|
120
|
-
return
|
|
137
|
+
return this.handleInbound({
|
|
121
138
|
senderId: isGroup ? peerId : senderId,
|
|
122
139
|
peerId,
|
|
123
140
|
isGroup,
|
|
124
|
-
text,
|
|
141
|
+
text: fullText,
|
|
125
142
|
})
|
|
126
143
|
}
|
|
127
144
|
|
|
145
|
+
async _processMedia(messageId, messageType, contentObj = {}, sessionCwd) {
|
|
146
|
+
if (!messageId || !messageType) return []
|
|
147
|
+
const mediaDir = path.join(sessionCwd || this.config.cwd || process.cwd(), '.feishu-media')
|
|
148
|
+
try { await fs.promises.mkdir(mediaDir, { recursive: true }) } catch {}
|
|
149
|
+
|
|
150
|
+
const downloaded = []
|
|
151
|
+
const fileKey = contentObj.image_key || contentObj.file_key
|
|
152
|
+
const fileName = contentObj.file_name
|
|
153
|
+
|
|
154
|
+
if (fileKey) {
|
|
155
|
+
try {
|
|
156
|
+
const type = messageType === 'image' ? 'image' : 'file'
|
|
157
|
+
const buf = await this.gateway.downloadMessageResource({ messageId, fileKey, type })
|
|
158
|
+
if (buf && buf.length > 0) {
|
|
159
|
+
const ext = fileName ? path.extname(fileName) : (type === 'image' ? '.png' : '.bin')
|
|
160
|
+
const safeName = fileName ? path.basename(fileName) : `feishu_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`
|
|
161
|
+
const filePath = path.join(mediaDir, safeName)
|
|
162
|
+
await fs.promises.writeFile(filePath, buf)
|
|
163
|
+
downloaded.push({ filename: safeName, path: filePath, size: buf.length })
|
|
164
|
+
this.logger.info?.(`[dsh-bridge feishu] downloaded media ${safeName} (${buf.length} bytes)`)
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
this.logger.warn?.(`[dsh-bridge feishu] download media failed: ${err.message}`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return downloaded
|
|
171
|
+
}
|
|
172
|
+
|
|
128
173
|
async _handleAction(event) {
|
|
129
174
|
const { operatorId, value } = event
|
|
130
175
|
const approvalId = Number(value.approvalId)
|