dsh-remote-agent 0.9.0 → 0.9.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/CHANGELOG.md +211 -0
- package/README.en.md +12 -5
- package/README.md +39 -12
- package/bin/dsh-remote-setup.js +75 -2
- package/cordis.patch.yml +31 -24
- package/lib/client.js +117 -11
- package/lib/env-check.js +99 -7
- package/lib/hub/index.js +5 -2
- package/lib/hub/registry-ingest.js +21 -2
- package/lib/hub/templates.js +5 -2
- package/lib/hub/ui-routes.js +14 -1
- package/lib/node/share-service.js +43 -2
- package/package.json +1 -1
- package/scripts/check-doc-drift.mjs +92 -15
- package/scripts/diag-worker.ps1 +148 -0
|
@@ -929,37 +929,55 @@ check('65', 'eslint 严格档:lint script + 配置文件 + CI job 三件套齐
|
|
|
929
929
|
'lib/env-check.js(可达性/inject/roots/role + fix + 不代下载边界)')
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
-
/* ── 79~80. 开箱即用(2026-10-17
|
|
932
|
+
/* ── 79~80. 开箱即用(2026-10-17,2026-10-18 修订)──
|
|
933
933
|
*
|
|
934
934
|
* 产品标准:用户装完就该能用,不该读文档、改 YAML、猜字段。
|
|
935
935
|
*
|
|
936
|
-
* 此前三道门槛:
|
|
937
|
-
* 1. `role` 默认 '' → 两台机器都要操作一次
|
|
938
|
-
* 2. `webserver` 没绑 0.0.0.0 → 副机永远连不上(且完全静默)
|
|
939
|
-
* 3. 副机表单 4 个字段 → 小白不知道节点 ID / 工作区填什么
|
|
940
|
-
*
|
|
941
936
|
* 核心设计:`role` 默认 **auto**,按 `hubUrl` 推断。
|
|
942
937
|
* · 默认 hub → 副机装完以 hub 身份启动,注册无鉴权端点(**攻击面**)
|
|
943
938
|
* · 默认 '' → 两台都要操作,不是开箱即用
|
|
944
939
|
* · 默认 auto → 主机零操作、副机贴完配对码零操作,且不给副机开错端点
|
|
940
|
+
*
|
|
941
|
+
* ⚠ **2026-10-18 事故修订**:79 号断言原来**要求**包内 patch 绑
|
|
942
|
+
* `host: '0.0.0.0'` —— 那正是导致 **DSH 启动崩溃**的写法。新版
|
|
943
|
+
* `dsh-host-webserver` 在 schema 校验阶段就硬拒绝通配地址:
|
|
944
|
+
*
|
|
945
|
+
* host "0.0.0.0" is an unspecified (wildcard) address, which is not
|
|
946
|
+
* supported: binding every interface would expose remote code execution
|
|
947
|
+
* to the network; bind one concrete IPv4 or IPv6 address ...
|
|
948
|
+
*
|
|
949
|
+
* 后果:webserver(required)激活失败 → webServer 服务缺失 → 至少 6 个插件
|
|
950
|
+
* 排队等它 → host 启动失败(实测 10 份 crash 日志)。
|
|
951
|
+
*
|
|
952
|
+
* 所以断言方向**反过来**:现在要求包内 patch **不碰** webserver 的任何字段,
|
|
953
|
+
* 且所有提示文案都不得建议 0.0.0.0。
|
|
945
954
|
*/
|
|
946
955
|
{
|
|
947
956
|
const indexSrc = read('lib/index.js')
|
|
948
957
|
const patchSrc = read('cordis.patch.yml')
|
|
949
958
|
const clientSrc3 = read('lib/client.js')
|
|
950
959
|
const setupSrc = read('lib/setup-routes.js')
|
|
951
|
-
|
|
952
|
-
|
|
960
|
+
const envSrc = read('lib/env-check.js')
|
|
961
|
+
const hubSrc = read('lib/hub/index.js')
|
|
962
|
+
|
|
963
|
+
// ⚠ 只扫**非注释行**:注释里必须保留那次事故的原文(含 `0.0.0.0` 与
|
|
964
|
+
// `- id: webserver` 示例),那是给后来者的警示 —— 不该被断言判违规。
|
|
965
|
+
// 这一点和 L3 断言踩过的坑同源(扫全文会命中"正确留档的注释")。
|
|
966
|
+
const patchCode = patchSrc
|
|
967
|
+
.split('\n')
|
|
968
|
+
.filter((l) => !/^\s*#/.test(l))
|
|
969
|
+
.join('\n')
|
|
970
|
+
|
|
971
|
+
check('79', '开箱即用:role 默认 auto;且包内 patch **不得**绑 0.0.0.0(会让 DSH 崩溃)',
|
|
953
972
|
/\.default\('auto'\)/.test(indexSrc)
|
|
954
973
|
&& /ROLE\.AUTO/.test(indexSrc)
|
|
955
974
|
&& /config\.hubUrl/.test(indexSrc)
|
|
956
|
-
//
|
|
957
|
-
&&
|
|
958
|
-
&&
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
'lib/index.js(auto 推断)+ cordis.patch.yml(webserver 0.0.0.0:19387 + 安全披露)')
|
|
975
|
+
// ⚠ 关键:包内 patch 的**生效行**不得 override webserver 的 host
|
|
976
|
+
&& !/^- id: webserver$/m.test(patchCode)
|
|
977
|
+
&& !/host:\s*'0\.0\.0\.0'/.test(patchCode)
|
|
978
|
+
// 必须把这个坑写进注释(防止后来者又"帮用户改好")
|
|
979
|
+
&& /启动崩溃/.test(patchSrc),
|
|
980
|
+
'lib/index.js(auto 推断)+ cordis.patch.yml(生效行不碰 webserver + 事故留档)')
|
|
963
981
|
|
|
964
982
|
check('80', '开箱即用:副机表单简化 + 工作区自动创建 + 首次自动体检',
|
|
965
983
|
// 工作区预填
|
|
@@ -972,6 +990,19 @@ check('65', 'eslint 严格档:lint script + 配置文件 + CI job 三件套齐
|
|
|
972
990
|
&& /mkdirSync\(r, \{ recursive: true \}\)/.test(setupSrc)
|
|
973
991
|
&& /warnings: applyWarnings/.test(setupSrc),
|
|
974
992
|
'lib/client.js(预填/折叠/自动体检)+ lib/setup-routes.js(自动建目录)')
|
|
993
|
+
|
|
994
|
+
// ── 79b:**所有面向用户的修法文案**都不得建议 0.0.0.0 ──────────────
|
|
995
|
+
//
|
|
996
|
+
// 这是本次事故最有价值的一条守卫:代码自己不能犯那个错,提示文案也不能。
|
|
997
|
+
// 检查的是字符串字面量(`host: '0.0.0.0'` 这种形态),不是注释里的说明。
|
|
998
|
+
check('79b', '所有修法文案都不得建议 host 0.0.0.0(会让 DSH 启动崩溃)',
|
|
999
|
+
!/host:\s*'0\.0\.0\.0'/.test(clientSrc3)
|
|
1000
|
+
&& !/host:\s*'0\.0\.0\.0'/.test(envSrc)
|
|
1001
|
+
&& !/加 host:\s*'0\.0\.0\.0'/.test(hubSrc)
|
|
1002
|
+
// 但要**明确警告不能用**它(否则用户可能自己想到这个"标准做法")
|
|
1003
|
+
&& /不能用 0\.0\.0\.0/.test(envSrc)
|
|
1004
|
+
&& /不能用 0\.0\.0\.0/.test(clientSrc3),
|
|
1005
|
+
'lib/client.js + lib/env-check.js + lib/hub/index.js(修法不含 0.0.0.0,且有明确警告)')
|
|
975
1006
|
}
|
|
976
1007
|
|
|
977
1008
|
/* ── 81~82. 工作区 SMB 共享(2026-10-17)──
|
|
@@ -1052,6 +1083,52 @@ check('65', 'eslint 严格档:lint script + 配置文件 + CI job 三件套齐
|
|
|
1052
1083
|
+ ` vs README.md(声明 ${declared === null ? '未找到' : declared} 个)`)
|
|
1053
1084
|
}
|
|
1054
1085
|
|
|
1086
|
+
/* ── 84~85. v0.9.0 审查的两个 High:**接线断链**(2026-10-17)──
|
|
1087
|
+
*
|
|
1088
|
+
* 这两个 bug 的共同点:**模块本身完全正确,装配进系统时接线断了**。
|
|
1089
|
+
*
|
|
1090
|
+
* H1 env-check.js 的 24 条单测全绿,但 /env-check 路由传的是**原始
|
|
1091
|
+
* config.role**('auto')→ 五项检查全跳过、两项误报 → 对 100% 默认
|
|
1092
|
+
* 安装用户失效,且首次自动体检让它在每个新用户面前弹出。
|
|
1093
|
+
* H2 share-service.js 的 17 条单测全绿,但面板恒发 dir: '' → 副机按设计
|
|
1094
|
+
* 拒绝 → SMB 共享的唯一主动作 100% 失败。
|
|
1095
|
+
*
|
|
1096
|
+
* 单测证明不了"模块被装配进系统时的形状"。这两条断言锚在接线处。
|
|
1097
|
+
*/
|
|
1098
|
+
{
|
|
1099
|
+
const uiSrc4 = read('lib/hub/ui-routes.js')
|
|
1100
|
+
const clientSrc5 = read('lib/client.js')
|
|
1101
|
+
const setupBin = read('bin/dsh-remote-setup.js')
|
|
1102
|
+
|
|
1103
|
+
check('84', 'env-check 路由传解析后角色(v0.9.0 审查 H1:否则 auto 判成非 hub)',
|
|
1104
|
+
// 反例:把原始 config.role 直接传给 envChecks
|
|
1105
|
+
(() => {
|
|
1106
|
+
const i = uiSrc4.indexOf('/env-check`')
|
|
1107
|
+
if (i < 0) return false
|
|
1108
|
+
const seg = uiSrc4.slice(i, i + 900)
|
|
1109
|
+
// 不得出现 `role: typeof config.role === 'string' ? config.role`
|
|
1110
|
+
if (/role:\s*typeof config\.role === 'string'\s*\?\s*config\.role/.test(seg)) return false
|
|
1111
|
+
// 必须用 resolveRole(或字面量 'hub')
|
|
1112
|
+
return /resolveRole/.test(seg)
|
|
1113
|
+
})(),
|
|
1114
|
+
'lib/hub/ui-routes.js(role: resolveRole(config) 而非原始 config.role)')
|
|
1115
|
+
|
|
1116
|
+
check('85', 'SMB 面板发真实 dir + CLI doctor 有 loopback 检查(H2 / M1)',
|
|
1117
|
+
// H2:不得写死 dir: ''
|
|
1118
|
+
(() => {
|
|
1119
|
+
const i = clientSrc5.indexOf('function doPlan()')
|
|
1120
|
+
if (i < 0) return false
|
|
1121
|
+
const seg = clientSrc5.slice(i, i + 900)
|
|
1122
|
+
return !/dir:\s*''/.test(seg) && /dir:\s*shareDir/.test(seg)
|
|
1123
|
+
})()
|
|
1124
|
+
// H2:必须有目录选择 UI(数据源 node.roots)
|
|
1125
|
+
&& /nodeRoots/.test(clientSrc5)
|
|
1126
|
+
// M1:CLI 必须调 envChecks(否则没有 loopback 诊断)
|
|
1127
|
+
&& /envChecks/.test(setupBin)
|
|
1128
|
+
&& /isLoopbackOrigin/.test(setupBin),
|
|
1129
|
+
'lib/client.js(dir: shareDir + nodeRoots UI)+ bin/dsh-remote-setup.js(envChecks)')
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1055
1132
|
/* ── 输出 ── */
|
|
1056
1133
|
const failed = results.filter((r) => !r.ok)
|
|
1057
1134
|
if (process.argv.includes('--json')) {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# dsh-remote-agent worker diagnostic
|
|
2
|
+
#
|
|
3
|
+
# Read-only: changes nothing. Paste into PowerShell on the WORKER machine:
|
|
4
|
+
# powershell -ExecutionPolicy Bypass -File .\diag-worker.ps1
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'SilentlyContinue'
|
|
7
|
+
$profiles = Join-Path $env:USERPROFILE '.dsh\profiles'
|
|
8
|
+
|
|
9
|
+
function Line { param($t) Write-Host $t }
|
|
10
|
+
|
|
11
|
+
Write-Host ''
|
|
12
|
+
Write-Host '=========================================='
|
|
13
|
+
Write-Host ' dsh-remote-agent WORKER DIAGNOSTIC'
|
|
14
|
+
Write-Host '=========================================='
|
|
15
|
+
Write-Host ''
|
|
16
|
+
|
|
17
|
+
# ---- 1. profile dirs ----
|
|
18
|
+
Write-Host '[1] DSH profile directories'
|
|
19
|
+
if (Test-Path $profiles) {
|
|
20
|
+
Get-ChildItem $profiles -Directory | ForEach-Object {
|
|
21
|
+
$patch = Join-Path $_.FullName 'cordis.patch.yml'
|
|
22
|
+
if (Test-Path $patch) {
|
|
23
|
+
$f = Get-Item $patch
|
|
24
|
+
Line (" {0} patch={1}B mtime={2}" -f $_.Name, $f.Length, $f.LastWriteTime.ToString('MM-dd HH:mm:ss'))
|
|
25
|
+
} else {
|
|
26
|
+
Line (" {0} (no cordis.patch.yml)" -f $_.Name)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if ($env:DSH_PROFILE) {
|
|
30
|
+
Line (" env DSH_PROFILE = " + $env:DSH_PROFILE)
|
|
31
|
+
} else {
|
|
32
|
+
Line ' env DSH_PROFILE = (not set -> code defaults to "desktop")'
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
Line ' ERROR: no profile directory'
|
|
36
|
+
}
|
|
37
|
+
Write-Host ''
|
|
38
|
+
|
|
39
|
+
# ---- 2. remote-agent config per profile ----
|
|
40
|
+
Write-Host '[2] remote-agent config in each profile'
|
|
41
|
+
if (Test-Path $profiles) {
|
|
42
|
+
Get-ChildItem $profiles -Directory | ForEach-Object {
|
|
43
|
+
$patch = Join-Path $_.FullName 'cordis.patch.yml'
|
|
44
|
+
if (-not (Test-Path $patch)) { return }
|
|
45
|
+
$c = Get-Content $patch -Raw
|
|
46
|
+
if ($c -notmatch 'remote-agent') { return }
|
|
47
|
+
|
|
48
|
+
$roleM = [regex]::Match($c, '(?m)^\s*role:\s*(\S+)')
|
|
49
|
+
$injectM = [regex]::Match($c, '(?m)^\s*inject:\s*(.+)$')
|
|
50
|
+
$hubM = [regex]::Match($c, '(?m)^\s*hubUrl:\s*(\S+)')
|
|
51
|
+
$wsM = [regex]::Match($c, '(?ms)- id:\s*webserver.*?host:\s*(\S+)')
|
|
52
|
+
$tokM = [regex]::Match($c, '(?m)^\s*nodeToken:\s*(\S+)')
|
|
53
|
+
|
|
54
|
+
Line (' --- ' + $_.Name + ' ---')
|
|
55
|
+
if ($roleM.Success) { Line (' role = ' + $roleM.Groups[1].Value) } else { Line ' role = (NOT SET -> package default)' }
|
|
56
|
+
if ($injectM.Success) { Line (' inject = ' + $injectM.Groups[1].Value.Trim()) } else { Line ' inject = (not set)' }
|
|
57
|
+
if ($hubM.Success) { Line (' hubUrl = ' + $hubM.Groups[1].Value) } else { Line ' hubUrl = (not set)' }
|
|
58
|
+
if ($wsM.Success) { Line (' webserver = host=' + $wsM.Groups[1].Value) } else { Line ' webserver = (no webserver section)' }
|
|
59
|
+
if ($tokM.Success) { Line ' nodeToken = (set)' } else { Line ' nodeToken = (not set)' }
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
Line ' (skipped)'
|
|
63
|
+
}
|
|
64
|
+
Write-Host ''
|
|
65
|
+
|
|
66
|
+
# ---- 3. installed plugin version ----
|
|
67
|
+
Write-Host '[3] installed dsh-remote-agent version'
|
|
68
|
+
$found = $false
|
|
69
|
+
if (Test-Path $profiles) {
|
|
70
|
+
Get-ChildItem $profiles -Directory | ForEach-Object {
|
|
71
|
+
$nm = Join-Path $_.FullName 'node_modules\dsh-remote-agent'
|
|
72
|
+
$pj = Join-Path $nm 'package.json'
|
|
73
|
+
if (Test-Path $pj) {
|
|
74
|
+
$script:found = $true
|
|
75
|
+
$v = (Get-Content $pj -Raw | ConvertFrom-Json).version
|
|
76
|
+
$hasEnvCheck = Test-Path (Join-Path $nm 'lib\env-check.js')
|
|
77
|
+
$hasShare = Test-Path (Join-Path $nm 'lib\node\share-service.js')
|
|
78
|
+
$pkgPatch = Join-Path $nm 'cordis.patch.yml'
|
|
79
|
+
$pkgRole = ''
|
|
80
|
+
$hasWs = $false
|
|
81
|
+
if (Test-Path $pkgPatch) {
|
|
82
|
+
$pc = Get-Content $pkgPatch -Raw
|
|
83
|
+
$m = [regex]::Match($pc, '(?m)^\s*role:\s*(\S+)')
|
|
84
|
+
if ($m.Success) { $pkgRole = $m.Groups[1].Value }
|
|
85
|
+
$hasWs = $pc -match '(?m)^- id:\s*webserver'
|
|
86
|
+
}
|
|
87
|
+
Line (' ' + $_.Name + ': version ' + $v)
|
|
88
|
+
Line (' has lib/env-check.js (0.9+) : ' + $hasEnvCheck)
|
|
89
|
+
Line (' has lib/node/share-service.js : ' + $hasShare)
|
|
90
|
+
if ($pkgRole -ne '') { Line (' package default role : ' + $pkgRole) } else { Line ' package default role : (not found)' }
|
|
91
|
+
Line (' package has webserver binding : ' + $hasWs)
|
|
92
|
+
if (-not $hasEnvCheck) {
|
|
93
|
+
Line ' WARNING: this is pre-0.9, no "works out of the box"'
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (-not $script:found) {
|
|
99
|
+
Line ' ERROR: dsh-remote-agent not found under any profile'
|
|
100
|
+
}
|
|
101
|
+
Write-Host ''
|
|
102
|
+
|
|
103
|
+
# ---- 4. panel endpoints ----
|
|
104
|
+
Write-Host '[4] panel endpoint probe (is the plugin loaded at all?)'
|
|
105
|
+
$ports = @()
|
|
106
|
+
$dshPids = (Get-Process 'DeepSeek Harness').Id
|
|
107
|
+
if ($dshPids) {
|
|
108
|
+
$ports += (Get-NetTCPConnection -State Listen | Where-Object { $dshPids -contains $_.OwningProcess } | ForEach-Object { $_.LocalPort })
|
|
109
|
+
}
|
|
110
|
+
$ports += 19387
|
|
111
|
+
$ports = $ports | Sort-Object -Unique
|
|
112
|
+
foreach ($port in $ports) {
|
|
113
|
+
foreach ($path in @('/api/remote-agent/setup/state', '/api/remote-agent/status')) {
|
|
114
|
+
$url = 'http://127.0.0.1:' + $port + $path
|
|
115
|
+
try {
|
|
116
|
+
$r = Invoke-WebRequest $url -UseBasicParsing -TimeoutSec 3
|
|
117
|
+
$body = $r.Content
|
|
118
|
+
if ($body.Length -gt 240) { $body = $body.Substring(0, 240) + '...' }
|
|
119
|
+
Line (' [' + $port + '] ' + $path + ' -> HTTP ' + $r.StatusCode)
|
|
120
|
+
Line (' ' + $body)
|
|
121
|
+
} catch {
|
|
122
|
+
$code = $_.Exception.Response.StatusCode.value__
|
|
123
|
+
if ($code) {
|
|
124
|
+
Line (' [' + $port + '] ' + $path + ' -> HTTP ' + $code)
|
|
125
|
+
} else {
|
|
126
|
+
Line (' [' + $port + '] ' + $path + ' -> no listener')
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
Write-Host ''
|
|
132
|
+
|
|
133
|
+
# ---- 5. recent backups ----
|
|
134
|
+
Write-Host '[5] recent config backups (was the file rewritten?)'
|
|
135
|
+
if (Test-Path $profiles) {
|
|
136
|
+
Get-ChildItem $profiles -Recurse -File -Filter 'cordis.patch.yml*' |
|
|
137
|
+
Sort-Object LastWriteTime -Descending |
|
|
138
|
+
Select-Object -First 8 |
|
|
139
|
+
ForEach-Object {
|
|
140
|
+
Line (' ' + $_.LastWriteTime.ToString('MM-dd HH:mm:ss') + ' ' + $_.Length + 'B ' + $_.Name)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
Write-Host ''
|
|
144
|
+
|
|
145
|
+
Write-Host '=========================================='
|
|
146
|
+
Write-Host ' END. Paste the whole output back.'
|
|
147
|
+
Write-Host '=========================================='
|
|
148
|
+
Write-Host ''
|