@ai-wendao/ant 0.3.13 → 0.3.15

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.
Files changed (4) hide show
  1. package/README.md +5 -3
  2. package/client.js +428 -81
  3. package/index.js +38 -3
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -6,15 +6,17 @@ ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to its team's
6
6
 
7
7
  ## 企业精选技能
8
8
 
9
- 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员在 DSH 的 ANT 插件设置页点击“打开扩展中心”,进入“技能”页面浏览并安装。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
9
+ 团队管理员可在 ANT 管理台从 GitHub 目录、ZIP、TAR、TAR.GZ/TGZ 或单份 `SKILL.md` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员从 DSH 左侧“企业扩展”入口进入独立页面浏览并安装技能;ANT 插件设置页只处理连接、模型和更新。ANT Host 将固定版本的完整文件集放进 DSH 的用户技能目录,并重新核对本机文件;启用本地技能的 DSH 会话随后由原生技能提供者发现和使用。ANT 不用 Host 全局技能列表判断会话是否已发现技能。
10
10
 
11
11
  安装不会自动运行技能脚本或安装 Python、Node 等系统依赖。ANT 只更新或卸载自己管理且本机内容未被修改的技能目录;发现同名个人技能或本机改动时会停止覆盖。
12
12
 
13
13
  ## 企业精选插件
14
14
 
15
- 管理员可从 npm 官方源的 DSH Bundle 精确版本创建草稿,填写源码、许可证、依赖和安装脚本的审核记录后发布。成员在“扩展中心 → 插件”查看审核信息和本机状态。当前 DSH RC2 无法让 ANT 在安装脚本运行前确认实际下载制品与审核制品一致,因此插件一键安装暂停;需要使用时请联系企业管理员安排。安全撤回版本会在扩展中心标明;本机安装受影响版本或无法读取本机版本时,ANT 设置页也显示相应风险公告。
15
+ 管理员可从 npm 官方源的 DSH Bundle 精确版本创建草稿,填写源码、许可证、依赖和安装脚本的审核记录后发布。成员在“企业扩展 → 插件”查看审核信息和本机状态。对无新增依赖、无安装阶段脚本且归档符合首批准入条件的草稿,管理员可从 npm 官方源锁定实际 `.tgz` 并查看文件清单、下载核验制品;这类草稿经审核发布后,成员可让 DSH 从 ANT 的固定制品地址一键安装。其他版本仍是目录项,需联系管理员安排。安全撤回版本会停止新的 ANT 下载,但不会自动删除本机副本;ANT 设置页继续显示相应风险公告。范围与限制见[上线边界](../../docs/decisions/2026-10-15-plugin-center-rollout.md)。
16
16
 
17
- 企业精选是团队推荐目录,不是强制白名单;ANT 不能阻止成员从 DSH 原生插件管理页自行安装其他包。当前 DSH RC2 接口无法让 ANT 强制校验成员安装的包字节与审核时的摘要一致,也不能保证 profile 的依赖范围始终为精确版本;上线边界见 [待决策记录](../../docs/decisions/2026-10-15-plugin-center-rollout.md)。
17
+ ANT 自身更新使用单独的已审核安装包。管理员将与 npm 发布一致的 `.tgz` 上传到 ANT 服务端并登记版本;新版 ANT 页面可点击“更新”,DSH 原位替换同名 Bundle 后提示重启。旧版 `@ai-wendao/ant` 用户可打开团队服务的 `/ant-update` 指引,在 DSH“添加插件”中粘贴该安装包地址完成一次性原位更新,无需先卸载或使用终端。此入口只分发 ANT 自身,不启用扩展中心其他插件的一键安装。
18
+
19
+ 企业精选是团队推荐目录,不是强制白名单;ANT 不能阻止成员从 DSH 原生插件管理页自行安装其他包。第一批一键安装只分发 ANT 已核验并保存的顶层包,不把有依赖或安装脚本的版本伪装成已受控;DSH RC2 后续包管理能否一直保留这一固定制品仍需真机验收。上线边界见[待决策记录](../../docs/decisions/2026-10-15-plugin-center-rollout.md)。
18
20
 
19
21
  ## Existing `ant` alias installations
20
22
 
package/client.js CHANGED
@@ -3,12 +3,13 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  var module = { exports: {} }
5
5
  var exports = module.exports
6
- const { createElement, useEffect, useState } = require('react')
6
+ const { createElement, useEffect, useRef, useState } = require('react')
7
7
  const h = createElement
8
8
  const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
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,14 +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
+ const categoryButtonStyle = { ...secondaryButtonStyle, minHeight: 34, padding: '6px 12px', whiteSpace: 'nowrap' }
36
38
  function ExtensionRequestNotice() {
37
39
  return h('aside', { role: 'note', style: {
38
- ...panelStyle,
39
- borderColor: 'var(--dsw-alias-state-business-primary)',
40
- gap: 5,
40
+ display: 'flex', alignItems: 'baseline', gap: 8, padding: '9px 12px',
41
+ borderRadius: 8, backgroundColor: 'var(--dsw-alias-bg-layer-2)',
42
+ color: 'var(--dsw-alias-label-secondary)', fontSize: 13, lineHeight: 1.6,
41
43
  } },
42
- h('h2', { style: { margin: 0, fontSize: 16 } }, '没有找到想用的技能或插件?'),
43
- h('p', { style: mutedTextStyle }, '请联系企业管理员,说明你希望完成的任务或提供扩展地址。管理员审核后会将合适的版本加入扩展中心。技能可在这里安装;插件请由管理员安排,勿自行安装未经审核的扩展。'))
44
+ h('strong', { style: { flexShrink: 0, color: 'var(--dsw-alias-label-primary)' } }, '提示'),
45
+ h('span', null, '没有找到想用的扩展,或插件没有安装按钮?联系企业管理员,说明用途或提供地址。已开放的扩展可直接安装;未经审核的版本请勿自行安装。'))
44
46
  }
45
47
  const normalizeServiceUrl = (value) => {
46
48
  let url
@@ -97,6 +99,35 @@ window.__ModuleLoader__.load({
97
99
  : ''
98
100
  }
99
101
 
102
+ function approvedArtifactUrl(release) {
103
+ const expectedPath = `/api/ant-bundles/${release?.version}/package.tgz`
104
+ let url
105
+ try { url = new URL(release?.artifact_url) } catch { return null }
106
+ return normalizeServiceUrl(url.origin) === url.origin
107
+ && !url.username && !url.password && !url.search && !url.hash
108
+ && url.pathname === expectedPath && release?.artifact_path === expectedPath
109
+ && /^[a-f0-9]{64}$/u.test(release?.artifact_sha256 || '')
110
+ ? url.href : null
111
+ }
112
+
113
+ function approvedPluginArtifactUrl(plugin) {
114
+ if (plugin?.status !== 'published' || typeof plugin.id !== 'string'
115
+ || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(plugin.id)) return null
116
+ const expectedPath = `/api/plugin-artifacts/${plugin.id}/package.tgz`
117
+ let url
118
+ try { url = new URL(plugin.artifact_url) } catch { return null }
119
+ return normalizeServiceUrl(url.origin) === url.origin
120
+ && !url.username && !url.password && !url.search && !url.hash
121
+ && url.pathname === expectedPath && plugin.artifact_path === expectedPath
122
+ && /^[a-f0-9]{64}$/u.test(plugin.artifact_sha256 || '')
123
+ ? url.href : null
124
+ }
125
+
126
+ function previouslyInstalledReviewedPlugin(plugin) {
127
+ try { return localStorage.getItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`) === plugin.artifact_sha256 }
128
+ catch { return false }
129
+ }
130
+
100
131
  function loadServiceUrl() {
101
132
  try { return normalizeServiceUrl(localStorage.getItem(SERVICE_URL_STORAGE_KEY) || '') || '' } catch { return '' }
102
133
  }
@@ -116,11 +147,41 @@ window.__ModuleLoader__.load({
116
147
  return deviceId
117
148
  }
118
149
 
119
- function TeamServiceConfig({ remote, openSkillCenter, openPluginCenter }) {
150
+ function TeamServiceConfig({ remote, openInstalledPlugin }) {
120
151
  const [serviceUrl, setServiceUrl] = useState(loadServiceUrl)
121
152
  const [draft, setDraft] = useState(serviceUrl)
153
+ const [binding, setBinding] = useState({ state: 'checking', url: null })
154
+ const bindingRevision = useRef(0)
122
155
  const [message, setMessage] = useState('')
123
156
  const [pluginAlerts, setPluginAlerts] = useState([])
157
+ useEffect(() => {
158
+ let disposed = false
159
+ const revision = bindingRevision.current
160
+ const current = () => !disposed && bindingRevision.current === revision
161
+ const inspectBinding = async () => {
162
+ try {
163
+ const response = await fetch('/api/ant-teams/connection-origin', { cache: 'no-store' })
164
+ const result = await readLocalJsonResponse(response)
165
+ if (!response.ok) throw new Error()
166
+ const pinned = result.service_url == null ? null : normalizeServiceUrl(result.service_url)
167
+ if (result.service_url != null && !pinned) throw new Error()
168
+ if (pinned) {
169
+ if (current()) {
170
+ setBinding({ state: 'ready', url: pinned })
171
+ setServiceUrl(pinned)
172
+ setDraft(pinned)
173
+ try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, pinned) } catch {}
174
+ }
175
+ return
176
+ }
177
+ const described = await remote?.credentials?.describe?.(['ANT_MODEL_API_KEY'])
178
+ if (!described?.ok) throw new Error()
179
+ if (current()) setBinding({ state: described.value?.ANT_MODEL_API_KEY?.configured ? 'unverified' : 'ready', url: null })
180
+ } catch { if (current()) setBinding({ state: 'error', url: null }) }
181
+ }
182
+ void inspectBinding()
183
+ return () => { disposed = true }
184
+ }, [remote])
124
185
  useEffect(() => {
125
186
  let disposed = false
126
187
  const inspect = async () => {
@@ -145,6 +206,10 @@ window.__ModuleLoader__.load({
145
206
  }, [remote])
146
207
  const save = (event) => {
147
208
  event.preventDefault()
209
+ if (binding.state !== 'ready' || binding.url) {
210
+ setMessage(binding.url ? '当前已连接团队;切换前请先在下方断开当前设备。' : '尚未核对这台 DSH 的团队绑定,不能修改地址。')
211
+ return
212
+ }
148
213
  const normalized = normalizeServiceUrl(draft)
149
214
  if (!normalized) {
150
215
  setMessage('请输入 HTTPS 团队服务地址;本机测试可使用 localhost。')
@@ -159,39 +224,50 @@ window.__ModuleLoader__.load({
159
224
  setMessage('浏览器无法保存此地址,请检查本地存储权限。')
160
225
  }
161
226
  }
162
- const link = consoleUrl(serviceUrl)
227
+ const link = binding.state === 'ready' ? consoleUrl(binding.url || serviceUrl) : null
228
+ const onConnectionChange = (url) => {
229
+ bindingRevision.current += 1
230
+ setBinding({ state: 'ready', url: url || null })
231
+ if (url) {
232
+ setServiceUrl(url)
233
+ setDraft(url)
234
+ try { localStorage.setItem(SERVICE_URL_STORAGE_KEY, url) } catch {}
235
+ }
236
+ }
163
237
  return h('section', { style: { display: 'grid', gap: 16, width: '100%', maxWidth: 1120 } },
164
238
  h('p', { style: mutedTextStyle }, '连接团队服务后,即可同步获准模型并使用团队协作。'),
165
239
  h('section', { style: panelStyle },
166
240
  h('div', null,
167
241
  h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '团队服务'),
168
242
  h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '由团队管理员提供服务地址;管理员可在控制台管理成员、模型和协作设置。')),
169
- h('form', { onSubmit: save, style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', alignItems: 'end', gap: 10 } },
170
- h('label', { htmlFor: 'ant-team-service-url', style: { gridColumn: '1 / -1', fontSize: 13, fontWeight: 550 } }, '服务地址'),
243
+ h('form', { onSubmit: save, style: { display: 'grid', gridTemplateColumns: binding.url ? '1fr' : 'minmax(0, 1fr) auto', alignItems: 'end', gap: 10 } },
244
+ h('label', { htmlFor: 'ant-team-service-url', style: { gridColumn: '1 / -1', fontSize: 13, fontWeight: 550 } }, binding.url ? '当前连接的团队服务地址' : '服务地址'),
171
245
  h('input', {
172
246
  id: 'ant-team-service-url',
173
247
  type: 'url',
174
248
  required: true,
175
- value: draft,
249
+ value: binding.url || draft,
250
+ readOnly: binding.state !== 'ready' || Boolean(binding.url),
176
251
  placeholder: 'https://ant.example.com',
177
- onChange: (event) => setDraft(event.target.value),
252
+ onChange: (event) => { if (binding.state === 'ready' && !binding.url) setDraft(event.target.value) },
178
253
  style: inputStyle,
179
254
  }),
180
- h('button', { type: 'submit', style: primaryButtonStyle }, '保存地址')),
255
+ binding.url ? null : h('button', { type: 'submit', disabled: binding.state !== 'ready', style: primaryButtonStyle }, '保存地址')),
256
+ binding.url ? h('p', { style: mutedTextStyle }, '若要切换团队,请先在下方断开当前设备,再填写新地址并验证邮箱。') : null,
257
+ binding.state === 'checking' ? h('p', { role: 'status', style: mutedTextStyle }, '正在核对这台 DSH 绑定的团队…') : null,
258
+ binding.state === 'unverified' ? h('p', { role: 'status', style: mutedTextStyle }, '本机仍有 ANT 凭证,但无法核对所属团队。请联系原团队管理员核查并撤销旧设备;确认后可在下方断开或仅清理本机。') : null,
259
+ binding.state === 'error' ? h('p', { role: 'status', style: mutedTextStyle }, '暂时无法核对这台 DSH 的团队绑定,请重新打开 ANT 或联系管理员。') : null,
181
260
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 } },
182
261
  message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
183
262
  link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null)),
184
- h('section', { style: { ...panelStyle, display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between' } },
185
- h('div', null,
186
- h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '企业精选扩展'),
187
- h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '浏览团队审核发布的技能与插件;技能可在这里安装,插件安全安装暂未开放。没有找到需要的扩展时,请联系企业管理员上架。')),
188
- h('button', { type: 'button', onClick: openSkillCenter, style: primaryButtonStyle }, '打开扩展中心 →')),
189
263
  pluginAlerts.length ? h('section', { role: 'alert', style: { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } },
190
264
  h('h3', { style: { margin: 0, fontSize: 16 } }, '已安装插件存在安全撤回公告'),
191
265
  ...pluginAlerts.map((plugin) => h('p', { key: plugin.id, style: mutedTextStyle },
192
266
  `${plugin.title} ${plugin.version}${plugin.versionUnknown ? '(本机版本暂无法读取,请核对是否受影响)' : ''}:${plugin.withdrawal_reason}`)),
193
- h('button', { type: 'button', onClick: openPluginCenter, style: secondaryButtonStyle }, '查看插件并打开 DSH 管理 →')) : null,
194
- h(ModelSyncFooter, { remote, serviceUrl, onServiceUrlChange: setServiceUrl }))
267
+ ...pluginAlerts.map((plugin) => h('button', { key: `${plugin.id}-open`, type: 'button',
268
+ onClick: () => openInstalledPlugin(plugin.package_name), style: secondaryButtonStyle }, `在 DSH 中查看 ${plugin.title}`))) : null,
269
+ h(ModelSyncFooter, { remote, serviceUrl, bindingReady: binding.state === 'ready',
270
+ onServiceUrlChange: setServiceUrl, onConnectionChange }))
195
271
  }
196
272
 
197
273
  function SkillCenterPage({ onBack, onPlugins }) {
@@ -250,9 +326,11 @@ window.__ModuleLoader__.load({
250
326
  height: '100%', overflowY: 'auto', boxSizing: 'border-box',
251
327
  padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
252
328
  } }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
253
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
254
- h('button', { type: 'button', onClick: onBack, style: secondaryButtonStyle }, '← 返回 ANT 插件设置'),
255
- h('button', { type: 'button', onClick: onPlugins, style: secondaryButtonStyle }, '查看企业精选插件 →')),
329
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
330
+ h('nav', { 'aria-label': '企业扩展分类', style: { display: 'flex', gap: 6 } },
331
+ h('span', { 'aria-current': 'page', style: { ...categoryButtonStyle, display: 'inline-flex', alignItems: 'center', cursor: 'default', backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '技能'),
332
+ h('button', { type: 'button', onClick: onPlugins, style: categoryButtonStyle }, '插件')),
333
+ h('button', { type: 'button', onClick: onBack, style: { ...categoryButtonStyle, marginLeft: 'auto' } }, 'ANT 设置')),
256
334
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'end', justifyContent: 'space-between', gap: 16 } },
257
335
  h('div', null,
258
336
  h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选技能'),
@@ -283,9 +361,9 @@ window.__ModuleLoader__.load({
283
361
  skill.local_status === 'conflict'
284
362
  ? h('p', { role: 'status', style: mutedTextStyle }, '本机同名技能有未受 ANT 管理的内容、文件已被修改或备份损坏;请联系管理员处理。')
285
363
  : h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 'auto' } },
286
- h('button', { type: 'button', disabled: Boolean(workingId) || skill.local_status === 'installed',
364
+ skill.local_status === 'installed' ? null : h('button', { type: 'button', disabled: Boolean(workingId),
287
365
  onClick: () => void changeInstall(skill), style: primaryButtonStyle },
288
- skill.local_status === 'installed' ? '文件已安装' : skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
366
+ skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
289
367
  skill.local_status !== 'not-installed' ? h('button', { type: 'button', disabled: Boolean(workingId),
290
368
  onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载') : null))
291
369
  })) : !loading && skills.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null))
@@ -294,7 +372,11 @@ window.__ModuleLoader__.load({
294
372
  function PluginCenterPage({ remote, onBack, onSkills, openBundle }) {
295
373
  const [plugins, setPlugins] = useState([])
296
374
  const [bundles, setBundles] = useState([])
297
- const [loading, setLoading] = useState(false)
375
+ const [loading, setLoading] = useState(true)
376
+ const [loadError, setLoadError] = useState(false)
377
+ const [workingId, setWorkingId] = useState('')
378
+ const [uncertainId, setUncertainId] = useState('')
379
+ const [completedId, setCompletedId] = useState('')
298
380
  const [message, setMessage] = useState('')
299
381
  const [query, setQuery] = useState('')
300
382
  const [filter, setFilter] = useState('all')
@@ -312,53 +394,150 @@ window.__ModuleLoader__.load({
312
394
  if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 本机插件。')
313
395
  setPlugins(result.plugins)
314
396
  setBundles(listed.value)
397
+ setLoadError(false)
315
398
  setMessage(result.plugins.length ? '' : '团队暂未发布精选插件。')
316
399
  } catch (error) {
317
400
  setPlugins([])
318
401
  setBundles([])
402
+ setLoadError(true)
319
403
  setMessage(error instanceof Error ? error.message : '无法读取企业精选插件。')
320
404
  } finally { setLoading(false) }
321
405
  }
322
406
  useEffect(() => { void refresh() }, [])
407
+ const install = async (plugin) => {
408
+ const artifactUrl = approvedPluginArtifactUrl(plugin)
409
+ if (!artifactUrl || workingId || uncertainId === plugin.id || completedId === plugin.id) return
410
+ const manager = remote?.pluginManager
411
+ if (typeof manager?.installBundle !== 'function') {
412
+ setMessage('当前 DSH 未提供插件安装接口,请联系管理员。')
413
+ return
414
+ }
415
+ const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
416
+ const action = local ? local.version === plugin.version ? '重新安装' : '更新' : '安装'
417
+ if (!window.confirm(`在这台 DSH ${action}团队审核的 ${plugin.title} ${plugin.version}?插件将在本机运行,完成后需要完全重启 DSH。`)) return
418
+ setWorkingId(plugin.id)
419
+ setMessage(`正在安装 ${plugin.title},请不要退出 DSH…`)
420
+ try {
421
+ const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
422
+ const catalog = await readLocalJsonResponse(response)
423
+ const latest = Array.isArray(catalog?.plugins)
424
+ ? catalog.plugins.find((item) => item.id === plugin.id) : null
425
+ if (!response.ok || latest?.package_name !== plugin.package_name
426
+ || latest?.version !== plugin.version || latest?.artifact_sha256 !== plugin.artifact_sha256
427
+ || approvedPluginArtifactUrl(latest) !== artifactUrl) {
428
+ throw new Error('插件审核信息已变化,请刷新目录后重试。')
429
+ }
430
+ const requestId = crypto.randomUUID()
431
+ let result
432
+ try { result = await manager.installBundle(artifactUrl, { enabled: true, requestId }) }
433
+ catch { result = null }
434
+ if (!result?.ok) {
435
+ let recovered = null
436
+ if (typeof manager.waitForInstall === 'function') {
437
+ try {
438
+ const pending = await manager.waitForInstall(requestId)
439
+ if (pending.ok && pending.value) recovered = { ok: true, value: pending.value }
440
+ } catch {}
441
+ }
442
+ if (!recovered) {
443
+ setUncertainId(plugin.id)
444
+ setMessage(`暂时无法确认 ${plugin.title} 的安装结果。请先在 DSH 插件列表核对,勿重复点击安装。`)
445
+ return
446
+ }
447
+ result = recovered
448
+ }
449
+ const outcome = result.value
450
+ if (!outcome || typeof outcome !== 'object') {
451
+ setUncertainId(plugin.id)
452
+ setMessage(`DSH 未返回 ${plugin.title} 的安装结果。请先在插件列表核对,勿重复点击安装。`)
453
+ return
454
+ }
455
+ if (outcome.application === 'failed' || outcome.application === 'cancelled') {
456
+ if (outcome.error?.code === 'ambiguous-install' && local?.version === plugin.version) {
457
+ setMessage(`DSH 无法确定 ${plugin.title} 本次安装的唯一结果,未确认完成。请先在 DSH 插件列表核对;若已经正常使用同版则无需重复安装,仍需核对来源时请联系管理员。`)
458
+ return
459
+ }
460
+ const diagnostic = typeof outcome.error?.diagnostic === 'string'
461
+ ? outcome.error.diagnostic.slice(0, 180) : ''
462
+ setMessage(`${plugin.title} 安装未完成。请在 DSH 插件管理查看状态或联系管理员。${diagnostic ? `原因:${diagnostic}` : ''}`)
463
+ return
464
+ }
465
+ if (outcome.bundle !== plugin.package_name
466
+ || !['restart-required', 'applied'].includes(outcome.application)) {
467
+ setUncertainId(plugin.id)
468
+ setMessage(`DSH 未确认 ${plugin.title} 已安装。请先在插件列表核对,勿重复点击安装。`)
469
+ return
470
+ }
471
+ let observed = null
472
+ try {
473
+ const installed = await manager.listBundles()
474
+ if (installed.ok && Array.isArray(installed.value)) {
475
+ observed = installed.value.find((bundle) => bundle.name === plugin.package_name && bundle.installed) || null
476
+ }
477
+ } catch {}
478
+ if ((outcome.version && outcome.version !== plugin.version) || observed?.version !== plugin.version) {
479
+ setUncertainId(plugin.id)
480
+ setMessage(`DSH 未确认 ${plugin.title} 的审核版本已安装。请先在插件列表核对,勿重复点击安装。`)
481
+ return
482
+ }
483
+ await refresh()
484
+ try { localStorage.setItem(`${PLUGIN_INSTALL_RECORD_PREFIX}${plugin.id}`, plugin.artifact_sha256) } catch {}
485
+ setCompletedId(plugin.id)
486
+ setMessage(`${plugin.title} 已安装。请完全退出并重新启动 DSH 后使用。`)
487
+ } catch (error) {
488
+ setMessage(error instanceof Error ? error.message : '插件安装未完成,请稍后重试。')
489
+ } finally { setWorkingId('') }
490
+ }
323
491
  const visible = plugins.filter((plugin) => {
324
492
  const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
493
+ const artifactUrl = approvedPluginArtifactUrl(plugin)
325
494
  const matches = !query.trim() || `${plugin.title} ${plugin.description} ${plugin.package_name}`.toLowerCase().includes(query.trim().toLowerCase())
326
- return matches && (filter === 'all' || filter === 'installed' && installed?.version === plugin.version
327
- || filter === 'available' && plugin.status === 'published' && !installed
495
+ return matches && (filter === 'all' || filter === 'installed' && Boolean(installed)
496
+ || filter === 'available' && artifactUrl && (!installed || (installed.version && installed.version !== plugin.version))
328
497
  || filter === 'retired' && plugin.status !== 'published')
329
498
  })
330
499
  return h('section', { 'aria-label': '企业精选插件', style: {
331
500
  height: '100%', overflowY: 'auto', boxSizing: 'border-box',
332
501
  padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
333
502
  } }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
334
- h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
335
- h('button', { type: 'button', onClick: onBack, style: secondaryButtonStyle }, '← 返回 ANT 插件设置'),
336
- h('button', { type: 'button', onClick: onSkills, style: secondaryButtonStyle }, '查看企业精选技能 →')),
503
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
504
+ h('nav', { 'aria-label': '企业扩展分类', style: { display: 'flex', gap: 6 } },
505
+ h('button', { type: 'button', onClick: onSkills, style: categoryButtonStyle }, '技能'),
506
+ h('span', { 'aria-current': 'page', style: { ...categoryButtonStyle, display: 'inline-flex', alignItems: 'center', cursor: 'default', backgroundColor: 'var(--dsw-alias-bg-layer-2)' } }, '插件')),
507
+ h('button', { type: 'button', onClick: onBack, style: { ...categoryButtonStyle, marginLeft: 'auto' } }, 'ANT 设置')),
337
508
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 16 } },
338
509
  h('div', null,
339
510
  h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选插件'),
340
511
  h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。')),
341
512
  h('button', { type: 'button', disabled: loading, onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
342
- h('aside', { role: 'note', style: { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } },
343
- h('strong', null, '插件一键安装暂未开放'),
344
- h('p', { style: mutedTextStyle }, '当前 DSH 无法让 ANT 在运行代码前核对实际下载的插件及其依赖。需要使用某个插件时,请联系企业管理员安排审核与安全安装;请勿自行从其他来源安装。')),
345
- h(ExtensionRequestNotice),
346
- h('div', { style: panelStyle },
513
+ !loading && !plugins.length && !loadError ? h('section', { role: 'status', style: panelStyle },
514
+ h('h2', { style: { margin: 0, fontSize: 18 } }, '团队暂未发布精选插件'),
515
+ h('p', { style: mutedTextStyle }, '需要某个插件?请联系企业管理员,说明用途或提供插件地址。审核发布后会显示在这里。')) : null,
516
+ !loading && loadError ? h('p', { role: 'alert', style: mutedTextStyle }, message) : null,
517
+ plugins.length ? h(ExtensionRequestNotice) : null,
518
+ plugins.length ? h('div', { style: panelStyle },
347
519
  h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
348
520
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
349
- ...[['all', '全部'], ['available', '已审核'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
521
+ ...[['all', '全部'], ['available', '可安装或升级'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
350
522
  h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
351
- style: { ...secondaryButtonStyle, minHeight: 34, backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)))),
352
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
523
+ style: { ...secondaryButtonStyle, minHeight: 34, backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)))) : null,
524
+ plugins.length && message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
353
525
  visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
354
526
  ...visible.map((plugin) => {
355
527
  const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
356
528
  const sameVersion = local?.version === plugin.version
357
529
  const security = plugin.status === 'security_withdrawn'
530
+ const artifactUrl = approvedPluginArtifactUrl(plugin)
531
+ const sourceKnown = typeof local?.source === 'string'
532
+ const verifiedLocal = sameVersion && artifactUrl && sourceKnown && local.source === artifactUrl
533
+ const recordedInstall = sameVersion && !sourceKnown && previouslyInstalledReviewedPlugin(plugin)
358
534
  const status = security ? '已废弃(安全风险)'
359
- : plugin.status === 'deprecated' ? '已废弃' : sameVersion
360
- ? local.error ? '加载异常' : local.enabled ? '已安装(配置已启用)' : '已安装未启用'
361
- : local ? `本机版本 ${local.version || '暂无法读取'}` : '已审核,等待安全安装'
535
+ : plugin.status === 'deprecated' ? '已废弃' : local?.error ? '加载异常' : sameVersion
536
+ ? verifiedLocal ? local.enabled ? '本机已装(团队来源,已启用)' : '本机已装(团队来源,未启用)'
537
+ : sourceKnown ? '本机同版,来源不同' : recordedInstall
538
+ ? '本机同版,曾由企业扩展安装;当前 DSH 无法复核来源' : '本机同版,当前 DSH 无法核对来源'
539
+ : artifactUrl ? local ? local.version ? `可更新 · 本机 ${local.version}` : '本机版本暂无法读取' : '可安装'
540
+ : local ? `本机版本 ${local.version || '暂无法读取'}` : '已审核,暂需管理员安装'
362
541
  return h('article', { key: plugin.id, style: security
363
542
  ? { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } : panelStyle },
364
543
  h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 8 } },
@@ -374,16 +553,27 @@ window.__ModuleLoader__.load({
374
553
  ? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)' } : mutedTextStyle },
375
554
  plugin.withdrawal_reason || '团队已停止推荐此版本。') : null,
376
555
  local?.error ? h('p', { role: 'status', style: mutedTextStyle }, local.error.message || String(local.error)) : null,
556
+ sameVersion && !sourceKnown && artifactUrl ? h('p', { style: mutedTextStyle }, recordedInstall
557
+ ? '此前曾由企业扩展安装;当前 DSH 无法再次核对来源。正常使用无需重复安装;若后来换过来源,可以重新安装。'
558
+ : '已装同版且使用正常时无需重复安装;若想改用团队审核包,可自愿安装。') : null,
377
559
  h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
378
- plugin.status === 'published' && !local
560
+ artifactUrl && (!local || (local.version && !verifiedLocal))
561
+ ? h('button', { type: 'button', disabled: Boolean(workingId) || uncertainId === plugin.id || completedId === plugin.id,
562
+ onClick: () => void install(plugin), style: primaryButtonStyle },
563
+ workingId === plugin.id ? '安装中…' : uncertainId === plugin.id ? '先核对安装结果'
564
+ : completedId === plugin.id ? '重启 DSH 后生效'
565
+ : local ? sameVersion ? '按团队审核包重新安装' : '更新到审核版本' : '安装到这台 DSH') : null,
566
+ plugin.status === 'published' && !local && !artifactUrl
379
567
  ? h('p', { style: mutedTextStyle }, '如需安装,请联系企业管理员安排。') : null,
380
- local ? h('button', { type: 'button', onClick: () => openBundle(plugin.package_name), style: secondaryButtonStyle }, '打开 DSH 插件管理') : null,
381
- local && !sameVersion && plugin.status === 'published'
568
+ local || uncertainId === plugin.id ? h('button', { type: 'button', onClick: () => openBundle(plugin.package_name), style: secondaryButtonStyle }, '打开 DSH 插件管理') : null,
569
+ local && !sameVersion && plugin.status === 'published' && !artifactUrl
382
570
  ? h('p', { style: mutedTextStyle }, '本机版本与团队审核版本不同;请联系企业管理员核对。') : null))
383
- })) : !loading && plugins.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的插件。') : null))
571
+ })) : !loading && plugins.length ? h('p', { style: mutedTextStyle }, filter === 'available'
572
+ ? '当前没有可一键安装的插件。可在“全部”查看审核目录,或联系企业管理员。'
573
+ : '没有符合筛选条件的插件。') : null))
384
574
  }
385
575
 
386
- function ModelSyncFooter({ remote, serviceUrl, onServiceUrlChange }) {
576
+ function ModelSyncFooter({ remote, serviceUrl, bindingReady, onServiceUrlChange, onConnectionChange }) {
387
577
  const [email, setEmail] = useState('')
388
578
  const [code, setCode] = useState('')
389
579
  const [message, setMessage] = useState('')
@@ -396,6 +586,7 @@ window.__ModuleLoader__.load({
396
586
  const [bundleRelease, setBundleRelease] = useState(null)
397
587
  const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
398
588
  const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
589
+ const [pinnedServiceUrl, setPinnedServiceUrl] = useState(null)
399
590
 
400
591
  const connectionErrorMessage = (result, status) => {
401
592
  const message = result?.message || `请求失败(${status})`
@@ -407,13 +598,13 @@ window.__ModuleLoader__.load({
407
598
  : `${message} 本机无法确认旧设备编号;请让原团队管理员按成员查看设备清单,勿用新编号猜测。`
408
599
  }
409
600
 
410
- const inspectServiceCompatibility = async (rawServiceUrl, useSavedConnection = false) => {
601
+ const inspectServiceCompatibility = async (rawServiceUrl, useSavedConnection = false, isCurrent = () => true) => {
411
602
  const normalized = normalizeServiceUrl(rawServiceUrl)
412
603
  if (!normalized && !useSavedConnection) {
413
- setServiceCompatibility({ state: 'idle', message: '' })
604
+ if (isCurrent()) setServiceCompatibility({ state: 'idle', message: '' })
414
605
  return null
415
606
  }
416
- setServiceCompatibility({ state: 'checking', message: '正在识别 ANT 客户端与服务端版本…' })
607
+ if (isCurrent()) setServiceCompatibility({ state: 'checking', message: '正在识别 ANT 客户端与服务端版本…' })
417
608
  try {
418
609
  const manager = remote?.pluginManager
419
610
  if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 未提供 Bundle 版本读取接口。')
@@ -444,14 +635,14 @@ window.__ModuleLoader__.load({
444
635
  : `已识别 ANT Bundle v${result.bundle_version} 和服务端 v${result.server_version};服务端未提供协议能力,兼容性未知,仍允许继续。`
445
636
  const state = compatibility === true ? 'compatible' : compatibility === false ? 'incompatible' : 'unknown'
446
637
  const status = { state, message, ...result }
447
- setServiceCompatibility(status)
638
+ if (isCurrent()) setServiceCompatibility(status)
448
639
  return status
449
640
  } catch (error) {
450
641
  const status = {
451
642
  state: 'unknown',
452
643
  message: `无法确认 ANT 版本兼容性,仍允许继续:${error instanceof Error ? error.message : '连接检查失败。'}`,
453
644
  }
454
- setServiceCompatibility(status)
645
+ if (isCurrent()) setServiceCompatibility(status)
455
646
  return status
456
647
  }
457
648
  }
@@ -496,6 +687,10 @@ window.__ModuleLoader__.load({
496
687
  }
497
688
 
498
689
  const refreshModels = async () => {
690
+ if (!bindingReady) {
691
+ setMessage('请先等待团队绑定检查完成,再刷新团队模型。')
692
+ return
693
+ }
499
694
  setBusy(true)
500
695
  setMessage('正在从 ANT 刷新获准模型…')
501
696
  try {
@@ -563,16 +758,105 @@ window.__ModuleLoader__.load({
563
758
  throw new Error(`当前 DSH 未找到已安装的 ${release.package_name} Bundle。`)
564
759
  }
565
760
  const available = newerVersion(release.version, installed.version)
761
+ if (release.artifact_status === 'revoked') {
762
+ setBundleUpdate({ state: 'revoked', currentVersion: installed.version,
763
+ message: installed.version === release.version
764
+ ? `当前 ANT ${installed.version} 的安装包已因安全问题停用。请联系企业管理员获取处理方式;ANT 不会自动删除本机副本。`
765
+ : `发布版本 ${release.version} 的安装包已因安全问题停用,请等待管理员发布修复版本。` })
766
+ return
767
+ }
768
+ const artifactUrl = approvedArtifactUrl(release)
566
769
  setBundleUpdate({
567
- state: available ? 'available' : 'current',
770
+ state: available ? artifactUrl ? 'available' : 'artifact-missing' : 'current',
568
771
  currentVersion: installed.version,
569
- message: available ? `发现新版本 ${release.version}。` : `当前已是最新发布版本(${installed.version})。`,
772
+ message: available
773
+ ? artifactUrl
774
+ ? `发现新版本 ${release.version},可直接更新。`
775
+ : `发现新版本 ${release.version},但管理员尚未提供已审核安装包。请联系管理员。`
776
+ : `当前已是最新发布版本(${installed.version})。`,
570
777
  })
571
778
  } catch (error) {
572
779
  setBundleUpdate({ state: 'failed', currentVersion: '', message: error instanceof Error ? error.message : '检查 ANT 更新失败。' })
573
780
  }
574
781
  }
575
782
 
783
+ const installBundleUpdate = async () => {
784
+ if (bundleUpdate.state !== 'available') return
785
+ const artifactUrl = approvedArtifactUrl(bundleRelease)
786
+ if (!artifactUrl) return
787
+ const manager = remote?.pluginManager
788
+ if (typeof manager?.installBundle !== 'function') {
789
+ setBundleUpdate((current) => ({ ...current, state: 'failed', message: '当前 DSH 未提供 Bundle 更新接口。' }))
790
+ return
791
+ }
792
+ setBusy(true)
793
+ setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在由 DSH 安装已审核的 ANT 更新…' }))
794
+ try {
795
+ const releaseResponse = await fetch('/api/ant-teams/bundle-release', {
796
+ cache: 'no-store', headers: {
797
+ 'x-ant-bundle-capabilities': 'direct-package-install',
798
+ 'x-ant-bundle-update-mode': 'check-only',
799
+ },
800
+ })
801
+ const latest = await readLocalJsonResponse(releaseResponse)
802
+ if (!releaseResponse.ok || latest.version !== bundleRelease.version
803
+ || latest.artifact_sha256 !== bundleRelease.artifact_sha256
804
+ || approvedArtifactUrl(latest) !== artifactUrl) {
805
+ throw new Error('ANT 更新信息已变化,请重新检查更新。')
806
+ }
807
+ const requestId = crypto.randomUUID()
808
+ let result
809
+ try {
810
+ result = await manager.installBundle(artifactUrl, { enabled: true, requestId })
811
+ } catch {
812
+ result = null
813
+ }
814
+ if (!result?.ok) {
815
+ let recovered = null
816
+ if (typeof manager.waitForInstall === 'function') {
817
+ try {
818
+ const pending = await manager.waitForInstall(requestId)
819
+ if (pending.ok && pending.value) recovered = { ok: true, value: pending.value }
820
+ } catch {}
821
+ }
822
+ if (!recovered) {
823
+ setBundleUpdate((current) => ({ ...current, state: 'unknown',
824
+ message: 'DSH 连接中断,更新结果暂时无法确认。请先在 DSH 插件列表查看 ANT 版本,勿重复点击更新。' }))
825
+ return
826
+ }
827
+ result = recovered
828
+ }
829
+ const outcome = result.value
830
+ if (outcome.application === 'failed' || outcome.application === 'cancelled') {
831
+ const reason = outcome.error?.diagnostic || outcome.packageResult?.output || '安装未完成。'
832
+ setBundleUpdate((current) => ({ ...current, state: 'failed', message: `更新失败:${reason}` }))
833
+ return
834
+ }
835
+ if (outcome.bundle !== ANT_BUNDLE_PACKAGE_NAME
836
+ || !['restart-required', 'applied'].includes(outcome.application)) {
837
+ throw new Error('DSH 未确认 ANT 安装成功,请联系管理员检查插件状态。')
838
+ }
839
+ let observed = null
840
+ try {
841
+ const installed = await manager.listBundles()
842
+ if (installed.ok && Array.isArray(installed.value)) {
843
+ observed = installed.value.find((bundle) => bundle.name === ANT_BUNDLE_PACKAGE_NAME && bundle.installed) || null
844
+ }
845
+ } catch {}
846
+ if ((outcome.version && outcome.version !== bundleRelease.version) || observed?.version !== bundleRelease.version) {
847
+ setBundleUpdate((current) => ({ ...current, state: 'unknown',
848
+ message: 'DSH 未确认新版本已安装。请先在 DSH 插件列表核对 ANT 版本,勿重复点击更新。' }))
849
+ return
850
+ }
851
+ setBundleUpdate({ state: 'done', currentVersion: bundleRelease.version,
852
+ message: `ANT ${bundleRelease.version} 已安装。请完全退出并重新启动 DSH 后生效。` })
853
+ } catch (error) {
854
+ setBundleUpdate((current) => ({ ...current, state: 'failed', message: error instanceof Error ? error.message : '更新未完成。' }))
855
+ } finally {
856
+ setBusy(false)
857
+ }
858
+ }
859
+
576
860
  const clearLocalConnection = async () => {
577
861
  if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
578
862
  const settingsResult = await remote.settings.mutate('llm-pi-ai', [
@@ -584,6 +868,7 @@ window.__ModuleLoader__.load({
584
868
  const credentialResult = await remote.credentials.unset('ANT_MODEL_API_KEY')
585
869
  if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
586
870
  setCredentialConfigured(false)
871
+ onConnectionChange?.(null)
587
872
  setNeedsReconnect(false)
588
873
  setReauthenticate(false)
589
874
  setAllowLocalCleanup(false)
@@ -635,20 +920,47 @@ window.__ModuleLoader__.load({
635
920
  if (!current) return
636
921
  const configured = result.ok ? Boolean(result.value?.ANT_MODEL_API_KEY?.configured) : null
637
922
  setCredentialConfigured(configured)
638
- if (configured) {
923
+ if (configured && bindingReady) {
639
924
  void refreshModels()
640
925
  void checkBundleUpdate()
641
926
  }
642
927
  }).catch(() => { if (current) setCredentialConfigured(null) })
643
928
  return () => { current = false }
644
- }, [remote])
929
+ }, [remote, bindingReady])
645
930
 
646
931
  useEffect(() => {
647
- void inspectServiceCompatibility(serviceUrl, credentialConfigured === true)
648
- }, [remote, serviceUrl, credentialConfigured])
932
+ let current = true
933
+ if (credentialConfigured !== true) {
934
+ setPinnedServiceUrl(null)
935
+ return () => { current = false }
936
+ }
937
+ const readPinnedOrigin = async () => {
938
+ try {
939
+ const response = await fetch('/api/ant-teams/connection-origin', { cache: 'no-store' })
940
+ const result = await readLocalJsonResponse(response)
941
+ if (current) setPinnedServiceUrl(response.ok ? normalizeServiceUrl(result.service_url) : null)
942
+ } catch { if (current) setPinnedServiceUrl(null) }
943
+ }
944
+ void readPinnedOrigin()
945
+ return () => { current = false }
946
+ }, [remote, credentialConfigured])
947
+
948
+ useEffect(() => {
949
+ if (!bindingReady) {
950
+ setServiceCompatibility({ state: 'idle', message: '' })
951
+ return
952
+ }
953
+ let current = true
954
+ void inspectServiceCompatibility(serviceUrl, credentialConfigured === true, () => current)
955
+ return () => { current = false }
956
+ }, [remote, serviceUrl, credentialConfigured, bindingReady])
649
957
 
650
958
  const requestCode = async (event) => {
651
959
  event.preventDefault()
960
+ if (!bindingReady) {
961
+ setMessage('请先等待团队绑定检查完成,再验证工作邮箱。')
962
+ return
963
+ }
652
964
  const normalized = normalizeServiceUrl(serviceUrl)
653
965
  const normalizedEmail = email.trim()
654
966
  if (!normalized) {
@@ -690,6 +1002,10 @@ window.__ModuleLoader__.load({
690
1002
 
691
1003
  const connect = async (event) => {
692
1004
  event.preventDefault()
1005
+ if (!bindingReady) {
1006
+ setMessage('请先等待团队绑定检查完成,再连接团队。')
1007
+ return
1008
+ }
693
1009
  const normalized = normalizeServiceUrl(serviceUrl)
694
1010
  const normalizedEmail = email.trim()
695
1011
  const normalizedCode = code.trim()
@@ -706,6 +1022,7 @@ window.__ModuleLoader__.load({
706
1022
  }
707
1023
  setBusy(true)
708
1024
  setMessage('正在验证邮箱并同步团队模型…')
1025
+ let credentialSaved = false
709
1026
  try {
710
1027
  await requireCompatibleService(normalized)
711
1028
  localStorage.setItem(SERVICE_URL_STORAGE_KEY, normalized)
@@ -729,23 +1046,36 @@ window.__ModuleLoader__.load({
729
1046
  }
730
1047
  const credentialWrite = await remote.credentials.set('ANT_MODEL_API_KEY', result.token)
731
1048
  if (!credentialWrite.ok) throw new Error(credentialWrite.error?.message || 'DSH 无法安全保存 ANT 凭证。')
1049
+ credentialSaved = true
732
1050
  await applyAuthorizedModels(result.models, normalized)
733
1051
  setCode('')
734
1052
  onServiceUrlChange(normalized)
735
1053
  setCodeRequested(false)
736
1054
  setCredentialConfigured(true)
1055
+ onConnectionChange?.(normalized)
737
1056
  setAllowLocalCleanup(false)
738
1057
  setNeedsReconnect(false)
739
1058
  setReauthenticate(false)
740
1059
  setMessage(`已连接 ${result.email || normalizedEmail},同步 ${result.models.length} 个获准模型。它们现已出现在 DSH 模型选择器中。`)
741
1060
  } catch (error) {
742
- setMessage(error instanceof Error ? error.message : '连接失败,请稍后重试。')
1061
+ if (credentialSaved) {
1062
+ setCredentialConfigured(true)
1063
+ setCode('')
1064
+ setCodeRequested(false)
1065
+ onConnectionChange?.(normalized)
1066
+ setMessage(`ANT 凭证已保存,但团队模型未能同步:${error instanceof Error ? error.message : '请稍后重试。'} 请点击“刷新团队模型”重试。`)
1067
+ } else {
1068
+ setMessage(error instanceof Error ? error.message : '连接失败,请稍后重试。')
1069
+ }
743
1070
  } finally {
744
1071
  setBusy(false)
745
1072
  }
746
1073
  }
747
1074
 
748
1075
  const showConnectForm = credentialConfigured !== true || reauthenticate || needsReconnect
1076
+ const canCheckUpdate = bindingReady && credentialConfigured === true && !needsReconnect
1077
+ const updateGuideUrl = pinnedServiceUrl
1078
+ ? new URL('/ant-update', pinnedServiceUrl).href : null
749
1079
  return h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 400px), 1fr))', alignItems: 'start', gap: 16 } },
750
1080
  h('section', { style: panelStyle },
751
1081
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
@@ -754,11 +1084,13 @@ window.__ModuleLoader__.load({
754
1084
  h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '使用上方团队服务地址验证工作邮箱并同步授权模型。连接状态变化时可随时刷新。')),
755
1085
  h('span', { style: {
756
1086
  flexShrink: 0, padding: '4px 9px', borderRadius: 999,
757
- color: credentialConfigured === true ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
758
- backgroundColor: credentialConfigured === true ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
1087
+ color: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)',
1088
+ backgroundColor: credentialConfigured === true && bindingReady ? 'var(--dsw-alias-state-success-tertiary)' : 'var(--dsw-alias-bg-layer-2)',
759
1089
  fontSize: 12,
760
- } }, credentialConfigured === true ? '已连接' : credentialConfigured === false ? '未连接' : '检查中')),
761
- h('p', { role: 'status', style: mutedTextStyle }, credentialConfigured === true
1090
+ } }, credentialConfigured === true ? bindingReady ? '已连接' : '待核对' : credentialConfigured === false ? '未连接' : '检查中')),
1091
+ h('p', { role: 'status', style: mutedTextStyle }, credentialConfigured === true && !bindingReady
1092
+ ? '本机有 ANT 凭证,但尚未确认它绑定的团队;请先完成上方的绑定检查。'
1093
+ : credentialConfigured === true
762
1094
  ? 'DSH 已安全保存此设备的 ANT 团队凭证。'
763
1095
  : credentialConfigured === false ? '验证工作邮箱后即可连接团队模型。' : '正在检查 ANT 连接状态…'),
764
1096
  serviceCompatibility.state !== 'idle' ? h('p', {
@@ -769,25 +1101,30 @@ window.__ModuleLoader__.load({
769
1101
  },
770
1102
  }, serviceCompatibility.message) : null,
771
1103
  credentialConfigured === true && !showConnectForm ? h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
772
- h('button', { type: 'button', disabled: busy, onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
773
- h('button', { type: 'button', disabled: busy, onClick: () => { setReauthenticate(true); setMessage('') }, style: secondaryButtonStyle }, '重新验证邮箱'),
1104
+ h('button', { type: 'button', disabled: busy || !bindingReady, onClick: () => void refreshModels(), style: primaryButtonStyle }, busy ? '刷新中…' : '刷新团队模型'),
1105
+ h('button', { type: 'button', disabled: busy || !bindingReady, onClick: () => { setReauthenticate(true); setMessage('') }, style: secondaryButtonStyle }, '重新验证邮箱'),
774
1106
  h('button', { type: 'button', disabled: busy, onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() }, style: secondaryButtonStyle }, '断开当前设备')) : null,
775
- credentialConfigured === true && !showConnectForm ? h('section', { style: { ...panelStyle, padding: 14, gap: 10 } },
1107
+ h('section', { style: { display: 'grid', gap: 8, paddingTop: 12, borderTop: '0.5px solid var(--dsw-alias-border-l2)' } },
776
1108
  h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 } },
777
1109
  h('h4', { style: { margin: 0, fontSize: 14, fontWeight: 650 } }, 'ANT 插件更新'),
778
- h('button', { type: 'button', disabled: busy || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
779
- bundleUpdate.message ? h('p', { role: 'status', style: mutedTextStyle }, bundleUpdate.message) : null,
780
- bundleRelease?.version && bundleUpdate.state === 'available' ? h('div', { style: { display: 'grid', gap: 8 } },
1110
+ h('button', { type: 'button', disabled: busy || !canCheckUpdate || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
1111
+ !canCheckUpdate ? h('p', { role: 'status', style: mutedTextStyle }, '连接团队后可在这里检查并安装 ANT 更新;需要先恢复插件时,可打开团队管理台的更新指引。') : null,
1112
+ updateGuideUrl ? h('a', { href: updateGuideUrl, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, `打开 ${pinnedServiceUrl} 的更新指引 ↗`)
1113
+ : !canCheckUpdate ? h('p', { style: mutedTextStyle }, '无法核对原团队地址时,请向企业管理员索取更新指引。') : null,
1114
+ bundleUpdate.message ? h('p', { role: 'status', style: bundleUpdate.state === 'revoked'
1115
+ ? { ...mutedTextStyle, color: 'var(--dsw-alias-state-error-primary)' } : mutedTextStyle }, bundleUpdate.message) : null,
1116
+ canCheckUpdate && bundleRelease?.version && ['available', 'artifact-missing'].includes(bundleUpdate.state) ? h('div', { style: { display: 'grid', gap: 8 } },
781
1117
  h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · 精确包名:${directInstallSpec(bundleRelease)}`),
782
1118
  bundleRelease.release_notes ? h('p', { style: mutedTextStyle }, bundleRelease.release_notes) : null,
783
- h('p', { style: mutedTextStyle }, 'ANT 暂停一键安装更新:当前 DSH 无法在执行安装脚本前核对审核制品。请联系团队管理员确认升级方式。')) : null) : null,
1119
+ bundleUpdate.state === 'available'
1120
+ ? h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '更新中…' : '更新 ANT') : null) : null),
784
1121
  showConnectForm ? h('form', { onSubmit: requestCode, style: { display: 'grid', gap: 9, maxWidth: 560 } },
785
1122
  h('label', { htmlFor: 'ant-team-model-email', style: { fontSize: 13, fontWeight: 550 } }, '工作邮箱'),
786
1123
  h('input', {
787
1124
  id: 'ant-team-model-email', type: 'email', autoComplete: 'email', required: true, value: email,
788
1125
  onChange: (event) => setEmail(event.target.value), style: inputStyle,
789
1126
  }),
790
- h('button', { type: 'submit', disabled: busy, style: primaryButtonStyle }, busy ? '发送中…' : '发送验证码')) : null,
1127
+ h('button', { type: 'submit', disabled: busy || !bindingReady, style: primaryButtonStyle }, busy ? '发送中…' : '发送验证码')) : null,
791
1128
  credentialConfigured === true && showConnectForm ? h('button', {
792
1129
  type: 'button', disabled: busy,
793
1130
  onClick: () => { if (window.confirm('断开当前设备与 ANT 的连接?本机 ANT 凭证和模型设置将被清除,团队服务地址会保留。')) void disconnect() },
@@ -800,7 +1137,7 @@ window.__ModuleLoader__.load({
800
1137
  required: true, maxLength: 8, value: code, placeholder: '8 位验证码',
801
1138
  onChange: (event) => setCode(event.target.value.replace(/\D/gu, '').slice(0, 8)), style: inputStyle,
802
1139
  }),
803
- h('button', { type: 'submit', disabled: busy || code.length !== 8, style: primaryButtonStyle }, busy ? '连接中…' : '验证并连接')) : null,
1140
+ h('button', { type: 'submit', disabled: busy || !bindingReady || code.length !== 8, style: primaryButtonStyle }, busy ? '连接中…' : '验证并连接')) : null,
804
1141
  allowLocalCleanup ? h('section', { style: { ...panelStyle, padding: 14, gap: 9 } },
805
1142
  h('p', { style: mutedTextStyle }, savedDeviceId()
806
1143
  ? `已保存的设备编号:${savedDeviceId()}。请交给原团队管理员核对。`
@@ -822,6 +1159,20 @@ window.__ModuleLoader__.load({
822
1159
 
823
1160
  }
824
1161
 
1162
+ function EnterpriseExtensionsPanelIcon({ size }) {
1163
+ return h('span', { 'aria-hidden': true, style: {
1164
+ display: 'inline-grid', placeItems: 'center', width: size, height: size,
1165
+ fontSize: size, lineHeight: 1,
1166
+ } }, '✦')
1167
+ }
1168
+
1169
+ function EnterpriseExtensionsPage({ remote, onBack, openBundle }) {
1170
+ const [tab, setTab] = useState('skills')
1171
+ return tab === 'skills'
1172
+ ? h(SkillCenterPage, { onBack, onPlugins: () => setTab('plugins') })
1173
+ : h(PluginCenterPage, { remote, onBack, onSkills: () => setTab('skills'), openBundle })
1174
+ }
1175
+
825
1176
  function apply(ctx) {
826
1177
  const remote = {
827
1178
  credentials: ctx.remote.credentials,
@@ -829,17 +1180,14 @@ window.__ModuleLoader__.load({
829
1180
  pluginManager: ctx.remote.pluginManager,
830
1181
  }
831
1182
  ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'ant-skills' },
832
- () => h(SkillCenterPage, {
833
- onBack: () => ctx.pluginNavigation.openBundle(ANT_BUNDLE_PACKAGE_NAME),
834
- onPlugins: () => ctx.layout.selectPanel('ant-plugins'),
835
- })))
836
- ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'ant-plugins' },
837
- () => h(PluginCenterPage, {
1183
+ () => h(EnterpriseExtensionsPage, {
838
1184
  remote,
839
1185
  onBack: () => ctx.pluginNavigation.openBundle(ANT_BUNDLE_PACKAGE_NAME),
840
- onSkills: () => ctx.layout.selectPanel('ant-skills'),
841
1186
  openBundle: (name) => ctx.pluginNavigation.openBundle(name),
842
1187
  })))
1188
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
1189
+ name: 'sidebar.panellist', id: 'ant-skills', order: 5, label: '企业扩展',
1190
+ }, EnterpriseExtensionsPanelIcon))
843
1191
  ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
844
1192
  name: 'plugins.bundle.config',
845
1193
  key: ANT_BUNDLE_PACKAGE_NAME,
@@ -848,8 +1196,7 @@ window.__ModuleLoader__.load({
848
1196
  ? h('p', null, 'ANT 团队服务入口。')
849
1197
  : h(TeamServiceConfig, {
850
1198
  remote: injectedRemote,
851
- openSkillCenter: () => ctx.layout.selectPanel('ant-skills'),
852
- openPluginCenter: () => ctx.layout.selectPanel('ant-plugins'),
1199
+ openInstalledPlugin: (name) => ctx.pluginNavigation.openBundle(name),
853
1200
  })))
854
1201
  }
855
1202
 
package/index.js CHANGED
@@ -8,6 +8,7 @@ export const inject = []
8
8
  const MODEL_SYNC_PATH = '/api/ant-teams/models'
9
9
  const SERVICE_INFO_PATH = '/api/ant-teams/service-info'
10
10
  const BUNDLE_RELEASE_PATH = '/api/ant-teams/bundle-release'
11
+ const CONNECTION_ORIGIN_PATH = '/api/ant-teams/connection-origin'
11
12
  const DISCONNECT_PATH = '/api/ant-teams/disconnect'
12
13
  const REQUEST_CODE_PATH = '/api/ant-teams/request-code'
13
14
  const CONNECT_PATH = '/api/ant-teams/connect'
@@ -292,19 +293,37 @@ async function fetchBundleRelease(request, credentials, settings) {
292
293
  const directInstallSpec = release?.package_name && release?.version
293
294
  ? `${release.package_name}@${release.version}`
294
295
  : ''
296
+ const artifactPath = release?.version ? `/api/ant-bundles/${release.version}/package.tgz` : ''
297
+ const hasArtifact = release?.artifact_path === artifactPath
298
+ && /^[a-f0-9]{64}$/u.test(release?.artifact_sha256 || '')
299
+ const noArtifact = !release?.artifact_path && !release?.artifact_sha256
295
300
  if (release?.bundle_name !== 'ant' || !validPackageName || !validVersion
296
301
  || typeof release?.install_spec !== 'string' || typeof release?.release_notes !== 'string'
297
302
  || (release.version && release.install_spec !== installSpec)
298
303
  || (!release.version && release.install_spec !== '')
299
- || (release.direct_install_spec !== undefined && release.direct_install_spec !== directInstallSpec)) {
304
+ || (release.direct_install_spec !== undefined && release.direct_install_spec !== directInstallSpec)
305
+ || (!hasArtifact && !noArtifact)) {
300
306
  return jsonResponse(502, { message: 'ANT 更新信息字段无效。' })
301
307
  }
302
- return jsonResponse(200, release)
308
+ return jsonResponse(200, hasArtifact ? {
309
+ ...release,
310
+ artifact_url: new URL(artifactPath, connection.serviceUrl).href,
311
+ } : release)
303
312
  } catch {
304
313
  return jsonResponse(502, { message: '无法连接 ANT 更新服务。' })
305
314
  }
306
315
  }
307
316
 
317
+ async function fetchConnectionOrigin(request, credentials, settings) {
318
+ if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
319
+ try {
320
+ const connection = await readMemberConnection(credentials, settings)
321
+ return jsonResponse(200, { service_url: connection?.serviceUrl || null })
322
+ } catch {
323
+ return jsonResponse(500, { message: '无法核对已绑定的 ANT 团队地址。' })
324
+ }
325
+ }
326
+
308
327
  async function disconnectMember(request, credentials, settings) {
309
328
  if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
310
329
  let connection
@@ -397,7 +416,17 @@ async function fetchMemberPlugins(request, credentials, settings) {
397
416
  { message: '无法读取企业精选插件目录。' })
398
417
  const plugins = JSON.parse(text)
399
418
  if (!Array.isArray(plugins)) throw new Error('插件目录格式无效。')
400
- return jsonResponse(200, { plugins })
419
+ return jsonResponse(200, { plugins: plugins.map((plugin) => {
420
+ const validId = typeof plugin?.id === 'string'
421
+ && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(plugin.id)
422
+ const validDigest = /^[0-9a-f]{64}$/u.test(plugin?.artifact_sha256 || '')
423
+ if (plugin?.status !== 'published' || !validId || !validDigest) {
424
+ return { ...plugin, artifact_sha256: null }
425
+ }
426
+ const artifactPath = `/api/plugin-artifacts/${plugin.id}/package.tgz`
427
+ return { ...plugin, artifact_path: artifactPath,
428
+ artifact_url: new URL(artifactPath, connection.serviceUrl).href }
429
+ }) })
401
430
  } catch {
402
431
  return jsonResponse(502, { message: '无法读取企业精选插件目录。' })
403
432
  }
@@ -505,6 +534,12 @@ export function apply(ctx) {
505
534
  requestBody: 'buffered',
506
535
  fetch: (request) => fetchBundleRelease(request, connectionCtx.credentials, connectionCtx.settings),
507
536
  }), 'ant-teams: authenticated Bundle release relay')
537
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
538
+ path: CONNECTION_ORIGIN_PATH,
539
+ methods: ['GET'],
540
+ requestBody: 'buffered',
541
+ fetch: (request) => fetchConnectionOrigin(request, connectionCtx.credentials, connectionCtx.settings),
542
+ }), 'ant-teams: pinned connection origin for update guide')
508
543
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
509
544
  path: SKILLS_PATH, methods: ['GET'], requestBody: 'buffered',
510
545
  fetch: (request) => fetchMemberSkills(request, connectionCtx.credentials, connectionCtx.settings),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-wendao/ant",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to team identity and approved models.",
5
5
  "type": "module",
6
6
  "scripts": {