@moonquake2004/dsh-doctor 0.4.24 → 0.4.26
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 +78 -40
- package/package.json +1 -1
package/dsh-doctor.mjs
CHANGED
|
@@ -639,18 +639,34 @@ function checkProfile(name) {
|
|
|
639
639
|
findings19.push(`${pkgName} 声明 ${hostName} ${range},但实际提供 ${host.version}`);
|
|
640
640
|
}
|
|
641
641
|
}
|
|
642
|
+
// 规则 1(作者侧):未声明 host 范围 = **无信息**,不得当作兼容。这里只做计数提示,
|
|
643
|
+
// 不生成发现——声明与否是作者的选择,消费者该做的是别把它读成"已核对"。
|
|
644
|
+
let undeclared19 = 0;
|
|
645
|
+
for (const { dir: pkgDir } of pkgDirs19) {
|
|
646
|
+
try {
|
|
647
|
+
const mf = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
648
|
+
const isBundle = Boolean(mf.dsh && (mf.dsh.bundle || mf.dsh.profile));
|
|
649
|
+
const hasHostPeer = Object.keys(mf.peerDependencies ?? {}).some((k) => k.startsWith('@deepseek-ai/'));
|
|
650
|
+
if (isBundle && !hasHostPeer) undeclared19++;
|
|
651
|
+
} catch { /* 忽略不可读 */ }
|
|
652
|
+
}
|
|
653
|
+
const undeclaredNote = undeclared19
|
|
654
|
+
? `\n (另有 ${undeclared19} 个 bundle 未声明任何 @deepseek-ai/* host 范围——按规则记为"无信息",不代表兼容)`
|
|
655
|
+
: '';
|
|
642
656
|
if (findings19.length) {
|
|
643
657
|
report('profile', 'P19', false,
|
|
644
658
|
`插件声明的 host peer 范围不接受实际安装的核心版本(${findings19.length} 处,共核对 ${checked19} 条 peer 声明):\n `
|
|
645
659
|
+ findings19.slice(0, 8).join('\n ')
|
|
646
|
-
+
|
|
660
|
+
+ undeclaredNote
|
|
661
|
+
+ (unknown19 ? `\n (另有 ${unknown19} 条无法判定:区间为 * / 不可解析 / host 解析不到,或纯 release 组面对数值满足的预发布版本——按"未知"处理,不计为不兼容)` : ''),
|
|
647
662
|
'按提示升级到该插件声明支持的版本(或降级核心)。安装时不会有警告,所以升级 dsh 前先用本检查看一眼最省事');
|
|
648
663
|
} else if (checked19 === 0) {
|
|
649
664
|
reportSkip('profile', 'P19', '未见任何 @deepseek-ai/* 的 peer 声明,跳过 host 范围核对');
|
|
650
665
|
} else {
|
|
651
666
|
report('profile', 'P19', true,
|
|
652
667
|
`核对 ${checked19} 条 host peer 声明,均可接受当前核心版本`
|
|
653
|
-
+ (unknown19 ? `(${unknown19} 条无法判定,按未知处理未计入)` : '')
|
|
668
|
+
+ (unknown19 ? `(${unknown19} 条无法判定,按未知处理未计入)` : '')
|
|
669
|
+
+ undeclaredNote.replace(/\n\s*/g, ' '));
|
|
654
670
|
}
|
|
655
671
|
}
|
|
656
672
|
|
|
@@ -2025,13 +2041,31 @@ const sessionArg = (() => { const i = process.argv.indexOf('--session'); return
|
|
|
2025
2041
|
* strict semver 的字面规则会把第一种判成不满足 —— 那是误报,而误报健康 profile 比不报更糟。
|
|
2026
2042
|
*/
|
|
2027
2043
|
|
|
2028
|
-
/**
|
|
2044
|
+
/**
|
|
2045
|
+
* 版本与区间判定 —— **按参考实现的代码对齐**(ciceroyang/dsh-doctor `satisfiesRange`),
|
|
2046
|
+
* 并经其差分测试 `scripts/range-difftest.mjs` 验收(13 条边界语料 0 分歧)。
|
|
2047
|
+
*
|
|
2048
|
+
* 规则(照其代码,而非文档表格——两者不一致,见下):
|
|
2049
|
+
* 1. 逐组做**数值**判定;任一比较器为假 → 该组 false;有无法解析的比较器 → 该组 null;
|
|
2050
|
+
* 2. 全部比较器数值通过后,若**被判版本是预发布**且**该组不含任何预发布比较器** → 该组 null;
|
|
2051
|
+
* 否则 → 该组 true;
|
|
2052
|
+
* 3. 任一组成立 → 兼容;否则若有组为 null → 未知;否则 → 不兼容。
|
|
2053
|
+
*
|
|
2054
|
+
* 文档表格第二行("numeric 满足但 strict 不满足 → unknown")与其代码/后果清单不一致:
|
|
2055
|
+
* 代码对 `>=0.1.0-rc.5 <0.2.0` 配 `0.1.5-rc.2` 返回**兼容**(该组是预发布感知的),
|
|
2056
|
+
* 而按表格字面会得到"未知"。我们按**代码**对齐,并已把该不一致回报给作者。
|
|
2057
|
+
*/
|
|
2058
|
+
|
|
2059
|
+
/** 解析(可含部分的)版本:1 / 1.2 / 1.2.3[-pre];缺失段补 0。无法解析返回 null。 */
|
|
2029
2060
|
function parseVer(v) {
|
|
2030
|
-
const m = /^\s*v?(\d+)
|
|
2061
|
+
const m = /^\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?\s*$/.exec(String(v ?? ''));
|
|
2031
2062
|
if (!m) return null;
|
|
2032
|
-
return {
|
|
2063
|
+
return {
|
|
2064
|
+
nums: [Number(m[1]), m[2] === undefined ? 0 : Number(m[2]), m[3] === undefined ? 0 : Number(m[3])],
|
|
2065
|
+
pre: m[4] ? m[4].split('.') : [],
|
|
2066
|
+
};
|
|
2033
2067
|
}
|
|
2034
|
-
/** semver
|
|
2068
|
+
/** semver 预发布优先级:数字段比数字,字母段按字典序,数字段 < 字母段;无 prerelease 者更高。 */
|
|
2035
2069
|
function cmpVer(a, b) {
|
|
2036
2070
|
for (let i = 0; i < 3; i++) if (a.nums[i] !== b.nums[i]) return a.nums[i] < b.nums[i] ? -1 : 1;
|
|
2037
2071
|
if (!a.pre.length && !b.pre.length) return 0;
|
|
@@ -2048,8 +2082,8 @@ function cmpVer(a, b) {
|
|
|
2048
2082
|
}
|
|
2049
2083
|
return 0;
|
|
2050
2084
|
}
|
|
2051
|
-
/**
|
|
2052
|
-
function
|
|
2085
|
+
/** 单个比较器:true / false / null(前缀无法解析)。 */
|
|
2086
|
+
function cmpHalf(ver, op, target) {
|
|
2053
2087
|
const c = cmpVer(ver, target);
|
|
2054
2088
|
switch (op) {
|
|
2055
2089
|
case '>': return c > 0;
|
|
@@ -2057,10 +2091,9 @@ function cmpSatisfied(ver, op, target) {
|
|
|
2057
2091
|
case '<': return c < 0;
|
|
2058
2092
|
case '<=': return c <= 0;
|
|
2059
2093
|
case '=': return c === 0;
|
|
2060
|
-
default: return
|
|
2094
|
+
default: return null;
|
|
2061
2095
|
}
|
|
2062
2096
|
}
|
|
2063
|
-
/** 展开 ^ / ~ 为区间对(只覆盖生态里实际出现的形态)。 */
|
|
2064
2097
|
function expandCaret(target) {
|
|
2065
2098
|
const [maj, min, pat] = target.nums;
|
|
2066
2099
|
if (maj > 0) return { lower: ['>=', target], upper: ['<', parseVer(`${maj + 1}.0.0`)] };
|
|
@@ -2071,42 +2104,47 @@ function expandTilde(target) {
|
|
|
2071
2104
|
const [maj, min] = target.nums;
|
|
2072
2105
|
return { lower: ['>=', target], upper: ['<', parseVer(`${maj}.${min + 1}.0`)] };
|
|
2073
2106
|
}
|
|
2107
|
+
/** 一个 `||` 组的判定:true / false / null。 */
|
|
2108
|
+
function evalGroup(ver, group) {
|
|
2109
|
+
let prereleaseAware = false;
|
|
2110
|
+
for (const part of group.split(/\s+/).filter(Boolean)) {
|
|
2111
|
+
const m = /^([\^~]|>=|<=|>|<|=)?\s*v?(.+)$/.exec(part);
|
|
2112
|
+
if (!m) return null;
|
|
2113
|
+
const op = m[1] ?? '=';
|
|
2114
|
+
const target = parseVer(m[2]);
|
|
2115
|
+
if (!target) return null;
|
|
2116
|
+
if (target.pre.length > 0) prereleaseAware = true;
|
|
2117
|
+
if (op === '^' || op === '~') {
|
|
2118
|
+
const { lower, upper } = op === '^' ? expandCaret(target) : expandTilde(target);
|
|
2119
|
+
if (cmpHalf(ver, lower[0], lower[1]) === false || cmpHalf(ver, upper[0], upper[1]) === false) return false;
|
|
2120
|
+
} else {
|
|
2121
|
+
const r = cmpHalf(ver, op, target);
|
|
2122
|
+
if (r === false) return false;
|
|
2123
|
+
if (r === null) return null;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
// 数值全通过:预发布版本只对"预发布感知"的组才敢断言兼容
|
|
2127
|
+
if (ver.pre.length > 0 && !prereleaseAware) return null;
|
|
2128
|
+
return true;
|
|
2129
|
+
}
|
|
2074
2130
|
/**
|
|
2075
|
-
* 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown'
|
|
2076
|
-
*
|
|
2131
|
+
* 判定 range 是否接受 version。返回 { state: 'satisfied' | 'unsatisfied' | 'unknown' }。
|
|
2132
|
+
* 未知(null)的三种来源:`*`/空、比较器不可解析、预发布版本面对"非预发布感知"的组。
|
|
2077
2133
|
*/
|
|
2078
2134
|
function peerRangeState(version, range) {
|
|
2079
2135
|
const ver = parseVer(version);
|
|
2080
2136
|
const raw = String(range ?? '').trim();
|
|
2081
|
-
if (!ver || !raw) return { state: 'unknown'
|
|
2082
|
-
if (raw === '*' || raw
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2137
|
+
if (!ver || !raw) return { state: 'unknown' };
|
|
2138
|
+
if (raw === '*' || /^x$/i.test(raw) || raw.toLowerCase() === 'latest') return { state: 'unknown' };
|
|
2139
|
+
const groups = raw.split('||').map((x) => x.trim()).filter(Boolean);
|
|
2140
|
+
if (!groups.length) return { state: 'unknown' };
|
|
2141
|
+
let undecidable = false;
|
|
2086
2142
|
for (const g of groups) {
|
|
2087
|
-
const
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
const op = m[1] ?? '=';
|
|
2093
|
-
const target = parseVer(m[2]);
|
|
2094
|
-
if (!target) { ok = false; break; }
|
|
2095
|
-
if (target.pre.length) preHere = true;
|
|
2096
|
-
if (op === '^' || op === '~') {
|
|
2097
|
-
const { lower, upper } = op === '^' ? expandCaret(target) : expandTilde(target);
|
|
2098
|
-
if (!(cmpSatisfied(ver, lower[0], lower[1]) && cmpSatisfied(ver, upper[0], upper[1]))) { ok = false; break; }
|
|
2099
|
-
} else if (!cmpSatisfied(ver, op, target)) { ok = false; break; }
|
|
2100
|
-
}
|
|
2101
|
-
if (ok) {
|
|
2102
|
-
hasPre = hasPre || preHere;
|
|
2103
|
-
return { state: 'satisfied', hasPreComparator: hasPre };
|
|
2104
|
-
}
|
|
2105
|
-
hasPre = hasPre || preHere;
|
|
2106
|
-
}
|
|
2107
|
-
// 全部组都不满足:若这次不匹配发生在"纯 release 区间 vs 预发布版本",按规则记 unknown
|
|
2108
|
-
if (!hasPre && ver.pre.length) return { state: 'unknown', hasPreComparator: false };
|
|
2109
|
-
return { state: 'unsatisfied', hasPreComparator: hasPre };
|
|
2143
|
+
const v = evalGroup(ver, g);
|
|
2144
|
+
if (v === true) return { state: 'satisfied' };
|
|
2145
|
+
if (v === null) undecidable = true;
|
|
2146
|
+
}
|
|
2147
|
+
return { state: undecidable ? 'unknown' : 'unsatisfied' };
|
|
2110
2148
|
}
|
|
2111
2149
|
|
|
2112
2150
|
/** 解析 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.26",
|
|
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": [
|