dsh-wechat-remote 1.7.0 → 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 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
- ```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`,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
@@ -1,17 +1,18 @@
1
1
  {
2
- "version": "1.7.0",
2
+ "version": "1.7.2",
3
3
  "catalog": {
4
4
  "schemaVersion": 1,
5
- "revision": "installer-1.7.0",
6
- "issuedAt": 1788963160754,
7
- "expiresAt": 1791382360754,
5
+ "revision": "installer-1.7.2",
6
+ "issuedAt": 1789134534877,
7
+ "expiresAt": 1791553734877,
8
8
  "releases": [
9
9
  {
10
- "version": "1.7.0",
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.0/harness-remote-dsh-wechat-remote-1.7.0.tgz",
26
- "sha256": "594f42f1b959c5579b82df458932fc6dfa276abe88b6e94e789cbf639335f98d",
27
- "bytes": 9524528
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
  ],
@@ -1,55 +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
- }
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,152 +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 } 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, 'web', '--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
- try { ref = await waitForJson(path.join(directory, 'control-ready.json'), v => Number.isInteger(v.pid), 5000) }
93
- catch {
94
- if (launched || await portBusy(Number(process.env.DSH_PORT || 3080))) throw new Error('当前 DSH 尚未完成安装握手,未替换插件。')
95
- const log = fs.openSync(path.join(directory, 'startup.log'), 'a', 0o600)
96
- launched = spawn(process.execPath, [cli, 'web', '--profile', profileName, '--no-open'], {
97
- cwd: process.cwd(), env: process.env, detached: true, windowsHide: true, stdio: ['ignore', log, log] })
98
- fs.closeSync(log); launched.on('error', () => {}); launched.unref()
99
- ref = await waitForJson(path.join(directory, 'control-ready.json'), v => v.pid === launched.pid, 20000)
100
- }
101
- job = { controlOrigin: ref.origin, statusToken: token }
102
- const host = await control(job, 'describe')
103
- if (host.home !== home || host.profile !== profile || host.cli !== fs.realpathSync(cli) || (launched && host.pid !== launched.pid)) throw new Error('当前 DSH 身份与安装目标不一致,未修改安装。')
104
- remove(); remove = undefined
105
- await sleep(600)
106
- const selected = await selectRelease(host, assetsRoot, repair)
107
- if (!selected.archive) { console.log(`插件 ${host.pluginVersion} 无需更新。`); return { version: host.pluginVersion, changed: false } }
108
- console.log(`正在安装插件 ${selected.release.version},保留原配对与会话…`)
109
- job = { ...host, id, directory, parentPid: host.pid, pnpm: runtime.cli, targetVersion: selected.release.version,
110
- previousVersion: host.pluginVersion, statusToken: token, controlOrigin: ref.origin }
111
- validateJob(job)
112
- fs.writeFileSync(path.join(directory, 'release.tgz'), selected.archive, { mode: 0o600, flag: 'wx' })
113
- fs.copyFileSync(path.join(root, 'lib/update-worker.js'), path.join(directory, 'update-worker.js'))
114
- writePrivateJsonAtomic(path.join(directory, 'job.json'), job)
115
- await control(job, 'launch')
116
- const ready = await waitForJson(path.join(directory, 'worker-ready.json'), v => v.id === id)
117
- const scope = createHash('sha256').update(profileName).digest('hex').slice(0, 24)
118
- writePrivateJsonAtomic(path.join(home, 'harness-remote-updates', `profile-${scope}.json`), { jobId: id, statusOrigin: ready.origin, statusToken: token })
119
- writePrivateJsonAtomic(path.join(directory, 'authorized.json'), { id }); authorized = true
120
- let previous = '', result
121
- const until = Date.now() + 600000
122
- while (Date.now() < until) {
123
- try { result = JSON.parse(fs.readFileSync(path.join(directory, 'result.json'), 'utf8')) } catch {}
124
- if (result?.phase !== previous && result?.phase) { console.log(`${result.progress}% ${result.message}`); previous = result.phase }
125
- if (result?.terminal) break
126
- await sleep(500)
127
- }
128
- if (!result?.terminal || !result.ok) throw new Error(result?.message || '安装结果待确认,请保留本机安装记录,不要重复安装。')
129
- if (open) {
130
- try {
131
- const origin = `http://127.0.0.1:${host.webPort}`
132
- const response = await fetch(`http://127.0.0.1:${host.localPort}/gate/update/resume?job=${id}`, { headers: { origin }, signal: AbortSignal.timeout(10000) })
133
- const value = await response.json(), url = new URL(value.url)
134
- if (!response.ok || url.origin !== origin || url.pathname !== '/' || url.hash || url.username || url.password) throw new Error('invalid resume')
135
- const command = process.platform === 'win32' ? ['rundll32.exe', ['url.dll,FileProtocolHandler', url.href]] : process.platform === 'darwin' ? ['/usr/bin/open', [url.href]] : ['xdg-open', [url.href]]
136
- const browser = spawn(command[0], command[1], { detached: true, windowsHide: true, stdio: 'ignore' }); browser.on('error', () => {}); browser.unref()
137
- } catch { console.log('插件已更新;请返回原 DSH WebUI 查看。') }
138
- }
139
- return { version: selected.release.version, changed: true }
140
- } finally {
141
- remove?.()
142
- if (ref && !authorized) { try { await control({ controlOrigin: ref.origin, statusToken: token }, 'close') } catch {} }
143
- if (locked && !authorized) releaseOwnedUpdateLock(lockFile, id)
144
- }
145
- }
146
- if (process.argv[1] && fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
147
- const args = process.argv.slice(2), repair = args.includes('--repair')
148
- if (repair) args.splice(args.indexOf('--repair'), 1)
149
- if (args.includes('--help') || args.includes('-h')) console.log('安装或升级 DSH 微信连接插件:npx -y dsh-wechat-remote@latest\n可选:--profile <名称>(默认 web);--repair(重新安装当前兼容版本,不降级)')
150
- else if (args.length && !(args.length === 2 && args[0] === '--profile')) { console.error('不支持的参数。使用 --help 查看用法。'); process.exitCode = 1 }
151
- else install({ profileName: args[1] || 'web', repair }).catch(error => { console.error(error.message); process.exitCode = 1 })
152
- }
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
+ }
@@ -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
- return candidate && typeof candidate.invoke === "function" && typeof candidate.stream === "function" ? candidate : null;
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.0",
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
+ }