dsh-wechat-remote 1.7.5 → 1.7.8
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/README.md +16 -12
- package/assets/plugin.tgz +0 -0
- package/assets/release.json +8 -10
- package/bin/dsh-discovery.mjs +175 -0
- package/bin/native-control.mjs +4 -2
- package/bin/native-recovery.mjs +44 -0
- package/bin/release-selection.mjs +36 -33
- package/bin/setup.mjs +129 -61
- package/lib/install-control.js +1 -1
- package/lib/install-profile.js +113 -159
- package/lib/install-runtime.js +17 -0
- package/lib/native-recovery.js +7478 -0
- package/lib/update-policy.js +26 -27
- package/lib/update-worker.js +295 -176
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Agent远程管理助手 · DSH 插件安装器
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# Agent远程管理助手 · DSH 插件安装器
|
|
2
|
+
|
|
3
|
+
当前版本为 `1.7.8`,内嵌同版本正式插件。
|
|
4
4
|
|
|
5
5
|
首次安装和升级旧插件使用同一个命令:
|
|
6
6
|
|
|
@@ -8,16 +8,20 @@
|
|
|
8
8
|
npx -y dsh-wechat-remote@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
在与 DSH 相同的系统用户下运行即可。无需改成全局安装 DSH;使用 `npx @deepseek-ai/dsh web` 的用户也使用同一条安装命令。需要 Node.js 22.13 或更高版本,无需另装 pnpm 或 Git。Windows CMD / PowerShell 推荐使用 `npx.cmd -y dsh-wechat-remote@latest`,不必修改脚本执行策略。
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
DSH 的新版本、预览版本或未登记的主机架构不会仅因缺少验证记录而被拒绝安装。安装器仍检查必要运行能力、安装包完整性和已知不兼容问题,并保留失败恢复所需的备份;无法确认恢复结果时会提示保留数据。这不代表所有宿主组合均已通过功能验证。
|
|
13
|
+
安装器保留原配置、配对和会话,不会升级 DSH 本体。请先结束运行中的任务;安装完成后自动重启原 DSH 并尝试打开 WebUI。DSH 已关闭时会尝试启动;若启动失败,按提示查看启动日志,不要删除原有数据。
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
另已在 ARM64 Linux 电视盒子上验证 DSH `0.1.5-rc.1` 的首次安装、重复执行及重装。1.7.4 改善较慢主机启动时被误判为安装超时的问题;安装过程中请等待完成,勿重复执行命令。
|
|
15
|
+
DSH 的新版本、预览版本或未登记的主机架构不会仅因缺少验证记录而被拒绝安装。安装器仍检查必要运行能力、安装包完整性和已知不兼容问题,并保留失败恢复所需的备份;无法确认恢复结果时会提示保留数据。这不代表所有宿主组合均已通过功能验证。
|
|
20
16
|
|
|
21
|
-
|
|
17
|
+
本版沿用此前已验证的跨平台安装方案,具体发行包与宿主组合以验收记录为准。普通 Node 启动、独立的 macOS launchd 和 Linux 用户级 systemd 服务有对应的自动恢复方式;无法确认启动方式时不自动停止主机。
|
|
22
18
|
|
|
23
|
-
|
|
19
|
+
另已在 ARM64 Linux 电视盒子上验证 DSH `0.1.5-rc.1` 的首次安装、重复执行及重装。1.7.4 改善较慢主机启动时被误判为安装超时的问题;安装过程中请等待完成,勿重复执行命令。
|
|
20
|
+
|
|
21
|
+
旧插件的一键更新如被版本或架构判断拦截,可用上述安装命令升级一次。从插件 1.7.6 起,未登记的 CPU 架构或 DSH 版本不再影响更新入口;执行前仍会检查实际安装与重启条件,无需因此降低 DSH 版本或删除节点。
|
|
22
|
+
|
|
23
|
+
默认使用 `web` 配置和原来的 DSH 数据目录。自定义数据目录时,在新终端设置相同的 `DSH_HOME`,或附加 `--home "数据目录"`;使用其他配置时附加 `--profile 配置名称`。WSL、容器和其他系统用户内的 DSH,需要在对应环境中执行安装,不能跨环境替换。
|
|
24
|
+
|
|
25
|
+
DSH 正在运行时,优先连接它自己的安装接口;未运行时,优先使用当前终端默认的 DSH,再检查全局、当前项目和 NPX 缓存。只有存在多份缓存且没有默认选择时才询问。也可用 `--dsh-cli "DSH的lib/bin.js路径"` 明确指定。
|
|
26
|
+
|
|
27
|
+
已运行的同版本插件卡在更新验证、尚未就绪时,重新执行命令会尝试修复。DSH 已退出或怀疑插件文件损坏时,附加 `--repair` 主动重装,不会自动降级。宿主本体或其他插件的故障仍可能需要另行处理;安装日志会保留,不必先删除会话或重新配对。
|
package/assets/plugin.tgz
CHANGED
|
Binary file
|
package/assets/release.json
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.7.
|
|
2
|
+
"version": "1.7.8",
|
|
3
3
|
"catalog": {
|
|
4
4
|
"schemaVersion": 1,
|
|
5
|
-
"revision": "installer-1.7.
|
|
6
|
-
"issuedAt":
|
|
7
|
-
"expiresAt":
|
|
5
|
+
"revision": "installer-1.7.8",
|
|
6
|
+
"issuedAt": 1789590295076,
|
|
7
|
+
"expiresAt": 1792009495076,
|
|
8
8
|
"releases": [
|
|
9
9
|
{
|
|
10
|
-
"version": "1.7.
|
|
10
|
+
"version": "1.7.8",
|
|
11
11
|
"channel": "stable",
|
|
12
12
|
"dsh": [
|
|
13
|
-
"0.1.1-rc.2",
|
|
14
|
-
"0.1.2-rc.1",
|
|
15
13
|
"0.1.5-rc.1"
|
|
16
14
|
],
|
|
17
15
|
"platforms": [
|
|
@@ -23,9 +21,9 @@
|
|
|
23
21
|
"x64"
|
|
24
22
|
],
|
|
25
23
|
"asset": {
|
|
26
|
-
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.
|
|
27
|
-
"sha256": "
|
|
28
|
-
"bytes":
|
|
24
|
+
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.8/harness-remote-dsh-wechat-remote-1.7.8.tgz",
|
|
25
|
+
"sha256": "bcadaf80e8a2dff008b421870bb42cad89d490178104df1f4d9103aa03cd62a6",
|
|
26
|
+
"bytes": 9554570
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
],
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/** Read-only discovery. A candidate is NOT the installation target until the
|
|
2
|
+
* native, token-bound host handshake confirms its home/profile/CLI/PID.
|
|
3
|
+
* Never run an unvalidated shell shim, fetch a different DSH, or scan a disk. */
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import { execFile } from 'node:child_process'
|
|
8
|
+
import { promisify } from 'node:util'
|
|
9
|
+
import { createInterface } from 'node:readline/promises'
|
|
10
|
+
|
|
11
|
+
const execute = promisify(execFile)
|
|
12
|
+
const PACKAGE = '@deepseek-ai/dsh'
|
|
13
|
+
const clean = value => String(value).replace(/[\x00-\x1f\x7f-\x9f]/g, '')
|
|
14
|
+
const environmentPath = env => env[Object.keys(env).find(key => key.toLowerCase() === 'path')] || ''
|
|
15
|
+
|
|
16
|
+
export function validateDshCli(filename) {
|
|
17
|
+
const cli = fs.realpathSync(path.resolve(filename))
|
|
18
|
+
const root = path.resolve(cli, '../..')
|
|
19
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
20
|
+
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.dsh
|
|
21
|
+
if (manifest.name !== PACKAGE || path.basename(cli) !== 'bin.js' || !fs.statSync(cli).isFile()
|
|
22
|
+
|| typeof bin !== 'string' || fs.realpathSync(path.resolve(root, bin)) !== cli) {
|
|
23
|
+
throw new Error('指定路径不是有效的 DSH 启动程序,未修改安装。')
|
|
24
|
+
}
|
|
25
|
+
return { cli, version: String(manifest.version || '') }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveHome(value, fallback = os.homedir()) {
|
|
29
|
+
const raw = String(value || '')
|
|
30
|
+
const configured = raw.trim() ? raw : ''
|
|
31
|
+
return path.resolve(configured === '~' ? fallback : /^~[/\\]/.test(configured)
|
|
32
|
+
? path.join(fallback, configured.slice(2)) : configured || path.join(fallback, '.dsh'))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** npm's own config resolves .npmrc, custom cache/prefix and Node managers.
|
|
36
|
+
* Query individual public values; never read or log registry credentials. */
|
|
37
|
+
async function npmLocation(key, env, cwd) {
|
|
38
|
+
let npm = env.npm_execpath
|
|
39
|
+
if (!npm || !fs.existsSync(npm)) {
|
|
40
|
+
const roots = [path.dirname(process.execPath), ...environmentPath(env).split(path.delimiter)]
|
|
41
|
+
npm = roots.flatMap(dir => [path.join(dir, 'node_modules/npm/bin/npm-cli.js'),
|
|
42
|
+
path.join(dir, '../lib/node_modules/npm/bin/npm-cli.js')]).find(file => fs.existsSync(file))
|
|
43
|
+
}
|
|
44
|
+
if (!npm) return ''
|
|
45
|
+
try {
|
|
46
|
+
const { stdout } = await execute(process.execPath, [npm, 'config', 'get', key], {
|
|
47
|
+
cwd, env, windowsHide: true, timeout: 10000, maxBuffer: 16384,
|
|
48
|
+
})
|
|
49
|
+
const value = stdout.trim()
|
|
50
|
+
return value && !/[\r\n\0]/.test(value) && path.isAbsolute(value) ? value : ''
|
|
51
|
+
} catch { return '' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function discoverDsh({ env = process.env, cwd = process.cwd(), cache, prefix } = {}) {
|
|
55
|
+
const found = new Map()
|
|
56
|
+
const add = (filename, source) => {
|
|
57
|
+
try {
|
|
58
|
+
const item = validateDshCli(filename)
|
|
59
|
+
if (!found.has(item.cli)) found.set(item.cli, { ...item, source })
|
|
60
|
+
} catch { /* A missing/broken cache entry is not a usable DSH. */ }
|
|
61
|
+
}
|
|
62
|
+
for (const dir of environmentPath(env).split(path.delimiter).filter(Boolean)) {
|
|
63
|
+
add(path.join(dir, 'dsh'), 'PATH') // POSIX symlink, never execute a Windows shim.
|
|
64
|
+
add(path.join(dir, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'PATH')
|
|
65
|
+
add(path.join(dir, '../lib/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'PATH')
|
|
66
|
+
add(path.join(dir, '../@deepseek-ai/dsh/lib/bin.js'), 'PATH') // npm .bin, including Windows.
|
|
67
|
+
}
|
|
68
|
+
for (let dir = path.resolve(cwd);;) {
|
|
69
|
+
add(path.join(dir, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'project')
|
|
70
|
+
add(path.join(dir, 'apps/cli/lib/bin.js'), 'source')
|
|
71
|
+
const parent = path.dirname(dir)
|
|
72
|
+
if (parent === dir) break
|
|
73
|
+
dir = parent
|
|
74
|
+
}
|
|
75
|
+
const locations = await Promise.all([
|
|
76
|
+
cache !== undefined ? cache : env.npm_config_cache || npmLocation('cache', env, cwd),
|
|
77
|
+
prefix !== undefined ? prefix : npmLocation('prefix', env, cwd),
|
|
78
|
+
])
|
|
79
|
+
if (locations[1]) {
|
|
80
|
+
add(path.join(locations[1], 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'npm-global')
|
|
81
|
+
add(path.join(locations[1], 'lib/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'npm-global')
|
|
82
|
+
}
|
|
83
|
+
const cacheRoot = locations[0]
|
|
84
|
+
// Cached NPX versions cannot supersede a normal default executable. Avoid
|
|
85
|
+
// probing a large/unreadable cache when the terminal already has that default.
|
|
86
|
+
if (cacheRoot && found.size === 0) {
|
|
87
|
+
try {
|
|
88
|
+
const entries = fs.readdirSync(path.join(cacheRoot, '_npx'), { withFileTypes: true })
|
|
89
|
+
if (entries.length > 512) throw new Error('npx 缓存较多,请先启动要使用的 DSH,再运行安装命令。')
|
|
90
|
+
for (const entry of entries) if (/^[a-f0-9]+$/i.test(entry.name) && entry.isDirectory()) {
|
|
91
|
+
add(path.join(cacheRoot, '_npx', entry.name, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'npx')
|
|
92
|
+
}
|
|
93
|
+
} catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error }
|
|
94
|
+
}
|
|
95
|
+
return [...found.values()]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function chooseDsh(options = {}) {
|
|
99
|
+
if (options.cli) return validateDshCli(options.cli).cli
|
|
100
|
+
const candidates = await discoverDsh(options)
|
|
101
|
+
if (!candidates.length) throw new Error('未找到 DSH。请先按原来的方式启动 DSH WebUI,再运行本命令。')
|
|
102
|
+
// A stopped host has no handshake yet. Follow the terminal's ordinary DSH
|
|
103
|
+
// resolution before consulting old NPX cache entries; no version sorting or
|
|
104
|
+
// downloading. This fallback never overrides an already running host.
|
|
105
|
+
const normal = candidates.find(item => item.source === 'PATH')
|
|
106
|
+
|| candidates.find(item => item.source === 'npm-global')
|
|
107
|
+
|| candidates.find(item => item.source === 'project' || item.source === 'source')
|
|
108
|
+
if (normal) return normal.cli
|
|
109
|
+
if (candidates.length === 1) return candidates[0].cli
|
|
110
|
+
// Multiple cache-only versions are not evidence of the user's choice.
|
|
111
|
+
// A live host normally bypasses this fallback via its native handshake.
|
|
112
|
+
const prompt = options.prompt || (process.stdin.isTTY && process.stdout.isTTY ? async message => {
|
|
113
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
114
|
+
try { return await rl.question(message) } finally { rl.close() }
|
|
115
|
+
} : undefined)
|
|
116
|
+
if (!prompt) throw new Error('找到多份 DSH。请先启动要使用的 DSH,或用 --dsh-cli 指定其 lib/bin.js;未修改插件。')
|
|
117
|
+
const list = candidates.map((item, index) => `${index + 1}. DSH ${clean(item.version)} — ${clean(item.cli)}`).join('\n')
|
|
118
|
+
const answer = String(await prompt(`找到多份 DSH,请选择要使用的一份(直接回车取消):\n${list}\n编号:`)).trim()
|
|
119
|
+
const index = Number(answer) - 1
|
|
120
|
+
if (!/^\d+$/.test(answer) || !Number.isInteger(index) || !candidates[index]) throw new Error('已取消安装,未修改插件。')
|
|
121
|
+
return candidates[index].cli
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// POSIX npm/npx normally starts through a bin symlink, not lib/bin.js.
|
|
125
|
+
// This is a conservative stop signal only; target authority is the handshake.
|
|
126
|
+
export function looksLikeDshProcess(command) {
|
|
127
|
+
return /(?:[/\\](?:dsh|apps[/\\]cli)[/\\]lib[/\\]bin\.js|[/\\](?:\.bin|bin)[/\\]dsh)(?:["'\s]|$)/i.test(command || '')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Conservatively prevent a second host while a live profile reloads on a
|
|
131
|
+
* non-default port. We use process listings only as a stop signal, never as
|
|
132
|
+
* authority to install, stop a PID, or select another user's data directory. */
|
|
133
|
+
export async function mayHaveRunningDsh() {
|
|
134
|
+
try {
|
|
135
|
+
if (process.platform === 'win32') {
|
|
136
|
+
const command = "Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='node'\" | Select-Object -ExpandProperty CommandLine | ConvertTo-Json -Compress"
|
|
137
|
+
const { stdout } = await execute('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], {
|
|
138
|
+
windowsHide: true, timeout: 10000, maxBuffer: 1024 * 1024,
|
|
139
|
+
})
|
|
140
|
+
const data = stdout.trim() ? JSON.parse(stdout) : []
|
|
141
|
+
return [data].flat().some(looksLikeDshProcess)
|
|
142
|
+
}
|
|
143
|
+
if (process.platform === 'linux') {
|
|
144
|
+
for (const pid of fs.readdirSync('/proc').filter(name => /^\d+$/.test(name))) {
|
|
145
|
+
try {
|
|
146
|
+
const args = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0')
|
|
147
|
+
if (args.some(looksLikeDshProcess)) return true
|
|
148
|
+
} catch { /* Exited or inaccessible process. */ }
|
|
149
|
+
}
|
|
150
|
+
return false
|
|
151
|
+
}
|
|
152
|
+
const { stdout } = await execute('/bin/ps', ['-axo', 'command='], { timeout: 10000, maxBuffer: 2 * 1024 * 1024 })
|
|
153
|
+
return looksLikeDshProcess(stdout)
|
|
154
|
+
} catch { return true } // Unknown process ownership must never authorize an extra host.
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function assertInstallTarget(host, { home, profile, cli, pid }) {
|
|
158
|
+
if (host.home !== home || host.profile !== profile || !Number.isInteger(host.pid) || host.pid < 1
|
|
159
|
+
|| host.pid !== pid || (cli && host.cli !== validateDshCli(cli).cli)
|
|
160
|
+
|| host.cli !== validateDshCli(host.cli).cli) {
|
|
161
|
+
throw new Error('当前 DSH 身份与安装目标不一致,未修改安装。')
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Preserve arguments but use the validated real entry point for restart.
|
|
166
|
+
* On macOS/Linux argv[0] can be npm's .bin/dsh symlink while host.cli is its
|
|
167
|
+
* canonical target. The worker requires a canonical CLI, as the WebUI updater
|
|
168
|
+
* already does. Never accept a different executable hidden in argv. */
|
|
169
|
+
export function installHostArgv(host) {
|
|
170
|
+
if (!Array.isArray(host.argv) || !host.argv.length || !host.argv.every(arg => typeof arg === 'string')
|
|
171
|
+
|| validateDshCli(host.argv[0]).cli !== host.cli) {
|
|
172
|
+
throw new Error('DSH 启动参数与安装目标不一致,未修改安装。')
|
|
173
|
+
}
|
|
174
|
+
return [host.cli, ...host.argv.slice(1)]
|
|
175
|
+
}
|
package/bin/native-control.mjs
CHANGED
|
@@ -51,14 +51,16 @@ export async function waitForJson(filename, accept, timeout = 20000) {
|
|
|
51
51
|
try { const value = JSON.parse(fs.readFileSync(filename, 'utf8')); if (accept(value)) return value } catch {}
|
|
52
52
|
await new Promise(resolve => setTimeout(resolve, 100))
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
const error = new Error('DSH 没有完成安装握手;原插件未替换。请确认 DSH WebUI 正在运行且允许原生插件热加载。')
|
|
55
|
+
error.code = 'DSH_HANDSHAKE_TIMEOUT'
|
|
56
|
+
throw error
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/** One bounded native handshake, including a possible offline host start.
|
|
58
60
|
* A listening WebUI is not proof that the asynchronous profile reload finished.
|
|
59
61
|
* Keep the same control entry/token while waiting; never restart a live host. */
|
|
60
62
|
export async function waitForInstallControl(filename, {
|
|
61
|
-
accept, ensureRunning, onWaiting = () => {}, timeoutMs = 180000, initialWaitMs = 5000,
|
|
63
|
+
accept, ensureRunning, onWaiting = () => {}, timeoutMs = 180000, initialWaitMs = 5000,
|
|
62
64
|
}) {
|
|
63
65
|
const deadline = Date.now() + timeoutMs
|
|
64
66
|
try { return await waitForJson(filename, accept, Math.min(initialWaitMs, timeoutMs)) }
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Verify a restored, plugin-free DSH through the same native control adapter.
|
|
2
|
+
* Only the first-install rollback uses this; ordinary updates probe the plugin.
|
|
3
|
+
* Bundled outside the profile so neither a failed add nor rollback removes it. */
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { randomBytes } from 'node:crypto'
|
|
7
|
+
import { attachControl, waitForJson } from './native-control.mjs'
|
|
8
|
+
|
|
9
|
+
export async function verifyNativeRestore(job, start, sessionIds, readableIds) {
|
|
10
|
+
const directory = path.join(job.home, 'harness-remote-updates', randomBytes(16).toString('hex'))
|
|
11
|
+
fs.mkdirSync(directory, { mode: 0o700 })
|
|
12
|
+
fs.writeFileSync(path.join(directory, 'package.json'), '{"type":"module"}\n', { mode: 0o600 })
|
|
13
|
+
const helper = path.join(directory, 'install-control.js'), token = randomBytes(24).toString('hex')
|
|
14
|
+
fs.copyFileSync(path.join(job.directory, 'install-control.js'), helper)
|
|
15
|
+
const remove = attachControl(job.profile, helper, { directory, token, pnpm: job.pnpm })
|
|
16
|
+
let ref
|
|
17
|
+
const request = async (method, payload = {}) => {
|
|
18
|
+
const response = await fetch(ref.origin + '/' + method, { method: 'POST', headers: {
|
|
19
|
+
authorization: `Bearer ${token}`, 'content-type': 'application/json',
|
|
20
|
+
}, body: JSON.stringify(payload), redirect: 'error', signal: AbortSignal.timeout(12000) })
|
|
21
|
+
const value = await response.json()
|
|
22
|
+
if (!response.ok) throw new Error('原 DSH 恢复检查未通过。')
|
|
23
|
+
return value
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const child = start()
|
|
27
|
+
ref = await waitForJson(path.join(directory, 'control-ready.json'), value =>
|
|
28
|
+
Number.isInteger(value.pid) && value.pid > 0 && (!child || value.pid === child.pid)
|
|
29
|
+
&& /^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(value.origin), 180000)
|
|
30
|
+
const host = await request('describe')
|
|
31
|
+
if (host.pid !== ref.pid || host.home !== job.home || host.profile !== job.profile
|
|
32
|
+
|| host.cli !== job.cli || host.dshVersion !== job.dshVersion || host.pluginVersion !== '0.0.0') {
|
|
33
|
+
throw new Error('恢复后的 DSH 身份不匹配。')
|
|
34
|
+
}
|
|
35
|
+
const items = (await request('read', { method: 'session.list' })).items
|
|
36
|
+
if (!Array.isArray(items) || JSON.stringify(items.map(s => s.sessionId).sort()) !== JSON.stringify(sessionIds)) {
|
|
37
|
+
throw new Error('恢复后的会话列表不一致。')
|
|
38
|
+
}
|
|
39
|
+
for (const sessionId of readableIds) await request('read', { method: 'session.history', payload: { sessionId, maxMessages: 1 } })
|
|
40
|
+
} finally {
|
|
41
|
+
try { remove() }
|
|
42
|
+
finally { if (ref) { try { await request('close') } catch {} } }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,33 +1,36 @@
|
|
|
1
|
-
import { validateCatalog, compareVersions, trustedReleaseAsset } from '../lib/update-policy.js'
|
|
2
|
-
|
|
3
|
-
/** Explicit CLI installation is not an automatic-update recommendation.
|
|
4
|
-
* Tested DSH/platform/architecture lists describe evidence, not admission.
|
|
5
|
-
* Keep trusted artifacts, explicit withdrawals and the no-downgrade floor.
|
|
6
|
-
*/
|
|
7
|
-
export function selectInstallTarget(pinned, remote, current, now = Date.now()) {
|
|
8
|
-
const local = validateCatalog(pinned.catalog)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
1
|
+
import { validateCatalog, compareVersions, trustedReleaseAsset } from '../lib/update-policy.js'
|
|
2
|
+
|
|
3
|
+
/** Explicit CLI installation is not an automatic-update recommendation.
|
|
4
|
+
* Tested DSH/platform/architecture lists describe evidence, not admission.
|
|
5
|
+
* Keep trusted artifacts, explicit withdrawals and the no-downgrade floor.
|
|
6
|
+
*/
|
|
7
|
+
export function selectInstallTarget(pinned, remote, current, now = Date.now()) {
|
|
8
|
+
const local = validateCatalog(pinned.catalog)
|
|
9
|
+
// Choosing an explicit prerelease installer authorizes its exact embedded
|
|
10
|
+
// preview, not arbitrary remote previews or a change to stable recommendations.
|
|
11
|
+
const channel = pinned.version.includes('-') ? 'preview' : 'stable'
|
|
12
|
+
const bundled = local.releases.find(r => r.version === pinned.version && r.channel === channel)
|
|
13
|
+
if (!bundled || !trustedReleaseAsset(bundled.asset, bundled.version)) {
|
|
14
|
+
throw new Error('安装包信息不完整,请重新获取官方安装器。')
|
|
15
|
+
}
|
|
16
|
+
let online
|
|
17
|
+
try {
|
|
18
|
+
const candidate = validateCatalog(remote)
|
|
19
|
+
if (candidate.issuedAt <= now + 300000 && candidate.expiresAt > now) online = candidate
|
|
20
|
+
} catch { /* The authenticated npm package remains usable without the catalog. */ }
|
|
21
|
+
|
|
22
|
+
// An installer never silently substitutes an older plugin than its own bundle.
|
|
23
|
+
// Keep the bundled bytes/hash for the same version even if remote metadata differs.
|
|
24
|
+
const candidates = [bundled, ...(online?.releases || []).filter(r => r.channel === 'stable'
|
|
25
|
+
&& compareVersions(r.version, bundled.version) > 0 && trustedReleaseAsset(r.asset, r.version))]
|
|
26
|
+
.sort((a, b) => compareVersions(b.version, a.version))
|
|
27
|
+
const rules = [...local.blocked, ...(online?.blocked || [])]
|
|
28
|
+
const blocked = release => rules.find(b => b.pluginVersion === release.version
|
|
29
|
+
&& (!b.dsh || b.dsh.includes(current.agentVersion))
|
|
30
|
+
&& (!b.platforms || b.platforms.includes(current.platform)))
|
|
31
|
+
const release = candidates.find(r => !blocked(r))
|
|
32
|
+
if (!release) {
|
|
33
|
+
throw new Error(`当前插件存在已知不兼容问题:${blocked(bundled).reason};未替换现有插件。`)
|
|
34
|
+
}
|
|
35
|
+
return release
|
|
36
|
+
}
|