@ai-wendao/ant 0.0.0-stage → 0.3.8

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/client.js ADDED
@@ -0,0 +1,680 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'ant',
3
+ factory: (require) => {
4
+ var module = { exports: {} }
5
+ var exports = module.exports
6
+ const { createElement, useEffect, useState } = require('react')
7
+ const h = createElement
8
+ const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
9
+ const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
10
+ const ANT_API_PROTOCOL_VERSIONS = [1]
11
+ const panelStyle = {
12
+ display: 'grid', alignContent: 'start', gap: 14, padding: 18,
13
+ border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 14,
14
+ backgroundColor: 'var(--dsw-alias-bg-module-platform)', boxSizing: 'border-box',
15
+ }
16
+ const mutedTextStyle = { margin: 0, color: 'var(--dsw-alias-label-secondary)', fontSize: 13, lineHeight: 1.6 }
17
+ const inputStyle = {
18
+ width: '100%', boxSizing: 'border-box', padding: '10px 12px',
19
+ color: 'var(--dsw-alias-label-primary)', backgroundColor: 'var(--dsw-specific-input-major)',
20
+ border: '0.5px solid var(--dsw-alias-border-l3)', borderRadius: 8,
21
+ }
22
+ const primaryButtonStyle = {
23
+ minHeight: 40, padding: '9px 14px', color: 'var(--dsw-alias-label-primary-foreground)', fontWeight: 600,
24
+ backgroundColor: 'var(--dsw-alias-state-business-primary)',
25
+ border: '0.5px solid var(--dsw-alias-state-business-primary)', borderRadius: 8,
26
+ cursor: 'pointer', whiteSpace: 'nowrap',
27
+ }
28
+ const secondaryButtonStyle = {
29
+ minHeight: 40, padding: '9px 14px', color: 'var(--dsw-alias-label-primary)', fontWeight: 500,
30
+ backgroundColor: 'var(--dsw-alias-button-primary-dimmed)',
31
+ border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 8,
32
+ cursor: 'pointer',
33
+ }
34
+ const normalizeServiceUrl = (value) => {
35
+ let url
36
+ try { url = new URL(value.trim()) } catch { return null }
37
+ const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
38
+ if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password || url.search || url.hash || (url.pathname !== '/' && url.pathname !== '')) return null
39
+ return url.origin
40
+ }
41
+ const consoleUrl = (value) => {
42
+ const normalized = normalizeServiceUrl(value)
43
+ return normalized ? new URL('/console', normalized).toString() : null
44
+ }
45
+ const versionParts = (value) => {
46
+ if (typeof value !== 'string' || !/^\d+\.\d+\.\d+$/u.test(value)) return null
47
+ return value.split('.')
48
+ }
49
+ const newerVersion = (candidate, current) => {
50
+ const next = versionParts(candidate)
51
+ const installed = versionParts(current)
52
+ if (!next || !installed) return false
53
+ for (let index = 0; index < next.length; index += 1) {
54
+ if (next[index] !== installed[index]) {
55
+ return next[index].length === installed[index].length
56
+ ? next[index] > installed[index]
57
+ : next[index].length > installed[index].length
58
+ }
59
+ }
60
+ return false
61
+ }
62
+ const inject = ['slots', 'remote', 'remote.credentials', 'remote.settings', 'remote.pluginManager']
63
+
64
+ function loadServiceUrl() {
65
+ try { return normalizeServiceUrl(localStorage.getItem(SERVICE_URL_STORAGE_KEY) || '') || '' } catch { return '' }
66
+ }
67
+
68
+ function loadDeviceId() {
69
+ const existing = localStorage.getItem(DEVICE_ID_STORAGE_KEY)
70
+ if (existing && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(existing)) return existing
71
+ const deviceId = crypto.randomUUID()
72
+ localStorage.setItem(DEVICE_ID_STORAGE_KEY, deviceId)
73
+ return deviceId
74
+ }
75
+
76
+ function TeamServiceConfig({ remote }) {
77
+ const [serviceUrl, setServiceUrl] = useState(loadServiceUrl)
78
+ const [draft, setDraft] = useState(serviceUrl)
79
+ const [message, setMessage] = useState('')
80
+ const save = (event) => {
81
+ event.preventDefault()
82
+ const normalized = normalizeServiceUrl(draft)
83
+ if (!normalized) {
84
+ setMessage('请输入 HTTPS 团队服务地址;本机测试可使用 localhost。')
85
+ return
86
+ }
87
+ try {
88
+ localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized)
89
+ setServiceUrl(normalized)
90
+ setDraft(normalized)
91
+ setMessage('团队服务地址已保存。')
92
+ } catch {
93
+ setMessage('浏览器无法保存此地址,请检查本地存储权限。')
94
+ }
95
+ }
96
+ const link = consoleUrl(serviceUrl)
97
+ return h('section', { style: { display: 'grid', gap: 16, width: '100%', maxWidth: 1120 } },
98
+ h('p', { style: mutedTextStyle }, '连接团队服务后,即可同步获准模型并使用团队协作。'),
99
+ h('section', { style: panelStyle },
100
+ h('div', null,
101
+ h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '团队服务'),
102
+ h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '由团队管理员提供服务地址;管理员可在控制台管理成员、模型和协作设置。')),
103
+ h('form', { onSubmit: save, style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', alignItems: 'end', gap: 10 } },
104
+ h('label', { htmlFor: 'ant-team-service-url', style: { gridColumn: '1 / -1', fontSize: 13, fontWeight: 550 } }, '服务地址'),
105
+ h('input', {
106
+ id: 'ant-team-service-url',
107
+ type: 'url',
108
+ required: true,
109
+ value: draft,
110
+ placeholder: 'https://ant.example.com',
111
+ onChange: (event) => setDraft(event.target.value),
112
+ style: inputStyle,
113
+ }),
114
+ h('button', { type: 'submit', style: primaryButtonStyle }, '保存地址')),
115
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 } },
116
+ message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
117
+ link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null)),
118
+ h(ModelSyncFooter, { remote, serviceUrl, onServiceUrlChange: setServiceUrl }))
119
+ }
120
+
121
+ function ModelSyncFooter({ remote, serviceUrl, onServiceUrlChange }) {
122
+ const [email, setEmail] = useState('')
123
+ const [code, setCode] = useState('')
124
+ const [message, setMessage] = useState('')
125
+ const [busy, setBusy] = useState(false)
126
+ const [codeRequested, setCodeRequested] = useState(false)
127
+ const [credentialConfigured, setCredentialConfigured] = useState(null)
128
+ const [reauthenticate, setReauthenticate] = useState(false)
129
+ const [needsReconnect, setNeedsReconnect] = useState(false)
130
+ const [feishuCode, setFeishuCode] = useState('')
131
+ const [feishuCodeMessage, setFeishuCodeMessage] = useState('')
132
+ const [feishuApps, setFeishuApps] = useState([])
133
+ const [feishuAppId, setFeishuAppId] = useState('')
134
+ const [feishuAppSecret, setFeishuAppSecret] = useState('')
135
+ const [feishuVerificationToken, setFeishuVerificationToken] = useState('')
136
+ const [feishuEncryptKey, setFeishuEncryptKey] = useState('')
137
+ const [feishuAppStatus, setFeishuAppStatus] = useState('')
138
+ const [bundleRelease, setBundleRelease] = useState(null)
139
+ const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
140
+ const [approvedBuilds, setApprovedBuilds] = useState([])
141
+ const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
142
+
143
+ const inspectServiceCompatibility = async (rawServiceUrl, useSavedConnection = false) => {
144
+ const normalized = normalizeServiceUrl(rawServiceUrl)
145
+ if (!normalized && !useSavedConnection) {
146
+ setServiceCompatibility({ state: 'idle', message: '' })
147
+ return null
148
+ }
149
+ setServiceCompatibility({ state: 'checking', message: '正在识别 ANT 客户端与服务端版本…' })
150
+ try {
151
+ const manager = remote?.pluginManager
152
+ if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 未提供 Bundle 版本读取接口。')
153
+ const listed = await manager.listBundles()
154
+ if (!listed.ok) throw new Error(listed.error?.message || '无法读取已安装的 ANT Bundle 版本。')
155
+ const installed = listed.value.find((bundle) => bundle.name === 'ant' && bundle.installed)
156
+ if (typeof installed?.version !== 'string') throw new Error('无法确定当前 ANT Bundle 版本。')
157
+ const response = await fetch('/api/ant-teams/service-info', {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({
161
+ service_url: normalized || '',
162
+ bundle_version: installed.version,
163
+ api_protocol_versions: ANT_API_PROTOCOL_VERSIONS,
164
+ }),
165
+ })
166
+ const result = await response.json()
167
+ if (!response.ok) throw new Error(result.message || `版本检查失败(${response.status})`)
168
+ const serverProtocols = Array.isArray(result.server_api_protocol_versions) ? result.server_api_protocol_versions : []
169
+ const sharedProtocols = Array.isArray(result.shared_api_protocol_versions) ? result.shared_api_protocol_versions : []
170
+ const compatibility = typeof result.compatibility === 'boolean'
171
+ ? result.compatibility
172
+ : serverProtocols.length === 0 ? null : sharedProtocols.length > 0
173
+ const message = compatibility === true
174
+ ? `服务端 v${result.server_version} · ANT Bundle v${result.bundle_version} · API 协议 v${sharedProtocols.join(', v')}:兼容。`
175
+ : compatibility === false
176
+ ? `服务端 v${result.server_version} · ANT Bundle v${result.bundle_version}:没有共同 API 协议(服务端 v${serverProtocols.join(', v')},客户端 v${ANT_API_PROTOCOL_VERSIONS.join(', v')})。请更新 ANT 插件或联系管理员升级服务端。`
177
+ : `已识别 ANT Bundle v${result.bundle_version} 和服务端 v${result.server_version};服务端未提供协议能力,兼容性未知,仍允许继续。`
178
+ const state = compatibility === true ? 'compatible' : compatibility === false ? 'incompatible' : 'unknown'
179
+ const status = { state, message, ...result }
180
+ setServiceCompatibility(status)
181
+ return status
182
+ } catch (error) {
183
+ const status = {
184
+ state: 'unknown',
185
+ message: `无法确认 ANT 版本兼容性,仍允许继续:${error instanceof Error ? error.message : '连接检查失败。'}`,
186
+ }
187
+ setServiceCompatibility(status)
188
+ return status
189
+ }
190
+ }
191
+
192
+ const requireCompatibleService = async (rawServiceUrl, useSavedConnection = false) => {
193
+ const status = await inspectServiceCompatibility(rawServiceUrl, useSavedConnection)
194
+ if (status?.state === 'incompatible') throw new Error(status.message)
195
+ return status
196
+ }
197
+
198
+ const applyAuthorizedModels = async (models, rawServiceUrl) => {
199
+ const normalized = normalizeServiceUrl(rawServiceUrl)
200
+ if (!normalized || !Array.isArray(models)) throw new Error('ANT 返回的团队模型目录格式无效。')
201
+ if (typeof remote?.settings?.mutate !== 'function') throw new Error('当前 DSH 版本未提供模型设置接口。')
202
+ const grouped = {
203
+ openai: models.filter((model) => model.protocol === 'openai'),
204
+ anthropic: models.filter((model) => model.protocol === 'anthropic'),
205
+ }
206
+ const ops = []
207
+ for (const protocol of ['openai', 'anthropic']) {
208
+ const route = protocol === 'openai' ? 'ant-team-openai' : 'ant-team-anthropic'
209
+ const protocolModels = grouped[protocol]
210
+ if (protocolModels.length === 0) {
211
+ ops.push({ op: 'unset', path: ['providers', route] })
212
+ continue
213
+ }
214
+ ops.push({ op: 'set', path: ['providers', route], value: {
215
+ displayName: protocol === 'openai' ? 'Lakala (OpenAI)' : 'Lakala (Anthropic)',
216
+ apiKeyEnv: 'ANT_MODEL_API_KEY',
217
+ api: protocol === 'openai' ? 'openai-completions' : 'anthropic-messages',
218
+ baseURL: protocol === 'openai'
219
+ ? new URL('/model-gateway/openai/v1', normalized).toString()
220
+ : new URL('/model-gateway/anthropic', normalized).toString(),
221
+ models: protocolModels.map((model) => ({ id: model.model_id, name: model.display_name })),
222
+ } })
223
+ }
224
+ const settingsWrite = await remote.settings.mutate('llm-pi-ai', ops, undefined)
225
+ if (!settingsWrite.ok) throw new Error(settingsWrite.error?.message || 'DSH 无法更新团队模型设置。')
226
+ try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized) } catch {}
227
+ onServiceUrlChange(normalized)
228
+ return models.length
229
+ }
230
+
231
+ const refreshModels = async () => {
232
+ setBusy(true)
233
+ setMessage('正在从 ANT 刷新获准模型…')
234
+ try {
235
+ await requireCompatibleService(serviceUrl, true)
236
+ const response = await fetch('/api/ant-teams/models', { method: 'POST' })
237
+ const result = await response.json()
238
+ if (!response.ok) {
239
+ if (response.status === 401) setNeedsReconnect(true)
240
+ throw new Error(result.message || `刷新失败(${response.status})`)
241
+ }
242
+ const count = await applyAuthorizedModels(result.models, result.service_url)
243
+ setCredentialConfigured(true)
244
+ setNeedsReconnect(false)
245
+ setReauthenticate(false)
246
+ setMessage(`模型目录已刷新,共 ${count} 个获准模型。`)
247
+ } catch (error) {
248
+ setMessage(error instanceof Error ? error.message : '刷新模型失败,请稍后重试。')
249
+ } finally {
250
+ setBusy(false)
251
+ }
252
+ }
253
+
254
+ const checkBundleUpdate = async () => {
255
+ setBundleUpdate({ state: 'checking', currentVersion: '', message: '正在检查 ANT 更新…' })
256
+ setApprovedBuilds([])
257
+ try {
258
+ const response = await fetch('/api/ant-teams/bundle-release', { cache: 'no-store' })
259
+ const release = await response.json()
260
+ if (!response.ok) throw new Error(release.message || `检查失败(${response.status})`)
261
+ setBundleRelease(release)
262
+ if (!release.version) {
263
+ setBundleUpdate({ state: 'none', currentVersion: '', message: '管理员尚未发布 ANT 更新。' })
264
+ return
265
+ }
266
+ const manager = remote?.pluginManager
267
+ if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 版本未提供官方 Bundle 管理器。')
268
+ const listed = await manager.listBundles()
269
+ if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 已安装 Bundle。')
270
+ const installed = listed.value.find((bundle) => bundle.name === release.bundle_name && bundle.installed)
271
+ if (!installed || typeof installed.version !== 'string') {
272
+ throw new Error(`当前 DSH 未找到已安装的 ${release.bundle_name} Bundle。`)
273
+ }
274
+ const available = newerVersion(release.version, installed.version)
275
+ setBundleUpdate({
276
+ state: available ? 'available' : 'current',
277
+ currentVersion: installed.version,
278
+ message: available ? `发现新版本 ${release.version}。` : `当前已是最新发布版本(${installed.version})。`,
279
+ })
280
+ } catch (error) {
281
+ setBundleUpdate({ state: 'failed', currentVersion: '', message: error instanceof Error ? error.message : '检查 ANT 更新失败。' })
282
+ }
283
+ }
284
+
285
+ const installBundleUpdate = async (allowScripts = false) => {
286
+ if (!bundleRelease?.install_spec
287
+ || (!allowScripts && bundleUpdate.state !== 'available')
288
+ || (allowScripts && approvedBuilds.length === 0)) return
289
+ const manager = remote?.pluginManager
290
+ if (typeof manager?.installBundle !== 'function') {
291
+ setBundleUpdate((current) => ({ ...current, state: 'failed', message: '当前 DSH 版本未提供官方 Bundle 安装接口。' }))
292
+ return
293
+ }
294
+ const scripts = allowScripts ? approvedBuilds : []
295
+ if (scripts.length && !window.confirm(`DSH 安装需要运行以下依赖的安装脚本:\n\n${scripts.join('\n')}\n\n仅在信任这些依赖时继续。`)) return
296
+ setBusy(true)
297
+ setApprovedBuilds([])
298
+ setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在委托 DSH 官方管理器安装更新…' }))
299
+ try {
300
+ const result = await manager.installBundle(bundleRelease.install_spec, {
301
+ enabled: true,
302
+ ...(scripts.length ? { approvedBuilds: scripts } : {}),
303
+ })
304
+ if (!result.ok) throw new Error(result.error?.message || 'DSH 安装请求未返回结果。')
305
+ const outcome = result.value
306
+ if (outcome.application === 'failed') {
307
+ const pending = Array.isArray(outcome.pendingBuilds) ? outcome.pendingBuilds : []
308
+ setApprovedBuilds(pending)
309
+ const reason = outcome.error?.diagnostic || outcome.packageResult?.output || 'DSH 未能安装该版本。'
310
+ setBundleUpdate({
311
+ state: 'failed', currentVersion: bundleUpdate.currentVersion,
312
+ message: pending.length
313
+ ? `DSH 暂停了安装,因为以下依赖需要明确授权运行安装脚本:${pending.join('、')}`
314
+ : reason,
315
+ })
316
+ return
317
+ }
318
+ if (outcome.application === 'cancelled') {
319
+ setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已取消更新,当前运行版本未切换。' })
320
+ return
321
+ }
322
+ if (outcome.application === 'overridden') {
323
+ setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已记录更新,但其他插件配置覆盖了 ANT。请检查 DSH 插件配置后再重启。' })
324
+ return
325
+ }
326
+ if (outcome.bundle !== bundleRelease.bundle_name) {
327
+ setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 返回的安装包名称与 ANT 发布信息不一致。请联系管理员核对发布包。' })
328
+ return
329
+ }
330
+ setBundleUpdate({
331
+ state: 'done', currentVersion: bundleRelease.version,
332
+ message: outcome.application === 'restart-required'
333
+ ? `DSH 已安装 ${bundleRelease.install_spec}。请完全退出并重新启动 DSH 后生效。`
334
+ : `DSH 已安装 ${bundleRelease.install_spec}。`,
335
+ })
336
+ } catch (error) {
337
+ setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: error instanceof Error ? error.message : 'DSH 未能安装 ANT 更新。' })
338
+ } finally {
339
+ setBusy(false)
340
+ }
341
+ }
342
+
343
+ const disconnect = async () => {
344
+ setBusy(true)
345
+ setMessage('正在断开此设备的 ANT 连接…')
346
+ try {
347
+ if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
348
+ const result = await remote.settings.mutate('llm-pi-ai', [
349
+ { op: 'unset', path: ['providers', 'ant-team-openai'] },
350
+ { op: 'unset', path: ['providers', 'ant-team-anthropic'] },
351
+ ], undefined)
352
+ if (!result.ok) throw new Error(result.error?.message || 'ANT 模型设置未能清除。')
353
+ } catch (error) {
354
+ setBusy(false)
355
+ setMessage(`无法清除本机 ANT 模型设置,因此连接仍保留。${error instanceof Error ? error.message : '请稍后重试。'}`)
356
+ return
357
+ }
358
+ let serverRevoked = false
359
+ try {
360
+ const response = await fetch('/api/ant-teams/disconnect', { method: 'POST' })
361
+ const result = await response.json()
362
+ serverRevoked = response.ok && result.revoked === true
363
+ } catch {}
364
+ let credentialRemoved = false
365
+ try {
366
+ if (typeof remote?.credentials?.unset !== 'function') throw new Error('DSH 未提供凭证删除接口。')
367
+ const result = await remote.credentials.unset('ANT_MODEL_API_KEY')
368
+ if (!result.ok) throw new Error(result.error?.message || 'ANT 成员凭证未能清除。')
369
+ credentialRemoved = true
370
+ setCredentialConfigured(false)
371
+ } catch (error) {
372
+ setMessage(`本机 ANT 模型设置已清除,但成员凭证未能移除:${error instanceof Error ? error.message : '请稍后重试。'}`)
373
+ }
374
+ setBusy(false)
375
+ if (credentialRemoved && serverRevoked) setMessage('此设备的 ANT 凭证已撤销,ANT 模型设置已清除。团队服务地址已保留。')
376
+ else if (credentialRemoved) setMessage('本机 ANT 凭证与模型设置已清除;服务端未确认撤销,活动会话会在 90 秒内到期。团队服务地址已保留。')
377
+ }
378
+
379
+ useEffect(() => {
380
+ let current = true
381
+ const describe = remote?.credentials?.describe
382
+ if (typeof describe !== 'function') {
383
+ setCredentialConfigured(null)
384
+ return () => { current = false }
385
+ }
386
+ describe.call(remote.credentials, ['ANT_MODEL_API_KEY']).then((result) => {
387
+ if (!current) return
388
+ const configured = result.ok ? Boolean(result.value?.ANT_MODEL_API_KEY?.configured) : null
389
+ setCredentialConfigured(configured)
390
+ if (configured) {
391
+ void refreshModels()
392
+ void checkBundleUpdate()
393
+ void refreshFeishuApps().catch((error) => setFeishuAppStatus(error.message))
394
+ }
395
+ }).catch(() => { if (current) setCredentialConfigured(null) })
396
+ return () => { current = false }
397
+ }, [remote])
398
+
399
+ useEffect(() => {
400
+ void inspectServiceCompatibility(serviceUrl, credentialConfigured === true)
401
+ }, [remote, serviceUrl, credentialConfigured])
402
+
403
+ const requestCode = async (event) => {
404
+ event.preventDefault()
405
+ const normalized = normalizeServiceUrl(serviceUrl)
406
+ const normalizedEmail = email.trim()
407
+ if (!normalized) {
408
+ setMessage('请先在上方填写并保存团队服务地址。')
409
+ return
410
+ }
411
+ if (!normalizedEmail) {
412
+ setMessage('请填写工作邮箱。')
413
+ return
414
+ }
415
+ setBusy(true)
416
+ setMessage('正在发送邮箱验证码…')
417
+ try {
418
+ await requireCompatibleService(normalized)
419
+ localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized)
420
+ const response = await fetch('/api/ant-teams/request-code', {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/json' },
423
+ body: JSON.stringify({ service_url: normalized, email: normalizedEmail }),
424
+ })
425
+ const result = await response.json()
426
+ if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
427
+ setEmail(normalizedEmail)
428
+ setCodeRequested(true)
429
+ setMessage(result.message || '如果该邮箱符合团队规则,验证码将发送至邮箱。')
430
+ } catch (error) {
431
+ setMessage(error instanceof Error ? error.message : '验证码发送失败,请稍后重试。')
432
+ } finally {
433
+ setBusy(false)
434
+ }
435
+ }
436
+
437
+ const connect = async (event) => {
438
+ event.preventDefault()
439
+ const normalized = normalizeServiceUrl(serviceUrl)
440
+ const normalizedEmail = email.trim()
441
+ const normalizedCode = code.trim()
442
+ if (!normalized || !normalizedEmail || !/^\d{8}$/u.test(normalizedCode)) {
443
+ setMessage('请填写有效的服务地址、工作邮箱和 8 位验证码。')
444
+ return
445
+ }
446
+ setBusy(true)
447
+ setMessage('正在验证邮箱并同步团队模型…')
448
+ try {
449
+ await requireCompatibleService(normalized)
450
+ localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized)
451
+ const response = await fetch('/api/ant-teams/connect', {
452
+ method: 'POST',
453
+ headers: { 'Content-Type': 'application/json' },
454
+ body: JSON.stringify({
455
+ service_url: normalized,
456
+ email: normalizedEmail,
457
+ code: normalizedCode,
458
+ device_id: loadDeviceId(),
459
+ }),
460
+ })
461
+ const result = await response.json()
462
+ if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
463
+ if (typeof result.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
464
+ throw new Error('ANT 返回的连接信息格式无效。')
465
+ }
466
+ if (typeof remote?.credentials?.set !== 'function' || typeof remote?.settings?.mutate !== 'function') {
467
+ throw new Error('当前 DSH 版本未提供安全凭证和模型设置接口。')
468
+ }
469
+ const credentialWrite = await remote.credentials.set('ANT_MODEL_API_KEY', result.token)
470
+ if (!credentialWrite.ok) throw new Error(credentialWrite.error?.message || 'DSH 无法安全保存 ANT 凭证。')
471
+ await applyAuthorizedModels(result.models, normalized)
472
+ setCode('')
473
+ onServiceUrlChange(normalized)
474
+ setCodeRequested(false)
475
+ setCredentialConfigured(true)
476
+ setNeedsReconnect(false)
477
+ setReauthenticate(false)
478
+ await refreshFeishuApps().catch((error) => setFeishuAppStatus(error.message))
479
+ setMessage(`已连接 ${result.email || normalizedEmail},同步 ${result.models.length} 个获准模型。它们现已出现在 DSH 模型选择器中。`)
480
+ } catch (error) {
481
+ setMessage(error instanceof Error ? error.message : '连接失败,请稍后重试。')
482
+ } finally {
483
+ setBusy(false)
484
+ }
485
+ }
486
+
487
+ const manageFeishuApps = async (body) => {
488
+ const response = await fetch('/api/ant-teams/feishu/apps', {
489
+ method: 'POST',
490
+ headers: { 'Content-Type': 'application/json' },
491
+ body: JSON.stringify(body),
492
+ })
493
+ const result = await response.json()
494
+ if (!response.ok) throw new Error(result.message || `飞书操作失败(${response.status})`)
495
+ return result
496
+ }
497
+
498
+ const refreshFeishuApps = async () => {
499
+ const result = await manageFeishuApps({ action: 'list' })
500
+ if (!Array.isArray(result.apps)) throw new Error('ANT 返回的飞书应用列表格式无效。')
501
+ setFeishuApps(result.apps)
502
+ }
503
+
504
+ const saveFeishuApp = async (event) => {
505
+ event.preventDefault()
506
+ setBusy(true)
507
+ setFeishuAppStatus('正在验证飞书应用并保存密钥…')
508
+ setFeishuCode('')
509
+ setFeishuCodeMessage('')
510
+ try {
511
+ const result = await manageFeishuApps({
512
+ action: 'save',
513
+ app_id: feishuAppId.trim(),
514
+ app_secret: feishuAppSecret,
515
+ verification_token: feishuVerificationToken,
516
+ encrypt_key: feishuEncryptKey,
517
+ })
518
+ if (!result.app?.id || !result.app?.callback_url) throw new Error('ANT 未返回飞书回调信息。')
519
+ setFeishuAppId('')
520
+ setFeishuAppSecret('')
521
+ setFeishuVerificationToken('')
522
+ setFeishuEncryptKey('')
523
+ setFeishuApps((apps) => [...apps.filter((app) => app.id !== result.app.id), result.app])
524
+ setFeishuAppStatus('App ID 和 App Secret 已验证;回调密钥已加密保存。请把下方回调地址填入该飞书应用的事件订阅设置。')
525
+ } catch (error) {
526
+ setFeishuAppStatus(error instanceof Error ? error.message : '飞书应用保存失败,请稍后重试。')
527
+ } finally {
528
+ setBusy(false)
529
+ }
530
+ }
531
+
532
+ const createFeishuLinkCode = async (app) => {
533
+ setBusy(true)
534
+ setFeishuCode('')
535
+ setFeishuCodeMessage('正在生成一次性飞书绑定码…')
536
+ try {
537
+ const result = await manageFeishuApps({ action: 'link-code', id: app.id })
538
+ if (typeof result.code !== 'string') throw new Error(result.message || 'ANT 未返回绑定码。')
539
+ setFeishuCode(result.code)
540
+ setFeishuCodeMessage(`有效期 ${Math.ceil(result.expires_in_seconds / 60)} 分钟。请在飞书中私聊该机器人,发送:绑定 ${result.code}`)
541
+ } catch (error) {
542
+ setFeishuCodeMessage(error instanceof Error ? error.message : '飞书绑定码生成失败,请稍后重试。')
543
+ } finally {
544
+ setBusy(false)
545
+ }
546
+ }
547
+
548
+ const disableFeishuApp = async (app) => {
549
+ if (!window.confirm(`停用飞书应用 ${app.app_id}?停用后它不能再触发新任务,已接收的任务仍会尝试完成并回复。`)) return
550
+ setBusy(true)
551
+ setFeishuAppStatus('正在停用飞书应用…')
552
+ try {
553
+ await manageFeishuApps({ action: 'disable', id: app.id })
554
+ await refreshFeishuApps()
555
+ setFeishuAppStatus('飞书应用已停用,并解除其成员身份绑定。')
556
+ } catch (error) {
557
+ setFeishuAppStatus(error instanceof Error ? error.message : '飞书应用停用失败。')
558
+ } finally {
559
+ setBusy(false)
560
+ }
561
+ }
562
+
563
+ const showConnectForm = credentialConfigured !== true || reauthenticate || needsReconnect
564
+ return h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 400px), 1fr))', alignItems: 'start', gap: 16 } },
565
+ h('section', { style: panelStyle },
566
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
567
+ h('div', null,
568
+ h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '团队模型'),
569
+ h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '使用上方团队服务地址验证工作邮箱并同步授权模型。连接状态变化时可随时刷新。')),
570
+ h('span', { style: {
571
+ flexShrink: 0, padding: '4px 9px', borderRadius: 999,
572
+ color: credentialConfigured === true ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
573
+ backgroundColor: credentialConfigured === true ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
574
+ fontSize: 12,
575
+ } }, credentialConfigured === true ? '已连接' : credentialConfigured === false ? '未连接' : '检查中')),
576
+ h('p', { role: 'status', style: mutedTextStyle }, credentialConfigured === true
577
+ ? 'DSH 已安全保存此设备的 ANT 团队凭证。'
578
+ : credentialConfigured === false ? '验证工作邮箱后即可连接团队模型。' : '正在检查 ANT 连接状态…'),
579
+ serviceCompatibility.state !== 'idle' ? h('p', {
580
+ role: 'status',
581
+ style: {
582
+ ...mutedTextStyle,
583
+ color: serviceCompatibility.state === 'incompatible' ? 'var(--dsw-alias-state-error-primary)' : mutedTextStyle.color,
584
+ },
585
+ }, serviceCompatibility.message) : null,
586
+ credentialConfigured === true && !showConnectForm ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
587
+ h('button', { type: 'button', disabled: busy, onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
588
+ h('button', { type: 'button', disabled: busy, onClick: () => { setReauthenticate(true); setMessage('') }, style: secondaryButtonStyle }, '重新验证邮箱'),
589
+ h('button', { type: 'button', disabled: busy, onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() }, style: secondaryButtonStyle }, '断开当前设备')) : null,
590
+ credentialConfigured === true && !showConnectForm ? h('section', { style: { ...panelStyle, padding: 14, gap: 10 } },
591
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 } },
592
+ h('h4', { style: { margin: 0, fontSize: 14, fontWeight: 650 } }, 'ANT 插件更新'),
593
+ h('button', { type: 'button', disabled: busy || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
594
+ bundleUpdate.message ? h('p', { role: 'status', style: mutedTextStyle }, bundleUpdate.message) : null,
595
+ bundleRelease?.version && bundleUpdate.state === 'available' ? h('div', { style: { display: 'grid', gap: 8 } },
596
+ h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · npm:${bundleRelease.install_spec}`),
597
+ bundleRelease.release_notes ? h('p', { style: mutedTextStyle }, bundleRelease.release_notes) : null,
598
+ h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '安装中…' : '由 DSH 安装并更新')) : null,
599
+ approvedBuilds.length ? h('div', { style: { display: 'grid', gap: 8 } },
600
+ h('p', { style: mutedTextStyle }, `DSH 暂停安装,等待授权运行以下依赖的安装脚本:${approvedBuilds.join('、')}。`),
601
+ h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(true), style: secondaryButtonStyle }, '信任这些依赖并继续安装')) : null) : null,
602
+ showConnectForm ? h('form', { onSubmit: requestCode, style: { display: 'grid', gap: 9, maxWidth: 560 } },
603
+ h('label', { htmlFor: 'ant-team-model-email', style: { fontSize: 13, fontWeight: 550 } }, '工作邮箱'),
604
+ h('input', {
605
+ id: 'ant-team-model-email', type: 'email', autoComplete: 'email', required: true, value: email,
606
+ onChange: (event) => setEmail(event.target.value), style: inputStyle,
607
+ }),
608
+ h('button', { type: 'submit', disabled: busy, style: primaryButtonStyle }, busy ? '发送中…' : '发送验证码')) : null,
609
+ credentialConfigured === true && showConnectForm ? h('button', {
610
+ type: 'button', disabled: busy,
611
+ onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() },
612
+ style: secondaryButtonStyle,
613
+ }, '断开当前设备') : null,
614
+ showConnectForm && codeRequested ? h('form', { onSubmit: connect, style: { display: 'grid', gap: 9, maxWidth: 560 } },
615
+ h('label', { htmlFor: 'ant-team-model-code', style: { fontSize: 13, fontWeight: 550 } }, '邮箱验证码'),
616
+ h('input', {
617
+ id: 'ant-team-model-code', type: 'text', inputMode: 'numeric', autoComplete: 'one-time-code',
618
+ required: true, maxLength: 8, value: code, placeholder: '8 位验证码',
619
+ onChange: (event) => setCode(event.target.value.replace(/\D/gu, '').slice(0, 8)), style: inputStyle,
620
+ }),
621
+ h('button', { type: 'submit', disabled: busy || code.length !== 8, style: primaryButtonStyle }, busy ? '连接中…' : '验证并连接')) : null,
622
+ message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null),
623
+ credentialConfigured === true && !needsReconnect && !reauthenticate ? h('section', { style: panelStyle },
624
+ h('div', null,
625
+ h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '个人飞书应用'),
626
+ h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '每位成员自行管理飞书应用和身份绑定。群 ID 不参与授权;只有绑定到此应用的本人可以触发自己的 DSH。')),
627
+ h('form', { onSubmit: saveFeishuApp, style: { display: 'grid', gap: 9 } },
628
+ h('label', { htmlFor: 'ant-feishu-app-id', style: { fontSize: 13, fontWeight: 550 } }, '飞书 App ID'),
629
+ h('input', { id: 'ant-feishu-app-id', value: feishuAppId, required: true, maxLength: 128, placeholder: 'cli_…', onChange: (event) => setFeishuAppId(event.target.value), style: inputStyle }),
630
+ h('p', { style: mutedTextStyle }, '在飞书开放平台的应用凭证页查看,通常以 cli_ 开头。'),
631
+ h('label', { htmlFor: 'ant-feishu-app-secret', style: { fontSize: 13, fontWeight: 550 } }, 'App Secret'),
632
+ h('input', { id: 'ant-feishu-app-secret', type: 'password', autoComplete: 'new-password', value: feishuAppSecret, required: true, maxLength: 512, onChange: (event) => setFeishuAppSecret(event.target.value), style: inputStyle }),
633
+ h('p', { style: mutedTextStyle }, '同一应用凭证页中的密钥;ANT 用它验证 App ID 并读取机器人身份。'),
634
+ h('label', { htmlFor: 'ant-feishu-verification-token', style: { fontSize: 13, fontWeight: 550 } }, 'Verification Token'),
635
+ h('input', { id: 'ant-feishu-verification-token', type: 'password', autoComplete: 'new-password', value: feishuVerificationToken, required: true, maxLength: 512, onChange: (event) => setFeishuVerificationToken(event.target.value), style: inputStyle }),
636
+ h('p', { style: mutedTextStyle }, '在事件订阅设置中生成,用来校验飞书回调是否属于此应用。'),
637
+ h('label', { htmlFor: 'ant-feishu-encrypt-key', style: { fontSize: 13, fontWeight: 550 } }, 'Encrypt Key'),
638
+ h('input', { id: 'ant-feishu-encrypt-key', type: 'password', autoComplete: 'new-password', value: feishuEncryptKey, required: true, maxLength: 512, onChange: (event) => setFeishuEncryptKey(event.target.value), style: inputStyle }),
639
+ h('p', { style: mutedTextStyle }, '在事件订阅设置中生成;启用事件加密时,ANT 用它校验签名并解密回调。'),
640
+ h('p', { style: mutedTextStyle }, '保存时 ANT 会验证 App ID 和 App Secret,并读取该应用机器人的身份。所有密钥只在服务端加密保存。'),
641
+ h('button', { type: 'submit', disabled: busy, style: primaryButtonStyle }, busy ? '正在验证并保存…' : '验证并保存应用')),
642
+ feishuAppStatus ? h('p', { role: 'status', style: mutedTextStyle }, feishuAppStatus) : null,
643
+ feishuApps.length ? h('div', { style: { display: 'grid', gap: 10 } }, ...feishuApps.map((app) => h('article', { key: app.id, style: { ...panelStyle, padding: 14, gap: 8 } },
644
+ h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 12 } },
645
+ h('strong', null, app.app_id),
646
+ h('span', { style: mutedTextStyle }, !app.active ? '已停用' : app.linked ? '已绑定飞书身份' : '尚未绑定')),
647
+ h('p', { style: mutedTextStyle }, '飞书事件回调地址:'),
648
+ h('code', { style: { overflowWrap: 'anywhere', padding: 8, borderRadius: 6, backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, app.callback_url),
649
+ app.active ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
650
+ h('button', { type: 'button', disabled: busy, onClick: () => void createFeishuLinkCode(app), style: primaryButtonStyle }, '生成此机器人的绑定码'),
651
+ h('button', { type: 'button', disabled: busy, onClick: () => void disableFeishuApp(app), style: secondaryButtonStyle }, '停用应用'))
652
+ : h('button', { type: 'button', disabled: busy, onClick: () => { setFeishuAppId(app.app_id); setFeishuAppStatus('请重新填写此应用的三个密钥以启用。') }, style: secondaryButtonStyle }, '重新配置启用')))) : null,
653
+ feishuCode ? h('code', { style: {
654
+ display: 'block', padding: '12px 14px', borderRadius: 9,
655
+ color: 'var(--dsw-alias-label-primary)',
656
+ backgroundColor: 'var(--dsw-alias-bg-layer-2)', fontSize: 18, letterSpacing: '.12em',
657
+ } }, feishuCode) : null,
658
+ feishuCodeMessage ? h('p', { role: 'status', style: mutedTextStyle }, feishuCodeMessage) : null,
659
+ h('p', { style: mutedTextStyle }, '保存后,把对应应用显示的回调地址填入飞书事件订阅,订阅 im.message.receive_v1。私聊绑定和机器人收发消息需要 im:message;群内 @ 机器人触发还需要 im:message.group_at_msg。验证回调后生成绑定码,再在飞书中私聊该机器人发送“绑定 <验证码>”。之后可在任意群中 @ 该机器人;ANT 会核对机器人和发送者身份,再把任务交给你的 DSH。')) : null)
660
+ }
661
+
662
+ function apply(ctx) {
663
+ const remote = {
664
+ credentials: ctx.remote.credentials,
665
+ settings: ctx.remote.settings,
666
+ pluginManager: ctx.remote.pluginManager,
667
+ }
668
+ ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
669
+ name: 'plugins.bundle.config',
670
+ key: 'ant',
671
+ inject: () => ({ remote }),
672
+ }, ({ view, remote: injectedRemote }) => view === 'summary'
673
+ ? h('p', null, 'ANT 团队服务入口。')
674
+ : h(TeamServiceConfig, { remote: injectedRemote })))
675
+ }
676
+
677
+ Object.assign(exports, { inject, apply })
678
+ return module.exports
679
+ },
680
+ })