@wenbin_wb/dsh-bridge 2.10.7 → 2.10.9

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.
@@ -1,225 +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
- 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
- }
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
+ }