@moonquake2004/dsh-doctor 0.4.23 → 0.4.25
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/dsh-doctor.mjs +67 -42
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -241,8 +241,11 @@ function resolveProfile(name) {
|
|
|
241
241
|
|
|
242
242
|
/* ================= env ================= */
|
|
243
243
|
/** v1 词汇表 r5(#1719/#2259)node 语义:pass = 满足 ^22.19.0 || >=24.0.0,其余 warn——无民间 fail 阈值。
|
|
244
|
-
*
|
|
245
|
-
*
|
|
244
|
+
* 出处(2026-09 两次更正后的准确版本,由社区 @ciceroyang 核实、我们复核):
|
|
245
|
+
* 该范围**确实声明在 manifest 里** —— 仓库根 `package.json` 的 `engines.node`,
|
|
246
|
+
* 即 `@deepseek-ai/dsh-root`(`private: true`,非发布包)。**没有任何已发布包继承它**,
|
|
247
|
+
* 所以 registry 查不到、npm 安装时既不校验也不警告;这正是社区里"没有 engines"印象的由来。
|
|
248
|
+
* 我们内置的范围与之一致(等于官方声明值),但它是**手写维护**的:上游改动时需同步。 */
|
|
246
249
|
function nodeInSupportedRange(v) {
|
|
247
250
|
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v));
|
|
248
251
|
if (!m) return false;
|
|
@@ -266,8 +269,8 @@ function checkEnv() {
|
|
|
266
269
|
const version = String(nv.stdout).trim();
|
|
267
270
|
const supported = nodeInSupportedRange(version);
|
|
268
271
|
report('env', 'E3-node', supported,
|
|
269
|
-
supported ? `node ${version}
|
|
270
|
-
supported ? undefined : '升级 node 到 ^22.19.0 或 >=24.0.0
|
|
272
|
+
supported ? `node ${version}(满足声明范围 ^22.19.0 || >=24.0.0——出处为仓库根私有 workspace package.json 的 engines,#2259)` : `node ${version} 不在支持范围(^22.19.0 || >=24.0.0,v1 词汇表)——会话日志读取等能力受限`,
|
|
273
|
+
supported ? undefined : '升级 node 到 ^22.19.0 或 >=24.0.0(仓库根 engines 声明的范围,见 #2259)');
|
|
271
274
|
}
|
|
272
275
|
|
|
273
276
|
// E12:运行时 zstd 稳定性(#6651 的运行时线索)
|
|
@@ -2022,13 +2025,31 @@ const sessionArg = (() => { const i = process.argv.indexOf('--session'); return
|
|
|
2022
2025
|
* strict semver 的字面规则会把第一种判成不满足 —— 那是误报,而误报健康 profile 比不报更糟。
|
|
2023
2026
|
*/
|
|
2024
2027
|
|
|
2025
|
-
/**
|
|
2028
|
+
/**
|
|
2029
|
+
* 版本与区间判定 —— **按参考实现的代码对齐**(ciceroyang/dsh-doctor `satisfiesRange`),
|
|
2030
|
+
* 并经其差分测试 `scripts/range-difftest.mjs` 验收(13 条边界语料 0 分歧)。
|
|
2031
|
+
*
|
|
2032
|
+
* 规则(照其代码,而非文档表格——两者不一致,见下):
|
|
2033
|
+
* 1. 逐组做**数值**判定;任一比较器为假 → 该组 false;有无法解析的比较器 → 该组 null;
|
|
2034
|
+
* 2. 全部比较器数值通过后,若**被判版本是预发布**且**该组不含任何预发布比较器** → 该组 null;
|
|
2035
|
+
* 否则 → 该组 true;
|
|
2036
|
+
* 3. 任一组成立 → 兼容;否则若有组为 null → 未知;否则 → 不兼容。
|
|
2037
|
+
*
|
|
2038
|
+
* 文档表格第二行("numeric 满足但 strict 不满足 → unknown")与其代码/后果清单不一致:
|
|
2039
|
+
* 代码对 `>=0.1.0-rc.5 <0.2.0` 配 `0.1.5-rc.2` 返回**兼容**(该组是预发布感知的),
|
|
2040
|
+
* 而按表格字面会得到"未知"。我们按**代码**对齐,并已把该不一致回报给作者。
|
|
2041
|
+
*/
|
|
2042
|
+
|
|
2043
|
+
/** 解析(可含部分的)版本:1 / 1.2 / 1.2.3[-pre];缺失段补 0。无法解析返回 null。 */
|
|
2026
2044
|
function parseVer(v) {
|
|
2027
|
-
const m = /^\s*v?(\d+)
|
|
2045
|
+
const m = /^\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?\s*$/.exec(String(v ?? ''));
|
|
2028
2046
|
if (!m) return null;
|
|
2029
|
-
return {
|
|
2047
|
+
return {
|
|
2048
|
+
nums: [Number(m[1]), m[2] === undefined ? 0 : Number(m[2]), m[3] === undefined ? 0 : Number(m[3])],
|
|
2049
|
+
pre: m[4] ? m[4].split('.') : [],
|
|
2050
|
+
};
|
|
2030
2051
|
}
|
|
2031
|
-
/** semver
|
|
2052
|
+
/** semver 预发布优先级:数字段比数字,字母段按字典序,数字段 < 字母段;无 prerelease 者更高。 */
|
|
2032
2053
|
function cmpVer(a, b) {
|
|
2033
2054
|
for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] < b.nums[i] ? -1 : 1;
|
|
2034
2055
|
if (!a.pre.length && !b.pre.length) return 0;
|
|
@@ -2045,8 +2066,8 @@ function cmpVer(a, b) {
|
|
|
2045
2066
|
}
|
|
2046
2067
|
return 0;
|
|
2047
2068
|
}
|
|
2048
|
-
/**
|
|
2049
|
-
function
|
|
2069
|
+
/** 单个比较器:true / false / null(前缀无法解析)。 */
|
|
2070
|
+
function cmpHalf(ver, op, target) {
|
|
2050
2071
|
const c = cmpVer(ver, target);
|
|
2051
2072
|
switch (op) {
|
|
2052
2073
|
case '>': return c > 0;
|
|
@@ -2054,10 +2075,9 @@ function cmpSatisfied(ver, op, target) {
|
|
|
2054
2075
|
case '<': return c < 0;
|
|
2055
2076
|
case '<=': return c <= 0;
|
|
2056
2077
|
case '=': return c === 0;
|
|
2057
|
-
default: return
|
|
2078
|
+
default: return null;
|
|
2058
2079
|
}
|
|
2059
2080
|
}
|
|
2060
|
-
/** 展开 ^ / ~ 为区间对(只覆盖生态里实际出现的形态)。 */
|
|
2061
2081
|
function expandCaret(target) {
|
|
2062
2082
|
const [maj, min, pat] = target.nums;
|
|
2063
2083
|
if (maj > 0) return { lower: ['>=', target], upper: ['<', parseVer(`${maj + 1}.0.0`)] };
|
|
@@ -2068,42 +2088,47 @@ function expandTilde(target) {
|
|
|
2068
2088
|
const [maj, min] = target.nums;
|
|
2069
2089
|
return { lower: ['>=', target], upper: ['<', parseVer(`${maj}.${min + 1}.0`)] };
|
|
2070
2090
|
}
|
|
2091
|
+
/** 一个 `||` 组的判定:true / false / null。 */
|
|
2092
|
+
function evalGroup(ver, group) {
|
|
2093
|
+
let prereleaseAware = false;
|
|
2094
|
+
for (const part of group.split(/\s+/).filter(Boolean)) {
|
|
2095
|
+
const m = /^([\^~]|>=|<=|>|<|=)?\s*v?(.+)$/.exec(part);
|
|
2096
|
+
if (!m) return null;
|
|
2097
|
+
const op = m[1] ?? '=';
|
|
2098
|
+
const target = parseVer(m[2]);
|
|
2099
|
+
if (!target) return null;
|
|
2100
|
+
if (target.pre.length > 0) prereleaseAware = true;
|
|
2101
|
+
if (op === '^' || op === '~') {
|
|
2102
|
+
const { lower, upper } = op === '^' ? expandCaret(target) : expandTilde(target);
|
|
2103
|
+
if (cmpHalf(ver, lower[0], lower[1]) === false || cmpHalf(ver, upper[0], upper[1]) === false) return false;
|
|
2104
|
+
} else {
|
|
2105
|
+
const r = cmpHalf(ver, op, target);
|
|
2106
|
+
if (r === false) return false;
|
|
2107
|
+
if (r === null) return null;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
// 数值全通过:预发布版本只对"预发布感知"的组才敢断言兼容
|
|
2111
|
+
if (ver.pre.length > 0 && !prereleaseAware) return null;
|
|
2112
|
+
return true;
|
|
2113
|
+
}
|
|
2071
2114
|
/**
|
|
2072
|
-
* 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown'
|
|
2073
|
-
*
|
|
2115
|
+
* 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown' }。
|
|
2116
|
+
* 未知(null)的三种来源:`*`/空、比较器不可解析、预发布版本面对"非预发布感知"的组。
|
|
2074
2117
|
*/
|
|
2075
2118
|
function peerRangeState(version, range) {
|
|
2076
2119
|
const ver = parseVer(version);
|
|
2077
2120
|
const raw = String(range ?? '').trim();
|
|
2078
|
-
if (!ver || !raw) return { state: 'unknown'
|
|
2079
|
-
if (raw === '*' || raw
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2121
|
+
if (!ver || !raw) return { state: 'unknown' };
|
|
2122
|
+
if (raw === '*' || /^x$/i.test(raw) || raw.toLowerCase() === 'latest') return { state: 'unknown' };
|
|
2123
|
+
const groups = raw.split('||').map((x) => x.trim()).filter(Boolean);
|
|
2124
|
+
if (!groups.length) return { state: 'unknown' };
|
|
2125
|
+
let undecidable = false;
|
|
2083
2126
|
for (const g of groups) {
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
const op = m[1] ?? '=';
|
|
2090
|
-
const target = parseVer(m[2]);
|
|
2091
|
-
if (!target) { ok = false; break; }
|
|
2092
|
-
if (target.pre.length) preHere = true;
|
|
2093
|
-
if (op === '^' || op === '~') {
|
|
2094
|
-
const { lower, upper } = op === '^' ? expandCaret(target) : expandTilde(target);
|
|
2095
|
-
if (!(cmpSatisfied(ver, lower[0], lower[1]) && cmpSatisfied(ver, upper[0], upper[1]))) { ok = false; break; }
|
|
2096
|
-
} else if (!cmpSatisfied(ver, op, target)) { ok = false; break; }
|
|
2097
|
-
}
|
|
2098
|
-
if (ok) {
|
|
2099
|
-
hasPre = hasPre || preHere;
|
|
2100
|
-
return { state: 'satisfied', hasPreComparator: hasPre };
|
|
2101
|
-
}
|
|
2102
|
-
hasPre = hasPre || preHere;
|
|
2103
|
-
}
|
|
2104
|
-
// 全部组都不满足:若这次不匹配发生在"纯 release 区间 vs 预发布版本",按规则记 unknown
|
|
2105
|
-
if (!hasPre && ver.pre.length) return { state: 'unknown', hasPreComparator: false };
|
|
2106
|
-
return { state: 'unsatisfied', hasPreComparator: hasPre };
|
|
2127
|
+
const v = evalGroup(ver, g);
|
|
2128
|
+
if (v === true) return { state: 'satisfied' };
|
|
2129
|
+
if (v === null) undecidable = true;
|
|
2130
|
+
}
|
|
2131
|
+
return { state: undecidable ? 'unknown' : 'unsatisfied' };
|
|
2107
2132
|
}
|
|
2108
2133
|
|
|
2109
2134
|
/** 解析 host 包版本:profile node_modules → 共享镜像根;**跟随软链接**(readFileSync 会跟随)。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.25",
|
|
4
4
|
"description": "Offline diagnostic for DeepSeek Harness — 28 built-in + 5 catalog checks across env/profile/session (Layer A checks-as-data), self-update (Layer B), and a semi-automatic LLM observer (Layer C, --observe); Doctor panel in web UI settings.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|