@qzsy/dsh-bridge 0.0.0-stage → 2.18.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 +1107 -0
- package/LICENSE +21 -0
- package/README.en.md +488 -0
- package/README.md +509 -2
- package/client/build.mjs +43 -0
- package/client/client.js +9864 -0
- package/client/host-families.js +442 -0
- package/client/index.js +6883 -0
- package/client/locales.mjs +242 -0
- package/client/mobile-styles.js +1299 -0
- package/client/mobile-yield.js +219 -0
- package/client/panel-locales.mjs +777 -0
- package/client/picker-yield.js +77 -0
- package/client/resource-url-compat.js +90 -0
- package/client/settings-nav-icon.js +105 -0
- package/client/theme-tokens.js +49 -0
- package/client/tunnel-guard.js +46 -0
- package/client/unlock-manager.js +144 -0
- package/cordis.patch.yml +4 -0
- package/lib/auth/dsh-native-cookie.js +148 -0
- package/lib/auth/login-template.js +516 -0
- package/lib/auth/manager.js +596 -0
- package/lib/bridge-rpc-constants.js +66 -0
- package/lib/bridge-rpc.js +722 -0
- package/lib/cloudflared-manager.mjs +1277 -0
- package/lib/cloudflared-network-args.js +99 -0
- package/lib/cloudflared-process-scan.js +153 -0
- package/lib/compat.js +172 -0
- package/lib/connection-compat.js +115 -0
- package/lib/feishu/comment.js +50 -0
- package/lib/feishu/gateway.js +644 -0
- package/lib/feishu/index.js +432 -0
- package/lib/feishu/lark-bundled.mjs +131929 -0
- package/lib/feishu/markdown.js +148 -0
- package/lib/feishu/node.js +1152 -0
- package/lib/feishu/slash-commands.js +255 -0
- package/lib/index.js +3554 -0
- package/lib/platform/account-ops.js +248 -0
- package/lib/platform/activity.js +169 -0
- package/lib/platform/ask-user.js +293 -0
- package/lib/platform/base.js +147 -0
- package/lib/platform/changes-digest.js +93 -0
- package/lib/platform/commands.js +267 -0
- package/lib/platform/config-restore.js +78 -0
- package/lib/platform/conversation-bridge.js +1544 -0
- package/lib/platform/dsh-storage.js +222 -0
- package/lib/platform/index.js +10 -0
- package/lib/platform/instances.js +458 -0
- package/lib/platform/manager.js +130 -0
- package/lib/platform/message-split.js +229 -0
- package/lib/platform/session-catalog.js +405 -0
- package/lib/platform/session-config.js +52 -0
- package/lib/platform/stream-slices.js +21 -0
- package/lib/qq/gateway.js +1107 -0
- package/lib/qq/index.js +561 -0
- package/lib/qq/node.js +1153 -0
- package/lib/qq/qqbot-bundled.mjs +8273 -0
- package/lib/restart-helper.mjs +133 -0
- package/lib/saas/device-auth.mjs +366 -0
- package/lib/saas/tunnel-client.mjs +695 -0
- package/lib/security/log-sanitize.js +50 -0
- package/lib/security/path-validator.js +195 -0
- package/lib/security/rate-limiter.js +93 -0
- package/lib/session-strip.js +100 -0
- package/lib/sse-keepalive.js +115 -0
- package/lib/telegram/gateway.js +643 -0
- package/lib/telegram/index.js +255 -0
- package/lib/telegram/node.js +410 -0
- package/lib/tunnel-client.mjs +537 -0
- package/lib/wechat/gateway.js +1000 -0
- package/lib/wechat/index.js +282 -0
- package/lib/wechat/media.js +302 -0
- package/lib/wechat/node.js +360 -0
- package/llms.txt +36 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +135 -4
|
@@ -0,0 +1,432 @@
|
|
|
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
|
+
}
|