@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/lib/compat.js ADDED
@@ -0,0 +1,129 @@
1
+ // 运行时兼容垫片(lib/compat.js)
2
+ //
3
+ // 背景:DSH 核心依赖链(@deepseek-ai/dsh-timeout ← dsh-llm)在每次 agent 请求上
4
+ // 调用 AbortSignal.any([...]),该 API 仅存在于 Node 20.3+/22+。运行在低版本 Node
5
+ // (如 Node 18)上时,通过桥接发送消息会直接抛
6
+ // "AbortSignal.any is not a function ... (internal)"
7
+ // 本模块在缺失时安装规范兼容的垫片;环境本身支持时不做任何事(返回 false)。
8
+ //
9
+ // 浏览器侧(旧 Safari/WebView)由 lib/index.js 的 HTML_HEAD_INJECTIONS 注入同语义垫片。
10
+
11
+ function abortWith(controller, reason) {
12
+ try {
13
+ controller.abort(reason)
14
+ } catch {
15
+ try { controller.abort() } catch { /* 不可中止的信号:忽略 */ }
16
+ }
17
+ }
18
+
19
+ /**
20
+ * 在目标环境中安装 AbortSignal.any / AbortSignal.timeout 垫片(仅缺失时)。
21
+ * @param {object} [target] 可注入的环境(默认 globalThis),便于测试
22
+ * @returns {boolean} 是否实际安装了垫片
23
+ */
24
+ export function installAbortSignalCompat(target = globalThis) {
25
+ const Signal = target.AbortSignal
26
+ const Controller = target.AbortController
27
+ if (!Signal || !Controller) return false
28
+
29
+ let installed = false
30
+
31
+ // ---- AbortSignal.any ----
32
+ if (typeof Signal.any !== 'function') {
33
+ Signal.any = (signals) => {
34
+ const list = Array.from(signals ?? [])
35
+ const controller = new Controller()
36
+ // 规范语义:任一源信号已中止 → 立即以该原因中止
37
+ for (const s of list) {
38
+ if (s && s.aborted) {
39
+ abortWith(controller, s.reason)
40
+ return controller.signal
41
+ }
42
+ }
43
+ const onAbort = (eventOrSignal) => {
44
+ // 真实 EventTarget 的 abort 事件参数是 event(event.target = 信号),
45
+ // 部分非标准实现直接传信号本身——两者都兼容取 reason
46
+ const src = eventOrSignal && eventOrSignal.target ? eventOrSignal.target : eventOrSignal
47
+ cleanup()
48
+ abortWith(controller, src ? src.reason : undefined)
49
+ }
50
+ const cleanup = () => {
51
+ for (const s of list) {
52
+ try { s.removeEventListener('abort', onAbort) } catch { /* 非标准信号:忽略 */ }
53
+ }
54
+ }
55
+ for (const s of list) {
56
+ try { s.addEventListener('abort', onAbort, { once: true }) } catch { /* 非标准信号:忽略 */ }
57
+ }
58
+ return controller.signal
59
+ }
60
+ installed = true
61
+ }
62
+
63
+ // ---- AbortSignal.timeout(Node 17.3+ / 较新浏览器才有,顺手补齐)----
64
+ if (typeof Signal.timeout !== 'function') {
65
+ Signal.timeout = (delayMs) => {
66
+ const controller = new Controller()
67
+ const delay = Math.max(0, Number(delayMs) || 0)
68
+ const reason = typeof target.DOMException === 'function'
69
+ ? new target.DOMException('The operation timed out.', 'TimeoutError')
70
+ : new Error('The operation timed out.')
71
+ const timer = setTimeout(() => abortWith(controller, reason), delay)
72
+ // 规范语义:timeout 信号的超时定时器不阻止进程退出
73
+ if (typeof timer.unref === 'function') timer.unref()
74
+ return controller.signal
75
+ }
76
+ installed = true
77
+ }
78
+
79
+ return installed
80
+ }
81
+
82
+ /**
83
+ * 浏览器侧垫片源码:由代理注入到 HTML <head>(在宿主所有脚本之前执行),
84
+ * 为 iOS 16 / 旧 Safari / 旧 WebView(无 AbortSignal.any,Safari 17.4 才加入)
85
+ * 上的 DSH 网页客户端补齐。多行可读源码,勿手工压缩成单行(配平易错)。
86
+ */
87
+ export const BROWSER_ABORT_SIGNAL_POLYFILL = `<script data-dsh-bridge-polyfill="2">
88
+ !function () {
89
+ try {
90
+ var S = self.AbortSignal;
91
+ if (typeof S !== 'function') return;
92
+ if (typeof S.any !== 'function') {
93
+ S.any = function (signals) {
94
+ var list = Array.prototype.slice.call(signals || []);
95
+ var c = new self.AbortController();
96
+ var onAbort = function (ev) {
97
+ var src = ev && ev.target ? ev.target : ev;
98
+ cleanup();
99
+ try { c.abort(src ? src.reason : undefined); } catch (e) { c.abort(); }
100
+ };
101
+ var cleanup = function () {
102
+ for (var i = 0; i < list.length; i++) {
103
+ try { list[i].removeEventListener('abort', onAbort); } catch (e) {}
104
+ }
105
+ };
106
+ for (var i = 0; i < list.length; i++) {
107
+ if (list[i] && list[i].aborted) {
108
+ try { c.abort(list[i].reason); } catch (e) { c.abort(); }
109
+ return c.signal;
110
+ }
111
+ }
112
+ for (var i = 0; i < list.length; i++) {
113
+ try { list[i].addEventListener('abort', onAbort, { once: true }); } catch (e) {}
114
+ }
115
+ return c.signal;
116
+ };
117
+ }
118
+ if (typeof S.timeout !== 'function') {
119
+ S.timeout = function (ms) {
120
+ var c = new self.AbortController();
121
+ setTimeout(function () {
122
+ try { c.abort(new Error('The operation timed out.')); } catch (e) { c.abort(); }
123
+ }, Math.max(0, Number(ms) || 0));
124
+ return c.signal;
125
+ };
126
+ }
127
+ } catch (e) { /* 环境异常时不影响页面其余脚本 */ }
128
+ }();
129
+ </script>`
@@ -1,222 +1,225 @@
1
- // dsh-bridge Feishu / Lark platform adapter
2
- // 编排 FeishuGateway(官方 OpenAPI/WSClient 网关)+ FeishuConversationNode(飞书⇄DSH 会话桥)。
3
- // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
-
5
- import QRCode from 'qrcode'
6
- import { Platform } from '../platform/base.js'
7
- import { FeishuGateway } from './gateway.js'
8
- import { FeishuConversationNode } from './node.js'
9
-
10
- export class FeishuService extends Platform {
11
- /**
12
- * @param {object} opts
13
- * @param {object} opts.ctx Cordis 上下文
14
- * @param {object} opts.logger 日志器
15
- * @param {object} [opts.config] 已持久化的 feishu 配置(凭证 + allowFrom + 间隔)
16
- * @param {(patch: object) => (void|Promise<void>)} opts.onPersist 主插件保存回调
17
- */
18
- constructor({ ctx, logger, config = {}, onPersist }) {
19
- super({ ctx, logger, config, onPersist })
20
- this.id = 'feishu'
21
- this.name = 'Feishu'
22
- this._botQrCache = { appId: '', qr: '' }
23
-
24
- this.gateway = new FeishuGateway(ctx, {
25
- appId: config.appId ?? '',
26
- appSecret: config.appSecret ?? '',
27
- domain: config.domain ?? 'feishu',
28
- })
29
-
30
- // 挂到 ctx 供会话节点读取
31
- try { ctx.feishu = this.gateway } catch { /* 挂载失败不致命 */ }
32
-
33
- this.node = new FeishuConversationNode(ctx, {
34
- allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
35
- digestIntervalSec: config.digestIntervalSec,
36
- approvalTimeoutSec: config.approvalTimeoutSec,
37
- maxMessageChars: config.maxMessageChars || 2000,
38
- sendChunkDelayMs: config.sendChunkDelayMs,
39
- activeSessionId: config.activeSessionId,
40
- }, logger, {
41
- onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
42
- onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
43
- })
44
- this.bridge = this.node
45
-
46
- if (this.gateway.configured) {
47
- void this.start().catch((err) => {
48
- this.logger.error?.('[dsh-bridge feishu] start failed:', err?.message ?? err)
49
- })
50
- }
51
- }
52
-
53
- // ---- Platform 接口 ----
54
-
55
- get configured() { return this.gateway.configured }
56
- get accountId() { return this.gateway.botInfo?.openId || '' }
57
-
58
- get capabilities() {
59
- return {
60
- group: true,
61
- media: true,
62
- approvals: true,
63
- maxMessageChars: this.node.config.maxMessageChars || 2000,
64
- }
65
- }
66
-
67
- async sendText(peerId, text, opts = {}) {
68
- return this.gateway.sendMarkdownCard(peerId, text, opts)
69
- }
70
-
71
- async sendTyping(peerId, opts = {}) {
72
- return this.gateway.sendTyping?.(peerId, opts)
73
- }
74
-
75
- // ---- 生命周期控制 ----
76
-
77
- async start() {
78
- if (!this.gateway.configured) {
79
- this.setStatus('idle')
80
- return { success: false, error: 'App ID 与 App Secret 未配置' }
81
- }
82
- this.setStatus('starting')
83
- const ok = await this.gateway.start()
84
- if (ok) {
85
- this.setStatus('connected')
86
- return { success: true }
87
- } else {
88
- this.setStatus('error', this.gateway.error || '连接失败')
89
- return { success: false, error: this.gateway.error }
90
- }
91
- }
92
-
93
- async stop() {
94
- await this.gateway.stop()
95
- this.setStatus('offline')
96
- return { success: true }
97
- }
98
-
99
- /**
100
- * 配置或登录
101
- * @param {object} creds - { appId, appSecret, domain }
102
- */
103
- async login(creds = {}) {
104
- const patch = {}
105
- if (creds.appId !== undefined) patch.appId = String(creds.appId).trim()
106
- if (creds.appSecret !== undefined) patch.appSecret = String(creds.appSecret).trim()
107
- if (creds.domain !== undefined) patch.domain = creds.domain === 'lark' ? 'lark' : 'feishu'
108
-
109
- this.gateway.updateConfig(patch)
110
- this.persist(patch)
111
-
112
- if (!this.gateway.configured) {
113
- await this.stop()
114
- return { success: false, error: '请填写完整的 App ID 和 App Secret' }
115
- }
116
-
117
- return this.start()
118
- }
119
-
120
- async unbind() {
121
- await this.stop()
122
- this.gateway.updateConfig({ appId: '', appSecret: '' })
123
- this.persist({ appId: '', appSecret: '', allowFrom: [] })
124
- if (this.node) this.node.config.allowFrom = []
125
- return { success: true }
126
- }
127
-
128
- getStatus() {
129
- const allowFrom = [...(this.node?.config?.allowFrom ?? [])]
130
- const appId = this.gateway.config.appId || ''
131
- const botLink = appId ? `https://applink.feishu.cn/client/bot/open?appId=${encodeURIComponent(appId)}` : null
132
- if (botLink && this._botQrCache.appId !== appId) {
133
- this._botQrCache.appId = appId
134
- void QRCode.toDataURL(botLink, {
135
- width: 260,
136
- margin: 2,
137
- color: { dark: '#1F2421', light: '#FFFFFF' },
138
- }).then((qr) => {
139
- this._botQrCache.qr = qr
140
- }).catch(() => {})
141
- }
142
- return {
143
- id: this.id,
144
- name: this.name,
145
- status: this.status === 'connected' ? 'connected' : this.gateway.status,
146
- configured: this.gateway.configured,
147
- accountId: this.gateway.botInfo?.appName
148
- ? `${this.gateway.botInfo.appName} (${this.gateway.botInfo.openId || this.gateway.config.appId})`
149
- : (this.gateway.botInfo?.openId || this.gateway.config.appId || ''),
150
- allowFrom,
151
- peerId: this.node?.peerId,
152
- sessionId: this.node?.activeSessionId,
153
- login: {
154
- phase: this.status === 'connected' ? 'done' : this.status === 'error' ? 'error' : 'idle',
155
- error: this.gateway.error,
156
- },
157
- capabilities: { ...this.capabilities },
158
- config: {
159
- digestIntervalSec: this.node?.config?.digestIntervalSec,
160
- approvalTimeoutSec: this.node?.config?.approvalTimeoutSec,
161
- maxMessageChars: this.node?.config?.maxMessageChars,
162
- sendChunkDelayMs: this.node?.config?.sendChunkDelayMs,
163
- appId: this.gateway.config.appId,
164
- appSecret: '',
165
- domain: this.gateway.config.domain,
166
- },
167
- botInfo: this.gateway.botInfo,
168
- botLink,
169
- botQr: this._botQrCache.qr,
170
- error: this.gateway.error,
171
- }
172
- }
173
-
174
- setAllowFrom(allowFrom) {
175
- const list = Array.isArray(allowFrom) ? allowFrom.map((s) => String(s).trim()).filter(Boolean) : []
176
- if (this.node) this.node.config.allowFrom = list
177
- this.persist({ allowFrom: list })
178
- return { success: true, allowFrom: list }
179
- }
180
-
181
- async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, appId, appSecret, domain } = {}) {
182
- if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
183
- if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
184
- if (maxMessageChars != null) {
185
- const val = Number(maxMessageChars)
186
- this.node.config.maxMessageChars = (val >= 200) ? val : 2000
187
- }
188
- if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
189
- if (appId !== undefined || appSecret !== undefined || domain !== undefined) {
190
- this.gateway.updateConfig({
191
- appId: appId !== undefined ? appId.trim() : this.gateway.config.appId,
192
- appSecret: appSecret?.trim() ? appSecret.trim() : this.gateway.config.appSecret,
193
- domain: domain || this.gateway.config.domain || 'feishu',
194
- })
195
- }
196
- const patch = {
197
- digestIntervalSec: this.node.config.digestIntervalSec,
198
- approvalTimeoutSec: this.node.config.approvalTimeoutSec,
199
- maxMessageChars: this.node.config.maxMessageChars,
200
- sendChunkDelayMs: this.node.config.sendChunkDelayMs,
201
- }
202
- if (appId !== undefined || appSecret !== undefined || domain !== undefined) {
203
- patch.appId = this.gateway.config.appId
204
- patch.appSecret = this.gateway.config.appSecret
205
- patch.domain = this.gateway.config.domain
206
- }
207
- await this.persist(patch)
208
-
209
- // 凭证配置完成后自动启动网关(前端「保存并连接」自动连接)
210
- if (this.gateway.configured && (appId !== undefined || appSecret !== undefined)) {
211
- await this.start().catch((err) => {
212
- this.logger?.warn?.('[dsh-bridge feishu] auto-start failed: %s', err?.message ?? err)
213
- })
214
- }
215
- return { success: true }
216
- }
217
-
218
- dispose() {
219
- super.dispose()
220
- this.gateway?.dispose?.()
221
- }
222
- }
1
+ // dsh-bridge Feishu / Lark platform adapter
2
+ // 编排 FeishuGateway(官方 OpenAPI/WSClient 网关)+ FeishuConversationNode(飞书⇄DSH 会话桥)。
3
+ // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
+
5
+ import QRCode from 'qrcode'
6
+ import { Platform } from '../platform/base.js'
7
+ import { FeishuGateway } from './gateway.js'
8
+ import { FeishuConversationNode } from './node.js'
9
+
10
+ export class FeishuService extends Platform {
11
+ /**
12
+ * @param {object} opts
13
+ * @param {object} opts.ctx Cordis 上下文
14
+ * @param {object} opts.logger 日志器
15
+ * @param {object} [opts.config] 已持久化的 feishu 配置(凭证 + allowFrom + 间隔)
16
+ * @param {(patch: object) => (void|Promise<void>)} opts.onPersist 主插件保存回调
17
+ */
18
+ constructor({ ctx, logger, config = {}, onPersist }) {
19
+ super({ ctx, logger, config, onPersist })
20
+ this.id = 'feishu'
21
+ this.name = 'Feishu'
22
+ this._botQrCache = { appId: '', qr: '' }
23
+
24
+ this.gateway = new FeishuGateway(ctx, {
25
+ appId: config.appId ?? '',
26
+ appSecret: config.appSecret ?? '',
27
+ domain: config.domain ?? 'feishu',
28
+ })
29
+
30
+ // 挂到 ctx 供会话节点读取
31
+ try { ctx.feishu = this.gateway } catch { /* 挂载失败不致命 */ }
32
+
33
+ this.node = new FeishuConversationNode(ctx, {
34
+ allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
35
+ groupAutoApprove: config.groupAutoApprove,
36
+ digestIntervalSec: config.digestIntervalSec,
37
+ approvalTimeoutSec: config.approvalTimeoutSec,
38
+ maxMessageChars: config.maxMessageChars || 2000,
39
+ sendChunkDelayMs: config.sendChunkDelayMs,
40
+ activeSessionId: config.activeSessionId,
41
+ }, logger, {
42
+ onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
43
+ onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
44
+ })
45
+ this.bridge = this.node
46
+
47
+ if (this.gateway.configured) {
48
+ void this.start().catch((err) => {
49
+ this.logger.error?.('[dsh-bridge feishu] start failed:', err?.message ?? err)
50
+ })
51
+ }
52
+ }
53
+
54
+ // ---- Platform 接口 ----
55
+
56
+ get configured() { return this.gateway.configured }
57
+ get accountId() { return this.gateway.botInfo?.openId || '' }
58
+
59
+ get capabilities() {
60
+ return {
61
+ group: true,
62
+ media: true,
63
+ approvals: true,
64
+ maxMessageChars: this.node.config.maxMessageChars || 2000,
65
+ }
66
+ }
67
+
68
+ async sendText(peerId, text, opts = {}) {
69
+ return this.gateway.sendMarkdownCard(peerId, text, opts)
70
+ }
71
+
72
+ async sendTyping(peerId, opts = {}) {
73
+ return this.gateway.sendTyping?.(peerId, opts)
74
+ }
75
+
76
+ // ---- 生命周期控制 ----
77
+
78
+ async start() {
79
+ if (!this.gateway.configured) {
80
+ this.setStatus('idle')
81
+ return { success: false, error: 'App ID 与 App Secret 未配置' }
82
+ }
83
+ this.setStatus('starting')
84
+ const ok = await this.gateway.start()
85
+ if (ok) {
86
+ this.setStatus('connected')
87
+ return { success: true }
88
+ } else {
89
+ this.setStatus('error', this.gateway.error || '连接失败')
90
+ return { success: false, error: this.gateway.error }
91
+ }
92
+ }
93
+
94
+ async stop() {
95
+ await this.gateway.stop()
96
+ this.setStatus('offline')
97
+ return { success: true }
98
+ }
99
+
100
+ /**
101
+ * 配置或登录
102
+ * @param {object} creds - { appId, appSecret, domain }
103
+ */
104
+ async login(creds = {}) {
105
+ const patch = {}
106
+ if (creds.appId !== undefined) patch.appId = String(creds.appId).trim()
107
+ if (creds.appSecret !== undefined) patch.appSecret = String(creds.appSecret).trim()
108
+ if (creds.domain !== undefined) patch.domain = creds.domain === 'lark' ? 'lark' : 'feishu'
109
+
110
+ this.gateway.updateConfig(patch)
111
+ this.persist(patch)
112
+
113
+ if (!this.gateway.configured) {
114
+ await this.stop()
115
+ return { success: false, error: '请填写完整的 App ID 和 App Secret' }
116
+ }
117
+
118
+ return this.start()
119
+ }
120
+
121
+ async unbind() {
122
+ await this.stop()
123
+ this.gateway.updateConfig({ appId: '', appSecret: '' })
124
+ this.persist({ appId: '', appSecret: '', allowFrom: [] })
125
+ if (this.node) this.node.config.allowFrom = []
126
+ return { success: true }
127
+ }
128
+
129
+ getStatus() {
130
+ const allowFrom = [...(this.node?.config?.allowFrom ?? [])]
131
+ const appId = this.gateway.config.appId || ''
132
+ const botLink = appId ? `https://applink.feishu.cn/client/bot/open?appId=${encodeURIComponent(appId)}` : null
133
+ if (botLink && this._botQrCache.appId !== appId) {
134
+ this._botQrCache.appId = appId
135
+ void QRCode.toDataURL(botLink, {
136
+ width: 260,
137
+ margin: 2,
138
+ color: { dark: '#1F2421', light: '#FFFFFF' },
139
+ }).then((qr) => {
140
+ this._botQrCache.qr = qr
141
+ }).catch(() => {})
142
+ }
143
+ return {
144
+ id: this.id,
145
+ name: this.name,
146
+ status: this.status === 'connected' ? 'connected' : this.gateway.status,
147
+ configured: this.gateway.configured,
148
+ accountId: this.gateway.botInfo?.appName
149
+ ? `${this.gateway.botInfo.appName} (${this.gateway.botInfo.openId || this.gateway.config.appId})`
150
+ : (this.gateway.botInfo?.openId || this.gateway.config.appId || ''),
151
+ allowFrom,
152
+ peerId: this.node?.peerId,
153
+ sessionId: this.node?.activeSessionId,
154
+ login: {
155
+ phase: this.status === 'connected' ? 'done' : this.status === 'error' ? 'error' : 'idle',
156
+ error: this.gateway.error,
157
+ },
158
+ capabilities: { ...this.capabilities },
159
+ config: {
160
+ digestIntervalSec: this.node?.config?.digestIntervalSec,
161
+ approvalTimeoutSec: this.node?.config?.approvalTimeoutSec,
162
+ maxMessageChars: this.node?.config?.maxMessageChars,
163
+ sendChunkDelayMs: this.node?.config?.sendChunkDelayMs,
164
+ appId: this.gateway.config.appId,
165
+ appSecret: '',
166
+ domain: this.gateway.config.domain,
167
+ },
168
+ botInfo: this.gateway.botInfo,
169
+ botLink,
170
+ botQr: this._botQrCache.qr,
171
+ error: this.gateway.error,
172
+ }
173
+ }
174
+
175
+ setAllowFrom(allowFrom) {
176
+ const list = Array.isArray(allowFrom) ? allowFrom.map((s) => String(s).trim()).filter(Boolean) : []
177
+ if (this.node) this.node.config.allowFrom = list
178
+ this.persist({ allowFrom: list })
179
+ return { success: true, allowFrom: list }
180
+ }
181
+
182
+ async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, appId, appSecret, domain } = {}) {
183
+ if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
184
+ if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
185
+ if (maxMessageChars != null) {
186
+ const val = Number(maxMessageChars)
187
+ this.node.config.maxMessageChars = (val >= 200) ? val : 2000
188
+ }
189
+ if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
190
+ if (groupAutoApprove != null) this.node.config.groupAutoApprove = groupAutoApprove === true
191
+ if (appId !== undefined || appSecret !== undefined || domain !== undefined) {
192
+ this.gateway.updateConfig({
193
+ appId: appId !== undefined ? appId.trim() : this.gateway.config.appId,
194
+ appSecret: appSecret?.trim() ? appSecret.trim() : this.gateway.config.appSecret,
195
+ domain: domain || this.gateway.config.domain || 'feishu',
196
+ })
197
+ }
198
+ const patch = {
199
+ digestIntervalSec: this.node.config.digestIntervalSec,
200
+ approvalTimeoutSec: this.node.config.approvalTimeoutSec,
201
+ maxMessageChars: this.node.config.maxMessageChars,
202
+ sendChunkDelayMs: this.node.config.sendChunkDelayMs,
203
+ groupAutoApprove: this.node.config.groupAutoApprove === true,
204
+ }
205
+ if (appId !== undefined || appSecret !== undefined || domain !== undefined) {
206
+ patch.appId = this.gateway.config.appId
207
+ patch.appSecret = this.gateway.config.appSecret
208
+ patch.domain = this.gateway.config.domain
209
+ }
210
+ await this.persist(patch)
211
+
212
+ // 凭证配置完成后自动启动网关(前端「保存并连接」自动连接)
213
+ if (this.gateway.configured && (appId !== undefined || appSecret !== undefined)) {
214
+ await this.start().catch((err) => {
215
+ this.logger?.warn?.('[dsh-bridge feishu] auto-start failed: %s', err?.message ?? err)
216
+ })
217
+ }
218
+ return { success: true }
219
+ }
220
+
221
+ dispose() {
222
+ super.dispose()
223
+ this.gateway?.dispose?.()
224
+ }
225
+ }