@moonquake2004/dsh-doctor 0.3.3 → 0.4.0

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 CHANGED
@@ -8,7 +8,7 @@ Zero npm dependencies. One file. Runs anywhere `node` exists (`zstd` needed only
8
8
 
9
9
  ## Why
10
10
 
11
- dsh's plugin tree is "fragile by install": a dangling reference, a broken `file:` link, a duplicate entry id, or a corrupted session log can brick the profile at boot or stall the whole web server — and `--dump-config` never mounts the loader, so it passes on broken setups. This class of failure was consolidated in [dsh discussion #1496](https://github.com/deepseek-ai/deepseek-harness/discussions/1496) (Advisory: plugin-install path needs guardrails). `dsh-doctor` is the offline check that advisory calls for — 20 built-in checks mapped to 18 community reports, each verified with synthetic negative fixtures, plus a self-updating remote catalog of declarative pattern checks (v0.2.0).
11
+ dsh's plugin tree is "fragile by install": a dangling reference, a broken `file:` link, a duplicate entry id, or a corrupted session log can brick the profile at boot or stall the whole web server — and `--dump-config` never mounts the loader, so it passes on broken setups. This class of failure was consolidated in [dsh discussion #1496](https://github.com/deepseek-ai/deepseek-harness/discussions/1496) (Advisory: plugin-install path needs guardrails). `dsh-doctor` is the offline check that advisory calls for — 28 built-in checks (env/profile/session) mapped to 18 community reports, each verified with synthetic negative fixtures, plus a self-updating remote catalog of 5 declarative pattern checks.
12
12
 
13
13
  ## Usage
14
14
 
@@ -26,7 +26,7 @@ Exit codes (default mode): `0` = all pass · `1` = problems found (built-in chec
26
26
 
27
27
  With `--envelope` (doctor-contract mode): `0` = all pass · `1` = any WARN · `2` = any FAIL. The envelope follows the shared `dsh-doctor/v1` schema (`{ schema, generatedAt, profile, exitCode, summary, ok, checks:[{name,status,detail}] }`) so implementations are interchangeable for CI/marketplace use. Installed via npm, the CLI is also available as the `dsh-doctor` bin.
28
28
 
29
- ## Checks (20)
29
+ ## Checks (28 built-in + 5 catalog = 33)
30
30
 
31
31
  ### env
32
32
  | ID | Checks | Discussion |
@@ -47,7 +47,9 @@ With `--envelope` (doctor-contract mode): `0` = all pass · `1` = any WARN · `2
47
47
  | P4 | `file:` dependencies intact | [#1197](https://github.com/deepseek-ai/deepseek-harness/discussions/1197) |
48
48
  | P5 | no top-level `@deepseek-ai/*` duplication (dual module instances) | [#1486](https://github.com/deepseek-ai/deepseek-harness/discussions/1486), [#1697](https://github.com/deepseek-ai/deepseek-harness/discussions/1697) |
49
49
  | P7 | `cordis.patch.yml` structural lint (`~ insert:` null-literal typo, tab indentation, missing colon → UI won't boot) | [#1724](https://github.com/deepseek-ai/deepseek-harness/discussions/1724) |
50
- | P12 | profile-installed bundle version vs running CLI (warn on divergence; the web "Doctor" panel / `/dsh-doctor/run` API runs the bundle) | [#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719) v1.1 `installed_bundle` |
50
+ | P12 | profile-installed bundle version vs running CLI (emits vocabulary name `installed_bundle`, #1719 v1.1: skip when unlisted / warn on manifest-lies or divergence / pass when equal; the web "Doctor" panel / `/dsh-doctor/run` API run the bundle) | [#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719) |
51
+ | P13 | client-half `provide` service name clashes with core client services (`chatFileMentions` etc. from `@deepseek-ai/dsh-client-*`, warn) or cross-bundle same-name grabs (browser-side "service already registered" → UI white screen, server logs see nothing) | [#2752](https://github.com/deepseek-ai/deepseek-harness/discussions/2752) |
52
+ | P14 | declared `bin` executability (target file present + shebang required for text `bin`; exec-bit alone does not identify the interpreter → ENOEXEC on direct run, #1846) | [#1846](https://github.com/deepseek-ai/deepseek-harness/discussions/1846) |
51
53
 
52
54
  ### session
53
55
  | ID | Checks | Discussion |
@@ -70,7 +72,7 @@ With `--envelope` (doctor-contract mode): `0` = all pass · `1` = any WARN · `2
70
72
 
71
73
  ## Related community tools
72
74
 
73
- > **dsh-doctor/v1 vocabulary r5 compatible** — drafted by [@ciceroyang](https://github.com/ciceroyang) (ciceroyang/dsh-doctor), reviewed by [@sjh9714](https://github.com/sjh9714) (dsh-win32) and [@moonquake2004](https://github.com/moonquake2004) ([#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719)). Our `node`/`pnpm` checks emit the vocabulary names with r5 semantics (pass/warn/fail/skip; `summary.skip` always present).
75
+ > **dsh-doctor/v1 vocabulary r5 compatible, v1.1 `installed_bundle` pending** — drafted by [@ciceroyang](https://github.com/ciceroyang) (ciceroyang/dsh-doctor), reviewed by [@sjh9714](https://github.com/sjh9714) (dsh-win32) and [@moonquake2004](https://github.com/moonquake2004) ([#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719)). Our `node`/`pnpm` checks emit the vocabulary names with r5 semantics (pass/warn/fail/skip; `summary.skip` always present); P12 emits the v1.1 vocabulary name `installed_bundle` (skip/warn/pass/warn four-state, r6 sheet pending).
74
76
 
75
77
  - [zoahdev/dsh-plugin-doctor](https://github.com/zoahdev/dsh-plugin-doctor) — pre-publish plugin bundle health checks (manifest/patch/entry/files/build/pack+fresh-profile install) plus a `profile-shadow` tripwire for host-shadowing (author/CI side). Complementary to this tool's user-side profile/session/env diagnostics; its `profile-shadow` and our P5 flag the same host-shadowing precondition from two sides.
76
78
  - [boyin111-1/dsh-doctor](https://github.com/boyin111-1/dsh-doctor) — sibling offline diagnostic, cross-verified against the same broken fixtures.
@@ -113,7 +115,7 @@ The tool also watches its own npm version: each run compares the installed versi
113
115
 
114
116
  ## Remote check catalog (v0.2.0)
115
117
 
116
- The built-in 20 checks are compiled into the tool. The **catalog** is a second, self-updating layer: `plugin/checks.json` in this repo holds declarative rules (data, not code), and every installed instance picks up new rules automatically — no reinstall needed.
118
+ The built-in 26 checks are compiled into the tool. The **catalog** is a second, self-updating layer: `plugin/checks.json` in this repo holds declarative rules (data, not code), and every installed instance picks up new rules automatically — no reinstall needed.
117
119
 
118
120
  - **How it works**: each run tries to fetch `plugin/checks.json` from GitHub (3s timeout) → on success it's cached to `$DSH_HOME/.cache/dsh-doctor/checks.json` (TTL 6h) → on failure it falls back to the last-known-good cache, then to the bundled copy. New checks therefore arrive within ≤6h of being committed upstream.
119
121
  - **Safety**: rules are **read-only probes** executed by the built-in engine (`command-exists`, `path-*`, `json-valid`, `text-contains` / `text-not-contains`, `file-size-above`, `glob-count`). The remote payload can never run code — it can only add pattern checks.
@@ -141,7 +143,7 @@ Safety invariants: closed probe vocabulary (LLM output is data, never code), pro
141
143
 
142
144
  ## Also installable as a dsh plugin
143
145
 
144
- The tool ships as a proper dsh bundle (`plugin/`), so you can run the same checks (20 built-in + catalog rules) from inside the web UI:
146
+ The tool ships as a proper dsh bundle (`plugin/`), so you can run the same checks (28 built-in + 5 catalog rules) from inside the web UI:
145
147
 
146
148
  ```bash
147
149
  # install into a profile (works from a checkout or a published path)
package/README.zh.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 为什么
8
8
 
9
- dsh 的插件树"装出来就是脆的":一个悬空引用、一个断掉的 `file:` 链接、一个重复的 entry id、一段损坏的会话日志,都能让 profile 在启动时直接挂掉或拖垮整个 web 服务器——而 `--dump-config` 从不挂载 loader,所以在坏配置上也"一切正常"。这类故障被汇总在 [dsh discussion #1496](https://github.com/deepseek-ai/deepseek-harness/discussions/1496)(Advisory:插件安装路径需要护栏)。`dsh-doctor` 就是这个护栏——**20 项内置检查**映射到 18 个社区报告,每一项都用合成负样例验证过;外加一个**自更新的远程检查目录**(声明式规则,v0.2.0 起)。
9
+ dsh 的插件树"装出来就是脆的":一个悬空引用、一个断掉的 `file:` 链接、一个重复的 entry id、一段损坏的会话日志,都能让 profile 在启动时直接挂掉或拖垮整个 web 服务器——而 `--dump-config` 从不挂载 loader,所以在坏配置上也"一切正常"。这类故障被汇总在 [dsh discussion #1496](https://github.com/deepseek-ai/deepseek-harness/discussions/1496)(Advisory:插件安装路径需要护栏)。`dsh-doctor` 就是这个护栏——**26 项内置检查**(env 7 / profile 11 / session 8)映射到 18 个社区报告,每一项都用合成负样例验证过;外加一个**自更新的远程检查目录**(声明式规则,v0.2.0 起)。
10
10
 
11
11
  ## 用法
12
12
 
@@ -21,7 +21,7 @@ node dsh-doctor.mjs --no-catalog # 不拉远程目录(只用内置副
21
21
 
22
22
  退出码:`0` = 全部通过 · `1` = 发现问题(内置检查 + 目录中 `severity: error` 的项)· warn 级目录失败不改退出码。
23
23
 
24
- ## 检查项(20 内置 + 4 目录)
24
+ ## 检查项(28 内置 + 5 目录)
25
25
 
26
26
  ### env
27
27
  | ID | 检查 | 对应讨论 |
@@ -42,7 +42,9 @@ node dsh-doctor.mjs --no-catalog # 不拉远程目录(只用内置副
42
42
  | P4 | `file:` 依赖完整 | [#1197](https://github.com/deepseek-ai/deepseek-harness/discussions/1197) |
43
43
  | P5 | 顶层无 `@deepseek-ai/*` 重复(双模块实例) | [#1486](https://github.com/deepseek-ai/deepseek-harness/discussions/1486), [#1697](https://github.com/deepseek-ai/deepseek-harness/discussions/1697) |
44
44
  | P7 | `cordis.patch.yml` 结构 lint(`~ insert:` null 字面量、tab 缩进、缺冒号、顶层映射+序列混排 → UI 打不开) | [#1724](https://github.com/deepseek-ai/deepseek-harness/discussions/1724) |
45
- | P12 | profile 内 bundle 版本 vs 运行 CLI(分歧 warn;web「诊断」面板 / `/dsh-doctor/run` API 跑的是 bundle) | [#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719) v1.1 `installed_bundle` |
45
+ | P12 | profile 内 bundle 版本 vs 运行 CLI(发词汇名 `installed_bundle`,#1719 v1.1:未声明=skip / manifest 撒谎或分歧=warn / 一致=pass;web「诊断」面板 / `/dsh-doctor/run` API 跑的是 bundle) | [#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719) |
46
+ | P13 | client 半 `provide` 服务名抢注核心客户端服务(`chatFileMentions` 等 `@deepseek-ai/dsh-client-*`,warn)或跨 bundle 同名(浏览器端 service already registered → UI 白屏、服务端日志无感知) | [#2752](https://github.com/deepseek-ai/deepseek-harness/discussions/2752) |
47
+ | P14 | 声明 `bin` 可执行性(目标文件在位 + 文本 bin 必须带 shebang;仅可执行位不识别解释器 → 直接执行 ENOEXEC,#1846) | [#1846](https://github.com/deepseek-ai/deepseek-harness/discussions/1846) |
46
48
 
47
49
  ### session
48
50
  | ID | 检查 | 对应讨论 |
@@ -65,7 +67,7 @@ node dsh-doctor.mjs --no-catalog # 不拉远程目录(只用内置副
65
67
 
66
68
  ## 相关社区工具
67
69
 
68
- > **dsh-doctor/v1 词汇表 r5 兼容** —— 起草 [@ciceroyang](https://github.com/ciceroyang)(ciceroyang/dsh-doctor),审阅 [@sjh9714](https://github.com/sjh9714)(dsh-win32)与 [@moonquake2004](https://github.com/moonquake2004)([#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719))。我们的 `node`/`pnpm` 检查按 r5 语义输出词汇名(pass/warn/fail/skip;`summary.skip` 常驻)。
70
+ > **dsh-doctor/v1 词汇表 r5 兼容,v1.1 `installed_bundle` 待认领** —— 起草 [@ciceroyang](https://github.com/ciceroyang)(ciceroyang/dsh-doctor),审阅 [@sjh9714](https://github.com/sjh9714)(dsh-win32)与 [@moonquake2004](https://github.com/moonquake2004)([#1719](https://github.com/deepseek-ai/deepseek-harness/discussions/1719))。我们的 `node`/`pnpm` 检查按 r5 语义输出词汇名(pass/warn/fail/skip;`summary.skip` 常驻);P12 直接发 v1.1 词汇名 `installed_bundle`(四态 skip/warn/pass/warn,r6 表待发)。
69
71
 
70
72
  - [zoahdev/dsh-plugin-doctor](https://github.com/zoahdev/dsh-plugin-doctor) —— 发布前插件 bundle 健康检查(manifest/patch/entry/files/build/pack+全新 profile 安装)+ 宿主遮蔽 `profile-shadow` 哨兵(作者/CI 侧)。与本工具的用户侧 profile/session/env 诊断互补;它的 `profile-shadow` 与我们的 P5 从两个方向标记同一个宿主遮蔽前置条件。
71
73
  - [boyin111-1/dsh-doctor](https://github.com/boyin111-1/dsh-doctor) —— 同生态位离线诊断兄弟实现,用同一批坏 fixture 交叉验证。
@@ -106,7 +108,7 @@ The `dsh-doctor/v1` envelope (`--json --envelope`) is the machine-readable form
106
108
 
107
109
  ## 远程检查目录(v0.2.0,层 A)
108
110
 
109
- 内置 20 项检查编译在工具里。**目录**是第二层、自更新的:本仓库的 `plugin/checks.json` 放声明式规则(**规则是数据,不是代码**),所有已装实例自动获取新规则——无需重装。
111
+ 内置 26 项检查编译在工具里。**目录**是第二层、自更新的:本仓库的 `plugin/checks.json` 放声明式规则(**规则是数据,不是代码**),所有已装实例自动获取新规则——无需重装。
110
112
 
111
113
  - **工作机制**:每次运行尝试从 GitHub 拉 `plugin/checks.json`(3s 超时)→ 成功后缓存到 `$DSH_HOME/.cache/dsh-doctor/checks.json`(TTL 6h)→ 失败回退 last-known-good 缓存 → 再回退内置副本。新检查因此在上游提交后 ≤6h 内自动到达。
112
114
  - **安全性**:规则是**只读探测原语**,由内置引擎执行(`command-exists`、`path-*`、`json-valid`、`text-contains` / `text-not-contains`、`file-size-above`、`glob-count`)。远程内容永远无法执行代码——只能新增模式检查。
@@ -134,7 +136,7 @@ The `dsh-doctor/v1` envelope (`--json --envelope`) is the machine-readable form
134
136
 
135
137
  ## 也可作为 dsh 插件安装
136
138
 
137
- 工具以标准 dsh bundle 形态发布(`plugin/`),可以在 web UI 里跑同样的检查(20 内置 + 目录规则):
139
+ 工具以标准 dsh bundle 形态发布(`plugin/`),可以在 web UI 里跑同样的检查(28 内置 + 5 目录规则):
138
140
 
139
141
  ```bash
140
142
  # 装进 profile(checkout 或已发布路径均可)
package/client/client.js CHANGED
@@ -1,4 +1,4 @@
1
- window.__ModuleLoader__.load({ id: "dsh-doctor", factory: (require) => {
1
+ window.__ModuleLoader__.load({ id: "@moonquake2004/dsh-doctor", factory: (require) => {
2
2
  var module = { exports: {} }; var exports = module.exports;
3
3
  'use strict'
4
4
 
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  # dsh bundle patch: inserts this plugin into a profile's layer stack.
2
2
  - insert:
3
3
  - id: dsh-doctor
4
- name: 'dsh-doctor'
4
+ name: '@moonquake2004/dsh-doctor'
package/dsh-doctor.mjs CHANGED
@@ -14,7 +14,9 @@
14
14
  * P9 ctx.settings 未声明 inject: ['settings'](#1904⑤:先于 settings 就绪激活 → namespace not registered)
15
15
  * P10 inject 引用客户端专属服务(#1947:@deepseek-ai/dsh-client-* 服务端永不提供 → Fiber 永久 PENDING → web boot 失败)
16
16
  * P11 已装 bundle 的 main 入口产物缺失(#1965:市场装未构建源码树 → ERR_MODULE_NOT_FOUND → boot 崩)
17
- * P12 profile bundle 版本 vs 运行 CLI 版本(#1719 v1.1 `installed_bundle` 候选:web 面板/API 跑的是 profile 里装的 bundle,可与独立 CLI 版本不一致)
17
+ * P13 client provide 服务名抢注核心客户端服务 / bundle 同名(#2752:浏览器端 service already registered UI 白屏,服务端日志无感知)
18
+ * P14 declared bin 可执行性(#1846:打包成功但 bin 缺 shebang/产物 → 直接执行 ENOEXEC;与 P11 互补)
19
+ * P12 `installed_bundle`(#1719 v1.1 词汇:profile 内 bundle 版本 vs 运行 CLI 版本——web 面板/API 跑的是 profile 里装的 bundle,可与独立 CLI 版本不一致)
18
20
  * [session]
19
21
  * S1 孤儿 tool_call(#1363:assistant tool_calls 无对应 tool 结果 → INVALID_REQUEST)
20
22
  * S2 未闭合 turn(#466/#1265:turn/start 无 turn/end → 会话永久"运行中")
@@ -99,6 +101,11 @@ function report(section, id, ok, detail, fix, src) {
99
101
  results.push({ section, id, ok, detail, fix, src: src ?? 'builtin' });
100
102
  }
101
103
 
104
+ /** skip 状态(v1 词汇表 r5:#1719)——"不适用"而非"通过",必须带 reason(detail)。不计入 pass/fail,不翻退出码。 */
105
+ function reportSkip(section, id, detail, src) {
106
+ results.push({ section, id, ok: true, skip: true, detail, src: src ?? 'builtin' });
107
+ }
108
+
102
109
  /** 解析 --profile 参数:名字(如 web)→ $DSH_HOME/profiles/<name>;含路径分隔符/~/开头 → 直接当 profile 目录(契约 harness 传绝对路径)。 */
103
110
  function resolveProfile(name) {
104
111
  if (!name) throw new Error('无效 profile 名');
@@ -532,13 +539,135 @@ function checkProfile(name) {
532
539
  if (entryIssues.length) report('profile', 'P11', false, `已装 bundle 的 main 入口缺失(#1965:市场装源码不跑构建 → ERR_MODULE_NOT_FOUND → dsh web boot 崩溃): ${entryIssues.join('; ')}`, '在插件目录跑构建(pnpm install && pnpm run build 产出 main 指向的文件),或改用打包好的 npm 包安装;monorepo 插件需装子包(dsh-market #18 同族)');
533
540
  else report('profile', 'P11', true, '已装 bundle 的 main 入口产物均在', undefined);
534
541
 
535
- // P12profile 内 bundle 版本 vs 运行 CLI 版本(#1719 v1.1 `installed_bundle` 候选)
542
+ // P13client 端服务名抢注核心客户端服务(#2752:ctx.provide("chatFileMentions") 撞核心 dsh-client-ui-deliverables
543
+ // → 浏览器端 service already registered → Web UI 白屏,服务端日志无感知、报错无冲突来源)
544
+ // 与 P8(adapter provider 服务端冲突)互补:P8 跳过 client/web 产物,P13 专门只扫 client 侧——
545
+ // browser 端 provide 的服务名若与核心客户端服务(@deepseek-ai/dsh-client-*)重名,或两个 bundle 抢注同名,
546
+ // 都会在 client-modules 加载期崩掉整个 UI(fail to load plugins / service has been registered)。
547
+ // 核心名单来源:宿主 dsh 安装目录 + profile node_modules 里的 @deepseek-ai/dsh-client-* 包 client 产物实时收集,
548
+ // 叠加内置种子名单兜底(宿主不可达时仍能查 #2752 的 chatFileMentions 等已知核心服务)。
549
+ const coreClientServices = new Set([
550
+ // 内置种子(核心客户端服务,随 dsh 版本演进,宿主不可达时兜底)
551
+ 'chatFileMentions', 'connection', 'sessions', 'workspaces', 'modules', 'locale',
552
+ ]);
553
+ const collectProvideNames = (fp) => {
554
+ let src;
555
+ try { src = readFileSync(fp, 'utf8'); } catch { return []; }
556
+ const out = [];
557
+ for (const m of src.matchAll(/\.provide\(\s*['"]([^'"]+)['"]/g)) out.push(m[1]);
558
+ return out;
559
+ };
560
+ // client 产物位置:dsh.client 入口(package.json 的 dsh.client 指向的文件)+ client/ 目录下 js
561
+ const collectClientJsFiles = (root) => {
562
+ const out = [];
563
+ try {
564
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
565
+ const entry = pkg.dsh?.client;
566
+ if (typeof entry === 'string' && entry.endsWith('.js')) {
567
+ const ep = join(root, entry);
568
+ if (existsSync(ep)) out.push(ep);
569
+ } else if (entry && typeof entry === 'object' && typeof entry.entry === 'string' && entry.entry.endsWith('.js')) {
570
+ const ep = join(root, entry.entry);
571
+ if (existsSync(ep)) out.push(ep);
572
+ }
573
+ } catch { /* 无 manifest */ }
574
+ const walk = (dir, depth) => {
575
+ if (depth > 3) return;
576
+ let entries;
577
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
578
+ for (const e of entries) {
579
+ if (e.name.startsWith('.') || e.name === 'node_modules') continue;
580
+ const fp = join(dir, e.name);
581
+ if (e.isDirectory()) walk(fp, depth + 1);
582
+ else if (e.name.endsWith('.js')) out.push(fp);
583
+ }
584
+ };
585
+ const cdir = join(root, 'client');
586
+ if (existsSync(cdir)) walk(cdir, 0);
587
+ return [...new Set(out)];
588
+ };
589
+ // 核心客户端服务名单实时收集(宿主 anchor + profile node_modules)
590
+ if (installAnchor) {
591
+ const coreScope = join(installAnchor, '@deepseek-ai');
592
+ if (existsSync(coreScope)) {
593
+ for (const p of readdirSync(coreScope)) {
594
+ if (!/^dsh-client-/.test(p)) continue;
595
+ for (const f of collectClientJsFiles(join(coreScope, p))) {
596
+ for (const n of collectProvideNames(f)) coreClientServices.add(n);
597
+ }
598
+ }
599
+ }
600
+ }
601
+ const clientProvideMap = new Map(); // 服务名 → Set(bundle)
602
+ for (const [b, d] of bundleDirs) {
603
+ for (const f of collectClientJsFiles(d)) {
604
+ for (const n of collectProvideNames(f)) {
605
+ if (!clientProvideMap.has(n)) clientProvideMap.set(n, new Set());
606
+ clientProvideMap.get(n).add(b);
607
+ }
608
+ }
609
+ }
610
+ const coreHits = [...clientProvideMap].filter(([n]) => coreClientServices.has(n));
611
+ const dupHits = [...clientProvideMap].filter(([, v]) => v.size > 1);
612
+ const p13Issues = [];
613
+ for (const [n, bs] of coreHits) {
614
+ p13Issues.push(`服务名 ${n} ∈ 核心客户端服务(${[...bs].join(', ')} 抢注 → 浏览器端 service already registered,UI 白屏 #2752)`);
615
+ }
616
+ for (const [n, bs] of dupHits) {
617
+ if (!coreClientServices.has(n)) p13Issues.push(`服务名 ${n} 被多个插件 client 同时提供(${[...bs].join(', ')} → 同名注册冲突,加载期崩)`);
618
+ }
619
+ if (p13Issues.length) {
620
+ report('profile', 'P13', false, `client 端服务名冲突(#2752:浏览器端 provide 撞核心服务 → UI 白屏且服务端日志无感知): ${p13Issues.join('; ')}`, '改名自有 client 服务(避开核心 dsh-client-* 已注册名),或让冲突双方协商唯一命名;冲突在应用侧降级为局部警告前仍需避名');
621
+ } else {
622
+ report('profile', 'P13', true, 'client 端 provide 服务名无冲突(未撞核心客户端服务、无跨 bundle 同名抢注)', undefined);
623
+ }
624
+
625
+ // P14:declared bin 可执行性(#1846 1052326311 贡献检查点②:dsh-instruction-audit v0.1.0 打包成功但 bin 缺 shebang
626
+ // → 直接执行 ENOEXEC;安装/注册/schema 全过但 pnpm dlx 跑不起来)。与 P11(main 产物缺失)互补:
627
+ // P11 查运行时入口,P14 查 CLI 入口。
628
+ // 判定(2026-08-17 1052326311 实证修正,见 #1846 comment 18056208):文本 bin 必须带 shebang——
629
+ // POSIX 上 executable bit 只授予执行权限,不标识文本文件的解释器;仅 exec bit 无 shebang,os.execve
630
+ // 仍返回 ENOEXEC(errno 8)。故"shebang OR exec-bit"会误放行坏包(good/bad fixture 均 100755,只差 shebang)。
631
+ // 离线静态改查"存在 + shebang"两个必要条件(bin 均为 JS 文本,shebang 是解释器声明的唯一可靠来源)。
632
+ const binIssues = [];
633
+ for (const [b, d] of bundleDirs) {
634
+ let pkg;
635
+ try { pkg = JSON.parse(readFileSync(join(d, 'package.json'), 'utf8')); } catch { continue; }
636
+ const bin = pkg.bin;
637
+ if (!bin) continue;
638
+ const bins = typeof bin === 'string' ? { [b.split('/').pop()]: bin } : bin;
639
+ for (const [binName, rel] of Object.entries(bins)) {
640
+ if (typeof rel !== 'string') continue;
641
+ const fp = join(d, rel);
642
+ if (!existsSync(fp)) {
643
+ binIssues.push(`${b}: bin 声明 ${binName} → ${rel} 但产物缺失(发布后 pnpm dlx/直接执行会失败)`);
644
+ continue;
645
+ }
646
+ let head;
647
+ try { head = readFileSync(fp, 'utf8').slice(0, 2); } catch { head = ''; }
648
+ const shebang = head === '#!';
649
+ // execBit 仅作兜底提示(文本文件解释器识别靠 shebang),不作为通过条件
650
+ if (!shebang) {
651
+ binIssues.push(`${b}: bin ${binName}(${rel})无 shebang——文本 bin 无解释器声明,直接执行 ENOEXEC(#1846 同型,exec bit 不识别解释器)`);
652
+ }
653
+ }
654
+ }
655
+ if (binIssues.length) {
656
+ report('profile', 'P14', false, `declared bin 不可执行(#1846:安装/注册全过但 bin 跑不起来): ${binIssues.join('; ')}`, '给 bin 入口补 `#!/usr/bin/env node`(或 chmod +x);发布前用打包产物实测 `pnpm dlx <pkg>` / 直接执行一次(dsh-testkit 可代为跑真实宿主)');
657
+ } else {
658
+ report('profile', 'P14', true, 'declared bin 均在(存在 + shebang/可执行位)', undefined);
659
+ }
660
+
661
+ // P12 `installed_bundle`(#1719 v1.1 词汇条目):profile 内 bundle 版本 vs 运行 CLI 版本
536
662
  // web 设置「诊断」面板与 /dsh-doctor/run API 跑的是 profile 里装的 bundle;独立 CLI(checkout/npx)是另一个副本——
537
663
  // Layer-B 自更新只比 npm latest vs 运行模块,profile 内 bundle 落后/超前都不报警(dsh-win32/bundle 同坑,sjh9714 先发现的)。
538
- // 语义(#1719 合稿,sjh9714 四态分析):pass/warn 两态 + detail 注明条件——
539
- // manifest 未声明 = pass(CLI 独立运行);manifest 声明但 node_modules 缺失 = warn(manifest 撒谎,运行时从不加载);
664
+ // 语义(#1719 合稿,sjh9714 四态分析 + skip 修正):pass/warn/skip 三态 + detail 注明条件——
665
+ // manifest 未声明 = skip(无对比对象,pass 会让 CI 误判"已同步"——git_bash 同形);
666
+ // manifest 声明但 node_modules 缺失 = warn(manifest 撒谎,运行时从不加载);
540
667
  // 已装且版本一致 = pass;已装但版本分歧 = warn(detail 含 age-gate 提示,升级可能被 pnpm-workspace.yaml 的
541
668
  // minimumReleaseAgeExclude 年龄门暂缓一天,指令不再静默无效——sjh9714 实测)。
669
+ // r6/v1.1 认领后:信封检查名从厂商本地 id `P12-bundle-version` 改为词汇名 `installed_bundle`(#1719 三家对齐,
670
+ // sjh9714 同步改名 `dsh-win32/bundle`→`installed_bundle`,CI 可跨实现断言)。
542
671
  try {
543
672
  const selfName = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).name ?? '@moonquake2004/dsh-doctor';
544
673
  const listed = Object.keys(deps).some((k) => k === selfName || k === 'dsh-doctor');
@@ -549,19 +678,19 @@ function checkProfile(name) {
549
678
  if (existsSync(p)) { bundlePkg = p; break; }
550
679
  }
551
680
  if (!listed && !bundlePkg) {
552
- report('profile', 'P12-bundle-version', true, 'profile 未声明也未安装 dsh-doctor bundle,跳过版本对比(CLI 独立运行)', undefined);
681
+ reportSkip('profile', 'installed_bundle', 'profile 未声明也未安装 dsh-doctor bundle——无对比对象(CLI 独立运行),skip 而非 pass(#1719 installed_bundle 合稿,sjh9714:pass 会让 CI 误判"已同步")');
553
682
  } else if (listed && !bundlePkg) {
554
- report('profile', 'P12-bundle-version', false, `profile 的 package.json 声明了 ${selfName} 依赖,但 node_modules 里没有对应包(manifest 与运行时不一致,web 面板/API 实际加载不到)`, `dsh plugin --profile ${name} install ${selfName}(或先移除该依赖再重装)`);
683
+ report('profile', 'installed_bundle', false, `profile 的 package.json 声明了 ${selfName} 依赖,但 node_modules 里没有对应包(manifest 与运行时不一致,web 面板/API 实际加载不到)`, `dsh plugin --profile ${name} install ${selfName}(或先移除该依赖再重装)`);
555
684
  } else {
556
685
  const bundleVersion = JSON.parse(readFileSync(bundlePkg, 'utf8')).version;
557
686
  const cliVersion = localVersion();
558
687
  const same = bundleVersion === cliVersion;
559
- report('profile', 'P12-bundle-version', same,
688
+ report('profile', 'installed_bundle', same,
560
689
  same ? `profile 内 bundle 版本 ${bundleVersion} 与运行 CLI ${cliVersion} 一致` : `profile 内 bundle 版本 ${bundleVersion} ≠ 运行 CLI ${cliVersion}(web 面板/API 跑的是 bundle,两边行为可能不一致;若刚发布过新版本,升级可能被 pnpm-workspace.yaml 的 minimumReleaseAgeExclude 年龄门暂缓,可次日重试)`,
561
690
  same ? undefined : `同步安装版本:dsh plugin --profile ${name} update ${selfName}(或让 CLI 与 bundle 走同一安装方式)`);
562
691
  }
563
692
  } catch (e) {
564
- report('profile', 'P12-bundle-version', false, `bundle 版本对比异常: ${e.message.slice(0, 60)}`, undefined);
693
+ report('profile', 'installed_bundle', false, `bundle 版本对比异常: ${e.message.slice(0, 60)}`, undefined);
565
694
  }
566
695
  }
567
696
 
@@ -795,10 +924,12 @@ function scanAllSessions() {
795
924
  const REMOTE_CATALOG_URL = 'https://raw.githubusercontent.com/moonquake2004/dsh-doctor/main/plugin/checks.json';
796
925
  const CATALOG_TTL_MS = 6 * 60 * 60 * 1000; // 6h:新检查最长 6h 内自动生效
797
926
  const catalogSeverity = new Map(); // catalog 检查 id → severity('error' | 'warn')
798
- // v1 词汇表 r5 对齐(#1719):E1-pnpm 缺失=warn(corepack 可恢复)、E3-node 越界=warn(EBADENGINE 语义)、P12 bundle 版本分歧=warn(v1.1 `installed_bundle` 候选)——均不翻退出码
927
+ // v1 词汇表 r5 对齐(#1719):E1-pnpm 缺失=warn(corepack 可恢复)、E3-node 越界=warn(EBADENGINE 语义)、installed_bundle(P12)分歧=warn(v1.1 词汇条目)、P13 client 服务名冲突=warn(#2752:按帖子建议降级为局部警告而非白屏)、P14 bin 不可执行=warn(#1846:发布卫生问题,不影响已有 boot 但对新用户 pnpm dlx 失败)——均不翻退出码
799
928
  catalogSeverity.set('E1-pnpm', 'warn');
800
929
  catalogSeverity.set('E3-node', 'warn');
801
- catalogSeverity.set('P12-bundle-version', 'warn');
930
+ catalogSeverity.set('installed_bundle', 'warn');
931
+ catalogSeverity.set('P13', 'warn');
932
+ catalogSeverity.set('P14', 'warn');
802
933
 
803
934
  function bundledCatalog() {
804
935
  const p = new URL('./checks.json', import.meta.url);
@@ -1151,8 +1282,8 @@ async function run() {
1151
1282
  const bad = results.filter((r) => !r.ok && catalogSeverity.get(r.id) !== 'warn');
1152
1283
  if (jsonOut && process.argv.includes('--envelope')) {
1153
1284
  // v1 契约信封(dsh doctor 规格,zoahdev/doctor 对齐):status 小写 + 退出码 0/1/2
1154
- const st = (r) => (!r.ok ? (catalogSeverity.get(r.id) === 'warn' ? 'warn' : 'fail') : 'pass');
1155
- const summary = { pass: 0, warn: 0, fail: 0, skip: 0 }; // skip 常驻(v1 词汇表 r5:#1719),当前无平台作用域检查所以恒为 0
1285
+ const st = (r) => (r.skip ? 'skip' : (!r.ok ? (catalogSeverity.get(r.id) === 'warn' ? 'warn' : 'fail') : 'pass'));
1286
+ const summary = { pass: 0, warn: 0, fail: 0, skip: 0 }; // skip 常驻(v1 词汇表 r5:#1719),r5 后 P12 会在未装 bundle 时实际触发
1156
1287
  const checks = results.map((r) => { summary[st(r)]++; return { name: r.id, status: st(r), detail: r.detail }; });
1157
1288
  const exitCode = summary.fail > 0 ? 2 : summary.warn > 0 ? 1 : 0;
1158
1289
  console.log(JSON.stringify({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.3.3",
4
- "description": "Offline diagnostic for DeepSeek Harness — 25 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.",
3
+ "version": "0.4.0",
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": [
7
7
  "README.md",