@ai-wendao/ant 0.3.11 → 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.11`,然后点击 **安装**。
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.11 -- ant-migrate inspect
63
- npm exec --yes --package @ai-wendao/ant@0.3.11 -- 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.11 -- 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.11 -- 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.11 -- ant-migrate rollback --backup "<备份目录>"
80
- npm exec --yes --package @ai-wendao/ant@0.3.11 -- 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` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员在 ANT 插件页面点击“安装到这台 DSH”,由 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
@@ -9,6 +9,7 @@ window.__ModuleLoader__.load({
9
9
  const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
10
10
  const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
11
11
  const ANT_API_PROTOCOL_VERSIONS = [1]
12
+ const fileCountFormat = new Intl.NumberFormat('zh-CN')
12
13
  const panelStyle = {
13
14
  display: 'grid', alignContent: 'start', gap: 14, padding: 18,
14
15
  border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 14,
@@ -32,6 +33,15 @@ window.__ModuleLoader__.load({
32
33
  border: '0.5px solid var(--dsw-alias-border-l2)', borderRadius: 8,
33
34
  cursor: 'pointer',
34
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
+ }
35
45
  const normalizeServiceUrl = (value) => {
36
46
  let url
37
47
  try { url = new URL(value.trim()) } catch { return null }
@@ -60,7 +70,7 @@ window.__ModuleLoader__.load({
60
70
  }
61
71
  return false
62
72
  }
63
- const inject = ['slots', 'remote', 'remote.credentials', 'remote.settings', 'remote.pluginManager']
73
+ const inject = ['slots', 'layout', 'pluginNavigation', 'remote', 'remote.credentials', 'remote.settings', 'remote.pluginManager']
64
74
 
65
75
  async function readLocalJsonResponse(response) {
66
76
  const contentType = response.headers?.get?.('content-type') || ''
@@ -91,18 +101,48 @@ window.__ModuleLoader__.load({
91
101
  try { return normalizeServiceUrl(localStorage.getItem(SERVICE_URL_STORAGE_KEY) || '') || '' } catch { return '' }
92
102
  }
93
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
+
94
111
  function loadDeviceId() {
95
- const existing = localStorage.getItem(DEVICE_ID_STORAGE_KEY)
96
- 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
97
114
  const deviceId = crypto.randomUUID()
98
115
  localStorage.setItem(DEVICE_ID_STORAGE_KEY, deviceId)
99
116
  return deviceId
100
117
  }
101
118
 
102
- function TeamServiceConfig({ remote }) {
119
+ function TeamServiceConfig({ remote, openSkillCenter, openPluginCenter }) {
103
120
  const [serviceUrl, setServiceUrl] = useState(loadServiceUrl)
104
121
  const [draft, setDraft] = useState(serviceUrl)
105
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])
106
146
  const save = (event) => {
107
147
  event.preventDefault()
108
148
  const normalized = normalizeServiceUrl(draft)
@@ -141,21 +181,34 @@ window.__ModuleLoader__.load({
141
181
  h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 } },
142
182
  message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
143
183
  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
+ 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,
144
194
  h(ModelSyncFooter, { remote, serviceUrl, onServiceUrlChange: setServiceUrl }))
145
195
  }
146
196
 
147
- function SkillCenter({ connected }) {
197
+ function SkillCenterPage({ onBack, onPlugins }) {
148
198
  const [skills, setSkills] = useState([])
149
199
  const [loading, setLoading] = useState(false)
150
200
  const [workingId, setWorkingId] = useState('')
151
201
  const [message, setMessage] = useState('')
202
+ const [query, setQuery] = useState('')
203
+ const [filter, setFilter] = useState('all')
152
204
  const refresh = async () => {
153
- if (!connected) return
154
205
  setLoading(true)
155
206
  try {
156
207
  const response = await fetch('/api/ant-teams/skills', { cache: 'no-store' })
157
208
  const result = await readLocalJsonResponse(response)
158
- if (!response.ok) throw new Error(result.message || `读取技能失败(${response.status})。`)
209
+ if (!response.ok) throw new Error(response.status === 401
210
+ ? '请返回 ANT 插件设置,先验证工作邮箱并连接团队。'
211
+ : result.message || `读取技能失败(${response.status})。`)
159
212
  if (!Array.isArray(result.skills)) throw new Error('企业技能目录格式无效。')
160
213
  setSkills(result.skills)
161
214
  setMessage(result.skills.length ? '' : '团队暂未发布精选技能。')
@@ -165,10 +218,7 @@ window.__ModuleLoader__.load({
165
218
  }
166
219
  finally { setLoading(false) }
167
220
  }
168
- useEffect(() => {
169
- if (connected) void refresh()
170
- else { setSkills([]); setMessage('') }
171
- }, [connected])
221
+ useEffect(() => { void refresh() }, [])
172
222
  const changeInstall = async (skill, remove = false) => {
173
223
  const action = remove ? '卸载' : skill.local_status === 'update-available' ? '更新' : '安装'
174
224
  const prompt = remove
@@ -185,35 +235,152 @@ window.__ModuleLoader__.load({
185
235
  const result = await readLocalJsonResponse(response)
186
236
  if (!response.ok) throw new Error(result.message || `${action}失败(${response.status})。`)
187
237
  await refresh()
188
- setMessage(`${skill.title} 已${action}。DSH 将从本机技能目录发现它。`)
238
+ setMessage(remove ? `${skill.title} 已从 ANT 管理的本机目录卸载。`
239
+ : `${skill.title} 的文件已${action}到 DSH 用户技能目录。DSH 会在后续会话或模型步骤中读取;若不可用,请检查当前预设是否启用了本地技能。`)
189
240
  } catch (error) { setMessage(error instanceof Error ? error.message : `${action}失败。`) }
190
241
  finally { setWorkingId('') }
191
242
  }
192
- return h('section', { style: panelStyle },
193
- h('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 } },
243
+ const visible = skills.filter((skill) => {
244
+ const matchesFilter = filter === 'all' || skill.local_status === filter
245
+ const matchesQuery = !query.trim() || `${skill.title} ${skill.description} ${skill.dsh_name}`.toLowerCase().includes(query.trim().toLowerCase())
246
+ return matchesFilter && matchesQuery
247
+ })
248
+ const statusLabel = { installed: '已放入本机目录', 'update-available': '可更新', 'not-installed': '未安装', conflict: '本机冲突' }
249
+ return h('section', { 'aria-label': '企业精选技能', style: {
250
+ height: '100%', overflowY: 'auto', boxSizing: 'border-box',
251
+ padding: 'calc(var(--dsh-frame-top-clearance, 0px) + 20px) clamp(16px, 3vw, 36px) 48px',
252
+ } }, 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 }, '查看企业精选插件 →')),
256
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'end', justifyContent: 'space-between', gap: 16 } },
257
+ h('div', null,
258
+ h('h1', { style: { margin: 0, fontSize: 28 } }, '企业精选技能'),
259
+ h('p', { style: { ...mutedTextStyle, marginTop: 6 } }, '团队管理员审核后发布;安装到这台 DSH 后,由 DSH 原生技能系统发现和使用。')),
260
+ h('button', { type: 'button', disabled: loading || Boolean(workingId), onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新目录')),
261
+ h(ExtensionRequestNotice),
262
+ h('div', { style: { ...panelStyle, gap: 12 } },
263
+ h('input', { type: 'search', value: query, onChange: (event) => setQuery(event.target.value), placeholder: '搜索技能名称或说明', 'aria-label': '搜索企业精选技能', style: inputStyle }),
264
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 } },
265
+ ...[['all', '全部'], ['not-installed', '可安装'], ['installed', '已安装'], ['update-available', '可更新']].map(([value, label]) =>
266
+ h('button', { key: value, type: 'button', onClick: () => setFilter(value), 'aria-pressed': filter === value,
267
+ style: { ...secondaryButtonStyle, minHeight: 34, padding: '6px 12px', backgroundColor: filter === value ? 'var(--dsw-alias-bg-layer-2)' : secondaryButtonStyle.backgroundColor } }, label)),
268
+ h('span', { style: { ...mutedTextStyle, marginLeft: 'auto' } }, `显示 ${visible.length} / ${skills.length} 个技能`))),
269
+ message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
270
+ visible.length ? h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 420px), 1fr))', gap: 16 } },
271
+ ...visible.map((skill) => {
272
+ const description = skill.description || ''
273
+ return h('article', { key: skill.id, style: { ...panelStyle, gap: 13 } },
274
+ h('div', { style: { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 } },
275
+ h('h2', { style: { margin: 0, fontSize: 19 } }, skill.title),
276
+ h('span', { style: mutedTextStyle }, statusLabel[skill.local_status] || '状态未知')),
277
+ h('p', { style: mutedTextStyle }, description.length > 180 ? `${description.slice(0, 180)}…` : description),
278
+ description.length > 180 ? h('details', null,
279
+ h('summary', { style: { cursor: 'pointer', fontSize: 13 } }, '查看完整说明'),
280
+ h('p', { style: { ...mutedTextStyle, marginTop: 8, whiteSpace: 'pre-wrap' } }, description)) : null,
281
+ h('p', { style: mutedTextStyle }, `来源:${skill.source_kind === 'github' ? 'GitHub' : '管理员上传'} · 许可证:${skill.license} · ${fileCountFormat.format(skill.file_count)} 个文件`),
282
+ h('p', { style: mutedTextStyle }, `本机依赖:${skill.requirements}`),
283
+ skill.local_status === 'conflict'
284
+ ? h('p', { role: 'status', style: mutedTextStyle }, '本机同名技能有未受 ANT 管理的内容、文件已被修改或备份损坏;请联系管理员处理。')
285
+ : h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, marginTop: 'auto' } },
286
+ h('button', { type: 'button', disabled: Boolean(workingId) || skill.local_status === 'installed',
287
+ onClick: () => void changeInstall(skill), style: primaryButtonStyle },
288
+ skill.local_status === 'installed' ? '文件已安装' : skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
289
+ skill.local_status !== 'not-installed' ? h('button', { type: 'button', disabled: Boolean(workingId),
290
+ onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载') : null))
291
+ })) : !loading && skills.length ? h('p', { style: mutedTextStyle }, '没有符合筛选条件的技能。') : null))
292
+ }
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 } },
194
338
  h('div', null,
195
- h('h3', { style: { margin: 0, fontSize: 16, fontWeight: 650 } }, '企业精选技能'),
196
- h('p', { style: { ...mutedTextStyle, marginTop: 4 } }, '管理员审核后发布;点击安装即可交给 DSH 原生技能系统使用。')),
197
- h('button', { type: 'button', disabled: !connected || loading || Boolean(workingId), onClick: () => void refresh(), style: secondaryButtonStyle }, loading ? '读取中…' : '刷新')),
198
- !connected ? h('p', { style: mutedTextStyle }, '连接团队后可浏览和安装精选技能。') : 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)))),
199
352
  message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null,
200
- ...skills.map((skill) => h('div', { key: skill.id, style: { display: 'grid', gap: 7, padding: '12px 0', borderTop: '1px solid var(--dsw-alias-border, #8c99994d)' } },
201
- h('strong', { style: { fontSize: 14 } }, skill.title),
202
- h('p', { style: mutedTextStyle }, skill.description),
203
- h('p', { style: mutedTextStyle }, `来源:${skill.source_kind === 'github' ? 'GitHub' : '管理员上传'} · 许可证:${skill.license} · ${skill.file_count} 个文件`),
204
- h('p', { style: mutedTextStyle }, `本机依赖:${skill.requirements}`),
205
- skill.local_status === 'conflict'
206
- ? h('p', { role: 'status', style: mutedTextStyle }, '本机同名技能有未受 ANT 管理的内容、文件已被修改或备份损坏;请联系管理员处理。')
207
- : skill.local_status === 'not-discovered'
208
- ? h('div', null,
209
- h('p', { role: 'status', style: mutedTextStyle }, '文件已在本机,但 DSH 技能服务没有发现它;请检查 DSH 技能目录配置。'),
210
- h('button', { type: 'button', disabled: Boolean(workingId), onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载本机副本'))
211
- : h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 8 } },
212
- h('button', { type: 'button', disabled: !connected || Boolean(workingId) || skill.local_status === 'installed',
213
- onClick: () => void changeInstall(skill), style: primaryButtonStyle },
214
- skill.local_status === 'installed' ? '已安装' : skill.local_status === 'update-available' ? '更新技能' : '安装到这台 DSH'),
215
- skill.local_status !== 'not-installed' ? h('button', { type: 'button', disabled: Boolean(workingId),
216
- onClick: () => void changeInstall(skill, true), style: secondaryButtonStyle }, '卸载') : 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))
217
384
  }
218
385
 
219
386
  function ModelSyncFooter({ remote, serviceUrl, onServiceUrlChange }) {
@@ -225,11 +392,21 @@ window.__ModuleLoader__.load({
225
392
  const [credentialConfigured, setCredentialConfigured] = useState(null)
226
393
  const [reauthenticate, setReauthenticate] = useState(false)
227
394
  const [needsReconnect, setNeedsReconnect] = useState(false)
395
+ const [allowLocalCleanup, setAllowLocalCleanup] = useState(false)
228
396
  const [bundleRelease, setBundleRelease] = useState(null)
229
397
  const [bundleUpdate, setBundleUpdate] = useState({ state: 'idle', currentVersion: '', message: '' })
230
- const [approvedBuilds, setApprovedBuilds] = useState([])
231
398
  const [serviceCompatibility, setServiceCompatibility] = useState({ state: 'idle', message: '' })
232
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
+
233
410
  const inspectServiceCompatibility = async (rawServiceUrl, useSavedConnection = false) => {
234
411
  const normalized = normalizeServiceUrl(rawServiceUrl)
235
412
  if (!normalized && !useSavedConnection) {
@@ -343,10 +520,12 @@ window.__ModuleLoader__.load({
343
520
 
344
521
  const checkBundleUpdate = async () => {
345
522
  setBundleUpdate({ state: 'checking', currentVersion: '', message: '正在检查 ANT 更新…' })
346
- setApprovedBuilds([])
347
523
  try {
348
524
  const response = await fetch('/api/ant-teams/bundle-release', {
349
- 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
+ },
350
529
  })
351
530
  const release = await readLocalJsonResponse(response)
352
531
  if (!response.ok) throw new Error(release.message || `检查失败(${response.status})`)
@@ -394,99 +573,55 @@ window.__ModuleLoader__.load({
394
573
  }
395
574
  }
396
575
 
397
- const installBundleUpdate = async (allowScripts = false) => {
398
- const installSpec = directInstallSpec(bundleRelease)
399
- if (!installSpec
400
- || (!allowScripts && bundleUpdate.state !== 'available')
401
- || (allowScripts && approvedBuilds.length === 0)) return
402
- const manager = remote?.pluginManager
403
- if (typeof manager?.installBundle !== 'function') {
404
- setBundleUpdate((current) => ({ ...current, state: 'failed', message: '当前 DSH 版本未提供官方 Bundle 安装接口。' }))
405
- return
406
- }
407
- const scripts = allowScripts ? approvedBuilds : []
408
- if (scripts.length && !window.confirm(`DSH 安装需要运行以下依赖的安装脚本:\n\n${scripts.join('\n')}\n\n仅在信任这些依赖时继续。`)) return
409
- setBusy(true)
410
- setApprovedBuilds([])
411
- setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在委托 DSH 官方管理器安装更新…' }))
412
- try {
413
- const result = await manager.installBundle(installSpec, {
414
- enabled: true,
415
- ...(scripts.length ? { approvedBuilds: scripts } : {}),
416
- })
417
- if (!result.ok) throw new Error(result.error?.message || 'DSH 安装请求未返回结果。')
418
- const outcome = result.value
419
- if (outcome.application === 'failed') {
420
- const pending = Array.isArray(outcome.pendingBuilds) ? outcome.pendingBuilds : []
421
- setApprovedBuilds(pending)
422
- const reason = outcome.error?.diagnostic || outcome.packageResult?.output || 'DSH 未能安装该版本。'
423
- setBundleUpdate({
424
- state: 'failed', currentVersion: bundleUpdate.currentVersion,
425
- message: pending.length
426
- ? `DSH 暂停了安装,因为以下依赖需要明确授权运行安装脚本:${pending.join('、')}`
427
- : reason,
428
- })
429
- return
430
- }
431
- if (outcome.application === 'cancelled') {
432
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已取消更新,当前运行版本未切换。' })
433
- return
434
- }
435
- if (outcome.application === 'overridden') {
436
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已记录更新,但其他插件配置覆盖了 ANT。请检查 DSH 插件配置后再重启。' })
437
- return
438
- }
439
- if (outcome.bundle !== bundleRelease.package_name) {
440
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 返回的安装包名称与 ANT 发布信息不一致。请联系管理员核对发布包。' })
441
- return
442
- }
443
- setBundleUpdate({
444
- state: 'done', currentVersion: bundleRelease.version,
445
- message: outcome.application === 'restart-required'
446
- ? `DSH 已安装 ${installSpec}。请完全退出并重新启动 DSH 后生效。`
447
- : `DSH 已安装 ${installSpec}。`,
448
- })
449
- } catch (error) {
450
- setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: error instanceof Error ? error.message : 'DSH 未能安装 ANT 更新。' })
451
- } finally {
452
- setBusy(false)
453
- }
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)
454
590
  }
455
591
 
456
592
  const disconnect = async () => {
457
593
  setBusy(true)
458
594
  setMessage('正在断开此设备的 ANT 连接…')
459
595
  try {
460
- if (typeof remote?.settings?.mutate !== 'function') throw new Error('DSH 未提供模型设置接口。')
461
- const result = await remote.settings.mutate('llm-pi-ai', [
462
- { op: 'unset', path: ['providers', 'ant-team-openai'] },
463
- { op: 'unset', path: ['providers', 'ant-team-anthropic'] },
464
- ], undefined)
465
- 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
+ }
466
601
  } catch (error) {
602
+ setAllowLocalCleanup(true)
467
603
  setBusy(false)
468
- setMessage(`无法清除本机 ANT 模型设置,因此连接仍保留。${error instanceof Error ? error.message : '请稍后重试。'}`)
604
+ setMessage(`未清除本机凭证和模型设置:${error instanceof Error ? error.message : '请稍后重试。'}`)
469
605
  return
470
606
  }
471
- let serverRevoked = false
472
607
  try {
473
- const response = await fetch('/api/ant-teams/disconnect', { method: 'POST' })
474
- const result = await readLocalJsonResponse(response)
475
- serverRevoked = response.ok && result.revoked === true
476
- } catch {}
477
- 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)
478
619
  try {
479
- if (typeof remote?.credentials?.unset !== 'function') throw new Error('DSH 未提供凭证删除接口。')
480
- const result = await remote.credentials.unset('ANT_MODEL_API_KEY')
481
- if (!result.ok) throw new Error(result.error?.message || 'ANT 成员凭证未能清除。')
482
- credentialRemoved = true
483
- setCredentialConfigured(false)
620
+ await clearLocalConnection()
621
+ setMessage('已仅清理本机 ANT 凭证和模型设置。此操作未确认服务端撤销,请向原团队管理员核对。')
484
622
  } catch (error) {
485
- setMessage(`本机 ANT 模型设置已清除,但成员凭证未能移除:${error instanceof Error ? error.message : '请稍后重试。'}`)
486
- }
487
- setBusy(false)
488
- if (credentialRemoved && serverRevoked) setMessage('此设备的 ANT 凭证已撤销,ANT 模型设置已清除。团队服务地址已保留。')
489
- else if (credentialRemoved) setMessage('本机 ANT 凭证与模型设置已清除;服务端未确认撤销,活动会话会在 90 秒内到期。团队服务地址已保留。')
623
+ setMessage(`本机清理未完成:${error instanceof Error ? error.message : '请稍后重试。'}`)
624
+ } finally { setBusy(false) }
490
625
  }
491
626
 
492
627
  useEffect(() => {
@@ -524,6 +659,13 @@ window.__ModuleLoader__.load({
524
659
  setMessage('请填写工作邮箱。')
525
660
  return
526
661
  }
662
+ if (credentialConfigured !== false && !savedDeviceId()) {
663
+ if (credentialConfigured === true) setAllowLocalCleanup(true)
664
+ setMessage(credentialConfigured === true
665
+ ? '旧 ANT 凭证仍在,但本机无法确认它绑定的设备编号。请让原团队管理员按成员核查并撤销旧设备,再仅清理本机。'
666
+ : '尚未确认本机凭证状态,也没有已保存的设备编号。请等待检查完成,或修复 DSH 凭证接口后重试。')
667
+ return
668
+ }
527
669
  setBusy(true)
528
670
  setMessage('正在发送邮箱验证码…')
529
671
  try {
@@ -535,7 +677,7 @@ window.__ModuleLoader__.load({
535
677
  body: JSON.stringify({ service_url: normalized, email: normalizedEmail }),
536
678
  })
537
679
  const result = await readLocalJsonResponse(response)
538
- if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
680
+ if (!response.ok) throw new Error(connectionErrorMessage(result, response.status))
539
681
  setEmail(normalizedEmail)
540
682
  setCodeRequested(true)
541
683
  setMessage(result.message || '如果该邮箱符合团队规则,验证码将发送至邮箱。')
@@ -555,6 +697,13 @@ window.__ModuleLoader__.load({
555
697
  setMessage('请填写有效的服务地址、工作邮箱和 8 位验证码。')
556
698
  return
557
699
  }
700
+ if (credentialConfigured !== false && !savedDeviceId()) {
701
+ if (credentialConfigured === true) setAllowLocalCleanup(true)
702
+ setMessage(credentialConfigured === true
703
+ ? '旧 ANT 凭证仍在,但本机无法确认旧设备编号。请先联系原团队管理员撤销旧设备,再仅清理本机。'
704
+ : '尚未确认本机凭证状态,也没有已保存的设备编号。请等待检查完成,或修复 DSH 凭证接口后重试。')
705
+ return
706
+ }
558
707
  setBusy(true)
559
708
  setMessage('正在验证邮箱并同步团队模型…')
560
709
  try {
@@ -571,7 +720,7 @@ window.__ModuleLoader__.load({
571
720
  }),
572
721
  })
573
722
  const result = await readLocalJsonResponse(response)
574
- if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
723
+ if (!response.ok) throw new Error(connectionErrorMessage(result, response.status))
575
724
  if (typeof result.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
576
725
  throw new Error('ANT 返回的连接信息格式无效。')
577
726
  }
@@ -585,6 +734,7 @@ window.__ModuleLoader__.load({
585
734
  onServiceUrlChange(normalized)
586
735
  setCodeRequested(false)
587
736
  setCredentialConfigured(true)
737
+ setAllowLocalCleanup(false)
588
738
  setNeedsReconnect(false)
589
739
  setReauthenticate(false)
590
740
  setMessage(`已连接 ${result.email || normalizedEmail},同步 ${result.models.length} 个获准模型。它们现已出现在 DSH 模型选择器中。`)
@@ -628,12 +778,9 @@ window.__ModuleLoader__.load({
628
778
  h('button', { type: 'button', disabled: busy || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
629
779
  bundleUpdate.message ? h('p', { role: 'status', style: mutedTextStyle }, bundleUpdate.message) : null,
630
780
  bundleRelease?.version && bundleUpdate.state === 'available' ? h('div', { style: { display: 'grid', gap: 8 } },
631
- h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · npm:${directInstallSpec(bundleRelease)}`),
781
+ h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · 精确包名:${directInstallSpec(bundleRelease)}`),
632
782
  bundleRelease.release_notes ? h('p', { style: mutedTextStyle }, bundleRelease.release_notes) : null,
633
- h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '安装中…' : '由 DSH 安装并更新')) : null,
634
- approvedBuilds.length ? h('div', { style: { display: 'grid', gap: 8 } },
635
- h('p', { style: mutedTextStyle }, `DSH 暂停安装,等待授权运行以下依赖的安装脚本:${approvedBuilds.join('、')}。`),
636
- h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(true), style: secondaryButtonStyle }, '信任这些依赖并继续安装')) : null) : null,
783
+ h('p', { style: mutedTextStyle }, 'ANT 暂停一键安装更新:当前 DSH 无法在执行安装脚本前核对审核制品。请联系团队管理员确认升级方式。')) : null) : null,
637
784
  showConnectForm ? h('form', { onSubmit: requestCode, style: { display: 'grid', gap: 9, maxWidth: 560 } },
638
785
  h('label', { htmlFor: 'ant-team-model-email', style: { fontSize: 13, fontWeight: 550 } }, '工作邮箱'),
639
786
  h('input', {
@@ -654,8 +801,24 @@ window.__ModuleLoader__.load({
654
801
  onChange: (event) => setCode(event.target.value.replace(/\D/gu, '').slice(0, 8)), style: inputStyle,
655
802
  }),
656
803
  h('button', { type: 'submit', disabled: busy || code.length !== 8, style: primaryButtonStyle }, busy ? '连接中…' : '验证并连接')) : null,
657
- message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null),
658
- h(SkillCenter, { connected: credentialConfigured === true }))
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,
821
+ message ? h('p', { role: 'status', style: mutedTextStyle }, message) : null))
659
822
 
660
823
  }
661
824
 
@@ -665,13 +828,29 @@ window.__ModuleLoader__.load({
665
828
  settings: ctx.remote.settings,
666
829
  pluginManager: ctx.remote.pluginManager,
667
830
  }
831
+ 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, {
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
+ })))
668
843
  ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
669
844
  name: 'plugins.bundle.config',
670
845
  key: ANT_BUNDLE_PACKAGE_NAME,
671
846
  inject: () => ({ remote }),
672
847
  }, ({ view, remote: injectedRemote }) => view === 'summary'
673
848
  ? h('p', null, 'ANT 团队服务入口。')
674
- : h(TeamServiceConfig, { remote: injectedRemote })))
849
+ : h(TeamServiceConfig, {
850
+ remote: injectedRemote,
851
+ openSkillCenter: () => ctx.layout.selectPanel('ant-skills'),
852
+ openPluginCenter: () => ctx.layout.selectPanel('ant-plugins'),
853
+ })))
675
854
  }
676
855
 
677
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.11",
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": {
@@ -25,6 +25,7 @@
25
25
  "platform": "web",
26
26
  "inject": [
27
27
  "@deepseek-ai/dsh-api-remotes",
28
+ "@deepseek-ai/dsh-client-ui-layout",
28
29
  "@deepseek-ai/dsh-client-ui-plugin-manager"
29
30
  ]
30
31
  }
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)