@wenbin_wb/dsh-bridge 2.1.1 → 2.2.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/index.js +3 -0
- package/lib/qq/gateway.js +90 -6
- package/lib/qq/index.js +71 -4
- package/lib/qq/node.js +79 -36
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -545,6 +545,9 @@ function apply(ctx, config = {}) {
|
|
|
545
545
|
accountId: cfg.accountId,
|
|
546
546
|
});
|
|
547
547
|
logger.info('dsh-bridge: loaded saved qq bot config, starting gateway');
|
|
548
|
+
await qq.start().catch((err) => {
|
|
549
|
+
logger.error('dsh-bridge: qq auto-start failed: %s', err?.message ?? err);
|
|
550
|
+
});
|
|
548
551
|
}
|
|
549
552
|
}
|
|
550
553
|
}).catch(() => {});
|
package/lib/qq/gateway.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Service } from '@deepseek-ai/cordis'
|
|
|
5
5
|
import WebSocket from 'ws'
|
|
6
6
|
|
|
7
7
|
const TOKEN_URL = 'https://bots.qq.com/app/getAppAccessToken'
|
|
8
|
-
const API_BASE = 'https://api.
|
|
8
|
+
const API_BASE = 'https://api.bot.qq.com'
|
|
9
9
|
const DEFAULT_GATEWAY = 'wss://api.sgroup.qq.com/websocket/'
|
|
10
10
|
const MAX_MESSAGE_CHARS = 2000
|
|
11
11
|
const TOKEN_MARGIN_MS = 5 * 60_000
|
|
@@ -323,6 +323,7 @@ export class QqGateway extends Service {
|
|
|
323
323
|
method: 'POST',
|
|
324
324
|
body: {
|
|
325
325
|
markdown: typeof markdown === 'string' ? { content: markdown } : markdown,
|
|
326
|
+
keyboard: opts.keyboard,
|
|
326
327
|
msg_type: 2,
|
|
327
328
|
msg_id: opts.msgId,
|
|
328
329
|
event_id: opts.eventId,
|
|
@@ -331,12 +332,14 @@ export class QqGateway extends Service {
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
async sendKeyboard(peerId, content, keyboard, opts = {}) {
|
|
335
|
+
// 官方文档:keyboard 为附加字段,配合 msg_type=0(content) 或 msg_type=2(markdown) 使用
|
|
336
|
+
// 纯文本 + 键盘时使用 msg_type=0
|
|
334
337
|
return this.api(this.endpoint(peerId, opts.scope), {
|
|
335
338
|
method: 'POST',
|
|
336
339
|
body: {
|
|
337
340
|
content: stringValue(content),
|
|
338
341
|
keyboard,
|
|
339
|
-
msg_type:
|
|
342
|
+
msg_type: 0,
|
|
340
343
|
msg_id: opts.msgId,
|
|
341
344
|
event_id: opts.eventId,
|
|
342
345
|
},
|
|
@@ -355,14 +358,19 @@ export class QqGateway extends Service {
|
|
|
355
358
|
}
|
|
356
359
|
|
|
357
360
|
async sendStream(peerId, content, opts = {}) {
|
|
358
|
-
const endpoint = this.endpoint(peerId, opts.scope, '
|
|
361
|
+
const endpoint = this.endpoint(peerId, opts.scope, 'stream_messages')
|
|
359
362
|
return this.api(endpoint, {
|
|
360
363
|
method: 'POST',
|
|
361
364
|
body: {
|
|
362
|
-
|
|
363
|
-
|
|
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, // 第一片由服务端返回,后续片需携带
|
|
364
371
|
msg_id: opts.msgId,
|
|
365
372
|
event_id: opts.eventId,
|
|
373
|
+
msg_seq: opts.msgSeq,
|
|
366
374
|
},
|
|
367
375
|
})
|
|
368
376
|
}
|
|
@@ -374,7 +382,83 @@ export class QqGateway extends Service {
|
|
|
374
382
|
})
|
|
375
383
|
}
|
|
376
384
|
|
|
377
|
-
async sendTyping(
|
|
385
|
+
async sendTyping(peerId, opts = {}) {
|
|
386
|
+
// QQ Bot API v2 使用 msg_type: 6 发送输入状态通知
|
|
387
|
+
// 参考:https://bot.q.qq.com/wiki/develop/api-v2/autogen/api/v2_users_user_openid_messages.post.html
|
|
388
|
+
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,
|
|
399
|
+
},
|
|
400
|
+
})
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---- 自定义菜单(单聊底部菜单,全局生效)----
|
|
404
|
+
// 参考:https://bot.q.qq.com/wiki/develop/api-v2/autogen/api/v2_menu.get.html
|
|
405
|
+
// https://bot.q.qq.com/wiki/develop/api-v2/autogen/api/v2_menu.put.html
|
|
406
|
+
|
|
407
|
+
/** 查询全局自定义菜单 */
|
|
408
|
+
async getMenu() {
|
|
409
|
+
return this.api('/v2/menu', { method: 'GET' })
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 修改全局自定义菜单(覆盖式)
|
|
414
|
+
* @param {Array} items 菜单项,最多 10 个
|
|
415
|
+
* { name, type: 'switch'|'send_message'|'link'|'menu', sub_menu_items?, send_message?, link?, switch? }
|
|
416
|
+
*/
|
|
417
|
+
async setMenu(items) {
|
|
418
|
+
return this.api('/v2/menu', {
|
|
419
|
+
method: 'PUT',
|
|
420
|
+
body: { menu: { items } },
|
|
421
|
+
})
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---- 指令面板(c2c/group/channel/dm 场景)----
|
|
425
|
+
// 参考:https://bot.q.qq.com/wiki/develop/api-v2/server-inter/menu-panel/
|
|
426
|
+
// https://bot.q.qq.com/wiki/develop/api-v2/autogen/api/v2_panels.post.html
|
|
427
|
+
|
|
428
|
+
/** 查询指令面板列表(按场景筛选) */
|
|
429
|
+
async listPanels(scope) {
|
|
430
|
+
const query = scope ? `?scope=${encodeURIComponent(scope)}` : ''
|
|
431
|
+
return this.api(`/v2/panels${query}`, { method: 'GET' })
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* 创建指令面板
|
|
436
|
+
* @param {object} body { scope, target_type?, user_openids?, group_openids?, panel: { items, remark?, version? } }
|
|
437
|
+
* @returns {Promise<{panel_id: string}>}
|
|
438
|
+
*/
|
|
439
|
+
async createPanel(body) {
|
|
440
|
+
return this.api('/v2/panels', { method: 'POST', body })
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** 查询指令面板详情 */
|
|
444
|
+
async getPanel(panelId) {
|
|
445
|
+
return this.api(`/v2/panels/${encodeURIComponent(panelId)}`, { method: 'GET' })
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** 修改指令面板 */
|
|
449
|
+
async updatePanel(panelId, body) {
|
|
450
|
+
return this.api(`/v2/panels/${encodeURIComponent(panelId)}`, { method: 'PUT', body })
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** 删除指令面板 */
|
|
454
|
+
async deletePanel(panelId) {
|
|
455
|
+
return this.api(`/v2/panels/${encodeURIComponent(panelId)}`, { method: 'DELETE' })
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** 修改指令面板关联对象(增删指定用户/群) */
|
|
459
|
+
async updatePanelTarget(panelId, body) {
|
|
460
|
+
return this.api(`/v2/panels/${encodeURIComponent(panelId)}/target`, { method: 'PUT', body })
|
|
461
|
+
}
|
|
378
462
|
|
|
379
463
|
setCredentials(values = {}) {
|
|
380
464
|
for (const key of ['appId', 'clientSecret', 'accessToken', 'accessTokenExpiresAt', 'gatewayUrl', 'intents']) {
|
package/lib/qq/index.js
CHANGED
|
@@ -42,6 +42,16 @@ export class QqService extends Platform {
|
|
|
42
42
|
// 挂到 ctx 供会话节点读取
|
|
43
43
|
try { ctx.qq = this.gateway } catch { /* 挂载失败不致命 */ }
|
|
44
44
|
|
|
45
|
+
// 网关连接成功后自动配置指令面板与自定义菜单(一次性)
|
|
46
|
+
this._panelSetupDone = false
|
|
47
|
+
try {
|
|
48
|
+
ctx.on?.('qq/status', (status) => {
|
|
49
|
+
if (status === 'connected') {
|
|
50
|
+
void this._ensurePanelSetup().catch(() => {})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
} catch { /* 忽略事件订阅失败 */ }
|
|
54
|
+
|
|
45
55
|
this.node = new QqConversationNode(ctx, {
|
|
46
56
|
allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
|
|
47
57
|
digestIntervalSec: config.digestIntervalSec,
|
|
@@ -79,8 +89,10 @@ export class QqService extends Platform {
|
|
|
79
89
|
return this.gateway.sendText(peerId, text, opts)
|
|
80
90
|
}
|
|
81
91
|
|
|
82
|
-
async sendTyping(peerId,
|
|
83
|
-
|
|
92
|
+
async sendTyping(peerId, opts = {}) {
|
|
93
|
+
// 兼容旧调用:sendTyping(peerId, durationSeconds)
|
|
94
|
+
const normalized = typeof opts === 'number' ? { durationSeconds: opts } : opts
|
|
95
|
+
return this.gateway.sendTyping(peerId, normalized)
|
|
84
96
|
}
|
|
85
97
|
|
|
86
98
|
async sendMedia(peerId, media, opts = {}) {
|
|
@@ -91,8 +103,8 @@ export class QqService extends Platform {
|
|
|
91
103
|
return this.gateway.sendMarkdown(peerId, markdown, opts)
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
async sendKeyboard(peerId, keyboard, opts = {}) {
|
|
95
|
-
return this.gateway.sendKeyboard(peerId, keyboard, opts)
|
|
106
|
+
async sendKeyboard(peerId, content, keyboard, opts = {}) {
|
|
107
|
+
return this.gateway.sendKeyboard(peerId, content, keyboard, opts)
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
/** 合并展示状态给浏览器 UI。 */
|
|
@@ -189,6 +201,58 @@ export class QqService extends Platform {
|
|
|
189
201
|
await this.persist({ allowFrom: clean })
|
|
190
202
|
}
|
|
191
203
|
|
|
204
|
+
// ---- 指令面板 / 自定义菜单(一次性自动配置)----
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 连接成功后自动创建 c2c + group 指令面板,并配置单聊自定义菜单。
|
|
208
|
+
* 幂等:已存在 remark 匹配的面板时跳过创建;失败仅记日志,不影响连接。
|
|
209
|
+
*/
|
|
210
|
+
async _ensurePanelSetup() {
|
|
211
|
+
if (this._panelSetupDone) return
|
|
212
|
+
this._panelSetupDone = true // 防并发重复执行
|
|
213
|
+
|
|
214
|
+
const gate = this.gateway
|
|
215
|
+
const remark = PANEL_REMARK
|
|
216
|
+
const items = [
|
|
217
|
+
{ name: '/new', desc: '新建对话', type: 'command' },
|
|
218
|
+
{ name: '/list', desc: '查看会话列表', type: 'command' },
|
|
219
|
+
{ name: '/resume', desc: '恢复会话', type: 'command' },
|
|
220
|
+
{ name: '/sessions', desc: '切换会话', type: 'command' },
|
|
221
|
+
{ name: '/help', desc: '命令帮助', type: 'command' },
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
// 指令面板:c2c(单聊)与 group(群聊)各建一个全局面板
|
|
225
|
+
for (const scope of ['c2c', 'group']) {
|
|
226
|
+
try {
|
|
227
|
+
const list = await gate.listPanels(scope)
|
|
228
|
+
const exists = (list?.records || []).some((r) => r?.panel?.remark === remark)
|
|
229
|
+
if (!exists) {
|
|
230
|
+
const res = await gate.createPanel({
|
|
231
|
+
scope,
|
|
232
|
+
target_type: 'all',
|
|
233
|
+
panel: { items, remark, version: 1 },
|
|
234
|
+
})
|
|
235
|
+
this.logger.info('[dsh-bridge qq] created %s command panel: %s', scope, res?.panel_id ?? '(no id)')
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
this.logger.warn('[dsh-bridge qq] command panel setup for %s skipped: %s', scope, err?.message ?? err)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 自定义菜单:单聊底部菜单(send_message 类型,点击自动填入命令)
|
|
243
|
+
try {
|
|
244
|
+
const menuItems = [
|
|
245
|
+
{ name: '新建', type: 'send_message', send_message: '/new' },
|
|
246
|
+
{ name: '列表', type: 'send_message', send_message: '/list' },
|
|
247
|
+
{ name: '帮助', type: 'send_message', send_message: '/help' },
|
|
248
|
+
]
|
|
249
|
+
await gate.setMenu(menuItems)
|
|
250
|
+
this.logger.info('[dsh-bridge qq] custom menu configured')
|
|
251
|
+
} catch (err) {
|
|
252
|
+
this.logger.warn('[dsh-bridge qq] custom menu setup skipped: %s', err?.message ?? err)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
192
256
|
/** 更新运行时配置并持久化。 */
|
|
193
257
|
async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, appId, clientSecret } = {}) {
|
|
194
258
|
if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
|
|
@@ -226,3 +290,6 @@ export class QqService extends Platform {
|
|
|
226
290
|
function gatewayMaxChars() {
|
|
227
291
|
return 2000
|
|
228
292
|
}
|
|
293
|
+
|
|
294
|
+
// 指令面板 remark 标识,用于幂等判断(区分本插件创建的面板)
|
|
295
|
+
const PANEL_REMARK = 'dsh-bridge 常用命令'
|
package/lib/qq/node.js
CHANGED
|
@@ -18,8 +18,9 @@ function makePlatform(gateway) {
|
|
|
18
18
|
name: 'QQ',
|
|
19
19
|
get accountId() { return gateway.accountId ?? '' },
|
|
20
20
|
get capabilities() { return gateway.capabilities },
|
|
21
|
+
// sendText / sendTyping 由 QqConversationNode 覆盖,这里仅提供兜底
|
|
21
22
|
sendText: (peer, text) => gateway.sendText(peer, text, {}),
|
|
22
|
-
sendTyping: (
|
|
23
|
+
sendTyping: () => Promise.resolve({ ok: true }),
|
|
23
24
|
sendKeyboard: (peer, content, keyboard) => gateway.sendKeyboard(peer, content, keyboard, {}),
|
|
24
25
|
}
|
|
25
26
|
}
|
|
@@ -44,6 +45,9 @@ export class QqConversationNode extends ConversationBridge {
|
|
|
44
45
|
})
|
|
45
46
|
this.gateway = ctx.qq
|
|
46
47
|
this.lastMessageId = null // 存储最后发送的消息 ID,用于消息引用
|
|
48
|
+
// 当前对话 peer 信息(由 _handleInbound 在每次收到消息时刷新)
|
|
49
|
+
this._lastPeer = null // { peerId, scope: 'c2c'|'group' }
|
|
50
|
+
this._replyMsgId = null // 被动回复用的用户消息 ID(事件 d.id)
|
|
47
51
|
|
|
48
52
|
// 订阅网关入站事件
|
|
49
53
|
this.ctx.on('qq/message', (event) => {
|
|
@@ -54,15 +58,22 @@ export class QqConversationNode extends ConversationBridge {
|
|
|
54
58
|
})
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// ---- 出站:覆盖 sendText
|
|
61
|
+
// ---- 出站:覆盖 sendText / sendTyping,正确处理 scope 与被动回复 ----
|
|
62
|
+
|
|
63
|
+
/** 解析当前对话 peer 信息;无活动 peer 时返回 null */
|
|
64
|
+
_currentPeer() {
|
|
65
|
+
return this._lastPeer
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
async sendText(text) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
|
|
69
|
+
const peerInfo = this._currentPeer()
|
|
70
|
+
if (!peerInfo) return
|
|
71
|
+
const { peerId, scope } = peerInfo
|
|
72
|
+
const replyMsgId = this._replyMsgId || undefined
|
|
73
|
+
|
|
63
74
|
// 检测是否是提示用户开始新会话的消息
|
|
64
75
|
const isPromptMessage = text.includes('没有活动会话') || text.includes('恢复会话失败')
|
|
65
|
-
|
|
76
|
+
|
|
66
77
|
if (isPromptMessage) {
|
|
67
78
|
// 发送带按钮的消息,方便用户快速操作
|
|
68
79
|
const keyboard = {
|
|
@@ -80,56 +91,73 @@ export class QqConversationNode extends ConversationBridge {
|
|
|
80
91
|
},
|
|
81
92
|
],
|
|
82
93
|
}
|
|
83
|
-
|
|
84
|
-
// 保存消息 ID
|
|
85
|
-
if (result?.data?.message_id) {
|
|
86
|
-
this.lastMessageId = result.data.message_id
|
|
87
|
-
}
|
|
88
|
-
return result
|
|
94
|
+
return this.gateway.sendKeyboard(peerId, text, keyboard, { scope, msgId: replyMsgId })
|
|
89
95
|
}
|
|
90
|
-
|
|
96
|
+
|
|
91
97
|
// 其他消息使用流式发送
|
|
92
98
|
const content = String(text || '').trim()
|
|
93
99
|
if (content.length === 0) return { success: true }
|
|
94
|
-
|
|
95
|
-
const STREAM_CHUNK_SIZE =
|
|
96
|
-
|
|
100
|
+
|
|
101
|
+
const STREAM_CHUNK_SIZE = 500
|
|
102
|
+
|
|
97
103
|
// 如果消息较短,直接发送普通消息
|
|
98
104
|
if (content.length <= STREAM_CHUNK_SIZE) {
|
|
99
|
-
const result = await this.gateway.sendText(
|
|
100
|
-
|
|
101
|
-
if (result?.data?.message_id) {
|
|
102
|
-
this.lastMessageId = result.data.message_id
|
|
103
|
-
}
|
|
105
|
+
const result = await this.gateway.sendText(peerId, content, { scope, msgId: replyMsgId })
|
|
106
|
+
if (result?.id) this.lastMessageId = result.id
|
|
104
107
|
return result
|
|
105
108
|
}
|
|
106
|
-
|
|
107
|
-
//
|
|
109
|
+
|
|
110
|
+
// 流式发送:把内容切分成多段(append 模式,服务端拼接为同一条消息)
|
|
108
111
|
const chunks = []
|
|
109
112
|
for (let i = 0; i < content.length; i += STREAM_CHUNK_SIZE) {
|
|
110
113
|
chunks.push(content.slice(i, i + STREAM_CHUNK_SIZE))
|
|
111
114
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
115
|
+
|
|
116
|
+
let streamMsgId = null
|
|
117
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
118
|
+
const chunk = chunks[i]
|
|
119
|
+
const isLast = i === chunks.length - 1
|
|
120
|
+
const result = await this.gateway.sendStream(peerId, chunk, {
|
|
121
|
+
scope,
|
|
122
|
+
msgId: i === 0 ? replyMsgId : undefined, // 首片带被动回复 msg_id
|
|
123
|
+
streamMsgId, // 后续片携带服务端返回的 stream_msg_id
|
|
124
|
+
index: i, // 分片序号从 0 递增
|
|
125
|
+
inputState: isLast ? 10 : 1, // 1=生成中, 10=生成结束
|
|
126
|
+
inputMode: 'append', // 追加模式:服务端拼接到同一条消息
|
|
118
127
|
})
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
|
|
129
|
+
// 首片返回 stream_msg_id,后续片需携带
|
|
130
|
+
if (i === 0 && result?.id) {
|
|
131
|
+
streamMsgId = result.id
|
|
132
|
+
this.lastMessageId = result.id // 保存消息 ID 用于消息引用
|
|
122
133
|
}
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
|
|
135
|
+
if (result?.code !== undefined && result.code !== 0) {
|
|
136
|
+
return { success: false, error: result.message || `QQ API error ${result.code}` }
|
|
125
137
|
}
|
|
138
|
+
|
|
126
139
|
// 流式发送间隔稍短,避免刷屏
|
|
127
|
-
|
|
140
|
+
if (!isLast) {
|
|
141
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
142
|
+
}
|
|
128
143
|
}
|
|
129
|
-
|
|
144
|
+
|
|
130
145
|
return { success: true }
|
|
131
146
|
}
|
|
132
147
|
|
|
148
|
+
/** 发送"正在输入"状态(QQ 通过 msg_type=6 + input_notify 显示 N 秒) */
|
|
149
|
+
async sendTyping(state) {
|
|
150
|
+
const peerInfo = this._currentPeer()
|
|
151
|
+
if (!peerInfo) return
|
|
152
|
+
// state=2(停止)时无需显式结束——input_second 到期自动消失
|
|
153
|
+
if (Number(state) === 2) return { ok: true }
|
|
154
|
+
return this.gateway.sendTyping(peerInfo.peerId, {
|
|
155
|
+
scope: peerInfo.scope,
|
|
156
|
+
durationSeconds: 8,
|
|
157
|
+
msgId: this._replyMsgId || undefined,
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
133
161
|
// ---- 入站 ----
|
|
134
162
|
|
|
135
163
|
async _handleInbound(event) {
|
|
@@ -151,6 +179,13 @@ export class QqConversationNode extends ConversationBridge {
|
|
|
151
179
|
// 群聊:仅在群聊 @ 机器人事件中处理(GROUP_AT_MESSAGE_CREATE 已由网关归一化)
|
|
152
180
|
const isGroup = event.scope === 'group' || event.scope === 'guild'
|
|
153
181
|
|
|
182
|
+
// 记录当前 peer 信息与被动回复消息 ID(事件 d.id),供出站使用
|
|
183
|
+
const peerId = isGroup ? event.groupId || event.peerId : event.peerId || event.senderId
|
|
184
|
+
if (peerId) {
|
|
185
|
+
this._lastPeer = { peerId: String(peerId), scope: isGroup ? 'group' : 'c2c' }
|
|
186
|
+
if (event.id) this._replyMsgId = String(event.id)
|
|
187
|
+
}
|
|
188
|
+
|
|
154
189
|
// 检测消息引用(文本交互):如果用户回复了机器人的消息,且当前没有活动会话或消息以 /new 开头,
|
|
155
190
|
// 则自动创建新会话并发送消息
|
|
156
191
|
const messageReference = event.messageReference
|
|
@@ -178,6 +213,14 @@ export class QqConversationNode extends ConversationBridge {
|
|
|
178
213
|
return
|
|
179
214
|
}
|
|
180
215
|
|
|
216
|
+
// 记录 peer 信息,供命令回复使用
|
|
217
|
+
const isGroup = event.scope === 'group'
|
|
218
|
+
const peerId = isGroup ? event.groupId || event.peerId : event.peerId || sender
|
|
219
|
+
if (peerId) {
|
|
220
|
+
this._lastPeer = { peerId: String(peerId), scope: isGroup ? 'group' : 'c2c' }
|
|
221
|
+
this._replyMsgId = null
|
|
222
|
+
}
|
|
223
|
+
|
|
181
224
|
const data = event.data || {}
|
|
182
225
|
const resolved = data.resolved || {}
|
|
183
226
|
const buttonId = resolved.button_id || ''
|
package/package.json
CHANGED