@ai-wendao/ant 0.3.12 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION.md +6 -6
- package/README.md +9 -1
- package/client.js +503 -99
- package/index.js +166 -54
- package/package.json +1 -1
- package/skill-install.js +13 -33
- package/tools/migrate-legacy.mjs +11 -5
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 = []
|
|
@@ -7,12 +8,14 @@ export const inject = []
|
|
|
7
8
|
const MODEL_SYNC_PATH = '/api/ant-teams/models'
|
|
8
9
|
const SERVICE_INFO_PATH = '/api/ant-teams/service-info'
|
|
9
10
|
const BUNDLE_RELEASE_PATH = '/api/ant-teams/bundle-release'
|
|
11
|
+
const CONNECTION_ORIGIN_PATH = '/api/ant-teams/connection-origin'
|
|
10
12
|
const DISCONNECT_PATH = '/api/ant-teams/disconnect'
|
|
11
13
|
const REQUEST_CODE_PATH = '/api/ant-teams/request-code'
|
|
12
14
|
const CONNECT_PATH = '/api/ant-teams/connect'
|
|
13
15
|
const SKILLS_PATH = '/api/ant-teams/skills'
|
|
14
16
|
const SKILL_INSTALL_PATH = '/api/ant-teams/skills/install'
|
|
15
17
|
const SKILL_UNINSTALL_PATH = '/api/ant-teams/skills/uninstall'
|
|
18
|
+
const PLUGINS_PATH = '/api/ant-teams/plugins'
|
|
16
19
|
const ANT_NPM_PACKAGE_NAME = '@ai-wendao/ant'
|
|
17
20
|
const MAX_MODEL_RESPONSE_BYTES = 1024 * 1024
|
|
18
21
|
const MODEL_TOKEN_REF = 'ANT_MODEL_API_KEY'
|
|
@@ -34,28 +37,50 @@ function normalizeServiceUrl(value) {
|
|
|
34
37
|
return url.origin
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
function
|
|
40
|
+
function modelServiceBinding(settings) {
|
|
38
41
|
const descriptor = settings.describe().find((entry) => entry.ns === 'llm-pi-ai')
|
|
39
42
|
const providers = descriptor?.value?.providers
|
|
40
|
-
if (!providers || typeof providers !== 'object') return null
|
|
43
|
+
if (!providers || typeof providers !== 'object') return { configured: false, serviceUrl: null }
|
|
41
44
|
const origins = new Set()
|
|
45
|
+
let configured = false
|
|
42
46
|
for (const provider of Object.values(providers)) {
|
|
43
|
-
if (provider?.apiKeyEnv !== MODEL_TOKEN_REF
|
|
47
|
+
if (provider?.apiKeyEnv !== MODEL_TOKEN_REF) continue
|
|
48
|
+
configured = true
|
|
49
|
+
if (typeof provider.baseURL !== 'string') return { configured, serviceUrl: null }
|
|
44
50
|
let url
|
|
45
|
-
try { url = new URL(provider.baseURL) } catch { return null }
|
|
51
|
+
try { url = new URL(provider.baseURL) } catch { return { configured, serviceUrl: null } }
|
|
46
52
|
const localHttp = url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
|
|
47
53
|
if ((url.protocol !== 'https:' && !localHttp) || url.username || url.password || url.search || url.hash
|
|
48
|
-
|| !['/model-gateway/openai/v1', '/model-gateway/anthropic'].includes(url.pathname))
|
|
54
|
+
|| !['/model-gateway/openai/v1', '/model-gateway/anthropic'].includes(url.pathname)) {
|
|
55
|
+
return { configured, serviceUrl: null }
|
|
56
|
+
}
|
|
49
57
|
origins.add(url.origin)
|
|
50
58
|
}
|
|
51
|
-
return origins.size === 1 ? [...origins][0] : null
|
|
59
|
+
return { configured, serviceUrl: origins.size === 1 ? [...origins][0] : null }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tokenHash(token) {
|
|
63
|
+
return createHash('sha256').update(token).digest('hex')
|
|
52
64
|
}
|
|
53
65
|
|
|
54
|
-
async function pinServiceUrl(credentials, serviceUrl) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
async function pinServiceUrl(credentials, serviceUrl, token, previousToken = null) {
|
|
67
|
+
const previousHash = previousToken ? tokenHash(previousToken) : null
|
|
68
|
+
await credentials.modifyRecord(SERVICE_RECORD_KEY, async (current) => {
|
|
69
|
+
if (previousHash && (current?.kind !== 'grant'
|
|
70
|
+
|| normalizeServiceUrl(current.payload?.service_url) !== serviceUrl
|
|
71
|
+
|| (current.payload?.token_sha256 && current.payload.token_sha256 !== previousHash
|
|
72
|
+
&& current.payload?.previous_token_sha256 !== previousHash))) {
|
|
73
|
+
throw new Error('当前 ANT 凭证绑定已变化,请重试。')
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
kind: 'grant',
|
|
77
|
+
payload: {
|
|
78
|
+
service_url: serviceUrl,
|
|
79
|
+
token_sha256: tokenHash(token),
|
|
80
|
+
...(previousHash && previousHash !== tokenHash(token) ? { previous_token_sha256: previousHash } : {}),
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
})
|
|
59
84
|
}
|
|
60
85
|
|
|
61
86
|
async function readMemberConnection(credentials, settings) {
|
|
@@ -66,8 +91,15 @@ async function readMemberConnection(credentials, settings) {
|
|
|
66
91
|
const pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
|
|
67
92
|
? normalizeServiceUrl(record.payload.service_url)
|
|
68
93
|
: null
|
|
69
|
-
|
|
70
|
-
|
|
94
|
+
if (!pinnedUrl) return null
|
|
95
|
+
const binding = modelServiceBinding(settings)
|
|
96
|
+
if (pinnedUrl && binding.configured && binding.serviceUrl !== pinnedUrl) return null
|
|
97
|
+
if (pinnedUrl && typeof record.payload?.token_sha256 === 'string') {
|
|
98
|
+
const currentHash = tokenHash(token)
|
|
99
|
+
if (record.payload.token_sha256 !== currentHash
|
|
100
|
+
&& record.payload.previous_token_sha256 !== currentHash) return null
|
|
101
|
+
} else if (pinnedUrl && binding.serviceUrl !== pinnedUrl) return null
|
|
102
|
+
return { serviceUrl: pinnedUrl, token }
|
|
71
103
|
}
|
|
72
104
|
|
|
73
105
|
async function updateModelSession(serviceUrl, token, action) {
|
|
@@ -109,29 +141,23 @@ async function readLimitedBody(response) {
|
|
|
109
141
|
|
|
110
142
|
async function fetchMemberModels(request, credentials, settings) {
|
|
111
143
|
if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
|
|
112
|
-
let
|
|
113
|
-
let pinnedUrl
|
|
114
|
-
let serviceUrl
|
|
144
|
+
let connection
|
|
115
145
|
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)
|
|
146
|
+
connection = await readMemberConnection(credentials, settings)
|
|
122
147
|
} catch {
|
|
123
148
|
return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
|
|
124
149
|
}
|
|
125
|
-
if (!
|
|
150
|
+
if (!connection) return jsonResponse(401, { message: '当前 DSH 未连接 ANT,请验证工作邮箱。' })
|
|
151
|
+
const { serviceUrl, token } = connection
|
|
126
152
|
|
|
127
153
|
try {
|
|
128
|
-
const session = await updateModelSession(serviceUrl, token
|
|
154
|
+
const session = await updateModelSession(serviceUrl, token, 'heartbeat')
|
|
129
155
|
if (session.status === 401 || session.status === 403) {
|
|
130
156
|
return jsonResponse(401, { message: 'ANT 成员凭证已失效,请重新验证工作邮箱。' })
|
|
131
157
|
}
|
|
132
158
|
if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
|
|
133
159
|
const upstream = await fetch(new URL('/api/me/models', serviceUrl), {
|
|
134
|
-
headers: { authorization: `Bearer ${token
|
|
160
|
+
headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
|
|
135
161
|
redirect: 'manual',
|
|
136
162
|
signal: AbortSignal.timeout(10_000),
|
|
137
163
|
})
|
|
@@ -146,11 +172,6 @@ async function fetchMemberModels(request, credentials, settings) {
|
|
|
146
172
|
if (!Array.isArray(result?.models) || normalizeServiceUrl(result.service_url) !== serviceUrl) {
|
|
147
173
|
return jsonResponse(502, { message: 'ANT 团队模型目录与已连接服务不匹配。' })
|
|
148
174
|
}
|
|
149
|
-
if (!pinnedUrl) {
|
|
150
|
-
try { await pinServiceUrl(credentials, serviceUrl) } catch {
|
|
151
|
-
return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
175
|
return jsonResponse(200, { service_url: serviceUrl, models: result.models })
|
|
155
176
|
} catch {
|
|
156
177
|
return jsonResponse(502, { message: '无法连接 ANT 团队服务' })
|
|
@@ -246,6 +267,9 @@ async function fetchBundleRelease(request, credentials, settings) {
|
|
|
246
267
|
if (request.headers.get('x-ant-bundle-capabilities') === 'direct-package-install') {
|
|
247
268
|
headers['x-ant-bundle-capabilities'] = 'direct-package-install'
|
|
248
269
|
}
|
|
270
|
+
if (request.headers.get('x-ant-bundle-update-mode') === 'check-only') {
|
|
271
|
+
headers['x-ant-bundle-update-mode'] = 'check-only'
|
|
272
|
+
}
|
|
249
273
|
const upstream = await fetch(new URL('/api/me/bundle-release', connection.serviceUrl), {
|
|
250
274
|
headers,
|
|
251
275
|
redirect: 'manual',
|
|
@@ -255,7 +279,7 @@ async function fetchBundleRelease(request, credentials, settings) {
|
|
|
255
279
|
if (text === null) return jsonResponse(502, { message: 'ANT 更新信息响应过大。' })
|
|
256
280
|
if (upstream.status === 401 || upstream.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
|
|
257
281
|
if (upstream.status === 426) {
|
|
258
|
-
return jsonResponse(426, { message: '当前 ANT
|
|
282
|
+
return jsonResponse(426, { message: '当前 ANT 插件仍可能自动安装未经核验的制品。请联系团队管理员采用受控升级方式;旧 alias 还需在 DSH 插件管理器中迁移。' })
|
|
259
283
|
}
|
|
260
284
|
if (!upstream.ok) return jsonResponse(502, { message: 'ANT 暂时无法提供插件更新信息。' })
|
|
261
285
|
let release
|
|
@@ -269,26 +293,47 @@ async function fetchBundleRelease(request, credentials, settings) {
|
|
|
269
293
|
const directInstallSpec = release?.package_name && release?.version
|
|
270
294
|
? `${release.package_name}@${release.version}`
|
|
271
295
|
: ''
|
|
296
|
+
const artifactPath = release?.version ? `/api/ant-bundles/${release.version}/package.tgz` : ''
|
|
297
|
+
const hasArtifact = release?.artifact_path === artifactPath
|
|
298
|
+
&& /^[a-f0-9]{64}$/u.test(release?.artifact_sha256 || '')
|
|
299
|
+
const noArtifact = !release?.artifact_path && !release?.artifact_sha256
|
|
272
300
|
if (release?.bundle_name !== 'ant' || !validPackageName || !validVersion
|
|
273
301
|
|| typeof release?.install_spec !== 'string' || typeof release?.release_notes !== 'string'
|
|
274
302
|
|| (release.version && release.install_spec !== installSpec)
|
|
275
303
|
|| (!release.version && release.install_spec !== '')
|
|
276
|
-
|| (release.direct_install_spec !== undefined && release.direct_install_spec !== directInstallSpec)
|
|
304
|
+
|| (release.direct_install_spec !== undefined && release.direct_install_spec !== directInstallSpec)
|
|
305
|
+
|| (!hasArtifact && !noArtifact)) {
|
|
277
306
|
return jsonResponse(502, { message: 'ANT 更新信息字段无效。' })
|
|
278
307
|
}
|
|
279
|
-
return jsonResponse(200,
|
|
308
|
+
return jsonResponse(200, hasArtifact ? {
|
|
309
|
+
...release,
|
|
310
|
+
artifact_url: new URL(artifactPath, connection.serviceUrl).href,
|
|
311
|
+
} : release)
|
|
280
312
|
} catch {
|
|
281
313
|
return jsonResponse(502, { message: '无法连接 ANT 更新服务。' })
|
|
282
314
|
}
|
|
283
315
|
}
|
|
284
316
|
|
|
317
|
+
async function fetchConnectionOrigin(request, credentials, settings) {
|
|
318
|
+
if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
|
|
319
|
+
try {
|
|
320
|
+
const connection = await readMemberConnection(credentials, settings)
|
|
321
|
+
return jsonResponse(200, { service_url: connection?.serviceUrl || null })
|
|
322
|
+
} catch {
|
|
323
|
+
return jsonResponse(500, { message: '无法核对已绑定的 ANT 团队地址。' })
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
285
327
|
async function disconnectMember(request, credentials, settings) {
|
|
286
328
|
if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
|
|
287
329
|
let connection
|
|
288
330
|
try { connection = await readMemberConnection(credentials, settings) } catch {
|
|
289
331
|
return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
|
|
290
332
|
}
|
|
291
|
-
if (!connection) return jsonResponse(
|
|
333
|
+
if (!connection) return jsonResponse(409, {
|
|
334
|
+
code: 'LEGACY_DEVICE_REVOKE_REQUIRED',
|
|
335
|
+
message: '无法确认旧凭证所属服务或当前设备令牌。请先让原团队管理员撤销此设备,再仅清理本机。',
|
|
336
|
+
})
|
|
292
337
|
try {
|
|
293
338
|
const response = await fetch(new URL('/auth/dsh-session/disconnect', connection.serviceUrl), {
|
|
294
339
|
method: 'POST',
|
|
@@ -296,12 +341,13 @@ async function disconnectMember(request, credentials, settings) {
|
|
|
296
341
|
redirect: 'manual',
|
|
297
342
|
signal: AbortSignal.timeout(10_000),
|
|
298
343
|
})
|
|
299
|
-
if (response.status === 204
|
|
300
|
-
|
|
344
|
+
if (response.status === 204) return jsonResponse(200, { revoked: true })
|
|
345
|
+
if (response.status === 401 || response.status === 403) {
|
|
346
|
+
return jsonResponse(409, { message: '服务端未确认撤销当前令牌。它可能已被停用或更换;请让原团队管理员确认远端状态,再选择仅清理本机。' })
|
|
301
347
|
}
|
|
302
|
-
return jsonResponse(
|
|
348
|
+
return jsonResponse(502, { message: '服务端未确认永久撤销,本机凭证仍保留。请稍后重试。' })
|
|
303
349
|
} catch {
|
|
304
|
-
return jsonResponse(
|
|
350
|
+
return jsonResponse(502, { message: '无法连接团队服务确认永久撤销,本机凭证仍保留。请稍后重试。' })
|
|
305
351
|
}
|
|
306
352
|
}
|
|
307
353
|
|
|
@@ -313,18 +359,18 @@ async function memberSkillConnection(credentials, settings) {
|
|
|
313
359
|
return connection
|
|
314
360
|
}
|
|
315
361
|
|
|
316
|
-
async function fetchMemberSkills(request, credentials, settings
|
|
362
|
+
async function fetchMemberSkills(request, credentials, settings) {
|
|
317
363
|
if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
|
|
318
364
|
try {
|
|
319
365
|
const connection = await memberSkillConnection(credentials, settings)
|
|
320
366
|
if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
|
|
321
|
-
return jsonResponse(200, { skills: await listSkills(connection
|
|
367
|
+
return jsonResponse(200, { skills: await listSkills(connection) })
|
|
322
368
|
} catch (error) {
|
|
323
369
|
return jsonResponse(502, { message: error instanceof Error ? error.message : '无法读取企业精选技能。' })
|
|
324
370
|
}
|
|
325
371
|
}
|
|
326
372
|
|
|
327
|
-
async function installMemberSkill(request, credentials, settings
|
|
373
|
+
async function installMemberSkill(request, credentials, settings) {
|
|
328
374
|
if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
|
|
329
375
|
let body
|
|
330
376
|
try {
|
|
@@ -335,7 +381,7 @@ async function installMemberSkill(request, credentials, settings, registry) {
|
|
|
335
381
|
try {
|
|
336
382
|
const connection = await memberSkillConnection(credentials, settings)
|
|
337
383
|
if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
|
|
338
|
-
return jsonResponse(200, await installSkill(connection, body?.skill_id
|
|
384
|
+
return jsonResponse(200, await installSkill(connection, body?.skill_id))
|
|
339
385
|
} catch (error) {
|
|
340
386
|
return jsonResponse(409, { message: error instanceof Error ? error.message : '技能安装失败。' })
|
|
341
387
|
}
|
|
@@ -355,7 +401,38 @@ async function uninstallMemberSkill(request) {
|
|
|
355
401
|
}
|
|
356
402
|
}
|
|
357
403
|
|
|
358
|
-
async function
|
|
404
|
+
async function fetchMemberPlugins(request, credentials, settings) {
|
|
405
|
+
if (request.method !== 'GET') return jsonResponse(405, { message: '只支持 GET 请求' })
|
|
406
|
+
try {
|
|
407
|
+
const connection = await memberSkillConnection(credentials, settings)
|
|
408
|
+
if (!connection) return jsonResponse(401, { message: '请先连接 ANT 团队,或重新验证工作邮箱。' })
|
|
409
|
+
const upstream = await fetch(new URL('/api/me/plugins', connection.serviceUrl), {
|
|
410
|
+
headers: { authorization: `Bearer ${connection.token}`, accept: 'application/json' },
|
|
411
|
+
redirect: 'manual', signal: AbortSignal.timeout(10_000),
|
|
412
|
+
})
|
|
413
|
+
const text = await readLimitedBody(upstream)
|
|
414
|
+
if (text === null) return jsonResponse(502, { message: '插件目录响应过大。' })
|
|
415
|
+
if (!upstream.ok) return jsonResponse(upstream.status === 401 || upstream.status === 403 ? 401 : 502,
|
|
416
|
+
{ message: '无法读取企业精选插件目录。' })
|
|
417
|
+
const plugins = JSON.parse(text)
|
|
418
|
+
if (!Array.isArray(plugins)) throw new Error('插件目录格式无效。')
|
|
419
|
+
return jsonResponse(200, { plugins: plugins.map((plugin) => {
|
|
420
|
+
const validId = typeof plugin?.id === 'string'
|
|
421
|
+
&& /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/iu.test(plugin.id)
|
|
422
|
+
const validDigest = /^[0-9a-f]{64}$/u.test(plugin?.artifact_sha256 || '')
|
|
423
|
+
if (plugin?.status !== 'published' || !validId || !validDigest) {
|
|
424
|
+
return { ...plugin, artifact_sha256: null }
|
|
425
|
+
}
|
|
426
|
+
const artifactPath = `/api/plugin-artifacts/${plugin.id}/package.tgz`
|
|
427
|
+
return { ...plugin, artifact_path: artifactPath,
|
|
428
|
+
artifact_url: new URL(artifactPath, connection.serviceUrl).href }
|
|
429
|
+
}) })
|
|
430
|
+
} catch {
|
|
431
|
+
return jsonResponse(502, { message: '无法读取企业精选插件目录。' })
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function fetchAuthRequest(request, endpoint, credentials, settings) {
|
|
359
436
|
if (request.method !== 'POST') return jsonResponse(405, { message: '只支持 POST 请求' })
|
|
360
437
|
let body
|
|
361
438
|
try { body = await request.json() } catch { return jsonResponse(400, { message: '请求格式无效' }) }
|
|
@@ -369,6 +446,33 @@ async function fetchAuthRequest(request, endpoint, credentials) {
|
|
|
369
446
|
return jsonResponse(400, { message: '团队服务地址或邮箱验证信息无效' })
|
|
370
447
|
}
|
|
371
448
|
|
|
449
|
+
let previousToken = null
|
|
450
|
+
try {
|
|
451
|
+
const existing = await credentials.resolve(MODEL_TOKEN_REF)
|
|
452
|
+
const current = existing?.value ? await readMemberConnection(credentials, settings) : null
|
|
453
|
+
const binding = modelServiceBinding(settings)
|
|
454
|
+
const record = existing?.value ? await credentials.readRecord(SERVICE_RECORD_KEY) : null
|
|
455
|
+
const pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
|
|
456
|
+
? normalizeServiceUrl(record.payload.service_url) : null
|
|
457
|
+
if (existing?.value && !pinnedUrl && !binding.configured) {
|
|
458
|
+
return jsonResponse(409, {
|
|
459
|
+
code: 'LEGACY_DEVICE_REVOKE_REQUIRED',
|
|
460
|
+
message: '旧 ANT 凭证没有可核实的团队服务地址。请先让原团队管理员撤销此设备,再仅清理本机并连接新团队。',
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
if ((existing?.value && pinnedUrl && pinnedUrl !== serviceUrl)
|
|
464
|
+
|| (current && current.serviceUrl !== serviceUrl)
|
|
465
|
+
|| (binding.configured && binding.serviceUrl !== serviceUrl)) {
|
|
466
|
+
return jsonResponse(409, {
|
|
467
|
+
code: 'ANT_CONNECTION_CLEANUP_REQUIRED',
|
|
468
|
+
message: '本机仍有旧团队的凭证或模型设置。请先让原团队管理员确认旧设备已撤销,再清理本机配置后连接。',
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
if (endpoint === 'dsh-connect' && current?.serviceUrl === serviceUrl) previousToken = existing.value
|
|
472
|
+
} catch {
|
|
473
|
+
return jsonResponse(500, { message: 'DSH 无法确认当前 ANT 凭证和服务地址。' })
|
|
474
|
+
}
|
|
475
|
+
|
|
372
476
|
try {
|
|
373
477
|
const upstream = await fetch(new URL(`/auth/${endpoint}`, serviceUrl), {
|
|
374
478
|
method: 'POST',
|
|
@@ -389,17 +493,17 @@ async function fetchAuthRequest(request, endpoint, credentials) {
|
|
|
389
493
|
if (typeof result?.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
|
|
390
494
|
return jsonResponse(502, { message: 'ANT 连接响应格式无效' })
|
|
391
495
|
}
|
|
392
|
-
try {
|
|
393
|
-
await pinServiceUrl(credentials, serviceUrl)
|
|
394
|
-
} catch {
|
|
395
|
-
return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
|
|
396
|
-
}
|
|
397
496
|
try {
|
|
398
497
|
const session = await updateModelSession(serviceUrl, result.token, 'heartbeat')
|
|
399
498
|
if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
|
|
400
499
|
} catch {
|
|
401
500
|
return jsonResponse(502, { message: '无法连接 ANT 模型会话服务。' })
|
|
402
501
|
}
|
|
502
|
+
try {
|
|
503
|
+
await pinServiceUrl(credentials, serviceUrl, result.token, previousToken)
|
|
504
|
+
} catch {
|
|
505
|
+
return jsonResponse(500, { message: 'DSH 无法保存已连接的 ANT 服务地址' })
|
|
506
|
+
}
|
|
403
507
|
}
|
|
404
508
|
return new Response(text, {
|
|
405
509
|
status: upstream.status,
|
|
@@ -411,8 +515,6 @@ async function fetchAuthRequest(request, endpoint, credentials) {
|
|
|
411
515
|
}
|
|
412
516
|
|
|
413
517
|
export function apply(ctx) {
|
|
414
|
-
let skillRegistry
|
|
415
|
-
ctx.inject(['skills'], (skillCtx) => { skillRegistry = skillCtx.skills })
|
|
416
518
|
ctx.inject(['connection', 'credentials', 'settings'], (connectionCtx) => {
|
|
417
519
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
418
520
|
path: SERVICE_INFO_PATH,
|
|
@@ -432,18 +534,28 @@ export function apply(ctx) {
|
|
|
432
534
|
requestBody: 'buffered',
|
|
433
535
|
fetch: (request) => fetchBundleRelease(request, connectionCtx.credentials, connectionCtx.settings),
|
|
434
536
|
}), 'ant-teams: authenticated Bundle release relay')
|
|
537
|
+
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
538
|
+
path: CONNECTION_ORIGIN_PATH,
|
|
539
|
+
methods: ['GET'],
|
|
540
|
+
requestBody: 'buffered',
|
|
541
|
+
fetch: (request) => fetchConnectionOrigin(request, connectionCtx.credentials, connectionCtx.settings),
|
|
542
|
+
}), 'ant-teams: pinned connection origin for update guide')
|
|
435
543
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
436
544
|
path: SKILLS_PATH, methods: ['GET'], requestBody: 'buffered',
|
|
437
|
-
fetch: (request) => fetchMemberSkills(request, connectionCtx.credentials, connectionCtx.settings
|
|
545
|
+
fetch: (request) => fetchMemberSkills(request, connectionCtx.credentials, connectionCtx.settings),
|
|
438
546
|
}), 'ant-teams: enterprise skill catalog')
|
|
439
547
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
440
548
|
path: SKILL_INSTALL_PATH, methods: ['POST'], requestBody: 'buffered',
|
|
441
|
-
fetch: (request) => installMemberSkill(request, connectionCtx.credentials, connectionCtx.settings
|
|
549
|
+
fetch: (request) => installMemberSkill(request, connectionCtx.credentials, connectionCtx.settings),
|
|
442
550
|
}), 'ant-teams: install approved skill into DSH')
|
|
443
551
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
444
552
|
path: SKILL_UNINSTALL_PATH, methods: ['POST'], requestBody: 'buffered',
|
|
445
553
|
fetch: (request) => uninstallMemberSkill(request),
|
|
446
554
|
}), 'ant-teams: remove managed DSH skill')
|
|
555
|
+
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
556
|
+
path: PLUGINS_PATH, methods: ['GET'], requestBody: 'buffered',
|
|
557
|
+
fetch: (request) => fetchMemberPlugins(request, connectionCtx.credentials, connectionCtx.settings),
|
|
558
|
+
}), 'ant-teams: enterprise plugin catalog')
|
|
447
559
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
448
560
|
path: DISCONNECT_PATH,
|
|
449
561
|
methods: ['POST'],
|
|
@@ -453,13 +565,13 @@ export function apply(ctx) {
|
|
|
453
565
|
path: REQUEST_CODE_PATH,
|
|
454
566
|
methods: ['POST'],
|
|
455
567
|
requestBody: 'buffered',
|
|
456
|
-
fetch: (request) => fetchAuthRequest(request, 'request-code', connectionCtx.credentials),
|
|
568
|
+
fetch: (request) => fetchAuthRequest(request, 'request-code', connectionCtx.credentials, connectionCtx.settings),
|
|
457
569
|
}), 'ant-teams: relay email verification code request')
|
|
458
570
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
459
571
|
path: CONNECT_PATH,
|
|
460
572
|
methods: ['POST'],
|
|
461
573
|
requestBody: 'buffered',
|
|
462
|
-
fetch: (request) => fetchAuthRequest(request, 'dsh-connect', connectionCtx.credentials),
|
|
574
|
+
fetch: (request) => fetchAuthRequest(request, 'dsh-connect', connectionCtx.credentials, connectionCtx.settings),
|
|
463
575
|
}), 'ant-teams: relay DSH account connection')
|
|
464
576
|
connectionCtx.effect(() => {
|
|
465
577
|
let stopped = false
|
package/package.json
CHANGED
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)
|