@noob-stupid/dsh-plugin-console 0.5.29 → 0.5.31
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/README.md +7 -7
- package/README.zh.md +8 -8
- package/SECURITY.md +2 -2
- package/lib/client.js +91 -5
- package/lib/index.js +141 -141
- package/lib/server/domain/ai-run.js +1 -1
- package/lib/server/domain/ai.js +39 -11
- package/lib/server/domain/compat.js +69 -3
- package/lib/server/domain/components.js +1 -1
- package/lib/server/domain/dep-source.js +1 -1
- package/lib/server/domain/framework-cleanup.js +225 -0
- package/lib/server/domain/framework-residuals.js +59 -0
- package/lib/server/domain/framework.js +65 -6
- package/lib/server/domain/install-job.js +1 -1
- package/lib/server/domain/install.js +1 -1
- package/lib/server/domain/jobs.js +1 -1
- package/lib/server/domain/market.js +1 -1
- package/lib/server/domain/patch-composition-audit.js +218 -0
- package/lib/server/domain/patch.js +69 -16
- package/lib/server/domain/preset-audit.js +144 -0
- package/lib/server/domain/presets.js +1 -1
- package/lib/server/domain/release-source.js +3 -3
- package/lib/server/domain/repoland.js +1 -1
- package/lib/server/domain/runtime.js +1 -1
- package/lib/server/domain/skills.js +1 -1
- package/lib/server/domain/sources.js +1 -1
- package/lib/server/domain/suite.js +1 -1
- package/lib/server/infra/exec.js +24 -3
- package/lib/server/infra/framework-root.js +99 -0
- package/lib/server/infra/fsx.js +3 -3
- package/lib/server/infra/http.js +1 -1
- package/lib/server/infra/httpd.js +1 -1
- package/lib/server/infra/mask.js +1 -1
- package/lib/server/infra/package-resolve.js +73 -0
- package/lib/server/infra/paths.js +1 -1
- package/lib/server/infra/registry-versions.js +64 -0
- package/lib/server/infra/semver.js +1 -1
- package/lib/server/routes/compat.js +41 -1
- package/lib/server/routes/components.js +1 -1
- package/lib/server/routes/framework-preflight.js +43 -2
- package/lib/server/routes/framework-upgrade.js +16 -17
- package/lib/server/routes/framework.js +18 -2
- package/lib/server/routes/github-login.js +2 -2
- package/lib/server/routes/index.js +17 -2
- package/lib/server/routes/patch-audit.js +59 -0
- package/lib/server/routes/plugins.js +20 -2
- package/lib/server/routes/preset-audit.js +24 -0
- package/lib/server/routes/state.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// L0 · infra —— framework-root.js(框架运行时解析基准;0.5.30 加法)
|
|
2
|
+
//
|
|
3
|
+
// 为什么必须有它(**真机事故 2026-09-28 / 2026-09-29**):
|
|
4
|
+
// 框架 0.2.0-rc.1 把 `@deepseek-ai/*` 的实体全部搬进 `.pnpm` **内部**那层,profile 顶层
|
|
5
|
+
// projection 还停在旧版(本机实测:顶层 239 个 `@deepseek-ai` 目录仍是 0.1.5-rc.2 的残影)。
|
|
6
|
+
// 任何"只拿 profileDir 当解析基准"的判断都会把**真存在的框架内置包**(`dsh-agent-preset`、
|
|
7
|
+
// `dsh-workflow-ptc`)判成"缺失" —— 实测后果:`healPatchSafety()` 给它们自动写了 4 个
|
|
8
|
+
// `disabled: true` 块(2026-09-29 01:05:44),把用户的 preset-router-* 三条全停掉。
|
|
9
|
+
//
|
|
10
|
+
// 所以「框架内置包在不在」必须走**多基准并集**:任一基准能解析即算命中。
|
|
11
|
+
// 本模块把该判据集中一处,供只读体检(routes/patch-audit.js)与补丁自愈(domain/patch.js)共用 ——
|
|
12
|
+
// 两处若各写一份,下次框架再挪目录就会只有一处被修好。
|
|
13
|
+
//
|
|
14
|
+
// 行数预算:独立小模块,留在 600 行架构硬顶内。
|
|
15
|
+
|
|
16
|
+
import { existsSync } from 'node:fs'
|
|
17
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path'
|
|
18
|
+
import { resolvePackageJson } from './paths.js'
|
|
19
|
+
|
|
20
|
+
/** 框架命名空间:`@deepseek-ai/*` 随框架版本走,承载者永远是框架安装树。 */
|
|
21
|
+
const FRAMEWORK_SCOPE = '@deepseek-ai/'
|
|
22
|
+
|
|
23
|
+
/** 包名归一:`@scope/name/sub` → `@scope/name`;裸名原样。 */
|
|
24
|
+
function basePackageName(moduleName) {
|
|
25
|
+
const text = String(moduleName ?? '')
|
|
26
|
+
if (text.startsWith('@')) {
|
|
27
|
+
const parts = text.split('/')
|
|
28
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : text
|
|
29
|
+
}
|
|
30
|
+
return text.split('/')[0]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 归一后的包名是否属于框架命名空间(`@deepseek-ai/dsh-*`)。 */
|
|
34
|
+
function isFrameworkModuleName(moduleName) {
|
|
35
|
+
const name = basePackageName(moduleName)
|
|
36
|
+
return name === FRAMEWORK_SCOPE.slice(0, -1) || name.startsWith(FRAMEWORK_SCOPE)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 路径是否落在给定根目录内(大小写不敏感,Windows 与 POSIX 都成立)。 */
|
|
40
|
+
function isUnder(childPath, rootDir) {
|
|
41
|
+
if (typeof childPath !== 'string' || childPath === '' || typeof rootDir !== 'string' || rootDir === '') return false
|
|
42
|
+
let child = resolve(childPath).toLowerCase()
|
|
43
|
+
let root = resolve(rootDir).toLowerCase()
|
|
44
|
+
if (!root.endsWith(sep)) root += sep
|
|
45
|
+
if (!child.endsWith(sep)) child += sep
|
|
46
|
+
return child.startsWith(root)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 框架运行时解析基准(**有序**,先准后宽):
|
|
51
|
+
* ① `@deepseek-ai/dsh` 所在的那层 `node_modules`(= `.pnpm` 实体内部,框架内置包的真正家)
|
|
52
|
+
* ② 从 ① 向上逐级找到的第一个含 `.pnpm` 的目录(与 ① 同层时去重)
|
|
53
|
+
* ③ `@deepseek-ai/dsh` 解析不出来时的兜底:`baseDir` / `profileDir` 自身(供离线测试桩使用)
|
|
54
|
+
* 返回去重后的绝对路径数组;一个都拿不到时返回 `[]`(调用方必须把空数组当"无法判定"而非"缺失")。
|
|
55
|
+
*/
|
|
56
|
+
function frameworkBases(baseDir, profileDir) {
|
|
57
|
+
const out = []
|
|
58
|
+
const push = (dir) => {
|
|
59
|
+
if (typeof dir !== 'string' || dir === '' || !isAbsolute(dir)) return
|
|
60
|
+
const norm = resolve(dir)
|
|
61
|
+
if (!out.includes(norm)) out.push(norm)
|
|
62
|
+
}
|
|
63
|
+
let found = null
|
|
64
|
+
// base 有两种合法写法(`…/profiles/web` 这种**含 node_modules 的目录**,或 `…/.pnpm/<pkg>/node_modules`
|
|
65
|
+
// 这种 **node_modules 本身**);resolvePackageJson 只认前者,所以后者要退一级再试 ——
|
|
66
|
+
// 否则框架基准这一路永远解析不到 `@deepseek-ai/dsh`,整个框架基准列表会退化成兜底值。
|
|
67
|
+
for (const base of [baseDir, profileDir]) {
|
|
68
|
+
if (typeof base !== 'string' || base === '') continue
|
|
69
|
+
for (const cand of [base, dirname(base)]) {
|
|
70
|
+
if (typeof cand !== 'string' || cand === '') continue
|
|
71
|
+
try {
|
|
72
|
+
found = resolvePackageJson('@deepseek-ai/dsh', cand, profileDir)
|
|
73
|
+
} catch { found = null }
|
|
74
|
+
if (found !== null) break
|
|
75
|
+
}
|
|
76
|
+
if (found !== null) break
|
|
77
|
+
}
|
|
78
|
+
if (found !== null) {
|
|
79
|
+
// ★ 2026-09-30 改错(本批修 A 事故时抓到):这里过去是 `dirname(dirname(pkgPath))` ——
|
|
80
|
+
// pkgPath 形如 `<…>/node_modules/@deepseek-ai/dsh/package.json`,两层 dirname 只退到
|
|
81
|
+
// **scope 目录** `<…>/node_modules/@deepseek-ai`(实测 `existsSync(inner@deepseek-ai)` = false),
|
|
82
|
+
// 拿它当解析基准必然解析不到任何包。正确是**三层**:package.json → dsh → @deepseek-ai → node_modules。
|
|
83
|
+
const inner = dirname(dirname(dirname(found)))
|
|
84
|
+
push(inner)
|
|
85
|
+
let dir = inner
|
|
86
|
+
for (let i = 0; i < 6; i += 1) {
|
|
87
|
+
if (existsSync(join(dir, '.pnpm'))) { push(dir); break }
|
|
88
|
+
const parent = dirname(dir)
|
|
89
|
+
if (parent === dir) break
|
|
90
|
+
dir = parent
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 兜底:框架树找不到时,至少把 profile 目录本身当基准 —— 否则 `bases` 为空会让
|
|
94
|
+
// 自愈把所有 insert 行都判成"无法确认"(宁缺勿滥,但那会让真缺失的行不再被停用)。
|
|
95
|
+
if (out.length === 0) { push(profileDir); push(baseDir) }
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { FRAMEWORK_SCOPE, basePackageName, frameworkBases, isFrameworkModuleName, isUnder }
|
package/lib/server/infra/fsx.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// 由 Step 1 搬运工具从 lib/index.js 原样切出(只移动、未改逻辑)
|
|
2
|
-
//
|
|
2
|
+
// 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
3
|
|
|
4
4
|
import { existsSync, rmSync, readdirSync, mkdirSync, copyFileSync, chmodSync, lstatSync, renameSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, basename } from 'node:path'
|
|
@@ -66,9 +66,9 @@ let writeQueue = Promise.resolve()
|
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
68
|
* 删除目录树并**核实删除结果**。
|
|
69
|
-
*
|
|
69
|
+
* 为什么要核实:某些环境(受限令牌/沙箱/杀软占用)下 `rmSync` 会**静默落空**——不抛错、目录仍在。
|
|
70
70
|
* 路由若删完直接 `{ok:true}` 就是对用户撒谎(2026-09-20 多类型演练实测:同一个 `rmSync` 在
|
|
71
|
-
* `
|
|
71
|
+
* 短路径(如 `C:\repos`)删得掉,在 `C:\Users\<user>\.dsh\...` 下返回成功但目录原封不动)。
|
|
72
72
|
* 返回 `{ok, attempts, error}`;`ok:false` 时调用方必须如实报错,不能吞。
|
|
73
73
|
*/
|
|
74
74
|
function removeDirVerified(dir) {
|
package/lib/server/infra/http.js
CHANGED
package/lib/server/infra/mask.js
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// L0 · infra —— package-resolve.js(Node 解析器封装:包名可解析性 + 可用包名枚举;0.5.30 加法)
|
|
2
|
+
//
|
|
3
|
+
// 为什么不用「store 目录名」判断包在不在(真机教训 2026-09-28):
|
|
4
|
+
// pnpm 会**截断超长目录名**(`@deepseek-ai+dsh-workflow-ptc` → `@deepseek-ai+dsh-workflow-p_<hash>`),
|
|
5
|
+
// 按目录名前缀匹配会把**存在的包**误判成不存在 —— 本机实测:`dsh-workflow-ptc`、`dsh-agent-preset`
|
|
6
|
+
// 两个真存在的包都被目录名法误报。所以体检一律走 **Node 自己的解析器**(与框架加载补丁行同一套解析),
|
|
7
|
+
// 并在多个基准上取并集:① profile 目录(第三方插件在这里)② 框架运行时根(`@deepseek-ai/*` 在这里)。
|
|
8
|
+
|
|
9
|
+
import { createRequire } from 'node:module'
|
|
10
|
+
import { existsSync, readdirSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 基准目录 → `createRequire` 的探针位置。
|
|
15
|
+
* base 有两种合法写法,本函数把两者都收下(判据:base 自己是不是就叫 node_modules):
|
|
16
|
+
* · base 是**含 node_modules 的目录**(如 profile 目录 `<…>/profiles/web`)→ 探针 `<base>/__patch_audit_probe__.js`
|
|
17
|
+
* · base 本身就是 **node_modules 目录**(如 `…/.pnpm/@deepseek-ai+dsh@…/node_modules`)→ 探针放它内部
|
|
18
|
+
* 为什么必须兼容第二种:框架内置包的实体只存在于 `.pnpm/<pkg>/node_modules/` 这一层,
|
|
19
|
+
* 说"框架基准"时最自然的写法就是那层本身;若一律拼 `<base>/node_modules/…`,
|
|
20
|
+
* 就会去找 `<…>/node_modules/node_modules` —— **真存在的包被判成不存在**(本套 ① 段即是回归)。
|
|
21
|
+
*/
|
|
22
|
+
function probePathOf(base) {
|
|
23
|
+
return base.replace(/[\\/]+$/u, '').toLowerCase().endsWith('node_modules')
|
|
24
|
+
? join(base, '__patch_audit_probe__.js')
|
|
25
|
+
: join(base, 'node_modules', '__patch_audit_probe__.js')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 造多基准解析器:任一基准能解析即算命中(`from` 如实回带,便于报告里说明"从哪解析到的")。 */
|
|
29
|
+
function makePackageResolver(bases) {
|
|
30
|
+
const reqs = []
|
|
31
|
+
for (const base of (Array.isArray(bases) ? bases : [])) {
|
|
32
|
+
if (typeof base !== 'string' || base === '') continue
|
|
33
|
+
try { reqs.push(createRequire(probePathOf(base))) } catch {}
|
|
34
|
+
}
|
|
35
|
+
return (name) => {
|
|
36
|
+
for (const req of reqs) {
|
|
37
|
+
try { return { ok: true, from: req.resolve(name) } } catch {}
|
|
38
|
+
}
|
|
39
|
+
return { ok: false, reason: '所有基准都解析不到(包可能已被移除/改名,或该子路径不存在)' }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 枚举一个 node_modules 目录下可见的包名(含 scope 内一层)。 */
|
|
44
|
+
function listPackageNames(nodeModulesDir, limit = 4000) {
|
|
45
|
+
const out = []
|
|
46
|
+
try {
|
|
47
|
+
for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
|
|
48
|
+
if (out.length >= limit) break
|
|
49
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
50
|
+
if (entry.name.startsWith('@')) {
|
|
51
|
+
for (const sub of readdirSync(join(nodeModulesDir, entry.name), { withFileTypes: true })) {
|
|
52
|
+
if (out.length >= limit) break
|
|
53
|
+
if (sub.isDirectory() && !sub.name.startsWith('.')) out.push(`${entry.name}/${sub.name}`)
|
|
54
|
+
}
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
out.push(entry.name)
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 合并多个 node_modules 目录下的包名(去重)—— 供「疑似改名」当候选集。 */
|
|
64
|
+
function listAvailablePackages(dirs) {
|
|
65
|
+
const set = new Set()
|
|
66
|
+
for (const dir of (Array.isArray(dirs) ? dirs : [])) {
|
|
67
|
+
if (typeof dir !== 'string' || dir === '' || !existsSync(dir)) continue
|
|
68
|
+
for (const name of listPackageNames(dir)) set.add(name)
|
|
69
|
+
}
|
|
70
|
+
return [...set]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { listAvailablePackages, listPackageNames, makePackageResolver }
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// L0 · infra(paths.js)—— 分层 Step 从 lib/index.js 搬出,只搬移未改逻辑
|
|
2
|
-
//
|
|
2
|
+
// 分层分组:L0 · infra(边界由 tests/test-architecture-guard.mjs 断言)
|
|
3
3
|
|
|
4
4
|
import { readFileSync, existsSync, statSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// L0 · infra —— registry-versions.js(registry 元数据探针:包名 → 某版本是否已发布;0.5.30 加法)
|
|
2
|
+
//
|
|
3
|
+
// 用途:**升级前**核对「profile 补丁里引用的框架包,在目标版本里还在不在」。
|
|
4
|
+
// 真机事故 2026-09-28:0.2.0-rc.1 移除了 `@deepseek-ai/dsh-workflow-worker-thread`(引擎改为 ptc),
|
|
5
|
+
// 补丁仍指向旧包 → 升级后 `workflowEngine` 无提供方 → 老会话 resume 失败。
|
|
6
|
+
// 这类「官方移除/改名」在升级前**只能用 registry 元数据看见**(目标版本还没装到本机)。
|
|
7
|
+
//
|
|
8
|
+
// 三个取舍:
|
|
9
|
+
// ① 同一次预检里**同一个包只查一次**(内存缓存)—— 补丁里同名包可能出现在多处;
|
|
10
|
+
// ② 网络失败/元数据缺失 → 返回 **null(未知)**,绝不当作「不存在」;
|
|
11
|
+
// ③ 小并发(默认 4)+ 单请求超时,避免预检被 30 个包串行拖死。
|
|
12
|
+
|
|
13
|
+
import { fetchJsonUrl } from './http.js'
|
|
14
|
+
|
|
15
|
+
/** 包名 → URL 片段(`@a/b` → `@a%2fb`)。 */
|
|
16
|
+
function encodePackageName(name) {
|
|
17
|
+
return String(name).replace('/', '%2f')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 造版本探针:`(name) => { ok, versions?, error? }`(带内存缓存)。 */
|
|
21
|
+
function makeVersionProbe({ registry, fetchJson, timeoutMs = 9000, cache = new Map() } = {}) {
|
|
22
|
+
const fetchImpl = typeof fetchJson === 'function' ? fetchJson : (url, t) => fetchJsonUrl(url, t)
|
|
23
|
+
return async (name) => {
|
|
24
|
+
if (cache.has(name)) return cache.get(name)
|
|
25
|
+
let result
|
|
26
|
+
try {
|
|
27
|
+
const meta = await fetchImpl(`${registry}/${encodePackageName(name)}`, timeoutMs)
|
|
28
|
+
const versions = meta && typeof meta === 'object' && meta.versions !== null && typeof meta.versions === 'object'
|
|
29
|
+
? Object.keys(meta.versions)
|
|
30
|
+
: []
|
|
31
|
+
result = { ok: true, versions }
|
|
32
|
+
} catch (error) {
|
|
33
|
+
result = { ok: false, error: String(error?.message ?? error) }
|
|
34
|
+
}
|
|
35
|
+
cache.set(name, result)
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 批量核对:`[{name, lines}]` → `[{name, lines, published: true|false|null}]`(小并发,保序)。 */
|
|
41
|
+
async function checkPublishedVersions(entries, { probe, version, concurrency = 4 } = {}) {
|
|
42
|
+
const list = Array.isArray(entries) ? entries : []
|
|
43
|
+
const width = Math.max(1, Number.isFinite(concurrency) && concurrency > 0 ? Math.floor(concurrency) : 4)
|
|
44
|
+
const out = new Array(list.length)
|
|
45
|
+
let cursor = 0
|
|
46
|
+
const worker = async () => {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const index = cursor
|
|
49
|
+
cursor += 1
|
|
50
|
+
if (index >= list.length) return
|
|
51
|
+
const entry = list[index]
|
|
52
|
+
let published = null
|
|
53
|
+
try {
|
|
54
|
+
const res = await probe(entry.name)
|
|
55
|
+
if (res?.ok === true) published = Array.isArray(res.versions) ? res.versions.includes(version) : null
|
|
56
|
+
} catch { published = null }
|
|
57
|
+
out[index] = { name: entry.name, lines: Array.isArray(entry.lines) ? entry.lines : [], published }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await Promise.all(Array.from({ length: Math.min(width, list.length) }, () => worker()))
|
|
61
|
+
return out.filter((x) => x !== undefined)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { checkPublishedVersions, encodePackageName, makeVersionProbe }
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { dirname } from 'node:path'
|
|
15
15
|
import { compareFingerprint, computeFingerprint, readCompatMode, readFingerprint, readRollbackRecord, stampFingerprint, writeCompatMode } from '../domain/compat-state.js'
|
|
16
|
+
import { NOTICE_MODES, detectCompat, readCompatAck, readCompatGate, writeCompatAck, writeCompatGate } from '../domain/compat.js'
|
|
16
17
|
import { currentFrameworkVersion } from '../domain/framework.js'
|
|
17
18
|
import { applyDependencyBackfill, planDependencyBackfill } from '../domain/manifest.js'
|
|
18
19
|
import { preferredRegistries } from '../domain/sources.js'
|
|
@@ -127,4 +128,43 @@ async function routeDeclareInstalled(req, res, rc) {
|
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
/** 兼容提示:手动关闭 / 设定展示模式(2026-09-28 加法)。
|
|
132
|
+
*
|
|
133
|
+
* 背景(用户实测):面板顶部那句「当前 DSH web 包版本 X 不在受支持的 0.1.x 系列内…」是**披露**,
|
|
134
|
+
* 但它以前**关不掉** —— 纯 `<p>`、没有关闭入口、也没有持久化。用户诉求:可以手动关。
|
|
135
|
+
*
|
|
136
|
+
* 两种入参(互斥,按字段判断):
|
|
137
|
+
* · `{ webAppVersion }` → 记住"这个版本我看过了":同版本不再提示,**框架版本一变重新提示**
|
|
138
|
+
* · `{ mode }` → 展示模式 `always` / `once-per-version`(默认)/ `off`
|
|
139
|
+
*
|
|
140
|
+
* ⚠ 硬约束:**只影响展示,不改 `supported`** —— 门控判定与"该不该拦"一个字都不变,
|
|
141
|
+
* 披露不能被关成"看不见的风险"。版本还会校验:只接受**当前探测到**的那一个,
|
|
142
|
+
* 避免把 ack 记到别的版本上(否则升到新版本时会被误吞)。
|
|
143
|
+
*/
|
|
144
|
+
async function routeCompatAck(req, res, rc) {
|
|
145
|
+
const body = rc.body ?? {}
|
|
146
|
+
if (typeof body.mode === 'string') {
|
|
147
|
+
const mode = body.mode.trim().toLowerCase()
|
|
148
|
+
if (!NOTICE_MODES.includes(mode)) {
|
|
149
|
+
sendError(res, 400, `未知的提示模式:${body.mode}(可选 ${NOTICE_MODES.join(' / ')})`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
const gate = writeCompatGate({ noticeMode: mode })
|
|
153
|
+
sendJson(res, 200, { ok: true, noticeMode: gate.noticeMode, ack: readCompatAck() })
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
const version = typeof body.webAppVersion === 'string' ? body.webAppVersion.trim() : ''
|
|
157
|
+
if (version === '' || !/^\d+\.\d+\.\d+/u.test(version)) {
|
|
158
|
+
sendError(res, 400, 'webAppVersion 缺失或格式不对(形如 0.2.0-rc.1)')
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
let detected = null
|
|
162
|
+
try { detected = (await detectCompat(rc?.ctx?.baseUrl ?? 'file:///')).webAppVersion } catch {}
|
|
163
|
+
if (detected !== null && detected !== version) {
|
|
164
|
+
sendError(res, 400, `版本不匹配:当前探测到 ${detected},请求里是 ${version}(请刷新后重试)`)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
const ack = writeCompatAck(version)
|
|
168
|
+
sendJson(res, 200, { ok: true, ack, noticeMode: readCompatGate().noticeMode })
|
|
169
|
+
}
|
|
170
|
+
export { routeCompatAck, routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled }
|
|
@@ -109,7 +109,7 @@ async function routeRepoLandConfig(req, res, rc) {
|
|
|
109
109
|
const body = rc.body
|
|
110
110
|
const dir = typeof body.dir === 'string' ? body.dir.trim() : ''
|
|
111
111
|
if (dir === '' || !/^[A-Za-z]:[\\/]/.test(dir) && !/^\\\\/.test(dir)) {
|
|
112
|
-
// 0.5.28
|
|
112
|
+
// 0.5.28 改错:示例原本写成本机真实目录(把开发者的目录当成了通用示例)。
|
|
113
113
|
// 换成中性的任意绝对路径示例,并说明"任意绝对路径"——判据是形状(盘符/UNC),不是具体目录。
|
|
114
114
|
sendError(res, 400, '保存路径不合法:请提供绝对路径(任意绝对路径均可,如 C:\\repos 或 \\\\server\\share\\repos)')
|
|
115
115
|
return
|
|
@@ -7,17 +7,20 @@
|
|
|
7
7
|
// 「一发消息就报错、会话整体不可用」。包级适配门(peerDependencies / 版本号)看不见这类
|
|
8
8
|
// 运行期契约破坏,所以必须在**升级前**单独预检、先把本地适配好再装。
|
|
9
9
|
|
|
10
|
-
import { readFileSync } from 'node:fs'
|
|
10
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
11
11
|
import { dirname, join } from 'node:path'
|
|
12
12
|
import { isFrameworkOwnedPackage } from '../domain/compat.js'
|
|
13
|
+
import { classifyTargetAvailability, frameworkPackageRows, parsePatchRows } from '../domain/patch-composition-audit.js'
|
|
14
|
+
import { collectPresetAudit } from '../domain/preset-audit.js'
|
|
13
15
|
import { loadContractRules, summarizeContractRules } from '../domain/contract-rules.js'
|
|
14
16
|
import { discoverFormatContract, summarizeContract } from '../domain/format-contract.js'
|
|
15
17
|
import { applyFormatPatch, collectProducerTargets, scanProducerFiles } from '../domain/format-scan.js'
|
|
16
18
|
import { currentFrameworkVersion } from '../domain/framework.js'
|
|
17
19
|
import { listEntries } from '../domain/runtime.js'
|
|
18
20
|
import { curlText, fetchJsonUrl } from '../infra/http.js'
|
|
21
|
+
import { checkPublishedVersions, makeVersionProbe } from '../infra/registry-versions.js'
|
|
19
22
|
import { sendError, sendJson } from '../infra/httpd.js'
|
|
20
|
-
import { baseDirOf, dshHome, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
|
|
23
|
+
import { baseDirOf, dshHome, findPatchPath, pluginRoot, profileDirOf, resolvePackageJson } from '../infra/paths.js'
|
|
21
24
|
|
|
22
25
|
const REGISTRY = 'https://registry.npmmirror.com'
|
|
23
26
|
|
|
@@ -109,6 +112,42 @@ async function routeFrameworkPreflight(req, res, rc) {
|
|
|
109
112
|
const rules = contract?.rules ?? { producerKindRequired: false, forbiddenKinds: ['plugin'], pluginPrefix: 'plugin:', renames: {} }
|
|
110
113
|
const scan = scanProducerFiles({ targets, rules, targetVersion })
|
|
111
114
|
|
|
115
|
+
// 补丁 ↔ 目标版本核对(2026-09-28 加法;真机事故:0.2.0-rc.1 移除了
|
|
116
|
+
// `@deepseek-ai/dsh-workflow-worker-thread`,补丁三处仍引用 → 升级后 `workflowEngine`
|
|
117
|
+
// 没有提供方 → `dsh-tool-workflow`/`dsh-tool-ralph` 永久 waiting → 老会话 resume 失败)。
|
|
118
|
+
// 只核对**随框架版本走**的包(`@deepseek-ai/*`):目标版本还没装到本机,只有 registry 元数据能提前看见。
|
|
119
|
+
// 网络不可用/查询失败一律记 `unknown`,**绝不判死**。
|
|
120
|
+
let patchTarget = { checked: 0, missing: [], unknown: [], skipped: null, patchPath: null }
|
|
121
|
+
try {
|
|
122
|
+
const patchPath = findPatchPath(rc.ctx)
|
|
123
|
+
patchTarget.patchPath = patchPath
|
|
124
|
+
if (patchPath === null || !existsSync(patchPath)) {
|
|
125
|
+
patchTarget.skipped = '找不到 profile 补丁文件(cordis.patch.yml)'
|
|
126
|
+
} else {
|
|
127
|
+
const rows = parsePatchRows(readFileSync(patchPath, 'utf8'))
|
|
128
|
+
const fwRows = frameworkPackageRows(rows)
|
|
129
|
+
if (fwRows.length === 0) patchTarget.skipped = '补丁里没有随框架版本走的包(@deepseek-ai/*)'
|
|
130
|
+
else if (!networkAllowed()) patchTarget.skipped = '已跳过网络(DSH_TEST_SKIP_NETWORK=1)'
|
|
131
|
+
else {
|
|
132
|
+
const probe = makeVersionProbe({ registry: REGISTRY, fetchJson: (url, timeoutMs) => fetchJsonUrl(url, timeoutMs) })
|
|
133
|
+
const results = await checkPublishedVersions(fwRows, { probe, version: targetVersion })
|
|
134
|
+
const publishedBy = new Map(results.map((row) => [row.name, row.published]))
|
|
135
|
+
const classified = classifyTargetAvailability(fwRows, { publishedBy, targetVersion })
|
|
136
|
+
patchTarget = { ...patchTarget, checked: results.length, missing: classified.missing, unknown: classified.unknown, skipped: null }
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
patchTarget.skipped = '核对失败:' + String(error?.message ?? error)
|
|
141
|
+
}
|
|
142
|
+
// 预设声明行体检(2026-09-28 加法):升级前就能看见"补丁里的声明行已陈旧/缺失"。
|
|
143
|
+
// 真机事故:预设自身被改成 v10、补丁行还指向 v1 → 老会话 resume 到错的模块,且没有任何地方会报。
|
|
144
|
+
// 只读、不阻塞:作为 presetAudit 字段随预检一起下发(blockers>0 时由面板提示先修再升)。
|
|
145
|
+
let presetAudit = null
|
|
146
|
+
try {
|
|
147
|
+
presetAudit = collectPresetAudit({ presetsRoot: join(dshHome(), '.agent-presets'), patchPath: findPatchPath(rc.ctx) })
|
|
148
|
+
} catch (error) {
|
|
149
|
+
presetAudit = { ok: false, error: String(error?.message ?? error), blockers: 0, stale: [], missing: [], orphan: [], targetMissing: [] }
|
|
150
|
+
}
|
|
112
151
|
sendJson(res, 200, {
|
|
113
152
|
ok: true,
|
|
114
153
|
targetVersion,
|
|
@@ -122,6 +161,8 @@ async function routeFrameworkPreflight(req, res, rc) {
|
|
|
122
161
|
rules: summarizeContractRules(loadContractRules()),
|
|
123
162
|
roots: roots.map((r) => ({ root: r.root, kind: r.kind, moduleName: r.moduleName ?? null })),
|
|
124
163
|
scan,
|
|
164
|
+
patchTarget,
|
|
165
|
+
presetAudit,
|
|
125
166
|
})
|
|
126
167
|
}
|
|
127
168
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// L2 · routes —— 框架升级(POST /framework-upgrade)
|
|
2
|
-
// 分层 Step 8b:从 lib/index.js 的 handle()
|
|
3
|
-
//
|
|
4
|
-
// 待抽包阶段把脚本模板挪进 domain/framework-script.js 后撤销例外。
|
|
2
|
+
// 分层 Step 8b:从 lib/index.js 的 handle() 原样搬出(只搬移未改逻辑)。这个 handler 内置大段
|
|
3
|
+
// PowerShell 脚本模板,是守卫「单文件 ≤ 600 行」的受限例外(见 SIZE_EXCEPTIONS),待抽包阶段撤销。
|
|
5
4
|
|
|
6
5
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
7
6
|
import { writeFile } from 'node:fs/promises'
|
|
@@ -11,7 +10,7 @@ import { tmpdir } from 'node:os'
|
|
|
11
10
|
import { pathToFileURL } from 'node:url'
|
|
12
11
|
import { createRequire } from 'node:module'
|
|
13
12
|
import { readCompatMode, writeCompatMode } from '../domain/compat-state.js'
|
|
14
|
-
import { FRAMEWORK_BACKUP_ROOT, backupProfileSnapshot, checkpointFrameworkTree, preflightDisableIncompatible, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, resolveFrameworkUpgradePlan, shellHostedRefusal } from '../domain/framework.js'
|
|
13
|
+
import { FRAMEWORK_BACKUP_ROOT, backupProfileSnapshot, checkpointFrameworkTree, platformUpgradeRefusal, preflightDisableIncompatible, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, resolveFrameworkUpgradePlan, shellHostedRefusal } from '../domain/framework.js'
|
|
15
14
|
import { fwIntegrityCheck } from '../infra/fw-integrity-check.js'
|
|
16
15
|
import { CORE_PATCH_ROW_IDS } from '../domain/patch.js'
|
|
17
16
|
import { migrateAgentConfigsForUpgrade } from '../domain/presets.js'
|
|
@@ -80,12 +79,15 @@ async function routeFrameworkUpgrade(req, res, rc) {
|
|
|
80
79
|
} catch {}
|
|
81
80
|
}
|
|
82
81
|
// 拉元数据 → 候选版本列表 → 本次目标(helper 里同时做「用户自选版本」的白名单校验)
|
|
83
|
-
const plan = await resolveFrameworkUpgradePlan({ current, wanted: body.version, fetchJson: fetchJsonUrl })
|
|
82
|
+
const plan = await resolveFrameworkUpgradePlan({ current, wanted: body.version, fetchJson: rc.deps.fetchJson ?? fetchJsonUrl })
|
|
84
83
|
const { latest, next, alpha, versions, tagDefault, registryError } = plan
|
|
85
84
|
if (plan.rejected !== null) {
|
|
86
85
|
sendError(res, 400, `升级目标 ${plan.rejected} 不在可选列表内(只接受比当前 ${current ?? '?'} 更新的已发布版本;降级请用「回滚到上一版」)`)
|
|
87
86
|
return
|
|
88
87
|
}
|
|
88
|
+
// 平台守卫(2026-09-29 改错):本通道 = schtasks.exe + PowerShell,只有 win32 有;判据与文案见
|
|
89
|
+
// domain/framework.js#platformUpgradeRefusal。放在入参校验之后、任何副作用之前 —— 绝不假成功。
|
|
90
|
+
const platformRefusal = platformUpgradeRefusal(); if (platformRefusal !== null) { sendError(res, 501, platformRefusal.error, platformRefusal.details); return }
|
|
89
91
|
const target = plan.target
|
|
90
92
|
const profileDir = dirname(findPatchPath(ctx))
|
|
91
93
|
// 1) 升级前打包备份现有配置(patch / profile package.json / 插件清单)
|
|
@@ -138,17 +140,16 @@ async function routeFrameworkUpgrade(req, res, rc) {
|
|
|
138
140
|
steps.push(`另有 ${preflight.skipped.length} 行无法预判(受保护行/包信息读不到),保持启用,由升级后的启动失败隔离兜底`)
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
|
-
// 2) 自动升级框架:运行中的服务锁着 dsh/lib 目录(EBUSY
|
|
142
|
-
//
|
|
143
|
-
// 与 /restart 同款自守护模式:端口无监听会自动拉起,不会留下"服务起不来"。
|
|
143
|
+
// 2) 自动升级框架:运行中的服务锁着 dsh/lib 目录(EBUSY 无法原地替换)→ 生成独立升级脚本
|
|
144
|
+
// (kill 服务 → npm 升级 → 失败回滚 → 拉起服务)后台执行;与 /restart 同款自守护,不会留下"服务起不来"。
|
|
144
145
|
if (hasUpdate && dshDir !== null) {
|
|
145
146
|
const port = webPort(ctx)
|
|
146
147
|
const nodePath = process.execPath
|
|
147
148
|
const binPath = resolveDshBin()
|
|
148
149
|
const npmCli = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js')
|
|
149
150
|
// 事故教训:npm 的 lib/cli.js 是「函数模块」(module.exports = (process) => validateEngines(...)),
|
|
150
|
-
//
|
|
151
|
-
//
|
|
151
|
+
// 直跑它只加载函数定义、不执行命令——立即退出 exit 0(假成功,"npm 退出码 0 但版本未更新")。
|
|
152
|
+
// 唯一正确入口是 bin/npm-cli.js(它调用 cli.js 函数)。
|
|
152
153
|
const corepackJs = join(dirname(process.execPath), 'node_modules', 'corepack', 'dist', 'corepack.js')
|
|
153
154
|
// 预检(事故教训):corepack(pnpm 通道)/ dsh bin.js 缺失时**拒绝生成升级脚本**(不 kill 服务)——
|
|
154
155
|
// 避免"kill 后升级/拉起双双失败,服务永久 down"
|
|
@@ -640,16 +641,14 @@ async function routeFrameworkUpgrade(req, res, rc) {
|
|
|
640
641
|
setFwCheckCache(null)
|
|
641
642
|
// UTF-8 BOM:powershell 5.1 按 ANSI 读无 BOM 文件,中文会乱码——加 BOM 保证解析正确。
|
|
642
643
|
// 执行方式:schtasks 一次性计划任务(Task Scheduler 启动,独立于服务进程树/Job Object)——
|
|
643
|
-
//
|
|
644
|
-
//
|
|
644
|
+
// 服务进程被杀时其子进程(execFile/detached 的 powershell)会被 Job 连带杀死,
|
|
645
|
+
// 计划任务彻底脱离,杀服务影响不到升级脚本。
|
|
645
646
|
writeFile(ps1, `\uFEFF${lines}`, 'utf8').then(
|
|
646
647
|
() => {
|
|
647
648
|
const ps1Posix = ps1.replace(/\\/gu, '/')
|
|
648
|
-
// 无引号 /tr(重要):Task Scheduler
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
// 格式(exe 与脚本路径均无空格时)任务正常执行、脚本完整跑通。仅当脚本路径含空格
|
|
652
|
-
// 时才退回带引号格式(schtasks 引号解析在服务 execFile 上下文不可靠,此时宁可用它)。
|
|
649
|
+
// 无引号 /tr(重要):Task Scheduler 会把带引号命令的 Command 拆坏成 `"powershell … -File \"`
|
|
650
|
+
// (非有效可执行文件)——任务显示 Ready、/run 报 SUCCESS 但永不执行(曾导致脚本反复"已启动"
|
|
651
|
+
// 却不动作)。实测无引号格式(exe 与脚本路径均无空格时)正常;仅脚本路径含空格时才退回带引号格式。
|
|
653
652
|
const tr = / /.test(ps1Posix)
|
|
654
653
|
? `"powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \\"${ps1Posix}\\""`
|
|
655
654
|
: `C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File ${ps1Posix}`
|
|
@@ -8,7 +8,7 @@ import { dirname, join, resolve } from 'node:path'
|
|
|
8
8
|
import { tmpdir } from 'node:os'
|
|
9
9
|
import { createRequire } from 'node:module'
|
|
10
10
|
import { readCompatGate, writeCompatGate } from '../domain/compat.js'
|
|
11
|
-
import { cleanupStaleFwTasks, currentFrameworkVersion, pickFrameworkTarget, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, shellHostedRefusal } from '../domain/framework.js'
|
|
11
|
+
import { cleanupStaleFwTasks, currentFrameworkVersion, pickFrameworkTarget, platformUpgradeRefusal, relaunchPrelude, resolveDshBin, resolveFrameworkRootNodeModules, shellHostedRefusal } from '../domain/framework.js'
|
|
12
12
|
import { readGithubAuth } from '../domain/install.js'
|
|
13
13
|
import { webPort } from '../domain/runtime.js'
|
|
14
14
|
import { fetchJsonUrl } from '../infra/http.js'
|
|
@@ -32,6 +32,21 @@ function refuseWhenShellHosted(res) {
|
|
|
32
32
|
return true
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 平台守卫(2026-09-29 改错,与宿主守卫同形):一键升级/回滚脚本整体依赖 Windows 机制 ——
|
|
37
|
+
* `schtasks.exe` 计划任务 + PowerShell + `Get-NetTCPConnection`;非 Windows 上 execFile 必然 ENOENT,
|
|
38
|
+
* 而旧实现会先写一个「脚本已启动」的状态文件、再去 exec 不存在的 powershell.exe,**最后仍回 ok:true**
|
|
39
|
+
* (用户以为在跑,实际零动作)。判据与文案见 domain/framework.js#platformUpgradeRefusal。
|
|
40
|
+
* 命中时:不写状态文件、不 spawn、不建计划任务,直接 501 + 手动升级出路。
|
|
41
|
+
* 返回 true = 已拒绝(调用方直接 return);false = win32,原路径一字不改。
|
|
42
|
+
*/
|
|
43
|
+
function refuseWhenPlatformUnsupported(res) {
|
|
44
|
+
const refusal = platformUpgradeRefusal()
|
|
45
|
+
if (refusal === null) return false
|
|
46
|
+
sendError(res, 501, refusal.error, refusal.details)
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
async function routeFrameworkUpgradeStatusGet(req, res, rc) {
|
|
36
51
|
const ctx = rc.ctx
|
|
37
52
|
const url = rc.url
|
|
@@ -355,6 +370,7 @@ async function routeFrameworkRollback(req, res, rc) {
|
|
|
355
370
|
sendError(res, 409, '没有可用的框架全树回滚点(framework-rollback.json 缺失或 checkpoint 已清理)')
|
|
356
371
|
return
|
|
357
372
|
}
|
|
373
|
+
if (refuseWhenPlatformUnsupported(res)) return // 平台守卫(改错):回滚脚本同样是 schtasks+PowerShell,非 win32 一律拒绝而不是假成功(放在只读状态校验之后、任何写盘/exec 之前)
|
|
358
374
|
const port = webPort(ctx)
|
|
359
375
|
const nodePath = process.execPath
|
|
360
376
|
const taskName = `DSH-FW-Rollback-${process.pid}`
|
|
@@ -576,4 +592,4 @@ async function routeFrameworkStatusClear(req, res, rc) {
|
|
|
576
592
|
}
|
|
577
593
|
sendJson(res, 200, { ok: true, removed, status: 'idle', message: null })
|
|
578
594
|
}
|
|
579
|
-
export { refuseWhenShellHosted, routeFrameworkStatusClear, routeFrameworkUpgradeStatusGet, routeFrameworkRelaunch, routeFrameworkCheck, routeCompatGate, routeCheckUpdate, routeFrameworkRollback, routeRestart }
|
|
595
|
+
export { refuseWhenShellHosted, refuseWhenPlatformUnsupported, routeFrameworkStatusClear, routeFrameworkUpgradeStatusGet, routeFrameworkRelaunch, routeFrameworkCheck, routeCompatGate, routeCheckUpdate, routeFrameworkRollback, routeRestart }
|
|
@@ -149,8 +149,8 @@ function shortOpenError(error) {
|
|
|
149
149
|
* POST /plugin-console/github-open-login —— 唤起 dsh-github-login 的设备码登录窗口。
|
|
150
150
|
*
|
|
151
151
|
* 为什么失败也回 200(而不是 500):这个能力是**外挂**的,装没装、装的是不是带 exe 的版本、
|
|
152
|
-
* 当前 profile 有没有把它挂到 webServer 上,全都不由本插件决定(2026-09-20
|
|
153
|
-
* 该插件、exe
|
|
152
|
+
* 当前 profile 有没有把它挂到 webServer 上,全都不由本插件决定(2026-09-20 实测:某个 profile 里虽然装了
|
|
153
|
+
* 该插件、exe 也在,但当前 profile 下 GET /github-auth/status 仍是
|
|
154
154
|
* 404)。这些都是"这个通道现在不可用",不是"服务端出错":
|
|
155
155
|
* · 回 500 → 前端弹一句看不懂的报错,还看不出"该走 token 兜底"这条正路;
|
|
156
156
|
* · 回 200 + started:false + reason → 前端照原样把原因念给用户,并自动展开 token 粘贴面板。
|
|
@@ -11,11 +11,14 @@ import { pnpmRemove, readExtraBundleRows, runInstallJob } from '../domain/instal
|
|
|
11
11
|
import { isProtectedModule, listEntries, webPort } from '../domain/runtime.js'
|
|
12
12
|
import { DEFAULT_SEARCH } from '../domain/sources.js'
|
|
13
13
|
import { runSuiteInstallJob } from '../domain/suite.js'
|
|
14
|
+
import { fetchJsonUrl } from '../infra/http.js'
|
|
14
15
|
import { readBody, sendError } from '../infra/httpd.js'
|
|
15
16
|
import { routeAiConsent, routeAiEmpowerCancel, routeAiEmpowerList, routeAiEmpowerPlan, routeAiEmpowerRun, routeAiEmpowerStatus } from './ai.js'
|
|
16
17
|
import { routeRunSuggested } from './actions.js'
|
|
17
18
|
import { routeComponentAutostart, routeComponentStart, routeComponentStatus, routeComponentStop, routeComponents, routeRepoClone, routeRepoLandConfig, routeRepoList, routeRepoOpen, routeRepoRemove } from './components.js'
|
|
18
|
-
import { routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled } from './compat.js'
|
|
19
|
+
import { routeCompatAck, routeCompatMode, routeCompatStamp, routeCompatStatusGet, routeDeclareInstalled } from './compat.js'
|
|
20
|
+
import { routePatchAuditGet } from './patch-audit.js'
|
|
21
|
+
import { routePresetAuditGet } from './preset-audit.js'
|
|
19
22
|
import { routeFrameworkUpgrade } from './framework-upgrade.js'
|
|
20
23
|
import { routeFrameworkPreflight, routeFrameworkPreflightPatch } from './framework-preflight.js'
|
|
21
24
|
import { routeCheckUpdate, routeCompatGate, routeFrameworkCheck, routeFrameworkRelaunch, routeFrameworkRollback, routeFrameworkStatusClear, routeFrameworkUpgradeStatusGet, routeRestart } from './framework.js'
|
|
@@ -37,12 +40,23 @@ const ROUTES_EARLY = [
|
|
|
37
40
|
{ methods: ['GET'], path: `${ROUTE_PREFIX}/gitee-oauth-callback`, handler: routeGiteeOauthCallbackGet },
|
|
38
41
|
{ methods: ['GET'], path: `${ROUTE_PREFIX}/skills-installed`, handler: routeSkillsInstalledGet }, { methods: ['GET'], path: `${ROUTE_PREFIX}/framework-upgrade-status`, handler: routeFrameworkUpgradeStatusGet },
|
|
39
42
|
{ methods: ['GET'], path: `${ROUTE_PREFIX}/compat-status`, handler: routeCompatStatusGet },
|
|
43
|
+
// 只读体检(2026-09-30 改错:这两条**必须**住在守卫之前)。
|
|
44
|
+
// 原实现把它们登记在守卫之后的 ROUTES 表里 → GET 在守卫处就被 405 掉,永远到不了 handler。
|
|
45
|
+
// 真机现场(0.5.29 活体实例):`GET /plugin-console/preset-audit` → 405「不支持的方法」,
|
|
46
|
+
// `POST` 同路径 → 404「未知接口」—— 两句都由 handle() 自己发出,说明**查表根本没参与**。
|
|
47
|
+
// 判据:405 守卫 `if (method !== 'POST' && !(method === 'GET' && GET_COMPAT.has(pathname)))`
|
|
48
|
+
// 跑在 ROUTES 查表之前,只放行 POST(外加 GET_COMPAT 白名单)→
|
|
49
|
+
// **任何声明 GET 的路由都必须登记在 ROUTES_EARLY**。该判据已固化进 tests/test-route-inventory.mjs。
|
|
50
|
+
{ methods: ['GET'], path: `${ROUTE_PREFIX}/patch-audit`, handler: routePatchAuditGet },
|
|
51
|
+
{ methods: ['GET'], path: `${ROUTE_PREFIX}/preset-audit`, handler: routePresetAuditGet },
|
|
40
52
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/framework-relaunch`, handler: routeFrameworkRelaunch },
|
|
41
53
|
|
|
42
54
|
]
|
|
43
55
|
|
|
44
56
|
function routeDeps() {
|
|
45
|
-
|
|
57
|
+
// fetchJson:registry 元数据取数的注入缝(默认就是真通道)。测试要断言「非 win32 拒绝且零副作用」
|
|
58
|
+
// 时用它喂固定元数据,免得把网络与 CI 稳定性绑在一起(与其余 deps 同一种注入风格)。
|
|
59
|
+
return { runInstallJob, runSuiteInstallJob, webPort, readExtraBundleRows, detectAdoptablePending, listEntries, detectCompat, detectFrameworkUpgrade, DEFAULT_SEARCH, aiEmpowerExecute, aiEmpowerPlan, backupProfileSnapshot, currentFrameworkVersion, frameworkCompatReportFor, isProtectedModule, pnpmRemove, preflightDisableIncompatible, rowIdModuleMap, fetchJson: fetchJsonUrl }
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
async function handle(ctx, req, res) {
|
|
@@ -98,6 +112,7 @@ const ROUTES = [
|
|
|
98
112
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/framework-status-clear`, handler: routeFrameworkStatusClear },
|
|
99
113
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/compat-mode`, handler: routeCompatMode },
|
|
100
114
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/compat-stamp`, handler: routeCompatStamp },
|
|
115
|
+
{ methods: ['POST'], path: `${ROUTE_PREFIX}/compat-ack`, handler: routeCompatAck },
|
|
101
116
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/declare-installed`, handler: routeDeclareInstalled },
|
|
102
117
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/framework-rollback`, handler: routeFrameworkRollback },
|
|
103
118
|
{ methods: ['POST'], path: `${ROUTE_PREFIX}/restart`, handler: routeRestart },
|