@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/lib/qq/node.js CHANGED
@@ -6,6 +6,8 @@
6
6
  // - 出站:sendText 走 gateway.sendText;长文本用 Markdown 分块
7
7
  // - 群聊:@提及消息仅在命中机器人才处理(GROUP_AT_MESSAGE_CREATE 已保证)
8
8
 
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
9
11
  import { ConversationBridge, conversationBridgeHelpers } from '../platform/conversation-bridge.js'
10
12
  import { gatewayConstants } from './gateway.js'
11
13
 
@@ -114,6 +116,7 @@ function makePlatform(gateway) {
114
116
  get capabilities() { return gateway.capabilities },
115
117
  // sendText / sendTyping 由 QqConversationNode 覆盖,这里仅提供兜底
116
118
  sendText: (peer, text) => gateway.sendText(peer, text, {}),
119
+ sendMediaFile: (peer, filePath, opts = {}) => gateway.sendMediaFile(peer, filePath, opts),
117
120
  sendTyping: () => Promise.resolve({ ok: true }),
118
121
  sendKeyboard: (peer, content, keyboard) => gateway.sendKeyboard(peer, content, keyboard, {}),
119
122
  }
@@ -144,12 +147,8 @@ export class QqConversationNode extends ConversationBridge {
144
147
  this._replyMsgId = null // 被动回复用的用户消息 ID(事件 d.id)
145
148
 
146
149
  // 订阅网关入站事件
147
- this.ctx.on('qq/message', (event) => {
148
- void this._handleInbound(event)
149
- })
150
- this.ctx.on('qq/interaction', (event) => {
151
- void this._handleInteraction(event)
152
- })
150
+ this.ctx.on('qq/message', (event) => this._handleInbound(event))
151
+ this.ctx.on('qq/interaction', (event) => this._handleInteraction(event))
153
152
  }
154
153
 
155
154
  // ---- 出站:覆盖 sendText / sendTyping,正确处理 scope 与被动回复 ----
@@ -373,7 +372,16 @@ export class QqConversationNode extends ConversationBridge {
373
372
  }
374
373
 
375
374
  const text = String(event.text ?? '').trim()
376
- if (!text) {
375
+ const attachments = event.attachments || []
376
+
377
+ let mediaFiles = []
378
+ if (attachments.length > 0) {
379
+ this.logger?.info?.(`[dsh-bridge qq] processing ${attachments.length} attachment(s) from ${sender}...`)
380
+ mediaFiles = await this._processAttachments(attachments, this.config.cwd)
381
+ this.logger?.info?.(`[dsh-bridge qq] downloaded ${mediaFiles.length} file(s)`)
382
+ }
383
+
384
+ if (!text && mediaFiles.length === 0) {
377
385
  this.logger?.info?.(`[dsh-bridge qq] ignore empty message from ${sender}`)
378
386
  return
379
387
  }
@@ -384,10 +392,16 @@ export class QqConversationNode extends ConversationBridge {
384
392
  if (event.msgSeq !== undefined) this._msgSeq = event.msgSeq
385
393
  }
386
394
 
395
+ let fullText = text
396
+ if (mediaFiles.length > 0) {
397
+ const mediaDesc = mediaFiles.map((f) => `[文件: ${f.path}]`).join('\n')
398
+ fullText = fullText ? `${text}\n\n${mediaDesc}` : mediaDesc
399
+ }
400
+
387
401
  // 检测消息引用(文本交互):如果用户回复了机器人的消息,且当前没有活动会话或消息以 /new 开头,
388
402
  // 则自动创建新会话并发送消息
389
403
  const messageReference = event.messageReference
390
- if (messageReference && !this.activeSessionId && !text.startsWith('/')) {
404
+ if (messageReference && !this.activeSessionId && !fullText.startsWith('/')) {
391
405
  this.logger?.info?.(`[dsh-bridge qq] detected message reference from ${sender}, auto-starting conversation`)
392
406
  // 先创建新会话,再处理消息
393
407
  await this.handleInbound({ senderId: authId, text: '/new', isGroup })
@@ -396,7 +410,33 @@ export class QqConversationNode extends ConversationBridge {
396
410
  }
397
411
 
398
412
  // 交给平台无关核心:白名单/群消息/命令路由/agent 分发
399
- await this.handleInbound({ senderId: authId, text, isGroup })
413
+ await this.handleInbound({ senderId: authId, text: fullText, isGroup })
414
+ }
415
+
416
+ /** 下载附件(图片/文件)并保存到本地工作目录 */
417
+ async _processAttachments(attachments, sessionCwd) {
418
+ if (!Array.isArray(attachments) || attachments.length === 0) return []
419
+ const mediaDir = path.join(sessionCwd || this.config.cwd || process.cwd(), '.qq-media')
420
+ try { await fs.promises.mkdir(mediaDir, { recursive: true }) } catch {}
421
+
422
+ const downloaded = []
423
+ for (const att of attachments) {
424
+ if (!att?.url) continue
425
+ try {
426
+ const rawUrl = att.url.startsWith('http') ? att.url : `https://${att.url}`
427
+ const res = await fetch(rawUrl, { signal: AbortSignal.timeout(15000) })
428
+ if (!res.ok) continue
429
+ const buf = Buffer.from(await res.arrayBuffer())
430
+ const ext = att.filename ? path.extname(att.filename) : (att.content_type?.includes('image') ? '.png' : '.bin')
431
+ const safeName = att.filename ? path.basename(att.filename) : `qq_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${ext}`
432
+ const filePath = path.join(mediaDir, safeName)
433
+ await fs.promises.writeFile(filePath, buf)
434
+ downloaded.push({ filename: safeName, path: filePath, size: buf.length })
435
+ } catch (err) {
436
+ this.logger?.warn?.('[dsh-bridge qq] download attachment error: %s', err?.message ?? err)
437
+ }
438
+ }
439
+ return downloaded
400
440
  }
401
441
 
402
442
  // ---- 互动事件 ----