@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 +6 -6
- package/README.md +7 -1
- package/client.js +307 -128
- package/index.js +129 -52
- package/package.json +2 -1
- package/skill-install.js +13 -33
- package/tools/migrate-legacy.mjs +11 -5
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. 回到插件列表,点击
|
|
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.
|
|
63
|
-
npm exec --yes --package @ai-wendao/ant@0.3.
|
|
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.
|
|
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.
|
|
80
|
-
npm exec --yes --package @ai-wendao/ant@0.3.
|
|
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` 导入技能,核对来源、文件和依赖后发布。已连接团队的成员在
|
|
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 =
|
|
96
|
-
if (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
|
|
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(
|
|
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}
|
|
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
|
-
|
|
193
|
-
|
|
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('
|
|
196
|
-
h('p', { style: { ...mutedTextStyle, marginTop:
|
|
197
|
-
h('button', { type: 'button', disabled:
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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: {
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
461
|
-
const result = await
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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(
|
|
604
|
+
setMessage(`未清除本机凭证和模型设置:${error instanceof Error ? error.message : '请稍后重试。'}`)
|
|
469
605
|
return
|
|
470
606
|
}
|
|
471
|
-
let serverRevoked = false
|
|
472
607
|
try {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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(
|
|
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
|
|
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
|
|
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} ·
|
|
781
|
+
h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · 精确包名:${directInstallSpec(bundleRelease)}`),
|
|
632
782
|
bundleRelease.release_notes ? h('p', { style: mutedTextStyle }, bundleRelease.release_notes) : null,
|
|
633
|
-
h('
|
|
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
|
-
|
|
658
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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))
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
113
|
-
let pinnedUrl
|
|
114
|
-
let serviceUrl
|
|
143
|
+
let connection
|
|
115
144
|
try {
|
|
116
|
-
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
300
|
-
|
|
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(
|
|
329
|
+
return jsonResponse(502, { message: '服务端未确认永久撤销,本机凭证仍保留。请稍后重试。' })
|
|
303
330
|
} catch {
|
|
304
|
-
return jsonResponse(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(() => {}) }
|
package/tools/migrate-legacy.mjs
CHANGED
|
@@ -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
|
|
449
|
-
|
|
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)
|