@ai-wendao/ant 0.3.12 → 0.3.14
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/MIGRATION.md +6 -6
- package/README.md +9 -1
- package/client.js +503 -99
- package/index.js +166 -54
- package/package.json +1 -1
- package/skill-install.js +13 -33
- package/tools/migrate-legacy.mjs +11 -5
package/client.js
CHANGED
|
@@ -9,6 +9,7 @@ window.__ModuleLoader__.load({
|
|
|
9
9
|
const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
|
|
10
10
|
const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
|
|
11
11
|
const ANT_API_PROTOCOL_VERSIONS = [1]
|
|
12
|
+
const PLUGIN_INSTALL_RECORD_PREFIX = 'ant-reviewed-plugin-install:'
|
|
12
13
|
const fileCountFormat = new Intl.NumberFormat('zh-CN')
|
|
13
14
|
const panelStyle = {
|
|
14
15
|
display: 'grid', alignContent: 'start', gap: 14, padding: 18,
|
|
@@ -33,6 +34,15 @@ window.__ModuleLoader__.load({
|
|
|
33
34
|
border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 8,
|
|
34
35
|
cursor: 'pointer',
|
|
35
36
|
}
|
|
37
|
+
function ExtensionRequestNotice() {
|
|
38
|
+
return h('aside', { role: 'note', style: {
|
|
39
|
+
...panelStyle,
|
|
40
|
+
borderColor: 'var(--dsw-alias-state-business-primary)',
|
|
41
|
+
gap: 5,
|
|
42
|
+
} },
|
|
43
|
+
h('h2', { style: { margin: 0, fontSize: 16 } }, '没有找到想用的技能或插件?'),
|
|
44
|
+
h('p', { style: mutedTextStyle }, '目录里没有想用的扩展,或插件没有安装按钮?请联系企业管理员,说明你希望完成的任务或提供扩展地址。已开放安装的技能和插件可在这里直接安装;勿自行安装未经审核的扩展。'))
|
|
45
|
+
}
|
|
36
46
|
const normalizeServiceUrl = (value) => {
|
|
37
47
|
let url
|
|
38
48
|
try { url = new URL(value.trim()) } catch { return null }
|
|
@@ -88,22 +98,81 @@ window.__ModuleLoader__.load({
|
|
|
88
98
|
: ''
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
function approvedArtifactUrl(release) {
|
|
102
|
+
const expectedPath = `/api/ant-bundles/${release?.version}/package.tgz`
|
|
103
|
+
let url
|
|
104
|
+
try { url = new URL(release?.artifact_url) } catch { return null }
|
|
105
|
+
return normalizeServiceUrl(url.origin) === url.origin
|
|
106
|
+
&& !url.username && !url.password && !url.search && !url.hash
|
|
107
|
+
&& url.pathname === expectedPath && release?.artifact_path === expectedPath
|
|
108
|
+
&& /^[a-f0-9]{64}$/u.test(release?.artifact_sha256 || '')
|
|
109
|
+
? url.href : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function approvedPluginArtifactUrl(plugin) {
|
|
113
|
+
if (plugin?.status !== 'published' || typeof plugin.id !== 'string'
|
|
114
|
+
|| !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(plugin.id)) return null
|
|
115
|
+
const expectedPath = `/api/plugin-artifacts/${plugin.id}/package.tgz`
|
|
116
|
+
let url
|
|
117
|
+
try { url = new URL(plugin.artifact_url) } catch { return null }
|
|
118
|
+
return normalizeServiceUrl(url.origin) === url.origin
|
|
119
|
+
&& !url.username && !url.password && !url.search && !url.hash
|
|
120
|
+
&& url.pathname === expectedPath && plugin.artifact_path === expectedPath
|
|
121
|
+
&& /^[a-f0-9]{64}$/u.test(plugin.artifact_sha256 || '')
|
|
122
|
+
? url.href : null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function previouslyInstalledReviewedPlugin(plugin) {
|
|
126
|
+
try { return localStorage.getItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`) === plugin.artifact_sha256 }
|
|
127
|
+
catch { return false }
|
|
128
|
+
}
|
|
129
|
+
|
|
91
130
|
function loadServiceUrl() {
|
|
92
131
|
try { return normalizeServiceUrl(localStorage.getItem(SERVICE_URL_STORAGE_KEY) || '') || '' } catch { return '' }
|
|
93
132
|
}
|
|
94
133
|
|
|
134
|
+
function savedDeviceId() {
|
|
135
|
+
try {
|
|
136
|
+
const existing = localStorage.getItem(DEVICE_ID_STORAGE_KEY)
|
|
137
|
+
return existing && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(existing) ? existing : null
|
|
138
|
+
} catch { return null }
|
|
139
|
+
}
|
|
140
|
+
|
|
95
141
|
function loadDeviceId() {
|
|
96
|
-
const existing =
|
|
97
|
-
if (existing
|
|
142
|
+
const existing = savedDeviceId()
|
|
143
|
+
if (existing) return existing
|
|
98
144
|
const deviceId = crypto.randomUUID()
|
|
99
145
|
localStorage.setItem(DEVICE_ID_STORAGE_KEY, deviceId)
|
|
100
146
|
return deviceId
|
|
101
147
|
}
|
|
102
148
|
|
|
103
|
-
function TeamServiceConfig({ remote,
|
|
149
|
+
function TeamServiceConfig({ remote, openInstalledPlugin }) {
|
|
104
150
|
const [serviceUrl, setServiceUrl] = useState(loadServiceUrl)
|
|
105
151
|
const [draft, setDraft] = useState(serviceUrl)
|
|
106
152
|
const [message, setMessage] = useState('')
|
|
153
|
+
const [pluginAlerts, setPluginAlerts] = useState([])
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
let disposed = false
|
|
156
|
+
const inspect = async () => {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
|
|
159
|
+
if (!response.ok) return
|
|
160
|
+
const catalog = await readLocalJsonResponse(response)
|
|
161
|
+
const listed = await remote?.pluginManager?.listBundles?.()
|
|
162
|
+
if (!listed?.ok || !Array.isArray(catalog.plugins)) return
|
|
163
|
+
const affected = catalog.plugins.flatMap((plugin) => {
|
|
164
|
+
if (plugin.status !== 'security_withdrawn') return []
|
|
165
|
+
const local = listed.value.find((bundle) => bundle.installed && bundle.name === plugin.package_name)
|
|
166
|
+
if (!local || (local.version && local.version !== plugin.version)) return []
|
|
167
|
+
return [{ ...plugin, versionUnknown: !local.version }]
|
|
168
|
+
})
|
|
169
|
+
if (!disposed) setPluginAlerts(affected)
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
void inspect()
|
|
173
|
+
const timer = setInterval(() => void inspect(), 5 * 60 * 1000)
|
|
174
|
+
return () => { disposed = true; clearInterval(timer) }
|
|
175
|
+
}, [remote])
|
|
107
176
|
const save = (event) => {
|
|
108
177
|
event.preventDefault()
|
|
109
178
|
const normalized = normalizeServiceUrl(draft)
|
|
@@ -142,15 +211,16 @@ window.__ModuleLoader__.load({
|
|
|
142
211
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 } },
|
|
143
212
|
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
|
|
144
213
|
link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null)),
|
|
145
|
-
h('section', {
|
|
146
|
-
h('
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
h('button', {
|
|
214
|
+
pluginAlerts.length ? h('section', { role: 'alert', style: { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } },
|
|
215
|
+
h('h3', { style: { margin: 0, fontSize: 16 } }, '已安装插件存在安全撤回公告'),
|
|
216
|
+
...pluginAlerts.map((plugin) => h('p', { key: plugin.id, style: mutedTextStyle },
|
|
217
|
+
`${plugin.title} ${plugin.version}${plugin.versionUnknown ? '(本机版本暂无法读取,请核对是否受影响)' : ''}:${plugin.withdrawal_reason}`)),
|
|
218
|
+
...pluginAlerts.map((plugin) => h('button', { key: `${plugin.id}-open`, type: 'button',
|
|
219
|
+
onClick: () => openInstalledPlugin(plugin.package_name), style: secondaryButtonStyle }, `在 DSH 中查看 ${plugin.title}`))) : null,
|
|
150
220
|
h(ModelSyncFooter, { remote, serviceUrl, onServiceUrlChange: setServiceUrl }))
|
|
151
221
|
}
|
|
152
222
|
|
|
153
|
-
function SkillCenterPage({ onBack }) {
|
|
223
|
+
function SkillCenterPage({ onBack, onPlugins }) {
|
|
154
224
|
const [skills, setSkills] = useState([])
|
|
155
225
|
const [loading, setLoading] = useState(false)
|
|
156
226
|
const [workingId, setWorkingId] = useState('')
|
|
@@ -191,7 +261,8 @@ window.__ModuleLoader__.load({
|
|
|
191
261
|
const result = await readLocalJsonResponse(response)
|
|
192
262
|
if (!response.ok) throw new Error(result.message || `${action}失败(${response.status})。`)
|
|
193
263
|
await refresh()
|
|
194
|
-
setMessage(`${skill.title}
|
|
264
|
+
setMessage(remove ? `${skill.title} 已从 ANT 管理的本机目录卸载。`
|
|
265
|
+
: `${skill.title} 的文件已${action}到 DSH 用户技能目录。DSH 会在后续会话或模型步骤中读取;若不可用,请检查当前预设是否启用了本地技能。`)
|
|
195
266
|
} catch (error) { setMessage(error instanceof Error ? error.message : `${action}失败。`) }
|
|
196
267
|
finally { setWorkingId('') }
|
|
197
268
|
}
|
|
@@ -200,17 +271,20 @@ window.__ModuleLoader__.load({
|
|
|
200
271
|
const matchesQuery = !query.trim() || `${skill.title} ${skill.description} ${skill.dsh_name}`.toLowerCase().includes(query.trim().toLowerCase())
|
|
201
272
|
return matchesFilter && matchesQuery
|
|
202
273
|
})
|
|
203
|
-
const statusLabel = { installed: '
|
|
274
|
+
const statusLabel = { installed: '已放入本机目录', 'update-available': '可更新', 'not-installed': '未安装', conflict: '本机冲突' }
|
|
204
275
|
return h('section', { 'aria-label': '企业精选技能', style: {
|
|
205
276
|
height: '100%', overflowY: 'auto', boxSizing: 'border-box',
|
|
206
277
|
padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
|
|
207
278
|
} }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
|
|
208
|
-
h('
|
|
279
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
280
|
+
h('button', { type: 'button', onClick: onBack, style: secondaryButtonStyle }, '← 返回 ANT 插件设置'),
|
|
281
|
+
h('button', { type: 'button', onClick: onPlugins, style: secondaryButtonStyle }, '查看企业精选插件 →')),
|
|
209
282
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'end', justifyContent: 'space-between', gap: 16 } },
|
|
210
283
|
h('div', null,
|
|
211
284
|
h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选技能'),
|
|
212
285
|
h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '团队管理员审核后发布;安装到这台 DSH 后,由 DSH 原生技能系统发现和使用。')),
|
|
213
286
|
h('button', { type: 'button', disabled: loading || Boolean(workingId), onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
|
|
287
|
+
h(ExtensionRequestNotice),
|
|
214
288
|
h('div', { style: { ...panelStyle, gap: 12 } },
|
|
215
289
|
h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索技能名称或说明', 'aria-label': '搜索企业精选技能', style: inputStyle }),
|
|
216
290
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
|
|
@@ -234,19 +308,213 @@ window.__ModuleLoader__.load({
|
|
|
234
308
|
h('p', { style: mutedTextStyle }, `本机依赖:${skill.requirements}`),
|
|
235
309
|
skill.local_status === 'conflict'
|
|
236
310
|
? h('p', { role: 'status', style: mutedTextStyle }, '本机同名技能有未受 ANT 管理的内容、文件已被修改或备份损坏;请联系管理员处理。')
|
|
237
|
-
:
|
|
238
|
-
? h('div', { style: { display: 'grid', gap: 10 } },
|
|
239
|
-
h('p', { role: 'status', style: mutedTextStyle }, '文件已在本机,但 DSH 技能服务没有发现它;请检查 DSH 技能目录配置。'),
|
|
240
|
-
h('button', { type: 'button', disabled: Boolean(workingId), onClick: () => void changeInstall(skill, true), style: { ...secondaryButtonStyle, justifySelf: 'start' } }, '卸载本机副本'))
|
|
241
|
-
: h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 'auto' } },
|
|
311
|
+
: h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 'auto' } },
|
|
242
312
|
h('button', { type: 'button', disabled: Boolean(workingId) || skill.local_status === 'installed',
|
|
243
313
|
onClick: () => void changeInstall(skill), style: primaryButtonStyle },
|
|
244
|
-
skill.local_status === 'installed' ? '
|
|
314
|
+
skill.local_status === 'installed' ? '文件已安装' : skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
|
|
245
315
|
skill.local_status !== 'not-installed' ? h('button', { type: 'button', disabled: Boolean(workingId),
|
|
246
316
|
onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载') : null))
|
|
247
317
|
})) : !loading && skills.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null))
|
|
248
318
|
}
|
|
249
319
|
|
|
320
|
+
function PluginCenterPage({ remote, onBack, onSkills, openBundle }) {
|
|
321
|
+
const [plugins, setPlugins] = useState([])
|
|
322
|
+
const [bundles, setBundles] = useState([])
|
|
323
|
+
const [loading, setLoading] = useState(false)
|
|
324
|
+
const [workingId, setWorkingId] = useState('')
|
|
325
|
+
const [uncertainId, setUncertainId] = useState('')
|
|
326
|
+
const [completedId, setCompletedId] = useState('')
|
|
327
|
+
const [message, setMessage] = useState('')
|
|
328
|
+
const [query, setQuery] = useState('')
|
|
329
|
+
const [filter, setFilter] = useState('all')
|
|
330
|
+
const refresh = async () => {
|
|
331
|
+
setLoading(true)
|
|
332
|
+
try {
|
|
333
|
+
const manager = remote?.pluginManager
|
|
334
|
+
if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 未提供插件管理接口。')
|
|
335
|
+
const [response, listed] = await Promise.all([
|
|
336
|
+
fetch('/api/ant-teams/plugins', { cache: 'no-store' }), manager.listBundles(),
|
|
337
|
+
])
|
|
338
|
+
const result = await readLocalJsonResponse(response)
|
|
339
|
+
if (!response.ok) throw new Error(result.message || `读取插件目录失败(${response.status})。`)
|
|
340
|
+
if (!Array.isArray(result.plugins)) throw new Error('企业插件目录格式无效。')
|
|
341
|
+
if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 本机插件。')
|
|
342
|
+
setPlugins(result.plugins)
|
|
343
|
+
setBundles(listed.value)
|
|
344
|
+
setMessage(result.plugins.length ? '' : '团队暂未发布精选插件。')
|
|
345
|
+
} catch (error) {
|
|
346
|
+
setPlugins([])
|
|
347
|
+
setBundles([])
|
|
348
|
+
setMessage(error instanceof Error ? error.message : '无法读取企业精选插件。')
|
|
349
|
+
} finally { setLoading(false) }
|
|
350
|
+
}
|
|
351
|
+
useEffect(() => { void refresh() }, [])
|
|
352
|
+
const install = async (plugin) => {
|
|
353
|
+
const artifactUrl = approvedPluginArtifactUrl(plugin)
|
|
354
|
+
if (!artifactUrl || workingId || uncertainId === plugin.id || completedId === plugin.id) return
|
|
355
|
+
const manager = remote?.pluginManager
|
|
356
|
+
if (typeof manager?.installBundle !== 'function') {
|
|
357
|
+
setMessage('当前 DSH 未提供插件安装接口,请联系管理员。')
|
|
358
|
+
return
|
|
359
|
+
}
|
|
360
|
+
const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
|
|
361
|
+
const action = local ? local.version === plugin.version ? '重新安装' : '更新' : '安装'
|
|
362
|
+
if (!window.confirm(`在这台 DSH ${action}团队审核的 ${plugin.title} ${plugin.version}?插件将在本机运行,完成后需要完全重启 DSH。`)) return
|
|
363
|
+
setWorkingId(plugin.id)
|
|
364
|
+
setMessage(`正在安装 ${plugin.title},请不要退出 DSH…`)
|
|
365
|
+
try {
|
|
366
|
+
const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
|
|
367
|
+
const catalog = await readLocalJsonResponse(response)
|
|
368
|
+
const latest = Array.isArray(catalog?.plugins)
|
|
369
|
+
? catalog.plugins.find((item) => item.id === plugin.id) : null
|
|
370
|
+
if (!response.ok || latest?.package_name !== plugin.package_name
|
|
371
|
+
|| latest?.version !== plugin.version || latest?.artifact_sha256 !== plugin.artifact_sha256
|
|
372
|
+
|| approvedPluginArtifactUrl(latest) !== artifactUrl) {
|
|
373
|
+
throw new Error('插件审核信息已变化,请刷新目录后重试。')
|
|
374
|
+
}
|
|
375
|
+
const requestId = crypto.randomUUID()
|
|
376
|
+
let result
|
|
377
|
+
try { result = await manager.installBundle(artifactUrl, { enabled: true, requestId }) }
|
|
378
|
+
catch { result = null }
|
|
379
|
+
if (!result?.ok) {
|
|
380
|
+
let recovered = null
|
|
381
|
+
if (typeof manager.waitForInstall === 'function') {
|
|
382
|
+
try {
|
|
383
|
+
const pending = await manager.waitForInstall(requestId)
|
|
384
|
+
if (pending.ok && pending.value) recovered = { ok: true, value: pending.value }
|
|
385
|
+
} catch {}
|
|
386
|
+
}
|
|
387
|
+
if (!recovered) {
|
|
388
|
+
setUncertainId(plugin.id)
|
|
389
|
+
setMessage(`暂时无法确认 ${plugin.title} 的安装结果。请先在 DSH 插件列表核对,勿重复点击安装。`)
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
result = recovered
|
|
393
|
+
}
|
|
394
|
+
const outcome = result.value
|
|
395
|
+
if (!outcome || typeof outcome !== 'object') {
|
|
396
|
+
setUncertainId(plugin.id)
|
|
397
|
+
setMessage(`DSH 未返回 ${plugin.title} 的安装结果。请先在插件列表核对,勿重复点击安装。`)
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
if (outcome.application === 'failed' || outcome.application === 'cancelled') {
|
|
401
|
+
if (outcome.error?.code === 'ambiguous-install' && local?.version === plugin.version) {
|
|
402
|
+
setMessage(`DSH 无法确定 ${plugin.title} 本次安装的唯一结果,未确认完成。请先在 DSH 插件列表核对;若已经正常使用同版则无需重复安装,仍需核对来源时请联系管理员。`)
|
|
403
|
+
return
|
|
404
|
+
}
|
|
405
|
+
const diagnostic = typeof outcome.error?.diagnostic === 'string'
|
|
406
|
+
? outcome.error.diagnostic.slice(0, 180) : ''
|
|
407
|
+
setMessage(`${plugin.title} 安装未完成。请在 DSH 插件管理查看状态或联系管理员。${diagnostic ? `原因:${diagnostic}` : ''}`)
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
if (outcome.bundle !== plugin.package_name
|
|
411
|
+
|| !['restart-required', 'applied'].includes(outcome.application)) {
|
|
412
|
+
setUncertainId(plugin.id)
|
|
413
|
+
setMessage(`DSH 未确认 ${plugin.title} 已安装。请先在插件列表核对,勿重复点击安装。`)
|
|
414
|
+
return
|
|
415
|
+
}
|
|
416
|
+
let observed = null
|
|
417
|
+
try {
|
|
418
|
+
const installed = await manager.listBundles()
|
|
419
|
+
if (installed.ok && Array.isArray(installed.value)) {
|
|
420
|
+
observed = installed.value.find((bundle) => bundle.name === plugin.package_name && bundle.installed) || null
|
|
421
|
+
}
|
|
422
|
+
} catch {}
|
|
423
|
+
if ((outcome.version && outcome.version !== plugin.version) || observed?.version !== plugin.version) {
|
|
424
|
+
setUncertainId(plugin.id)
|
|
425
|
+
setMessage(`DSH 未确认 ${plugin.title} 的审核版本已安装。请先在插件列表核对,勿重复点击安装。`)
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
await refresh()
|
|
429
|
+
try { localStorage.setItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`, plugin.artifact_sha256) } catch {}
|
|
430
|
+
setCompletedId(plugin.id)
|
|
431
|
+
setMessage(`${plugin.title} 已安装。请完全退出并重新启动 DSH 后使用。`)
|
|
432
|
+
} catch (error) {
|
|
433
|
+
setMessage(error instanceof Error ? error.message : '插件安装未完成,请稍后重试。')
|
|
434
|
+
} finally { setWorkingId('') }
|
|
435
|
+
}
|
|
436
|
+
const visible = plugins.filter((plugin) => {
|
|
437
|
+
const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
|
|
438
|
+
const artifactUrl = approvedPluginArtifactUrl(plugin)
|
|
439
|
+
const matches = !query.trim() || `${plugin.title} ${plugin.description} ${plugin.package_name}`.toLowerCase().includes(query.trim().toLowerCase())
|
|
440
|
+
return matches && (filter === 'all' || filter === 'installed' && installed?.version === plugin.version
|
|
441
|
+
|| filter === 'available' && artifactUrl && (!installed || (installed.version && installed.version !== plugin.version))
|
|
442
|
+
|| filter === 'retired' && plugin.status !== 'published')
|
|
443
|
+
})
|
|
444
|
+
return h('section', { 'aria-label': '企业精选插件', style: {
|
|
445
|
+
height: '100%', overflowY: 'auto', boxSizing: 'border-box',
|
|
446
|
+
padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
|
|
447
|
+
} }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
|
|
448
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
449
|
+
h('button', { type: 'button', onClick: onBack, style: secondaryButtonStyle }, '← 返回 ANT 插件设置'),
|
|
450
|
+
h('button', { type: 'button', onClick: onSkills, style: secondaryButtonStyle }, '查看企业精选技能 →')),
|
|
451
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 16 } },
|
|
452
|
+
h('div', null,
|
|
453
|
+
h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选插件'),
|
|
454
|
+
h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。')),
|
|
455
|
+
h('button', { type: 'button', disabled: loading, onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
|
|
456
|
+
h('aside', { role: 'note', style: { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } },
|
|
457
|
+
h('strong', null, '只安装团队锁定的固定版本'),
|
|
458
|
+
h('p', { style: mutedTextStyle }, '显示“安装”按钮的版本已锁定实际安装包并由管理员发布;其他目录项仅供查看,如需使用请联系企业管理员。')),
|
|
459
|
+
h(ExtensionRequestNotice),
|
|
460
|
+
h('div', { style: panelStyle },
|
|
461
|
+
h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
|
|
462
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
463
|
+
...[['all', '全部'], ['available', '可安装或升级'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
|
|
464
|
+
h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
|
|
465
|
+
style: { ...secondaryButtonStyle, minHeight: 34, backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)))),
|
|
466
|
+
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
|
|
467
|
+
visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
|
|
468
|
+
...visible.map((plugin) => {
|
|
469
|
+
const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
|
|
470
|
+
const sameVersion = local?.version === plugin.version
|
|
471
|
+
const security = plugin.status === 'security_withdrawn'
|
|
472
|
+
const artifactUrl = approvedPluginArtifactUrl(plugin)
|
|
473
|
+
const sourceKnown = typeof local?.source === 'string'
|
|
474
|
+
const verifiedLocal = sameVersion && artifactUrl && sourceKnown && local.source === artifactUrl
|
|
475
|
+
const recordedInstall = sameVersion && !sourceKnown && previouslyInstalledReviewedPlugin(plugin)
|
|
476
|
+
const status = security ? '已废弃(安全风险)'
|
|
477
|
+
: plugin.status === 'deprecated' ? '已废弃' : local?.error ? '加载异常' : sameVersion
|
|
478
|
+
? verifiedLocal ? local.enabled ? '本机已装(团队来源,已启用)' : '本机已装(团队来源,未启用)'
|
|
479
|
+
: sourceKnown ? '本机同版,来源不同' : recordedInstall
|
|
480
|
+
? '本机同版,曾由企业扩展安装;当前 DSH 无法复核来源' : '本机同版,当前 DSH 无法核对来源'
|
|
481
|
+
: artifactUrl ? local ? local.version ? `可更新 · 本机 ${local.version}` : '本机版本暂无法读取' : '可安装'
|
|
482
|
+
: local ? `本机版本 ${local.version || '暂无法读取'}` : '已审核,暂需管理员安装'
|
|
483
|
+
return h('article', { key: plugin.id, style: security
|
|
484
|
+
? { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } : panelStyle },
|
|
485
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 8 } },
|
|
486
|
+
h('h2', { style: { margin: 0, fontSize: 19 } }, plugin.title),
|
|
487
|
+
h('span', { style: security
|
|
488
|
+
? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)', fontWeight: 650 } : mutedTextStyle }, status)),
|
|
489
|
+
h('p', { style: mutedTextStyle }, plugin.description),
|
|
490
|
+
h('p', { style: mutedTextStyle }, `${plugin.package_name}@${plugin.version} · 许可证:${plugin.license || '未声明'}`),
|
|
491
|
+
h('p', { style: mutedTextStyle }, `来源:${plugin.source_url || '未提供'} · DSH 版本要求:${plugin.peer_range || '包未声明'}`),
|
|
492
|
+
h('p', { style: mutedTextStyle }, `额外配置:${plugin.requirements || '无'}`),
|
|
493
|
+
plugin.scripts ? h('p', { style: mutedTextStyle }, `包自身的安装脚本:${plugin.scripts}`) : null,
|
|
494
|
+
plugin.status !== 'published' ? h('p', { role: 'status', style: security
|
|
495
|
+
? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)' } : mutedTextStyle },
|
|
496
|
+
plugin.withdrawal_reason || '团队已停止推荐此版本。') : null,
|
|
497
|
+
local?.error ? h('p', { role: 'status', style: mutedTextStyle }, local.error.message || String(local.error)) : null,
|
|
498
|
+
sameVersion && !sourceKnown && artifactUrl ? h('p', { style: mutedTextStyle }, recordedInstall
|
|
499
|
+
? '此前曾由企业扩展安装;当前 DSH 无法再次核对来源。正常使用无需重复安装;若后来换过来源,可以重新安装。'
|
|
500
|
+
: '已装同版且使用正常时无需重复安装;若想改用团队审核包,可自愿安装。') : null,
|
|
501
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
502
|
+
artifactUrl && (!local || (local.version && !verifiedLocal))
|
|
503
|
+
? h('button', { type: 'button', disabled: Boolean(workingId) || uncertainId === plugin.id || completedId === plugin.id,
|
|
504
|
+
onClick: () => void install(plugin), style: primaryButtonStyle },
|
|
505
|
+
workingId === plugin.id ? '安装中…' : uncertainId === plugin.id ? '先核对安装结果'
|
|
506
|
+
: completedId === plugin.id ? '重启 DSH 后生效'
|
|
507
|
+
: local ? sameVersion ? '按团队审核包重新安装' : '更新到审核版本' : '安装到这台 DSH') : null,
|
|
508
|
+
plugin.status === 'published' && !local && !artifactUrl
|
|
509
|
+
? h('p', { style: mutedTextStyle }, '如需安装,请联系企业管理员安排。') : null,
|
|
510
|
+
local || uncertainId === plugin.id ? h('button', { type: 'button', onClick: () => openBundle(plugin.package_name), style: secondaryButtonStyle }, '打开 DSH 插件管理') : null,
|
|
511
|
+
local && !sameVersion && plugin.status === 'published' && !artifactUrl
|
|
512
|
+
? h('p', { style: mutedTextStyle }, '本机版本与团队审核版本不同;请联系企业管理员核对。') : null))
|
|
513
|
+
})) : !loading && plugins.length ? h('p', { style: mutedTextStyle }, filter === 'available'
|
|
514
|
+
? '当前没有可一键安装的插件。可在“全部”查看审核目录,或联系企业管理员。'
|
|
515
|
+
: '没有符合筛选条件的插件。') : null))
|
|
516
|
+
}
|
|
517
|
+
|
|
250
518
|
function ModelSyncFooter({ remote, serviceUrl, onServiceUrlChange }) {
|
|
251
519
|
const [email, setEmail] = useState('')
|
|
252
520
|
const [code, setCode] = useState('')
|
|
@@ -256,10 +524,21 @@ window.__ModuleLoader__.load({
|
|
|
256
524
|
const [credentialConfigured, setCredentialConfigured] = useState(null)
|
|
257
525
|
const [reauthenticate, setReauthenticate] = useState(false)
|
|
258
526
|
const [needsReconnect, setNeedsReconnect] = useState(false)
|
|
527
|
+
const [allowLocalCleanup, setAllowLocalCleanup] = useState(false)
|
|
259
528
|
const [bundleRelease, setBundleRelease] = useState(null)
|
|
260
529
|
const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
|
|
261
|
-
const [approvedBuilds, setApprovedBuilds] = useState([])
|
|
262
530
|
const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
|
|
531
|
+
const [pinnedServiceUrl, setPinnedServiceUrl] = useState(null)
|
|
532
|
+
|
|
533
|
+
const connectionErrorMessage = (result, status) => {
|
|
534
|
+
const message = result?.message || `请求失败(${status})`
|
|
535
|
+
if (!['LEGACY_DEVICE_REVOKE_REQUIRED', 'ANT_CONNECTION_CLEANUP_REQUIRED'].includes(result?.code)) return message
|
|
536
|
+
setAllowLocalCleanup(true)
|
|
537
|
+
const deviceId = savedDeviceId()
|
|
538
|
+
return deviceId
|
|
539
|
+
? `${message} 设备编号:${deviceId}。请把编号提供给原团队管理员。`
|
|
540
|
+
: `${message} 本机无法确认旧设备编号;请让原团队管理员按成员查看设备清单,勿用新编号猜测。`
|
|
541
|
+
}
|
|
263
542
|
|
|
264
543
|
const inspectServiceCompatibility = async (rawServiceUrl, useSavedConnection = false) => {
|
|
265
544
|
const normalized = normalizeServiceUrl(rawServiceUrl)
|
|
@@ -374,10 +653,12 @@ window.__ModuleLoader__.load({
|
|
|
374
653
|
|
|
375
654
|
const checkBundleUpdate = async () => {
|
|
376
655
|
setBundleUpdate({ state: 'checking', currentVersion: '', message: '正在检查 ANT 更新…' })
|
|
377
|
-
setApprovedBuilds([])
|
|
378
656
|
try {
|
|
379
657
|
const response = await fetch('/api/ant-teams/bundle-release', {
|
|
380
|
-
cache: 'no-store', headers: {
|
|
658
|
+
cache: 'no-store', headers: {
|
|
659
|
+
'x-ant-bundle-capabilities': 'direct-package-install',
|
|
660
|
+
'x-ant-bundle-update-mode': 'check-only',
|
|
661
|
+
},
|
|
381
662
|
})
|
|
382
663
|
const release = await readLocalJsonResponse(response)
|
|
383
664
|
if (!response.ok) throw new Error(release.message || `检查失败(${response.status})`)
|
|
@@ -415,109 +696,154 @@ window.__ModuleLoader__.load({
|
|
|
415
696
|
throw new Error(`当前 DSH 未找到已安装的 ${release.package_name} Bundle。`)
|
|
416
697
|
}
|
|
417
698
|
const available = newerVersion(release.version, installed.version)
|
|
699
|
+
if (release.artifact_status === 'revoked') {
|
|
700
|
+
setBundleUpdate({ state: 'revoked', currentVersion: installed.version,
|
|
701
|
+
message: installed.version === release.version
|
|
702
|
+
? `当前 ANT ${installed.version} 的安装包已因安全问题停用。请联系企业管理员获取处理方式;ANT 不会自动删除本机副本。`
|
|
703
|
+
: `发布版本 ${release.version} 的安装包已因安全问题停用,请等待管理员发布修复版本。` })
|
|
704
|
+
return
|
|
705
|
+
}
|
|
706
|
+
const artifactUrl = approvedArtifactUrl(release)
|
|
418
707
|
setBundleUpdate({
|
|
419
|
-
state: available ? 'available' : 'current',
|
|
708
|
+
state: available ? artifactUrl ? 'available' : 'artifact-missing' : 'current',
|
|
420
709
|
currentVersion: installed.version,
|
|
421
|
-
message: available
|
|
710
|
+
message: available
|
|
711
|
+
? artifactUrl
|
|
712
|
+
? `发现新版本 ${release.version},可直接更新。`
|
|
713
|
+
: `发现新版本 ${release.version},但管理员尚未提供已审核安装包。请联系管理员。`
|
|
714
|
+
: `当前已是最新发布版本(${installed.version})。`,
|
|
422
715
|
})
|
|
423
716
|
} catch (error) {
|
|
424
717
|
setBundleUpdate({ state: 'failed', currentVersion: '', message: error instanceof Error ? error.message : '检查 ANT 更新失败。' })
|
|
425
718
|
}
|
|
426
719
|
}
|
|
427
720
|
|
|
428
|
-
const installBundleUpdate = async (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|| (allowScripts && approvedBuilds.length === 0)) return
|
|
721
|
+
const installBundleUpdate = async () => {
|
|
722
|
+
if (bundleUpdate.state !== 'available') return
|
|
723
|
+
const artifactUrl = approvedArtifactUrl(bundleRelease)
|
|
724
|
+
if (!artifactUrl) return
|
|
433
725
|
const manager = remote?.pluginManager
|
|
434
726
|
if (typeof manager?.installBundle !== 'function') {
|
|
435
|
-
setBundleUpdate((current) => ({ ...current, state: 'failed', message: '当前 DSH
|
|
727
|
+
setBundleUpdate((current) => ({ ...current, state: 'failed', message: '当前 DSH 未提供 Bundle 更新接口。' }))
|
|
436
728
|
return
|
|
437
729
|
}
|
|
438
|
-
const scripts = allowScripts ? approvedBuilds : []
|
|
439
|
-
if (scripts.length && !window.confirm(`DSH 安装需要运行以下依赖的安装脚本:\n\n${scripts.join('\n')}\n\n仅在信任这些依赖时继续。`)) return
|
|
440
730
|
setBusy(true)
|
|
441
|
-
|
|
442
|
-
setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在委托 DSH 官方管理器安装更新…' }))
|
|
731
|
+
setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在由 DSH 安装已审核的 ANT 更新…' }))
|
|
443
732
|
try {
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
733
|
+
const releaseResponse = await fetch('/api/ant-teams/bundle-release', {
|
|
734
|
+
cache: 'no-store', headers: {
|
|
735
|
+
'x-ant-bundle-capabilities': 'direct-package-install',
|
|
736
|
+
'x-ant-bundle-update-mode': 'check-only',
|
|
737
|
+
},
|
|
447
738
|
})
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const reason = outcome.error?.diagnostic || outcome.packageResult?.output || 'DSH 未能安装该版本。'
|
|
454
|
-
setBundleUpdate({
|
|
455
|
-
state: 'failed', currentVersion: bundleUpdate.currentVersion,
|
|
456
|
-
message: pending.length
|
|
457
|
-
? `DSH 暂停了安装,因为以下依赖需要明确授权运行安装脚本:${pending.join('、')}`
|
|
458
|
-
: reason,
|
|
459
|
-
})
|
|
460
|
-
return
|
|
739
|
+
const latest = await readLocalJsonResponse(releaseResponse)
|
|
740
|
+
if (!releaseResponse.ok || latest.version !== bundleRelease.version
|
|
741
|
+
|| latest.artifact_sha256 !== bundleRelease.artifact_sha256
|
|
742
|
+
|| approvedArtifactUrl(latest) !== artifactUrl) {
|
|
743
|
+
throw new Error('ANT 更新信息已变化,请重新检查更新。')
|
|
461
744
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
745
|
+
const requestId = crypto.randomUUID()
|
|
746
|
+
let result
|
|
747
|
+
try {
|
|
748
|
+
result = await manager.installBundle(artifactUrl, { enabled: true, requestId })
|
|
749
|
+
} catch {
|
|
750
|
+
result = null
|
|
751
|
+
}
|
|
752
|
+
if (!result?.ok) {
|
|
753
|
+
let recovered = null
|
|
754
|
+
if (typeof manager.waitForInstall === 'function') {
|
|
755
|
+
try {
|
|
756
|
+
const pending = await manager.waitForInstall(requestId)
|
|
757
|
+
if (pending.ok && pending.value) recovered = { ok: true, value: pending.value }
|
|
758
|
+
} catch {}
|
|
759
|
+
}
|
|
760
|
+
if (!recovered) {
|
|
761
|
+
setBundleUpdate((current) => ({ ...current, state: 'unknown',
|
|
762
|
+
message: 'DSH 连接中断,更新结果暂时无法确认。请先在 DSH 插件列表查看 ANT 版本,勿重复点击更新。' }))
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
result = recovered
|
|
465
766
|
}
|
|
466
|
-
|
|
467
|
-
|
|
767
|
+
const outcome = result.value
|
|
768
|
+
if (outcome.application === 'failed' || outcome.application === 'cancelled') {
|
|
769
|
+
const reason = outcome.error?.diagnostic || outcome.packageResult?.output || '安装未完成。'
|
|
770
|
+
setBundleUpdate((current) => ({ ...current, state: 'failed', message: `更新失败:${reason}` }))
|
|
468
771
|
return
|
|
469
772
|
}
|
|
470
|
-
if (outcome.bundle !==
|
|
471
|
-
|
|
773
|
+
if (outcome.bundle !== ANT_BUNDLE_PACKAGE_NAME
|
|
774
|
+
|| !['restart-required', 'applied'].includes(outcome.application)) {
|
|
775
|
+
throw new Error('DSH 未确认 ANT 安装成功,请联系管理员检查插件状态。')
|
|
776
|
+
}
|
|
777
|
+
let observed = null
|
|
778
|
+
try {
|
|
779
|
+
const installed = await manager.listBundles()
|
|
780
|
+
if (installed.ok && Array.isArray(installed.value)) {
|
|
781
|
+
observed = installed.value.find((bundle) => bundle.name === ANT_BUNDLE_PACKAGE_NAME && bundle.installed) || null
|
|
782
|
+
}
|
|
783
|
+
} catch {}
|
|
784
|
+
if ((outcome.version && outcome.version !== bundleRelease.version) || observed?.version !== bundleRelease.version) {
|
|
785
|
+
setBundleUpdate((current) => ({ ...current, state: 'unknown',
|
|
786
|
+
message: 'DSH 未确认新版本已安装。请先在 DSH 插件列表核对 ANT 版本,勿重复点击更新。' }))
|
|
472
787
|
return
|
|
473
788
|
}
|
|
474
|
-
setBundleUpdate({
|
|
475
|
-
|
|
476
|
-
message: outcome.application === 'restart-required'
|
|
477
|
-
? `DSH 已安装 ${installSpec}。请完全退出并重新启动 DSH 后生效。`
|
|
478
|
-
: `DSH 已安装 ${installSpec}。`,
|
|
479
|
-
})
|
|
789
|
+
setBundleUpdate({ state: 'done', currentVersion: bundleRelease.version,
|
|
790
|
+
message: `ANT ${bundleRelease.version} 已安装。请完全退出并重新启动 DSH 后生效。` })
|
|
480
791
|
} catch (error) {
|
|
481
|
-
setBundleUpdate({ state: 'failed',
|
|
792
|
+
setBundleUpdate((current) => ({ ...current, state: 'failed', message: error instanceof Error ? error.message : '更新未完成。' }))
|
|
482
793
|
} finally {
|
|
483
794
|
setBusy(false)
|
|
484
795
|
}
|
|
485
796
|
}
|
|
486
797
|
|
|
798
|
+
const clearLocalConnection = async () => {
|
|
799
|
+
if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
|
|
800
|
+
const settingsResult = await remote.settings.mutate('llm-pi-ai', [
|
|
801
|
+
{ op: 'unset', path: ['providers', 'ant-team-openai'] },
|
|
802
|
+
{ op: 'unset', path: ['providers', 'ant-team-anthropic'] },
|
|
803
|
+
], undefined)
|
|
804
|
+
if (!settingsResult.ok) throw new Error(settingsResult.error?.message || 'ANT 模型设置未能清除。')
|
|
805
|
+
if (typeof remote?.credentials?.unset !== 'function') throw new Error('DSH 未提供凭证删除接口。')
|
|
806
|
+
const credentialResult = await remote.credentials.unset('ANT_MODEL_API_KEY')
|
|
807
|
+
if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
|
|
808
|
+
setCredentialConfigured(false)
|
|
809
|
+
setNeedsReconnect(false)
|
|
810
|
+
setReauthenticate(false)
|
|
811
|
+
setAllowLocalCleanup(false)
|
|
812
|
+
}
|
|
813
|
+
|
|
487
814
|
const disconnect = async () => {
|
|
488
815
|
setBusy(true)
|
|
489
816
|
setMessage('正在断开此设备的 ANT 连接…')
|
|
490
817
|
try {
|
|
491
|
-
|
|
492
|
-
const result = await
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (!result.ok) throw new Error(result.error?.message || 'ANT 模型设置未能清除。')
|
|
818
|
+
const response = await fetch('/api/ant-teams/disconnect', { method: 'POST' })
|
|
819
|
+
const result = await readLocalJsonResponse(response)
|
|
820
|
+
if (!response.ok || result.revoked !== true) {
|
|
821
|
+
throw new Error(connectionErrorMessage(result, response.status))
|
|
822
|
+
}
|
|
497
823
|
} catch (error) {
|
|
824
|
+
setAllowLocalCleanup(true)
|
|
498
825
|
setBusy(false)
|
|
499
|
-
setMessage(
|
|
826
|
+
setMessage(`未清除本机凭证和模型设置:${error instanceof Error ? error.message : '请稍后重试。'}`)
|
|
500
827
|
return
|
|
501
828
|
}
|
|
502
|
-
let serverRevoked = false
|
|
503
829
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
830
|
+
await clearLocalConnection()
|
|
831
|
+
setMessage('此设备的 ANT 凭证已撤销,ANT 模型设置已清除。团队服务地址已保留。')
|
|
832
|
+
} catch (error) {
|
|
833
|
+
setAllowLocalCleanup(true)
|
|
834
|
+
setMessage(`服务端已撤销设备令牌,但本机清理失败:${error instanceof Error ? error.message : '请稍后重试'}。可选择仅清理本机。`)
|
|
835
|
+
} finally { setBusy(false) }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const clearOnlyLocal = async () => {
|
|
839
|
+
if (!window.confirm('仅清理这台 DSH 的 ANT 凭证和模型设置,不保证服务端设备令牌已撤销。请先让原团队管理员确认远端撤销;否则令牌可能仍可被使用。确定继续吗?')) return
|
|
840
|
+
setBusy(true)
|
|
509
841
|
try {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (!result.ok) throw new Error(result.error?.message || 'ANT 成员凭证未能清除。')
|
|
513
|
-
credentialRemoved = true
|
|
514
|
-
setCredentialConfigured(false)
|
|
842
|
+
await clearLocalConnection()
|
|
843
|
+
setMessage('已仅清理本机 ANT 凭证和模型设置。此操作未确认服务端撤销,请向原团队管理员核对。')
|
|
515
844
|
} catch (error) {
|
|
516
|
-
setMessage(
|
|
517
|
-
}
|
|
518
|
-
setBusy(false)
|
|
519
|
-
if (credentialRemoved && serverRevoked) setMessage('此设备的 ANT 凭证已撤销,ANT 模型设置已清除。团队服务地址已保留。')
|
|
520
|
-
else if (credentialRemoved) setMessage('本机 ANT 凭证与模型设置已清除;服务端未确认撤销,活动会话会在 90 秒内到期。团队服务地址已保留。')
|
|
845
|
+
setMessage(`本机清理未完成:${error instanceof Error ? error.message : '请稍后重试。'}`)
|
|
846
|
+
} finally { setBusy(false) }
|
|
521
847
|
}
|
|
522
848
|
|
|
523
849
|
useEffect(() => {
|
|
@@ -539,6 +865,23 @@ window.__ModuleLoader__.load({
|
|
|
539
865
|
return () => { current = false }
|
|
540
866
|
}, [remote])
|
|
541
867
|
|
|
868
|
+
useEffect(() => {
|
|
869
|
+
let current = true
|
|
870
|
+
if (credentialConfigured !== true) {
|
|
871
|
+
setPinnedServiceUrl(null)
|
|
872
|
+
return () => { current = false }
|
|
873
|
+
}
|
|
874
|
+
const readPinnedOrigin = async () => {
|
|
875
|
+
try {
|
|
876
|
+
const response = await fetch('/api/ant-teams/connection-origin', { cache: 'no-store' })
|
|
877
|
+
const result = await readLocalJsonResponse(response)
|
|
878
|
+
if (current) setPinnedServiceUrl(response.ok ? normalizeServiceUrl(result.service_url) : null)
|
|
879
|
+
} catch { if (current) setPinnedServiceUrl(null) }
|
|
880
|
+
}
|
|
881
|
+
void readPinnedOrigin()
|
|
882
|
+
return () => { current = false }
|
|
883
|
+
}, [remote, credentialConfigured])
|
|
884
|
+
|
|
542
885
|
useEffect(() => {
|
|
543
886
|
void inspectServiceCompatibility(serviceUrl, credentialConfigured === true)
|
|
544
887
|
}, [remote, serviceUrl, credentialConfigured])
|
|
@@ -555,6 +898,13 @@ window.__ModuleLoader__.load({
|
|
|
555
898
|
setMessage('请填写工作邮箱。')
|
|
556
899
|
return
|
|
557
900
|
}
|
|
901
|
+
if (credentialConfigured !== false && !savedDeviceId()) {
|
|
902
|
+
if (credentialConfigured === true) setAllowLocalCleanup(true)
|
|
903
|
+
setMessage(credentialConfigured === true
|
|
904
|
+
? '旧 ANT 凭证仍在,但本机无法确认它绑定的设备编号。请让原团队管理员按成员核查并撤销旧设备,再仅清理本机。'
|
|
905
|
+
: '尚未确认本机凭证状态,也没有已保存的设备编号。请等待检查完成,或修复 DSH 凭证接口后重试。')
|
|
906
|
+
return
|
|
907
|
+
}
|
|
558
908
|
setBusy(true)
|
|
559
909
|
setMessage('正在发送邮箱验证码…')
|
|
560
910
|
try {
|
|
@@ -566,7 +916,7 @@ window.__ModuleLoader__.load({
|
|
|
566
916
|
body: JSON.stringify({ service_url: normalized, email: normalizedEmail }),
|
|
567
917
|
})
|
|
568
918
|
const result = await readLocalJsonResponse(response)
|
|
569
|
-
if (!response.ok) throw new Error(result
|
|
919
|
+
if (!response.ok) throw new Error(connectionErrorMessage(result, response.status))
|
|
570
920
|
setEmail(normalizedEmail)
|
|
571
921
|
setCodeRequested(true)
|
|
572
922
|
setMessage(result.message || '如果该邮箱符合团队规则,验证码将发送至邮箱。')
|
|
@@ -586,6 +936,13 @@ window.__ModuleLoader__.load({
|
|
|
586
936
|
setMessage('请填写有效的服务地址、工作邮箱和 8 位验证码。')
|
|
587
937
|
return
|
|
588
938
|
}
|
|
939
|
+
if (credentialConfigured !== false && !savedDeviceId()) {
|
|
940
|
+
if (credentialConfigured === true) setAllowLocalCleanup(true)
|
|
941
|
+
setMessage(credentialConfigured === true
|
|
942
|
+
? '旧 ANT 凭证仍在,但本机无法确认旧设备编号。请先联系原团队管理员撤销旧设备,再仅清理本机。'
|
|
943
|
+
: '尚未确认本机凭证状态,也没有已保存的设备编号。请等待检查完成,或修复 DSH 凭证接口后重试。')
|
|
944
|
+
return
|
|
945
|
+
}
|
|
589
946
|
setBusy(true)
|
|
590
947
|
setMessage('正在验证邮箱并同步团队模型…')
|
|
591
948
|
try {
|
|
@@ -602,7 +959,7 @@ window.__ModuleLoader__.load({
|
|
|
602
959
|
}),
|
|
603
960
|
})
|
|
604
961
|
const result = await readLocalJsonResponse(response)
|
|
605
|
-
if (!response.ok) throw new Error(result
|
|
962
|
+
if (!response.ok) throw new Error(connectionErrorMessage(result, response.status))
|
|
606
963
|
if (typeof result.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
|
|
607
964
|
throw new Error('ANT 返回的连接信息格式无效。')
|
|
608
965
|
}
|
|
@@ -616,6 +973,7 @@ window.__ModuleLoader__.load({
|
|
|
616
973
|
onServiceUrlChange(normalized)
|
|
617
974
|
setCodeRequested(false)
|
|
618
975
|
setCredentialConfigured(true)
|
|
976
|
+
setAllowLocalCleanup(false)
|
|
619
977
|
setNeedsReconnect(false)
|
|
620
978
|
setReauthenticate(false)
|
|
621
979
|
setMessage(`已连接 ${result.email || normalizedEmail},同步 ${result.models.length} 个获准模型。它们现已出现在 DSH 模型选择器中。`)
|
|
@@ -627,6 +985,9 @@ window.__ModuleLoader__.load({
|
|
|
627
985
|
}
|
|
628
986
|
|
|
629
987
|
const showConnectForm = credentialConfigured !== true || reauthenticate || needsReconnect
|
|
988
|
+
const canCheckUpdate = credentialConfigured === true && !needsReconnect
|
|
989
|
+
const updateGuideUrl = pinnedServiceUrl
|
|
990
|
+
? new URL('/ant-update', pinnedServiceUrl).href : null
|
|
630
991
|
return h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 400px), 1fr))', alignItems: 'start', gap: 16 } },
|
|
631
992
|
h('section', { style: panelStyle },
|
|
632
993
|
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
|
|
@@ -653,18 +1014,20 @@ window.__ModuleLoader__.load({
|
|
|
653
1014
|
h('button', { type: 'button', disabled: busy, onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
|
|
654
1015
|
h('button', { type: 'button', disabled: busy, onClick: () => { setReauthenticate(true); setMessage('') }, style: secondaryButtonStyle }, '重新验证邮箱'),
|
|
655
1016
|
h('button', { type: 'button', disabled: busy, onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() }, style: secondaryButtonStyle }, '断开当前设备')) : null,
|
|
656
|
-
|
|
1017
|
+
h('section', { style: { ...panelStyle, padding: 14, gap: 10 } },
|
|
657
1018
|
h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 } },
|
|
658
1019
|
h('h4', { style: { margin: 0, fontSize: 14, fontWeight: 650 } }, 'ANT 插件更新'),
|
|
659
|
-
h('button', { type: 'button', disabled: busy || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
h('p', { style: mutedTextStyle },
|
|
1020
|
+
h('button', { type: 'button', disabled: busy || !canCheckUpdate || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
|
|
1021
|
+
!canCheckUpdate ? h('p', { role: 'status', style: mutedTextStyle }, '连接团队后可在这里检查并安装 ANT 更新;需要先恢复插件时,可打开团队管理台的更新指引。') : null,
|
|
1022
|
+
updateGuideUrl ? h('a', { href: updateGuideUrl, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, `打开 ${pinnedServiceUrl} 的更新指引 ↗`)
|
|
1023
|
+
: !canCheckUpdate ? h('p', { style: mutedTextStyle }, '无法核对原团队地址时,请向企业管理员索取更新指引。') : null,
|
|
1024
|
+
bundleUpdate.message ? h('p', { role: 'status', style: bundleUpdate.state === 'revoked'
|
|
1025
|
+
? { ...mutedTextStyle, color: 'var(--dsw-alias-state-error-primary)' } : mutedTextStyle }, bundleUpdate.message) : null,
|
|
1026
|
+
canCheckUpdate && bundleRelease?.version && ['available', 'artifact-missing'].includes(bundleUpdate.state) ? h('div', { style: { display: 'grid', gap: 8 } },
|
|
1027
|
+
h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · 精确包名:${directInstallSpec(bundleRelease)}`),
|
|
663
1028
|
bundleRelease.release_notes ? h('p', { style: mutedTextStyle }, bundleRelease.release_notes) : null,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
h('p', { style: mutedTextStyle }, `DSH 暂停安装,等待授权运行以下依赖的安装脚本:${approvedBuilds.join('、')}。`),
|
|
667
|
-
h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(true), style: secondaryButtonStyle }, '信任这些依赖并继续安装')) : null) : null,
|
|
1029
|
+
bundleUpdate.state === 'available'
|
|
1030
|
+
? h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '更新中…' : '更新 ANT') : null) : null),
|
|
668
1031
|
showConnectForm ? h('form', { onSubmit: requestCode, style: { display: 'grid', gap: 9, maxWidth: 560 } },
|
|
669
1032
|
h('label', { htmlFor: 'ant-team-model-email', style: { fontSize: 13, fontWeight: 550 } }, '工作邮箱'),
|
|
670
1033
|
h('input', {
|
|
@@ -685,10 +1048,41 @@ window.__ModuleLoader__.load({
|
|
|
685
1048
|
onChange: (event) => setCode(event.target.value.replace(/\D/gu, '').slice(0, 8)), style: inputStyle,
|
|
686
1049
|
}),
|
|
687
1050
|
h('button', { type: 'submit', disabled: busy || code.length !== 8, style: primaryButtonStyle }, busy ? '连接中…' : '验证并连接')) : null,
|
|
1051
|
+
allowLocalCleanup ? h('section', { style: { ...panelStyle, padding: 14, gap: 9 } },
|
|
1052
|
+
h('p', { style: mutedTextStyle }, savedDeviceId()
|
|
1053
|
+
? `已保存的设备编号:${savedDeviceId()}。请交给原团队管理员核对。`
|
|
1054
|
+
: '本机无法确认旧设备编号。请让原团队管理员按成员查看设备清单,勿用新编号猜测。'),
|
|
1055
|
+
h('p', { style: mutedTextStyle }, '仅清理本机不会确认服务端令牌已撤销;请先取得管理员确认。'),
|
|
1056
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
|
|
1057
|
+
h('button', { type: 'button', disabled: busy, onClick: () => void clearOnlyLocal(), style: secondaryButtonStyle }, '仅清理本机'),
|
|
1058
|
+
h('button', { type: 'button', disabled: busy || !savedDeviceId(), onClick: async () => {
|
|
1059
|
+
try {
|
|
1060
|
+
const deviceId = savedDeviceId()
|
|
1061
|
+
if (!deviceId) throw new Error('device id unavailable')
|
|
1062
|
+
if (typeof window.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable')
|
|
1063
|
+
await window.navigator.clipboard.writeText(deviceId)
|
|
1064
|
+
setMessage('设备编号已复制,请发给原团队管理员。')
|
|
1065
|
+
}
|
|
1066
|
+
catch { setMessage(savedDeviceId() ? `请复制设备编号:${savedDeviceId()}` : '本机无法确认旧设备编号,请联系原团队管理员核查。') }
|
|
1067
|
+
}, style: secondaryButtonStyle }, '复制设备编号'))) : null,
|
|
688
1068
|
message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null))
|
|
689
1069
|
|
|
690
1070
|
}
|
|
691
1071
|
|
|
1072
|
+
function EnterpriseExtensionsPanelIcon({ size }) {
|
|
1073
|
+
return h('span', { 'aria-hidden': true, style: {
|
|
1074
|
+
display: 'inline-grid', placeItems: 'center', width: size, height: size,
|
|
1075
|
+
fontSize: size, lineHeight: 1,
|
|
1076
|
+
} }, '✦')
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function EnterpriseExtensionsPage({ remote, onBack, openBundle }) {
|
|
1080
|
+
const [tab, setTab] = useState('skills')
|
|
1081
|
+
return tab === 'skills'
|
|
1082
|
+
? h(SkillCenterPage, { onBack, onPlugins: () => setTab('plugins') })
|
|
1083
|
+
: h(PluginCenterPage, { remote, onBack, onSkills: () => setTab('skills'), openBundle })
|
|
1084
|
+
}
|
|
1085
|
+
|
|
692
1086
|
function apply(ctx) {
|
|
693
1087
|
const remote = {
|
|
694
1088
|
credentials: ctx.remote.credentials,
|
|
@@ -696,14 +1090,24 @@ window.__ModuleLoader__.load({
|
|
|
696
1090
|
pluginManager: ctx.remote.pluginManager,
|
|
697
1091
|
}
|
|
698
1092
|
ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'ant-skills' },
|
|
699
|
-
() => h(
|
|
1093
|
+
() => h(EnterpriseExtensionsPage, {
|
|
1094
|
+
remote,
|
|
1095
|
+
onBack: () => ctx.pluginNavigation.openBundle(ANT_BUNDLE_PACKAGE_NAME),
|
|
1096
|
+
openBundle: (name) => ctx.pluginNavigation.openBundle(name),
|
|
1097
|
+
})))
|
|
1098
|
+
ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
|
|
1099
|
+
name: 'sidebar.panellist', id: 'ant-skills', order: 5, label: '企业扩展',
|
|
1100
|
+
}, EnterpriseExtensionsPanelIcon))
|
|
700
1101
|
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
701
1102
|
name: 'plugins.bundle.config',
|
|
702
1103
|
key: ANT_BUNDLE_PACKAGE_NAME,
|
|
703
1104
|
inject: () => ({ remote }),
|
|
704
1105
|
}, ({ view, remote: injectedRemote }) => view === 'summary'
|
|
705
1106
|
? h('p', null, 'ANT 团队服务入口。')
|
|
706
|
-
: h(TeamServiceConfig, {
|
|
1107
|
+
: h(TeamServiceConfig, {
|
|
1108
|
+
remote: injectedRemote,
|
|
1109
|
+
openInstalledPlugin: (name) => ctx.pluginNavigation.openBundle(name),
|
|
1110
|
+
})))
|
|
707
1111
|
}
|
|
708
1112
|
|
|
709
1113
|
Object.assign(exports, { inject, apply })
|