@wenbin_wb/dsh-bridge 2.8.7 → 2.9.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/CHANGELOG.md +295 -250
- package/README.en.md +408 -572
- package/README.md +430 -570
- package/client/client.js +3844 -3764
- package/client/index.js +140 -876
- package/client/mobile-styles.js +802 -0
- package/docs/fix-plan-202608.md +108 -0
- package/lib/auth/login-template.js +381 -381
- package/lib/auth/manager.js +531 -469
- package/lib/bridge-rpc.js +436 -511
- package/lib/cloudflared-manager.mjs +361 -345
- package/lib/compat.js +129 -0
- package/lib/feishu/index.js +225 -222
- package/lib/feishu/node.js +433 -409
- package/lib/index.js +1765 -1804
- package/lib/platform/base.js +147 -156
- package/lib/platform/commands.js +221 -0
- package/lib/platform/conversation-bridge.js +816 -1570
- package/lib/platform/dsh-storage.js +117 -0
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +191 -0
- package/lib/platform/session-catalog.js +372 -0
- package/lib/platform/stream-slices.js +21 -0
- package/lib/qq/index.js +312 -309
- package/lib/qq/node.js +532 -533
- package/lib/telegram/index.js +215 -212
- package/lib/telegram/node.js +348 -350
- package/lib/tunnel-client.mjs +39 -15
- package/lib/wechat/gateway.js +973 -960
- package/lib/wechat/index.js +244 -241
- package/lib/wechat/media.js +285 -281
- package/lib/wechat/node.js +352 -350
- package/package.json +106 -102
package/lib/qq/index.js
CHANGED
|
@@ -1,309 +1,312 @@
|
|
|
1
|
-
// dsh-bridge QQ platform adapter
|
|
2
|
-
// 编排 QqGateway(OpenAPI v2 网关)+ QqConversationNode(QQ⇄DSH 会话桥)。
|
|
3
|
-
// 作为 Platform 子类,注册进 PlatformManager 统一管理。
|
|
4
|
-
//
|
|
5
|
-
// 对外暴露给 bridge-rpc 的接口(与 wechat 一致):getStatus / login / stop /
|
|
6
|
-
// setAllowFrom / setConfig / unbind / destroy。
|
|
7
|
-
//
|
|
8
|
-
// 持久化策略:凭证(appId/clientSecret/accessToken/accountId)与配置由主插件
|
|
9
|
-
// 通过回调 `onPersist` 保存在 $DSH_HOME/dsh-bridge/config.json。
|
|
10
|
-
|
|
11
|
-
import { Platform } from '../platform/base.js'
|
|
12
|
-
import { QqGateway } from './gateway.js'
|
|
13
|
-
import { QqConversationNode } from './node.js'
|
|
14
|
-
|
|
15
|
-
export class QqService extends Platform {
|
|
16
|
-
/**
|
|
17
|
-
* @param {object} opts
|
|
18
|
-
* @param {object} opts.ctx Cordis 上下文
|
|
19
|
-
* @param {object} opts.logger 日志器
|
|
20
|
-
* @param {object} [opts.config] 已持久化的 qq 配置(凭证 + allowFrom + 间隔)
|
|
21
|
-
* @param {(patch: object) => (void|Promise<void>)} opts.onPersist 主插件保存回调
|
|
22
|
-
*/
|
|
23
|
-
constructor({ ctx, logger, config = {}, onPersist }) {
|
|
24
|
-
super({ ctx, logger, config, onPersist })
|
|
25
|
-
this.id = 'qq'
|
|
26
|
-
this.name = 'QQ'
|
|
27
|
-
|
|
28
|
-
this.gateway = new QqGateway({
|
|
29
|
-
ctx,
|
|
30
|
-
logger,
|
|
31
|
-
config: {
|
|
32
|
-
appId: config.appId ?? '',
|
|
33
|
-
clientSecret: config.clientSecret ?? '',
|
|
34
|
-
accessToken: config.accessToken ?? '',
|
|
35
|
-
accessTokenExpiresAt: config.accessTokenExpiresAt ?? 0,
|
|
36
|
-
gatewayUrl: config.gatewayUrl ?? '',
|
|
37
|
-
accountId: config.accountId ?? '',
|
|
38
|
-
},
|
|
39
|
-
onPersist: (patch) => this.persist(patch),
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// 挂到 ctx 供会话节点读取
|
|
43
|
-
try { ctx.qq = this.gateway } catch { /* 挂载失败不致命 */ }
|
|
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
|
-
|
|
55
|
-
this.node = new QqConversationNode(ctx, {
|
|
56
|
-
allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
get
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.gateway.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
*
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
this.gateway.
|
|
195
|
-
|
|
196
|
-
this.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
*
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
this._panelSetupDone
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
{ name: '/
|
|
221
|
-
{ name: '/
|
|
222
|
-
{ name: '/
|
|
223
|
-
{ name: '/
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
{ name: '
|
|
250
|
-
{ name: '
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
1
|
+
// dsh-bridge QQ platform adapter
|
|
2
|
+
// 编排 QqGateway(OpenAPI v2 网关)+ QqConversationNode(QQ⇄DSH 会话桥)。
|
|
3
|
+
// 作为 Platform 子类,注册进 PlatformManager 统一管理。
|
|
4
|
+
//
|
|
5
|
+
// 对外暴露给 bridge-rpc 的接口(与 wechat 一致):getStatus / login / stop /
|
|
6
|
+
// setAllowFrom / setConfig / unbind / destroy。
|
|
7
|
+
//
|
|
8
|
+
// 持久化策略:凭证(appId/clientSecret/accessToken/accountId)与配置由主插件
|
|
9
|
+
// 通过回调 `onPersist` 保存在 $DSH_HOME/dsh-bridge/config.json。
|
|
10
|
+
|
|
11
|
+
import { Platform } from '../platform/base.js'
|
|
12
|
+
import { QqGateway } from './gateway.js'
|
|
13
|
+
import { QqConversationNode } from './node.js'
|
|
14
|
+
|
|
15
|
+
export class QqService extends Platform {
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} opts
|
|
18
|
+
* @param {object} opts.ctx Cordis 上下文
|
|
19
|
+
* @param {object} opts.logger 日志器
|
|
20
|
+
* @param {object} [opts.config] 已持久化的 qq 配置(凭证 + allowFrom + 间隔)
|
|
21
|
+
* @param {(patch: object) => (void|Promise<void>)} opts.onPersist 主插件保存回调
|
|
22
|
+
*/
|
|
23
|
+
constructor({ ctx, logger, config = {}, onPersist }) {
|
|
24
|
+
super({ ctx, logger, config, onPersist })
|
|
25
|
+
this.id = 'qq'
|
|
26
|
+
this.name = 'QQ'
|
|
27
|
+
|
|
28
|
+
this.gateway = new QqGateway({
|
|
29
|
+
ctx,
|
|
30
|
+
logger,
|
|
31
|
+
config: {
|
|
32
|
+
appId: config.appId ?? '',
|
|
33
|
+
clientSecret: config.clientSecret ?? '',
|
|
34
|
+
accessToken: config.accessToken ?? '',
|
|
35
|
+
accessTokenExpiresAt: config.accessTokenExpiresAt ?? 0,
|
|
36
|
+
gatewayUrl: config.gatewayUrl ?? '',
|
|
37
|
+
accountId: config.accountId ?? '',
|
|
38
|
+
},
|
|
39
|
+
onPersist: (patch) => this.persist(patch),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// 挂到 ctx 供会话节点读取
|
|
43
|
+
try { ctx.qq = this.gateway } catch { /* 挂载失败不致命 */ }
|
|
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
|
+
|
|
55
|
+
this.node = new QqConversationNode(ctx, {
|
|
56
|
+
allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
|
|
57
|
+
groupAutoApprove: config.groupAutoApprove,
|
|
58
|
+
digestIntervalSec: config.digestIntervalSec,
|
|
59
|
+
approvalTimeoutSec: config.approvalTimeoutSec,
|
|
60
|
+
maxMessageChars: config.maxMessageChars,
|
|
61
|
+
sendChunkDelayMs: config.sendChunkDelayMs,
|
|
62
|
+
activeSessionId: config.activeSessionId,
|
|
63
|
+
}, logger, {
|
|
64
|
+
onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
|
|
65
|
+
onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
|
|
66
|
+
})
|
|
67
|
+
this.bridge = this.node
|
|
68
|
+
|
|
69
|
+
if (this.gateway.configured) {
|
|
70
|
+
void this.start().catch((err) => {
|
|
71
|
+
this.logger.error('[dsh-bridge qq] start failed: %s', err?.message ?? err)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- Platform 接口 ----
|
|
77
|
+
|
|
78
|
+
get configured() { return this.gateway.configured }
|
|
79
|
+
get accountId() { return this.gateway.accountId }
|
|
80
|
+
|
|
81
|
+
get capabilities() {
|
|
82
|
+
return {
|
|
83
|
+
...this.gateway.capabilities,
|
|
84
|
+
maxMessageChars: this.node.config.maxMessageChars ?? gatewayMaxChars(),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 消息抽象:委托 gateway(供 PlatformManager/未来多平台统一调用)。 */
|
|
89
|
+
async sendText(peerId, text, opts = {}) {
|
|
90
|
+
return this.gateway.sendText(peerId, text, opts)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async sendTyping(peerId, opts = {}) {
|
|
94
|
+
// 兼容旧调用:sendTyping(peerId, durationSeconds)
|
|
95
|
+
const normalized = typeof opts === 'number' ? { durationSeconds: opts } : opts
|
|
96
|
+
return this.gateway.sendTyping(peerId, normalized)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async sendMedia(peerId, media, opts = {}) {
|
|
100
|
+
return this.gateway.sendMedia(peerId, media, opts)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async sendMarkdown(peerId, markdown, opts = {}) {
|
|
104
|
+
return this.gateway.sendMarkdown(peerId, markdown, opts)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async sendKeyboard(peerId, content, keyboard, opts = {}) {
|
|
108
|
+
return this.gateway.sendKeyboard(peerId, content, keyboard, opts)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 合并展示状态给浏览器 UI。 */
|
|
112
|
+
getStatus() {
|
|
113
|
+
const allowFrom = [...(this.node.config.allowFrom ?? [])]
|
|
114
|
+
return {
|
|
115
|
+
id: this.id,
|
|
116
|
+
name: this.name,
|
|
117
|
+
status: this.gateway.status,
|
|
118
|
+
configured: this.gateway.configured,
|
|
119
|
+
accountId: this.gateway.accountId,
|
|
120
|
+
allowFrom,
|
|
121
|
+
peerId: this.node.peerId,
|
|
122
|
+
sessionId: this.node.activeSessionId,
|
|
123
|
+
login: { ...this.loginState },
|
|
124
|
+
capabilities: { ...this.capabilities },
|
|
125
|
+
config: {
|
|
126
|
+
digestIntervalSec: this.node.config.digestIntervalSec,
|
|
127
|
+
approvalTimeoutSec: this.node.config.approvalTimeoutSec,
|
|
128
|
+
maxMessageChars: this.node.config.maxMessageChars,
|
|
129
|
+
sendChunkDelayMs: this.node.config.sendChunkDelayMs,
|
|
130
|
+
appId: this.gateway.config.appId,
|
|
131
|
+
// 密钥不回传到浏览器;设置页留空时保持已保存密钥
|
|
132
|
+
clientSecret: '',
|
|
133
|
+
accountId: this.gateway.accountId,
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 可编辑配置(供通用 PlatformPanel 读取)。 */
|
|
139
|
+
getEditableConfig() {
|
|
140
|
+
return {
|
|
141
|
+
digestIntervalSec: this.node.config.digestIntervalSec,
|
|
142
|
+
approvalTimeoutSec: this.node.config.approvalTimeoutSec,
|
|
143
|
+
maxMessageChars: this.node.config.maxMessageChars,
|
|
144
|
+
sendChunkDelayMs: this.node.config.sendChunkDelayMs,
|
|
145
|
+
appId: this.gateway.config.appId,
|
|
146
|
+
clientSecret: '',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 启动网关(凭证已配置时生效)。 */
|
|
151
|
+
async start() {
|
|
152
|
+
if (!this.gateway.configured) {
|
|
153
|
+
this.setStatus('idle')
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
await this.gateway.start()
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 停止网关(保留凭证与配置)。 */
|
|
160
|
+
async stop() {
|
|
161
|
+
await this.gateway.stop()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 完整关停(dispose 时调用)。 */
|
|
165
|
+
async dispose() {
|
|
166
|
+
await this.gateway.stop()
|
|
167
|
+
this.gateway.dispose()
|
|
168
|
+
super.dispose()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 兼容 v1.x 的别名。 */
|
|
172
|
+
async destroy() {
|
|
173
|
+
await this.dispose()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 发起登录(配置 AppID/Secret 后即视为已连接;后台执行,状态通过 getStatus 轮询读取)。
|
|
178
|
+
* @returns {Promise<{ok: boolean, error?: string}>} 立即返回(已启动)
|
|
179
|
+
*/
|
|
180
|
+
async login(opts = {}) {
|
|
181
|
+
// 无二维码流程:QQ 官方 Bot 用 AppID/Secret 鉴权,登录即配置凭证 + 启动网关
|
|
182
|
+
if (!this.gateway.configured) {
|
|
183
|
+
return { ok: false, error: '请先在配置面板填写 QQ 开放平台的 AppID 与 ClientSecret' }
|
|
184
|
+
}
|
|
185
|
+
this.loginState = { phase: 'done', qrPayload: null, qrKind: null, error: null }
|
|
186
|
+
void this.gateway.start().catch((err) => {
|
|
187
|
+
this.logger.error('[dsh-bridge qq] login start failed: %s', err?.message ?? err)
|
|
188
|
+
})
|
|
189
|
+
return { ok: true }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 解绑:停止网关并清除凭证,下次重启不再自动重连。 */
|
|
193
|
+
async unbind() {
|
|
194
|
+
await this.gateway.stop()
|
|
195
|
+
this.gateway.setCredentials({ appId: '', clientSecret: '', accessToken: '', accessTokenExpiresAt: 0 })
|
|
196
|
+
await this.persist({ appId: '', clientSecret: '', accessToken: '', accessTokenExpiresAt: 0, accountId: '' })
|
|
197
|
+
this.logger.info('[dsh-bridge qq] unbound: credentials cleared')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 更新白名单并发起持久化。 */
|
|
201
|
+
async setAllowFrom(list) {
|
|
202
|
+
const clean = Array.isArray(list) ? [...new Set(list.map((x) => String(x).trim()).filter(Boolean))] : []
|
|
203
|
+
this.node.config.allowFrom = clean
|
|
204
|
+
await this.persist({ allowFrom: clean })
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---- 指令面板 / 自定义菜单(一次性自动配置)----
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 连接成功后自动创建 c2c + group 指令面板,并配置单聊自定义菜单。
|
|
211
|
+
* 幂等:已存在 remark 匹配的面板时跳过创建;失败仅记日志,不影响连接。
|
|
212
|
+
*/
|
|
213
|
+
async _ensurePanelSetup() {
|
|
214
|
+
if (this._panelSetupDone) return
|
|
215
|
+
this._panelSetupDone = true // 防并发重复执行
|
|
216
|
+
|
|
217
|
+
const gate = this.gateway
|
|
218
|
+
const remark = PANEL_REMARK
|
|
219
|
+
const items = [
|
|
220
|
+
{ name: '/new', desc: '新建对话', type: 'command' },
|
|
221
|
+
{ name: '/list', desc: '查看会话列表', type: 'command' },
|
|
222
|
+
{ name: '/resume', desc: '恢复会话', type: 'command' },
|
|
223
|
+
{ name: '/sessions', desc: '切换会话', type: 'command' },
|
|
224
|
+
{ name: '/help', desc: '命令帮助', type: 'command' },
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
// 指令面板:c2c(单聊)与 group(群聊)各建一个全局面板
|
|
228
|
+
for (const scope of ['c2c', 'group']) {
|
|
229
|
+
try {
|
|
230
|
+
const list = await gate.listPanels(scope)
|
|
231
|
+
const exists = (list?.records || []).some((r) => r?.panel?.remark === remark)
|
|
232
|
+
if (!exists) {
|
|
233
|
+
const res = await gate.createPanel({
|
|
234
|
+
scope,
|
|
235
|
+
target_type: 'all',
|
|
236
|
+
panel: { items, remark, version: 1 },
|
|
237
|
+
})
|
|
238
|
+
this.logger.info('[dsh-bridge qq] created %s command panel: %s', scope, res?.panel_id ?? '(no id)')
|
|
239
|
+
}
|
|
240
|
+
} catch (err) {
|
|
241
|
+
this.logger.warn('[dsh-bridge qq] command panel setup for %s skipped: %s', scope, err?.message ?? err)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 自定义菜单:单聊底部菜单(send_message 类型,点击自动填入命令)
|
|
246
|
+
// 幂等:先查询,内容一致则跳过 PUT,避免每次重启都覆盖(version 递增)
|
|
247
|
+
try {
|
|
248
|
+
const menuItems = [
|
|
249
|
+
{ name: '新建', type: 'send_message', send_message: '/new' },
|
|
250
|
+
{ name: '列表', type: 'send_message', send_message: '/list' },
|
|
251
|
+
{ name: '帮助', type: 'send_message', send_message: '/help' },
|
|
252
|
+
]
|
|
253
|
+
const existing = await gate.getMenu()
|
|
254
|
+
const existingItems = existing?.menu?.items || []
|
|
255
|
+
const same = existingItems.length === menuItems.length &&
|
|
256
|
+
menuItems.every((item, i) => existingItems[i]?.name === item.name && existingItems[i]?.send_message === item.send_message)
|
|
257
|
+
if (!same) {
|
|
258
|
+
await gate.setMenu(menuItems)
|
|
259
|
+
this.logger.info('[dsh-bridge qq] custom menu configured')
|
|
260
|
+
} else {
|
|
261
|
+
this.logger.info('[dsh-bridge qq] custom menu already configured')
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
this.logger.warn('[dsh-bridge qq] custom menu setup skipped: %s', err?.message ?? err)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 更新运行时配置并持久化。 */
|
|
269
|
+
async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, appId, clientSecret } = {}) {
|
|
270
|
+
if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
|
|
271
|
+
if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
|
|
272
|
+
if (maxMessageChars != null) {
|
|
273
|
+
const val = Number(maxMessageChars)
|
|
274
|
+
this.node.config.maxMessageChars = (val >= 200) ? val : 2000
|
|
275
|
+
}
|
|
276
|
+
if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
|
|
277
|
+
if (groupAutoApprove != null) this.node.config.groupAutoApprove = groupAutoApprove === true
|
|
278
|
+
if (appId !== undefined || clientSecret !== undefined) {
|
|
279
|
+
this.gateway.setCredentials({
|
|
280
|
+
appId: appId !== undefined ? appId : this.gateway.config.appId,
|
|
281
|
+
// 空字符串表示 UI 未修改密钥,避免误覆盖已保存的 secret
|
|
282
|
+
clientSecret: clientSecret?.trim() ? clientSecret.trim() : this.gateway.config.clientSecret,
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
const patch = {
|
|
286
|
+
digestIntervalSec: this.node.config.digestIntervalSec,
|
|
287
|
+
approvalTimeoutSec: this.node.config.approvalTimeoutSec,
|
|
288
|
+
maxMessageChars: this.node.config.maxMessageChars,
|
|
289
|
+
sendChunkDelayMs: this.node.config.sendChunkDelayMs,
|
|
290
|
+
groupAutoApprove: this.node.config.groupAutoApprove === true,
|
|
291
|
+
}
|
|
292
|
+
if (appId !== undefined || clientSecret !== undefined) {
|
|
293
|
+
patch.appId = this.gateway.config.appId
|
|
294
|
+
patch.clientSecret = this.gateway.config.clientSecret
|
|
295
|
+
}
|
|
296
|
+
await this.persist(patch)
|
|
297
|
+
|
|
298
|
+
// 凭证配置完成后自动启动网关(前端「保存并连接」无需二次点击)
|
|
299
|
+
if (this.gateway.configured && (appId !== undefined || clientSecret !== undefined)) {
|
|
300
|
+
await this.gateway.start().catch((err) => {
|
|
301
|
+
this.logger?.warn?.('[dsh-bridge qq] auto-start failed: %s', err?.message ?? err)
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function gatewayMaxChars() {
|
|
308
|
+
return 2000
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 指令面板 remark 标识,用于幂等判断(区分本插件创建的面板)
|
|
312
|
+
const PANEL_REMARK = 'dsh-bridge 常用命令'
|