@moonquake2004/dsh-doctor 0.8.3 → 0.8.4
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/CHANGELOG.md +18 -0
- package/dsh-doctor.mjs +79 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,24 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [0.8.4] — 2026-09-16
|
|
20
|
+
|
|
21
|
+
**巡查产出的新检查**:社区 #6789 报告的形状,用新机制(语料优先 + 变体分离)验证后发现是**真实漏检**。
|
|
22
|
+
|
|
23
|
+
### Added — P23:插件把宿主机包声明为普通依赖(社区 #6789)
|
|
24
|
+
|
|
25
|
+
- **报告的现象**:`@deepseek-ai/dsh-experimental-browser-use-runtime` 把 `@deepseek-ai/dsh-scope` 写成
|
|
26
|
+
**dependency**(而非 peer)→ 该包自带**第二份副本** → 两份实例 Symbol/协议不匹配 → **第一个会话能用、之后每个会话都失败**。
|
|
27
|
+
- **为什么此前抓不到**:`P5` 只扫 profile **顶层** `node_modules/@deepseek-ai`;未 hoist 时副本**嵌套在插件自己的 node_modules 里**,
|
|
28
|
+
顶层什么也看不到。**用两个变体分别实测确认**:有顶层副本 → P5 命中;**只有嵌套副本 → 没有任何检查提及**
|
|
29
|
+
(这一步很关键:若只看"顶层也有一份"的那个变体,就会误判为"我们已覆盖"——正是本会话反复犯的"用巧合冒充覆盖")。
|
|
30
|
+
- **新检查判据**:已装 bundle 的 manifest 里 `dependencies` 出现 `@deepseek-ai/*` 即 fail(宿主包应由宿主共享根提供),
|
|
31
|
+
并申报 `examined`/`examinedWhat`。对照实测:改用 `peerDependencies` 的插件 **不误报**。
|
|
32
|
+
- **配套(W4/W6)**:语料新增两条(病态 + **健康对照**),共 16 条;变异清单新增 `P23-host-dep-detection`
|
|
33
|
+
(把检出判据改成恒假,语料必须变红)。
|
|
34
|
+
|
|
35
|
+
测试 214 → **218**;变异 **9 个 → 8 杀死 / 0 存活 / 1 等价**。
|
|
36
|
+
|
|
19
37
|
## [0.8.3] — 2026-09-16
|
|
20
38
|
|
|
21
39
|
**把"自造的对抗者"变成默认动作**(用户要求:红队入流程 + 不依赖外部报错也能自我进化)。
|
package/dsh-doctor.mjs
CHANGED
|
@@ -857,6 +857,83 @@ function checkProfile(name) {
|
|
|
857
857
|
}
|
|
858
858
|
}
|
|
859
859
|
|
|
860
|
+
// P23:bundle **真的自带了一份宿主机包的副本**(社区 #6789)
|
|
861
|
+
//
|
|
862
|
+
// 报告的现象:`@deepseek-ai/dsh-experimental-browser-use-runtime` 把 `@deepseek-ai/dsh-scope` 写成普通 dependency,
|
|
863
|
+
// 于是 profile 里出现**第二份实例** → 两份的 Symbol/协议不匹配 → "第一个会话能用、之后每个会话都失败"。
|
|
864
|
+
//
|
|
865
|
+
// 三轮红队把这条检查打穿过三次,教训是**判"代理"而不是判"条件"**:
|
|
866
|
+
// · 第一版只看 manifest 键名(`dependencies` 里有 `@deepseek-ai/*`)→
|
|
867
|
+
// 副本**真的躺在盘上**但 manifest 没写时完全失明(红队 F1);`optionalDependencies`(F2)、
|
|
868
|
+
// `npm:`/`file:` 别名(F3)全漏;同 scope 的**插件依赖**被误报(F5);
|
|
869
|
+
// bundle 装在父层 `profiles/node_modules` 时整条跳过(F4)。
|
|
870
|
+
// 现在改为**判条件本身**:
|
|
871
|
+
// ① 扫 bundle 自己的 `node_modules/@deepseek-ai/*`(真实存在的副本才报——这才是 #6789 的状态);
|
|
872
|
+
// ② 再看 `dependencies`/`optionalDependencies` 里指向宿主包的**别名**(`npm:` 前缀或键名本身就是宿主包名);
|
|
873
|
+
// ③ "是不是宿主机包"用 `resolveHostVersion`(profile nm + 父层两根)判定,**不用裸前缀**(修 F5);
|
|
874
|
+
// ④ 解析根与 `resolveHostVersion` 一致(含父层,修 F4);排除 `_quarantined` 与本身就是 bundle 的插件包(F5/F6)。
|
|
875
|
+
{
|
|
876
|
+
const roots23 = [join(dir, 'node_modules'), join(dirname(dir), 'node_modules')];
|
|
877
|
+
const bundleSet = new Set(bundles.map(String));
|
|
878
|
+
const quarantined23 = new Set((manifest._quarantined ?? []).map((q) => String(q.name ?? q)));
|
|
879
|
+
const resolveBundleDir = (b) => roots23.map((r) => join(r, String(b))).find((d) => existsSync(join(d, 'package.json'))) ?? null;
|
|
880
|
+
// 判定"这是宿主库还是插件"用**内容**,不用锚点也不用裸前缀——
|
|
881
|
+
// 教训链:① 只看 manifest 键名 → 副本躺在盘上也失明(红队 F1);
|
|
882
|
+
// ② 改用 CLI 安装树(installAnchor)→ 它依赖 PATH 里的 node_modules/.bin,
|
|
883
|
+
// 实测**本机也是 null**,于是永远判不出宿主包(等于静默失效);
|
|
884
|
+
// ③ 现在:读**副本自己的 manifest** —— 带 `dsh.bundle`/`dsh.client` 的是**插件**(依赖它是正确写法,红队 F5),
|
|
885
|
+
// 不带的才是**宿主库**(出现第二份副本即 #6789 的条件)。
|
|
886
|
+
const isPluginPkg = (mf) => Boolean(mf && mf.dsh && (mf.dsh.bundle || mf.dsh.client));
|
|
887
|
+
const readPkg = (d) => { try { return readJsonReportingBom(join(d, 'package.json')).data; } catch { return null; } };
|
|
888
|
+
let scanned23 = 0;
|
|
889
|
+
const issues23 = [];
|
|
890
|
+
for (const b of bundles) {
|
|
891
|
+
if (quarantined23.has(String(b))) continue;
|
|
892
|
+
const bdir = resolveBundleDir(b);
|
|
893
|
+
if (!bdir) continue; // 解析不到 → 由 P1 负责
|
|
894
|
+
scanned23++;
|
|
895
|
+
// ① 真实存在的第二份副本
|
|
896
|
+
let nested = [];
|
|
897
|
+
try { nested = readdirSync(join(bdir, 'node_modules', '@deepseek-ai')); } catch { /* 无嵌套目录 */ }
|
|
898
|
+
for (const n of nested) {
|
|
899
|
+
const full = `@deepseek-ai/${n}`;
|
|
900
|
+
const nestedDir = join(bdir, 'node_modules', '@deepseek-ai', n);
|
|
901
|
+
const nmf = readPkg(nestedDir);
|
|
902
|
+
if (!isPluginPkg(nmf)) {
|
|
903
|
+
issues23.push(`${b} 自带**宿主库**副本 ${full}${nmf?.version ? `@${nmf.version}` : ''}(嵌套在它自己的 node_modules 里,P5 只看顶层所以看不到)`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// ② 别名 / optional 指向宿主包
|
|
907
|
+
let mf23 = null;
|
|
908
|
+
try { mf23 = readJsonReportingBom(join(bdir, 'package.json')).data; } catch { /* 忽略 */ }
|
|
909
|
+
for (const field of ['dependencies', 'optionalDependencies']) {
|
|
910
|
+
for (const [k, v] of Object.entries(mf23?.[field] ?? {})) {
|
|
911
|
+
const target = /^npm:(@deepseek-ai\/[^@]+)@/.exec(String(v));
|
|
912
|
+
const name = target ? target[1] : (k.startsWith('@deepseek-ai/') ? k : null);
|
|
913
|
+
if (!name || bundleSet.has(name)) continue; // 同 scope 的插件依赖插件 = 正确写法(F5)
|
|
914
|
+
// 目标落在 profile 两根里且**不是插件** → 是宿主库被当普通依赖引入(会带来第二份副本)
|
|
915
|
+
const dirs = roots23.map((r) => join(r, name)).concat([join(bdir, 'node_modules', name)]);
|
|
916
|
+
const found = dirs.map(readPkg).find((m) => m && !isPluginPkg(m));
|
|
917
|
+
if (found) issues23.push(`${b} 用 ${field} 引入宿主库 ${name}${target ? `(别名 ${k} → ${v})` : ''}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (issues23.length) {
|
|
922
|
+
report('profile', 'P23', false,
|
|
923
|
+
`已装 bundle 自带**宿主包的第二份副本**(${issues23.length} 处 / 扫过 ${scanned23} 个 bundle)——`
|
|
924
|
+
+ `两份实例的 Symbol/协议不匹配时,典型症状是"第一个会话能用、之后每个会话都失败"(社区 #6789:\`dsh-scope\`):\n `
|
|
925
|
+
+ issues23.slice(0, 5).join('\n ')
|
|
926
|
+
+ (issues23.length > 5 ? `\n …另有 ${issues23.length - 5} 处` : ''),
|
|
927
|
+
'在该插件的 package.json 里把宿主机包从 dependencies/optionalDependencies 改为 **peerDependencies**(宿主机包由宿主共享根提供);'
|
|
928
|
+
+ '若副本是构建产物带进来的,清理其 node_modules 后重装;并向上游报告该发布缺陷',
|
|
929
|
+
undefined, scanned23, '已装 bundle');
|
|
930
|
+
} else {
|
|
931
|
+
report('profile', 'P23', true,
|
|
932
|
+
`已装 bundle 均未自带宿主包副本(扫过 ${scanned23} 个 bundle 的 node_modules 与依赖声明)`,
|
|
933
|
+
undefined, undefined, scanned23, '已装 bundle');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
860
937
|
// P20 / P21:第三方插件把整棵插件树拖垮的两种**静态可判**形态(社区 #6693 实测)
|
|
861
938
|
// 报告者在 host 侧 `apply()` 里连续撞了两个独立致命错、client 侧撞了两个静默白屏错;
|
|
862
939
|
// 其核心抱怨是"唯一的诊断入口被故障本身摧毁"。这两条都不需要执行插件代码即可判定。
|
|
@@ -957,7 +1034,7 @@ function checkProfile(name) {
|
|
|
957
1034
|
} else if (notes21.length) {
|
|
958
1035
|
report('profile', 'P21', true, `未见裸引用沙箱专属符号(${notes21.length} 处有 typeof 守卫,未计入)`, undefined, undefined, hostFiles.length + clientFiles.length, 'host/client 文件');
|
|
959
1036
|
} else {
|
|
960
|
-
report('profile', 'P21', true, `扫描 ${hostFiles.length} 个 host 入口与 ${clientFiles.length} 个 client
|
|
1037
|
+
report('profile', 'P21', true, `扫描 ${hostFiles.length} 个 host 入口与 ${clientFiles.length} 个 client 产物,未见沙箱专属符号引用`, undefined, undefined, hostFiles.length + clientFiles.length, 'host/client 文件');
|
|
961
1038
|
}
|
|
962
1039
|
}
|
|
963
1040
|
|
|
@@ -2391,6 +2468,7 @@ function checkCatalog(ctx, catalog) {
|
|
|
2391
2468
|
// 于是"没有目标可查"被记成**通过**(实测:无 profile 目录时 P6 仍 pass)。判据用探测自己的话:
|
|
2392
2469
|
// **凡自述跳过的一律是 skip** —— 这类"文案与状态不一致"只能从结构上消灭,不能靠逐个改分支。
|
|
2393
2470
|
if (!r.skipped && r.ok === true && /跳过|不适用/.test(String(r.detail))) r.skipped = true;
|
|
2471
|
+
// R1(红队,2026-09-16;**曾被后续编辑覆盖回去,由离线目录用例抓回**):自报 skipped 必须是 skip。
|
|
2394
2472
|
if (r.skipped) { reportSkip(check.section, check.id, r.detail, 'catalog'); continue; }
|
|
2395
2473
|
const severity = check.severity ?? 'error';
|
|
2396
2474
|
catalogSeverity.set(check.id, severity);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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": [
|