@ai-wendao/ant 0.3.8 → 0.3.10
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 +83 -0
- package/README.md +9 -0
- package/client.js +68 -19
- package/cordis.patch.yml +1 -1
- package/index.js +16 -4
- package/package.json +6 -3
- package/tools/migrate-legacy.mjs +639 -0
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# DSH Agent 操作说明:迁移旧 ANT alias
|
|
2
|
+
|
|
3
|
+
本说明用于指导已安装旧 `ant` alias 的成员迁移,不代表允许静默修改 profile。修改前必须向成员说明操作并取得确认。
|
|
4
|
+
|
|
5
|
+
## 适用范围
|
|
6
|
+
|
|
7
|
+
仅迁移 DSH Desktop 的 `desktop` profile 中以依赖键 `ant` 安装、指向 `@ai-wendao/ant` 的旧 npm alias。已经直接安装 `@ai-wendao/ant` 的 profile 无需迁移。检测到其他来源、重复依赖、异常清单时停止,不要自行修复。
|
|
8
|
+
|
|
9
|
+
目标 DSH RC2 要求 Bundle 客户端模块身份与 npm 包名称一致。旧 alias 会让冷启动时的客户端身份不匹配。新版应直接安装 `@ai-wendao/ant`。**普通 Desktop profile 优先使用 DSH 自带的插件管理界面;不需要系统 Node.js 或 npm。**旧 alias 先卸载,新包安装完成后再启用。不要删除整个 DSH profile。
|
|
10
|
+
|
|
11
|
+
## 推荐方式:在 DSH 插件页操作
|
|
12
|
+
|
|
13
|
+
此方式适用于 DSH Desktop RC2 使用默认、启用 HMR 的 Desktop profile,且目标 ANT 版本已发布到 npm:
|
|
14
|
+
|
|
15
|
+
1. 打开 **插件 → ANT**,点击 **卸载** 并确认。
|
|
16
|
+
2. 回到插件列表,点击 **添加插件**,输入已发布的直接包名,例如 `@ai-wendao/ant@0.3.10`,然后点击 **安装**。
|
|
17
|
+
3. 安装完成后启用 ANT,打开设置页,检查服务连接与获准模型。
|
|
18
|
+
|
|
19
|
+
RC2 的 Desktop 基础 profile 默认启用 HMR;官方管理器在该模式下会先卸载 Bundle,再用随 DSH 提供的包管理器移除依赖。包卸载本身不会调用 ANT 的“断开设备”操作,也不会清空 DSH 凭证存储。若 profile 禁用了 HMR,管理器会提示先停止 profile;此时改用下方 CLI 备用流程。真实成员 profile 的凭证保留仍需在目标 RC2 客户端上验收。
|
|
20
|
+
|
|
21
|
+
## CLI 备用方式
|
|
22
|
+
|
|
23
|
+
仅当 DSH 插件页提示需要停止 profile,或需要管理员批量、受控处理时使用。该 CLI 是同一份跨 macOS/Windows 的 JavaScript 文件,不是双击式安装器;要求 Node.js 20+、npm 和 DSH Desktop CLI,并需要在终端执行。若团队要求成员不安装 Node/npm 且不使用终端,这条备用路径不满足该体验,需要另行选定免安装分发方式。
|
|
24
|
+
|
|
25
|
+
### 执行规程
|
|
26
|
+
|
|
27
|
+
1. 仅当决定使用 CLI 备用路径时,运行 `ant-migrate inspect`。它只检查 `DSH_HOME/profiles/desktop/package.json`,不读取凭证文件或把密钥复制到命令参数、日志或备份。
|
|
28
|
+
2. 若状态是 `current` 或 `not-installed`,结束流程。若是 `conflict`、`unknown-alias` 或 `inconsistent`,停止并报告诊断,不要尝试清理。
|
|
29
|
+
3. 对 `legacy-alias`,向成员解释需要完全退出并重新打开 DSH、迁移期间不会运行 DSH,以及失败会保留备份;确认成员没有需要继续运行的任务,并取得明确确认。
|
|
30
|
+
4. 先运行 `ant-migrate migrate` 查看预检结果。让成员完全退出 DSH,再从独立终端运行下方的 `--apply` 命令。迁移助手确认 DSH 已退出、CLI 能读取 Desktop profile 后,会备份依赖清单和锁文件;随后先安装 scoped 包,再移除旧 alias。
|
|
31
|
+
5. 记录命令输出的备份目录。成功后重新打开 DSH,检查 ANT 设置页、连接状态和获准模型;如需重新验证邮箱,先检查凭证状态,不要自动清空或重建 profile。
|
|
32
|
+
6. 安装新版失败时,旧 alias 会保留。若在“新旧包都已安装”的中间状态中断,保持 DSH 关闭并重跑 `migrate --apply` 续跑;`rollback` 仅用于在该中间态取消迁移。完成迁移后不提供回到旧 alias 的自动回滚,因为这会恢复已知的加载故障。不要手工改 `package.json`、`pnpm-lock.yaml`,不要复制 `node_modules`。
|
|
33
|
+
|
|
34
|
+
DSH 必须完全退出后才能运行包变更命令。迁移工具是独立进程,不依赖 DSH 退出后仍有子进程驻留;它不会静默关闭 DSH、终止任务或自动重启应用。迁移期间工具会持有 RC2 Desktop profile 启动锁,防止并发打开 profile;请等命令结束后再启动 DSH。过程若被中断,备份目录中的状态记录用于安全续跑,不应手工编辑 profile 清单。
|
|
35
|
+
|
|
36
|
+
## 权限与数据
|
|
37
|
+
|
|
38
|
+
迁移工具不会提权。若读取 profile 或创建备份时收到权限错误,它会停止;先为运行迁移命令的终端授予访问 DSH profile 与备份目录的权限,再重试。不要用 `sudo`,也不要把 profile 复制到其他位置来绕过系统权限。
|
|
39
|
+
|
|
40
|
+
成员凭证、ANT 服务地址和模型设置保存在 DSH/ANT 的独立存储键中,不属于 npm 包目录。卸载旧 Bundle 不会执行“断开账号”或删除这些凭证;活动会话会在 Bundle 退出时结束,重新加载后由保留的凭证恢复。工具只备份包清单和锁文件,不读取或复制密钥。
|
|
41
|
+
|
|
42
|
+
## 运行方式
|
|
43
|
+
|
|
44
|
+
要求 Node.js 20+,并需要 DSH Desktop 自带的 CLI。工具只有一份跨平台 JavaScript 实现,不需要分别签名、打包 macOS 与 Windows 应用。若终端找不到 `dsh`,在 DSH 的“更多 → 管理 dsh 命令”安装官方命令,或通过 `--dsh-cli` 传入 Desktop CLI 完整路径。Windows 使用 DSH 安装目录中的 `resources/runtime/cli/bin/dsh.cmd`;macOS 使用应用包中的 `Contents/Resources/runtime/cli/bin/dsh`。
|
|
45
|
+
|
|
46
|
+
如果从仓库运行:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
node packages/ant-bundle/tools/migrate-legacy.mjs inspect
|
|
50
|
+
node packages/ant-bundle/tools/migrate-legacy.mjs migrate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
成员完全退出 DSH 后再应用:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
node packages/ant-bundle/tools/migrate-legacy.mjs migrate --apply
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
安装了系统 Node.js 的成员也可以从已发布 npm 包运行:
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
npm exec --yes --package @ai-wendao/ant@0.3.10 -- ant-migrate inspect
|
|
63
|
+
npm exec --yes --package @ai-wendao/ant@0.3.10 -- ant-migrate migrate
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
退出 DSH 后执行变更:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npm exec --yes --package @ai-wendao/ant@0.3.10 -- ant-migrate migrate --apply
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
若 CLI 不在 PATH,可追加 `--dsh-cli "<Desktop CLI 完整路径>"`;若 DSH 使用自定义数据目录,可追加 `--dsh-home "<DSH_HOME 路径>"`。
|
|
73
|
+
|
|
74
|
+
## 回退
|
|
75
|
+
|
|
76
|
+
`migrate --apply` 会输出受限权限的备份目录,备份包含 profile 的 `package.json`、`pnpm-lock.yaml`、`pnpm-workspace.yaml`(若存在)及其 SHA-256 校验值,不包含密钥库。备份仅用于迁移状态核验和保留原始清单,不等于离线恢复包。确认 DSH 已退出后,通过同一工具调用:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
npm exec --yes --package @ai-wendao/ant@0.3.10 -- ant-migrate rollback --backup "<备份目录>"
|
|
80
|
+
npm exec --yes --package @ai-wendao/ant@0.3.10 -- ant-migrate rollback --backup "<备份目录>" --apply
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
回退只在 scoped 包已安装、旧 alias 尚未移除的中间态工作;它会卸载 scoped 包并保留旧 alias。已完成迁移的 profile 不接受该回退,避免自动恢复已知有加载问题的 alias。若安装包时 npm registry 不可用,保持 DSH 关闭,稍后重新运行 `migrate --apply`。
|
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# ANT for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
ANT connects a DeepSeek Harness Desktop profile to its team's approved models and collaboration services.
|
|
4
|
+
|
|
5
|
+
## Existing `ant` alias installations
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
For the normal Desktop profile, migrate from DSH's **Plugins** page by uninstalling the old ANT package and installing `@ai-wendao/ant` directly. This uses DSH's bundled package manager and does not require system Node.js/npm. Read [MIGRATION.md](./MIGRATION.md) first. The `ant-migrate` CLI is a fallback for profiles where the UI cannot unload the running bundle; it requires Node.js 20+, npm, and the DSH Desktop CLI. Neither path clears ANT credentials or model settings by design. This package does not publish or deploy ANT.
|
package/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({
|
|
2
|
-
id: 'ant',
|
|
2
|
+
id: '@ai-wendao/ant',
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
var module = { exports: {} }
|
|
5
5
|
var exports = module.exports
|
|
@@ -7,6 +7,7 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
const h = createElement
|
|
8
8
|
const SERVICE_URL_STORAGE_KEY = 'ant-team-service-url'
|
|
9
9
|
const DEVICE_ID_STORAGE_KEY = 'ant-team-device-id'
|
|
10
|
+
const ANT_BUNDLE_PACKAGE_NAME = '@ai-wendao/ant'
|
|
10
11
|
const ANT_API_PROTOCOL_VERSIONS = [1]
|
|
11
12
|
const panelStyle = {
|
|
12
13
|
display: 'grid', alignContent: 'start', gap: 14, padding: 18,
|
|
@@ -61,6 +62,31 @@ window.__ModuleLoader__.load({
|
|
|
61
62
|
}
|
|
62
63
|
const inject = ['slots', 'remote', 'remote.credentials', 'remote.settings', 'remote.pluginManager']
|
|
63
64
|
|
|
65
|
+
async function readLocalJsonResponse(response) {
|
|
66
|
+
const contentType = response.headers?.get?.('content-type') || ''
|
|
67
|
+
const isJson = /^(?:application|text)\/(?:[a-z0-9!#$&^_.+-]+\+)?json(?:\s*;|$)/iu.test(contentType.trim())
|
|
68
|
+
if (contentType && !isJson) {
|
|
69
|
+
if (response.status === 404) {
|
|
70
|
+
throw new Error('DSH 未找到 ANT 本地接口。请确认 ANT Bundle 已更新,并完全退出后重新启动 DSH。')
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`DSH 返回了非 JSON 响应(HTTP ${response.status})。`)
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
return await response.json()
|
|
76
|
+
} catch {
|
|
77
|
+
if (response.status === 404) {
|
|
78
|
+
throw new Error('DSH 未找到 ANT 本地接口。请确认 ANT Bundle 已更新,并完全退出后重新启动 DSH。')
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`DSH 返回的 ANT 响应格式无效(HTTP ${response.status})。`)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function directInstallSpec(release) {
|
|
85
|
+
return release?.version && release?.package_name
|
|
86
|
+
? `${release.package_name}@${release.version}`
|
|
87
|
+
: ''
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
function loadServiceUrl() {
|
|
65
91
|
try { return normalizeServiceUrl(localStorage.getItem(SERVICE_URL_STORAGE_KEY) || '') || '' } catch { return '' }
|
|
66
92
|
}
|
|
@@ -152,7 +178,7 @@ window.__ModuleLoader__.load({
|
|
|
152
178
|
if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 未提供 Bundle 版本读取接口。')
|
|
153
179
|
const listed = await manager.listBundles()
|
|
154
180
|
if (!listed.ok) throw new Error(listed.error?.message || '无法读取已安装的 ANT Bundle 版本。')
|
|
155
|
-
const installed = listed.value.find((bundle) => bundle.name ===
|
|
181
|
+
const installed = listed.value.find((bundle) => bundle.name === ANT_BUNDLE_PACKAGE_NAME && bundle.installed)
|
|
156
182
|
if (typeof installed?.version !== 'string') throw new Error('无法确定当前 ANT Bundle 版本。')
|
|
157
183
|
const response = await fetch('/api/ant-teams/service-info', {
|
|
158
184
|
method: 'POST',
|
|
@@ -163,7 +189,7 @@ window.__ModuleLoader__.load({
|
|
|
163
189
|
api_protocol_versions: ANT_API_PROTOCOL_VERSIONS,
|
|
164
190
|
}),
|
|
165
191
|
})
|
|
166
|
-
const result = await response
|
|
192
|
+
const result = await readLocalJsonResponse(response)
|
|
167
193
|
if (!response.ok) throw new Error(result.message || `版本检查失败(${response.status})`)
|
|
168
194
|
const serverProtocols = Array.isArray(result.server_api_protocol_versions) ? result.server_api_protocol_versions : []
|
|
169
195
|
const sharedProtocols = Array.isArray(result.shared_api_protocol_versions) ? result.shared_api_protocol_versions : []
|
|
@@ -234,7 +260,7 @@ window.__ModuleLoader__.load({
|
|
|
234
260
|
try {
|
|
235
261
|
await requireCompatibleService(serviceUrl, true)
|
|
236
262
|
const response = await fetch('/api/ant-teams/models', { method: 'POST' })
|
|
237
|
-
const result = await response
|
|
263
|
+
const result = await readLocalJsonResponse(response)
|
|
238
264
|
if (!response.ok) {
|
|
239
265
|
if (response.status === 401) setNeedsReconnect(true)
|
|
240
266
|
throw new Error(result.message || `刷新失败(${response.status})`)
|
|
@@ -255,9 +281,23 @@ window.__ModuleLoader__.load({
|
|
|
255
281
|
setBundleUpdate({ state: 'checking', currentVersion: '', message: '正在检查 ANT 更新…' })
|
|
256
282
|
setApprovedBuilds([])
|
|
257
283
|
try {
|
|
258
|
-
const response = await fetch('/api/ant-teams/bundle-release', {
|
|
259
|
-
|
|
284
|
+
const response = await fetch('/api/ant-teams/bundle-release', {
|
|
285
|
+
cache: 'no-store', headers: { 'x-ant-bundle-capabilities': 'direct-package-install' },
|
|
286
|
+
})
|
|
287
|
+
const release = await readLocalJsonResponse(response)
|
|
260
288
|
if (!response.ok) throw new Error(release.message || `检查失败(${response.status})`)
|
|
289
|
+
const legacyInstallSpec = `ant@npm:${ANT_BUNDLE_PACKAGE_NAME}@${release.version}`
|
|
290
|
+
const directSpec = directInstallSpec(release)
|
|
291
|
+
const validPackageName = release.package_name === ANT_BUNDLE_PACKAGE_NAME
|
|
292
|
+
const validVersion = typeof release.version === 'string'
|
|
293
|
+
&& (release.version === '' || /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u.test(release.version))
|
|
294
|
+
if (release.bundle_name !== 'ant' || !validPackageName || !validVersion
|
|
295
|
+
|| typeof release.install_spec !== 'string' || typeof release.release_notes !== 'string'
|
|
296
|
+
|| (release.version && release.install_spec !== legacyInstallSpec)
|
|
297
|
+
|| (!release.version && release.install_spec !== '')
|
|
298
|
+
|| (release.direct_install_spec !== undefined && release.direct_install_spec !== directSpec)) {
|
|
299
|
+
throw new Error('ANT 更新信息字段无效。')
|
|
300
|
+
}
|
|
261
301
|
setBundleRelease(release)
|
|
262
302
|
if (!release.version) {
|
|
263
303
|
setBundleUpdate({ state: 'none', currentVersion: '', message: '管理员尚未发布 ANT 更新。' })
|
|
@@ -267,9 +307,17 @@ window.__ModuleLoader__.load({
|
|
|
267
307
|
if (typeof manager?.listBundles !== 'function') throw new Error('当前 DSH 版本未提供官方 Bundle 管理器。')
|
|
268
308
|
const listed = await manager.listBundles()
|
|
269
309
|
if (!listed.ok) throw new Error(listed.error?.message || '无法读取 DSH 已安装 Bundle。')
|
|
270
|
-
const installed = listed.value.find((bundle) => bundle.name === release.
|
|
310
|
+
const installed = listed.value.find((bundle) => bundle.name === release.package_name && bundle.installed)
|
|
271
311
|
if (!installed || typeof installed.version !== 'string') {
|
|
272
|
-
|
|
312
|
+
const legacy = listed.value.find((bundle) => bundle.name === 'ant' && bundle.installed)
|
|
313
|
+
if (legacy) {
|
|
314
|
+
setBundleUpdate({
|
|
315
|
+
state: 'migration-required', currentVersion: legacy.version || '',
|
|
316
|
+
message: `当前安装使用旧 alias(${legacy.name})。请在 DSH 插件管理器卸载旧 ANT,再直接安装 ${release.package_name}@${release.version} 并重启 DSH。若管理器无法完成迁移,请参考 ANT 包内的备用迁移说明。`,
|
|
317
|
+
})
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
throw new Error(`当前 DSH 未找到已安装的 ${release.package_name} Bundle。`)
|
|
273
321
|
}
|
|
274
322
|
const available = newerVersion(release.version, installed.version)
|
|
275
323
|
setBundleUpdate({
|
|
@@ -283,7 +331,8 @@ window.__ModuleLoader__.load({
|
|
|
283
331
|
}
|
|
284
332
|
|
|
285
333
|
const installBundleUpdate = async (allowScripts = false) => {
|
|
286
|
-
|
|
334
|
+
const installSpec = directInstallSpec(bundleRelease)
|
|
335
|
+
if (!installSpec
|
|
287
336
|
|| (!allowScripts && bundleUpdate.state !== 'available')
|
|
288
337
|
|| (allowScripts && approvedBuilds.length === 0)) return
|
|
289
338
|
const manager = remote?.pluginManager
|
|
@@ -297,7 +346,7 @@ window.__ModuleLoader__.load({
|
|
|
297
346
|
setApprovedBuilds([])
|
|
298
347
|
setBundleUpdate((current) => ({ ...current, state: 'installing', message: '正在委托 DSH 官方管理器安装更新…' }))
|
|
299
348
|
try {
|
|
300
|
-
const result = await manager.installBundle(
|
|
349
|
+
const result = await manager.installBundle(installSpec, {
|
|
301
350
|
enabled: true,
|
|
302
351
|
...(scripts.length ? { approvedBuilds: scripts } : {}),
|
|
303
352
|
})
|
|
@@ -323,15 +372,15 @@ window.__ModuleLoader__.load({
|
|
|
323
372
|
setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 已记录更新,但其他插件配置覆盖了 ANT。请检查 DSH 插件配置后再重启。' })
|
|
324
373
|
return
|
|
325
374
|
}
|
|
326
|
-
if (outcome.bundle !== bundleRelease.
|
|
375
|
+
if (outcome.bundle !== bundleRelease.package_name) {
|
|
327
376
|
setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: 'DSH 返回的安装包名称与 ANT 发布信息不一致。请联系管理员核对发布包。' })
|
|
328
377
|
return
|
|
329
378
|
}
|
|
330
379
|
setBundleUpdate({
|
|
331
380
|
state: 'done', currentVersion: bundleRelease.version,
|
|
332
381
|
message: outcome.application === 'restart-required'
|
|
333
|
-
? `DSH 已安装 ${
|
|
334
|
-
: `DSH 已安装 ${
|
|
382
|
+
? `DSH 已安装 ${installSpec}。请完全退出并重新启动 DSH 后生效。`
|
|
383
|
+
: `DSH 已安装 ${installSpec}。`,
|
|
335
384
|
})
|
|
336
385
|
} catch (error) {
|
|
337
386
|
setBundleUpdate({ state: 'failed', currentVersion: bundleUpdate.currentVersion, message: error instanceof Error ? error.message : 'DSH 未能安装 ANT 更新。' })
|
|
@@ -358,7 +407,7 @@ window.__ModuleLoader__.load({
|
|
|
358
407
|
let serverRevoked = false
|
|
359
408
|
try {
|
|
360
409
|
const response = await fetch('/api/ant-teams/disconnect', { method: 'POST' })
|
|
361
|
-
const result = await response
|
|
410
|
+
const result = await readLocalJsonResponse(response)
|
|
362
411
|
serverRevoked = response.ok && result.revoked === true
|
|
363
412
|
} catch {}
|
|
364
413
|
let credentialRemoved = false
|
|
@@ -422,7 +471,7 @@ window.__ModuleLoader__.load({
|
|
|
422
471
|
headers: { 'Content-Type': 'application/json' },
|
|
423
472
|
body: JSON.stringify({ service_url: normalized, email: normalizedEmail }),
|
|
424
473
|
})
|
|
425
|
-
const result = await response
|
|
474
|
+
const result = await readLocalJsonResponse(response)
|
|
426
475
|
if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
|
|
427
476
|
setEmail(normalizedEmail)
|
|
428
477
|
setCodeRequested(true)
|
|
@@ -458,7 +507,7 @@ window.__ModuleLoader__.load({
|
|
|
458
507
|
device_id: loadDeviceId(),
|
|
459
508
|
}),
|
|
460
509
|
})
|
|
461
|
-
const result = await response
|
|
510
|
+
const result = await readLocalJsonResponse(response)
|
|
462
511
|
if (!response.ok) throw new Error(result.message || `请求失败(${response.status})`)
|
|
463
512
|
if (typeof result.token !== 'string' || !result.token.startsWith('antm_') || !Array.isArray(result.models)) {
|
|
464
513
|
throw new Error('ANT 返回的连接信息格式无效。')
|
|
@@ -490,7 +539,7 @@ window.__ModuleLoader__.load({
|
|
|
490
539
|
headers: { 'Content-Type': 'application/json' },
|
|
491
540
|
body: JSON.stringify(body),
|
|
492
541
|
})
|
|
493
|
-
const result = await response
|
|
542
|
+
const result = await readLocalJsonResponse(response)
|
|
494
543
|
if (!response.ok) throw new Error(result.message || `飞书操作失败(${response.status})`)
|
|
495
544
|
return result
|
|
496
545
|
}
|
|
@@ -593,7 +642,7 @@ window.__ModuleLoader__.load({
|
|
|
593
642
|
h('button', { type: 'button', disabled: busy || bundleUpdate.state === 'checking', onClick: () => void checkBundleUpdate(), style: secondaryButtonStyle }, bundleUpdate.state === 'checking' ? '检查中…' : '检查更新')),
|
|
594
643
|
bundleUpdate.message ? h('p', { role: 'status', style: mutedTextStyle }, bundleUpdate.message) : null,
|
|
595
644
|
bundleRelease?.version && bundleUpdate.state === 'available' ? h('div', { style: { display: 'grid', gap: 8 } },
|
|
596
|
-
h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · npm:${bundleRelease
|
|
645
|
+
h('p', { style: mutedTextStyle }, `发布版本:${bundleRelease.version} · npm:${directInstallSpec(bundleRelease)}`),
|
|
597
646
|
bundleRelease.release_notes ? h('p', { style: mutedTextStyle }, bundleRelease.release_notes) : null,
|
|
598
647
|
h('button', { type: 'button', disabled: busy, onClick: () => void installBundleUpdate(), style: primaryButtonStyle }, busy ? '安装中…' : '由 DSH 安装并更新')) : null,
|
|
599
648
|
approvedBuilds.length ? h('div', { style: { display: 'grid', gap: 8 } },
|
|
@@ -667,7 +716,7 @@ window.__ModuleLoader__.load({
|
|
|
667
716
|
}
|
|
668
717
|
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
669
718
|
name: 'plugins.bundle.config',
|
|
670
|
-
key:
|
|
719
|
+
key: ANT_BUNDLE_PACKAGE_NAME,
|
|
671
720
|
inject: () => ({ remote }),
|
|
672
721
|
}, ({ view, remote: injectedRemote }) => view === 'summary'
|
|
673
722
|
? h('p', null, 'ANT 团队服务入口。')
|
package/cordis.patch.yml
CHANGED
package/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const FEISHU_CLAIM_PATH = '/api/ant-teams/feishu/tasks/claim'
|
|
|
14
14
|
const FEISHU_HEARTBEAT_PATH = '/api/ant-teams/feishu/tasks/heartbeat'
|
|
15
15
|
const FEISHU_COMPLETE_PATH = '/api/ant-teams/feishu/tasks/complete'
|
|
16
16
|
const FEISHU_FINISH_PATH = '/api/ant-teams/feishu/tasks/finish'
|
|
17
|
+
const ANT_NPM_PACKAGE_NAME = '@ai-wendao/ant'
|
|
17
18
|
const MAX_MODEL_RESPONSE_BYTES = 1024 * 1024
|
|
18
19
|
const MODEL_TOKEN_REF = 'ANT_MODEL_API_KEY'
|
|
19
20
|
const SERVICE_RECORD_KEY = 'ant/team-service'
|
|
@@ -245,28 +246,38 @@ async function fetchBundleRelease(request, credentials, settings) {
|
|
|
245
246
|
const session = await updateModelSession(connection.serviceUrl, connection.token, 'heartbeat')
|
|
246
247
|
if (session.status === 401 || session.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
|
|
247
248
|
if (!session.ok) return jsonResponse(502, { message: '无法激活 ANT 模型会话。' })
|
|
249
|
+
const headers = { authorization: `Bearer ${connection.token}`, accept: 'application/json' }
|
|
250
|
+
if (request.headers.get('x-ant-bundle-capabilities') === 'direct-package-install') {
|
|
251
|
+
headers['x-ant-bundle-capabilities'] = 'direct-package-install'
|
|
252
|
+
}
|
|
248
253
|
const upstream = await fetch(new URL('/api/me/bundle-release', connection.serviceUrl), {
|
|
249
|
-
headers
|
|
254
|
+
headers,
|
|
250
255
|
redirect: 'manual',
|
|
251
256
|
signal: AbortSignal.timeout(10_000),
|
|
252
257
|
})
|
|
253
258
|
const text = await readLimitedBody(upstream)
|
|
254
259
|
if (text === null) return jsonResponse(502, { message: 'ANT 更新信息响应过大。' })
|
|
255
260
|
if (upstream.status === 401 || upstream.status === 403) return jsonResponse(401, { message: 'ANT 成员凭证已失效。' })
|
|
261
|
+
if (upstream.status === 426) {
|
|
262
|
+
return jsonResponse(426, { message: '当前 ANT 插件版本不能安全自动安装 scoped Bundle。请通过 DSH 官方插件管理器完成一次包名迁移。' })
|
|
263
|
+
}
|
|
256
264
|
if (!upstream.ok) return jsonResponse(502, { message: 'ANT 暂时无法提供插件更新信息。' })
|
|
257
265
|
let release
|
|
258
266
|
try { release = JSON.parse(text) } catch { return jsonResponse(502, { message: 'ANT 更新信息格式无效。' }) }
|
|
259
|
-
const validPackageName =
|
|
260
|
-
&& (release.package_name === 'ant' || /^@[a-z0-9][a-z0-9._-]*\/ant$/u.test(release.package_name))
|
|
267
|
+
const validPackageName = release?.package_name === ANT_NPM_PACKAGE_NAME
|
|
261
268
|
const validVersion = typeof release?.version === 'string'
|
|
262
269
|
&& (release.version === '' || /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/u.test(release.version))
|
|
263
270
|
const installSpec = release?.package_name === 'ant'
|
|
264
271
|
? `ant@${release?.version}`
|
|
265
272
|
: `ant@npm:${release?.package_name}@${release?.version}`
|
|
273
|
+
const directInstallSpec = release?.package_name && release?.version
|
|
274
|
+
? `${release.package_name}@${release.version}`
|
|
275
|
+
: ''
|
|
266
276
|
if (release?.bundle_name !== 'ant' || !validPackageName || !validVersion
|
|
267
277
|
|| typeof release?.install_spec !== 'string' || typeof release?.release_notes !== 'string'
|
|
268
278
|
|| (release.version && release.install_spec !== installSpec)
|
|
269
|
-
|| (!release.version && release.install_spec !== '')
|
|
279
|
+
|| (!release.version && release.install_spec !== '')
|
|
280
|
+
|| (release.direct_install_spec !== undefined && release.direct_install_spec !== directInstallSpec)) {
|
|
270
281
|
return jsonResponse(502, { message: 'ANT 更新信息字段无效。' })
|
|
271
282
|
}
|
|
272
283
|
return jsonResponse(200, release)
|
|
@@ -598,6 +609,7 @@ export function apply(ctx) {
|
|
|
598
609
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
|
599
610
|
path: BUNDLE_RELEASE_PATH,
|
|
600
611
|
methods: ['GET'],
|
|
612
|
+
requestBody: 'buffered',
|
|
601
613
|
fetch: (request) => fetchBundleRelease(request, connectionCtx.credentials, connectionCtx.settings),
|
|
602
614
|
}), 'ant-teams: authenticated Bundle release relay')
|
|
603
615
|
connectionCtx.effect(() => connectionCtx.connection.fetch.register({
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-wendao/ant",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "AI-native team service for DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node --test client.test.js index.test.js"
|
|
7
|
+
"test": "node --test client.test.js index.test.js tools/migrate-legacy.test.mjs"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"ant-migrate": "tools/migrate-legacy.mjs"
|
|
8
11
|
},
|
|
9
12
|
"main": "index.js",
|
|
10
13
|
"exports": {
|
|
@@ -13,7 +16,7 @@
|
|
|
13
16
|
"./client": "./client.js",
|
|
14
17
|
"./locale/*.json": "./locale/*.json"
|
|
15
18
|
},
|
|
16
|
-
"files": ["index.js", "client.js", "cordis.patch.yml", "locale/*.json"],
|
|
19
|
+
"files": ["index.js", "client.js", "cordis.patch.yml", "locale/*.json", "tools/migrate-legacy.mjs", "MIGRATION.md", "README.md"],
|
|
17
20
|
"dsh": {
|
|
18
21
|
"bundle": {
|
|
19
22
|
"patch": "./cordis.patch.yml"
|
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto'
|
|
5
|
+
import { chmod, lstat, mkdir, open, readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const PACKAGE_NAME = '@ai-wendao/ant'
|
|
11
|
+
const ALIAS_NAME = 'ant'
|
|
12
|
+
const PROFILE_NAME = 'desktop'
|
|
13
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
14
|
+
const PACKAGE_VERSION = JSON.parse(await readFile(join(PACKAGE_ROOT, 'package.json'), 'utf8')).version
|
|
15
|
+
const PROFILE_FILES = ['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
process.stdout.write(`ANT 存量 Bundle 迁移工具
|
|
19
|
+
|
|
20
|
+
用法:
|
|
21
|
+
ant-migrate inspect [--dsh-home <路径>]
|
|
22
|
+
ant-migrate migrate [--apply] [--dsh-cli <路径>] [--dsh-home <路径>]
|
|
23
|
+
ant-migrate rollback --backup <目录> [--apply] [--dsh-cli <路径>]
|
|
24
|
+
|
|
25
|
+
migrate 和 rollback 默认只预检;只有显式传入 --apply 才会修改 DSH profile。
|
|
26
|
+
`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const [command = 'inspect', ...rest] = argv
|
|
31
|
+
if (command === '--help' || command === '-h' || command === 'help') return { help: true }
|
|
32
|
+
if (!['inspect', 'migrate', 'rollback'].includes(command)) throw new Error(`未知命令:${command}`)
|
|
33
|
+
const options = { command, apply: false }
|
|
34
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
35
|
+
const item = rest[index]
|
|
36
|
+
if (item === '--apply') options.apply = true
|
|
37
|
+
else if (item === '--dsh-home' || item === '--dsh-cli' || item === '--backup') {
|
|
38
|
+
const value = rest[index + 1]
|
|
39
|
+
if (!value || value.startsWith('--')) throw new Error(`${item} 需要一个路径`)
|
|
40
|
+
options[item.slice(2).replaceAll('-', '_')] = value
|
|
41
|
+
index += 1
|
|
42
|
+
} else throw new Error(`未知参数:${item}`)
|
|
43
|
+
}
|
|
44
|
+
if (command === 'rollback' && !options.backup) throw new Error('rollback 需要 --backup <备份目录>')
|
|
45
|
+
if (command !== 'rollback' && options.backup) throw new Error('--backup 仅用于 rollback')
|
|
46
|
+
return options
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveDshHome(configured) {
|
|
50
|
+
const candidate = configured?.trim() || process.env.DSH_HOME?.trim() || join(homedir(), '.dsh')
|
|
51
|
+
const expanded = candidate === '~' ? homedir()
|
|
52
|
+
: candidate.startsWith('~/') || candidate.startsWith('~\\')
|
|
53
|
+
? join(homedir(), candidate.slice(2))
|
|
54
|
+
: candidate
|
|
55
|
+
return resolve(expanded)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function aliasPointsToAnt(spec) {
|
|
59
|
+
return typeof spec === 'string' && /^npm:@ai-wendao\/ant(?:@[^\s]+)?$/u.test(spec)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function directSpecIsAnt(spec) {
|
|
63
|
+
return typeof spec === 'string' && /^(?:=|\^|~)?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(spec)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function directSpecMatchesTarget(spec, version = PACKAGE_VERSION) {
|
|
67
|
+
return spec === version
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readProfile(dshHome) {
|
|
71
|
+
const dir = join(dshHome, 'profiles', PROFILE_NAME)
|
|
72
|
+
const manifestPath = join(dir, 'package.json')
|
|
73
|
+
let manifest
|
|
74
|
+
try {
|
|
75
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error?.code === 'ENOENT') throw new Error(`找不到 DSH Desktop profile:${manifestPath}。请先打开并初始化 DSH。`)
|
|
78
|
+
throw new Error(`无法读取 DSH profile 清单:${error instanceof Error ? error.message : String(error)}`)
|
|
79
|
+
}
|
|
80
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
81
|
+
throw new Error('DSH profile 清单不是 JSON 对象;工具不会修改。')
|
|
82
|
+
}
|
|
83
|
+
const dependencies = manifest.dependencies ?? {}
|
|
84
|
+
if (typeof dependencies !== 'object' || Array.isArray(dependencies)) {
|
|
85
|
+
throw new Error('DSH profile dependencies 格式异常;工具不会修改。')
|
|
86
|
+
}
|
|
87
|
+
const aliasSpec = dependencies[ALIAS_NAME]
|
|
88
|
+
const directSpec = dependencies[PACKAGE_NAME]
|
|
89
|
+
const bundles = manifest.dsh?.profile?.bundles ?? []
|
|
90
|
+
if (!Array.isArray(bundles) || !bundles.every((name) => typeof name === 'string')) {
|
|
91
|
+
throw new Error('DSH profile Bundle 列表格式异常;工具不会修改。')
|
|
92
|
+
}
|
|
93
|
+
if (aliasSpec !== undefined && directSpec !== undefined) {
|
|
94
|
+
if (aliasPointsToAnt(aliasSpec) && directSpecIsAnt(directSpec)
|
|
95
|
+
&& bundles.includes(ALIAS_NAME) && bundles.includes(PACKAGE_NAME)) {
|
|
96
|
+
return { state: 'both', dir, manifestPath, manifest, aliasSpec, directSpec, bundles }
|
|
97
|
+
}
|
|
98
|
+
return { state: 'conflict', dir, manifestPath, manifest, aliasSpec, directSpec, bundles }
|
|
99
|
+
}
|
|
100
|
+
if (directSpec !== undefined) {
|
|
101
|
+
if (!bundles.includes(PACKAGE_NAME)) return { state: 'inconsistent', dir, manifestPath, manifest, directSpec, bundles }
|
|
102
|
+
return { state: 'current', dir, manifestPath, manifest, directSpec, bundles }
|
|
103
|
+
}
|
|
104
|
+
if (aliasSpec !== undefined) {
|
|
105
|
+
if (!aliasPointsToAnt(aliasSpec)) {
|
|
106
|
+
return { state: 'unknown-alias', dir, manifestPath, manifest, aliasSpec, bundles }
|
|
107
|
+
}
|
|
108
|
+
if (!bundles.includes(ALIAS_NAME)) {
|
|
109
|
+
return { state: 'inconsistent', dir, manifestPath, manifest, aliasSpec, bundles }
|
|
110
|
+
}
|
|
111
|
+
return { state: 'legacy-alias', dir, manifestPath, manifest, aliasSpec, bundles }
|
|
112
|
+
}
|
|
113
|
+
if (bundles.includes(ALIAS_NAME) || bundles.includes(PACKAGE_NAME)) {
|
|
114
|
+
return { state: 'inconsistent', dir, manifestPath, manifest, bundles }
|
|
115
|
+
}
|
|
116
|
+
return { state: 'not-installed', dir, manifestPath, manifest, bundles }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function findOnPath(command) {
|
|
120
|
+
const pathEntries = (process.env.PATH || '').split(delimiter)
|
|
121
|
+
const extensions = process.platform === 'win32'
|
|
122
|
+
? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';')
|
|
123
|
+
: ['']
|
|
124
|
+
for (const entry of pathEntries) {
|
|
125
|
+
for (const extension of extensions) {
|
|
126
|
+
const candidate = join(entry, `${command}${extension}`)
|
|
127
|
+
try {
|
|
128
|
+
if (process.platform === 'win32') {
|
|
129
|
+
const result = spawnSync('where.exe', [candidate], { encoding: 'utf8', windowsHide: true })
|
|
130
|
+
if (result.status === 0) return result.stdout.trim().split(/\r?\n/u)[0]
|
|
131
|
+
} else {
|
|
132
|
+
const result = spawnSync('test', ['-x', candidate])
|
|
133
|
+
if (result.status === 0) return candidate
|
|
134
|
+
}
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function findDshCli(explicit) {
|
|
142
|
+
const candidates = [explicit, process.env.DSH_DESKTOP_CLI]
|
|
143
|
+
if (process.platform === 'darwin') {
|
|
144
|
+
candidates.push(
|
|
145
|
+
'/Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh',
|
|
146
|
+
join(homedir(), 'Applications/DeepSeek Harness.app/Contents/Resources/runtime/cli/bin/dsh'),
|
|
147
|
+
)
|
|
148
|
+
} else if (process.platform === 'win32') {
|
|
149
|
+
const local = process.env.LOCALAPPDATA
|
|
150
|
+
const programFiles = process.env.ProgramFiles
|
|
151
|
+
if (local) candidates.push(join(local, 'Programs/DeepSeek Harness/resources/runtime/cli/bin/dsh.cmd'))
|
|
152
|
+
if (programFiles) candidates.push(join(programFiles, 'DeepSeek Harness/resources/runtime/cli/bin/dsh.cmd'))
|
|
153
|
+
}
|
|
154
|
+
candidates.push(findOnPath(process.platform === 'win32' ? 'dsh.cmd' : 'dsh'), findOnPath('dsh'))
|
|
155
|
+
for (const candidate of candidates) {
|
|
156
|
+
if (!candidate) continue
|
|
157
|
+
try {
|
|
158
|
+
if ((await stat(candidate)).isFile() && await isDesktopCli(candidate)) return resolve(candidate)
|
|
159
|
+
} catch {}
|
|
160
|
+
}
|
|
161
|
+
throw new Error('找不到可确认属于 DSH Desktop 的 dsh 命令。请在 DSH「更多 → 管理 dsh 命令」安装命令,或用 --dsh-cli 指定 Desktop CLI 完整路径。')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function isDesktopCli(candidate) {
|
|
165
|
+
const source = await readFile(candidate, 'utf8')
|
|
166
|
+
return source.includes('ELECTRON_RUN_AS_NODE=1')
|
|
167
|
+
&& source.includes('--expose-internals')
|
|
168
|
+
&& source.includes('dsh-desktop-host')
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function powershellQuote(value) {
|
|
172
|
+
return `'${String(value).replaceAll("'", "''")}'`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function runDsh(cli, args, dshHome) {
|
|
176
|
+
const env = { ...process.env, DSH_HOME: dshHome }
|
|
177
|
+
if (process.platform !== 'win32' || extname(cli).toLowerCase() !== '.cmd') {
|
|
178
|
+
return spawnSync(cli, args, { encoding: 'utf8', windowsHide: true, maxBuffer: 8 * 1024 * 1024, env })
|
|
179
|
+
}
|
|
180
|
+
const body = [
|
|
181
|
+
'$ErrorActionPreference = \'Stop\'',
|
|
182
|
+
`& ${powershellQuote(cli)} ${args.map(powershellQuote).join(' ')}`,
|
|
183
|
+
'exit $LASTEXITCODE',
|
|
184
|
+
].join('\n')
|
|
185
|
+
return spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', body], {
|
|
186
|
+
encoding: 'utf8', windowsHide: true, maxBuffer: 8 * 1024 * 1024, env,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function assertDesktopClosed() {
|
|
191
|
+
if (process.platform === 'darwin') {
|
|
192
|
+
const result = spawnSync('ps', ['-A', '-o', 'comm=,args='], { encoding: 'utf8' })
|
|
193
|
+
if (result.error || result.status !== 0) throw new Error('无法确认 DSH 是否已退出;请完全退出 DSH 后再运行。')
|
|
194
|
+
const running = result.stdout.split(/\r?\n/u).some((line) =>
|
|
195
|
+
line.includes('DeepSeek Harness.app/Contents/MacOS/DeepSeek Harness')
|
|
196
|
+
|| /^\s*DeepSeek Harness(?: Helper)?(?:\s|$)/u.test(line))
|
|
197
|
+
if (running) throw new Error('请先完全退出 DSH,再从独立终端重新运行迁移命令。')
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
if (process.platform === 'win32') {
|
|
201
|
+
const script = "$p = Get-CimInstance Win32_Process | Where-Object { $_.Name -like 'DeepSeek Harness*.exe' }; if ($p) { exit 3 } else { exit 0 }"
|
|
202
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
203
|
+
encoding: 'utf8', windowsHide: true,
|
|
204
|
+
})
|
|
205
|
+
if (result.error || result.status === null) throw new Error('无法确认 DSH 是否已退出;请完全退出 DSH 后再运行。')
|
|
206
|
+
if (result.status !== 0) throw new Error('请先完全退出 DSH,再从独立终端重新运行迁移命令。')
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
throw new Error('目前仅支持 macOS 和 Windows 上的 DSH Desktop profile。')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function printJson(value) {
|
|
213
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function stateMessage(profile) {
|
|
217
|
+
const messages = {
|
|
218
|
+
'legacy-alias': '发现旧 ant alias,可以迁移到直接安装的 @ai-wendao/ant。',
|
|
219
|
+
both: '发现新旧 ANT 包都在 profile 中;只有存在有效迁移记录时才会续跑。',
|
|
220
|
+
current: '已经直接安装 @ai-wendao/ant,无需迁移。',
|
|
221
|
+
'not-installed': '这个 DSH profile 没有安装 ANT。',
|
|
222
|
+
conflict: 'profile 同时包含旧 alias 和 scoped 包;工具不会自动修改。',
|
|
223
|
+
'unknown-alias': 'ant 依赖指向非 ANT 包;工具不会修改。',
|
|
224
|
+
inconsistent: '依赖清单与 DSH Bundle 列表不一致;工具不会修改。',
|
|
225
|
+
}
|
|
226
|
+
return messages[profile.state] || 'profile 状态无法识别;工具不会修改。'
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function inspect(options) {
|
|
230
|
+
const dshHome = resolveDshHome(options.dsh_home)
|
|
231
|
+
const profile = await readProfile(dshHome)
|
|
232
|
+
printJson({
|
|
233
|
+
platform: process.platform,
|
|
234
|
+
profile: PROFILE_NAME,
|
|
235
|
+
dshHome,
|
|
236
|
+
state: profile.state,
|
|
237
|
+
aliasSpec: profile.aliasSpec ?? null,
|
|
238
|
+
directSpec: profile.directSpec ?? null,
|
|
239
|
+
target: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
|
|
240
|
+
message: stateMessage(profile),
|
|
241
|
+
})
|
|
242
|
+
if (profile.state === 'conflict' || profile.state === 'unknown-alias' || profile.state === 'inconsistent') process.exitCode = 2
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function createBackup(dshHome, profile) {
|
|
246
|
+
const backupDir = join(dshHome, 'ant-migration-backups', `${new Date().toISOString().replaceAll(':', '').replaceAll('.', '-')}-${randomUUID()}`)
|
|
247
|
+
await mkdir(backupDir, { recursive: true, mode: 0o700 })
|
|
248
|
+
const fileRows = []
|
|
249
|
+
for (const name of PROFILE_FILES) {
|
|
250
|
+
const source = join(profile.dir, name)
|
|
251
|
+
try {
|
|
252
|
+
const contents = await readFile(source)
|
|
253
|
+
await writeFile(join(backupDir, name), contents, { mode: 0o600, flag: 'wx' })
|
|
254
|
+
fileRows.push({ name, existed: true, sha256: createHash('sha256').update(contents).digest('hex') })
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error?.code !== 'ENOENT') throw error
|
|
257
|
+
fileRows.push({ name, existed: false })
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const record = {
|
|
261
|
+
format: 1,
|
|
262
|
+
createdAt: new Date().toISOString(),
|
|
263
|
+
dshHome: resolve(dshHome),
|
|
264
|
+
profileDir: resolve(profile.dir),
|
|
265
|
+
profile: PROFILE_NAME,
|
|
266
|
+
originalAliasSpec: profile.aliasSpec,
|
|
267
|
+
targetSpec: `${PACKAGE_NAME}@${PACKAGE_VERSION}`,
|
|
268
|
+
targetVersion: PACKAGE_VERSION,
|
|
269
|
+
status: 'pending',
|
|
270
|
+
phase: 'prepared',
|
|
271
|
+
files: fileRows,
|
|
272
|
+
}
|
|
273
|
+
await writeFile(join(backupDir, 'migration.json'), `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
|
|
274
|
+
if (process.platform !== 'win32') await chmod(backupDir, 0o700)
|
|
275
|
+
return { backupDir, record }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function saveBackupRecord(backupDir, record) {
|
|
279
|
+
const temporary = join(backupDir, `.migration-${randomUUID()}.tmp`)
|
|
280
|
+
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
|
|
281
|
+
await rename(temporary, join(backupDir, 'migration.json'))
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function validateBackup(backupDir, dshHome) {
|
|
285
|
+
const backupRoot = resolve(dshHome, 'ant-migration-backups')
|
|
286
|
+
const absolute = resolve(backupDir)
|
|
287
|
+
const backupRelative = relative(backupRoot, absolute)
|
|
288
|
+
if (!backupRelative || backupRelative === '..' || backupRelative.startsWith(`..${sep}`) || isAbsolute(backupRelative)) {
|
|
289
|
+
throw new Error('备份目录不属于当前 DSH home;没有修改任何文件。')
|
|
290
|
+
}
|
|
291
|
+
const record = JSON.parse(await readFile(join(absolute, 'migration.json'), 'utf8'))
|
|
292
|
+
if (record.format !== 1 || record.profile !== PROFILE_NAME || record.dshHome !== resolve(dshHome)
|
|
293
|
+
|| record.profileDir !== resolve(dshHome, 'profiles', PROFILE_NAME)
|
|
294
|
+
|| !aliasPointsToAnt(record.originalAliasSpec) || !directSpecIsAnt(record.targetVersion)
|
|
295
|
+
|| record.targetSpec !== `${PACKAGE_NAME}@${record.targetVersion}`
|
|
296
|
+
|| !['pending', 'completed', 'rolled-back'].includes(record.status)
|
|
297
|
+
|| !['prepared', 'installing-scoped', 'scoped-installed', 'removing-alias', 'complete', 'rolled-back'].includes(record.phase)
|
|
298
|
+
|| !Array.isArray(record.files)) {
|
|
299
|
+
throw new Error('备份归属、格式或原 alias 无效;没有修改任何文件。')
|
|
300
|
+
}
|
|
301
|
+
const seen = new Set()
|
|
302
|
+
for (const file of record.files) {
|
|
303
|
+
if (!PROFILE_FILES.includes(file.name) || seen.has(file.name) || typeof file.existed !== 'boolean') {
|
|
304
|
+
throw new Error('备份文件清单异常;没有修改任何文件。')
|
|
305
|
+
}
|
|
306
|
+
seen.add(file.name)
|
|
307
|
+
if (file.existed) {
|
|
308
|
+
if (typeof file.sha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(file.sha256)) {
|
|
309
|
+
throw new Error(`备份文件 ${file.name} 校验值异常;没有修改任何文件。`)
|
|
310
|
+
}
|
|
311
|
+
const contents = await readFile(join(absolute, file.name))
|
|
312
|
+
const digest = createHash('sha256').update(contents).digest('hex')
|
|
313
|
+
if (digest !== file.sha256) throw new Error(`备份文件 ${file.name} 校验失败;没有修改任何文件。`)
|
|
314
|
+
} else if (file.sha256 !== undefined) {
|
|
315
|
+
throw new Error(`备份文件 ${file.name} 元数据异常;没有修改任何文件。`)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (seen.size !== PROFILE_FILES.length) throw new Error('备份文件清单不完整;没有修改任何文件。')
|
|
319
|
+
return { absolute, record }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function findPendingBackup(dshHome) {
|
|
323
|
+
const root = join(dshHome, 'ant-migration-backups')
|
|
324
|
+
let entries
|
|
325
|
+
try { entries = await readdir(root, { withFileTypes: true }) } catch (error) {
|
|
326
|
+
if (error?.code === 'ENOENT') return undefined
|
|
327
|
+
throw error
|
|
328
|
+
}
|
|
329
|
+
const pending = []
|
|
330
|
+
for (const entry of entries) {
|
|
331
|
+
if (!entry.isDirectory()) continue
|
|
332
|
+
const filename = join(root, entry.name, 'migration.json')
|
|
333
|
+
let record
|
|
334
|
+
try { record = JSON.parse(await readFile(filename, 'utf8')) } catch (error) {
|
|
335
|
+
if (error?.code === 'ENOENT') continue
|
|
336
|
+
throw new Error(`迁移记录无法读取:${filename}。请勿手动修改 profile。`)
|
|
337
|
+
}
|
|
338
|
+
if (record.status === 'pending' && record.dshHome === resolve(dshHome)) {
|
|
339
|
+
pending.push(await validateBackup(join(root, entry.name), dshHome))
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (pending.length > 1) throw new Error('发现多条未完成迁移记录;工具不会猜测目标。请保留备份并联系管理员。')
|
|
343
|
+
return pending[0]
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function processIsAlive(pid) {
|
|
347
|
+
try { process.kill(pid, 0); return true } catch (error) { return error?.code === 'EPERM' }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function acquireMigrationLock(dshHome) {
|
|
351
|
+
const lockPath = join(dshHome, 'ant-migration.lock')
|
|
352
|
+
const token = randomUUID()
|
|
353
|
+
await mkdir(dshHome, { recursive: true })
|
|
354
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
355
|
+
let handle
|
|
356
|
+
try {
|
|
357
|
+
handle = await open(lockPath, 'wx', 0o600)
|
|
358
|
+
} catch (error) {
|
|
359
|
+
if (error?.code !== 'EEXIST') throw error
|
|
360
|
+
let current
|
|
361
|
+
try { current = JSON.parse(await readFile(lockPath, 'utf8')) } catch {
|
|
362
|
+
throw new Error(`发现无法识别的迁移锁:${lockPath}。确认没有迁移进程运行后,请联系管理员处理。`)
|
|
363
|
+
}
|
|
364
|
+
if (!Number.isSafeInteger(current.pid) || current.pid < 1 || processIsAlive(current.pid)) {
|
|
365
|
+
throw new Error(`另一个迁移进程正在运行(PID ${String(current.pid)});请等待它结束后重试。`)
|
|
366
|
+
}
|
|
367
|
+
const stalePath = `${lockPath}.${randomUUID()}.stale`
|
|
368
|
+
try { await rename(lockPath, stalePath); await unlink(stalePath) } catch (renameError) {
|
|
369
|
+
if (renameError?.code !== 'ENOENT') throw renameError
|
|
370
|
+
}
|
|
371
|
+
continue
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, token, createdAt: new Date().toISOString() })}\n`)
|
|
375
|
+
await handle.sync()
|
|
376
|
+
await handle.close()
|
|
377
|
+
} catch (error) {
|
|
378
|
+
await handle.close().catch(() => undefined)
|
|
379
|
+
await unlink(lockPath).catch(() => undefined)
|
|
380
|
+
throw error
|
|
381
|
+
}
|
|
382
|
+
return async () => {
|
|
383
|
+
try {
|
|
384
|
+
const current = JSON.parse(await readFile(lockPath, 'utf8'))
|
|
385
|
+
if (current.token === token) await unlink(lockPath)
|
|
386
|
+
} catch (error) { if (error?.code !== 'ENOENT') throw error }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
throw new Error('无法取得迁移锁;请稍后重试。')
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function acquireDesktopProfileLock(dshHome) {
|
|
393
|
+
const profileDir = join(dshHome, 'profiles', PROFILE_NAME)
|
|
394
|
+
const lockPath = join(profileDir, 'lock')
|
|
395
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
396
|
+
let handle
|
|
397
|
+
try {
|
|
398
|
+
handle = await open(lockPath, 'wx', 0o600)
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error?.code !== 'EEXIST') throw error
|
|
401
|
+
const details = await lstat(lockPath)
|
|
402
|
+
if (details.isSymbolicLink() || !details.isFile()) {
|
|
403
|
+
throw new Error('DSH Desktop profile lock 不是普通文件;工具不会修改。')
|
|
404
|
+
}
|
|
405
|
+
const rawOwner = await readFile(lockPath, 'utf8')
|
|
406
|
+
const owner = Number.parseInt(rawOwner, 10)
|
|
407
|
+
if (!Number.isSafeInteger(owner) || owner < 1 || processIsAlive(owner)) {
|
|
408
|
+
throw new Error(`DSH 正在启动或另一个 profile 操作正在运行(PID ${String(owner)});请等待并确认 DSH 完全退出后重试。`)
|
|
409
|
+
}
|
|
410
|
+
await unlink(lockPath)
|
|
411
|
+
continue
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
await handle.writeFile(`${process.pid}\n`)
|
|
415
|
+
await handle.sync()
|
|
416
|
+
} catch (error) {
|
|
417
|
+
await handle.close().catch(() => undefined)
|
|
418
|
+
await unlink(lockPath).catch(() => undefined)
|
|
419
|
+
throw error
|
|
420
|
+
}
|
|
421
|
+
return async () => {
|
|
422
|
+
try {
|
|
423
|
+
const current = Number.parseInt(await readFile(lockPath, 'utf8'), 10)
|
|
424
|
+
if (current === process.pid) await unlink(lockPath)
|
|
425
|
+
} catch (error) { if (error?.code !== 'ENOENT') throw error }
|
|
426
|
+
finally { await handle.close() }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
throw new Error('无法锁定 DSH Desktop profile;请稍后重试。')
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function showDshOutput(result) {
|
|
433
|
+
if (result.stdout) process.stdout.write(result.stdout)
|
|
434
|
+
if (result.stderr) process.stderr.write(result.stderr)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function invokeDsh(cli, args, label, dshHome) {
|
|
438
|
+
await assertDesktopClosed()
|
|
439
|
+
process.stdout.write(`执行:dsh plugin --profile ${PROFILE_NAME} ${args.at(-1)}(${label})\n`)
|
|
440
|
+
const result = await runDsh(cli, ['plugin', '--profile', PROFILE_NAME, ...args], dshHome)
|
|
441
|
+
showDshOutput(result)
|
|
442
|
+
if (result.error) throw new Error(`${label}失败:${result.error.message}`)
|
|
443
|
+
if (result.status !== 0) throw new Error(`${label}失败,退出码 ${String(result.status)}`)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function rollbackWithCli(cli, profile, aliasSpec) {
|
|
447
|
+
let current = await readProfile(profile.dshHome)
|
|
448
|
+
if (current.state === 'both') {
|
|
449
|
+
await invokeDsh(cli, ['remove', PACKAGE_NAME], '移除未完成的 scoped Bundle', profile.dshHome)
|
|
450
|
+
}
|
|
451
|
+
current = await readProfile(profile.dshHome)
|
|
452
|
+
if (current.aliasSpec === undefined) {
|
|
453
|
+
await invokeDsh(cli, ['add', `${ALIAS_NAME}@${aliasSpec}`], '恢复旧 ANT alias', profile.dshHome)
|
|
454
|
+
}
|
|
455
|
+
current = await readProfile(profile.dshHome)
|
|
456
|
+
if (current.aliasSpec !== aliasSpec || current.directSpec !== undefined
|
|
457
|
+
|| !current.bundles.includes(ALIAS_NAME) || current.bundles.includes(PACKAGE_NAME)) {
|
|
458
|
+
throw new Error('旧 alias 回退后状态未通过检查;请保留备份并联系管理员。')
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function migrate(options) {
|
|
463
|
+
const dshHome = resolveDshHome(options.dsh_home)
|
|
464
|
+
if (!options.apply) {
|
|
465
|
+
const profile = await readProfile(dshHome)
|
|
466
|
+
process.stdout.write(`${stateMessage(profile)}\n`)
|
|
467
|
+
if (profile.state === 'current' || profile.state === 'not-installed') return
|
|
468
|
+
if (profile.state !== 'legacy-alias') throw new Error('检测到非标准 profile 状态;工具不会修改。')
|
|
469
|
+
process.stdout.write(`预检完成。执行迁移前请完全退出 DSH;确认后运行:ant-migrate migrate --apply${options.dsh_home ? ` --dsh-home "${options.dsh_home}"` : ''}${options.dsh_cli ? ` --dsh-cli "${options.dsh_cli}"` : ''}\n`)
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
const releaseLock = await acquireMigrationLock(dshHome)
|
|
473
|
+
let releaseProfileLock
|
|
474
|
+
try {
|
|
475
|
+
await assertDesktopClosed()
|
|
476
|
+
releaseProfileLock = await acquireDesktopProfileLock(dshHome)
|
|
477
|
+
await assertDesktopClosed()
|
|
478
|
+
let profile = await readProfile(dshHome)
|
|
479
|
+
let pending = await findPendingBackup(dshHome)
|
|
480
|
+
process.stdout.write(`${stateMessage(profile)}\n`)
|
|
481
|
+
if (!pending && (profile.state === 'current' || profile.state === 'not-installed')) return
|
|
482
|
+
if (!pending && profile.state !== 'legacy-alias') {
|
|
483
|
+
throw new Error('profile 不是可识别的旧 ANT alias;工具不会修改。')
|
|
484
|
+
}
|
|
485
|
+
if (!pending) {
|
|
486
|
+
const initial = profile
|
|
487
|
+
const backup = await createBackup(dshHome, initial)
|
|
488
|
+
pending = { absolute: backup.backupDir, record: backup.record }
|
|
489
|
+
process.stdout.write(`profile 备份已保存:${pending.absolute}\n`)
|
|
490
|
+
}
|
|
491
|
+
const { absolute: backupDir, record } = pending
|
|
492
|
+
const targetVersion = record.targetVersion || PACKAGE_VERSION
|
|
493
|
+
const targetSpec = `${PACKAGE_NAME}@${targetVersion}`
|
|
494
|
+
const cli = await findDshCli(options.dsh_cli)
|
|
495
|
+
await assertDesktopClosed()
|
|
496
|
+
const listResult = await runDsh(cli, ['plugin', '--profile', PROFILE_NAME, 'list'], dshHome)
|
|
497
|
+
showDshOutput(listResult)
|
|
498
|
+
if (listResult.error || listResult.status !== 0) {
|
|
499
|
+
throw new Error('DSH Desktop CLI 未能读取 desktop profile;请确认 CLI 路径正确且 profile 已初始化。')
|
|
500
|
+
}
|
|
501
|
+
profile = await readProfile(dshHome)
|
|
502
|
+
if (['conflict', 'unknown-alias', 'inconsistent'].includes(profile.state)) {
|
|
503
|
+
throw new Error('预检后 profile 状态与迁移记录不一致;迁移尚未继续,备份已保留。')
|
|
504
|
+
}
|
|
505
|
+
if (profile.state === 'current') {
|
|
506
|
+
if (!directSpecMatchesTarget(profile.directSpec, targetVersion)) {
|
|
507
|
+
throw new Error(`已发现直接安装的 ANT,但版本范围与迁移记录目标 ${targetSpec} 不符;工具不会覆盖。`)
|
|
508
|
+
}
|
|
509
|
+
record.status = 'completed'
|
|
510
|
+
record.phase = 'complete'
|
|
511
|
+
record.completedAt = new Date().toISOString()
|
|
512
|
+
await saveBackupRecord(backupDir, record)
|
|
513
|
+
printJson({ state: 'migrated', profile: PROFILE_NAME, package: PACKAGE_NAME, version: targetVersion, backupDir,
|
|
514
|
+
next: '请重新打开 DSH;检查 ANT 设置页、连接状态和模型目录。' })
|
|
515
|
+
return
|
|
516
|
+
}
|
|
517
|
+
if (profile.state === 'legacy-alias' || profile.state === 'not-installed') {
|
|
518
|
+
record.phase = 'installing-scoped'
|
|
519
|
+
await saveBackupRecord(backupDir, record)
|
|
520
|
+
try {
|
|
521
|
+
await invokeDsh(cli, ['add', '--save-exact', targetSpec], '安装 scoped ANT Bundle', dshHome)
|
|
522
|
+
} catch (error) {
|
|
523
|
+
const afterAttempt = await readProfile(dshHome)
|
|
524
|
+
if (afterAttempt.state !== 'both' && afterAttempt.state !== 'current') throw error
|
|
525
|
+
}
|
|
526
|
+
profile = await readProfile(dshHome)
|
|
527
|
+
}
|
|
528
|
+
if (profile.state === 'current' && directSpecMatchesTarget(profile.directSpec, targetVersion)) {
|
|
529
|
+
record.status = 'completed'
|
|
530
|
+
record.phase = 'complete'
|
|
531
|
+
record.completedAt = new Date().toISOString()
|
|
532
|
+
await saveBackupRecord(backupDir, record)
|
|
533
|
+
printJson({ state: 'migrated', profile: PROFILE_NAME, package: PACKAGE_NAME, version: targetVersion, backupDir,
|
|
534
|
+
next: '请重新打开 DSH;检查 ANT 设置页、连接状态和模型目录。' })
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
if (profile.state !== 'both' || !directSpecMatchesTarget(profile.directSpec, targetVersion)
|
|
538
|
+
|| profile.aliasSpec !== record.originalAliasSpec) {
|
|
539
|
+
throw new Error('scoped 包未按预期安装;旧 alias 保留,备份已保留。关闭 DSH 后重试 migrate --apply。')
|
|
540
|
+
}
|
|
541
|
+
record.phase = 'scoped-installed'
|
|
542
|
+
await saveBackupRecord(backupDir, record)
|
|
543
|
+
record.phase = 'removing-alias'
|
|
544
|
+
await saveBackupRecord(backupDir, record)
|
|
545
|
+
try {
|
|
546
|
+
await invokeDsh(cli, ['remove', ALIAS_NAME], '移除旧 ANT alias', dshHome)
|
|
547
|
+
} catch (error) {
|
|
548
|
+
const afterAttempt = await readProfile(dshHome)
|
|
549
|
+
if (afterAttempt.state !== 'current') throw error
|
|
550
|
+
}
|
|
551
|
+
const finalProfile = await readProfile(dshHome)
|
|
552
|
+
if (finalProfile.state !== 'current' || !directSpecMatchesTarget(finalProfile.directSpec, targetVersion)) {
|
|
553
|
+
throw new Error('移除旧 alias 后 profile 未达到预期状态;请保持 DSH 关闭,再运行 migrate --apply 续跑。')
|
|
554
|
+
}
|
|
555
|
+
record.status = 'completed'
|
|
556
|
+
record.phase = 'complete'
|
|
557
|
+
record.completedAt = new Date().toISOString()
|
|
558
|
+
await saveBackupRecord(backupDir, record)
|
|
559
|
+
printJson({ state: 'migrated', profile: PROFILE_NAME, package: PACKAGE_NAME, version: targetVersion, backupDir,
|
|
560
|
+
next: '请重新打开 DSH;检查 ANT 设置页、连接状态和模型目录。' })
|
|
561
|
+
} catch (error) {
|
|
562
|
+
const pending = await findPendingBackup(dshHome).catch(() => undefined)
|
|
563
|
+
if (pending) {
|
|
564
|
+
process.stderr.write(`迁移未完成:${error instanceof Error ? error.message : String(error)}\n`)
|
|
565
|
+
process.stderr.write(`旧 alias 会保留到新版安装成功。保持 DSH 关闭,检查 profile 后可重试 ant-migrate migrate --apply。备份:${pending.absolute}\n`)
|
|
566
|
+
process.exitCode = 1
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
throw error
|
|
570
|
+
} finally {
|
|
571
|
+
try { if (releaseProfileLock) await releaseProfileLock() }
|
|
572
|
+
finally { await releaseLock() }
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function rollback(options) {
|
|
577
|
+
const dshHome = resolveDshHome(options.dsh_home)
|
|
578
|
+
const releaseLock = await acquireMigrationLock(dshHome)
|
|
579
|
+
let releaseProfileLock
|
|
580
|
+
try {
|
|
581
|
+
const { absolute, record } = await validateBackup(options.backup, dshHome)
|
|
582
|
+
const profile = await readProfile(dshHome)
|
|
583
|
+
if (record.status !== 'pending') throw new Error('该迁移记录已结束;rollback 只用于取消尚未完成的迁移。')
|
|
584
|
+
if (profile.state === 'legacy-alias' && profile.aliasSpec === record.originalAliasSpec) {
|
|
585
|
+
if (options.apply) {
|
|
586
|
+
record.status = 'rolled-back'
|
|
587
|
+
record.phase = 'rolled-back'
|
|
588
|
+
await saveBackupRecord(absolute, record)
|
|
589
|
+
process.stdout.write('旧 alias 仍完整存在;已将迁移标记为取消。\n')
|
|
590
|
+
} else {
|
|
591
|
+
process.stdout.write('预检完成:旧 alias 仍完整存在,无需回退。运行 rollback --apply 可取消这条迁移记录。\n')
|
|
592
|
+
}
|
|
593
|
+
return
|
|
594
|
+
}
|
|
595
|
+
if (profile.state !== 'both' || profile.aliasSpec !== record.originalAliasSpec) {
|
|
596
|
+
throw new Error('当前状态无法无损取消迁移;该工具不会重建旧 alias 或覆盖现有依赖。优先继续 migrate --apply。')
|
|
597
|
+
}
|
|
598
|
+
if (!options.apply) {
|
|
599
|
+
process.stdout.write(`预检完成。确认 DSH 已完全退出后运行:ant-migrate rollback --backup "${absolute}" --apply\n`)
|
|
600
|
+
return
|
|
601
|
+
}
|
|
602
|
+
await assertDesktopClosed()
|
|
603
|
+
releaseProfileLock = await acquireDesktopProfileLock(dshHome)
|
|
604
|
+
await assertDesktopClosed()
|
|
605
|
+
const cli = await findDshCli(options.dsh_cli)
|
|
606
|
+
const listResult = await runDsh(cli, ['plugin', '--profile', PROFILE_NAME, 'list'], dshHome)
|
|
607
|
+
showDshOutput(listResult)
|
|
608
|
+
if (listResult.error || listResult.status !== 0) throw new Error('DSH Desktop CLI 未能读取 desktop profile。')
|
|
609
|
+
const beforeRollback = await readProfile(dshHome)
|
|
610
|
+
if (beforeRollback.state !== 'both' || beforeRollback.aliasSpec !== record.originalAliasSpec) {
|
|
611
|
+
throw new Error('执行回退前 profile 已变化;工具不会继续。')
|
|
612
|
+
}
|
|
613
|
+
await rollbackWithCli(cli, { dshHome }, record.originalAliasSpec)
|
|
614
|
+
record.status = 'rolled-back'
|
|
615
|
+
record.phase = 'rolled-back'
|
|
616
|
+
await saveBackupRecord(absolute, record)
|
|
617
|
+
printJson({ state: 'rolled-back', profile: PROFILE_NAME, backupDir: absolute,
|
|
618
|
+
next: '新包已移除,原 alias 恢复。它会恢复迁移前的加载限制;优先修复并继续迁移。' })
|
|
619
|
+
} finally {
|
|
620
|
+
try { if (releaseProfileLock) await releaseProfileLock() }
|
|
621
|
+
finally { await releaseLock() }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function main() {
|
|
626
|
+
const options = parseArgs(process.argv.slice(2))
|
|
627
|
+
if (options.help) return usage()
|
|
628
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10)
|
|
629
|
+
if (!Number.isInteger(nodeMajor) || nodeMajor < 20) throw new Error('迁移工具需要 Node.js 20 或更高版本。')
|
|
630
|
+
if (!['darwin', 'win32'].includes(process.platform)) throw new Error('目前仅支持 macOS 和 Windows 上的 DSH Desktop。')
|
|
631
|
+
if (options.command === 'inspect') return inspect(options)
|
|
632
|
+
if (options.command === 'migrate') return migrate(options)
|
|
633
|
+
return rollback(options)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
main().catch((error) => {
|
|
637
|
+
process.stderr.write(`ant-migrate:${error instanceof Error ? error.message : String(error)}\n`)
|
|
638
|
+
process.exitCode = 1
|
|
639
|
+
})
|