dsh-wechat-remote 1.7.5 → 1.7.6
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 +14 -12
- package/assets/plugin.tgz +0 -0
- package/assets/release.json +47 -10
- package/bin/dsh-discovery.mjs +165 -0
- package/bin/native-control.mjs +4 -2
- package/bin/release-selection.mjs +36 -33
- package/bin/setup.mjs +51 -43
- package/lib/update-policy.js +26 -27
- package/lib/update-worker.js +54 -3
- 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.6,安装同版本正式插件。改善全局安装及 npx 启动的识别,修复 ARM64 设备的一键更新误拦截。
|
|
4
4
|
|
|
5
5
|
首次安装和升级旧插件使用同一个命令:
|
|
6
6
|
|
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
npx -y dsh-wechat-remote@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
请先按原来的方式启动 DSH WebUI,再在同一个系统用户下另开终端运行本命令。无需改成全局安装 DSH;使用 `npx @deepseek-ai/dsh web` 的用户也使用同一条安装命令。需要 Node.js 22.13 或更高版本,无需另装 pnpm 或 Git。
|
|
12
|
+
|
|
13
|
+
安装器安装正式插件,保留原配置、配对和会话,不会升级 DSH 本体。请先结束运行中的任务;安装完成后自动重启原 DSH 并尝试打开 WebUI。
|
|
14
|
+
|
|
15
|
+
DSH 的新版本、预览版本或未登记的主机架构不会仅因缺少验证记录而被拒绝安装。安装器仍检查必要运行能力、安装包完整性和已知不兼容问题,并保留失败恢复所需的备份;无法确认恢复结果时会提示保留数据。这不代表所有宿主组合均已通过功能验证。
|
|
16
|
+
|
|
17
|
+
支持已验证的 DSH `0.1.1-rc.2`、`0.1.2-rc.1`、`0.1.5-rc.1`,Windows / macOS Intel / Linux x64。普通 Node 启动、独立的 macOS launchd 和 Linux 用户级 systemd 服务可自动恢复;无法确认启动方式时不自动停止主机。
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
DSH 的新版本、预览版本或未登记的主机架构不会仅因缺少验证记录而被拒绝安装。安装器仍检查必要运行能力、安装包完整性和已知不兼容问题,并保留失败恢复所需的备份;无法确认恢复结果时会提示保留数据。这不代表所有宿主组合均已通过功能验证。
|
|
19
|
+
另已在 ARM64 Linux 电视盒子上验证 DSH `0.1.5-rc.1` 的首次安装、重复执行及重装。1.7.4 改善较慢主机启动时被误判为安装超时的问题;安装过程中请等待完成,勿重复执行命令。
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
另已在 ARM64 Linux 电视盒子上验证 DSH `0.1.5-rc.1` 的首次安装、重复执行及重装。1.7.4 改善较慢主机启动时被误判为安装超时的问题;安装过程中请等待完成,勿重复执行命令。
|
|
21
|
+
旧插件的一键更新如被版本或架构判断拦截,可用上述安装命令升级一次。从插件 1.7.6 起,未登记的 CPU 架构或 DSH 版本不再影响更新入口;执行前仍会检查实际安装与重启条件,无需因此降低 DSH 版本或删除节点。
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
默认使用 `web` 配置和原来的 DSH 数据目录。自定义数据目录时,在新终端设置相同的 `DSH_HOME`,或附加 `--home "数据目录"`;使用其他配置时附加 `--profile 配置名称`。WSL、容器和其他系统用户内的 DSH,需要在对应环境中执行安装,不能跨环境替换。
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
DSH 未启动且找到多个安装版本时,会要求选择,不会随意选一份。也可用 `--dsh-cli "DSH的lib/bin.js路径"` 明确指定。安装损坏需要重装时附加 `--repair`;不会自动降级。
|
package/assets/plugin.tgz
CHANGED
|
Binary file
|
package/assets/release.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.7.
|
|
2
|
+
"version": "1.7.6",
|
|
3
3
|
"catalog": {
|
|
4
4
|
"schemaVersion": 1,
|
|
5
|
-
"revision": "installer-1.7.
|
|
6
|
-
"issuedAt":
|
|
7
|
-
"expiresAt":
|
|
5
|
+
"revision": "installer-1.7.6",
|
|
6
|
+
"issuedAt": 1789480914884,
|
|
7
|
+
"expiresAt": 1791900114884,
|
|
8
8
|
"releases": [
|
|
9
9
|
{
|
|
10
|
-
"version": "1.7.
|
|
10
|
+
"version": "1.7.6",
|
|
11
11
|
"channel": "stable",
|
|
12
12
|
"dsh": [
|
|
13
13
|
"0.1.1-rc.2",
|
|
@@ -20,13 +20,50 @@
|
|
|
20
20
|
"linux"
|
|
21
21
|
],
|
|
22
22
|
"architectures": [
|
|
23
|
-
"x64"
|
|
23
|
+
"x64",
|
|
24
|
+
"arm64"
|
|
24
25
|
],
|
|
25
26
|
"asset": {
|
|
26
|
-
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.
|
|
27
|
-
"sha256": "
|
|
28
|
-
"bytes":
|
|
29
|
-
}
|
|
27
|
+
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.6/harness-remote-dsh-wechat-remote-1.7.6.tgz",
|
|
28
|
+
"sha256": "ddd14140e93eaf1704dcca7380b1f99d8243089c2be337eaf94d2e87de4a0ff5",
|
|
29
|
+
"bytes": 9549941
|
|
30
|
+
},
|
|
31
|
+
"targets": [
|
|
32
|
+
{
|
|
33
|
+
"platform": "windows",
|
|
34
|
+
"arch": "x64",
|
|
35
|
+
"dsh": [
|
|
36
|
+
"0.1.1-rc.2",
|
|
37
|
+
"0.1.2-rc.1",
|
|
38
|
+
"0.1.5-rc.1"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"platform": "macos",
|
|
43
|
+
"arch": "x64",
|
|
44
|
+
"dsh": [
|
|
45
|
+
"0.1.1-rc.2",
|
|
46
|
+
"0.1.2-rc.1",
|
|
47
|
+
"0.1.5-rc.1"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"platform": "linux",
|
|
52
|
+
"arch": "x64",
|
|
53
|
+
"dsh": [
|
|
54
|
+
"0.1.1-rc.2",
|
|
55
|
+
"0.1.2-rc.1",
|
|
56
|
+
"0.1.5-rc.1"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"platform": "linux",
|
|
61
|
+
"arch": "arm64",
|
|
62
|
+
"dsh": [
|
|
63
|
+
"0.1.5-rc.1"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
30
67
|
}
|
|
31
68
|
],
|
|
32
69
|
"blocked": [],
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
|
|
15
|
+
export function validateDshCli(filename) {
|
|
16
|
+
const cli = fs.realpathSync(path.resolve(filename))
|
|
17
|
+
const root = path.resolve(cli, '../..')
|
|
18
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
19
|
+
const bin = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.dsh
|
|
20
|
+
if (manifest.name !== PACKAGE || path.basename(cli) !== 'bin.js' || !fs.statSync(cli).isFile()
|
|
21
|
+
|| typeof bin !== 'string' || fs.realpathSync(path.resolve(root, bin)) !== cli) {
|
|
22
|
+
throw new Error('指定路径不是有效的 DSH 启动程序,未修改安装。')
|
|
23
|
+
}
|
|
24
|
+
return { cli, version: String(manifest.version || '') }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveHome(value, fallback = os.homedir()) {
|
|
28
|
+
const raw = String(value || '')
|
|
29
|
+
const configured = raw.trim() ? raw : ''
|
|
30
|
+
return path.resolve(configured === '~' ? fallback : /^~[/\\]/.test(configured)
|
|
31
|
+
? path.join(fallback, configured.slice(2)) : configured || path.join(fallback, '.dsh'))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** npm's own config resolves .npmrc, custom cache/prefix and Node managers.
|
|
35
|
+
* Query individual public values; never read or log registry credentials. */
|
|
36
|
+
async function npmLocation(key, env, cwd) {
|
|
37
|
+
let npm = env.npm_execpath
|
|
38
|
+
if (!npm || !fs.existsSync(npm)) {
|
|
39
|
+
const roots = [path.dirname(process.execPath), ...(env.PATH || '').split(path.delimiter)]
|
|
40
|
+
npm = roots.flatMap(dir => [path.join(dir, 'node_modules/npm/bin/npm-cli.js'),
|
|
41
|
+
path.join(dir, '../lib/node_modules/npm/bin/npm-cli.js')]).find(file => fs.existsSync(file))
|
|
42
|
+
}
|
|
43
|
+
if (!npm) return ''
|
|
44
|
+
try {
|
|
45
|
+
const { stdout } = await execute(process.execPath, [npm, 'config', 'get', key], {
|
|
46
|
+
cwd, env, windowsHide: true, timeout: 10000, maxBuffer: 16384,
|
|
47
|
+
})
|
|
48
|
+
const value = stdout.trim()
|
|
49
|
+
return value && !/[\r\n\0]/.test(value) && path.isAbsolute(value) ? value : ''
|
|
50
|
+
} catch { return '' }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function discoverDsh({ env = process.env, cwd = process.cwd(), cache, prefix } = {}) {
|
|
54
|
+
const found = new Map()
|
|
55
|
+
const add = (filename, source) => {
|
|
56
|
+
try {
|
|
57
|
+
const item = validateDshCli(filename)
|
|
58
|
+
if (!found.has(item.cli)) found.set(item.cli, { ...item, source })
|
|
59
|
+
} catch { /* A missing/broken cache entry is not a usable DSH. */ }
|
|
60
|
+
}
|
|
61
|
+
for (const dir of (env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
62
|
+
add(path.join(dir, 'dsh'), 'PATH') // POSIX symlink, never execute a Windows shim.
|
|
63
|
+
add(path.join(dir, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'PATH')
|
|
64
|
+
add(path.join(dir, '../lib/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'PATH')
|
|
65
|
+
add(path.join(dir, '../@deepseek-ai/dsh/lib/bin.js'), 'PATH') // npm .bin, including Windows.
|
|
66
|
+
}
|
|
67
|
+
for (let dir = path.resolve(cwd);;) {
|
|
68
|
+
add(path.join(dir, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'project')
|
|
69
|
+
add(path.join(dir, 'apps/cli/lib/bin.js'), 'source')
|
|
70
|
+
const parent = path.dirname(dir)
|
|
71
|
+
if (parent === dir) break
|
|
72
|
+
dir = parent
|
|
73
|
+
}
|
|
74
|
+
const locations = await Promise.all([
|
|
75
|
+
cache !== undefined ? cache : env.npm_config_cache || npmLocation('cache', env, cwd),
|
|
76
|
+
prefix !== undefined ? prefix : npmLocation('prefix', env, cwd),
|
|
77
|
+
])
|
|
78
|
+
if (locations[1]) {
|
|
79
|
+
add(path.join(locations[1], 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'npm-global')
|
|
80
|
+
add(path.join(locations[1], 'lib/node_modules/@deepseek-ai/dsh/lib/bin.js'), 'npm-global')
|
|
81
|
+
}
|
|
82
|
+
const cacheRoot = locations[0]
|
|
83
|
+
if (cacheRoot) {
|
|
84
|
+
try {
|
|
85
|
+
const entries = fs.readdirSync(path.join(cacheRoot, '_npx'), { withFileTypes: true })
|
|
86
|
+
if (entries.length > 512) throw new Error('npx 缓存较多,请先启动要使用的 DSH,再运行安装命令。')
|
|
87
|
+
for (const entry of entries) if (/^[a-f0-9]+$/i.test(entry.name) && entry.isDirectory()) {
|
|
88
|
+
add(path.join(cacheRoot, '_npx', entry.name, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), 'npx')
|
|
89
|
+
}
|
|
90
|
+
} catch (error) { if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') throw error }
|
|
91
|
+
}
|
|
92
|
+
return [...found.values()]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function chooseDsh(options = {}) {
|
|
96
|
+
if (options.cli) return validateDshCli(options.cli).cli
|
|
97
|
+
const candidates = await discoverDsh(options)
|
|
98
|
+
if (!candidates.length) throw new Error('未找到 DSH。请先按原来的方式启动 DSH WebUI,再运行本命令。')
|
|
99
|
+
if (candidates.length === 1) return candidates[0].cli
|
|
100
|
+
// Multiple installed/cache versions are not evidence of the user's choice.
|
|
101
|
+
// A live host normally bypasses this fallback via its native handshake.
|
|
102
|
+
const prompt = options.prompt || (process.stdin.isTTY && process.stdout.isTTY ? async message => {
|
|
103
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
104
|
+
try { return await rl.question(message) } finally { rl.close() }
|
|
105
|
+
} : undefined)
|
|
106
|
+
if (!prompt) throw new Error('找到多份 DSH。请先启动要使用的 DSH,或用 --dsh-cli 指定其 lib/bin.js;未修改插件。')
|
|
107
|
+
const list = candidates.map((item, index) => `${index + 1}. DSH ${clean(item.version)} — ${clean(item.cli)}`).join('\n')
|
|
108
|
+
const answer = String(await prompt(`找到多份 DSH,请选择要使用的一份(直接回车取消):\n${list}\n编号:`)).trim()
|
|
109
|
+
const index = Number(answer) - 1
|
|
110
|
+
if (!/^\d+$/.test(answer) || !Number.isInteger(index) || !candidates[index]) throw new Error('已取消安装,未修改插件。')
|
|
111
|
+
return candidates[index].cli
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// POSIX npm/npx normally starts through a bin symlink, not lib/bin.js.
|
|
115
|
+
// This is a conservative stop signal only; target authority is the handshake.
|
|
116
|
+
export function looksLikeDshProcess(command) {
|
|
117
|
+
return /(?:[/\\](?:dsh|apps[/\\]cli)[/\\]lib[/\\]bin\.js|[/\\](?:\.bin|bin)[/\\]dsh)(?:["'\s]|$)/i.test(command || '')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Conservatively prevent a second host while a live profile reloads on a
|
|
121
|
+
* non-default port. We use process listings only as a stop signal, never as
|
|
122
|
+
* authority to install, stop a PID, or select another user's data directory. */
|
|
123
|
+
export async function mayHaveRunningDsh() {
|
|
124
|
+
try {
|
|
125
|
+
if (process.platform === 'win32') {
|
|
126
|
+
const command = "Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='node'\" | Select-Object -ExpandProperty CommandLine | ConvertTo-Json -Compress"
|
|
127
|
+
const { stdout } = await execute('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], {
|
|
128
|
+
windowsHide: true, timeout: 10000, maxBuffer: 1024 * 1024,
|
|
129
|
+
})
|
|
130
|
+
const data = stdout.trim() ? JSON.parse(stdout) : []
|
|
131
|
+
return [data].flat().some(looksLikeDshProcess)
|
|
132
|
+
}
|
|
133
|
+
if (process.platform === 'linux') {
|
|
134
|
+
for (const pid of fs.readdirSync('/proc').filter(name => /^\d+$/.test(name))) {
|
|
135
|
+
try {
|
|
136
|
+
const args = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0')
|
|
137
|
+
if (args.some(looksLikeDshProcess)) return true
|
|
138
|
+
} catch { /* Exited or inaccessible process. */ }
|
|
139
|
+
}
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
const { stdout } = await execute('/bin/ps', ['-axo', 'command='], { timeout: 10000, maxBuffer: 2 * 1024 * 1024 })
|
|
143
|
+
return looksLikeDshProcess(stdout)
|
|
144
|
+
} catch { return true } // Unknown process ownership must never authorize an extra host.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function assertInstallTarget(host, { home, profile, cli, pid }) {
|
|
148
|
+
if (host.home !== home || host.profile !== profile || !Number.isInteger(host.pid) || host.pid < 1
|
|
149
|
+
|| host.pid !== pid || (cli && host.cli !== validateDshCli(cli).cli)
|
|
150
|
+
|| host.cli !== validateDshCli(host.cli).cli) {
|
|
151
|
+
throw new Error('当前 DSH 身份与安装目标不一致,未修改安装。')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Preserve arguments but use the validated real entry point for restart.
|
|
156
|
+
* On macOS/Linux argv[0] can be npm's .bin/dsh symlink while host.cli is its
|
|
157
|
+
* canonical target. The worker requires a canonical CLI, as the WebUI updater
|
|
158
|
+
* already does. Never accept a different executable hidden in argv. */
|
|
159
|
+
export function installHostArgv(host) {
|
|
160
|
+
if (!Array.isArray(host.argv) || !host.argv.length || !host.argv.every(arg => typeof arg === 'string')
|
|
161
|
+
|| validateDshCli(host.argv[0]).cli !== host.cli) {
|
|
162
|
+
throw new Error('DSH 启动参数与安装目标不一致,未修改安装。')
|
|
163
|
+
}
|
|
164
|
+
return [host.cli, ...host.argv.slice(1)]
|
|
165
|
+
}
|
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)) }
|
|
@@ -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
|
+
}
|
package/bin/setup.mjs
CHANGED
|
@@ -1,37 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
4
|
import net from 'node:net'
|
|
6
5
|
import { spawn } from 'node:child_process'
|
|
7
6
|
import { randomBytes, createHash } from 'node:crypto'
|
|
8
7
|
import { fileURLToPath } from 'node:url'
|
|
9
8
|
import { attachControl, waitForJson, waitForInstallControl } from './native-control.mjs'
|
|
10
9
|
import { resolveInstallRuntime, verifyInstallRuntime } from '../lib/install-runtime.js'
|
|
11
|
-
import { compareVersions } from '../lib/update-policy.js'
|
|
12
|
-
import { selectInstallTarget } from './release-selection.mjs'
|
|
10
|
+
import { compareVersions } from '../lib/update-policy.js'
|
|
11
|
+
import { selectInstallTarget } from './release-selection.mjs'
|
|
13
12
|
import { boundedFetch, downloadRelease, auditArchive } from '../lib/update-download.js'
|
|
14
13
|
import { writePrivateJsonAtomic } from '../lib/secure-file.js'
|
|
15
14
|
import { control, validateJob, releaseOwnedUpdateLock } from '../lib/update-worker.js'
|
|
15
|
+
import { chooseDsh, validateDshCli, resolveHome, mayHaveRunningDsh, assertInstallTarget, installHostArgv } from './dsh-discovery.mjs'
|
|
16
16
|
|
|
17
17
|
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
18
18
|
const packageName = '@harness-remote/dsh-wechat-remote'
|
|
19
19
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
20
|
-
export function findDsh() {
|
|
21
|
-
const candidates = []
|
|
22
|
-
for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
23
|
-
candidates.push(path.join(dir, 'node_modules/@deepseek-ai/dsh/lib/bin.js'), path.join(dir, '../lib/node_modules/@deepseek-ai/dsh/lib/bin.js'))
|
|
24
|
-
try { candidates.push(fs.realpathSync(path.join(dir, 'dsh'))) } catch {}
|
|
25
|
-
}
|
|
26
|
-
for (const candidate of candidates) {
|
|
27
|
-
try {
|
|
28
|
-
const cli = fs.realpathSync(candidate)
|
|
29
|
-
const manifest = JSON.parse(fs.readFileSync(path.resolve(cli, '../../package.json'), 'utf8'))
|
|
30
|
-
if (manifest.name === '@deepseek-ai/dsh' && path.basename(cli) === 'bin.js') return cli
|
|
31
|
-
} catch {}
|
|
32
|
-
}
|
|
33
|
-
throw new Error('未找到已安装的 DSH。请先安装并启动 DeepSeek Harness。')
|
|
34
|
-
}
|
|
35
20
|
function portBusy(port) {
|
|
36
21
|
return new Promise(resolve => {
|
|
37
22
|
const socket = net.connect({ host: '127.0.0.1', port })
|
|
@@ -39,16 +24,16 @@ function portBusy(port) {
|
|
|
39
24
|
socket.setTimeout(500, () => end(false)); socket.once('connect', () => end(true)); socket.once('error', () => end(false))
|
|
40
25
|
})
|
|
41
26
|
}
|
|
42
|
-
export async function selectRelease(host, assetsRoot = path.join(root, 'assets'), repair = false,
|
|
43
|
-
fetchCatalog = () => boundedFetch('https://relay.xyxfood.xyz/v1/update-policy', 256 * 1024)) {
|
|
27
|
+
export async function selectRelease(host, assetsRoot = path.join(root, 'assets'), repair = false,
|
|
28
|
+
fetchCatalog = () => boundedFetch('https://relay.xyxfood.xyz/v1/update-policy', 256 * 1024)) {
|
|
44
29
|
const current = { agentKind: 'dsh', agentVersion: host.dshVersion, pluginVersion: host.pluginVersion,
|
|
45
30
|
platform: { win32: 'windows', darwin: 'macos', linux: 'linux' }[host.platform], arch: host.arch }
|
|
46
31
|
const pinned = JSON.parse(fs.readFileSync(path.join(assetsRoot, 'release.json'), 'utf8'))
|
|
47
|
-
let remote
|
|
48
|
-
try {
|
|
49
|
-
remote = JSON.parse((await fetchCatalog()).toString('utf8'))
|
|
50
|
-
} catch { /* Authenticated npm package retains its bundled release. */ }
|
|
51
|
-
const release = selectInstallTarget(pinned, remote, current)
|
|
32
|
+
let remote
|
|
33
|
+
try {
|
|
34
|
+
remote = JSON.parse((await fetchCatalog()).toString('utf8'))
|
|
35
|
+
} catch { /* Authenticated npm package retains its bundled release. */ }
|
|
36
|
+
const release = selectInstallTarget(pinned, remote, current)
|
|
52
37
|
if (host.pluginVersion !== '0.0.0' && (compareVersions(host.pluginVersion, release.version) > 0
|
|
53
38
|
|| (compareVersions(host.pluginVersion, release.version) === 0 && !repair))) return { release, archive: null }
|
|
54
39
|
let archive
|
|
@@ -57,10 +42,11 @@ export async function selectRelease(host, assetsRoot = path.join(root, 'assets')
|
|
|
57
42
|
auditArchive(archive, release)
|
|
58
43
|
return { release, archive }
|
|
59
44
|
}
|
|
60
|
-
export async function install({ profileName = 'web', cli
|
|
45
|
+
export async function install({ profileName = 'web', cli, home: configuredHome, assetsRoot, open = true, repair = false } = {}) {
|
|
61
46
|
if (!/^[A-Za-z0-9_-]{1,80}$/.test(profileName)) throw new Error('无效的 profile 名称。')
|
|
47
|
+
if (cli) cli = validateDshCli(cli).cli
|
|
62
48
|
const runtime = resolveInstallRuntime(root); await verifyInstallRuntime(runtime)
|
|
63
|
-
const home =
|
|
49
|
+
const home = resolveHome(configuredHome || process.env.DSH_HOME)
|
|
64
50
|
fs.mkdirSync(home, { recursive: true, mode: 0o700 })
|
|
65
51
|
if (fs.realpathSync(home) !== home) throw new Error('DSH 数据目录是链接,暂不自动替换安装。')
|
|
66
52
|
const profile = path.join(home, 'profiles', profileName), id = randomBytes(16).toString('hex')
|
|
@@ -72,15 +58,17 @@ export async function install({ profileName = 'web', cli = findDsh(), assetsRoot
|
|
|
72
58
|
try {
|
|
73
59
|
// Initialize missing profiles through DSH itself, never synthesize a bundle list.
|
|
74
60
|
if (!fs.existsSync(path.join(profile, 'package.json'))) {
|
|
61
|
+
if (await mayHaveRunningDsh()) throw new Error('当前目录下没有此 DSH 配置,但检测到 DSH 正在运行。请使用相同的 DSH_HOME/--home 和 --profile,未修改正在运行的实例。')
|
|
62
|
+
cli ||= await chooseDsh()
|
|
75
63
|
const log = fs.openSync(path.join(directory, 'startup.log'), 'a', 0o600)
|
|
76
64
|
launched = spawn(process.execPath, [cli, '--profile', profileName, '--no-open'], {
|
|
77
|
-
cwd: process.cwd(), env: process.env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
65
|
+
cwd: process.cwd(), env: { ...process.env, DSH_HOME: home }, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
78
66
|
fs.closeSync(log); launched.on('error', () => {}); launched.unref()
|
|
79
|
-
const initializedBy = Date.now() + 180000
|
|
80
|
-
while (!fs.existsSync(path.join(profile, 'package.json')) && Date.now() < initializedBy) {
|
|
81
|
-
if (launched.exitCode !== null || launched.signalCode !== null) break
|
|
82
|
-
await sleep(100)
|
|
83
|
-
}
|
|
67
|
+
const initializedBy = Date.now() + 180000
|
|
68
|
+
while (!fs.existsSync(path.join(profile, 'package.json')) && Date.now() < initializedBy) {
|
|
69
|
+
if (launched.exitCode !== null || launched.signalCode !== null) break
|
|
70
|
+
await sleep(100)
|
|
71
|
+
}
|
|
84
72
|
if (!fs.existsSync(path.join(profile, 'package.json'))) throw new Error('DSH 未能初始化 profile;详情见本机安装记录。')
|
|
85
73
|
await sleep(2000)
|
|
86
74
|
}
|
|
@@ -95,17 +83,23 @@ export async function install({ profileName = 'web', cli = findDsh(), assetsRoot
|
|
|
95
83
|
accept: v => Number.isInteger(v.pid) && v.pid > 0 && (!launched || v.pid === launched.pid),
|
|
96
84
|
onWaiting: () => console.log('正在等待 DSH 完成安装准备,请勿重复执行命令…'),
|
|
97
85
|
ensureRunning: async () => {
|
|
98
|
-
if (launched || await portBusy(Number(process.env.DSH_PORT || 3080))) return
|
|
86
|
+
if (launched || await portBusy(Number(process.env.DSH_PORT || 3080)) || await mayHaveRunningDsh()) return
|
|
87
|
+
cli ||= await chooseDsh()
|
|
99
88
|
const log = fs.openSync(path.join(directory, 'startup.log'), 'a', 0o600)
|
|
100
89
|
launched = spawn(process.execPath, [cli, '--profile', profileName, '--no-open'], {
|
|
101
|
-
cwd: process.cwd(), env: process.env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
90
|
+
cwd: process.cwd(), env: { ...process.env, DSH_HOME: home }, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
102
91
|
fs.closeSync(log); launched.on('error', () => {}); launched.unref()
|
|
103
92
|
},
|
|
104
93
|
})
|
|
94
|
+
if (!/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(ref.origin) || Number(new URL(ref.origin).port) > 65535) {
|
|
95
|
+
ref = undefined
|
|
96
|
+
throw new Error('DSH 安装握手地址无效,未修改插件。')
|
|
97
|
+
}
|
|
105
98
|
writePrivateJsonAtomic(path.join(directory, 'handshake.json'), {
|
|
106
99
|
state: 'ready', elapsedMs: Date.now() - handshakeStarted, startedHost: Boolean(launched),
|
|
107
100
|
})
|
|
108
|
-
} catch {
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error.code !== 'DSH_HANDSHAKE_TIMEOUT') throw error
|
|
109
103
|
writePrivateJsonAtomic(path.join(directory, 'handshake.json'), {
|
|
110
104
|
state: 'timeout', elapsedMs: Date.now() - handshakeStarted, startedHost: Boolean(launched),
|
|
111
105
|
})
|
|
@@ -113,13 +107,16 @@ export async function install({ profileName = 'web', cli = findDsh(), assetsRoot
|
|
|
113
107
|
}
|
|
114
108
|
job = { controlOrigin: ref.origin, statusToken: token }
|
|
115
109
|
const host = await control(job, 'describe')
|
|
116
|
-
|
|
110
|
+
// The authenticated live host is authoritative. A global install or cached
|
|
111
|
+
// version found on PATH must not override the actual npx/local/source host.
|
|
112
|
+
// Explicit choices and installer-started hosts still require an exact match.
|
|
113
|
+
assertInstallTarget(host, { home, profile, cli, pid: launched?.pid || ref.pid })
|
|
117
114
|
remove(); remove = undefined
|
|
118
115
|
await sleep(600)
|
|
119
116
|
const selected = await selectRelease(host, assetsRoot, repair)
|
|
120
117
|
if (!selected.archive) { console.log(`插件 ${host.pluginVersion} 无需更新。`); return { version: host.pluginVersion, changed: false } }
|
|
121
118
|
console.log(`正在安装插件 ${selected.release.version},保留原配对与会话…`)
|
|
122
|
-
job = { ...host, id, directory, parentPid: host.pid, pnpm: runtime.cli, targetVersion: selected.release.version,
|
|
119
|
+
job = { ...host, argv: installHostArgv(host), id, directory, parentPid: host.pid, pnpm: runtime.cli, targetVersion: selected.release.version,
|
|
123
120
|
previousVersion: host.pluginVersion, statusToken: token, controlOrigin: ref.origin }
|
|
124
121
|
validateJob(job)
|
|
125
122
|
fs.writeFileSync(path.join(directory, 'release.tgz'), selected.archive, { mode: 0o600, flag: 'wx' })
|
|
@@ -156,10 +153,21 @@ export async function install({ profileName = 'web', cli = findDsh(), assetsRoot
|
|
|
156
153
|
if (locked && !authorized) releaseOwnedUpdateLock(lockFile, id)
|
|
157
154
|
}
|
|
158
155
|
}
|
|
156
|
+
export function parseArguments(args) {
|
|
157
|
+
const options = {}, seen = new Set()
|
|
158
|
+
const names = { '--profile': 'profileName', '--dsh-cli': 'cli', '--home': 'home' }
|
|
159
|
+
for (let i = 0; i < args.length; i++) {
|
|
160
|
+
const flag = args[i]
|
|
161
|
+
if (seen.has(flag)) throw new Error('参数重复。使用 --help 查看用法。')
|
|
162
|
+
seen.add(flag)
|
|
163
|
+
if (flag === '--repair') options.repair = true
|
|
164
|
+
else if (names[flag] && args[i + 1] && !args[i + 1].startsWith('--')) options[names[flag]] = args[++i]
|
|
165
|
+
else throw new Error('不支持的参数。使用 --help 查看用法。')
|
|
166
|
+
}
|
|
167
|
+
return options
|
|
168
|
+
}
|
|
159
169
|
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
160
|
-
const args = process.argv.slice(2)
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
else if (args.length && !(args.length === 2 && args[0] === '--profile')) { console.error('不支持的参数。使用 --help 查看用法。'); process.exitCode = 1 }
|
|
164
|
-
else install({ profileName: args[1] || 'web', repair }).catch(error => { console.error(error.message); process.exitCode = 1 })
|
|
170
|
+
const args = process.argv.slice(2)
|
|
171
|
+
if (args.includes('--help') || args.includes('-h')) console.log('安装或升级 DSH 微信连接插件:npx -y dsh-wechat-remote@latest\n请先按原来的方式启动 DSH WebUI,支持全局安装与 npx 启动。\n可选:--profile <名称>(默认 web);--home <DSH 数据目录>;--dsh-cli <DSH 的 lib/bin.js>;--repair(重新安装,不降级)')
|
|
172
|
+
else Promise.resolve().then(() => install(parseArguments(args))).catch(error => { console.error(error.message); process.exitCode = 1 })
|
|
165
173
|
}
|
package/lib/update-policy.js
CHANGED
|
@@ -41,8 +41,17 @@ function validateCatalog(value) {
|
|
|
41
41
|
if (!c || c.schemaVersion !== 1 || typeof c.revision !== "string" || !/^[\w.-]{1,80}$/.test(c.revision) || !Number.isSafeInteger(c.issuedAt) || !Number.isSafeInteger(c.expiresAt) || c.expiresAt <= c.issuedAt || c.expiresAt - c.issuedAt > 32 * 864e5 || !Array.isArray(c.releases) || c.releases.length > 100 || !Array.isArray(c.blocked) || c.blocked.length > 100 || !strings(c.retiredDsh)) throw new Error("Invalid update catalog");
|
|
42
42
|
const versions = /* @__PURE__ */ new Set();
|
|
43
43
|
for (const r of c.releases) {
|
|
44
|
-
if (!r || !validVersion(r.version) || versions.has(r.version) || !["stable", "preview"].includes(r.channel) || r.channel === "stable" && versionPattern.exec(r.version)[4] || !strings(r.dsh) || !r.dsh.length || !r.dsh.every(validVersion) || !strings(r.platforms, 3) || !r.platforms.length || !r.platforms.every((p) => ["windows", "macos", "linux"].includes(p)) || !strings(r.architectures,
|
|
44
|
+
if (!r || !validVersion(r.version) || versions.has(r.version) || !["stable", "preview"].includes(r.channel) || r.channel === "stable" && versionPattern.exec(r.version)[4] || !strings(r.dsh) || !r.dsh.length || !r.dsh.every(validVersion) || !strings(r.platforms, 3) || !r.platforms.length || !r.platforms.every((p) => ["windows", "macos", "linux"].includes(p)) || !strings(r.architectures, 32) || !r.architectures.length || !r.architectures.every((a) => /^[a-z0-9_-]{1,32}$/.test(a)) || r.asset && !trustedReleaseAsset(r.asset, r.version)) throw new Error("Invalid release entry");
|
|
45
45
|
versions.add(r.version);
|
|
46
|
+
if (r.targets !== void 0) {
|
|
47
|
+
if (!Array.isArray(r.targets) || !r.targets.length || r.targets.length > 12) throw new Error("Invalid release targets");
|
|
48
|
+
const seen = /* @__PURE__ */ new Set();
|
|
49
|
+
for (const target of r.targets) {
|
|
50
|
+
const key = `${target?.platform}:${target?.arch}`;
|
|
51
|
+
if (!target || !r.platforms.includes(target.platform) || !r.architectures.includes(target.arch) || !strings(target.dsh) || !target.dsh.length || !target.dsh.every((v) => r.dsh.includes(v)) || seen.has(key)) throw new Error("Invalid release target");
|
|
52
|
+
seen.add(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
for (const b of c.blocked) if (!b || !validVersion(b.pluginVersion) || typeof b.reason !== "string" || !b.reason || b.reason.length > 240 || b.dsh && !strings(b.dsh) || b.platforms && !strings(b.platforms, 3)) throw new Error("Invalid blocked entry");
|
|
48
57
|
if (!c.retiredDsh.every(validVersion)) throw new Error("Invalid retired DSH");
|
|
@@ -50,7 +59,7 @@ function validateCatalog(value) {
|
|
|
50
59
|
return c;
|
|
51
60
|
}
|
|
52
61
|
function releaseMatches(r, current) {
|
|
53
|
-
return r.dsh.includes(current.agentVersion) && r.platforms.includes(current.platform) && (
|
|
62
|
+
return r.dsh.includes(current.agentVersion) && r.platforms.includes(current.platform) && Boolean(current.arch && r.architectures.includes(current.arch)) && (!r.targets || r.targets.some((t) => t.platform === current.platform && (!current.arch || t.arch === current.arch) && t.dsh.includes(current.agentVersion)));
|
|
54
63
|
}
|
|
55
64
|
function assessUpdate(raw, current, now = Date.now(), preview = false) {
|
|
56
65
|
const base = {
|
|
@@ -79,23 +88,21 @@ function assessUpdate(raw, current, now = Date.now(), preview = false) {
|
|
|
79
88
|
const blocked = (version) => c.blocked.find((b) => b.pluginVersion === version && (!b.dsh || b.dsh.includes(current.agentVersion)) && (!b.platforms || b.platforms.includes(current.platform)));
|
|
80
89
|
const own = c.releases.find((r) => r.version === current.pluginVersion);
|
|
81
90
|
const compatible = Boolean(current.arch && own && releaseMatches(own, current) && !blocked(own.version));
|
|
82
|
-
|
|
91
|
+
if (c.retiredDsh.includes(current.agentVersion)) return {
|
|
92
|
+
...base,
|
|
93
|
+
severity: "required",
|
|
94
|
+
component: "agent",
|
|
95
|
+
code: "agent-retired",
|
|
96
|
+
label: "\u9700\u8981\u66F4\u65B0 DSH",
|
|
97
|
+
message: "\u6B64 DSH \u7248\u672C\u5DF2\u505C\u6B62\u652F\u6301\u3002\u8BF7\u5728\u4E3B\u673A\u4E0A\u6309\u53D1\u5E03\u8BF4\u660E\u5347\u7EA7 DSH\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u4FEE\u6539 DSH\u3002",
|
|
98
|
+
releaseUrl: `${RELEASE_REPOSITORY}/releases`
|
|
99
|
+
};
|
|
100
|
+
const target = c.releases.filter((r) => (r.channel === "stable" || preview) && !blocked(r.version) && compareVersions(r.version, current.pluginVersion) > 0).sort((a, b) => compareVersions(b.version, a.version))[0];
|
|
83
101
|
const issue = blocked(current.pluginVersion);
|
|
84
102
|
if (target) {
|
|
85
103
|
const manual = c.manualUpgradePlugins?.includes(current.pluginVersion) ? {
|
|
86
104
|
manualUpdate: `\u6B64\u65E7\u63D2\u4EF6\u6CA1\u6709\u68C0\u67E5\u66F4\u65B0\u548C\u4E00\u952E\u66F4\u65B0\u529F\u80FD\u3002\u9996\u6B21\u9700\u5728\u8282\u70B9\u6240\u5728\u7535\u8111\uFF0C\u6309\u76EE\u6807\u7248\u672C ${target.version} \u7684\u53D1\u5E03\u8BF4\u660E\u624B\u5DE5\u5347\u7EA7\u63D2\u4EF6\uFF0C\u5E76\u4F7F\u7528\u539F\u6765\u7684 DSH_HOME \u548C profile \u91CD\u542F\u3002\u4E0D\u8981\u5220\u9664\u8282\u70B9\u6216\u91CD\u65B0\u914D\u5BF9\uFF1B\u5347\u7EA7\u540E\u624D\u53EF\u4F7F\u7528\u4E00\u952E\u66F4\u65B0\u3002`
|
|
87
105
|
} : {};
|
|
88
|
-
if (!current.arch) return {
|
|
89
|
-
...base,
|
|
90
|
-
...manual,
|
|
91
|
-
severity: "recommended",
|
|
92
|
-
component: "plugin",
|
|
93
|
-
code: "plugin-check-host",
|
|
94
|
-
label: "\u63D2\u4EF6\u53EF\u66F4\u65B0",
|
|
95
|
-
message: `\u63D2\u4EF6 ${target.version} \u7684\u7248\u672C\u6761\u4EF6\u4E0E\u5F53\u524D DSH \u5339\u914D\uFF0C\u4F46\u65E7\u8282\u70B9\u672A\u4E0A\u62A5\u4E3B\u673A\u67B6\u6784\u3002\u8BF7\u5230\u6B64\u4E3B\u673A WebUI \u68C0\u67E5\u786E\u8BA4\u540E\u66F4\u65B0\u63D2\u4EF6\uFF1B\u65E0\u9700\u5148\u66F4\u65B0 DSH\u3002`,
|
|
96
|
-
targetVersion: target.version,
|
|
97
|
-
releaseUrl: `${RELEASE_REPOSITORY}/releases/tag/v${target.version}`
|
|
98
|
-
};
|
|
99
106
|
const required = Boolean(issue);
|
|
100
107
|
return {
|
|
101
108
|
...base,
|
|
@@ -104,7 +111,7 @@ function assessUpdate(raw, current, now = Date.now(), preview = false) {
|
|
|
104
111
|
component: "plugin",
|
|
105
112
|
code: required ? "plugin-required" : "plugin-available",
|
|
106
113
|
label: required ? "\u5FC5\u987B\u66F4\u65B0\u63D2\u4EF6" : "\u63D2\u4EF6\u53EF\u66F4\u65B0",
|
|
107
|
-
message: (issue
|
|
114
|
+
message: (issue ? issue.reason + "\u3002" : "") + `\u53EF\u66F4\u65B0\u81F3\u63D2\u4EF6 ${target.version}\uFF1B\u66F4\u65B0\u524D\u4F1A\u68C0\u67E5\u5F53\u524D\u4E3B\u673A\u7684\u5B89\u88C5\u4E0E\u91CD\u542F\u6761\u4EF6\uFF0C\u4E0D\u4F1A\u66F4\u6362 DSH \u672C\u4F53\u3002`,
|
|
108
115
|
targetVersion: target.version,
|
|
109
116
|
releaseUrl: `${RELEASE_REPOSITORY}/releases/tag/v${target.version}`
|
|
110
117
|
};
|
|
@@ -119,20 +126,12 @@ function assessUpdate(raw, current, now = Date.now(), preview = false) {
|
|
|
119
126
|
releaseUrl: `${RELEASE_REPOSITORY}/releases`
|
|
120
127
|
};
|
|
121
128
|
if (compatible) return { ...base, severity: "none", code: "compatible", label: "\u6682\u65E0\u53EF\u7528\u66F4\u65B0", message: "\u5F53\u524D\u7EC4\u5408\u5DF2\u9A8C\u8BC1\uFF0C\u6682\u672A\u53D1\u73B0\u9002\u7528\u4E8E\u6B64 DSH \u548C\u4E3B\u673A\u7684\u66F4\u65B0\u3002" };
|
|
122
|
-
if (c.retiredDsh.includes(current.agentVersion)) return {
|
|
123
|
-
...base,
|
|
124
|
-
severity: "required",
|
|
125
|
-
component: "agent",
|
|
126
|
-
code: "agent-retired",
|
|
127
|
-
label: "\u9700\u8981\u66F4\u65B0 DSH",
|
|
128
|
-
message: "\u6B64 DSH \u7248\u672C\u5DF2\u505C\u6B62\u652F\u6301\u3002\u8BF7\u5728\u4E3B\u673A\u4E0A\u6309\u517C\u5BB9\u53D1\u5E03\u8BF4\u660E\u5347\u7EA7 DSH \u548C\u8FDE\u63A5\u63D2\u4EF6\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u4FEE\u6539 DSH\u3002",
|
|
129
|
-
releaseUrl: `${RELEASE_REPOSITORY}/releases`
|
|
130
|
-
};
|
|
131
129
|
return {
|
|
132
130
|
...base,
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
131
|
+
severity: "none",
|
|
132
|
+
code: "no-update",
|
|
133
|
+
label: "\u6682\u65E0\u53EF\u7528\u66F4\u65B0",
|
|
134
|
+
message: "\u5F53\u524D\u901A\u9053\u6682\u65E0\u66F4\u9AD8\u7248\u672C\uFF0C\u4E0D\u5F71\u54CD\u7EE7\u7EED\u4F7F\u7528\u3002",
|
|
136
135
|
releaseUrl: `${RELEASE_REPOSITORY}/releases`
|
|
137
136
|
};
|
|
138
137
|
}
|
package/lib/update-worker.js
CHANGED
|
@@ -416,7 +416,7 @@ async function stopOriginal(job) {
|
|
|
416
416
|
}
|
|
417
417
|
throw new Error("\u539F DSH \u5C1A\u672A\u7ED3\u675F\uFF0C\u672A\u66FF\u6362\u63D2\u4EF6");
|
|
418
418
|
}
|
|
419
|
-
async function stopRestarted(child, timeoutMs = 3e4) {
|
|
419
|
+
async function stopRestarted(child, timeoutMs = 3e4, forceTimeoutMs = 5e3) {
|
|
420
420
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
421
421
|
child.kill("SIGTERM");
|
|
422
422
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -425,8 +425,54 @@ async function stopRestarted(child, timeoutMs = 3e4) {
|
|
|
425
425
|
await wait(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
426
426
|
}
|
|
427
427
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
428
|
+
child.kill("SIGKILL");
|
|
429
|
+
const forcedDeadline = Date.now() + forceTimeoutMs;
|
|
430
|
+
while (Date.now() < forcedDeadline) {
|
|
431
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
432
|
+
await wait(Math.min(100, Math.max(1, forcedDeadline - Date.now())));
|
|
433
|
+
}
|
|
434
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
428
435
|
throw new Error("\u66F4\u65B0\u540E\u7684 DSH \u672A\u6309\u65F6\u505C\u6B62");
|
|
429
436
|
}
|
|
437
|
+
function captureCandidateLock(home, child) {
|
|
438
|
+
if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
|
|
439
|
+
const filename = path3.join(home, ".credentials.yaml.lock");
|
|
440
|
+
try {
|
|
441
|
+
const info = fs3.lstatSync(filename);
|
|
442
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > 32) return;
|
|
443
|
+
const value = fs3.readFileSync(filename, "utf8");
|
|
444
|
+
let owned = value === `${child.pid}
|
|
445
|
+
`;
|
|
446
|
+
if (!owned && value === "" && process.platform === "linux") {
|
|
447
|
+
const directory = `/proc/${child.pid}/fd`;
|
|
448
|
+
owned = fs3.readdirSync(directory).some((fd) => {
|
|
449
|
+
try {
|
|
450
|
+
const file = path3.join(directory, fd);
|
|
451
|
+
if (fs3.readlinkSync(file) !== filename) return false;
|
|
452
|
+
const opened = fs3.statSync(file);
|
|
453
|
+
return opened.dev === info.dev && opened.ino === info.ino;
|
|
454
|
+
} catch {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (owned) return { filename, dev: info.dev, ino: info.ino, pid: child.pid };
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function retireCandidateLock(lock, child, directory) {
|
|
464
|
+
if (!lock || child.pid !== lock.pid || child.exitCode === null && child.signalCode === null) return;
|
|
465
|
+
try {
|
|
466
|
+
const info = fs3.lstatSync(lock.filename);
|
|
467
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.dev !== lock.dev || info.ino !== lock.ino || info.size > 32) return;
|
|
468
|
+
const value = fs3.readFileSync(lock.filename, "utf8");
|
|
469
|
+
if (value !== "" && value !== `${lock.pid}
|
|
470
|
+
`) return;
|
|
471
|
+
const saved = path3.join(directory, "candidate-credentials-lock.before-rollback");
|
|
472
|
+
if (!fs3.existsSync(saved)) fs3.renameSync(lock.filename, saved);
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
}
|
|
430
476
|
async function stopChild(pid) {
|
|
431
477
|
process.kill(pid, "SIGTERM");
|
|
432
478
|
for (let i = 0; i < 100; i++) {
|
|
@@ -524,8 +570,11 @@ async function executeUpdate(job, progress, quiesce) {
|
|
|
524
570
|
await stopOriginal(job);
|
|
525
571
|
stopped = true;
|
|
526
572
|
}
|
|
527
|
-
if (newChild)
|
|
528
|
-
|
|
573
|
+
if (newChild) {
|
|
574
|
+
const ownedLock = captureCandidateLock(job.home, newChild);
|
|
575
|
+
await stopRestarted(newChild);
|
|
576
|
+
retireCandidateLock(ownedLock, newChild, job.directory);
|
|
577
|
+
} else if (swapped && job.manager && job.manager.kind !== "process") stopManagedHost(job.manager);
|
|
529
578
|
if (swapped) {
|
|
530
579
|
fs3.renameSync(job.profile, path3.join(job.directory, "profile-failed"));
|
|
531
580
|
fs3.renameSync(previous, job.profile);
|
|
@@ -679,11 +728,13 @@ if (process.argv[1] && path3.resolve(process.argv[1]) === fileURLToPath(import.m
|
|
|
679
728
|
});
|
|
680
729
|
}
|
|
681
730
|
export {
|
|
731
|
+
captureCandidateLock,
|
|
682
732
|
control,
|
|
683
733
|
executeUpdate,
|
|
684
734
|
healthy,
|
|
685
735
|
migrateLegacyGrantOwner,
|
|
686
736
|
releaseOwnedUpdateLock,
|
|
737
|
+
retireCandidateLock,
|
|
687
738
|
stopRestarted,
|
|
688
739
|
validateJob
|
|
689
740
|
};
|