@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/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 = localStorage.getItem(DEVICE_ID_STORAGE_KEY)
97
- if (existing && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(existing)) return 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, openSkillCenter }) {
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', { style: { ...panelStyle, display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between' } },
146
- h('div', null,
147
- h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '企业精选技能'),
148
- h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '浏览团队审核发布的技能,在独立页面查看说明并安装到这台 DSH。')),
149
- h('button', { type: 'button', onClick: openSkillCenter, style: primaryButtonStyle }, '打开技能中心 →')),
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} 已${action}。DSH 将从本机技能目录发现它。`)
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: '已安装', 'update-available': '可更新', 'not-installed': '未安装', 'not-discovered': 'DSH 未发现', conflict: '本机冲突' }
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('button', { type: 'button', onClick: onBack, style: { ...secondaryButtonStyle, justifySelf: 'start' } }, '← 返回 ANT 插件设置'),
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
- : skill.local_status === 'not-discovered'
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' ? '已安装' : skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
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: { 'x-ant-bundle-capabilities': 'direct-package-install' },
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 ? `发现新版本 ${release.version}。` : `当前已是最新发布版本(${installed.version})。`,
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 (allowScripts = false) => {
429
- const installSpec = directInstallSpec(bundleRelease)
430
- if (!installSpec
431
- || (!allowScripts && bundleUpdate.state !== 'available')
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 版本未提供官方 Bundle 安装接口。' }))
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
- setApprovedBuilds([])
442
- setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在委托 DSH 官方管理器安装更新…' }))
731
+ setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在由 DSH 安装已审核的 ANT 更新…' }))
443
732
  try {
444
- const result = await manager.installBundle(installSpec, {
445
- enabled: true,
446
- ...(scripts.length ? { approvedBuilds: scripts } : {}),
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
- if (!result.ok) throw new Error(result.error?.message || 'DSH 安装请求未返回结果。')
449
- const outcome = result.value
450
- if (outcome.application === 'failed') {
451
- const pending = Array.isArray(outcome.pendingBuilds) ? outcome.pendingBuilds : []
452
- setApprovedBuilds(pending)
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
- if (outcome.application === 'cancelled') {
463
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已取消更新,当前运行版本未切换。' })
464
- return
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
- if (outcome.application === 'overridden') {
467
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已记录更新,但其他插件配置覆盖了 ANT。请检查 DSH 插件配置后再重启。' })
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 !== bundleRelease.package_name) {
471
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 返回的安装包名称与 ANT 发布信息不一致。请联系管理员核对发布包。' })
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
- state: 'done', currentVersion: bundleRelease.version,
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', currentVersion: bundleUpdate.currentVersion, message: error instanceof Error ? error.message : 'DSH 未能安装 ANT 更新。' })
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
- if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
492
- const result = await remote.settings.mutate('llm-pi-ai', [
493
- { op: 'unset', path: ['providers', 'ant-team-openai'] },
494
- { op: 'unset', path: ['providers', 'ant-team-anthropic'] },
495
- ], undefined)
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(`无法清除本机 ANT 模型设置,因此连接仍保留。${error instanceof Error ? error.message : '请稍后重试。'}`)
826
+ setMessage(`未清除本机凭证和模型设置:${error instanceof Error ? error.message : '请稍后重试。'}`)
500
827
  return
501
828
  }
502
- let serverRevoked = false
503
829
  try {
504
- const response = await fetch('/api/ant-teams/disconnect', { method: 'POST' })
505
- const result = await readLocalJsonResponse(response)
506
- serverRevoked = response.ok && result.revoked === true
507
- } catch {}
508
- let credentialRemoved = false
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
- if (typeof remote?.credentials?.unset !== 'function') throw new Error('DSH 未提供凭证删除接口。')
511
- const result = await remote.credentials.unset('ANT_MODEL_API_KEY')
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(`本机 ANT 模型设置已清除,但成员凭证未能移除:${error instanceof Error ? error.message : '请稍后重试。'}`)
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.message || `请求失败(${response.status})`)
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.message || `请求失败(${response.status})`)
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
- credentialConfigured === true && !showConnectForm ? h('section', { style: { ...panelStyle, padding: 14, gap: 10 } },
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
- bundleUpdate.message ? h('p', { role: 'status', style: mutedTextStyle }, bundleUpdate.message) : null,
661
- bundleRelease?.version && bundleUpdate.state === 'available' ? h('div', { style: { display: 'grid', gap: 8 } },
662
- h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · npm:${directInstallSpec(bundleRelease)}`),
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
- h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '安装中…' : '由 DSH 安装并更新')) : null,
665
- approvedBuilds.length ? h('div', { style: { display: 'grid', gap: 8 } },
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(SkillCenterPage, { onBack: () => ctx.pluginNavigation.openBundle(ANT_BUNDLE_PACKAGE_NAME) })))
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, { remote: injectedRemote, openSkillCenter: () => ctx.layout.selectPanel('ant-skills') })))
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 })