@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/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 serviceUrlFromModelSettings(settings) {
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 || typeof provider.baseURL !== 'string') continue
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)) return null
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
- await credentials.modifyRecord(SERVICE_RECORD_KEY, async () => ({
56
- kind: 'grant',
57
- payload: { service_url: serviceUrl },
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
- const serviceUrl = pinnedUrl || serviceUrlFromModelSettings(settings)
70
- return serviceUrl ? { serviceUrl, token } : null
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 token
113
- let pinnedUrl
114
- let serviceUrl
144
+ let connection
115
145
  try {
116
- token = await credentials.resolve(MODEL_TOKEN_REF)
117
- const record = await credentials.readRecord(SERVICE_RECORD_KEY)
118
- pinnedUrl = record?.kind === 'grant' && typeof record.payload?.service_url === 'string'
119
- ? normalizeServiceUrl(record.payload.service_url)
120
- : null
121
- serviceUrl = pinnedUrl || serviceUrlFromModelSettings(settings)
146
+ connection = await readMemberConnection(credentials, settings)
122
147
  } catch {
123
148
  return jsonResponse(500, { message: 'DSH 无法读取 ANT 连接配置。' })
124
149
  }
125
- if (!token || !serviceUrl) return jsonResponse(401, { message: '当前 DSH 未连接 ANT,请验证工作邮箱。' })
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.value, 'heartbeat')
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.value}`, accept: 'application/json' },
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 插件版本不能安全自动安装 scoped Bundle。请通过 DSH 官方插件管理器完成一次包名迁移。' })
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, release)
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(200, { revoked: false })
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 || response.status === 401 || response.status === 403) {
300
- return jsonResponse(200, { revoked: response.status === 204 })
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(200, { revoked: false })
348
+ return jsonResponse(502, { message: '服务端未确认永久撤销,本机凭证仍保留。请稍后重试。' })
303
349
  } catch {
304
- return jsonResponse(200, { revoked: false })
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, registry) {
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, registry) })
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, registry) {
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, registry))
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 fetchAuthRequest(request, endpoint, credentials) {
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, skillRegistry),
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, skillRegistry),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-wendao/ant",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "ANT (AI-native Teams) connects a DeepSeek Harness Desktop profile to team identity and approved models.",
5
5
  "type": "module",
6
6
  "scripts": {
package/skill-install.js CHANGED
@@ -12,19 +12,6 @@ const MAX_PACKAGE_BYTES = 80 * 1024 * 1024
12
12
  const MAX_JSON_BYTES = 4 * 1024 * 1024
13
13
  const activeSkills = new Set()
14
14
 
15
- async function assertDiscovered(registry, root, name) {
16
- if (typeof registry?.list !== 'function') throw new Error('DSH 技能服务不可用,无法确认安装结果。')
17
- const expected = resolve(root, name, 'SKILL.md')
18
- for (let attempt = 0; attempt < 6; attempt++) {
19
- const entries = await registry.list()
20
- if (Array.isArray(entries) && entries.some((entry) => entry.name === name
21
- && typeof entry.path === 'string'
22
- && (process.platform === 'win32' ? resolve(entry.path).toLowerCase() === expected.toLowerCase() : resolve(entry.path) === expected))) return
23
- if (attempt < 5) await new Promise((done) => setTimeout(done, 300))
24
- }
25
- throw new Error('DSH 没有发现此技能;请检查 DSH 技能目录配置。')
26
- }
27
-
28
15
  const hash = (bytes) => createHash('sha256').update(bytes).digest('hex')
29
16
  const safeName = (name) => typeof name === 'string' && /^ant-[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name) && name.length <= 64
30
17
  const safePath = (name) => typeof name === 'string' && name.length > 0 && name.length <= 1024
@@ -164,7 +151,7 @@ async function fetchJson(url, token) {
164
151
  try { return JSON.parse(bytes.toString('utf8')) } catch { throw new Error('团队技能目录格式无效。') }
165
152
  }
166
153
 
167
- export async function listSkills(connection, registry) {
154
+ export async function listSkills(connection) {
168
155
  const skills = await fetchJson(new URL('/api/me/skills', connection.serviceUrl), connection.token)
169
156
  if (!Array.isArray(skills)) throw new Error('团队技能目录格式无效。')
170
157
  const { root, conflicts } = await safeRoot()
@@ -180,10 +167,6 @@ export async function listSkills(connection, registry) {
180
167
  local_status = marker.content_sha256 === skill.content_sha256 ? 'installed' : 'update-available'
181
168
  }
182
169
  } catch { local_status = 'conflict' }
183
- if (local_status === 'installed' || local_status === 'update-available') {
184
- try { await assertDiscovered(registry, root, skill.dsh_name) }
185
- catch { local_status = 'not-discovered' }
186
- }
187
170
  return { ...skill, local_status }
188
171
  }))
189
172
  }
@@ -220,7 +203,7 @@ function contentDigest(files) {
220
203
  return digest.digest('hex')
221
204
  }
222
205
 
223
- export async function installSkill(connection, skillId, registry) {
206
+ export async function installSkill(connection, skillId) {
224
207
  if (typeof skillId !== 'string' || !/^[0-9a-f-]{36}$/iu.test(skillId)) throw new Error('技能编号无效。')
225
208
  const manifest = await fetchJson(new URL(`/api/me/skills/${skillId}/files`, connection.serviceUrl), connection.token)
226
209
  validateManifest(manifest, skillId)
@@ -228,18 +211,17 @@ export async function installSkill(connection, skillId, registry) {
228
211
  if (conflicts.has(manifest.dsh_name)) throw new Error('本机技能备份已损坏;请联系管理员处理,ANT 不会覆盖。')
229
212
  if (activeSkills.has(manifest.dsh_name)) throw new Error('这个技能正在安装或卸载,请稍后重试。')
230
213
  activeSkills.add(manifest.dsh_name)
231
- try { return await installValidatedSkill(connection, skillId, manifest, root, registry) }
214
+ try { return await installValidatedSkill(connection, skillId, manifest, root) }
232
215
  finally { activeSkills.delete(manifest.dsh_name) }
233
216
  }
234
217
 
235
- async function installValidatedSkill(connection, skillId, manifest, root, registry) {
218
+ async function installValidatedSkill(connection, skillId, manifest, root) {
236
219
  const target = join(root, manifest.dsh_name)
237
220
  const oldMarker = await readManaged(root, manifest.dsh_name)
238
221
  if (oldMarker) {
239
222
  if (oldMarker.service_url !== connection.serviceUrl) throw new Error('同名技能属于另一个 ANT 服务,无法覆盖。')
240
223
  await verifyManagedTree(root, manifest.dsh_name, oldMarker)
241
224
  if (oldMarker.content_sha256 === manifest.content_sha256) {
242
- await assertDiscovered(registry, root, manifest.dsh_name)
243
225
  return { status: 'installed', dsh_name: manifest.dsh_name }
244
226
  }
245
227
  }
@@ -268,8 +250,10 @@ async function installValidatedSkill(connection, skillId, manifest, root, regist
268
250
  const stagingStat = await isPresent(stagingRoot)
269
251
  if (stagingStat && (!stagingStat.isDirectory() || stagingStat.isSymbolicLink())) throw new Error('ANT 暂存目录不是普通目录。')
270
252
  await mkdir(stagingRoot, { recursive: true, mode: 0o700 })
271
- const staging = join(stagingRoot, `new-${randomUUID()}`)
253
+ const stagingName = `new-${randomUUID()}`
254
+ const staging = join(stagingRoot, stagingName)
272
255
  const backup = join(stagingRoot, `old-${manifest.dsh_name}-${randomUUID()}`)
256
+ const managedFiles = manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable }))
273
257
  await mkdir(staging, { mode: 0o700 })
274
258
  let backedUp = false
275
259
  let installedNew = false
@@ -283,29 +267,25 @@ async function installValidatedSkill(connection, skillId, manifest, root, regist
283
267
  await writeFile(join(staging, MARKER), JSON.stringify({
284
268
  dsh_name: manifest.dsh_name, skill_id: skillId, service_url: connection.serviceUrl,
285
269
  content_sha256: manifest.content_sha256,
286
- files: manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable })),
270
+ files: managedFiles,
287
271
  }), { flag: 'wx', mode: 0o600 })
272
+ await verifyManagedTree(stagingRoot, stagingName, { files: managedFiles })
288
273
  if (oldMarker) { await verifyManagedTree(root, manifest.dsh_name, oldMarker); await rename(target, backup); backedUp = true }
289
274
  else if (await isPresent(target)) throw new Error('同名技能已经存在,ANT 不会覆盖。')
290
275
  await rename(staging, target)
291
276
  installedNew = true
292
- await assertDiscovered(registry, root, manifest.dsh_name)
293
277
  if (backedUp) await rm(backup, { recursive: true }).catch(() => {})
294
278
  return { status: oldMarker ? 'updated' : 'installed', dsh_name: manifest.dsh_name }
295
279
  } catch (error) {
296
280
  if (backedUp && await isPresent(backup)) {
297
281
  if (await isPresent(target)) {
298
- await verifyManagedTree(root, manifest.dsh_name, {
299
- dsh_name: manifest.dsh_name,
300
- files: manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable })),
301
- }).then(() => rename(target, staging)).catch(() => {})
282
+ await verifyManagedTree(root, manifest.dsh_name, { files: managedFiles })
283
+ .then(() => rename(target, staging)).catch(() => {})
302
284
  }
303
285
  if (!(await isPresent(target))) await rename(backup, target).catch(() => {})
304
286
  } else if (installedNew) {
305
- await verifyManagedTree(root, manifest.dsh_name, {
306
- dsh_name: manifest.dsh_name,
307
- files: manifest.files.map(({ path, sha256, executable }) => ({ path, sha256, executable })),
308
- }).then(() => rm(target, { recursive: true })).catch(() => {})
287
+ await verifyManagedTree(root, manifest.dsh_name, { files: managedFiles })
288
+ .then(() => rm(target, { recursive: true })).catch(() => {})
309
289
  }
310
290
  throw error
311
291
  } finally { await rm(staging, { recursive: true, force: true }).catch(() => {}) }
@@ -443,11 +443,13 @@ async function invokeDsh(cli, args, label, dshHome) {
443
443
  if (result.status !== 0) throw new Error(`${label}失败,退出码 ${String(result.status)}`)
444
444
  }
445
445
 
446
- async function rollbackWithCli(cli, profile, aliasSpec) {
446
+ async function rollbackWithCli(cli, profile, aliasSpec, targetVersion) {
447
447
  let current = await readProfile(profile.dshHome)
448
- if (current.state === 'both') {
449
- await invokeDsh(cli, ['remove', PACKAGE_NAME], '移除未完成的 scoped Bundle', profile.dshHome)
448
+ if (current.state !== 'both' || current.aliasSpec !== aliasSpec
449
+ || !directSpecMatchesTarget(current.directSpec, targetVersion)) {
450
+ throw new Error('scoped ANT 已与迁移记录目标不同;工具不会删除现有版本。')
450
451
  }
452
+ await invokeDsh(cli, ['remove', PACKAGE_NAME], '移除未完成的 scoped Bundle', profile.dshHome)
451
453
  current = await readProfile(profile.dshHome)
452
454
  if (current.aliasSpec === undefined) {
453
455
  await invokeDsh(cli, ['add', `${ALIAS_NAME}@${aliasSpec}`], '恢复旧 ANT alias', profile.dshHome)
@@ -595,6 +597,9 @@ async function rollback(options) {
595
597
  if (profile.state !== 'both' || profile.aliasSpec !== record.originalAliasSpec) {
596
598
  throw new Error('当前状态无法无损取消迁移;该工具不会重建旧 alias 或覆盖现有依赖。优先继续 migrate --apply。')
597
599
  }
600
+ if (!directSpecMatchesTarget(profile.directSpec, record.targetVersion)) {
601
+ throw new Error('scoped ANT 已与迁移记录目标不同;工具不会删除现有版本。')
602
+ }
598
603
  if (!options.apply) {
599
604
  process.stdout.write(`预检完成。确认 DSH 已完全退出后运行:ant-migrate rollback --backup "${absolute}" --apply\n`)
600
605
  return
@@ -607,10 +612,11 @@ async function rollback(options) {
607
612
  showDshOutput(listResult)
608
613
  if (listResult.error || listResult.status !== 0) throw new Error('DSH Desktop CLI 未能读取 desktop profile。')
609
614
  const beforeRollback = await readProfile(dshHome)
610
- if (beforeRollback.state !== 'both' || beforeRollback.aliasSpec !== record.originalAliasSpec) {
615
+ if (beforeRollback.state !== 'both' || beforeRollback.aliasSpec !== record.originalAliasSpec
616
+ || !directSpecMatchesTarget(beforeRollback.directSpec, record.targetVersion)) {
611
617
  throw new Error('执行回退前 profile 已变化;工具不会继续。')
612
618
  }
613
- await rollbackWithCli(cli, { dshHome }, record.originalAliasSpec)
619
+ await rollbackWithCli(cli, { dshHome }, record.originalAliasSpec, record.targetVersion)
614
620
  record.status = 'rolled-back'
615
621
  record.phase = 'rolled-back'
616
622
  await saveBackupRecord(absolute, record)