dsh-wechat-remote 1.7.1 → 1.7.3

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,21 @@
1
1
  # Agent远程管理助手 · DSH 插件安装器
2
2
 
3
- 首次安装和升级旧插件使用同一个命令:
3
+ 当前安装器版本为 1.7.3,安装的正式插件仍为 1.7.2。已正常使用插件 1.7.2 的用户,无需因此重新安装。
4
+
5
+ 首次安装和升级旧插件使用同一个命令:
6
+
7
+ ```sh
8
+ npx -y dsh-wechat-remote@latest
9
+ ```
10
+
11
+ 需已安装 DSH,使用 Node.js 22.13 或更高版本。安装工具由包依赖自动提供,无需另装 pnpm 或 Git。
12
+
13
+ 安装器安装正式插件,保留原配置、配对和会话,不会升级 DSH 本体。请先结束运行中的任务;安装完成后自动重启原 DSH 并尝试打开 WebUI。
4
14
 
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
+ 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 服务可自动恢复;无法确认启动方式时不自动停止主机。
18
+
19
+ 旧插件 1.7.1 在 DSH 0.1.5 上无法一键更新时,可用本安装命令过渡一次。从插件 1.7.2 起,后续已验证的兼容更新不再受旧的 DSH 版本列表限制;不需要因此降低 DSH 版本或删除节点。
20
+
21
+ 使用其他配置时附加 `--profile 配置名称`。安装损坏需要重装时附加 `--repair`;不会自动降级。
package/assets/plugin.tgz CHANGED
Binary file
@@ -1,34 +1,35 @@
1
- {
2
- "version": "1.7.1",
3
- "catalog": {
4
- "schemaVersion": 1,
5
- "revision": "installer-1.7.1",
6
- "issuedAt": 1788979000679,
7
- "expiresAt": 1791398200679,
8
- "releases": [
9
- {
10
- "version": "1.7.1",
11
- "channel": "stable",
12
- "dsh": [
13
- "0.1.1-rc.2",
14
- "0.1.2-rc.1"
15
- ],
16
- "platforms": [
17
- "windows",
18
- "macos",
19
- "linux"
20
- ],
21
- "architectures": [
22
- "x64"
23
- ],
24
- "asset": {
25
- "url": "https://github.com/martinbear1/dsh-wechat-remote/releases/download/v1.7.1/harness-remote-dsh-wechat-remote-1.7.1.tgz",
26
- "sha256": "5274a6d5beb513124a6d1a916ddd6df7d3e236a7a970e48515d902fa4d8c94d0",
27
- "bytes": 9526754
28
- }
29
- }
30
- ],
31
- "blocked": [],
32
- "retiredDsh": []
33
- }
34
- }
1
+ {
2
+ "version": "1.7.2",
3
+ "catalog": {
4
+ "schemaVersion": 1,
5
+ "revision": "installer-1.7.2",
6
+ "issuedAt": 1789134534877,
7
+ "expiresAt": 1791553734877,
8
+ "releases": [
9
+ {
10
+ "version": "1.7.2",
11
+ "channel": "stable",
12
+ "dsh": [
13
+ "0.1.1-rc.2",
14
+ "0.1.2-rc.1",
15
+ "0.1.5-rc.1"
16
+ ],
17
+ "platforms": [
18
+ "windows",
19
+ "macos",
20
+ "linux"
21
+ ],
22
+ "architectures": [
23
+ "x64"
24
+ ],
25
+ "asset": {
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
29
+ }
30
+ }
31
+ ],
32
+ "blocked": [],
33
+ "retiredDsh": []
34
+ }
35
+ }
@@ -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
+ }
@@ -0,0 +1,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
+ const bundled = local.releases.find(r => r.version === pinned.version && r.channel === 'stable')
10
+ if (!bundled || !trustedReleaseAsset(bundled.asset, bundled.version)) {
11
+ throw new Error('安装包信息不完整,请重新获取官方安装器。')
12
+ }
13
+ let online
14
+ try {
15
+ const candidate = validateCatalog(remote)
16
+ if (candidate.issuedAt <= now + 300000 && candidate.expiresAt > now) online = candidate
17
+ } catch { /* The authenticated npm package remains usable without the catalog. */ }
18
+
19
+ // An installer never silently substitutes an older plugin than its own bundle.
20
+ // Keep the bundled bytes/hash for the same version even if remote metadata differs.
21
+ const candidates = [bundled, ...(online?.releases || []).filter(r => r.channel === 'stable'
22
+ && compareVersions(r.version, bundled.version) > 0 && trustedReleaseAsset(r.asset, r.version))]
23
+ .sort((a, b) => compareVersions(b.version, a.version))
24
+ const rules = [...local.blocked, ...(online?.blocked || [])]
25
+ const blocked = release => rules.find(b => b.pluginVersion === release.version
26
+ && (!b.dsh || b.dsh.includes(current.agentVersion))
27
+ && (!b.platforms || b.platforms.includes(current.platform)))
28
+ const release = candidates.find(r => !blocked(r))
29
+ if (!release) {
30
+ throw new Error(`当前插件存在已知不兼容问题:${blocked(bundled).reason};未替换现有插件。`)
31
+ }
32
+ return release
33
+ }