@moonquake2004/dsh-doctor 0.2.5 → 0.2.6

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.
Files changed (2) hide show
  1. package/dsh-doctor.mjs +80 -0
  2. package/package.json +1 -1
package/dsh-doctor.mjs CHANGED
@@ -10,6 +10,8 @@
10
10
  * P4 file: 依赖指向不存在的目录(#1197:悬空 file: 链接)
11
11
  * P5 profile 顶层 @deepseek-ai/* 与框架重复(#1486:双模块实例 → Symbol 不匹配)
12
12
  * P7 cordis.patch.yml 结构 lint(#1724:~ insert: 是 YAML null → parsePatchList 崩溃 → UI 打不开;tab 缩进/缺冒号同族)
13
+ * P8 adapter provider 注册冲突(#1904②:两 bundle 抢注同一 provider → boot 时 DUPLICATE_ADAPTER 崩溃)
14
+ * P9 ctx.settings 未声明 inject: ['settings'](#1904⑤:先于 settings 就绪激活 → namespace not registered)
13
15
  * [session]
14
16
  * S1 孤儿 tool_call(#1363:assistant tool_calls 无对应 tool 结果 → INVALID_REQUEST)
15
17
  * S2 未闭合 turn(#466/#1265:turn/start 无 turn/end → 会话永久"运行中")
@@ -398,6 +400,84 @@ function checkProfile(name) {
398
400
  }
399
401
  if (yamlProblems.length) report('profile', 'P7', false, `cordis.patch.yml 结构错误(boot 会崩,UI 打不开 #1724): ${yamlProblems.join('; ')}`, 'patch 必须是顶层纯列表(只有 - insert: / - id: 条目):删掉顶层 key: value 行;~ 是 YAML null;缩进用空格不用 tab');
400
402
  else report('profile', 'P7', true, 'cordis.patch.yml 结构正常(无 tab / 无 ~ insert / 无映射-序列混排)', undefined);
403
+
404
+ // P8/P9 需要扫描 bundle 构建产物:收集目录下有限深度的 .js 文件(lib/dist/根 + main 入口,跳过 node_modules)
405
+ const bundleDirs = new Map(); // bundle 名 → 目录(可解析的)
406
+ for (const b of bundles) {
407
+ const d = findPkg(b);
408
+ if (d) bundleDirs.set(b, d);
409
+ }
410
+ const collectJsFiles = (root, maxDepth = 3) => {
411
+ const out = [];
412
+ const walk = (dir, depth) => {
413
+ if (depth > maxDepth) return;
414
+ let entries;
415
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
416
+ for (const e of entries) {
417
+ if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
418
+ // client/web 是浏览器端产物,不在宿主进程运行(避免 ctx.settings 误报)
419
+ if (e.isDirectory() && (e.name === 'client' || e.name === 'web')) continue;
420
+ const fp = join(dir, e.name);
421
+ if (e.isDirectory()) walk(fp, depth + 1);
422
+ else if (e.name.endsWith('.js') && e.name !== 'cordis.patch.yml') out.push(fp);
423
+ }
424
+ };
425
+ walk(root, 0);
426
+ // 主入口(main 指向的 .js)单独兜底
427
+ try {
428
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
429
+ if (typeof pkg.main === 'string' && pkg.main.endsWith('.js')) {
430
+ const mp = join(root, pkg.main);
431
+ if (existsSync(mp) && !out.includes(mp)) out.push(mp);
432
+ }
433
+ } catch { /* 无 manifest */ }
434
+ return out;
435
+ };
436
+ const readJs = (fp) => { try { return readFileSync(fp, 'utf8'); } catch { return ''; } };
437
+
438
+ // P8:adapter provider 注册冲突(#1904②:两个 bundle 抢注同一 provider → boot 时 DUPLICATE_ADAPTER 崩溃)
439
+ const providerRegs = new Map(); // provider → Set(bundle)
440
+ for (const [b, d] of bundleDirs) {
441
+ for (const f of collectJsFiles(d)) {
442
+ const src = readJs(f);
443
+ for (const m of src.matchAll(/registerAdapter\s*\(\s*\[([^\]]*)\]/g)) {
444
+ for (const pm of m[1].matchAll(/['"]([^'"]+)['"]/g)) {
445
+ if (!providerRegs.has(pm[1])) providerRegs.set(pm[1], new Set());
446
+ providerRegs.get(pm[1]).add(b);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ const adapterConflicts = [...providerRegs].filter(([, v]) => v.size > 1);
452
+ if (adapterConflicts.length) {
453
+ report('profile', 'P8', false, `adapter provider 注册冲突(#1904②:boot 时 DUPLICATE_ADAPTER 崩溃): ${adapterConflicts.map(([p, v]) => `${p}(${[...v].join(' ↔ ')}})`).join('; ')}`, '冲突 provider 只能注册一次:让第三方路由插件用 registerConfigurableProviders 或只注册新路由,移除抢注一方');
454
+ } else {
455
+ report('profile', 'P8', true, '无 adapter provider 注册冲突', undefined);
456
+ }
457
+
458
+ // P9:ctx.settings/ctx.get('settings') 未声明 settings 依赖(#1904⑤:先于 settings 就绪激活 → namespace not registered)
459
+ // 注意边界:sctx.settings 不算(sctx 是别的变量);ctx.inject(["settings"], cb) 运行时声明算满足
460
+ const injectIssues = [];
461
+ for (const [b, d] of bundleDirs) {
462
+ const files = collectJsFiles(d);
463
+ const all = files.map(readJs).join('\n');
464
+ const usesSettings = /(?<![A-Za-z0-9_$])ctx\.(?:get\(\s*['"]settings['"]\s*\)|settings\b)/.test(all);
465
+ if (!usesSettings) continue;
466
+ const declared = [];
467
+ // 收集全部 inject 声明(bundle 里可能混有内部模块的 inject;任一含 settings 即满足)
468
+ for (const m of all.matchAll(/inject\s*=\s*\[([^\]]*)\]/gs)) {
469
+ declared.push(...[...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]));
470
+ }
471
+ for (const m of all.matchAll(/ctx\.inject\s*\(\s*\[([^\]]*)\]/g)) {
472
+ declared.push(...[...m[1].matchAll(/['"]([^'"]+)['"]/g)].map((x) => x[1]));
473
+ }
474
+ const uniq = declared.filter((v, i) => declared.indexOf(v) === i);
475
+ if (!uniq.includes('settings')) {
476
+ injectIssues.push(`${b}(用 ctx.settings 但 settings 依赖未声明${uniq.length ? `,全部 inject: [${uniq.join(', ')}]` : ',未找到任何 inject 声明'})`);
477
+ }
478
+ }
479
+ if (injectIssues.length) report('profile', 'P9', false, `插件用 ctx.settings 但未声明 settings 依赖(#1904⑤:激活时 settings 可能未就绪 → namespace not registered): ${injectIssues.join('; ')}`, '在插件代码加 export const inject = ["settings"](或对可选服务做 undefined 处理)');
480
+ else report('profile', 'P9', true, 'bundle 的 ctx.settings 用法均声明了 settings 依赖(模块 inject 或 ctx.inject)', undefined);
401
481
  }
402
482
 
403
483
  /* ================= session ================= */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Offline diagnostic for DeepSeek Harness — 19 built-in checks + self-updating catalog (Layer A) + self-update check (Layer B); 'Doctor' panel in web UI settings.",
5
5
  "main": "lib/index.js",
6
6
  "files": [