@qzsy/dsh-bridge 2.18.0 → 3.0.1

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +15 -1094
  2. package/LICENSE +21 -21
  3. package/README.md +73 -476
  4. package/client/build.mjs +1 -3
  5. package/client/client.js +381 -9747
  6. package/client/index.js +242 -6828
  7. package/client/locales.mjs +99 -233
  8. package/cordis.patch.yml +5 -3
  9. package/lib/config.js +93 -0
  10. package/lib/data-dir.js +16 -0
  11. package/lib/dsh-cookie.js +111 -0
  12. package/lib/gate.js +414 -0
  13. package/lib/identity.js +23 -0
  14. package/lib/index.js +303 -3496
  15. package/lib/saas/tunnel-client.mjs +529 -467
  16. package/locale/en.json +3 -3
  17. package/locale/zh.json +3 -3
  18. package/package.json +15 -70
  19. package/README.en.md +0 -488
  20. package/client/host-families.js +0 -442
  21. package/client/mobile-styles.js +0 -1299
  22. package/client/mobile-yield.js +0 -219
  23. package/client/panel-locales.mjs +0 -777
  24. package/client/picker-yield.js +0 -77
  25. package/client/resource-url-compat.js +0 -90
  26. package/client/settings-nav-icon.js +0 -105
  27. package/client/theme-tokens.js +0 -49
  28. package/client/tunnel-guard.js +0 -46
  29. package/client/unlock-manager.js +0 -144
  30. package/lib/auth/dsh-native-cookie.js +0 -148
  31. package/lib/auth/login-template.js +0 -516
  32. package/lib/auth/manager.js +0 -596
  33. package/lib/bridge-rpc-constants.js +0 -66
  34. package/lib/bridge-rpc.js +0 -722
  35. package/lib/cloudflared-manager.mjs +0 -1277
  36. package/lib/cloudflared-network-args.js +0 -99
  37. package/lib/cloudflared-process-scan.js +0 -153
  38. package/lib/compat.js +0 -172
  39. package/lib/connection-compat.js +0 -115
  40. package/lib/feishu/comment.js +0 -50
  41. package/lib/feishu/gateway.js +0 -644
  42. package/lib/feishu/index.js +0 -432
  43. package/lib/feishu/lark-bundled.mjs +0 -131929
  44. package/lib/feishu/markdown.js +0 -148
  45. package/lib/feishu/node.js +0 -1152
  46. package/lib/feishu/slash-commands.js +0 -255
  47. package/lib/platform/account-ops.js +0 -248
  48. package/lib/platform/activity.js +0 -169
  49. package/lib/platform/ask-user.js +0 -293
  50. package/lib/platform/base.js +0 -147
  51. package/lib/platform/changes-digest.js +0 -93
  52. package/lib/platform/commands.js +0 -267
  53. package/lib/platform/config-restore.js +0 -78
  54. package/lib/platform/conversation-bridge.js +0 -1544
  55. package/lib/platform/dsh-storage.js +0 -222
  56. package/lib/platform/index.js +0 -10
  57. package/lib/platform/instances.js +0 -458
  58. package/lib/platform/manager.js +0 -130
  59. package/lib/platform/message-split.js +0 -229
  60. package/lib/platform/session-catalog.js +0 -405
  61. package/lib/platform/session-config.js +0 -52
  62. package/lib/platform/stream-slices.js +0 -21
  63. package/lib/qq/gateway.js +0 -1107
  64. package/lib/qq/index.js +0 -561
  65. package/lib/qq/node.js +0 -1153
  66. package/lib/qq/qqbot-bundled.mjs +0 -8273
  67. package/lib/restart-helper.mjs +0 -133
  68. package/lib/security/log-sanitize.js +0 -50
  69. package/lib/security/path-validator.js +0 -195
  70. package/lib/security/rate-limiter.js +0 -93
  71. package/lib/session-strip.js +0 -100
  72. package/lib/sse-keepalive.js +0 -115
  73. package/lib/telegram/gateway.js +0 -643
  74. package/lib/telegram/index.js +0 -255
  75. package/lib/telegram/node.js +0 -410
  76. package/lib/tunnel-client.mjs +0 -537
  77. package/lib/wechat/gateway.js +0 -1000
  78. package/lib/wechat/index.js +0 -282
  79. package/lib/wechat/media.js +0 -302
  80. package/lib/wechat/node.js +0 -360
  81. package/llms.txt +0 -36
@@ -1,432 +0,0 @@
1
- // dsh-bridge Feishu / Lark platform adapter
2
- // 编排 FeishuGateway(官方 Channel SDK 网关)+ FeishuConversationNode(飞书⇄DSH 会话桥)。
3
- // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
- //
5
- // 凭证获取支持两条路径:
6
- // 1. 一键扫码创建应用(registerApp,官方 OAuth 2.0 设备码流程)——推荐,自动预置权限与事件订阅;
7
- // 2. 手动填写已有应用的 App ID / App Secret。
8
-
9
- import QRCode from 'qrcode'
10
- import { Platform } from '../platform/base.js'
11
- import { applySessionConfig, readSessionConfig } from '../platform/session-config.js'
12
- import { FeishuGateway } from './gateway.js'
13
- import { FeishuConversationNode } from './node.js'
14
-
15
- /** 等待二维码就绪的上限(设备码请求通常 <1s;超时则前端轮询后续状态) */
16
- const REGISTER_QR_TIMEOUT_MS = 10_000
17
-
18
- /** registerApp 文档化错误码 → 用户可读提示 */
19
- const REGISTER_ERROR_TEXT = {
20
- access_denied: '你已拒绝授权,可重新发起扫码创建',
21
- expired_token: '二维码已过期,请重新发起扫码创建',
22
- }
23
-
24
- function sleep(ms) {
25
- return new Promise((resolve) => setTimeout(resolve, ms))
26
- }
27
-
28
- function describeRegisterError(err) {
29
- const code = err?.code
30
- const detail = err?.description || err?.message || String(err)
31
- const friendly = code ? REGISTER_ERROR_TEXT[code] : ''
32
- if (friendly) return `${friendly}(${code})`
33
- return code ? `${detail}(${code})` : detail
34
- }
35
-
36
- export class FeishuService extends Platform {
37
- /**
38
- * @param {object} opts
39
- * @param {object} opts.ctx Cordis 上下文
40
- * @param {object} opts.logger 日志器
41
- * @param {object} [opts.config] 已持久化的 feishu 配置(凭证 + allowFrom + 间隔)
42
- * @param {(patch: object) => (void|Promise<void>)} opts.onPersist 主插件保存回调
43
- */
44
- constructor({ ctx, logger, config = {}, onPersist, instanceId, serviceName }) {
45
- super({ ctx, logger, config, onPersist })
46
- this.id = 'feishu'
47
- // issue #78:同类型可并行多实例;缺省回退类型 id 保持单实例旧行为
48
- this.instanceId = instanceId || this.id
49
- this.displayName = null
50
- this.name = 'Feishu'
51
- this._botQrCache = { appId: '', qr: '' }
52
- // 一键创建应用(registerApp)运行期状态:一次流程 = 一个 run 对象
53
- //(用对象而非散装字段,刷新二维码时替换 run 即可,旧流程的收尾不会污染新流程)
54
- this._registerRun = null
55
- this._registerSeq = 0
56
-
57
- this.gateway = new FeishuGateway(ctx, {
58
- // issue #78:instanceId 用于事件隔离;serviceName 是唯一 cordis 服务名(D2)
59
- instanceId: this.instanceId,
60
- serviceName,
61
- appId: config.appId ?? '',
62
- appSecret: config.appSecret ?? '',
63
- domain: config.domain ?? 'feishu',
64
- maxMessageChars: config.maxMessageChars || 2000,
65
- sendChunkDelayMs: config.sendChunkDelayMs,
66
- // 工作目录:网关据此设置 outbound.allowedFileDirs(本地文件走 SDK 流式上传)
67
- cwd: config.cwd ?? '',
68
- // 已注册的斜杠指令映射(由本服务持久化,gateway 只负责同步与调用回调)
69
- slashCommandIds: config.slashCommandIds ?? {},
70
- })
71
- this.gateway.onSlashCommandIds = (ids) => {
72
- void this.persist({ slashCommandIds: ids })
73
- }
74
-
75
- // 仅首个实例占用 ctx.feishu 槽位(兼容外部读取方);节点已改为显式接收网关
76
- try { if (!ctx.feishu) ctx.feishu = this.gateway } catch { /* 挂载失败不致命 */ }
77
-
78
- this.node = new FeishuConversationNode(ctx, {
79
- allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
80
- groupAutoApprove: config.groupAutoApprove,
81
- changesDigest: config.changesDigest,
82
- showActivity: config.showActivity,
83
- steerMode: config.steerMode,
84
- digestIntervalSec: config.digestIntervalSec,
85
- approvalTimeoutSec: config.approvalTimeoutSec,
86
- maxMessageChars: config.maxMessageChars || 2000,
87
- sendChunkDelayMs: config.sendChunkDelayMs,
88
- activeSessionId: config.activeSessionId,
89
- // 会话级配置:工作区 / Agent 预设 / 模型路由(构造期来自 cordis 配置,运行期由设置页写入)
90
- cwd: config.cwd,
91
- agentPreset: config.agentPreset,
92
- agentProvider: config.agentProvider,
93
- agentModel: config.agentModel,
94
- }, logger, {
95
- onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
96
- onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
97
- gateway: this.gateway,
98
- instanceId: this.instanceId,
99
- })
100
- this.bridge = this.node
101
-
102
- if (this.gateway.configured) {
103
- void this.start().catch((err) => {
104
- this.logger.error?.('[dsh-bridge feishu] start failed:', err?.message ?? err)
105
- })
106
- }
107
- }
108
-
109
- // ---- Platform 接口 ----
110
-
111
- get configured() { return this.gateway.configured }
112
- get accountId() { return this.gateway.botInfo?.openId || '' }
113
-
114
- get capabilities() {
115
- return {
116
- group: true,
117
- media: true,
118
- approvals: true,
119
- maxMessageChars: this.node.config.maxMessageChars || 2000,
120
- }
121
- }
122
-
123
- async sendText(peerId, text, opts = {}) {
124
- return this.gateway.sendText(peerId, text, opts)
125
- }
126
-
127
- // ---- 生命周期控制 ----
128
-
129
- async start() {
130
- if (!this.gateway.configured) {
131
- this.setStatus('idle')
132
- return { success: false, error: 'App ID 与 App Secret 未配置' }
133
- }
134
- this.setStatus('starting')
135
- const ok = await this.gateway.start()
136
- if (ok) {
137
- this.setStatus('connected')
138
- // 连接成功即视为流程闭环:清掉上一次扫码失败留下的错误提示
139
- if (this.loginState.phase === 'error') this.setLoginState({ phase: 'idle' })
140
- return { success: true }
141
- }
142
- this.setStatus('error', this.gateway.error || '连接失败')
143
- return { success: false, error: this.gateway.error }
144
- }
145
-
146
- async stop() {
147
- this.cancelRegisterApp()
148
- await this.gateway.stop()
149
- this.setStatus('offline')
150
- return { success: true }
151
- }
152
-
153
- /**
154
- * 配置或登录。
155
- * @param {object} [opts]
156
- * @param {string} [opts.qrType] 传 'register' 走「一键扫码创建应用」流程
157
- * @param {string} [opts.appId] / [opts.appSecret] / [opts.domain] 手动凭证
158
- */
159
- async login({ qrType, refresh, appId, appSecret, domain } = {}) {
160
- if (qrType === 'register') return this.startRegisterApp({ refresh })
161
-
162
- const patch = {}
163
- if (appId !== undefined) patch.appId = String(appId).trim()
164
- if (appSecret !== undefined) patch.appSecret = String(appSecret).trim()
165
- if (domain !== undefined) patch.domain = domain === 'lark' ? 'lark' : 'feishu'
166
- if (Object.keys(patch).length > 0) {
167
- this.gateway.updateConfig(patch)
168
- await this.persist(patch)
169
- }
170
-
171
- if (!this.gateway.configured) {
172
- await this.stop()
173
- return { ok: false, error: '请填写完整的 App ID 和 App Secret,或使用扫码一键创建应用' }
174
- }
175
-
176
- const result = await this.start()
177
- return result.success ? { ok: true } : { ok: false, error: result.error }
178
- }
179
-
180
- // ---- 一键扫码创建应用(官方 registerApp / OAuth 2.0 设备码流程) ----
181
-
182
- /**
183
- * 发起「一键创建飞书智能体应用」:立刻返回,二维码经 loginState 暴露给前端轮询。
184
- * 用户在飞书中扫码并确认后,凭据自动落盘并连接。
185
- *
186
- * @param {object} [opts]
187
- * @param {boolean} [opts.refresh] true = 作废当前设备码并重新生成二维码
188
- * (设备码一次性且 10 分钟有效;刷新后旧码立即失效,旧流程的迟到结果一律丢弃)
189
- */
190
- async startRegisterApp({ refresh = false } = {}) {
191
- const current = this._registerRun
192
- if (current && !refresh) return { ok: true } // 已在扫码中:复用当前二维码
193
- if (current) {
194
- this.logger?.info?.('[dsh-bridge feishu] 重新生成扫码创建二维码')
195
- current.controller.abort()
196
- }
197
-
198
- const run = {
199
- seq: (this._registerSeq += 1),
200
- controller: new AbortController(),
201
- }
202
- run.ready = new Promise((resolve) => { run.resolveReady = resolve })
203
- run.settled = new Promise((resolve) => { run.resolveSettled = resolve })
204
- this._registerRun = run
205
- this.setLoginState({ phase: 'qr', qrPayload: null, qrKind: 'text', error: null })
206
-
207
- void this._runRegisterApp(run)
208
-
209
- // 二维码就绪(或流程已提前结束/超时)后再返回,避免前端首帧空白
210
- await Promise.race([run.ready, run.settled, sleep(REGISTER_QR_TIMEOUT_MS)])
211
- return { ok: true }
212
- }
213
-
214
- /** 是否正在扫码创建流程中(状态快照与外部判断使用) */
215
- get registering() {
216
- return Boolean(this._registerRun)
217
- }
218
-
219
- cancelRegisterApp() {
220
- const run = this._registerRun
221
- if (!run) return
222
- this.logger?.info?.('[dsh-bridge feishu] 取消扫码创建应用')
223
- run.controller.abort()
224
- }
225
-
226
- async _runRegisterApp(run) {
227
- // 每次状态写入前确认自己仍是"当前流程":刷新用的新流程启动后,
228
- // 旧流程的二维码回调、成功结果、错误与收尾都必须丢弃,避免覆盖新流程
229
- const isCurrent = () => this._registerRun === run
230
- try {
231
- const result = await this.gateway.registerApp({
232
- source: 'dsh-bridge',
233
- signal: run.controller.signal,
234
- appPreset: {
235
- name: 'dsh-bridge',
236
- desc: 'DeepSeek Harness 远程连接:手机扫码即可远程继续本机 DSH 会话',
237
- },
238
- onQRCodeReady: ({ url }) => {
239
- if (!isCurrent()) return
240
- this.setLoginState({ phase: 'qr', qrPayload: url, qrKind: 'text' })
241
- run.resolveReady()
242
- },
243
- })
244
- if (!isCurrent()) return // 已被刷新流程取代:结果作废
245
-
246
- const appId = result?.client_id
247
- const appSecret = result?.client_secret
248
- if (!appId || !appSecret) throw new Error('扫码创建应用未返回完整凭据')
249
- // 扫码用户所在租户品牌 → 域名(feishu / lark),避免 Lark 用户走错认证域名
250
- const domain = result?.user_info?.tenant_brand === 'lark' ? 'lark' : 'feishu'
251
-
252
- this.gateway.updateConfig({ appId, appSecret, domain })
253
- await this.persist({ appId, appSecret, domain })
254
- this.logger?.info?.('[dsh-bridge feishu] 一键创建应用成功:%s(%s)', appId, domain)
255
- this.setLoginState({ phase: 'done', qrPayload: null, qrKind: null, error: null })
256
- await this.start()
257
- } catch (err) {
258
- if (!isCurrent()) return // 旧流程被 abort:静默退出
259
- if (err?.code === 'abort' || err?.name === 'AbortError') {
260
- this.setLoginState({ phase: 'idle', qrPayload: null, qrKind: null, error: null })
261
- } else {
262
- const message = describeRegisterError(err)
263
- this.logger?.warn?.('[dsh-bridge feishu] 扫码创建应用失败:%s', message)
264
- this.setLoginState({ phase: 'error', qrPayload: null, qrKind: null, error: message })
265
- }
266
- } finally {
267
- if (isCurrent()) this._registerRun = null
268
- run.resolveSettled()
269
- }
270
- }
271
-
272
- setLoginState(state) {
273
- this.loginState = {
274
- phase: 'idle', qrPayload: null, qrKind: null, error: null,
275
- ...state,
276
- }
277
- }
278
-
279
- async unbind() {
280
- this.cancelRegisterApp()
281
- await this.stop()
282
- this.gateway.updateConfig({ appId: '', appSecret: '' })
283
- this.persist({ appId: '', appSecret: '', allowFrom: [] })
284
- if (this.node) this.node.config.allowFrom = []
285
- return { success: true }
286
- }
287
-
288
- getStatus() {
289
- const allowFrom = [...(this.node?.config?.allowFrom ?? [])]
290
- const appId = this.gateway.config.appId || ''
291
- const botLink = appId ? `https://applink.feishu.cn/client/bot/open?appId=${encodeURIComponent(appId)}` : null
292
- if (botLink && this._botQrCache.appId !== appId) {
293
- this._botQrCache.appId = appId
294
- void QRCode.toDataURL(botLink, {
295
- width: 260,
296
- margin: 2,
297
- color: { dark: '#1F2421', light: '#FFFFFF' },
298
- }).then((qr) => {
299
- this._botQrCache.qr = qr
300
- }).catch(() => {})
301
- }
302
- const registering = this.registering
303
- return {
304
- id: this.id,
305
- // issue #78:实例标识(instanceId 为全局唯一键,platformId 为平台类型)
306
- instanceId: this.instanceId,
307
- platformId: this.id,
308
- displayName: this.displayName ?? this.name,
309
- name: this.name,
310
- // 通道重连中(SDK 事件驱动)要如实展示,不要钉在 connected 上
311
- status: this.gateway.status === 'reconnecting' && this.status === 'connected'
312
- ? 'reconnecting'
313
- : (this.status === 'connected' ? 'connected' : this.gateway.status),
314
- configured: this.gateway.configured,
315
- accountId: this.gateway.botInfo?.appName
316
- ? `${this.gateway.botInfo.appName} (${this.gateway.botInfo.openId || this.gateway.config.appId})`
317
- : (this.gateway.botInfo?.openId || this.gateway.config.appId || ''),
318
- allowFrom,
319
- peerId: this.node?.peerId,
320
- sessionId: this.node?.activeSessionId,
321
- login: {
322
- // 扫码创建流程的中间态(qr)与终态错误要如实回传,供设置页展示与重试
323
- phase: this.loginState.phase === 'error'
324
- ? 'error'
325
- : (registering
326
- ? this.loginState.phase
327
- : (this.status === 'connected' ? 'done' : this.status === 'error' ? 'error' : 'idle')),
328
- error: this.loginState.phase === 'error'
329
- ? this.loginState.error
330
- : (registering ? this.loginState.error : this.gateway.error),
331
- qrPayload: registering ? this.loginState.qrPayload : null,
332
- qrKind: registering ? this.loginState.qrKind : null,
333
- },
334
- capabilities: { ...this.capabilities },
335
- config: {
336
- name: this.config.name ?? '',
337
- digestIntervalSec: this.node?.config?.digestIntervalSec,
338
- approvalTimeoutSec: this.node?.config?.approvalTimeoutSec,
339
- maxMessageChars: this.node?.config?.maxMessageChars,
340
- sendChunkDelayMs: this.node?.config?.sendChunkDelayMs,
341
- // issue #77 ①:这三个开关必须回读,否则设置页读 status.config.showActivity 恒为
342
- // undefined,勾选后切页/重开面板就回退(磁盘其实已保存成功),用户以为"没保存"。
343
- groupAutoApprove: this.node?.config?.groupAutoApprove === true,
344
- changesDigest: this.node?.config?.changesDigest !== false,
345
- showActivity: this.node?.config?.showActivity === true,
346
- steerMode: this.node?.config?.steerMode === true,
347
- appId: this.gateway.config.appId,
348
- appSecret: '',
349
- domain: this.gateway.config.domain,
350
- ...readSessionConfig(this.node?.config),
351
- },
352
- botInfo: this.gateway.botInfo,
353
- botLink,
354
- botQr: this._botQrCache.qr,
355
- error: this.gateway.error,
356
- }
357
- }
358
-
359
- setAllowFrom(allowFrom) {
360
- const list = Array.isArray(allowFrom) ? allowFrom.map((s) => String(s).trim()).filter(Boolean) : []
361
- if (this.node) this.node.config.allowFrom = list
362
- this.persist({ allowFrom: list })
363
- return { success: true, allowFrom: list }
364
- }
365
-
366
- async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, changesDigest, showActivity, steerMode, appId, appSecret, domain, name, agentPreset, cwd, agentProvider, agentModel } = {}) {
367
- if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
368
- if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
369
- if (maxMessageChars != null) {
370
- const val = Number(maxMessageChars)
371
- this.node.config.maxMessageChars = (val >= 200) ? val : 2000
372
- }
373
- if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
374
- if (groupAutoApprove != null) this.node.config.groupAutoApprove = groupAutoApprove === true
375
- if (changesDigest != null) this.node.config.changesDigest = changesDigest === true
376
- if (showActivity != null) this.node.config.showActivity = showActivity === true
377
- if (steerMode != null) this.node.config.steerMode = steerMode === true
378
- applySessionConfig(this.node.config, { agentPreset, cwd, agentProvider, agentModel })
379
- // issue #78:账号备注名(面板回显 + displayName 来源),随 setConfig 持久化
380
- if (name !== undefined) this.config.name = String(name).trim()
381
-
382
- // 通道构造期参数:凭证与出站渲染(分片上限 / 流式节流)变更后需要重建通道才生效
383
- const channelPatch = {}
384
- if (appId !== undefined) channelPatch.appId = appId.trim()
385
- // 密码框留空表示「不修改已保存的密钥」
386
- if (appSecret !== undefined && appSecret.trim()) channelPatch.appSecret = appSecret.trim()
387
- if (domain !== undefined) channelPatch.domain = domain === 'lark' ? 'lark' : 'feishu'
388
- if (maxMessageChars != null) channelPatch.maxMessageChars = this.node.config.maxMessageChars
389
- if (sendChunkDelayMs != null) channelPatch.sendChunkDelayMs = this.node.config.sendChunkDelayMs
390
-
391
- const channelChanged = Object.entries(channelPatch).some(([key, value]) => this.gateway.config[key] !== value)
392
- if (Object.keys(channelPatch).length > 0) this.gateway.updateConfig(channelPatch)
393
-
394
- const patch = {
395
- digestIntervalSec: this.node.config.digestIntervalSec,
396
- approvalTimeoutSec: this.node.config.approvalTimeoutSec,
397
- maxMessageChars: this.node.config.maxMessageChars,
398
- sendChunkDelayMs: this.node.config.sendChunkDelayMs,
399
- groupAutoApprove: this.node.config.groupAutoApprove === true,
400
- changesDigest: this.node.config.changesDigest !== false,
401
- showActivity: this.node.config.showActivity === true,
402
- steerMode: this.node.config.steerMode === true,
403
- ...readSessionConfig(this.node.config),
404
- }
405
- if (name !== undefined) patch.name = this.config.name ?? ''
406
- if (appId !== undefined || appSecret !== undefined || domain !== undefined) {
407
- patch.appId = this.gateway.config.appId
408
- patch.appSecret = this.gateway.config.appSecret
409
- patch.domain = this.gateway.config.domain
410
- }
411
- await this.persist(patch)
412
-
413
- if (channelChanged) {
414
- // 通道参数/凭证变化:先收尾在途流式卡片(避免控制器悬挂),再重建通道
415
- await this.node?._finishStream?.()
416
- if (this.gateway.status !== 'idle') await this.gateway.stop()
417
- this.setStatus('idle')
418
- if (this.gateway.configured) {
419
- await this.start().catch((err) => {
420
- this.logger?.warn?.('[dsh-bridge feishu] 重连失败:%s', err?.message ?? err)
421
- })
422
- }
423
- }
424
- return { success: true }
425
- }
426
-
427
- dispose() {
428
- this.cancelRegisterApp()
429
- super.dispose()
430
- this.gateway?.dispose?.()
431
- }
432
- }