dsh-wechat-remote 1.7.1 → 1.7.2
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/LICENSE +21 -21
- package/README.md +16 -14
- package/assets/plugin.tgz +0 -0
- package/assets/release.json +10 -9
- package/bin/native-control.mjs +70 -70
- package/bin/setup.mjs +165 -165
- package/lib/install-control.js +65 -5
- package/package.json +15 -15
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 martinbear1
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 martinbear1
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
# Agent远程管理助手 · DSH 插件安装器
|
|
1
|
+
# Agent远程管理助手 · DSH 插件安装器
|
|
2
|
+
|
|
3
|
+
首次安装和升级旧插件使用同一个命令:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx -y dsh-wechat-remote@latest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
需已安装 DSH,使用 Node.js 22.13 或更高版本。安装工具由包依赖自动提供,无需另装 pnpm 或 Git。
|
|
10
|
+
|
|
11
|
+
安装器仅选择适配当前 DSH 和主机的正式插件,保留原配置、配对和会话,不会升级 DSH 本体。请先结束运行中的任务;安装完成后自动重启原 DSH 并尝试打开 WebUI。
|
|
12
|
+
|
|
13
|
+
支持已验证的 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 服务可自动恢复;无法确认启动方式时不自动停止主机。
|
|
2
14
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
npx -y dsh-wechat-remote@latest
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
需已安装 DSH,使用 Node.js 22.13 或更高版本。安装工具由包依赖自动提供,无需另装 pnpm 或 Git。
|
|
10
|
-
|
|
11
|
-
安装器仅选择适配当前 DSH 和主机的正式插件,保留原配置、配对和会话,不会升级 DSH 本体。请先结束运行中的任务;安装完成后自动重启原 DSH 并尝试打开 WebUI。
|
|
12
|
-
|
|
13
|
-
支持已验证的 DSH `0.1.1-rc.2`、`0.1.2-rc.1`,Windows / macOS Intel / Linux x64。普通 Node 启动、独立的 macOS launchd 和 Linux 用户级 systemd 服务可自动恢复;无法确认启动方式时不自动停止主机。
|
|
14
|
-
|
|
15
|
-
使用其他配置时附加 `--profile 配置名称`。安装损坏需要重装时附加 `--repair`;不会自动降级。
|
|
15
|
+
旧插件 1.7.1 在 DSH 0.1.5 上无法一键更新时,可用本安装命令过渡一次。从插件 1.7.2 起,后续已验证的兼容更新不再受旧的 DSH 版本列表限制;不需要因此降低 DSH 版本或删除节点。
|
|
16
|
+
|
|
17
|
+
使用其他配置时附加 `--profile 配置名称`。安装损坏需要重装时附加 `--repair`;不会自动降级。
|
package/assets/plugin.tgz
CHANGED
|
Binary file
|
package/assets/release.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.7.
|
|
2
|
+
"version": "1.7.2",
|
|
3
3
|
"catalog": {
|
|
4
4
|
"schemaVersion": 1,
|
|
5
|
-
"revision": "installer-1.7.
|
|
6
|
-
"issuedAt":
|
|
7
|
-
"expiresAt":
|
|
5
|
+
"revision": "installer-1.7.2",
|
|
6
|
+
"issuedAt": 1789134534877,
|
|
7
|
+
"expiresAt": 1791553734877,
|
|
8
8
|
"releases": [
|
|
9
9
|
{
|
|
10
|
-
"version": "1.7.
|
|
10
|
+
"version": "1.7.2",
|
|
11
11
|
"channel": "stable",
|
|
12
12
|
"dsh": [
|
|
13
13
|
"0.1.1-rc.2",
|
|
14
|
-
"0.1.2-rc.1"
|
|
14
|
+
"0.1.2-rc.1",
|
|
15
|
+
"0.1.5-rc.1"
|
|
15
16
|
],
|
|
16
17
|
"platforms": [
|
|
17
18
|
"windows",
|
|
@@ -22,9 +23,9 @@
|
|
|
22
23
|
"x64"
|
|
23
24
|
],
|
|
24
25
|
"asset": {
|
|
25
|
-
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.
|
|
26
|
-
"sha256": "
|
|
27
|
-
"bytes":
|
|
26
|
+
"url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.2/harness-remote-dsh-wechat-remote-1.7.2.tgz",
|
|
27
|
+
"sha256": "0eb296a3a0c84f080148280f9b9419516a9dc640c9e13aa8cf014120aea591ee",
|
|
28
|
+
"bytes": 9539789
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
],
|
package/bin/native-control.mjs
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
import fs from 'node:fs'
|
|
2
|
-
import path from 'node:path'
|
|
3
|
-
import { parseDocument, isSeq, isMap } from 'yaml'
|
|
4
|
-
import { createHash, randomBytes } from 'node:crypto'
|
|
5
|
-
import { pathToFileURL } from 'node:url'
|
|
6
|
-
|
|
7
|
-
const digest = value => createHash('sha256').update(value).digest('hex')
|
|
8
|
-
function writePatch(filename, text) {
|
|
9
|
-
const temporary = filename + '.wechat-' + randomBytes(8).toString('hex')
|
|
10
|
-
const mode = fs.existsSync(filename) ? fs.statSync(filename).mode : 0o600
|
|
11
|
-
try {
|
|
12
|
-
fs.writeFileSync(temporary, text, { mode, flag: 'wx' })
|
|
13
|
-
fs.renameSync(temporary, filename)
|
|
14
|
-
} finally {
|
|
15
|
-
try { fs.unlinkSync(temporary) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function document(text) {
|
|
19
|
-
const doc = parseDocument(text, { customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: value => value }] })
|
|
20
|
-
if (doc.errors.length || !isSeq(doc.contents)) throw new Error('DSH 配置不是有效的原生插件列表,未修改配置。')
|
|
21
|
-
return doc
|
|
22
|
-
}
|
|
23
|
-
/** Native user-patch insertion; remove only our entry and retain concurrent edits. */
|
|
24
|
-
export function attachControl(profile, helper, config) {
|
|
25
|
-
const filename = path.join(profile, 'cordis.patch.yml')
|
|
26
|
-
const original = fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : '[]\n'
|
|
27
|
-
const doc = document(original), id = 'wechat-installer-' + path.basename(config.directory)
|
|
28
|
-
doc.contents.flow = false
|
|
29
|
-
doc.contents.add(doc.createNode({ insert: [{ id, name: pathToFileURL(helper).href, config }] }))
|
|
30
|
-
const patched = String(doc)
|
|
31
|
-
fs.writeFileSync(path.join(config.directory, 'user-patch-before.yml'), original, { mode: 0o600, flag: 'wx' })
|
|
32
|
-
writePatch(filename, patched)
|
|
33
|
-
return () => {
|
|
34
|
-
const current = fs.readFileSync(filename, 'utf8')
|
|
35
|
-
if (digest(current) === digest(patched)) { writePatch(filename, original); return }
|
|
36
|
-
const latest = document(current)
|
|
37
|
-
latest.contents.items = latest.contents.items.filter(item => {
|
|
38
|
-
if (!isMap(item)) return true
|
|
39
|
-
const entries = item.get('insert', true)
|
|
40
|
-
if (!isSeq(entries)) return true
|
|
41
|
-
entries.items = entries.items.filter(entry => !isMap(entry) || entry.get('id') !== id)
|
|
42
|
-
return entries.items.length > 0 || item.items.length !== 1
|
|
43
|
-
})
|
|
44
|
-
writePatch(filename, String(latest))
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export async function waitForJson(filename, accept, timeout = 20000) {
|
|
49
|
-
const until = Date.now() + timeout
|
|
50
|
-
while (Date.now() < until) {
|
|
51
|
-
try { const value = JSON.parse(fs.readFileSync(filename, 'utf8')); if (accept(value)) return value } catch {}
|
|
52
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
53
|
-
}
|
|
54
|
-
throw new Error('DSH 没有完成安装握手;原插件未替换。请确认 DSH WebUI 正在运行且允许原生插件热加载。')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** One bounded native handshake, including a possible offline host start.
|
|
58
|
-
* A listening WebUI is not proof that the asynchronous profile reload finished.
|
|
59
|
-
* Keep the same control entry/token while waiting; never restart a live host. */
|
|
60
|
-
export async function waitForInstallControl(filename, {
|
|
61
|
-
accept, ensureRunning, onWaiting = () => {}, timeoutMs = 60000, initialWaitMs = 5000,
|
|
62
|
-
}) {
|
|
63
|
-
const deadline = Date.now() + timeoutMs
|
|
64
|
-
try { return await waitForJson(filename, accept, Math.min(initialWaitMs, timeoutMs)) }
|
|
65
|
-
catch {
|
|
66
|
-
onWaiting()
|
|
67
|
-
if (Date.now() < deadline) await ensureRunning()
|
|
68
|
-
return await waitForJson(filename, accept, Math.max(0, deadline - Date.now()))
|
|
69
|
-
}
|
|
70
|
-
}
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { parseDocument, isSeq, isMap } from 'yaml'
|
|
4
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const digest = value => createHash('sha256').update(value).digest('hex')
|
|
8
|
+
function writePatch(filename, text) {
|
|
9
|
+
const temporary = filename + '.wechat-' + randomBytes(8).toString('hex')
|
|
10
|
+
const mode = fs.existsSync(filename) ? fs.statSync(filename).mode : 0o600
|
|
11
|
+
try {
|
|
12
|
+
fs.writeFileSync(temporary, text, { mode, flag: 'wx' })
|
|
13
|
+
fs.renameSync(temporary, filename)
|
|
14
|
+
} finally {
|
|
15
|
+
try { fs.unlinkSync(temporary) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function document(text) {
|
|
19
|
+
const doc = parseDocument(text, { customTags: [{ tag: 'tag:yaml.org,2002:js', resolve: value => value }] })
|
|
20
|
+
if (doc.errors.length || !isSeq(doc.contents)) throw new Error('DSH 配置不是有效的原生插件列表,未修改配置。')
|
|
21
|
+
return doc
|
|
22
|
+
}
|
|
23
|
+
/** Native user-patch insertion; remove only our entry and retain concurrent edits. */
|
|
24
|
+
export function attachControl(profile, helper, config) {
|
|
25
|
+
const filename = path.join(profile, 'cordis.patch.yml')
|
|
26
|
+
const original = fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : '[]\n'
|
|
27
|
+
const doc = document(original), id = 'wechat-installer-' + path.basename(config.directory)
|
|
28
|
+
doc.contents.flow = false
|
|
29
|
+
doc.contents.add(doc.createNode({ insert: [{ id, name: pathToFileURL(helper).href, config }] }))
|
|
30
|
+
const patched = String(doc)
|
|
31
|
+
fs.writeFileSync(path.join(config.directory, 'user-patch-before.yml'), original, { mode: 0o600, flag: 'wx' })
|
|
32
|
+
writePatch(filename, patched)
|
|
33
|
+
return () => {
|
|
34
|
+
const current = fs.readFileSync(filename, 'utf8')
|
|
35
|
+
if (digest(current) === digest(patched)) { writePatch(filename, original); return }
|
|
36
|
+
const latest = document(current)
|
|
37
|
+
latest.contents.items = latest.contents.items.filter(item => {
|
|
38
|
+
if (!isMap(item)) return true
|
|
39
|
+
const entries = item.get('insert', true)
|
|
40
|
+
if (!isSeq(entries)) return true
|
|
41
|
+
entries.items = entries.items.filter(entry => !isMap(entry) || entry.get('id') !== id)
|
|
42
|
+
return entries.items.length > 0 || item.items.length !== 1
|
|
43
|
+
})
|
|
44
|
+
writePatch(filename, String(latest))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function waitForJson(filename, accept, timeout = 20000) {
|
|
49
|
+
const until = Date.now() + timeout
|
|
50
|
+
while (Date.now() < until) {
|
|
51
|
+
try { const value = JSON.parse(fs.readFileSync(filename, 'utf8')); if (accept(value)) return value } catch {}
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
53
|
+
}
|
|
54
|
+
throw new Error('DSH 没有完成安装握手;原插件未替换。请确认 DSH WebUI 正在运行且允许原生插件热加载。')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One bounded native handshake, including a possible offline host start.
|
|
58
|
+
* A listening WebUI is not proof that the asynchronous profile reload finished.
|
|
59
|
+
* Keep the same control entry/token while waiting; never restart a live host. */
|
|
60
|
+
export async function waitForInstallControl(filename, {
|
|
61
|
+
accept, ensureRunning, onWaiting = () => {}, timeoutMs = 60000, initialWaitMs = 5000,
|
|
62
|
+
}) {
|
|
63
|
+
const deadline = Date.now() + timeoutMs
|
|
64
|
+
try { return await waitForJson(filename, accept, Math.min(initialWaitMs, timeoutMs)) }
|
|
65
|
+
catch {
|
|
66
|
+
onWaiting()
|
|
67
|
+
if (Date.now() < deadline) await ensureRunning()
|
|
68
|
+
return await waitForJson(filename, accept, Math.max(0, deadline - Date.now()))
|
|
69
|
+
}
|
|
70
|
+
}
|
package/bin/setup.mjs
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import net from 'node:net'
|
|
6
|
-
import { spawn } from 'node:child_process'
|
|
7
|
-
import { randomBytes, createHash } from 'node:crypto'
|
|
8
|
-
import { fileURLToPath } from 'node:url'
|
|
9
|
-
import { attachControl, waitForJson, waitForInstallControl } from './native-control.mjs'
|
|
10
|
-
import { resolveInstallRuntime, verifyInstallRuntime } from '../lib/install-runtime.js'
|
|
11
|
-
import { validateCatalog, releaseMatches, compareVersions } from '../lib/update-policy.js'
|
|
12
|
-
import { boundedFetch, downloadRelease, auditArchive } from '../lib/update-download.js'
|
|
13
|
-
import { writePrivateJsonAtomic } from '../lib/secure-file.js'
|
|
14
|
-
import { control, validateJob, releaseOwnedUpdateLock } from '../lib/update-worker.js'
|
|
15
|
-
|
|
16
|
-
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
17
|
-
const packageName = '@harness-remote/dsh-wechat-remote'
|
|
18
|
-
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
19
|
-
export function findDsh() {
|
|
20
|
-
const candidates = []
|
|
21
|
-
for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
22
|
-
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'))
|
|
23
|
-
try { candidates.push(fs.realpathSync(path.join(dir, 'dsh'))) } catch {}
|
|
24
|
-
}
|
|
25
|
-
for (const candidate of candidates) {
|
|
26
|
-
try {
|
|
27
|
-
const cli = fs.realpathSync(candidate)
|
|
28
|
-
const manifest = JSON.parse(fs.readFileSync(path.resolve(cli, '../../package.json'), 'utf8'))
|
|
29
|
-
if (manifest.name === '@deepseek-ai/dsh' && path.basename(cli) === 'bin.js') return cli
|
|
30
|
-
} catch {}
|
|
31
|
-
}
|
|
32
|
-
throw new Error('未找到已安装的 DSH。请先安装并启动 DeepSeek Harness。')
|
|
33
|
-
}
|
|
34
|
-
function portBusy(port) {
|
|
35
|
-
return new Promise(resolve => {
|
|
36
|
-
const socket = net.connect({ host: '127.0.0.1', port })
|
|
37
|
-
const end = busy => { socket.destroy(); resolve(busy) }
|
|
38
|
-
socket.setTimeout(500, () => end(false)); socket.once('connect', () => end(true)); socket.once('error', () => end(false))
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
async function selectRelease(host, assetsRoot = path.join(root, 'assets'), repair = false) {
|
|
42
|
-
const current = { agentKind: 'dsh', agentVersion: host.dshVersion, pluginVersion: host.pluginVersion,
|
|
43
|
-
platform: { win32: 'windows', darwin: 'macos', linux: 'linux' }[host.platform], arch: host.arch }
|
|
44
|
-
const pinned = JSON.parse(fs.readFileSync(path.join(assetsRoot, 'release.json'), 'utf8'))
|
|
45
|
-
const catalog = validateCatalog(pinned.catalog)
|
|
46
|
-
let releases = catalog.releases
|
|
47
|
-
try {
|
|
48
|
-
const remote = validateCatalog(JSON.parse((await boundedFetch('https://relay.xyxfood.xyz/v1/update-policy', 256 * 1024)).toString('utf8')))
|
|
49
|
-
if (remote.issuedAt <= Date.now() + 300000 && remote.expiresAt > Date.now()) {
|
|
50
|
-
releases = [...releases, ...remote.releases].filter(r => !remote.blocked.some(b => b.pluginVersion === r.version && (!b.dsh || b.dsh.includes(current.agentVersion)) && (!b.platforms || b.platforms.includes(current.platform))))
|
|
51
|
-
}
|
|
52
|
-
} catch { /* Authenticated npm package retains its explicitly tested release. */ }
|
|
53
|
-
const release = releases.filter(r => r.channel === 'stable' && releaseMatches(r, current))
|
|
54
|
-
.sort((a, b) => compareVersions(b.version, a.version))[0]
|
|
55
|
-
if (!release) throw new Error('当前 DSH 版本或系统尚无已验证的插件版本;没有修改现有安装。')
|
|
56
|
-
if (host.pluginVersion !== '0.0.0' && (compareVersions(host.pluginVersion, release.version) > 0
|
|
57
|
-
|| (compareVersions(host.pluginVersion, release.version) === 0 && !repair))) return { release, archive: null }
|
|
58
|
-
let archive
|
|
59
|
-
if (release.version === pinned.version) archive = fs.readFileSync(path.join(assetsRoot, 'plugin.tgz'))
|
|
60
|
-
else archive = await downloadRelease(release)
|
|
61
|
-
auditArchive(archive, release)
|
|
62
|
-
return { release, archive }
|
|
63
|
-
}
|
|
64
|
-
export async function install({ profileName = 'web', cli = findDsh(), assetsRoot, open = true, repair = false } = {}) {
|
|
65
|
-
if (!/^[A-Za-z0-9_-]{1,80}$/.test(profileName)) throw new Error('无效的 profile 名称。')
|
|
66
|
-
const runtime = resolveInstallRuntime(root); await verifyInstallRuntime(runtime)
|
|
67
|
-
const home = path.resolve(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'))
|
|
68
|
-
fs.mkdirSync(home, { recursive: true, mode: 0o700 })
|
|
69
|
-
if (fs.realpathSync(home) !== home) throw new Error('DSH 数据目录是链接,暂不自动替换安装。')
|
|
70
|
-
const profile = path.join(home, 'profiles', profileName), id = randomBytes(16).toString('hex')
|
|
71
|
-
const directory = path.join(home, 'harness-remote-updates', id), token = randomBytes(24).toString('hex')
|
|
72
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
73
|
-
let remove, ref, job, locked = false, authorized = false, launched
|
|
74
|
-
const lockFile = path.join(profile, '.harness-remote-update.lock')
|
|
75
|
-
console.log('正在检查 DSH 与插件…')
|
|
76
|
-
try {
|
|
77
|
-
// Initialize missing profiles through DSH itself, never synthesize a bundle list.
|
|
78
|
-
if (!fs.existsSync(path.join(profile, 'package.json'))) {
|
|
79
|
-
const log = fs.openSync(path.join(directory, 'startup.log'), 'a', 0o600)
|
|
80
|
-
launched = spawn(process.execPath, [cli, '
|
|
81
|
-
cwd: process.cwd(), env: process.env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
82
|
-
fs.closeSync(log); launched.on('error', () => {}); launched.unref()
|
|
83
|
-
for (let i = 0; i < 100 && !fs.existsSync(path.join(profile, 'package.json')); i++) await sleep(100)
|
|
84
|
-
if (!fs.existsSync(path.join(profile, 'package.json'))) throw new Error('DSH 未能初始化 profile;详情见本机安装记录。')
|
|
85
|
-
await sleep(2000)
|
|
86
|
-
}
|
|
87
|
-
const fd = fs.openSync(lockFile, 'wx', 0o600); fs.writeFileSync(fd, id); fs.closeSync(fd); locked = true
|
|
88
|
-
const helper = path.join(directory, 'install-control.js')
|
|
89
|
-
fs.copyFileSync(path.join(root, 'lib/install-control.js'), helper)
|
|
90
|
-
writePrivateJsonAtomic(path.join(directory, 'package.json'), { type: 'module' })
|
|
91
|
-
remove = attachControl(profile, helper, { directory, token, pnpm: runtime.cli })
|
|
92
|
-
const handshakeStarted = Date.now()
|
|
93
|
-
try {
|
|
94
|
-
ref = await waitForInstallControl(path.join(directory, 'control-ready.json'), {
|
|
95
|
-
accept: v => Number.isInteger(v.pid) && v.pid > 0 && (!launched || v.pid === launched.pid),
|
|
96
|
-
onWaiting: () => console.log('正在等待 DSH 完成安装准备,请勿重复执行命令…'),
|
|
97
|
-
ensureRunning: async () => {
|
|
98
|
-
if (launched || await portBusy(Number(process.env.DSH_PORT || 3080))) return
|
|
99
|
-
const log = fs.openSync(path.join(directory, 'startup.log'), 'a', 0o600)
|
|
100
|
-
launched = spawn(process.execPath, [cli, '
|
|
101
|
-
cwd: process.cwd(), env: process.env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
102
|
-
fs.closeSync(log); launched.on('error', () => {}); launched.unref()
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
writePrivateJsonAtomic(path.join(directory, 'handshake.json'), {
|
|
106
|
-
state: 'ready', elapsedMs: Date.now() - handshakeStarted, startedHost: Boolean(launched),
|
|
107
|
-
})
|
|
108
|
-
} catch {
|
|
109
|
-
writePrivateJsonAtomic(path.join(directory, 'handshake.json'), {
|
|
110
|
-
state: 'timeout', elapsedMs: Date.now() - handshakeStarted, startedHost: Boolean(launched),
|
|
111
|
-
})
|
|
112
|
-
throw new Error('DSH 安装准备超时,原插件未替换。本机安装记录:' + path.join(directory, 'handshake.json'))
|
|
113
|
-
}
|
|
114
|
-
job = { controlOrigin: ref.origin, statusToken: token }
|
|
115
|
-
const host = await control(job, 'describe')
|
|
116
|
-
if (host.home !== home || host.profile !== profile || host.cli !== fs.realpathSync(cli) || (launched && host.pid !== launched.pid)) throw new Error('当前 DSH 身份与安装目标不一致,未修改安装。')
|
|
117
|
-
remove(); remove = undefined
|
|
118
|
-
await sleep(600)
|
|
119
|
-
const selected = await selectRelease(host, assetsRoot, repair)
|
|
120
|
-
if (!selected.archive) { console.log(`插件 ${host.pluginVersion} 无需更新。`); return { version: host.pluginVersion, changed: false } }
|
|
121
|
-
console.log(`正在安装插件 ${selected.release.version},保留原配对与会话…`)
|
|
122
|
-
job = { ...host, id, directory, parentPid: host.pid, pnpm: runtime.cli, targetVersion: selected.release.version,
|
|
123
|
-
previousVersion: host.pluginVersion, statusToken: token, controlOrigin: ref.origin }
|
|
124
|
-
validateJob(job)
|
|
125
|
-
fs.writeFileSync(path.join(directory, 'release.tgz'), selected.archive, { mode: 0o600, flag: 'wx' })
|
|
126
|
-
fs.copyFileSync(path.join(root, 'lib/update-worker.js'), path.join(directory, 'update-worker.js'))
|
|
127
|
-
writePrivateJsonAtomic(path.join(directory, 'job.json'), job)
|
|
128
|
-
await control(job, 'launch')
|
|
129
|
-
const ready = await waitForJson(path.join(directory, 'worker-ready.json'), v => v.id === id)
|
|
130
|
-
const scope = createHash('sha256').update(profileName).digest('hex').slice(0, 24)
|
|
131
|
-
writePrivateJsonAtomic(path.join(home, 'harness-remote-updates', `profile-${scope}.json`), { jobId: id, statusOrigin: ready.origin, statusToken: token })
|
|
132
|
-
writePrivateJsonAtomic(path.join(directory, 'authorized.json'), { id }); authorized = true
|
|
133
|
-
let previous = '', result
|
|
134
|
-
const until = Date.now() + 600000
|
|
135
|
-
while (Date.now() < until) {
|
|
136
|
-
try { result = JSON.parse(fs.readFileSync(path.join(directory, 'result.json'), 'utf8')) } catch {}
|
|
137
|
-
if (result?.phase !== previous && result?.phase) { console.log(`${result.progress}% ${result.message}`); previous = result.phase }
|
|
138
|
-
if (result?.terminal) break
|
|
139
|
-
await sleep(500)
|
|
140
|
-
}
|
|
141
|
-
if (!result?.terminal || !result.ok) throw new Error(result?.message || '安装结果待确认,请保留本机安装记录,不要重复安装。')
|
|
142
|
-
if (open) {
|
|
143
|
-
try {
|
|
144
|
-
const origin = `http://127.0.0.1:${host.webPort}`
|
|
145
|
-
const response = await fetch(`http://127.0.0.1:${host.localPort}/gate/update/resume?job=${id}`, { headers: { origin }, signal: AbortSignal.timeout(10000) })
|
|
146
|
-
const value = await response.json(), url = new URL(value.url)
|
|
147
|
-
if (!response.ok || url.origin !== origin || url.pathname !== '/' || url.hash || url.username || url.password) throw new Error('invalid resume')
|
|
148
|
-
const command = process.platform === 'win32' ? ['rundll32.exe', ['url.dll,FileProtocolHandler', url.href]] : process.platform === 'darwin' ? ['/usr/bin/open', [url.href]] : ['xdg-open', [url.href]]
|
|
149
|
-
const browser = spawn(command[0], command[1], { detached: true, windowsHide: true, stdio: 'ignore' }); browser.on('error', () => {}); browser.unref()
|
|
150
|
-
} catch { console.log('插件已更新;请返回原 DSH WebUI 查看。') }
|
|
151
|
-
}
|
|
152
|
-
return { version: selected.release.version, changed: true }
|
|
153
|
-
} finally {
|
|
154
|
-
remove?.()
|
|
155
|
-
if (ref && !authorized) { try { await control({ controlOrigin: ref.origin, statusToken: token }, 'close') } catch {} }
|
|
156
|
-
if (locked && !authorized) releaseOwnedUpdateLock(lockFile, id)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
160
|
-
const args = process.argv.slice(2), repair = args.includes('--repair')
|
|
161
|
-
if (repair) args.splice(args.indexOf('--repair'), 1)
|
|
162
|
-
if (args.includes('--help') || args.includes('-h')) console.log('安装或升级 DSH 微信连接插件:npx -y dsh-wechat-remote@latest\n可选:--profile <名称>(默认 web);--repair(重新安装当前兼容版本,不降级)')
|
|
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 })
|
|
165
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import net from 'node:net'
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
import { randomBytes, createHash } from 'node:crypto'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { attachControl, waitForJson, waitForInstallControl } from './native-control.mjs'
|
|
10
|
+
import { resolveInstallRuntime, verifyInstallRuntime } from '../lib/install-runtime.js'
|
|
11
|
+
import { validateCatalog, releaseMatches, compareVersions } from '../lib/update-policy.js'
|
|
12
|
+
import { boundedFetch, downloadRelease, auditArchive } from '../lib/update-download.js'
|
|
13
|
+
import { writePrivateJsonAtomic } from '../lib/secure-file.js'
|
|
14
|
+
import { control, validateJob, releaseOwnedUpdateLock } from '../lib/update-worker.js'
|
|
15
|
+
|
|
16
|
+
const root = fileURLToPath(new URL('../', import.meta.url))
|
|
17
|
+
const packageName = '@harness-remote/dsh-wechat-remote'
|
|
18
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
19
|
+
export function findDsh() {
|
|
20
|
+
const candidates = []
|
|
21
|
+
for (const dir of (process.env.PATH || '').split(path.delimiter).filter(Boolean)) {
|
|
22
|
+
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'))
|
|
23
|
+
try { candidates.push(fs.realpathSync(path.join(dir, 'dsh'))) } catch {}
|
|
24
|
+
}
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
try {
|
|
27
|
+
const cli = fs.realpathSync(candidate)
|
|
28
|
+
const manifest = JSON.parse(fs.readFileSync(path.resolve(cli, '../../package.json'), 'utf8'))
|
|
29
|
+
if (manifest.name === '@deepseek-ai/dsh' && path.basename(cli) === 'bin.js') return cli
|
|
30
|
+
} catch {}
|
|
31
|
+
}
|
|
32
|
+
throw new Error('未找到已安装的 DSH。请先安装并启动 DeepSeek Harness。')
|
|
33
|
+
}
|
|
34
|
+
function portBusy(port) {
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
const socket = net.connect({ host: '127.0.0.1', port })
|
|
37
|
+
const end = busy => { socket.destroy(); resolve(busy) }
|
|
38
|
+
socket.setTimeout(500, () => end(false)); socket.once('connect', () => end(true)); socket.once('error', () => end(false))
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
async function selectRelease(host, assetsRoot = path.join(root, 'assets'), repair = false) {
|
|
42
|
+
const current = { agentKind: 'dsh', agentVersion: host.dshVersion, pluginVersion: host.pluginVersion,
|
|
43
|
+
platform: { win32: 'windows', darwin: 'macos', linux: 'linux' }[host.platform], arch: host.arch }
|
|
44
|
+
const pinned = JSON.parse(fs.readFileSync(path.join(assetsRoot, 'release.json'), 'utf8'))
|
|
45
|
+
const catalog = validateCatalog(pinned.catalog)
|
|
46
|
+
let releases = catalog.releases
|
|
47
|
+
try {
|
|
48
|
+
const remote = validateCatalog(JSON.parse((await boundedFetch('https://relay.xyxfood.xyz/v1/update-policy', 256 * 1024)).toString('utf8')))
|
|
49
|
+
if (remote.issuedAt <= Date.now() + 300000 && remote.expiresAt > Date.now()) {
|
|
50
|
+
releases = [...releases, ...remote.releases].filter(r => !remote.blocked.some(b => b.pluginVersion === r.version && (!b.dsh || b.dsh.includes(current.agentVersion)) && (!b.platforms || b.platforms.includes(current.platform))))
|
|
51
|
+
}
|
|
52
|
+
} catch { /* Authenticated npm package retains its explicitly tested release. */ }
|
|
53
|
+
const release = releases.filter(r => r.channel === 'stable' && releaseMatches(r, current))
|
|
54
|
+
.sort((a, b) => compareVersions(b.version, a.version))[0]
|
|
55
|
+
if (!release) throw new Error('当前 DSH 版本或系统尚无已验证的插件版本;没有修改现有安装。')
|
|
56
|
+
if (host.pluginVersion !== '0.0.0' && (compareVersions(host.pluginVersion, release.version) > 0
|
|
57
|
+
|| (compareVersions(host.pluginVersion, release.version) === 0 && !repair))) return { release, archive: null }
|
|
58
|
+
let archive
|
|
59
|
+
if (release.version === pinned.version) archive = fs.readFileSync(path.join(assetsRoot, 'plugin.tgz'))
|
|
60
|
+
else archive = await downloadRelease(release)
|
|
61
|
+
auditArchive(archive, release)
|
|
62
|
+
return { release, archive }
|
|
63
|
+
}
|
|
64
|
+
export async function install({ profileName = 'web', cli = findDsh(), assetsRoot, open = true, repair = false } = {}) {
|
|
65
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(profileName)) throw new Error('无效的 profile 名称。')
|
|
66
|
+
const runtime = resolveInstallRuntime(root); await verifyInstallRuntime(runtime)
|
|
67
|
+
const home = path.resolve(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'))
|
|
68
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 })
|
|
69
|
+
if (fs.realpathSync(home) !== home) throw new Error('DSH 数据目录是链接,暂不自动替换安装。')
|
|
70
|
+
const profile = path.join(home, 'profiles', profileName), id = randomBytes(16).toString('hex')
|
|
71
|
+
const directory = path.join(home, 'harness-remote-updates', id), token = randomBytes(24).toString('hex')
|
|
72
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
73
|
+
let remove, ref, job, locked = false, authorized = false, launched
|
|
74
|
+
const lockFile = path.join(profile, '.harness-remote-update.lock')
|
|
75
|
+
console.log('正在检查 DSH 与插件…')
|
|
76
|
+
try {
|
|
77
|
+
// Initialize missing profiles through DSH itself, never synthesize a bundle list.
|
|
78
|
+
if (!fs.existsSync(path.join(profile, 'package.json'))) {
|
|
79
|
+
const log = fs.openSync(path.join(directory, 'startup.log'), 'a', 0o600)
|
|
80
|
+
launched = spawn(process.execPath, [cli, '--profile', profileName, '--no-open'], {
|
|
81
|
+
cwd: process.cwd(), env: process.env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
82
|
+
fs.closeSync(log); launched.on('error', () => {}); launched.unref()
|
|
83
|
+
for (let i = 0; i < 100 && !fs.existsSync(path.join(profile, 'package.json')); i++) await sleep(100)
|
|
84
|
+
if (!fs.existsSync(path.join(profile, 'package.json'))) throw new Error('DSH 未能初始化 profile;详情见本机安装记录。')
|
|
85
|
+
await sleep(2000)
|
|
86
|
+
}
|
|
87
|
+
const fd = fs.openSync(lockFile, 'wx', 0o600); fs.writeFileSync(fd, id); fs.closeSync(fd); locked = true
|
|
88
|
+
const helper = path.join(directory, 'install-control.js')
|
|
89
|
+
fs.copyFileSync(path.join(root, 'lib/install-control.js'), helper)
|
|
90
|
+
writePrivateJsonAtomic(path.join(directory, 'package.json'), { type: 'module' })
|
|
91
|
+
remove = attachControl(profile, helper, { directory, token, pnpm: runtime.cli })
|
|
92
|
+
const handshakeStarted = Date.now()
|
|
93
|
+
try {
|
|
94
|
+
ref = await waitForInstallControl(path.join(directory, 'control-ready.json'), {
|
|
95
|
+
accept: v => Number.isInteger(v.pid) && v.pid > 0 && (!launched || v.pid === launched.pid),
|
|
96
|
+
onWaiting: () => console.log('正在等待 DSH 完成安装准备,请勿重复执行命令…'),
|
|
97
|
+
ensureRunning: async () => {
|
|
98
|
+
if (launched || await portBusy(Number(process.env.DSH_PORT || 3080))) return
|
|
99
|
+
const log = fs.openSync(path.join(directory, 'startup.log'), 'a', 0o600)
|
|
100
|
+
launched = spawn(process.execPath, [cli, '--profile', profileName, '--no-open'], {
|
|
101
|
+
cwd: process.cwd(), env: process.env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
|
|
102
|
+
fs.closeSync(log); launched.on('error', () => {}); launched.unref()
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
writePrivateJsonAtomic(path.join(directory, 'handshake.json'), {
|
|
106
|
+
state: 'ready', elapsedMs: Date.now() - handshakeStarted, startedHost: Boolean(launched),
|
|
107
|
+
})
|
|
108
|
+
} catch {
|
|
109
|
+
writePrivateJsonAtomic(path.join(directory, 'handshake.json'), {
|
|
110
|
+
state: 'timeout', elapsedMs: Date.now() - handshakeStarted, startedHost: Boolean(launched),
|
|
111
|
+
})
|
|
112
|
+
throw new Error('DSH 安装准备超时,原插件未替换。本机安装记录:' + path.join(directory, 'handshake.json'))
|
|
113
|
+
}
|
|
114
|
+
job = { controlOrigin: ref.origin, statusToken: token }
|
|
115
|
+
const host = await control(job, 'describe')
|
|
116
|
+
if (host.home !== home || host.profile !== profile || host.cli !== fs.realpathSync(cli) || (launched && host.pid !== launched.pid)) throw new Error('当前 DSH 身份与安装目标不一致,未修改安装。')
|
|
117
|
+
remove(); remove = undefined
|
|
118
|
+
await sleep(600)
|
|
119
|
+
const selected = await selectRelease(host, assetsRoot, repair)
|
|
120
|
+
if (!selected.archive) { console.log(`插件 ${host.pluginVersion} 无需更新。`); return { version: host.pluginVersion, changed: false } }
|
|
121
|
+
console.log(`正在安装插件 ${selected.release.version},保留原配对与会话…`)
|
|
122
|
+
job = { ...host, id, directory, parentPid: host.pid, pnpm: runtime.cli, targetVersion: selected.release.version,
|
|
123
|
+
previousVersion: host.pluginVersion, statusToken: token, controlOrigin: ref.origin }
|
|
124
|
+
validateJob(job)
|
|
125
|
+
fs.writeFileSync(path.join(directory, 'release.tgz'), selected.archive, { mode: 0o600, flag: 'wx' })
|
|
126
|
+
fs.copyFileSync(path.join(root, 'lib/update-worker.js'), path.join(directory, 'update-worker.js'))
|
|
127
|
+
writePrivateJsonAtomic(path.join(directory, 'job.json'), job)
|
|
128
|
+
await control(job, 'launch')
|
|
129
|
+
const ready = await waitForJson(path.join(directory, 'worker-ready.json'), v => v.id === id)
|
|
130
|
+
const scope = createHash('sha256').update(profileName).digest('hex').slice(0, 24)
|
|
131
|
+
writePrivateJsonAtomic(path.join(home, 'harness-remote-updates', `profile-${scope}.json`), { jobId: id, statusOrigin: ready.origin, statusToken: token })
|
|
132
|
+
writePrivateJsonAtomic(path.join(directory, 'authorized.json'), { id }); authorized = true
|
|
133
|
+
let previous = '', result
|
|
134
|
+
const until = Date.now() + 600000
|
|
135
|
+
while (Date.now() < until) {
|
|
136
|
+
try { result = JSON.parse(fs.readFileSync(path.join(directory, 'result.json'), 'utf8')) } catch {}
|
|
137
|
+
if (result?.phase !== previous && result?.phase) { console.log(`${result.progress}% ${result.message}`); previous = result.phase }
|
|
138
|
+
if (result?.terminal) break
|
|
139
|
+
await sleep(500)
|
|
140
|
+
}
|
|
141
|
+
if (!result?.terminal || !result.ok) throw new Error(result?.message || '安装结果待确认,请保留本机安装记录,不要重复安装。')
|
|
142
|
+
if (open) {
|
|
143
|
+
try {
|
|
144
|
+
const origin = `http://127.0.0.1:${host.webPort}`
|
|
145
|
+
const response = await fetch(`http://127.0.0.1:${host.localPort}/gate/update/resume?job=${id}`, { headers: { origin }, signal: AbortSignal.timeout(10000) })
|
|
146
|
+
const value = await response.json(), url = new URL(value.url)
|
|
147
|
+
if (!response.ok || url.origin !== origin || url.pathname !== '/' || url.hash || url.username || url.password) throw new Error('invalid resume')
|
|
148
|
+
const command = process.platform === 'win32' ? ['rundll32.exe', ['url.dll,FileProtocolHandler', url.href]] : process.platform === 'darwin' ? ['/usr/bin/open', [url.href]] : ['xdg-open', [url.href]]
|
|
149
|
+
const browser = spawn(command[0], command[1], { detached: true, windowsHide: true, stdio: 'ignore' }); browser.on('error', () => {}); browser.unref()
|
|
150
|
+
} catch { console.log('插件已更新;请返回原 DSH WebUI 查看。') }
|
|
151
|
+
}
|
|
152
|
+
return { version: selected.release.version, changed: true }
|
|
153
|
+
} finally {
|
|
154
|
+
remove?.()
|
|
155
|
+
if (ref && !authorized) { try { await control({ controlOrigin: ref.origin, statusToken: token }, 'close') } catch {} }
|
|
156
|
+
if (locked && !authorized) releaseOwnedUpdateLock(lockFile, id)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
160
|
+
const args = process.argv.slice(2), repair = args.includes('--repair')
|
|
161
|
+
if (repair) args.splice(args.indexOf('--repair'), 1)
|
|
162
|
+
if (args.includes('--help') || args.includes('-h')) console.log('安装或升级 DSH 微信连接插件:npx -y dsh-wechat-remote@latest\n可选:--profile <名称>(默认 web);--repair(重新安装当前兼容版本,不降级)')
|
|
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 })
|
|
165
|
+
}
|
package/lib/install-control.js
CHANGED
|
@@ -573,7 +573,7 @@ function presentationProjection(key, raw) {
|
|
|
573
573
|
switch (key) {
|
|
574
574
|
case "sessionStats":
|
|
575
575
|
name = "metrics";
|
|
576
|
-
value = numbers(v, [["turns", "turns"], ["steps", "steps"], ["toolMs", "toolMs"], ["llmMs", "modelMs"], ["ttftMs", "firstTokenMs"], ["ttftSteps", "firstTokenSamples"]]);
|
|
576
|
+
value = numbers(v, [["turns", "turns"], ["steps", "steps"], ["toolMs", "toolMs"], ["llmMs", "modelMs"], ["ttftMs", "firstTokenMs"], ["ttftSteps", "firstTokenSamples"], ["decodeMs", "decodeMs"], ["decodeTokens", "decodeTokens"]]);
|
|
577
577
|
break;
|
|
578
578
|
case "tokenUsage":
|
|
579
579
|
name = "usage";
|
|
@@ -660,6 +660,23 @@ function planLegacyRpc(request) {
|
|
|
660
660
|
throw new Error(`unsupported Remote endpoint: ${method}`);
|
|
661
661
|
}
|
|
662
662
|
const supplied = recordOf(payload.args);
|
|
663
|
+
if (namespace === "commands" && remoteMethod === "execute") {
|
|
664
|
+
const args = supplied ?? {};
|
|
665
|
+
const line = typeof args.line === "string" ? args.line.trim() : "";
|
|
666
|
+
if (/^\/permission(?:\s|$)/.test(line)) {
|
|
667
|
+
const match = /^\/permission(?:[ \t]+([a-z][a-z0-9-]*))?[ \t]*$/.exec(line);
|
|
668
|
+
if (!match || typeof args.agentId !== "string" || !args.agentId.trim() || ["images", "submittedAttachments"].some((key) => args[key] !== void 0 && (!Array.isArray(args[key]) || args[key].length))) {
|
|
669
|
+
throw Object.assign(new Error("\u6743\u9650\u547D\u4EE4\u683C\u5F0F\u65E0\u6548\uFF1B\u672A\u66F4\u6539\u6743\u9650"), { code: "adapter/invalid-permission-command" });
|
|
670
|
+
}
|
|
671
|
+
return { kind: "permission-command", sessionId: args.agentId, line, nativeReceipt: true, ...match[1] ? { preset: match[1] } : {} };
|
|
672
|
+
}
|
|
673
|
+
const images = Array.isArray(args.images) ? args.images : [];
|
|
674
|
+
return { kind: "invoke", namespace, method: remoteMethod, args: {
|
|
675
|
+
...args,
|
|
676
|
+
images,
|
|
677
|
+
submittedAttachments: args.submittedAttachments ?? images.map((image) => ({ ...recordOf(image), type: "image" }))
|
|
678
|
+
} };
|
|
679
|
+
}
|
|
663
680
|
return { kind: "invoke", namespace, method: remoteMethod, args: supplied ?? {} };
|
|
664
681
|
}
|
|
665
682
|
if (method === "host.describe") return { kind: "host-describe" };
|
|
@@ -798,7 +815,31 @@ function planLegacyRpc(request) {
|
|
|
798
815
|
}
|
|
799
816
|
function resolveTypertGateway(ctx) {
|
|
800
817
|
const candidate = ctx.get("typertGateway");
|
|
801
|
-
|
|
818
|
+
if (!candidate || typeof candidate.invoke !== "function" || typeof candidate.stream !== "function") return null;
|
|
819
|
+
return {
|
|
820
|
+
wireStream: candidate.wireStream,
|
|
821
|
+
invoke: (request) => candidate.invoke(request),
|
|
822
|
+
stream: (request) => candidate.stream(request),
|
|
823
|
+
commandAttachmentField: () => {
|
|
824
|
+
const registry = ctx.get("typert");
|
|
825
|
+
const descriptor = registry?.local?.get("commands/execute");
|
|
826
|
+
if (descriptor) {
|
|
827
|
+
const fields = descriptor.parameters?.map((parameter) => parameter.wire) || [];
|
|
828
|
+
if (fields.includes("submittedAttachments") && !fields.includes("images")) return "submittedAttachments";
|
|
829
|
+
if (fields.includes("images") && !fields.includes("submittedAttachments")) return "images";
|
|
830
|
+
throw new Error("DSH \u547D\u4EE4\u9644\u4EF6\u53C2\u6570\u5DF2\u53D8\u66F4\uFF1B\u672A\u6267\u884C\u547D\u4EE4");
|
|
831
|
+
}
|
|
832
|
+
if (registry?.local?.hasSeen?.("commands/execute")) throw new Error("DSH \u547D\u4EE4\u63A5\u53E3\u6B63\u5728\u66F4\u65B0\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
|
|
833
|
+
return ctx.get("fileUploads") ? "submittedAttachments" : "images";
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
function commandArguments(gateway, args) {
|
|
838
|
+
const field = gateway.commandAttachmentField?.() || "images";
|
|
839
|
+
const { images, submittedAttachments, ...rest } = args;
|
|
840
|
+
const value = field === "submittedAttachments" ? submittedAttachments ?? (Array.isArray(images) ? images.map((image) => ({ ...recordOf(image), type: "image" })) : []) : images ?? [];
|
|
841
|
+
if (field === "images" && Array.isArray(submittedAttachments) && submittedAttachments.some((part) => recordOf(part)?.type !== "image")) throw new Error("\u6B64\u7248\u672C DSH \u547D\u4EE4\u4E0D\u652F\u6301\u6587\u4EF6\u9644\u4EF6");
|
|
842
|
+
return { ...rest, [field]: value };
|
|
802
843
|
}
|
|
803
844
|
function errorResult(error) {
|
|
804
845
|
const value = recordOf(error);
|
|
@@ -926,7 +967,7 @@ async function permissionCommandValue(gateway, plan, signal, readHistory, flushP
|
|
|
926
967
|
const command = recordOf(await gateway.invoke({
|
|
927
968
|
namespace: "commands",
|
|
928
969
|
method: "execute",
|
|
929
|
-
args: { agentId: plan.sessionId, line: plan.line, images: [] },
|
|
970
|
+
args: commandArguments(gateway, { agentId: plan.sessionId, line: plan.line, images: [], submittedAttachments: [] }),
|
|
930
971
|
signal
|
|
931
972
|
}));
|
|
932
973
|
const result = recordOf(command?.result);
|
|
@@ -943,7 +984,7 @@ async function permissionCommandValue(gateway, plan, signal, readHistory, flushP
|
|
|
943
984
|
code: "adapter/permission-not-applied"
|
|
944
985
|
});
|
|
945
986
|
}
|
|
946
|
-
return { accepted: true, command: true, permission: current, commandId: command?.commandId };
|
|
987
|
+
return plan.nativeReceipt ? command : { accepted: true, command: true, permission: current, commandId: command?.commandId };
|
|
947
988
|
}
|
|
948
989
|
async function sessionModelsValue(gateway, request, signal) {
|
|
949
990
|
const sessionId = typeof request.sessionId === "string" ? request.sessionId : "";
|
|
@@ -989,7 +1030,7 @@ async function invokeLegacyRpc(gateway, request, options) {
|
|
|
989
1030
|
value = await gateway.invoke({
|
|
990
1031
|
namespace: plan.namespace,
|
|
991
1032
|
method: plan.method,
|
|
992
|
-
args: plan.args,
|
|
1033
|
+
args: plan.namespace === "commands" && plan.method === "execute" ? commandArguments(gateway, plan.args) : plan.args,
|
|
993
1034
|
signal: options.signal
|
|
994
1035
|
});
|
|
995
1036
|
break;
|
|
@@ -1074,6 +1115,24 @@ function startUpdateWorker(manager, directory, executable) {
|
|
|
1074
1115
|
|
|
1075
1116
|
// src/install-control.ts
|
|
1076
1117
|
import { homedir as homedir4 } from "node:os";
|
|
1118
|
+
|
|
1119
|
+
// src/install-capabilities.ts
|
|
1120
|
+
function assertNativeUpdateCapabilities(context) {
|
|
1121
|
+
const ctx = context.root || context;
|
|
1122
|
+
const web = ctx.get("webServer");
|
|
1123
|
+
const sessions = ctx.get("sessions");
|
|
1124
|
+
const missing = [];
|
|
1125
|
+
for (const name of ["on", "listeners", "removeAllListeners", "removeListener"]) {
|
|
1126
|
+
if (typeof web?.server?.[name] !== "function") missing.push("webServer." + name);
|
|
1127
|
+
}
|
|
1128
|
+
for (const name of ["get", "list", "flush"]) {
|
|
1129
|
+
if (typeof sessions?.[name] !== "function") missing.push("sessions." + name);
|
|
1130
|
+
}
|
|
1131
|
+
if (typeof ctx.fiber?.dispose !== "function") missing.push("lifecycle.dispose");
|
|
1132
|
+
if (missing.length) throw new Error("\u5F53\u524D\u5BBF\u4E3B\u7F3A\u5C11\u5B89\u5168\u66F4\u65B0\u6240\u9700\u80FD\u529B\uFF0C\u672A\u505C\u6B62\u8282\u70B9\uFF1A" + missing.join(", "));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// src/install-control.ts
|
|
1077
1136
|
async function quiesceNativeHost(ctx, read, disposing) {
|
|
1078
1137
|
const items = (await read("session.list")).items;
|
|
1079
1138
|
if (!Array.isArray(items) || items.some((s) => s.running !== false)) throw new Error("\u8BF7\u7B49\u5F85\u8FD0\u884C\u4E2D\u7684\u4F1A\u8BDD\u7ED3\u675F\u540E\u518D\u66F4\u65B0\u3002");
|
|
@@ -1086,6 +1145,7 @@ async function quiesceNativeHost(ctx, read, disposing) {
|
|
|
1086
1145
|
}
|
|
1087
1146
|
async function createInstallControl(context, config) {
|
|
1088
1147
|
const ctx = context.root;
|
|
1148
|
+
assertNativeUpdateCapabilities(ctx);
|
|
1089
1149
|
const home = adapterDshHome(), id = path5.basename(config.directory);
|
|
1090
1150
|
if (!/^[a-f0-9]{32}$/.test(id) || !/^[a-f0-9]{48}$/.test(config.token) || path5.dirname(config.directory) !== path5.join(home, "harness-remote-updates") || fs2.realpathSync(config.directory) !== config.directory) throw new Error("\u5B89\u88C5\u63A7\u5236\u8BF7\u6C42\u4E0D\u5C5E\u4E8E\u5F53\u524D DSH\u3002");
|
|
1091
1151
|
const scope = resolveAgentProfileScope("", process.argv, home), profile = path5.join(home, "profiles", scope);
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-wechat-remote",
|
|
3
|
-
"version": "1.7.
|
|
4
|
-
"private": false,
|
|
5
|
-
"description": "Agent远程管理助手:DSH 微信连接插件的一键安装与升级",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"bin": { "dsh-wechat-remote": "bin/setup.mjs" },
|
|
9
|
-
"files": ["bin/*.mjs", "lib/*.js", "lib/package.json", "assets/plugin.tgz", "assets/release.json", "README.md", "LICENSE"],
|
|
10
|
-
"engines": { "node": ">=22.13.0" },
|
|
11
|
-
"dependencies": { "pnpm": "11.22.0", "yaml": "2.9.0" },
|
|
12
|
-
"bundleDependencies": ["pnpm", "yaml"],
|
|
13
|
-
"repository": { "type": "git", "url": "git+https://github.com/martinbear1/dsh-wechat-remote.git", "directory": "installer" },
|
|
14
|
-
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" }
|
|
15
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-wechat-remote",
|
|
3
|
+
"version": "1.7.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Agent远程管理助手:DSH 微信连接插件的一键安装与升级",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"bin": { "dsh-wechat-remote": "bin/setup.mjs" },
|
|
9
|
+
"files": ["bin/*.mjs", "lib/*.js", "lib/package.json", "assets/plugin.tgz", "assets/release.json", "README.md", "LICENSE"],
|
|
10
|
+
"engines": { "node": ">=22.13.0" },
|
|
11
|
+
"dependencies": { "pnpm": "11.22.0", "yaml": "2.9.0" },
|
|
12
|
+
"bundleDependencies": ["pnpm", "yaml"],
|
|
13
|
+
"repository": { "type": "git", "url": "git+https://github.com/martinbear1/dsh-wechat-remote.git", "directory": "installer" },
|
|
14
|
+
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" }
|
|
15
|
+
}
|