@ai-wendao/ant 0.3.12 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MIGRATION.md CHANGED
@@ -13,7 +13,7 @@
13
13
  此方式适用于 DSH Desktop RC2 使用默认、启用 HMR 的 Desktop profile,且目标 ANT 版本已发布到 npm:
14
14
 
15
15
  1. 打开 **插件 → ANT**,点击 **卸载** 并确认。
16
- 2. 回到插件列表,点击 **添加插件**,输入目标版本的直接包名(正式发布后例如 `@ai-wendao/ant@0.3.12`),然后点击 **安装**。
16
+ 2. 回到插件列表,点击 **添加插件**,输入目标版本的直接包名(正式发布后例如 `@ai-wendao/ant@0.3.13`),然后点击 **安装**。
17
17
  3. 安装完成后启用 ANT,打开设置页,检查服务连接与获准模型。
18
18
 
19
19
  RC2 的 Desktop 基础 profile 默认启用 HMR;官方管理器在该模式下会先卸载 Bundle,再用随 DSH 提供的包管理器移除依赖。包卸载本身不会调用 ANT 的“断开设备”操作,也不会清空 DSH 凭证存储。若 profile 禁用了 HMR,管理器会提示先停止 profile;此时改用下方 CLI 备用流程。真实成员 profile 的凭证保留仍需在目标 RC2 客户端上验收。
@@ -59,14 +59,14 @@ node packages/ant-bundle/tools/migrate-legacy.mjs migrate --apply
59
59
  安装了系统 Node.js 的成员也可以从已发布 npm 包运行:
60
60
 
61
61
  ```sh
62
- npm exec --yes --package @ai-wendao/ant@0.3.12 -- ant-migrate inspect
63
- npm exec --yes --package @ai-wendao/ant@0.3.12 -- ant-migrate migrate
62
+ npm exec --yes --package @ai-wendao/ant@0.3.13 -- ant-migrate inspect
63
+ npm exec --yes --package @ai-wendao/ant@0.3.13 -- ant-migrate migrate
64
64
  ```
65
65
 
66
66
  退出 DSH 后执行变更:
67
67
 
68
68
  ```sh
69
- npm exec --yes --package @ai-wendao/ant@0.3.12 -- ant-migrate migrate --apply
69
+ npm exec --yes --package @ai-wendao/ant@0.3.13 -- ant-migrate migrate --apply
70
70
  ```
71
71
 
72
72
  若 CLI 不在 PATH,可追加 `--dsh-cli "<Desktop CLI 完整路径>"`;若 DSH 使用自定义数据目录,可追加 `--dsh-home "<DSH_HOME 路径>"`。
@@ -76,8 +76,8 @@ npm exec --yes --package @ai-wendao/ant@0.3.12 -- ant-migrate migrate --apply
76
76
  `migrate --apply` 会输出受限权限的备份目录,备份包含 profile 的 `package.json`、`pnpm-lock.yaml`、`pnpm-workspace.yaml`(若存在)及其 SHA-256 校验值,不包含密钥库。备份仅用于迁移状态核验和保留原始清单,不等于离线恢复包。确认 DSH 已退出后,通过同一工具调用:
77
77
 
78
78
  ```sh
79
- npm exec --yes --package @ai-wendao/ant@0.3.12 -- ant-migrate rollback --backup "<备份目录>"
80
- npm exec --yes --package @ai-wendao/ant@0.3.12 -- ant-migrate rollback --backup "<备份目录>" --apply
79
+ npm exec --yes --package @ai-wendao/ant@0.3.13 -- ant-migrate rollback --backup "<备份目录>"
80
+ npm exec --yes --package @ai-wendao/ant@0.3.13 -- ant-migrate rollback --backup "<备份目录>" --apply
81
81
  ```
82
82
 
83
83
  回退只在 scoped 包已安装、旧 alias 尚未移除的中间态工作;它会卸载 scoped 包并保留旧 alias。已完成迁移的 profile 不接受该回退,避免自动恢复已知有加载问题的 alias。若安装包时 npm registry 不可用,保持 DSH 关闭,稍后重新运行 `migrate --apply`。
package/README.md CHANGED
@@ -6,10 +6,16 @@ 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 原生技能提供者发现和使用。
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
+ ## 企业精选插件
14
+
15
+ 管理员可从 npm 官方源的 DSH Bundle 精确版本创建草稿,填写源码、许可证、依赖和安装脚本的审核记录后发布。成员在“扩展中心 → 插件”查看审核信息和本机状态。当前 DSH RC2 无法让 ANT 在安装脚本运行前确认实际下载制品与审核制品一致,因此插件一键安装暂停;需要使用时请联系企业管理员安排。安全撤回版本会在扩展中心标明;本机安装受影响版本或无法读取本机版本时,ANT 设置页也显示相应风险公告。
16
+
17
+ 企业精选是团队推荐目录,不是强制白名单;ANT 不能阻止成员从 DSH 原生插件管理页自行安装其他包。当前 DSH RC2 接口无法让 ANT 强制校验成员安装的包字节与审核时的摘要一致,也不能保证 profile 的依赖范围始终为精确版本;上线边界见 [待决策记录](../../docs/decisions/2026-10-15-plugin-center-rollout.md)。
18
+
13
19
  ## Existing `ant` alias installations
14
20
 
15
21
  DSH RC2 requires a Bundle's client module identity to match its npm package name. Profiles that installed `ant` as an npm alias for `@ai-wendao/ant` need a one-time package-key migration. Use DSH's Plugins page for the normal Desktop profile; the cross-platform migration CLI is a fallback.
package/client.js CHANGED
@@ -33,6 +33,15 @@ window.__ModuleLoader__.load({
33
33
  border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 8,
34
34
  cursor: 'pointer',
35
35
  }
36
+ function ExtensionRequestNotice() {
37
+ return h('aside', { role: 'note', style: {
38
+ ...panelStyle,
39
+ borderColor: 'var(--dsw-alias-state-business-primary)',
40
+ gap: 5,
41
+ } },
42
+ h('h2', { style: { margin: 0, fontSize: 16 } }, '没有找到想用的技能或插件?'),
43
+ h('p', { style: mutedTextStyle }, '请联系企业管理员,说明你希望完成的任务或提供扩展地址。管理员审核后会将合适的版本加入扩展中心。技能可在这里安装;插件请由管理员安排,勿自行安装未经审核的扩展。'))
44
+ }
36
45
  const normalizeServiceUrl = (value) => {
37
46
  let url
38
47
  try { url = new URL(value.trim()) } catch { return null }
@@ -92,18 +101,48 @@ window.__ModuleLoader__.load({
92
101
  try { return normalizeServiceUrl(localStorage.getItem(SERVICE_URL_STORAGE_KEY) || '') || '' } catch { return '' }
93
102
  }
94
103
 
104
+ function savedDeviceId() {
105
+ try {
106
+ const existing = localStorage.getItem(DEVICE_ID_STORAGE_KEY)
107
+ return existing && /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(existing) ? existing : null
108
+ } catch { return null }
109
+ }
110
+
95
111
  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
112
+ const existing = savedDeviceId()
113
+ if (existing) return existing
98
114
  const deviceId = crypto.randomUUID()
99
115
  localStorage.setItem(DEVICE_ID_STORAGE_KEY, deviceId)
100
116
  return deviceId
101
117
  }
102
118
 
103
- function TeamServiceConfig({ remote, openSkillCenter }) {
119
+ function TeamServiceConfig({ remote, openSkillCenter, openPluginCenter }) {
104
120
  const [serviceUrl, setServiceUrl] = useState(loadServiceUrl)
105
121
  const [draft, setDraft] = useState(serviceUrl)
106
122
  const [message, setMessage] = useState('')
123
+ const [pluginAlerts, setPluginAlerts] = useState([])
124
+ useEffect(() => {
125
+ let disposed = false
126
+ const inspect = async () => {
127
+ try {
128
+ const response = await fetch('/api/ant-teams/plugins', { cache: 'no-store' })
129
+ if (!response.ok) return
130
+ const catalog = await readLocalJsonResponse(response)
131
+ const listed = await remote?.pluginManager?.listBundles?.()
132
+ if (!listed?.ok || !Array.isArray(catalog.plugins)) return
133
+ const affected = catalog.plugins.flatMap((plugin) => {
134
+ if (plugin.status !== 'security_withdrawn') return []
135
+ const local = listed.value.find((bundle) => bundle.installed && bundle.name === plugin.package_name)
136
+ if (!local || (local.version && local.version !== plugin.version)) return []
137
+ return [{ ...plugin, versionUnknown: !local.version }]
138
+ })
139
+ if (!disposed) setPluginAlerts(affected)
140
+ } catch {}
141
+ }
142
+ void inspect()
143
+ const timer = setInterval(() => void inspect(), 5 * 60 * 1000)
144
+ return () => { disposed = true; clearInterval(timer) }
145
+ }, [remote])
107
146
  const save = (event) => {
108
147
  event.preventDefault()
109
148
  const normalized = normalizeServiceUrl(draft)
@@ -144,13 +183,18 @@ window.__ModuleLoader__.load({
144
183
  link ? h('a', { href: link, target: '_blank', rel: 'noopener noreferrer', style: { color: 'var(--dsw-alias-link)', fontSize: 13 } }, '打开团队管理 ↗') : null)),
145
184
  h('section', { style: { ...panelStyle, display: 'flex', flexWrap: 'wrap', alignItems: 'center', justifyContent: 'space-between' } },
146
185
  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 }, '打开技能中心 →')),
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
+ pluginAlerts.length ? h('section', { role: 'alert', style: { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } },
190
+ h('h3', { style: { margin: 0, fontSize: 16 } }, '已安装插件存在安全撤回公告'),
191
+ ...pluginAlerts.map((plugin) => h('p', { key: plugin.id, style: mutedTextStyle },
192
+ `${plugin.title} ${plugin.version}${plugin.versionUnknown ? '(本机版本暂无法读取,请核对是否受影响)' : ''}:${plugin.withdrawal_reason}`)),
193
+ h('button', { type: 'button', onClick: openPluginCenter, style: secondaryButtonStyle }, '查看插件并打开 DSH 管理 →')) : null,
150
194
  h(ModelSyncFooter, { remote, serviceUrl, onServiceUrlChange: setServiceUrl }))
151
195
  }
152
196
 
153
- function SkillCenterPage({ onBack }) {
197
+ function SkillCenterPage({ onBack, onPlugins }) {
154
198
  const [skills, setSkills] = useState([])
155
199
  const [loading, setLoading] = useState(false)
156
200
  const [workingId, setWorkingId] = useState('')
@@ -191,7 +235,8 @@ window.__ModuleLoader__.load({
191
235
  const result = await readLocalJsonResponse(response)
192
236
  if (!response.ok) throw new Error(result.message || `${action}失败(${response.status})。`)
193
237
  await refresh()
194
- setMessage(`${skill.title} 已${action}。DSH 将从本机技能目录发现它。`)
238
+ setMessage(remove ? `${skill.title} 已从 ANT 管理的本机目录卸载。`
239
+ : `${skill.title} 的文件已${action}到 DSH 用户技能目录。DSH 会在后续会话或模型步骤中读取;若不可用,请检查当前预设是否启用了本地技能。`)
195
240
  } catch (error) { setMessage(error instanceof Error ? error.message : `${action}失败。`) }
196
241
  finally { setWorkingId('') }
197
242
  }
@@ -200,17 +245,20 @@ window.__ModuleLoader__.load({
200
245
  const matchesQuery = !query.trim() || `${skill.title} ${skill.description} ${skill.dsh_name}`.toLowerCase().includes(query.trim().toLowerCase())
201
246
  return matchesFilter && matchesQuery
202
247
  })
203
- const statusLabel = { installed: '已安装', 'update-available': '可更新', 'not-installed': '未安装', 'not-discovered': 'DSH 未发现', conflict: '本机冲突' }
248
+ const statusLabel = { installed: '已放入本机目录', 'update-available': '可更新', 'not-installed': '未安装', conflict: '本机冲突' }
204
249
  return h('section', { 'aria-label': '企业精选技能', style: {
205
250
  height: '100%', overflowY: 'auto', boxSizing: 'border-box',
206
251
  padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
207
252
  } }, h('div', { style: { maxWidth: 1440, margin: '0 auto', display: 'grid', gap: 22 } },
208
- h('button', { type: 'button', onClick: onBack, style: { ...secondaryButtonStyle, justifySelf: 'start' } }, '← 返回 ANT 插件设置'),
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 }, '查看企业精选插件 →')),
209
256
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'end', justifyContent: 'space-between', gap: 16 } },
210
257
  h('div', null,
211
258
  h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选技能'),
212
259
  h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '团队管理员审核后发布;安装到这台 DSH 后,由 DSH 原生技能系统发现和使用。')),
213
260
  h('button', { type: 'button', disabled: loading || Boolean(workingId), onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
261
+ h(ExtensionRequestNotice),
214
262
  h('div', { style: { ...panelStyle, gap: 12 } },
215
263
  h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索技能名称或说明', 'aria-label': '搜索企业精选技能', style: inputStyle }),
216
264
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
@@ -234,19 +282,107 @@ window.__ModuleLoader__.load({
234
282
  h('p', { style: mutedTextStyle }, `本机依赖:${skill.requirements}`),
235
283
  skill.local_status === 'conflict'
236
284
  ? 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' } },
285
+ : h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 'auto' } },
242
286
  h('button', { type: 'button', disabled: Boolean(workingId) || skill.local_status === 'installed',
243
287
  onClick: () => void changeInstall(skill), style: primaryButtonStyle },
244
- skill.local_status === 'installed' ? '已安装' : skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
288
+ skill.local_status === 'installed' ? '文件已安装' : skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
245
289
  skill.local_status !== 'not-installed' ? h('button', { type: 'button', disabled: Boolean(workingId),
246
290
  onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载') : null))
247
291
  })) : !loading && skills.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null))
248
292
  }
249
293
 
294
+ function PluginCenterPage({ remote, onBack, onSkills, openBundle }) {
295
+ const [plugins, setPlugins] = useState([])
296
+ const [bundles, setBundles] = useState([])
297
+ const [loading, setLoading] = useState(false)
298
+ const [message, setMessage] = useState('')
299
+ const [query, setQuery] = useState('')
300
+ const [filter, setFilter] = useState('all')
301
+ const refresh = async () => {
302
+ setLoading(true)
303
+ try {
304
+ const manager = remote?.pluginManager
305
+ if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 未提供插件管理接口。')
306
+ const [response, listed] = await Promise.all([
307
+ fetch('/api/ant-teams/plugins', { cache: 'no-store' }), manager.listBundles(),
308
+ ])
309
+ const result = await readLocalJsonResponse(response)
310
+ if (!response.ok) throw new Error(result.message || `读取插件目录失败(${response.status})。`)
311
+ if (!Array.isArray(result.plugins)) throw new Error('企业插件目录格式无效。')
312
+ if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 本机插件。')
313
+ setPlugins(result.plugins)
314
+ setBundles(listed.value)
315
+ setMessage(result.plugins.length ? '' : '团队暂未发布精选插件。')
316
+ } catch (error) {
317
+ setPlugins([])
318
+ setBundles([])
319
+ setMessage(error instanceof Error ? error.message : '无法读取企业精选插件。')
320
+ } finally { setLoading(false) }
321
+ }
322
+ useEffect(() => { void refresh() }, [])
323
+ const visible = plugins.filter((plugin) => {
324
+ const installed = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
325
+ 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
328
+ || filter === 'retired' && plugin.status !== 'published')
329
+ })
330
+ return h('section', { 'aria-label': '企业精选插件', style: {
331
+ height: '100%', overflowY: 'auto', boxSizing: 'border-box',
332
+ padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
333
+ } }, 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 }, '查看企业精选技能 →')),
337
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 16 } },
338
+ h('div', null,
339
+ h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选插件'),
340
+ h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '这里展示团队审核过的插件版本及本机状态。')),
341
+ 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 },
347
+ h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索插件名称或用途', 'aria-label': '搜索企业精选插件', style: inputStyle }),
348
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
349
+ ...[['all', '全部'], ['available', '已审核'], ['installed', '已安装'], ['retired', '已废弃']].map(([value, label]) =>
350
+ 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,
353
+ visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
354
+ ...visible.map((plugin) => {
355
+ const local = bundles.find((bundle) => bundle.name === plugin.package_name && bundle.installed)
356
+ const sameVersion = local?.version === plugin.version
357
+ const security = plugin.status === 'security_withdrawn'
358
+ const status = security ? '已废弃(安全风险)'
359
+ : plugin.status === 'deprecated' ? '已废弃' : sameVersion
360
+ ? local.error ? '加载异常' : local.enabled ? '已安装(配置已启用)' : '已安装未启用'
361
+ : local ? `本机版本 ${local.version || '暂无法读取'}` : '已审核,等待安全安装'
362
+ return h('article', { key: plugin.id, style: security
363
+ ? { ...panelStyle, borderColor: 'var(--dsw-alias-label-danger, #d44)' } : panelStyle },
364
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', gap: 8 } },
365
+ h('h2', { style: { margin: 0, fontSize: 19 } }, plugin.title),
366
+ h('span', { style: security
367
+ ? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)', fontWeight: 650 } : mutedTextStyle }, status)),
368
+ h('p', { style: mutedTextStyle }, plugin.description),
369
+ h('p', { style: mutedTextStyle }, `${plugin.package_name}@${plugin.version} · 许可证:${plugin.license || '未声明'}`),
370
+ h('p', { style: mutedTextStyle }, `来源:${plugin.source_url || '未提供'} · DSH 版本要求:${plugin.peer_range || '包未声明'}`),
371
+ h('p', { style: mutedTextStyle }, `额外配置:${plugin.requirements || '无'}`),
372
+ plugin.scripts ? h('p', { style: mutedTextStyle }, `包自身的安装脚本:${plugin.scripts}`) : null,
373
+ plugin.status !== 'published' ? h('p', { role: 'status', style: security
374
+ ? { ...mutedTextStyle, color: 'var(--dsw-alias-label-danger, #d44)' } : mutedTextStyle },
375
+ plugin.withdrawal_reason || '团队已停止推荐此版本。') : null,
376
+ local?.error ? h('p', { role: 'status', style: mutedTextStyle }, local.error.message || String(local.error)) : null,
377
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
378
+ plugin.status === 'published' && !local
379
+ ? 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'
382
+ ? h('p', { style: mutedTextStyle }, '本机版本与团队审核版本不同;请联系企业管理员核对。') : null))
383
+ })) : !loading && plugins.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的插件。') : null))
384
+ }
385
+
250
386
  function ModelSyncFooter({ remote, serviceUrl, onServiceUrlChange }) {
251
387
  const [email, setEmail] = useState('')
252
388
  const [code, setCode] = useState('')
@@ -256,11 +392,21 @@ window.__ModuleLoader__.load({
256
392
  const [credentialConfigured, setCredentialConfigured] = useState(null)
257
393
  const [reauthenticate, setReauthenticate] = useState(false)
258
394
  const [needsReconnect, setNeedsReconnect] = useState(false)
395
+ const [allowLocalCleanup, setAllowLocalCleanup] = useState(false)
259
396
  const [bundleRelease, setBundleRelease] = useState(null)
260
397
  const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
261
- const [approvedBuilds, setApprovedBuilds] = useState([])
262
398
  const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
263
399
 
400
+ const connectionErrorMessage = (result, status) => {
401
+ const message = result?.message || `请求失败(${status})`
402
+ if (!['LEGACY_DEVICE_REVOKE_REQUIRED', 'ANT_CONNECTION_CLEANUP_REQUIRED'].includes(result?.code)) return message
403
+ setAllowLocalCleanup(true)
404
+ const deviceId = savedDeviceId()
405
+ return deviceId
406
+ ? `${message} 设备编号:${deviceId}。请把编号提供给原团队管理员。`
407
+ : `${message} 本机无法确认旧设备编号;请让原团队管理员按成员查看设备清单,勿用新编号猜测。`
408
+ }
409
+
264
410
  const inspectServiceCompatibility = async (rawServiceUrl, useSavedConnection = false) => {
265
411
  const normalized = normalizeServiceUrl(rawServiceUrl)
266
412
  if (!normalized && !useSavedConnection) {
@@ -374,10 +520,12 @@ window.__ModuleLoader__.load({
374
520
 
375
521
  const checkBundleUpdate = async () => {
376
522
  setBundleUpdate({ state: 'checking', currentVersion: '', message: '正在检查 ANT 更新…' })
377
- setApprovedBuilds([])
378
523
  try {
379
524
  const response = await fetch('/api/ant-teams/bundle-release', {
380
- cache: 'no-store', headers: { 'x-ant-bundle-capabilities': 'direct-package-install' },
525
+ cache: 'no-store', headers: {
526
+ 'x-ant-bundle-capabilities': 'direct-package-install',
527
+ 'x-ant-bundle-update-mode': 'check-only',
528
+ },
381
529
  })
382
530
  const release = await readLocalJsonResponse(response)
383
531
  if (!response.ok) throw new Error(release.message || `检查失败(${response.status})`)
@@ -425,99 +573,55 @@ window.__ModuleLoader__.load({
425
573
  }
426
574
  }
427
575
 
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
433
- const manager = remote?.pluginManager
434
- if (typeof manager?.installBundle !== 'function') {
435
- setBundleUpdate((current) => ({ ...current, state: 'failed', message: '当前 DSH 版本未提供官方 Bundle 安装接口。' }))
436
- return
437
- }
438
- const scripts = allowScripts ? approvedBuilds : []
439
- if (scripts.length && !window.confirm(`DSH 安装需要运行以下依赖的安装脚本:\n\n${scripts.join('\n')}\n\n仅在信任这些依赖时继续。`)) return
440
- setBusy(true)
441
- setApprovedBuilds([])
442
- setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在委托 DSH 官方管理器安装更新…' }))
443
- try {
444
- const result = await manager.installBundle(installSpec, {
445
- enabled: true,
446
- ...(scripts.length ? { approvedBuilds: scripts } : {}),
447
- })
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
461
- }
462
- if (outcome.application === 'cancelled') {
463
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已取消更新,当前运行版本未切换。' })
464
- return
465
- }
466
- if (outcome.application === 'overridden') {
467
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已记录更新,但其他插件配置覆盖了 ANT。请检查 DSH 插件配置后再重启。' })
468
- return
469
- }
470
- if (outcome.bundle !== bundleRelease.package_name) {
471
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 返回的安装包名称与 ANT 发布信息不一致。请联系管理员核对发布包。' })
472
- return
473
- }
474
- setBundleUpdate({
475
- state: 'done', currentVersion: bundleRelease.version,
476
- message: outcome.application === 'restart-required'
477
- ? `DSH 已安装 ${installSpec}。请完全退出并重新启动 DSH 后生效。`
478
- : `DSH 已安装 ${installSpec}。`,
479
- })
480
- } catch (error) {
481
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: error instanceof Error ? error.message : 'DSH 未能安装 ANT 更新。' })
482
- } finally {
483
- setBusy(false)
484
- }
576
+ const clearLocalConnection = async () => {
577
+ if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
578
+ const settingsResult = await remote.settings.mutate('llm-pi-ai', [
579
+ { op: 'unset', path: ['providers', 'ant-team-openai'] },
580
+ { op: 'unset', path: ['providers', 'ant-team-anthropic'] },
581
+ ], undefined)
582
+ if (!settingsResult.ok) throw new Error(settingsResult.error?.message || 'ANT 模型设置未能清除。')
583
+ if (typeof remote?.credentials?.unset !== 'function') throw new Error('DSH 未提供凭证删除接口。')
584
+ const credentialResult = await remote.credentials.unset('ANT_MODEL_API_KEY')
585
+ if (!credentialResult.ok) throw new Error(credentialResult.error?.message || 'ANT 成员凭证未能清除。')
586
+ setCredentialConfigured(false)
587
+ setNeedsReconnect(false)
588
+ setReauthenticate(false)
589
+ setAllowLocalCleanup(false)
485
590
  }
486
591
 
487
592
  const disconnect = async () => {
488
593
  setBusy(true)
489
594
  setMessage('正在断开此设备的 ANT 连接…')
490
595
  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 模型设置未能清除。')
596
+ const response = await fetch('/api/ant-teams/disconnect', { method: 'POST' })
597
+ const result = await readLocalJsonResponse(response)
598
+ if (!response.ok || result.revoked !== true) {
599
+ throw new Error(connectionErrorMessage(result, response.status))
600
+ }
497
601
  } catch (error) {
602
+ setAllowLocalCleanup(true)
498
603
  setBusy(false)
499
- setMessage(`无法清除本机 ANT 模型设置,因此连接仍保留。${error instanceof Error ? error.message : '请稍后重试。'}`)
604
+ setMessage(`未清除本机凭证和模型设置:${error instanceof Error ? error.message : '请稍后重试。'}`)
500
605
  return
501
606
  }
502
- let serverRevoked = false
503
607
  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
608
+ await clearLocalConnection()
609
+ setMessage('此设备的 ANT 凭证已撤销,ANT 模型设置已清除。团队服务地址已保留。')
610
+ } catch (error) {
611
+ setAllowLocalCleanup(true)
612
+ setMessage(`服务端已撤销设备令牌,但本机清理失败:${error instanceof Error ? error.message : '请稍后重试'}。可选择仅清理本机。`)
613
+ } finally { setBusy(false) }
614
+ }
615
+
616
+ const clearOnlyLocal = async () => {
617
+ if (!window.confirm('仅清理这台 DSH 的 ANT 凭证和模型设置,不保证服务端设备令牌已撤销。请先让原团队管理员确认远端撤销;否则令牌可能仍可被使用。确定继续吗?')) return
618
+ setBusy(true)
509
619
  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)
620
+ await clearLocalConnection()
621
+ setMessage('已仅清理本机 ANT 凭证和模型设置。此操作未确认服务端撤销,请向原团队管理员核对。')
515
622
  } 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 秒内到期。团队服务地址已保留。')
623
+ setMessage(`本机清理未完成:${error instanceof Error ? error.message : '请稍后重试。'}`)
624
+ } finally { setBusy(false) }
521
625
  }
522
626
 
523
627
  useEffect(() => {
@@ -555,6 +659,13 @@ window.__ModuleLoader__.load({
555
659
  setMessage('请填写工作邮箱。')
556
660
  return
557
661
  }
662
+ if (credentialConfigured !== false && !savedDeviceId()) {
663
+ if (credentialConfigured === true) setAllowLocalCleanup(true)
664
+ setMessage(credentialConfigured === true
665
+ ? '旧 ANT 凭证仍在,但本机无法确认它绑定的设备编号。请让原团队管理员按成员核查并撤销旧设备,再仅清理本机。'
666
+ : '尚未确认本机凭证状态,也没有已保存的设备编号。请等待检查完成,或修复 DSH 凭证接口后重试。')
667
+ return
668
+ }
558
669
  setBusy(true)
559
670
  setMessage('正在发送邮箱验证码…')
560
671
  try {
@@ -566,7 +677,7 @@ window.__ModuleLoader__.load({
566
677
  body: JSON.stringify({ service_url: normalized, email: normalizedEmail }),
567
678
  })
568
679
  const result = await readLocalJsonResponse(response)
569
- if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
680
+ if (!response.ok) throw new Error(connectionErrorMessage(result, response.status))
570
681
  setEmail(normalizedEmail)
571
682
  setCodeRequested(true)
572
683
  setMessage(result.message || '如果该邮箱符合团队规则,验证码将发送至邮箱。')
@@ -586,6 +697,13 @@ window.__ModuleLoader__.load({
586
697
  setMessage('请填写有效的服务地址、工作邮箱和 8 位验证码。')
587
698
  return
588
699
  }
700
+ if (credentialConfigured !== false && !savedDeviceId()) {
701
+ if (credentialConfigured === true) setAllowLocalCleanup(true)
702
+ setMessage(credentialConfigured === true
703
+ ? '旧 ANT 凭证仍在,但本机无法确认旧设备编号。请先联系原团队管理员撤销旧设备,再仅清理本机。'
704
+ : '尚未确认本机凭证状态,也没有已保存的设备编号。请等待检查完成,或修复 DSH 凭证接口后重试。')
705
+ return
706
+ }
589
707
  setBusy(true)
590
708
  setMessage('正在验证邮箱并同步团队模型…')
591
709
  try {
@@ -602,7 +720,7 @@ window.__ModuleLoader__.load({
602
720
  }),
603
721
  })
604
722
  const result = await readLocalJsonResponse(response)
605
- if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
723
+ if (!response.ok) throw new Error(connectionErrorMessage(result, response.status))
606
724
  if (typeof result.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
607
725
  throw new Error('ANT 返回的连接信息格式无效。')
608
726
  }
@@ -616,6 +734,7 @@ window.__ModuleLoader__.load({
616
734
  onServiceUrlChange(normalized)
617
735
  setCodeRequested(false)
618
736
  setCredentialConfigured(true)
737
+ setAllowLocalCleanup(false)
619
738
  setNeedsReconnect(false)
620
739
  setReauthenticate(false)
621
740
  setMessage(`已连接 ${result.email || normalizedEmail},同步 ${result.models.length} 个获准模型。它们现已出现在 DSH 模型选择器中。`)
@@ -659,12 +778,9 @@ window.__ModuleLoader__.load({
659
778
  h('button', { type: 'button', disabled: busy || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
660
779
  bundleUpdate.message ? h('p', { role: 'status', style: mutedTextStyle }, bundleUpdate.message) : null,
661
780
  bundleRelease?.version && bundleUpdate.state === 'available' ? h('div', { style: { display: 'grid', gap: 8 } },
662
- h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · npm:${directInstallSpec(bundleRelease)}`),
781
+ h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · 精确包名:${directInstallSpec(bundleRelease)}`),
663
782
  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,
783
+ h('p', { style: mutedTextStyle }, 'ANT 暂停一键安装更新:当前 DSH 无法在执行安装脚本前核对审核制品。请联系团队管理员确认升级方式。')) : null) : null,
668
784
  showConnectForm ? h('form', { onSubmit: requestCode, style: { display: 'grid', gap: 9, maxWidth: 560 } },
669
785
  h('label', { htmlFor: 'ant-team-model-email', style: { fontSize: 13, fontWeight: 550 } }, '工作邮箱'),
670
786
  h('input', {
@@ -685,6 +801,23 @@ window.__ModuleLoader__.load({
685
801
  onChange: (event) => setCode(event.target.value.replace(/\D/gu, '').slice(0, 8)), style: inputStyle,
686
802
  }),
687
803
  h('button', { type: 'submit', disabled: busy || code.length !== 8, style: primaryButtonStyle }, busy ? '连接中…' : '验证并连接')) : null,
804
+ allowLocalCleanup ? h('section', { style: { ...panelStyle, padding: 14, gap: 9 } },
805
+ h('p', { style: mutedTextStyle }, savedDeviceId()
806
+ ? `已保存的设备编号:${savedDeviceId()}。请交给原团队管理员核对。`
807
+ : '本机无法确认旧设备编号。请让原团队管理员按成员查看设备清单,勿用新编号猜测。'),
808
+ h('p', { style: mutedTextStyle }, '仅清理本机不会确认服务端令牌已撤销;请先取得管理员确认。'),
809
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
810
+ h('button', { type: 'button', disabled: busy, onClick: () => void clearOnlyLocal(), style: secondaryButtonStyle }, '仅清理本机'),
811
+ h('button', { type: 'button', disabled: busy || !savedDeviceId(), onClick: async () => {
812
+ try {
813
+ const deviceId = savedDeviceId()
814
+ if (!deviceId) throw new Error('device id unavailable')
815
+ if (typeof window.navigator?.clipboard?.writeText !== 'function') throw new Error('clipboard unavailable')
816
+ await window.navigator.clipboard.writeText(deviceId)
817
+ setMessage('设备编号已复制,请发给原团队管理员。')
818
+ }
819
+ catch { setMessage(savedDeviceId() ? `请复制设备编号:${savedDeviceId()}` : '本机无法确认旧设备编号,请联系原团队管理员核查。') }
820
+ }, style: secondaryButtonStyle }, '复制设备编号'))) : null,
688
821
  message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null))
689
822
 
690
823
  }
@@ -696,14 +829,28 @@ window.__ModuleLoader__.load({
696
829
  pluginManager: ctx.remote.pluginManager,
697
830
  }
698
831
  ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: 'ant-skills' },
699
- () => h(SkillCenterPage, { onBack: () => ctx.pluginNavigation.openBundle(ANT_BUNDLE_PACKAGE_NAME) })))
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, {
838
+ remote,
839
+ onBack: () => ctx.pluginNavigation.openBundle(ANT_BUNDLE_PACKAGE_NAME),
840
+ onSkills: () => ctx.layout.selectPanel('ant-skills'),
841
+ openBundle: (name) => ctx.pluginNavigation.openBundle(name),
842
+ })))
700
843
  ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
701
844
  name: 'plugins.bundle.config',
702
845
  key: ANT_BUNDLE_PACKAGE_NAME,
703
846
  inject: () => ({ remote }),
704
847
  }, ({ view, remote: injectedRemote }) => view === 'summary'
705
848
  ? h('p', null, 'ANT 团队服务入口。')
706
- : h(TeamServiceConfig, { remote: injectedRemote, openSkillCenter: () => ctx.layout.selectPanel('ant-skills') })))
849
+ : h(TeamServiceConfig, {
850
+ remote: injectedRemote,
851
+ openSkillCenter: () => ctx.layout.selectPanel('ant-skills'),
852
+ openPluginCenter: () => ctx.layout.selectPanel('ant-plugins'),
853
+ })))
707
854
  }
708
855
 
709
856
  Object.assign(exports, { inject, apply })
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export const name = 'ant'
2
2
 
3
+ import { createHash } from 'node:crypto'
3
4
  import { installSkill, listSkills, uninstallSkill } from './skill-install.js'
4
5
 
5
6
  export const inject = []
@@ -13,6 +14,7 @@ const CONNECT_PATH = '/api/ant-teams/connect'
13
14
  const SKILLS_PATH = '/api/ant-teams/skills'
14
15
  const SKILL_INSTALL_PATH = '/api/ant-teams/skills/install'
15
16
  const SKILL_UNINSTALL_PATH = '/api/ant-teams/skills/uninstall'
17
+ const PLUGINS_PATH = '/api/ant-teams/plugins'
16
18
  const ANT_NPM_PACKAGE_NAME = '@ai-wendao/ant'
17
19
  const MAX_MODEL_RESPONSE_BYTES = 1024 * 1024
18
20
  const MODEL_TOKEN_REF = 'ANT_MODEL_API_KEY'
@@ -34,28 +36,50 @@ function normalizeServiceUrl(value) {
34
36
  return url.origin
35
37
  }
36
38
 
37
- function serviceUrlFromModelSettings(settings) {
39
+ function modelServiceBinding(settings) {
38
40
  const descriptor = settings.describe().find((entry) => entry.ns === 'llm-pi-ai')
39
41
  const providers = descriptor?.value?.providers
40
- if (!providers || typeof providers !== 'object') return null
42
+ if (!providers || typeof providers !== 'object') return { configured: false, serviceUrl: null }
41
43
  const origins = new Set()
44
+ let configured = false
42
45
  for (const provider of Object.values(providers)) {
43
- if (provider?.apiKeyEnv !== MODEL_TOKEN_REF || typeof provider.baseURL !== 'string') continue
46
+ if (provider?.apiKeyEnv !== MODEL_TOKEN_REF) continue
47
+ configured = true
48
+ if (typeof provider.baseURL !== 'string') return { configured, serviceUrl: null }
44
49
  let url
45
- try { url = new URL(provider.baseURL) } catch { return null }
50
+ try { url = new URL(provider.baseURL) } catch { return { configured, serviceUrl: null } }
46
51
  const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
47
52
  if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password || url.search || url.hash
48
- || !['/model-gateway/openai/v1', '/model-gateway/anthropic'].includes(url.pathname)) return null
53
+ || !['/model-gateway/openai/v1', '/model-gateway/anthropic'].includes(url.pathname)) {
54
+ return { configured, serviceUrl: null }
55
+ }
49
56
  origins.add(url.origin)
50
57
  }
51
- return origins.size === 1 ? [...origins][0] : null
58
+ return { configured, serviceUrl: origins.size === 1 ? [...origins][0] : null }
59
+ }
60
+
61
+ function tokenHash(token) {
62
+ return createHash('sha256').update(token).digest('hex')
52
63
  }
53
64
 
54
- async function pinServiceUrl(credentials, serviceUrl) {
55
- await credentials.modifyRecord(SERVICE_RECORD_KEY, async () => ({
56
- kind: 'grant',
57
- payload: { service_url: serviceUrl },
58
- }))
65
+ async function pinServiceUrl(credentials, serviceUrl, token, previousToken = null) {
66
+ const previousHash = previousToken ? tokenHash(previousToken) : null
67
+ await credentials.modifyRecord(SERVICE_RECORD_KEY, async (current) => {
68
+ if (previousHash && (current?.kind !== 'grant'
69
+ || normalizeServiceUrl(current.payload?.service_url) !== serviceUrl
70
+ || (current.payload?.token_sha256 && current.payload.token_sha256 !== previousHash
71
+ && current.payload?.previous_token_sha256 !== previousHash))) {
72
+ throw new Error('当前 ANT 凭证绑定已变化,请重试。')
73
+ }
74
+ return {
75
+ kind: 'grant',
76
+ payload: {
77
+ service_url: serviceUrl,
78
+ token_sha256: tokenHash(token),
79
+ ...(previousHash && previousHash !== tokenHash(token) ? { previous_token_sha256: previousHash } : {}),
80
+ },
81
+ }
82
+ })
59
83
  }
60
84
 
61
85
  async function readMemberConnection(credentials, settings) {
@@ -66,8 +90,15 @@ async function readMemberConnection(credentials, settings) {
66
90
  const pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
67
91
  ? normalizeServiceUrl(record.payload.service_url)
68
92
  : null
69
- const serviceUrl = pinnedUrl || serviceUrlFromModelSettings(settings)
70
- return serviceUrl ? { serviceUrl, token } : null
93
+ if (!pinnedUrl) return null
94
+ const binding = modelServiceBinding(settings)
95
+ if (pinnedUrl && binding.configured && binding.serviceUrl !== pinnedUrl) return null
96
+ if (pinnedUrl && typeof record.payload?.token_sha256 === 'string') {
97
+ const currentHash = tokenHash(token)
98
+ if (record.payload.token_sha256 !== currentHash
99
+ && record.payload.previous_token_sha256 !== currentHash) return null
100
+ } else if (pinnedUrl && binding.serviceUrl !== pinnedUrl) return null
101
+ return { serviceUrl: pinnedUrl, token }
71
102
  }
72
103
 
73
104
  async function updateModelSession(serviceUrl, token, action) {
@@ -109,29 +140,23 @@ async function readLimitedBody(response) {
109
140
 
110
141
  async function fetchMemberModels(request, credentials, settings) {
111
142
  if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
112
- let token
113
- let pinnedUrl
114
- let serviceUrl
143
+ let connection
115
144
  try {
116
- token = await credentials.resolve(MODEL_TOKEN_REF)
117
- const record = await credentials.readRecord(SERVICE_RECORD_KEY)
118
- pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
119
- ? normalizeServiceUrl(record.payload.service_url)
120
- : null
121
- serviceUrl = pinnedUrl || serviceUrlFromModelSettings(settings)
145
+ connection = await readMemberConnection(credentials, settings)
122
146
  } catch {
123
147
  return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
124
148
  }
125
- if (!token || !serviceUrl) return jsonResponse(401, { message: '当前 DSH 未连接 ANT,请验证工作邮箱。' })
149
+ if (!connection) return jsonResponse(401, { message: '当前 DSH 未连接 ANT,请验证工作邮箱。' })
150
+ const { serviceUrl, token } = connection
126
151
 
127
152
  try {
128
- const session = await updateModelSession(serviceUrl, token.value, 'heartbeat')
153
+ const session = await updateModelSession(serviceUrl, token, 'heartbeat')
129
154
  if (session.status === 401 || session.status === 403) {
130
155
  return jsonResponse(401, { message: 'ANT 成员凭证已失效,请重新验证工作邮箱。' })
131
156
  }
132
157
  if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
133
158
  const upstream = await fetch(new URL('/api/me/models', serviceUrl), {
134
- headers: { authorization: `Bearer ${token.value}`, accept: 'application/json' },
159
+ headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
135
160
  redirect: 'manual',
136
161
  signal: AbortSignal.timeout(10_000),
137
162
  })
@@ -146,11 +171,6 @@ async function fetchMemberModels(request, credentials, settings) {
146
171
  if (!Array.isArray(result?.models) || normalizeServiceUrl(result.service_url) !== serviceUrl) {
147
172
  return jsonResponse(502, { message: 'ANT 团队模型目录与已连接服务不匹配。' })
148
173
  }
149
- if (!pinnedUrl) {
150
- try { await pinServiceUrl(credentials, serviceUrl) } catch {
151
- return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
152
- }
153
- }
154
174
  return jsonResponse(200, { service_url: serviceUrl, models: result.models })
155
175
  } catch {
156
176
  return jsonResponse(502, { message: '无法连接 ANT 团队服务' })
@@ -246,6 +266,9 @@ async function fetchBundleRelease(request, credentials, settings) {
246
266
  if (request.headers.get('x-ant-bundle-capabilities') === 'direct-package-install') {
247
267
  headers['x-ant-bundle-capabilities'] = 'direct-package-install'
248
268
  }
269
+ if (request.headers.get('x-ant-bundle-update-mode') === 'check-only') {
270
+ headers['x-ant-bundle-update-mode'] = 'check-only'
271
+ }
249
272
  const upstream = await fetch(new URL('/api/me/bundle-release', connection.serviceUrl), {
250
273
  headers,
251
274
  redirect: 'manual',
@@ -255,7 +278,7 @@ async function fetchBundleRelease(request, credentials, settings) {
255
278
  if (text === null) return jsonResponse(502, { message: 'ANT 更新信息响应过大。' })
256
279
  if (upstream.status === 401 || upstream.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
257
280
  if (upstream.status === 426) {
258
- return jsonResponse(426, { message: '当前 ANT 插件版本不能安全自动安装 scoped Bundle。请通过 DSH 官方插件管理器完成一次包名迁移。' })
281
+ return jsonResponse(426, { message: '当前 ANT 插件仍可能自动安装未经核验的制品。请联系团队管理员采用受控升级方式;旧 alias 还需在 DSH 插件管理器中迁移。' })
259
282
  }
260
283
  if (!upstream.ok) return jsonResponse(502, { message: 'ANT 暂时无法提供插件更新信息。' })
261
284
  let release
@@ -288,7 +311,10 @@ async function disconnectMember(request, credentials, settings) {
288
311
  try { connection = await readMemberConnection(credentials, settings) } catch {
289
312
  return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
290
313
  }
291
- if (!connection) return jsonResponse(200, { revoked: false })
314
+ if (!connection) return jsonResponse(409, {
315
+ code: 'LEGACY_DEVICE_REVOKE_REQUIRED',
316
+ message: '无法确认旧凭证所属服务或当前设备令牌。请先让原团队管理员撤销此设备,再仅清理本机。',
317
+ })
292
318
  try {
293
319
  const response = await fetch(new URL('/auth/dsh-session/disconnect', connection.serviceUrl), {
294
320
  method: 'POST',
@@ -296,12 +322,13 @@ async function disconnectMember(request, credentials, settings) {
296
322
  redirect: 'manual',
297
323
  signal: AbortSignal.timeout(10_000),
298
324
  })
299
- if (response.status === 204 || response.status === 401 || response.status === 403) {
300
- return jsonResponse(200, { revoked: response.status === 204 })
325
+ if (response.status === 204) return jsonResponse(200, { revoked: true })
326
+ if (response.status === 401 || response.status === 403) {
327
+ return jsonResponse(409, { message: '服务端未确认撤销当前令牌。它可能已被停用或更换;请让原团队管理员确认远端状态,再选择仅清理本机。' })
301
328
  }
302
- return jsonResponse(200, { revoked: false })
329
+ return jsonResponse(502, { message: '服务端未确认永久撤销,本机凭证仍保留。请稍后重试。' })
303
330
  } catch {
304
- return jsonResponse(200, { revoked: false })
331
+ return jsonResponse(502, { message: '无法连接团队服务确认永久撤销,本机凭证仍保留。请稍后重试。' })
305
332
  }
306
333
  }
307
334
 
@@ -313,18 +340,18 @@ async function memberSkillConnection(credentials, settings) {
313
340
  return connection
314
341
  }
315
342
 
316
- async function fetchMemberSkills(request, credentials, settings, registry) {
343
+ async function fetchMemberSkills(request, credentials, settings) {
317
344
  if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
318
345
  try {
319
346
  const connection = await memberSkillConnection(credentials, settings)
320
347
  if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
321
- return jsonResponse(200, { skills: await listSkills(connection, registry) })
348
+ return jsonResponse(200, { skills: await listSkills(connection) })
322
349
  } catch (error) {
323
350
  return jsonResponse(502, { message: error instanceof Error ? error.message : '无法读取企业精选技能。' })
324
351
  }
325
352
  }
326
353
 
327
- async function installMemberSkill(request, credentials, settings, registry) {
354
+ async function installMemberSkill(request, credentials, settings) {
328
355
  if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
329
356
  let body
330
357
  try {
@@ -335,7 +362,7 @@ async function installMemberSkill(request, credentials, settings, registry) {
335
362
  try {
336
363
  const connection = await memberSkillConnection(credentials, settings)
337
364
  if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
338
- return jsonResponse(200, await installSkill(connection, body?.skill_id, registry))
365
+ return jsonResponse(200, await installSkill(connection, body?.skill_id))
339
366
  } catch (error) {
340
367
  return jsonResponse(409, { message: error instanceof Error ? error.message : '技能安装失败。' })
341
368
  }
@@ -355,7 +382,28 @@ async function uninstallMemberSkill(request) {
355
382
  }
356
383
  }
357
384
 
358
- async function fetchAuthRequest(request, endpoint, credentials) {
385
+ async function fetchMemberPlugins(request, credentials, settings) {
386
+ if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
387
+ try {
388
+ const connection = await memberSkillConnection(credentials, settings)
389
+ if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
390
+ const upstream = await fetch(new URL('/api/me/plugins', connection.serviceUrl), {
391
+ headers: { authorization: `Bearer ${connection.token}`, accept: 'application/json' },
392
+ redirect: 'manual', signal: AbortSignal.timeout(10_000),
393
+ })
394
+ const text = await readLimitedBody(upstream)
395
+ if (text === null) return jsonResponse(502, { message: '插件目录响应过大。' })
396
+ if (!upstream.ok) return jsonResponse(upstream.status === 401 || upstream.status === 403 ? 401 : 502,
397
+ { message: '无法读取企业精选插件目录。' })
398
+ const plugins = JSON.parse(text)
399
+ if (!Array.isArray(plugins)) throw new Error('插件目录格式无效。')
400
+ return jsonResponse(200, { plugins })
401
+ } catch {
402
+ return jsonResponse(502, { message: '无法读取企业精选插件目录。' })
403
+ }
404
+ }
405
+
406
+ async function fetchAuthRequest(request, endpoint, credentials, settings) {
359
407
  if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
360
408
  let body
361
409
  try { body = await request.json() } catch { return jsonResponse(400, { message: '请求格式无效' }) }
@@ -369,6 +417,33 @@ async function fetchAuthRequest(request, endpoint, credentials) {
369
417
  return jsonResponse(400, { message: '团队服务地址或邮箱验证信息无效' })
370
418
  }
371
419
 
420
+ let previousToken = null
421
+ try {
422
+ const existing = await credentials.resolve(MODEL_TOKEN_REF)
423
+ const current = existing?.value ? await readMemberConnection(credentials, settings) : null
424
+ const binding = modelServiceBinding(settings)
425
+ const record = existing?.value ? await credentials.readRecord(SERVICE_RECORD_KEY) : null
426
+ const pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
427
+ ? normalizeServiceUrl(record.payload.service_url) : null
428
+ if (existing?.value && !pinnedUrl && !binding.configured) {
429
+ return jsonResponse(409, {
430
+ code: 'LEGACY_DEVICE_REVOKE_REQUIRED',
431
+ message: '旧 ANT 凭证没有可核实的团队服务地址。请先让原团队管理员撤销此设备,再仅清理本机并连接新团队。',
432
+ })
433
+ }
434
+ if ((existing?.value && pinnedUrl && pinnedUrl !== serviceUrl)
435
+ || (current && current.serviceUrl !== serviceUrl)
436
+ || (binding.configured && binding.serviceUrl !== serviceUrl)) {
437
+ return jsonResponse(409, {
438
+ code: 'ANT_CONNECTION_CLEANUP_REQUIRED',
439
+ message: '本机仍有旧团队的凭证或模型设置。请先让原团队管理员确认旧设备已撤销,再清理本机配置后连接。',
440
+ })
441
+ }
442
+ if (endpoint === 'dsh-connect' && current?.serviceUrl === serviceUrl) previousToken = existing.value
443
+ } catch {
444
+ return jsonResponse(500, { message: 'DSH 无法确认当前 ANT 凭证和服务地址。' })
445
+ }
446
+
372
447
  try {
373
448
  const upstream = await fetch(new URL(`/auth/${endpoint}`, serviceUrl), {
374
449
  method: 'POST',
@@ -389,17 +464,17 @@ async function fetchAuthRequest(request, endpoint, credentials) {
389
464
  if (typeof result?.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
390
465
  return jsonResponse(502, { message: 'ANT 连接响应格式无效' })
391
466
  }
392
- try {
393
- await pinServiceUrl(credentials, serviceUrl)
394
- } catch {
395
- return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
396
- }
397
467
  try {
398
468
  const session = await updateModelSession(serviceUrl, result.token, 'heartbeat')
399
469
  if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
400
470
  } catch {
401
471
  return jsonResponse(502, { message: '无法连接 ANT 模型会话服务。' })
402
472
  }
473
+ try {
474
+ await pinServiceUrl(credentials, serviceUrl, result.token, previousToken)
475
+ } catch {
476
+ return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
477
+ }
403
478
  }
404
479
  return new Response(text, {
405
480
  status: upstream.status,
@@ -411,8 +486,6 @@ async function fetchAuthRequest(request, endpoint, credentials) {
411
486
  }
412
487
 
413
488
  export function apply(ctx) {
414
- let skillRegistry
415
- ctx.inject(['skills'], (skillCtx) => { skillRegistry = skillCtx.skills })
416
489
  ctx.inject(['connection', 'credentials', 'settings'], (connectionCtx) => {
417
490
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
418
491
  path: SERVICE_INFO_PATH,
@@ -434,16 +507,20 @@ export function apply(ctx) {
434
507
  }), 'ant-teams: authenticated Bundle release relay')
435
508
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
436
509
  path: SKILLS_PATH, methods: ['GET'], requestBody: 'buffered',
437
- fetch: (request) => fetchMemberSkills(request, connectionCtx.credentials, connectionCtx.settings, skillRegistry),
510
+ fetch: (request) => fetchMemberSkills(request, connectionCtx.credentials, connectionCtx.settings),
438
511
  }), 'ant-teams: enterprise skill catalog')
439
512
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
440
513
  path: SKILL_INSTALL_PATH, methods: ['POST'], requestBody: 'buffered',
441
- fetch: (request) => installMemberSkill(request, connectionCtx.credentials, connectionCtx.settings, skillRegistry),
514
+ fetch: (request) => installMemberSkill(request, connectionCtx.credentials, connectionCtx.settings),
442
515
  }), 'ant-teams: install approved skill into DSH')
443
516
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
444
517
  path: SKILL_UNINSTALL_PATH, methods: ['POST'], requestBody: 'buffered',
445
518
  fetch: (request) => uninstallMemberSkill(request),
446
519
  }), 'ant-teams: remove managed DSH skill')
520
+ connectionCtx.effect(() => connectionCtx.connection.fetch.register({
521
+ path: PLUGINS_PATH, methods: ['GET'], requestBody: 'buffered',
522
+ fetch: (request) => fetchMemberPlugins(request, connectionCtx.credentials, connectionCtx.settings),
523
+ }), 'ant-teams: enterprise plugin catalog')
447
524
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
448
525
  path: DISCONNECT_PATH,
449
526
  methods: ['POST'],
@@ -453,13 +530,13 @@ export function apply(ctx) {
453
530
  path: REQUEST_CODE_PATH,
454
531
  methods: ['POST'],
455
532
  requestBody: 'buffered',
456
- fetch: (request) => fetchAuthRequest(request, 'request-code', connectionCtx.credentials),
533
+ fetch: (request) => fetchAuthRequest(request, 'request-code', connectionCtx.credentials, connectionCtx.settings),
457
534
  }), 'ant-teams: relay email verification code request')
458
535
  connectionCtx.effect(() => connectionCtx.connection.fetch.register({
459
536
  path: CONNECT_PATH,
460
537
  methods: ['POST'],
461
538
  requestBody: 'buffered',
462
- fetch: (request) => fetchAuthRequest(request, 'dsh-connect', connectionCtx.credentials),
539
+ fetch: (request) => fetchAuthRequest(request, 'dsh-connect', connectionCtx.credentials, connectionCtx.settings),
463
540
  }), 'ant-teams: relay DSH account connection')
464
541
  connectionCtx.effect(() => {
465
542
  let stopped = false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-wendao/ant",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
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": {
package/skill-install.js CHANGED
@@ -12,19 +12,6 @@ const MAX_PACKAGE_BYTES = 80 * 1024 * 1024
12
12
  const MAX_JSON_BYTES = 4 * 1024 * 1024
13
13
  const activeSkills = new Set()
14
14
 
15
- async function assertDiscovered(registry, root, name) {
16
- if (typeof registry?.list !== 'function') throw new Error('DSH 技能服务不可用,无法确认安装结果。')
17
- const expected = resolve(root, name, 'SKILL.md')
18
- for (let attempt = 0; attempt < 6; attempt++) {
19
- const entries = await registry.list()
20
- if (Array.isArray(entries) && entries.some((entry) => entry.name === name
21
- && typeof entry.path === 'string'
22
- && (process.platform === 'win32' ? resolve(entry.path).toLowerCase() === expected.toLowerCase() : resolve(entry.path) === expected))) return
23
- if (attempt < 5) await new Promise((done) => setTimeout(done, 300))
24
- }
25
- throw new Error('DSH 没有发现此技能;请检查 DSH 技能目录配置。')
26
- }
27
-
28
15
  const hash = (bytes) => createHash('sha256').update(bytes).digest('hex')
29
16
  const safeName = (name) => typeof name === 'string' && /^ant-[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) && name.length <= 64
30
17
  const safePath = (name) => typeof name === 'string' && name.length > 0 && name.length <= 1024
@@ -164,7 +151,7 @@ async function fetchJson(url, token) {
164
151
  try { return JSON.parse(bytes.toString('utf8')) } catch { throw new Error('团队技能目录格式无效。') }
165
152
  }
166
153
 
167
- export async function listSkills(connection, registry) {
154
+ export async function listSkills(connection) {
168
155
  const skills = await fetchJson(new URL('/api/me/skills', connection.serviceUrl), connection.token)
169
156
  if (!Array.isArray(skills)) throw new Error('团队技能目录格式无效。')
170
157
  const { root, conflicts } = await safeRoot()
@@ -180,10 +167,6 @@ export async function listSkills(connection, registry) {
180
167
  local_status = marker.content_sha256 === skill.content_sha256 ? 'installed' : 'update-available'
181
168
  }
182
169
  } catch { local_status = 'conflict' }
183
- if (local_status === 'installed' || local_status === 'update-available') {
184
- try { await assertDiscovered(registry, root, skill.dsh_name) }
185
- catch { local_status = 'not-discovered' }
186
- }
187
170
  return { ...skill, local_status }
188
171
  }))
189
172
  }
@@ -220,7 +203,7 @@ function contentDigest(files) {
220
203
  return digest.digest('hex')
221
204
  }
222
205
 
223
- export async function installSkill(connection, skillId, registry) {
206
+ export async function installSkill(connection, skillId) {
224
207
  if (typeof skillId !== 'string' || !/^[0-9a-f-]{36}$/iu.test(skillId)) throw new Error('技能编号无效。')
225
208
  const manifest = await fetchJson(new URL(`/api/me/skills/${skillId}/files`, connection.serviceUrl), connection.token)
226
209
  validateManifest(manifest, skillId)
@@ -228,18 +211,17 @@ export async function installSkill(connection, skillId, registry) {
228
211
  if (conflicts.has(manifest.dsh_name)) throw new Error('本机技能备份已损坏;请联系管理员处理,ANT 不会覆盖。')
229
212
  if (activeSkills.has(manifest.dsh_name)) throw new Error('这个技能正在安装或卸载,请稍后重试。')
230
213
  activeSkills.add(manifest.dsh_name)
231
- try { return await installValidatedSkill(connection, skillId, manifest, root, registry) }
214
+ try { return await installValidatedSkill(connection, skillId, manifest, root) }
232
215
  finally { activeSkills.delete(manifest.dsh_name) }
233
216
  }
234
217
 
235
- async function installValidatedSkill(connection, skillId, manifest, root, registry) {
218
+ async function installValidatedSkill(connection, skillId, manifest, root) {
236
219
  const target = join(root, manifest.dsh_name)
237
220
  const oldMarker = await readManaged(root, manifest.dsh_name)
238
221
  if (oldMarker) {
239
222
  if (oldMarker.service_url !== connection.serviceUrl) throw new Error('同名技能属于另一个 ANT 服务,无法覆盖。')
240
223
  await verifyManagedTree(root, manifest.dsh_name, oldMarker)
241
224
  if (oldMarker.content_sha256 === manifest.content_sha256) {
242
- await assertDiscovered(registry, root, manifest.dsh_name)
243
225
  return { status: 'installed', dsh_name: manifest.dsh_name }
244
226
  }
245
227
  }
@@ -268,8 +250,10 @@ async function installValidatedSkill(connection, skillId, manifest, root, regist
268
250
  const stagingStat = await isPresent(stagingRoot)
269
251
  if (stagingStat && (!stagingStat.isDirectory() || stagingStat.isSymbolicLink())) throw new Error('ANT 暂存目录不是普通目录。')
270
252
  await mkdir(stagingRoot, { recursive: true, mode: 0o700 })
271
- const staging = join(stagingRoot, `new-${randomUUID()}`)
253
+ const stagingName = `new-${randomUUID()}`
254
+ const staging = join(stagingRoot, stagingName)
272
255
  const backup = join(stagingRoot, `old-${manifest.dsh_name}-${randomUUID()}`)
256
+ const managedFiles = manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable }))
273
257
  await mkdir(staging, { mode: 0o700 })
274
258
  let backedUp = false
275
259
  let installedNew = false
@@ -283,29 +267,25 @@ async function installValidatedSkill(connection, skillId, manifest, root, regist
283
267
  await writeFile(join(staging, MARKER), JSON.stringify({
284
268
  dsh_name: manifest.dsh_name, skill_id: skillId, service_url: connection.serviceUrl,
285
269
  content_sha256: manifest.content_sha256,
286
- files: manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable })),
270
+ files: managedFiles,
287
271
  }), { flag: 'wx', mode: 0o600 })
272
+ await verifyManagedTree(stagingRoot, stagingName, { files: managedFiles })
288
273
  if (oldMarker) { await verifyManagedTree(root, manifest.dsh_name, oldMarker); await rename(target, backup); backedUp = true }
289
274
  else if (await isPresent(target)) throw new Error('同名技能已经存在,ANT 不会覆盖。')
290
275
  await rename(staging, target)
291
276
  installedNew = true
292
- await assertDiscovered(registry, root, manifest.dsh_name)
293
277
  if (backedUp) await rm(backup, { recursive: true }).catch(() => {})
294
278
  return { status: oldMarker ? 'updated' : 'installed', dsh_name: manifest.dsh_name }
295
279
  } catch (error) {
296
280
  if (backedUp && await isPresent(backup)) {
297
281
  if (await isPresent(target)) {
298
- await verifyManagedTree(root, manifest.dsh_name, {
299
- dsh_name: manifest.dsh_name,
300
- files: manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable })),
301
- }).then(() => rename(target, staging)).catch(() => {})
282
+ await verifyManagedTree(root, manifest.dsh_name, { files: managedFiles })
283
+ .then(() => rename(target, staging)).catch(() => {})
302
284
  }
303
285
  if (!(await isPresent(target))) await rename(backup, target).catch(() => {})
304
286
  } else if (installedNew) {
305
- await verifyManagedTree(root, manifest.dsh_name, {
306
- dsh_name: manifest.dsh_name,
307
- files: manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable })),
308
- }).then(() => rm(target, { recursive: true })).catch(() => {})
287
+ await verifyManagedTree(root, manifest.dsh_name, { files: managedFiles })
288
+ .then(() => rm(target, { recursive: true })).catch(() => {})
309
289
  }
310
290
  throw error
311
291
  } finally { await rm(staging, { recursive: true, force: true }).catch(() => {}) }
@@ -443,11 +443,13 @@ async function invokeDsh(cli, args, label, dshHome) {
443
443
  if (result.status !== 0) throw new Error(`${label}失败,退出码 ${String(result.status)}`)
444
444
  }
445
445
 
446
- async function rollbackWithCli(cli, profile, aliasSpec) {
446
+ async function rollbackWithCli(cli, profile, aliasSpec, targetVersion) {
447
447
  let current = await readProfile(profile.dshHome)
448
- if (current.state === 'both') {
449
- await invokeDsh(cli, ['remove', PACKAGE_NAME], '移除未完成的 scoped Bundle', profile.dshHome)
448
+ if (current.state !== 'both' || current.aliasSpec !== aliasSpec
449
+ || !directSpecMatchesTarget(current.directSpec, targetVersion)) {
450
+ throw new Error('scoped ANT 已与迁移记录目标不同;工具不会删除现有版本。')
450
451
  }
452
+ await invokeDsh(cli, ['remove', PACKAGE_NAME], '移除未完成的 scoped Bundle', profile.dshHome)
451
453
  current = await readProfile(profile.dshHome)
452
454
  if (current.aliasSpec === undefined) {
453
455
  await invokeDsh(cli, ['add', `${ALIAS_NAME}@${aliasSpec}`], '恢复旧 ANT alias', profile.dshHome)
@@ -595,6 +597,9 @@ async function rollback(options) {
595
597
  if (profile.state !== 'both' || profile.aliasSpec !== record.originalAliasSpec) {
596
598
  throw new Error('当前状态无法无损取消迁移;该工具不会重建旧 alias 或覆盖现有依赖。优先继续 migrate --apply。')
597
599
  }
600
+ if (!directSpecMatchesTarget(profile.directSpec, record.targetVersion)) {
601
+ throw new Error('scoped ANT 已与迁移记录目标不同;工具不会删除现有版本。')
602
+ }
598
603
  if (!options.apply) {
599
604
  process.stdout.write(`预检完成。确认 DSH 已完全退出后运行:ant-migrate rollback --backup "${absolute}" --apply\n`)
600
605
  return
@@ -607,10 +612,11 @@ async function rollback(options) {
607
612
  showDshOutput(listResult)
608
613
  if (listResult.error || listResult.status !== 0) throw new Error('DSH Desktop CLI 未能读取 desktop profile。')
609
614
  const beforeRollback = await readProfile(dshHome)
610
- if (beforeRollback.state !== 'both' || beforeRollback.aliasSpec !== record.originalAliasSpec) {
615
+ if (beforeRollback.state !== 'both' || beforeRollback.aliasSpec !== record.originalAliasSpec
616
+ || !directSpecMatchesTarget(beforeRollback.directSpec, record.targetVersion)) {
611
617
  throw new Error('执行回退前 profile 已变化;工具不会继续。')
612
618
  }
613
- await rollbackWithCli(cli, { dshHome }, record.originalAliasSpec)
619
+ await rollbackWithCli(cli, { dshHome }, record.originalAliasSpec, record.targetVersion)
614
620
  record.status = 'rolled-back'
615
621
  record.phase = 'rolled-back'
616
622
  await saveBackupRecord(absolute, record)