@noob-stupid/dsh-plugin-console 0.5.50 → 0.5.54

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.
@@ -0,0 +1,60 @@
1
+ // 诊断工具(**不是测试**,不进 tests/ 目录、不进 CI 门禁):打印升级/回滚脚本块的实际切分结果。
2
+ //
3
+ // 来历:原先它在 tests/ 下叫 diag-blocks.mjs,读 `lib/server/domain/framework-install-script.js`
4
+ // ——那是**从未入库**的路径(生成器现在在 lib/server/infra/fw-integrity-check.js),
5
+ // 于是它每次运行都抛 ENOENT,把全量测试拖成 35/36。诊断脚本没有断言,本就不该占测试名额;
6
+ // 正确的位置是 scripts/,而且**读不到文件时必须明说"跳过 + 原因"**,不能假装成功、也不能崩。
7
+ //
8
+ // 用法:node scripts/diag-blocks.mjs
9
+ import { readFileSync, existsSync } from 'node:fs'
10
+ import { join, dirname } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ const REPO = join(dirname(fileURLToPath(import.meta.url)), '..')
14
+ const readSrc = (rel) => readFileSync(join(REPO, rel), 'utf8').replace(/\r\n/gu, '\n')
15
+
16
+ const SOURCES = [
17
+ ['RFU', 'lib/server/routes/framework-upgrade.js'],
18
+ ['FR', 'lib/server/routes/framework.js'],
19
+ ['IDX', 'lib/index.js'],
20
+ ['FWIS', 'lib/server/infra/fw-integrity-check.js'],
21
+ ]
22
+ const endMarker = ".filter((l) => l !== '').join('\\r\\n')"
23
+ const startMarker = 'const lines = ['
24
+ let blocks = 0
25
+
26
+ for (const [name, rel] of SOURCES) {
27
+ if (!existsSync(join(REPO, rel))) {
28
+ // 明确跳过:打印原因(不许静默)
29
+ console.log(`SKIP ${name} ${rel} —— 文件不在仓库里(该来源无法诊断)`)
30
+ continue
31
+ }
32
+ const source = readSrc(rel)
33
+ let at = source.indexOf(endMarker)
34
+ while (at !== -1) {
35
+ const from = source.lastIndexOf(startMarker, at)
36
+ if (from === -1) {
37
+ console.log(`WARN ${name} ${rel}@${at} —— 结束标记前找不到 'const lines = ['(该块会被抽取器静默丢弃)`)
38
+ } else {
39
+ const block = source.slice(from + startMarker.length - 1, at) + endMarker
40
+ blocks += 1
41
+ const flags = [
42
+ `Install-Framework=${block.includes('function Install-Framework')}`,
43
+ `Invoke-Quarantine=${block.includes('Invoke-Quarantine')}`,
44
+ `一键回滚=${block.includes('一键回滚脚本启动')}`,
45
+ ].join(' ')
46
+ console.log(`#${blocks} [${name}] 长度=${block.length} ${flags}`)
47
+ console.log(` 首行: ${JSON.stringify(block.split('\n')[0].slice(0, 90))}`)
48
+ console.log(` 末行: ${JSON.stringify(block.split('\n').slice(-1)[0].slice(0, 90))}`)
49
+ }
50
+ at = source.indexOf(endMarker, at + endMarker.length)
51
+ }
52
+ // FWIS 用的是 `return [ … ].join('\r\n')`(没有 .filter 结束标记),单独报一次
53
+ if (name === 'FWIS') {
54
+ const fwStart = source.indexOf('function fwIntegrityCheck')
55
+ console.log(fwStart === -1
56
+ ? `WARN FWIS ${rel} —— 找不到 function fwIntegrityCheck`
57
+ : `OK FWIS ${rel} —— fwIntegrityCheck 在(生成"安装后结构校验"那段脚本)`)
58
+ }
59
+ }
60
+ console.log(`共 ${blocks} 块(抽取标记:${JSON.stringify(startMarker)} → ${JSON.stringify(endMarker)})`)
@@ -0,0 +1,112 @@
1
+ # fw-watchdog-task.ps1 —— 注册/卸载 DSH 框架闭包看门狗的计划任务(Windows)
2
+ #
3
+ # 为什么用计划任务而不是改官方代码:官方启动链没有"启动前执行外部命令"的钩子
4
+ # (取证见 scripts/fw-watchdog.mjs 头注释)。计划任务是**不改任何官方文件**、
5
+ # 且 DSH 升级也不会被覆盖的唯一位置。
6
+ #
7
+ # 用法(在 scripts 目录下):
8
+ # powershell -ExecutionPolicy Bypass -File fw-watchdog-task.ps1 # 安装(幂等)
9
+ # powershell -ExecutionPolicy Bypass -File fw-watchdog-task.ps1 -Status # 看状态
10
+ # powershell -ExecutionPolicy Bypass -File fw-watchdog-task.ps1 -Uninstall # 卸载
11
+ # powershell -ExecutionPolicy Bypass -File fw-watchdog-task.ps1 -RunNow # 立刻跑一轮
12
+ #
13
+ # 任务形态:登录时启动 → 常驻(脚本内部每 20s 一轮)→ 失败自动重启(3 次 / 间隔 1 分钟)。
14
+ # · 为什么常驻而不是"每 1 分钟触发一次":任务计划程序的最小重复间隔是 1 分钟,
15
+ # 而我们要 20s;常驻 + 内部定时更精确,也避免每分钟拉起一个进程。
16
+ # · 只读判据 + 只修框架闭包:不杀进程、不抢端口、不 spawn dsh、不改官方文件。
17
+ Param(
18
+ [switch]$Uninstall,
19
+ [switch]$Status,
20
+ [switch]$RunNow
21
+ )
22
+
23
+ $ErrorActionPreference = 'Stop'
24
+ $TaskName = 'DSH-FwWatchdog'
25
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
26
+ $Watchdog = Join-Path $ScriptDir 'fw-watchdog.mjs'
27
+
28
+ if ($Status) {
29
+ $t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
30
+ if ($null -eq $t) { Write-Host "任务 $TaskName :未安装"; exit 0 }
31
+ $info = Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction SilentlyContinue
32
+ Write-Host "任务 : $($t.TaskName)"
33
+ Write-Host "状态 : $($t.State)"
34
+ Write-Host "上次运行 : $($info.LastRunTime) 结果码: $($info.LastTaskResult)"
35
+ Write-Host "下次运行 : $($info.NextRunTime)"
36
+ Write-Host "动作 : $($t.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" })"
37
+ exit 0
38
+ }
39
+
40
+ if ($Uninstall) {
41
+ $t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
42
+ if ($null -eq $t) { Write-Host "任务 $TaskName 本来就不存在,无需卸载"; exit 0 }
43
+ Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
44
+ Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
45
+ Write-Host "已卸载任务 $TaskName"
46
+ exit 0
47
+ }
48
+
49
+ if ($RunNow) {
50
+ Start-ScheduledTask -TaskName $TaskName
51
+ Write-Host "已触发一轮(看日志:~/.dsh/plugin-console/fw-watchdog.log)"
52
+ exit 0
53
+ }
54
+
55
+ # ── 解析 node(钉绝对路径:计划任务里的 PATH 不可靠)────────────────────────
56
+ $node = $null
57
+ $candidates = @()
58
+ if ($env:DSH_DESKTOP_NODE_EXECUTABLE) { $candidates += $env:DSH_DESKTOP_NODE_EXECUTABLE }
59
+ foreach ($root in @('D:\dsh-desktop', "$env:ProgramFiles\DeepSeek Harness", "$env:LOCALAPPDATA\Programs\DeepSeek Harness")) {
60
+ if ($root) { $candidates += (Join-Path $root 'resources\runtime\primary-runtime\dependencies\node\bin\node.exe') }
61
+ }
62
+ $onPath = (Get-Command node -ErrorAction SilentlyContinue).Source
63
+ if ($onPath) { $candidates += $onPath }
64
+ foreach ($c in $candidates) { if (-not $node -and $c -and (Test-Path -LiteralPath $c)) { $node = $c } }
65
+ if (-not $node) { throw "找不到可用的 node —— 请安装 Node.js 后重试" }
66
+
67
+ if (-not (Test-Path -LiteralPath $Watchdog)) { throw "找不到看门狗脚本:$Watchdog" }
68
+
69
+ # ── 为什么要 `--daemon` 而不是直接跑 node(★ 2026-10-14 两次实测踩坑后定案):
70
+ # ① 任务直接跑 node.exe ⇒ Windows 弹出**可见的控制台窗口**(本机实测桌面出现一个
71
+ # Windows Terminal,标题就是 node 的绝对路径),后台看门狗不该占用户桌面;
72
+ # ② 改用 .vbs 启动器(window style 0)⇒ wscript 挂住、拉不起子进程(实测两次都没起来)。
73
+ # 最终做法:任务跑 `node … --daemon`,脚本**自己以 detached + stdio:'ignore' +
74
+ # windowsHide 再拉起一份**并立刻返回 ⇒ 任务那条命令秒退、真正干活的那份无窗口在后台跑,
75
+ # 重复启动由 pid 锁挡住(幂等)。
76
+ Write-Host "node : $node"
77
+ Write-Host "看门狗 : $Watchdog (--daemon:自己脱离,无控制台窗口)"
78
+
79
+ # ── 注册(幂等:先卸旧的)──────────────────────────────────────────────────
80
+ $existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
81
+ if ($existing) {
82
+ Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
83
+ Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
84
+ Write-Host "已移除旧任务,准备重建"
85
+ }
86
+
87
+ $action = New-ScheduledTaskAction -Execute $node -Argument "`"$Watchdog`" --daemon" -WorkingDirectory $ScriptDir
88
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
89
+ $settings = New-ScheduledTaskSettingsSet `
90
+ -AllowStartIfOnBatteries `
91
+ -DontStopIfGoingOnBatteries `
92
+ -StartWhenAvailable `
93
+ -MultipleInstances IgnoreNew `
94
+ -RestartCount 3 `
95
+ -RestartInterval (New-TimeSpan -Minutes 1) `
96
+ -ExecutionTimeLimit ([TimeSpan]::Zero) # 零 = 不限制运行时长(常驻)
97
+
98
+ Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Settings $settings `
99
+ -Description 'DSH 框架闭包看门狗:外壳在跑但服务进程不在时,检查并修复被掏空的框架包(只修闭包,不杀进程/不抢端口/不改官方文件)。日志:~/.dsh/plugin-console/fw-watchdog.log' | Out-Null
100
+
101
+ Write-Host ""
102
+ Write-Host "✅ 已注册计划任务 $TaskName(登录时启动,常驻,20s 一轮)"
103
+ Write-Host " 看状态 : powershell -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -Status"
104
+ Write-Host " 跑一轮 : powershell -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -RunNow"
105
+ Write-Host " 卸载 : powershell -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -Uninstall"
106
+ Write-Host " 日志 : $env:USERPROFILE\.dsh\plugin-console\fw-watchdog.log"
107
+ Write-Host ""
108
+ Write-Host "现在就启动它(不用等下次登录):"
109
+ Start-ScheduledTask -TaskName $TaskName
110
+ Start-Sleep -Seconds 3
111
+ $info = Get-ScheduledTaskInfo -TaskName $TaskName
112
+ Write-Host " 当前状态: $((Get-ScheduledTask -TaskName $TaskName).State) 上次结果码: $($info.LastTaskResult)"
@@ -0,0 +1,30 @@
1
+ @echo off
2
+ rem fw-watchdog.cmd -- manual entry point for the framework-closure watchdog.
3
+ rem usage: fw-watchdog.cmd --status | --once | --tick | (no args = resident)
4
+ rem NOTE: this file must keep CRLF line endings and ASCII-only comments --
5
+ rem cmd.exe chokes on LF-only .cmd files and on multi-byte comments read in
6
+ rem the OEM code page (measured: the comment text got executed as commands).
7
+ setlocal
8
+ set "WD=%~dp0fw-watchdog.mjs"
9
+ set "NODE="
10
+
11
+ rem (1) the node the desktop host passes down
12
+ if defined DSH_DESKTOP_NODE_EXECUTABLE if exist "%DSH_DESKTOP_NODE_EXECUTABLE%" set "NODE=%DSH_DESKTOP_NODE_EXECUTABLE%"
13
+
14
+ rem (2) the node bundled with the desktop runtime (most stable)
15
+ if not defined NODE (
16
+ for %%D in ("D:\dsh-desktop" "%ProgramFiles%\DeepSeek Harness" "%LOCALAPPDATA%\Programs\DeepSeek Harness") do (
17
+ if not defined NODE if exist "%%~D\resources\runtime\primary-runtime\dependencies\node\bin\node.exe" set "NODE=%%~D\resources\runtime\primary-runtime\dependencies\node\bin\node.exe"
18
+ )
19
+ )
20
+
21
+ rem (3) whatever node is on PATH (last resort)
22
+ if not defined NODE for /f "delims=" %%N in ('where node 2^>nul') do if not defined NODE set "NODE=%%N"
23
+
24
+ if not defined NODE (
25
+ echo [fw-watchdog] no usable node found -- install Node.js, or run this from the desktop install.
26
+ exit /b 2
27
+ )
28
+
29
+ "%NODE%" "%WD%" %*
30
+ exit /b %ERRORLEVEL%
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ // fw-watchdog.mjs —— DSH 框架闭包看门狗(不改官方代码的"崩溃自愈")
3
+ //
4
+ // ── 为什么需要它 ────────────────────────────────────────────────────────────
5
+ // 真机事故(2026-10-14):框架树里 `@deepseek-ai/cordis` 的 junction 还在、但 `.pnpm`
6
+ // 实体被掏空;`dsh-llm`/`dsh-tools`/`dsh-web` 成了空目录 ⇒ dsh 启动即 54 个插件
7
+ // failed to import ⇒ 服务进程 exit code=1 ⇒ 桌面端弹「应用无法启动或已意外停止」。
8
+ //
9
+ // 官方启动链(已只读取证)**没有**"启动前执行外部命令"的钩子:
10
+ // DeepSeek Harness.exe → dsh-desktop-host/lib/cli.js → runCli()(dsh/lib/bin.js:207)
11
+ // → case "profile": runProfile() → 失败 → reportStartupFailure() → process.exit(1)
12
+ // · bin.js 全文 254 行,读的环境变量:零;
13
+ // · dsh-app-boot 只有 DSH_SNAPSHOT=replay(换配置)与 loadEnv()(读 .env),
14
+ // 而 .env 能设的名字有黑名单(PATH/HOME/NODE_OPTIONS/…)——只能设配置,不能"启动前跑脚本";
15
+ // · 失败路径不 spawn 任何外部命令。
16
+ // ⇒ 想在"每次 dsh 启动前自动修"必须改官方外壳(且一升级就被覆盖)。
17
+ // 所以本脚本走另一条路:**观测一个合法事实** —— 外壳进程还在,而它的 **HOST 服务进程没了**。
18
+ // 官方自己的崩溃恢复只知道"重启",不知道"修包";看门狗补的正是这一段:
19
+ // 它把地面修平,官方下一次重启自然成功 —— 用户观感就是"崩了自己又起来了"。
20
+ //
21
+ // ── 判据(★ 2026-10-14 修正:一开始用"端口无监听",那是错的)────────────────
22
+ // 为什么不能用端口:桌面实例监听 19387,3080 是 web profile 的端口;用固定端口探针
23
+ // 会在正常运行时恒判"服务没起来" ⇒ 每轮白跑一次全量闭包检查、日志刷屏。
24
+ // 现在的判据(进程树事实,与端口无关):
25
+ // ① 外壳进程在跑(`DeepSeek Harness.exe` 且命令行**不含** `--type=`);
26
+ // ② 它的 HOST 服务子进程**不在**(命令行含 `dsh-desktop-host/lib/index.js` 的那个);
27
+ // ③ 连续 N 轮(默认 2 轮 ≈ 40s)都如此 ⇒ 才动手(避开"正在启动/正在重启"的窗口)。
28
+ // 动作:`fw-preflight --check` → 闭包坏 ⇒ `--repair` → `--check` 复验。
29
+ // 修完**不自己拉起服务**:官方崩溃恢复/relaunch 会自己重试,我们只把地面修平。
30
+ // 红线:不杀进程、不抢端口、不 spawn dsh、不改官方任何文件。
31
+ //
32
+ // ── 用法 ──────────────────────────────────────────────────────────────────
33
+ // node fw-watchdog.mjs --daemon 后台守护(**计划任务用这个**):自己脱离、无控制台窗口、幂等
34
+ // node fw-watchdog.mjs --status 只打印当前判据事实,不动作
35
+ // node fw-watchdog.mjs --tick 跑一次完整逻辑(演练 / 手动触发)
36
+ // node fw-watchdog.mjs --once 诊断:判据 + 跑一次 --check 并打印结论
37
+ // node fw-watchdog.mjs 前台常驻(Ctrl-C 退出;排查时用)
38
+ // 日志:`<DSH_HOME>/plugin-console/fw-watchdog.log`(追加式,只在有事时写)
39
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
40
+ import { homedir } from 'node:os'
41
+ import { join } from 'node:path'
42
+ import { spawnSync } from 'node:child_process'
43
+ import { fileURLToPath } from 'node:url'
44
+
45
+ const ARGV = process.argv.slice(2)
46
+ const HAS = (f) => ARGV.includes(f)
47
+ const HOME = homedir()
48
+ const DSH_HOME = process.env.DSH_HOME?.trim() || join(HOME, '.dsh')
49
+ const LOG_DIR = join(DSH_HOME, 'plugin-console')
50
+ const LOG = join(LOG_DIR, 'fw-watchdog.log')
51
+ const INTERVAL_MS = Number(process.env.DSH_WATCHDOG_INTERVAL_MS ?? 20000)
52
+ const STRIKES = Number(process.env.DSH_WATCHDOG_STRIKES ?? 2)
53
+ const EXE = process.platform === 'win32' ? 'DeepSeek Harness.exe' : null
54
+
55
+ /** 找到本机已装的 fw-preflight.mjs。
56
+ * 扫描各个 profile 的 node_modules/<包>/scripts/(不写死 desktop —— 用户可能只装 web profile,
57
+ * 或 profile 名不同)。★ 注意:本注释里**不能**出现星号后跟斜杠那种通配写法,
58
+ * 那会提前闭合块注释(2026-10-14 实测踩到:脚本因此整份 SyntaxError、看门狗从未真正运行过)。 */
59
+ function findPreflight() {
60
+ if (typeof process.env.DSH_WATCHDOG_CONSOLE === 'string' && process.env.DSH_WATCHDOG_CONSOLE !== '') {
61
+ return join(process.env.DSH_WATCHDOG_CONSOLE, 'scripts', 'fw-preflight.mjs')
62
+ }
63
+ // 本脚本自己就在 <包>/scripts/ 下时,优先用同包那份(最不容易错配)
64
+ try {
65
+ const self = fileURLToPath(import.meta.url)
66
+ const samePkg = join(self, '..', 'fw-preflight.mjs')
67
+ if (existsSync(samePkg)) return samePkg
68
+ } catch {}
69
+ const profilesDir = join(DSH_HOME, 'profiles')
70
+ let names = []
71
+ try { names = readdirSync(profilesDir) } catch { return join(profilesDir, 'desktop', 'node_modules', '@noob-stupid', 'dsh-plugin-console', 'scripts', 'fw-preflight.mjs') }
72
+ for (const name of names) {
73
+ const p = join(profilesDir, name, 'node_modules', '@noob-stupid', 'dsh-plugin-console', 'scripts', 'fw-preflight.mjs')
74
+ if (existsSync(p)) return p
75
+ }
76
+ return join(profilesDir, 'desktop', 'node_modules', '@noob-stupid', 'dsh-plugin-console', 'scripts', 'fw-preflight.mjs')
77
+ }
78
+ const PREFLIGHT = findPreflight()
79
+
80
+ const log = (line) => {
81
+ const stamp = new Date().toISOString().replace('T', ' ').slice(0, 19)
82
+ const text = `[${stamp}] ${line}\n`
83
+ try { mkdirSync(LOG_DIR, { recursive: true }); appendFileSync(LOG, text, 'utf8') } catch {}
84
+ process.stdout.write(text)
85
+ }
86
+
87
+ /** 进程树事实(一次 PowerShell 调用取全,避免每条判据各 spawn 一次)。
88
+ * ★ 坑(2026-10-14 实测踩到):PowerShell 里 `$host` 是**只读自动变量**,赋值直接报
89
+ * "Cannot overwrite variable Host because it is read-only or constant" ⇒ 探针恒返回 0
90
+ * ⇒ 看门狗永远以为"外壳没在跑"、静默失效。所以变量名一律用 `$hostN`/`$shellN`。
91
+ * 同族坑:$HOME / $PID / $PSHOME 等一律不许赋值。 */
92
+ function probeProcesses() {
93
+ // 测试注入缝(只为离线演练;生产不设这个变量)
94
+ const fake = process.env.DSH_WATCHDOG_FAKE_PROCS
95
+ if (typeof fake === 'string' && /^\d+\|\d+$/u.test(fake)) {
96
+ const [s, h] = fake.split('|').map(Number)
97
+ return { shell: s, host: h, raw: `${s}|${h} (injected)` }
98
+ }
99
+ if (EXE === null) return { shell: 0, host: 0, raw: 'non-win32' }
100
+ const ps = [
101
+ `$all = @(Get-CimInstance Win32_Process -Filter "Name='${EXE}'" -ErrorAction SilentlyContinue)`,
102
+ `$shellN = @($all | Where-Object { $_.CommandLine -notmatch '--type=' }).Count`,
103
+ `$hostN = @($all | Where-Object { $_.CommandLine -match 'dsh-desktop-host[\\\\/]lib[\\\\/]index\\.js' }).Count`,
104
+ `Write-Output ($shellN.ToString() + '|' + $hostN.ToString())`,
105
+ ].join('; ')
106
+ const r = spawnSync('powershell', ['-NoProfile', '-Command', ps], { encoding: 'utf8', timeout: 20000, windowsHide: true })
107
+ const m = /(\d+)\|(\d+)/u.exec(String(r.stdout ?? ''))
108
+ return { shell: m ? Number(m[1]) : 0, host: m ? Number(m[2]) : 0, raw: String(r.stdout ?? '').trim() }
109
+ }
110
+
111
+ const preflight = (mode) => {
112
+ if (!existsSync(PREFLIGHT)) {
113
+ log(`找不到 fw-preflight:${PREFLIGHT}(控制台未安装或路径变了)—— 本轮跳过`)
114
+ return { ok: false, stdout: '', missing: true }
115
+ }
116
+ const r = spawnSync(process.execPath, [PREFLIGHT, mode], { encoding: 'utf8', timeout: 180000, windowsHide: true })
117
+ return { ok: r.status === 0, stdout: String(r.stdout ?? '') + String(r.stderr ?? ''), missing: false }
118
+ }
119
+
120
+ /** 闭包是否坏。判据取自 fw-preflight 自己那行结论:「走了 N 个包,M 个解析不了」,M>0 即坏。
121
+ * 认不出格式时按"坏"处理 —— 宁可多查一次,也不放过真事故。 */
122
+ function closureBroken(out) {
123
+ const m = /走了\s*(\d+)\s*个包[,,]\s*(\d+)\s*个解析不了/u.exec(out)
124
+ if (m) return Number(m[2]) > 0
125
+ if (/闭包完整/u.test(out)) return false
126
+ return true
127
+ }
128
+
129
+ let strikes = 0
130
+ let repairing = false
131
+
132
+ async function tick() {
133
+ if (repairing) return
134
+ try {
135
+ const p = probeProcesses()
136
+ if (p.shell === 0) { strikes = 0; return } // 外壳没开 = 用户没在用,不管
137
+ if (p.host > 0) { // HOST 在 = 服务活着 = 正常
138
+ if (strikes !== 0) log('HOST 服务进程已回来 —— 恢复正常,看门狗回待命')
139
+ strikes = 0
140
+ return
141
+ }
142
+ strikes += 1
143
+ if (strikes < STRIKES) return
144
+ log(`外壳在跑(${p.shell}) 但 HOST 服务进程不在(连续 ${strikes} 轮)⇒ 开始检查框架闭包`)
145
+ const check = preflight('--check')
146
+ if (check.missing) { strikes = 0; return }
147
+ if (!closureBroken(check.stdout)) {
148
+ log('闭包本身是好的 ⇒ 起不来另有原因(不是包被掏空),本轮不动作,等官方自己的恢复逻辑')
149
+ strikes = 0
150
+ return
151
+ }
152
+ log('闭包确实坏了 ⇒ 执行 --repair(从本地 npm 缓存按 integrity 离线装回,不联网)')
153
+ repairing = true
154
+ try {
155
+ const fix = preflight('--repair')
156
+ log(`--repair 退出码=${fix.ok ? 0 : '非0'}`)
157
+ const re = preflight('--check')
158
+ const good = re.ok && !closureBroken(re.stdout)
159
+ log(good
160
+ ? '✅ 修复成功:闭包复验通过。服务交由官方自己的恢复/relaunch 拉起(本脚本不抢端口、不 spawn dsh)。'
161
+ : '❌ 修复后复验仍未通过 —— 需人工介入(手动跑 --repair 看详细输出;日志在 ~/.dsh/plugin-console/)')
162
+ } finally { repairing = false; strikes = 0 }
163
+ } catch (error) {
164
+ log(`本轮异常(不影响下一轮):${String(error?.message ?? error)}`)
165
+ }
166
+ }
167
+
168
+ // ── 后台守护(计划任务用):自己脱离,不留控制台窗口 ──────────────────────────
169
+ // ★ 为什么要这一模式(2026-10-14 实测):计划任务直接跑 node.exe 会让 Windows 弹出**可见的
170
+ // 控制台窗口**(本机实测桌面出现一个 Windows Terminal);改用 .vbs 启动器(window style 0)
171
+ // 又踩到 wscript 挂住、拉不起子进程。最稳的是让脚本**自己以 detached 方式再拉起一份**,
172
+ // 并让它 `stdio: 'ignore' + windowsHide: true`(子进程没有控制台可挂)⇒ 任务那条命令立刻返回,
173
+ // 真正干活的那份在后台无窗口运行。重复启动由 pid 锁挡住。
174
+ if (HAS('--daemon')) {
175
+ const PID_FILE = join(LOG_DIR, 'fw-watchdog.pid')
176
+ const alive = (pid) => {
177
+ if (!Number.isInteger(pid) || pid <= 0) return false
178
+ try { process.kill(pid, 0); return true } catch { return false }
179
+ }
180
+ let already = null
181
+ try { already = Number(String(readFileSync(PID_FILE, 'utf8')).trim()) } catch {}
182
+ if (alive(already)) {
183
+ log(`后台看门狗已在运行(pid ${already})—— 本次不重复启动`)
184
+ process.exit(0)
185
+ }
186
+ const { spawn } = await import('node:child_process')
187
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url)], {
188
+ detached: true, stdio: 'ignore', windowsHide: true,
189
+ env: { ...process.env },
190
+ })
191
+ child.unref()
192
+ try { mkdirSync(LOG_DIR, { recursive: true }); writeFileSync(PID_FILE, String(child.pid), 'utf8') } catch {}
193
+ log(`已用后台守护模式拉起看门狗(pid ${child.pid},无窗口)`)
194
+ process.exit(0)
195
+ }
196
+
197
+ // ── 单轮完整逻辑(演练 / 计划任务手动触发)──
198
+ if (HAS('--tick')) {
199
+ log('── 单轮完整逻辑(--tick)──')
200
+ await tick()
201
+ log('── 本轮结束 ──')
202
+ process.exit(0)
203
+ }
204
+
205
+ // ── 状态 / 诊断 ──
206
+ if (HAS('--status') || HAS('--once')) {
207
+ const p = probeProcesses()
208
+ log(HAS('--once') ? '── 诊断(--once)──' : '── 状态(--status)──')
209
+ log(`外壳进程: ${p.shell} 个 HOST 服务进程: ${p.host} 个`)
210
+ log(`判定:${p.shell === 0 ? '外壳没在跑(不管)' : p.host > 0 ? '服务活着(正常)' : '★ 外壳在跑但服务不在(事故形态)'}`)
211
+ log(`预检脚本:${PREFLIGHT}`)
212
+ if (HAS('--once')) {
213
+ const check = preflight('--check')
214
+ log(`fw-preflight --check 退出码=${check.ok ? 0 : '非0'}${check.missing ? '(脚本缺失)' : ''}`)
215
+ if (!check.missing) {
216
+ log(check.stdout.trim().split('\n').map((l) => ' ' + l).join('\n'))
217
+ log(`闭包判定:${closureBroken(check.stdout) ? '★ 坏' : '好'}`)
218
+ }
219
+ }
220
+ process.exit(0)
221
+ }
222
+
223
+ // ── 常驻 ──
224
+ // ★ 主入口守卫(2026-10-14 补):没有它时,任何人 `import()` 本文件都会**意外启动常驻看门狗**
225
+ // (验证语法时就踩到过:一个 import 把看门狗跑起来了)。只在真正作为脚本执行时才启动。
226
+ if (import.meta.main !== true) {
227
+ process.exit(0)
228
+ }
229
+
230
+ log(`看门狗启动:间隔=${INTERVAL_MS}ms 连续${STRIKES}轮 预检=${PREFLIGHT}`)
231
+ log('判据:外壳在跑 + HOST 服务进程不在;只修框架闭包,不杀进程、不抢端口、不改官方文件')
232
+ // 记下自己的 pid(`--daemon` 靠它防重复启动;退出时清掉)
233
+ try { mkdirSync(LOG_DIR, { recursive: true }); writeFileSync(join(LOG_DIR, 'fw-watchdog.pid'), String(process.pid), 'utf8') } catch {}
234
+ await tick()
235
+ const timer = setInterval(tick, INTERVAL_MS)
236
+ const stop = () => {
237
+ clearInterval(timer)
238
+ try {
239
+ const f = join(LOG_DIR, 'fw-watchdog.pid')
240
+ if (String(readFileSync(f, 'utf8')).trim() === String(process.pid)) rmSync(f, { force: true })
241
+ } catch {}
242
+ log('看门狗停止')
243
+ process.exit(0)
244
+ }
245
+ process.on('SIGINT', stop)
246
+ process.on('SIGTERM', stop)