@wenbin_wb/dsh-bridge 2.2.2 → 2.2.3

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/gateway.js CHANGED
@@ -27,6 +27,8 @@ async function requestJson(url, { method = 'GET', token, body, timeoutMs = REQUE
27
27
  const controller = new AbortController()
28
28
  const timer = setTimeout(() => controller.abort(), timeoutMs)
29
29
  try {
30
+ // DEBUG: 记录请求详情
31
+ console.log('[QQ API DEBUG] Request:', { method, url, body })
30
32
  const response = await fetch(url, {
31
33
  method,
32
34
  headers: {
@@ -40,6 +42,8 @@ async function requestJson(url, { method = 'GET', token, body, timeoutMs = REQUE
40
42
  const raw = await response.text()
41
43
  let value = {}
42
44
  try { value = raw ? JSON.parse(raw) : {} } catch { value = { message: raw } }
45
+ // DEBUG: 记录响应详情
46
+ console.log('[QQ API DEBUG] Response:', { status: response.status, ok: response.ok, value })
43
47
  if (!response.ok) {
44
48
  const error = new Error(`QQ API ${response.status}: ${value?.message || value?.msg || value?.code || response.statusText}`)
45
49
  error.status = response.status
@@ -62,6 +66,7 @@ function normalizeEvent(payload) {
62
66
  text: stringValue(data.content).trim(),
63
67
  message: data,
64
68
  messageReference: data.message_reference,
69
+ msgSeq: data.msg_seq, // 用于避免去重
65
70
  }
66
71
  }
67
72
  if (event === 'GROUP_AT_MESSAGE_CREATE') {
@@ -74,6 +79,7 @@ function normalizeEvent(payload) {
74
79
  text: stringValue(data.content).trim(),
75
80
  message: data,
76
81
  messageReference: data.message_reference,
82
+ msgSeq: data.msg_seq, // 用于避免去重
77
83
  }
78
84
  }
79
85
  if (event === 'AT_MESSAGE_CREATE') {
@@ -307,43 +313,43 @@ export class QqGateway extends Service {
307
313
  }
308
314
 
309
315
  async sendText(peerId, content, opts = {}) {
310
- return this.api(this.endpoint(peerId, opts.scope), {
311
- method: 'POST',
312
- body: {
313
- content: stringValue(content).slice(0, MAX_MESSAGE_CHARS),
314
- msg_type: 0,
315
- msg_id: opts.msgId,
316
- event_id: opts.eventId,
317
- },
318
- })
316
+ const body = {
317
+ content: stringValue(content).slice(0, MAX_MESSAGE_CHARS),
318
+ msg_type: 0,
319
+ }
320
+ // 只添加有值的可选字段
321
+ if (opts.msgId !== undefined) body.msg_id = opts.msgId
322
+ if (opts.eventId !== undefined) body.event_id = opts.eventId
323
+ if (opts.msgSeq !== undefined) body.msg_seq = opts.msgSeq
324
+ return this.api(this.endpoint(peerId, opts.scope), { method: 'POST', body })
319
325
  }
320
326
 
321
327
  async sendMarkdown(peerId, markdown, opts = {}) {
322
- return this.api(this.endpoint(peerId, opts.scope), {
323
- method: 'POST',
324
- body: {
325
- markdown: typeof markdown === 'string' ? { content: markdown } : markdown,
326
- keyboard: opts.keyboard,
327
- msg_type: 2,
328
- msg_id: opts.msgId,
329
- event_id: opts.eventId,
330
- },
331
- })
328
+ const body = {
329
+ markdown: typeof markdown === 'string' ? { content: markdown } : markdown,
330
+ keyboard: opts.keyboard,
331
+ msg_type: 2,
332
+ }
333
+ // 只添加有值的可选字段
334
+ if (opts.msgId !== undefined) body.msg_id = opts.msgId
335
+ if (opts.eventId !== undefined) body.event_id = opts.eventId
336
+ if (opts.msgSeq !== undefined) body.msg_seq = opts.msgSeq
337
+ return this.api(this.endpoint(peerId, opts.scope), { method: 'POST', body })
332
338
  }
333
339
 
334
340
  async sendKeyboard(peerId, content, keyboard, opts = {}) {
335
341
  // 官方文档:keyboard 为附加字段,配合 msg_type=0(content) 或 msg_type=2(markdown) 使用
336
342
  // 纯文本 + 键盘时使用 msg_type=0
337
- return this.api(this.endpoint(peerId, opts.scope), {
338
- method: 'POST',
339
- body: {
340
- content: stringValue(content),
341
- keyboard,
342
- msg_type: 0,
343
- msg_id: opts.msgId,
344
- event_id: opts.eventId,
345
- },
346
- })
343
+ const body = {
344
+ content: stringValue(content),
345
+ keyboard,
346
+ msg_type: 0,
347
+ }
348
+ // 只添加有值的可选字段
349
+ if (opts.msgId !== undefined) body.msg_id = opts.msgId
350
+ if (opts.eventId !== undefined) body.event_id = opts.eventId
351
+ if (opts.msgSeq !== undefined) body.msg_seq = opts.msgSeq
352
+ return this.api(this.endpoint(peerId, opts.scope), { method: 'POST', body })
347
353
  }
348
354
 
349
355
  async sendMedia(peerId, media, opts = {}) {
@@ -359,20 +365,19 @@ export class QqGateway extends Service {
359
365
 
360
366
  async sendStream(peerId, content, opts = {}) {
361
367
  const endpoint = this.endpoint(peerId, opts.scope, 'stream_messages')
362
- return this.api(endpoint, {
363
- method: 'POST',
364
- body: {
365
- content_type: opts.contentType || 'text',
366
- content_raw: stringValue(content).slice(0, MAX_MESSAGE_CHARS),
367
- input_mode: opts.inputMode || 'replace',
368
- input_state: opts.inputState, // 1=生成中, 10=生成结束
369
- index: opts.index, // 分片序号,从0递增
370
- stream_msg_id: opts.streamMsgId, // 第一片由服务端返回,后续片需携带
371
- msg_id: opts.msgId,
372
- event_id: opts.eventId,
373
- msg_seq: opts.msgSeq,
374
- },
375
- })
368
+ const body = {
369
+ content_type: opts.contentType || 'text',
370
+ content_raw: stringValue(content).slice(0, MAX_MESSAGE_CHARS),
371
+ input_mode: opts.inputMode || 'replace',
372
+ input_state: opts.inputState, // 1=生成中, 10=生成结束
373
+ index: opts.index, // 分片序号,从0递增
374
+ }
375
+ // 只添加有值的可选字段,避免 undefined 被序列化
376
+ if (opts.streamMsgId !== undefined) body.stream_msg_id = opts.streamMsgId
377
+ if (opts.msgId !== undefined) body.msg_id = opts.msgId
378
+ if (opts.eventId !== undefined) body.event_id = opts.eventId
379
+ if (opts.msgSeq !== undefined) body.msg_seq = opts.msgSeq
380
+ return this.api(endpoint, { method: 'POST', body })
376
381
  }
377
382
 
378
383
  async respondInteraction(interactionId, response) {
@@ -386,18 +391,18 @@ export class QqGateway extends Service {
386
391
  // QQ Bot API v2 使用 msg_type: 6 发送输入状态通知
387
392
  // 参考:https://bot.q.qq.com/wiki/develop/api-v2/autogen/api/v2_users_user_openid_messages.post.html
388
393
  const endpoint = this.endpoint(peerId, opts.scope)
389
- return this.api(endpoint, {
390
- method: 'POST',
391
- body: {
392
- msg_type: 6,
393
- input_notify: {
394
- input_type: 1,
395
- input_second: Math.min(opts.durationSeconds || 5, 60), // 最长60秒
396
- },
397
- msg_id: opts.msgId,
398
- event_id: opts.eventId,
394
+ const body = {
395
+ msg_type: 6,
396
+ input_notify: {
397
+ input_type: 1,
398
+ input_second: Math.min(opts.durationSeconds || 5, 60), // 最长60秒
399
399
  },
400
- })
400
+ }
401
+ // 只添加有值的可选字段
402
+ if (opts.msgId !== undefined) body.msg_id = opts.msgId
403
+ if (opts.eventId !== undefined) body.event_id = opts.eventId
404
+ if (opts.msgSeq !== undefined) body.msg_seq = opts.msgSeq
405
+ return this.api(endpoint, { method: 'POST', body })
401
406
  }
402
407
 
403
408
  // ---- 自定义菜单(单聊底部菜单,全局生效)----
package/lib/qq/node.js CHANGED
@@ -185,9 +185,15 @@ export class QqConversationNode extends ConversationBridge {
185
185
  })
186
186
 
187
187
  // 首片返回 stream_msg_id,后续片需携带
188
- if (i === 0 && result?.id) {
189
- streamMsgId = result.id
190
- this.lastMessageId = result.id // 保存消息 ID 用于消息引用
188
+ if (i === 0) {
189
+ // 尝试多个可能的字段名
190
+ streamMsgId = result?.id || result?.message_id || result?.stream_msg_id
191
+ if (streamMsgId) {
192
+ this.lastMessageId = streamMsgId
193
+ this.logger?.info?.('[dsh-bridge qq] stream started, stream_msg_id=%s, chunks=%d', streamMsgId, chunks.length)
194
+ } else {
195
+ this.logger?.warn?.('[dsh-bridge qq] stream first chunk returned no id: %o', result)
196
+ }
191
197
  }
192
198
 
193
199
  if (result?.code !== undefined && result.code !== 0) {
@@ -205,7 +211,11 @@ export class QqConversationNode extends ConversationBridge {
205
211
  // 流式失败(如被动回复 msg_id 过期)→ 依次回退:带 msg_id 的 Markdown → 主动 Markdown
206
212
  this.logger?.warn?.('[dsh-bridge qq] stream send failed, fallback to markdown: %s', err?.message ?? err)
207
213
  try {
208
- const result = await this.gateway.sendMarkdown(peerId, md, { scope, msgId: replyMsgId })
214
+ const result = await this.gateway.sendMarkdown(peerId, md, {
215
+ scope,
216
+ msgId: replyMsgId,
217
+ msgSeq: this._msgSeq !== undefined ? this._msgSeq + 1 : undefined, // 使用递增的 msg_seq 避免去重
218
+ })
209
219
  if (result?.id) this.lastMessageId = result.id
210
220
  return result
211
221
  } catch (fallbackErr1) {
@@ -256,11 +266,12 @@ export class QqConversationNode extends ConversationBridge {
256
266
  // 群聊:仅在群聊 @ 机器人事件中处理(GROUP_AT_MESSAGE_CREATE 已由网关归一化)
257
267
  const isGroup = event.scope === 'group' || event.scope === 'guild'
258
268
 
259
- // 记录当前 peer 信息与被动回复消息 ID(事件 d.id),供出站使用
269
+ // 记录当前 peer 信息与被动回复消息 ID(事件 d.id)、msg_seq,供出站使用
260
270
  const peerId = isGroup ? event.groupId || event.peerId : event.peerId || event.senderId
261
271
  if (peerId) {
262
272
  this._lastPeer = { peerId: String(peerId), scope: isGroup ? 'group' : 'c2c' }
263
273
  if (event.id) this._replyMsgId = String(event.id)
274
+ if (event.msgSeq !== undefined) this._msgSeq = event.msgSeq
264
275
  }
265
276
 
266
277
  // 检测消息引用(文本交互):如果用户回复了机器人的消息,且当前没有活动会话或消息以 /new 开头,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenbin_wb/dsh-bridge",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "手机扫码即可在移动端/公网继续用 DeepSeek Harness,人不在电脑前也能接着干。一键局域网二维码、Cloudflare 公网隧道、自建隧道与微信 Bot(多工作区/会话持久化/媒体/审批),无需自己搭公网服务器。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",