@noob-stupid/dsh-plugin-console 0.5.71 → 0.5.85
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/lib/client.js +269 -25
- package/lib/server/domain/disk-audit.js +330 -0
- package/lib/server/domain/enrich-throttle.js +242 -0
- package/lib/server/domain/install-finalize.js +3 -1
- package/lib/server/domain/install-job.js +5 -5
- package/lib/server/domain/install-jobs-store.js +69 -0
- package/lib/server/domain/install.js +1 -1
- package/lib/server/domain/lock-autofix.js +310 -0
- package/lib/server/domain/manifest.js +15 -1
- package/lib/server/domain/market.js +13 -7
- package/lib/server/domain/persist.js +123 -3
- package/lib/server/domain/restart-launcher.js +110 -0
- package/lib/server/domain/restart.js +84 -10
- package/lib/server/domain/skills.js +9 -5
- package/lib/server/domain/update-candidates.js +25 -5
- package/lib/server/infra/egress.js +34 -1
- package/lib/server/routes/disk.js +59 -0
- package/lib/server/routes/framework.js +63 -2
- package/lib/server/routes/index.js +8 -0
- package/lib/server/routes/install.js +6 -0
- package/lib/server/routes/revoke-force.js +35 -0
- package/lib/server/routes/state.js +32 -2
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// L2 · routes —— `<DSH_HOME>/plugin-console` 的**磁盘体检 / 清理**(0.5.84 加法,用户第 7 项)
|
|
2
|
+
// GET /plugin-console/disk-audit 只读体检:谁占了多少、哪些可以清、哪些绝不能动
|
|
3
|
+
// POST /plugin-console/disk-cleanup **必须显式确认**(confirm:true)才删;未确认只回计划
|
|
4
|
+
//
|
|
5
|
+
// 用户原话(大意):这个目录已经 2.1 GB(framework-backups 含全平台 libreoffice),
|
|
6
|
+
// 要一个"只读体检 + 显式确认后清理"的能力,**绝不自动删除**。
|
|
7
|
+
//
|
|
8
|
+
// 安全语义(写死,判据全在 domain/disk-audit.js):
|
|
9
|
+
// · audit 只统计,不删不写;
|
|
10
|
+
// · cleanup 只认体检结果里出现过的条目名(拒绝路径穿越),且 protected(当前回滚点 /
|
|
11
|
+
// 控制台状态文件 / 未知条目)一律拒绝并说明原因;
|
|
12
|
+
// · 删完**读回核实**,没删干净如实回报(Windows 上 rmSync 会静默落空,本仓 fsx.js 记过)。
|
|
13
|
+
//
|
|
14
|
+
// 真机实测(2026-10):最大的那 2.1 GB 是 `framework-backups/0.2.0-rc.1`,而它**正是**
|
|
15
|
+
// `framework-rollback.json` 的 checkpointDir ⇒ 判据从回滚记录里读,绝不按体积猜"大的就可以删"。
|
|
16
|
+
|
|
17
|
+
import { auditConsoleDir, runCleanup } from '../domain/disk-audit.js'
|
|
18
|
+
import { sendError, sendJson } from '../infra/httpd.js'
|
|
19
|
+
import { dshHome } from '../infra/paths.js'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
/** 体检/清理的唯一根目录(绝不接受调用方传路径 —— 那等于开了一个任意路径删除的口子)。 */
|
|
23
|
+
function consoleDir() {
|
|
24
|
+
return join(dshHome(), 'plugin-console')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function routeDiskAudit(req, res, rc) {
|
|
28
|
+
const audit = auditConsoleDir({ root: consoleDir(), deps: rc.deps?.diskAuditDeps ?? {} })
|
|
29
|
+
if (audit.ok !== true) {
|
|
30
|
+
sendError(res, 500, `磁盘体检失败:${audit.error ?? '未知原因'}`, { ok: false, entries: [] })
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
sendJson(res, 200, audit)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function routeDiskCleanup(req, res, rc) {
|
|
37
|
+
const body = rc.body ?? {}
|
|
38
|
+
const ids = Array.isArray(body.ids) ? body.ids : (typeof body.ids === 'string' ? [body.ids] : [])
|
|
39
|
+
const root = consoleDir()
|
|
40
|
+
// 每次都**重新体检**(不信任客户端回传的体积/分级 —— 那等于让调用方决定删什么)
|
|
41
|
+
const audit = auditConsoleDir({ root, deps: rc.deps?.diskAuditDeps ?? {} })
|
|
42
|
+
if (audit.ok !== true) {
|
|
43
|
+
sendError(res, 500, `磁盘体检失败:${audit.error ?? '未知原因'}`, { ok: false })
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const result = runCleanup({ root, audit, ids, confirm: body.confirm === true, deps: rc.deps?.diskCleanupDeps ?? {} })
|
|
47
|
+
if (result.needsConfirm === true) {
|
|
48
|
+
// 未确认:200 + 计划(不是错误 —— 面板要用这份计划渲染「确认清理」按钮与体积)
|
|
49
|
+
sendJson(res, 200, { ok: true, needsConfirm: true, applied: false, plan: result.plan, note: result.note })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
if (result.plan?.refused?.length > 0 && result.removed?.length === 0) {
|
|
53
|
+
sendError(res, 400, `没有可以清理的条目:${result.plan.refused.map((r) => `${r.id}(${r.reason})`).join(';')}`, { ok: false, applied: false, refused: result.plan.refused })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
sendJson(res, 200, result)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { routeDiskAudit, routeDiskCleanup }
|
|
@@ -10,6 +10,9 @@ import { createRequire } from 'node:module'
|
|
|
10
10
|
import { readCompatGate, writeCompatGate } from '../domain/compat.js'
|
|
11
11
|
import { cleanupStaleFwTasks, currentFrameworkVersion, detectHostShape, pickFrameworkTarget, platformUpgradeRefusal, relaunchPrelude, resolveDshBin, shellHostedRefusal } from '../domain/framework.js'
|
|
12
12
|
import { restartPathDecision } from '../domain/restart.js'
|
|
13
|
+
// 0.5.84 加法(用户第 5 项):独立实例的 detached 重拉 —— 计划在 domain/restart-launcher.js,
|
|
14
|
+
// 进程原语仍走 infra/egress.js 的可注入出口(单测能证明"一次真进程都没起")。
|
|
15
|
+
import { planRelaunch, relaunchDetached } from '../domain/restart-launcher.js'
|
|
13
16
|
import { readGithubAuth } from '../domain/install.js'
|
|
14
17
|
import { webPort } from '../domain/runtime.js'
|
|
15
18
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
@@ -45,6 +48,31 @@ function hostedHere(rc) {
|
|
|
45
48
|
return rc.deps?.hosted ?? detectHostShape().hosted === true
|
|
46
49
|
}
|
|
47
50
|
|
|
51
|
+
/** 解析 DSH 的 bin.js(**失败不抛**):拿不到就返回 null,判据据此回落"手动重启"。 */
|
|
52
|
+
function resolveDshBinSafe() {
|
|
53
|
+
try {
|
|
54
|
+
return resolveDshBin()
|
|
55
|
+
} catch {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 本实例真实监听的端口(自定义 --port / 系统分配也对;拿不到返回 null)。 */
|
|
61
|
+
function webPortHard(rc) {
|
|
62
|
+
try {
|
|
63
|
+
return webPort(rc.ctx)
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 优雅退出的默认实现:回完响应后结束本进程(**不 kill 任何进程**,端口随进程结束而释放)。 */
|
|
70
|
+
function defaultGracefulExit(delayMs) {
|
|
71
|
+
const timer = setTimeout(() => { process.exit(0) }, Math.max(0, Number(delayMs) || 0))
|
|
72
|
+
// 不阻止进程退出:这个定时器只是"晚一点退",不是"必须跑完"
|
|
73
|
+
if (typeof timer?.unref === 'function') timer.unref()
|
|
74
|
+
}
|
|
75
|
+
|
|
48
76
|
/**
|
|
49
77
|
* 平台守卫(2026-09-29 改错,与宿主守卫同形):一键升级/回滚脚本整体依赖 Windows 机制 ——
|
|
50
78
|
* `schtasks.exe` 计划任务 + PowerShell + `Get-NetTCPConnection`;非 Windows 上 execFile 必然 ENOENT,
|
|
@@ -454,8 +482,41 @@ async function routeRestart(req, res, rc) {
|
|
|
454
482
|
//
|
|
455
483
|
// 注:清僵尸计划任务的既有能力**保留**(cleanupStaleFwTasks 见 lib/index.js 接线),
|
|
456
484
|
// 只是控制台自己再也不新建这类任务了。
|
|
457
|
-
|
|
458
|
-
|
|
485
|
+
//
|
|
486
|
+
// ★ 0.5.84(用户第 5 项,**只在非 shell 托管时**):独立 dsh web 实例现在支持**真重启** ——
|
|
487
|
+
// detach 起一个新实例,老进程自己优雅退出。桌面端托管时判据照旧拒绝(那一半一个字没动)。
|
|
488
|
+
// 判据仍在 domain/restart.js(唯一承担者);这里只做三件事,顺序是刻意的:
|
|
489
|
+
// ① 先算重拉计划(拿不到 bin.js 就如实回落"手动重启",不硬来);
|
|
490
|
+
// ② 起启动器(detached,它自己等端口空了再起新实例 —— 于是没有 EADDRINUSE 竞态);
|
|
491
|
+
// ③ **先回响应**,再让本进程退出(不 kill 自己、不建计划任务)。
|
|
492
|
+
const hosted = hostedHere(rc)
|
|
493
|
+
// `rc.deps.relaunch` 是单测的确定性注入口(与 rc.deps.hosted 同一风格):
|
|
494
|
+
// 传 `null` = 模拟"拿不到 bin.js";不传 = 按真实解析(resolveDshBin + webPort)算。
|
|
495
|
+
const relaunchPlan = hosted === true ? null : (rc.deps?.relaunch !== undefined ? rc.deps.relaunch : planRelaunch({
|
|
496
|
+
binPath: resolveDshBinSafe(),
|
|
497
|
+
// 与真机那条原始命令同形:`node <bin.js> web`;端口显式带上(自定义 --port 也能原样重启)
|
|
498
|
+
args: (() => { const p = webPortHard(rc); return p === null ? ['web'] : ['web', '--port', String(p)] })(),
|
|
499
|
+
port: webPortHard(rc),
|
|
500
|
+
cwd: null,
|
|
501
|
+
}))
|
|
502
|
+
const decision = restartPathDecision({ hosted, intent: 'restart', relaunch: relaunchPlan })
|
|
503
|
+
if (decision.spawns !== true) {
|
|
504
|
+
sendError(res, decision.status, decision.message, decision.details)
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
const kicked = await relaunchDetached(relaunchPlan.plan, { spawn: rc.deps?.spawnDetached })
|
|
508
|
+
if (kicked.started !== true) {
|
|
509
|
+
// 起不来就**如实说**,绝不让面板以为"重启了"(也不会退出本进程——那样用户就真的没有实例了)
|
|
510
|
+
sendError(res, 500, `重启失败:没能拉起新进程(${kicked.error ?? '原因未知'})。本进程仍在运行,请手动重启。`, {
|
|
511
|
+
code: 'standalone-relaunch-failed', channel: decision.channel, spawns: false, kill: false, scheduledTask: false, selfDelete: false,
|
|
512
|
+
})
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
sendJson(res, 200, { ok: true, restarted: true, channel: decision.channel, pid: kicked.pid, message: decision.message, details: decision.details })
|
|
516
|
+
// ★ 先回响应、再退("优雅退出"):让端口随本进程结束而释放;启动器在那之后才起新实例。
|
|
517
|
+
// 延时是给响应刷出去的时间;delayedExit 可注入(单测绝不真的退出进程)。
|
|
518
|
+
const exit = typeof rc.deps?.delayedExit === 'function' ? rc.deps.delayedExit : defaultGracefulExit
|
|
519
|
+
exit(500)
|
|
459
520
|
return
|
|
460
521
|
}
|
|
461
522
|
|
|
@@ -20,6 +20,8 @@ import { routeCompatAck, routeCompatMode, routeCompatStamp, routeCompatStatusGet
|
|
|
20
20
|
import { routeSafeBoot } from './safe-boot.js'
|
|
21
21
|
import { routePatchAuditGet } from './patch-audit.js'
|
|
22
22
|
import { routePresetAuditGet } from './preset-audit.js'
|
|
23
|
+
// 0.5.84 加法(用户第 7 项):<DSH_HOME>/plugin-console 的磁盘体检(只读)与**确认后**清理
|
|
24
|
+
import { routeDiskAudit, routeDiskCleanup } from './disk.js'
|
|
23
25
|
import { routeFrameworkUpgrade } from './framework-upgrade.js'
|
|
24
26
|
import { routeFrameworkPreflight, routeFrameworkPreflightPatch } from './framework-preflight.js'
|
|
25
27
|
import { routeCheckUpdate, routeCompatGate, routeFrameworkCheck, routeFrameworkRelaunch, routeFrameworkRollback, routeFrameworkStatusClear, routeFrameworkUpgradeStatusGet, routeRestart } from './framework.js'
|
|
@@ -27,6 +29,7 @@ import { routeGithubLogin, routeGithubOpenLogin } from './github-login.js'
|
|
|
27
29
|
import { routeInstall, routeInstallStatus } from './install.js'
|
|
28
30
|
import { routeLockfileCheck, routeLockfileRepair } from './lockfile.js'
|
|
29
31
|
import { routeEnrich, routeMarketIndex, routeRepo, routeSearch, routeSubpackages } from './market.js'
|
|
32
|
+
import { routeRevokeForce } from './revoke-force.js'
|
|
30
33
|
import { routeAdaptUnlock, routeAdaptUnlockAll, routeCleanResiduals, routeSelfUpdate, routeToggle, routeUninstall } from './plugins.js'
|
|
31
34
|
import { routeSkillRemove, routeSkillToggle, routeSkillsInstalledGet } from './skills.js'
|
|
32
35
|
import { routeGiteeOauthCallbackGet, routeGiteeOauthUrlGet, routeRegistryScan, routeSources, routeSourcesGet } from './sources.js'
|
|
@@ -57,6 +60,8 @@ const ROUTES_EARLY = [
|
|
|
57
60
|
// **任何声明 GET 的路由都必须登记在 ROUTES_EARLY**。该判据已固化进 tests/test-route-inventory.mjs。
|
|
58
61
|
{ methods: ['GET'], path: `${ROUTE_PREFIX}/patch-audit`, handler: routePatchAuditGet },
|
|
59
62
|
{ methods: ['GET'], path: `${ROUTE_PREFIX}/preset-audit`, handler: routePresetAuditGet },
|
|
63
|
+
// 0.5.84 加法(用户第 7 项):磁盘体检**只读** ⇒ 必须住在 405 守卫之前的表里(声明 GET 就要走得到)。
|
|
64
|
+
{ methods: ['GET'], path: `${ROUTE_PREFIX}/disk-audit`, handler: routeDiskAudit },
|
|
60
65
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/framework-relaunch`, handler: routeFrameworkRelaunch },
|
|
61
66
|
|
|
62
67
|
]
|
|
@@ -130,6 +135,7 @@ const ROUTES = [
|
|
|
130
135
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/toggle`, handler: routeToggle },
|
|
131
136
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/uninstall`, handler: routeUninstall },
|
|
132
137
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/adapt-unlock`, handler: routeAdaptUnlock },
|
|
138
|
+
{ methods: ['POST'], path: `${ROUTE_PREFIX}/revoke-force`, handler: routeRevokeForce },
|
|
133
139
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/adapt-unlock-all`, handler: routeAdaptUnlockAll },
|
|
134
140
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/clean-residuals`, handler: routeCleanResiduals },
|
|
135
141
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/self-update`, handler: routeSelfUpdate },
|
|
@@ -159,6 +165,8 @@ const ROUTES = [
|
|
|
159
165
|
// 依赖锁体检与重建(2026-09-27 加法):check 纯只读;repair 必须由用户显式调用
|
|
160
166
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/lockfile-check`, handler: routeLockfileCheck },
|
|
161
167
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/lockfile-repair`, handler: routeLockfileRepair },
|
|
168
|
+
// 0.5.84 加法(用户第 7 项):磁盘清理**默认只回计划**,confirm:true 才删(判据见 domain/disk-audit.js)
|
|
169
|
+
{ methods: ['POST'], path: `${ROUTE_PREFIX}/disk-cleanup`, handler: routeDiskCleanup },
|
|
162
170
|
// 结构化建议动作(2026-09-27 加法):只认白名单动作类型,argv 由服务端自拼(绝不接受命令字符串)
|
|
163
171
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/run-suggested`, handler: routeRunSuggested },
|
|
164
172
|
|
|
@@ -28,6 +28,11 @@ async function routeInstall(req, res, rc) {
|
|
|
28
28
|
try { repo = githubRepoInfo(rawRepo) } catch {}
|
|
29
29
|
}
|
|
30
30
|
const givenName = typeof body.packageName === 'string' ? body.packageName.trim() : ''
|
|
31
|
+
// ★ 0.5.81(用户 2026-10-18):目标版本("更新并适配"必须装它,否则 pnpm 按 latest 原地重装旧版)。
|
|
32
|
+
// 严格限制形态:只接受 semver 样式的版本,别的一律忽略(不把任意字符串塞进 pnpm 规格)。
|
|
33
|
+
const requestedVersion = typeof body.version === 'string' && /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u.test(body.version.trim())
|
|
34
|
+
? body.version.trim()
|
|
35
|
+
: null
|
|
31
36
|
// 框架本体拦截:deepseek-harness 仓库与其根包 @deepseek-ai/dsh-root 是 DSH 框架自身,
|
|
32
37
|
// 作为插件安装会试图构建整个框架源码——直接拒绝并提示
|
|
33
38
|
if (repo === 'deepseek-ai/deepseek-harness'
|
|
@@ -94,6 +99,7 @@ async function routeInstall(req, res, rc) {
|
|
|
94
99
|
subpackages: null,
|
|
95
100
|
lastError: null,
|
|
96
101
|
update: body.update === true,
|
|
102
|
+
version: requestedVersion, // ★ 目标版本(null = 照旧按 latest)
|
|
97
103
|
kind,
|
|
98
104
|
// 子目录插件安装(纯加法:不带 subdir 时这两项就是 null,作业形状与改动前逐字相同)
|
|
99
105
|
subdir,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// L2 · routes —— 撤销「无效的强制启用覆盖」(0.5.76,用户 2026-10-18)
|
|
2
|
+
//
|
|
3
|
+
// 场景:补丁里写了 `disabled: false`(强制开启),但该行运行时根本没加载、也不是 peer 否决造成的
|
|
4
|
+
// —— 这条覆盖是**死的**,留着只会让界面继续显示「强制启用未生效」而毫无作用。
|
|
5
|
+
//
|
|
6
|
+
// 语义(写死):
|
|
7
|
+
// · 只对**确实在 forced 列表**里的行生效 —— 不凭空改别人的补丁;
|
|
8
|
+
// · 复用 patch.js#removeDisableBlock(与 adapt-unlock 同一实现,不写第二份);
|
|
9
|
+
// · 移除后**不承诺"已启用"**:撤掉覆盖只代表不再强压,实际是否加载由框架决定。
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { findPatchPath } from '../infra/paths.js'
|
|
12
|
+
import { removeDisableBlock, readPatchState } from '../domain/patch.js'
|
|
13
|
+
import { sendError, sendJson } from '../infra/httpd.js'
|
|
14
|
+
|
|
15
|
+
async function routeRevokeForce(req, res, rc) {
|
|
16
|
+
const body = rc.body !== null && typeof rc.body === 'object' ? rc.body : {}
|
|
17
|
+
const entryId = typeof body.entryId === 'string' ? body.entryId : ''
|
|
18
|
+
if (entryId === '') { sendError(res, 400, '缺少 entryId'); return }
|
|
19
|
+
const rowId = entryId.includes(':') ? entryId.split(':').slice(1).join(':') : entryId
|
|
20
|
+
let patchPath = null
|
|
21
|
+
try { patchPath = findPatchPath(rc.ctx) } catch { patchPath = null }
|
|
22
|
+
if (typeof patchPath !== 'string' || patchPath === '' || !existsSync(patchPath)) {
|
|
23
|
+
sendError(res, 400, '当前 profile 没有可写的补丁文件'); return
|
|
24
|
+
}
|
|
25
|
+
let forced = []
|
|
26
|
+
try { const state = await readPatchState(patchPath); forced = Array.isArray(state?.forced) ? state.forced : [] } catch { forced = [] }
|
|
27
|
+
if (!forced.includes(rowId)) {
|
|
28
|
+
sendError(res, 400, `${rowId} 当前不是「补丁强制启用」状态(没有 disabled: false 覆盖块),无需撤销`); return
|
|
29
|
+
}
|
|
30
|
+
try { await removeDisableBlock(patchPath, rowId) }
|
|
31
|
+
catch (error) { sendError(res, 500, `撤销失败:${error instanceof Error ? error.message : String(error)}`); return }
|
|
32
|
+
sendJson(res, 200, { ok: true, rowId, revoked: true, note: `已移除 ${rowId} 的强制启用覆盖(disabled: false)。该行是否加载由框架决定,本次未改清单。` })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { routeRevokeForce }
|
|
@@ -17,12 +17,15 @@ import { applyPinDevDir, planPinDevDir } from '../domain/pin-dev-dir.js'
|
|
|
17
17
|
import { reconcileLockfile } from '../domain/selfupdate.js'
|
|
18
18
|
import { ensureLinkSelfSufficiency } from '../domain/link-self-sufficiency.js'
|
|
19
19
|
import { readBundlesSync } from '../domain/persist.js'
|
|
20
|
+
// 0.5.84 加法:清单 ↔ lock 口径不一致的**后台自动对账**(见 domain/lock-autofix.js 顶部三条硬约束)
|
|
21
|
+
import { lockAutofixStatus, scheduleLockAutofix } from '../domain/lock-autofix.js'
|
|
20
22
|
import { failedJobEvidence } from '../domain/install-verify.js'
|
|
21
23
|
import { installJobView, readGithubAuth } from '../domain/install.js'
|
|
22
24
|
import { readPluginDetails } from '../domain/market.js'
|
|
23
25
|
import { healPatchSafety, readPatchState } from '../domain/patch.js'
|
|
24
26
|
import { pendingRestartJobs } from '../domain/revoke.js'
|
|
25
27
|
import { mergePendingRestart } from '../domain/pending-restart.js'
|
|
28
|
+
import { recordInstallJob, readInstallJobs } from '../domain/install-jobs-store.js'
|
|
26
29
|
import { listEntries } from '../domain/runtime.js'
|
|
27
30
|
import { sendError, sendJson } from '../infra/httpd.js'
|
|
28
31
|
import { dshHome, entryPkgMeta, findPatchPath, pluginRoot, profileDirOf, rowIdOf } from '../infra/paths.js'
|
|
@@ -98,7 +101,7 @@ async function routeStateGet(req, res, rc) {
|
|
|
98
101
|
repository: meta?.repository ?? null,
|
|
99
102
|
// v0.3.45:只有"补丁里此刻确实还禁着它"的行才显示【待适配】——
|
|
100
103
|
// 记录与开关脱节时(用户手动启用过 / 补丁块被清掉)不能继续挂着「待适配」误导人
|
|
101
|
-
pendingCompat: compatPendingRows.has(entry.rowId) && disabledByPatch,
|
|
104
|
+
pendingCompat: (compatPendingRows.has(entry.rowId) && disabledByPatch) || veto !== null,
|
|
102
105
|
// 0.5.34 加法:记录的 `fail` 若已被**当前判据**撤回(旧误报),面板据此不再说「待适配」,
|
|
103
106
|
// 改说「判据已撤回·可解锁」并给一键解锁入口(判据本身一个字都没放宽:真 fail 仍 fail)
|
|
104
107
|
compatRescinded: compatPendingRows.has(entry.rowId) && verdict?.rescinded === true,
|
|
@@ -109,6 +112,14 @@ async function routeStateGet(req, res, rc) {
|
|
|
109
112
|
adoptable: adoptable.get(entry.rowId) ?? null,
|
|
110
113
|
// 框架 peer 预检的本地复算结果(null = 无话可说;只在这类行上下发,不新增常驻面板行)
|
|
111
114
|
veto,
|
|
115
|
+
// ★ 0.5.76(用户 2026-10-18):**禁用出处** + 无效强制覆盖的可见性(只加字段,不改既有判定)
|
|
116
|
+
// 'patch' = 本控制台补丁禁的(删块即可正常启用)
|
|
117
|
+
// 'bundle' = 插件自身/框架默认禁的(我们只能写 disabled:false 覆盖 ⇒ 补丁强制启用)
|
|
118
|
+
disableSource: !entry.enabled && entry.fiberPhase === null
|
|
119
|
+
? (disabledByPatch ? 'patch' : 'bundle')
|
|
120
|
+
: null,
|
|
121
|
+
// 强制开启写了、但运行时仍没起来,且不是 peer 否决造成的 ⇒ 这条覆盖是**无效的**,要说出来
|
|
122
|
+
forcedNotLoaded: patch.forced.includes(entry.rowId) && entry.enabled !== true && veto === null,
|
|
112
123
|
}
|
|
113
124
|
})
|
|
114
125
|
const compat = await detectCompat(ctx.baseUrl ?? 'file:///')
|
|
@@ -138,6 +149,13 @@ async function routeStateGet(req, res, rc) {
|
|
|
138
149
|
} catch {}
|
|
139
150
|
return set
|
|
140
151
|
})()
|
|
152
|
+
// ★ 0.5.83(用户 2026-10-18):作业结束后**落盘一条记录**(只记 done/failed,幂等),
|
|
153
|
+
// 宿主重启后仍能查到"上次更新干了什么、为什么失败"。写盘失败一律吞掉,绝不影响本接口。
|
|
154
|
+
try {
|
|
155
|
+
for (const job of installJobs.values()) {
|
|
156
|
+
if (job?.status === 'done' || job?.status === 'failed') recordInstallJob(job)
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
141
159
|
let pendingRestart = []
|
|
142
160
|
try { pendingRestart = mergePendingRestart(pendingRestartJobs([...installJobs.values()], listEntries(ctx)), livePackageNames) } catch { pendingRestart = pendingRestartJobs([...installJobs.values()], listEntries(ctx)) }
|
|
143
161
|
// 框架升级检测与适配(备份快照 + 重打框架补丁),try 包裹不阻塞 state 返回
|
|
@@ -234,7 +252,19 @@ async function routeStateGet(req, res, rc) {
|
|
|
234
252
|
patchUncertain: patchHeal === null ? [] : (patchHeal.uncertain ?? []),
|
|
235
253
|
})
|
|
236
254
|
} catch {}
|
|
237
|
-
|
|
255
|
+
// ★ 0.5.84 加法(用户 2026-10):**后台自动对账 lock**。
|
|
256
|
+
// 为什么放在 /state:这里是唯一"每次开面板都会被调"的地方 —— 于是"清单与 lock 对不上"能被
|
|
257
|
+
// 及时发现,而不必等用户自己去点「重建 lock」。
|
|
258
|
+
// 为什么**不 await**:pnpm 是墙钟动作(实测 1.5s 起步、最坏分钟级),等它就会把 /state 拖死。
|
|
259
|
+
// 冷却窗 / 失败停手 / 在跑去重都在 domain/lock-autofix.js 里,这里只负责"问一句、不挡路"。
|
|
260
|
+
let lockAutofix = null
|
|
261
|
+
try {
|
|
262
|
+
const kicked = scheduleLockAutofix({ profileDir, registries: [] })
|
|
263
|
+
lockAutofix = { ...lockAutofixStatus({ profileDir }), scheduled: kicked.started, skippedReason: kicked.started ? null : kicked.reason }
|
|
264
|
+
} catch (error) {
|
|
265
|
+
lockAutofix = { error: error instanceof Error ? error.message : String(error), scheduled: false }
|
|
266
|
+
}
|
|
267
|
+
sendJson(res, 200, { ok: true, entries: allEntries, skippedBundles, host, health, patchPath, compat, installJobs: jobs, recentFailures, pendingRestart, github: { loggedIn: auth.loggedIn, login: auth.login }, patch: { disables: patch.disables, forced: patch.forced, inserts: patch.inserts }, framework, lockAutofix, patchHeal: patchHeal === null ? null : { healed: patchHeal.healed ?? [], autoDisabled: patchHeal.autoDisabled ?? [], healedAt: patchHeal.healedAt ?? 0, written: patchHeal.written === true, writeError: patchHeal.writeError ?? null, skippedFrameworkOwned: (patchHeal.skipped ?? []).map((s) => s.id ?? null), uncertain: (patchHeal.uncertain ?? []).map((u) => u.id ?? null) }, gating: gatingSummary(ctx, compatPending, entries, compatVerdicts), compatPending: compatPending === null ? null : { frameworkVersion: compatPending.frameworkVersion ?? null, upgradeFrom: compatPending.upgradeFrom ?? null, pending: (compatPending.pending ?? []).filter((p) => (p.status ?? 'pending') === 'pending').map((p) => ({ rowId: p.rowId, moduleName: p.moduleName, version: p.version ?? null, checkNote: p.checkNote ?? null, check: p.check ?? null, riskyApprovedAt: p.riskyApprovedAt ?? null, adoptable: adoptable.get(p.rowId) ?? null, verdict: compatVerdicts.get(p.rowId) ?? null })), verdictRefresh: pendingState.refresh === null ? null : { version: pendingState.refresh.version ?? null, updated: pendingState.refresh.updated ?? [], unchanged: pendingState.refresh.unchanged ?? [], unresolved: pendingState.refresh.unresolved ?? [], wrote: pendingState.refresh.wrote === true, writeError: pendingState.refresh.writeError ?? null } }, compatGate, rollback: rollbackRec, selfVersion, components: findComponents().map((c) => ({ id: c.id, name: c.name, kind: c.kind ?? 'server', pid: c.pid ?? null, port: c.port ?? null, healthUrl: c.healthUrl ?? null, uiUrl: compUiUrl(c), autoStart: c.autoStart === true })) })
|
|
238
268
|
return
|
|
239
269
|
}
|
|
240
270
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noob-stupid/dsh-plugin-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.85",
|
|
4
4
|
"description": "DSH 框架升级安全与插件升级门控:一键升级、失败自动回滚、升级后回滚上版、旧插件不适配自动禁用;内置多源插件市场为发现层,插件源全部可自定义,可指向公司内网私有源 / 私有索引 / 本地 Git 仓库,纯内网离线可用 | Framework upgrade safety & plugin version gating for DSH, with a customizable multi-source plugin market: point every source at internal mirrors or a local file:// repo for intranet-only, offline installs.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|