dsh-skill-browser 1.9.10 → 2.0.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +80 -81
  3. package/docs/images/panel-browse.png +0 -0
  4. package/docs/images/panel-ledger.png +0 -0
  5. package/lib/client.js +1381 -269
  6. package/lib/client.js.bak-6colimp-20261028-230915 +3748 -0
  7. package/lib/client.js.bak-auth12-20261029-020107 +3749 -0
  8. package/lib/client.js.bak-auth12b-20261029-020140 +3749 -0
  9. package/lib/client.js.bak-auth12c-20261029-020206 +3749 -0
  10. package/lib/client.js.bak-auth12d-20261029-020232 +3747 -0
  11. package/lib/client.js.bak-auto-20261028-171351 +3669 -0
  12. package/lib/client.js.bak-auto2-20261028-171422 +3676 -0
  13. package/lib/client.js.bak-auto3-20261028-171545 +3676 -0
  14. package/lib/client.js.bak-followball-20261028-224743 +3720 -0
  15. package/lib/client.js.bak-gearico-20261028-230937 +3748 -0
  16. package/lib/client.js.bak-glow-20261030 +3676 -0
  17. package/lib/client.js.bak-grid-20261028-220142 +3680 -0
  18. package/lib/client.js.bak-guide-20261029-003108 +3750 -0
  19. package/lib/client.js.bak-guide2-20261029-003213 +3751 -0
  20. package/lib/client.js.bak-guide3-20261029-003236 +3763 -0
  21. package/lib/client.js.bak-guide4-20261029-003249 +3768 -0
  22. package/lib/client.js.bak-guidefc-20261029-003535 +3775 -0
  23. package/lib/client.js.bak-guideico-20261029-013809 +3776 -0
  24. package/lib/client.js.bak-guideorder-20261029-004345 +3775 -0
  25. package/lib/client.js.bak-headbtn-20261028-232114 +3750 -0
  26. package/lib/client.js.bak-menu6col-20261028-225126 +3741 -0
  27. package/lib/client.js.bak-menu6col2-20261028-225348 +3741 -0
  28. package/lib/client.js.bak-menu6col2-20261028-225527 +3741 -0
  29. package/lib/client.js.bak-monobtn-20261028-230502 +3741 -0
  30. package/lib/client.js.bak-navsimple-20261029-015235 +3786 -0
  31. package/lib/client.js.bak-nobadge-20261029-015354 +3784 -0
  32. package/lib/client.js.bak-nodrag-20261028-170713 +3651 -0
  33. package/lib/client.js.bak-official-20261028-171240 +3653 -0
  34. package/lib/client.js.bak-official2-20261028-171304 +3668 -0
  35. package/lib/client.js.bak-renumber-20261028-215518 +3681 -0
  36. package/lib/client.js.bak-sbgap-20261029-013144 +3776 -0
  37. package/lib/client.js.bak-sbgap-20261029-013202 +3776 -0
  38. package/lib/client.js.bak-sbrowh-20261029-012307 +3776 -0
  39. package/lib/client.js.bak-sbrowh-20261029-012333 +3776 -0
  40. package/lib/client.js.bak-scope-sb-20261029-011711 +3776 -0
  41. package/lib/client.js.bak-skinclarity-20261029-011027 +3776 -0
  42. package/lib/client.js.bak-skincols-20261028-224614 +3720 -0
  43. package/lib/client.js.bak-star-20261028-231205 +3748 -0
  44. package/lib/client.js.bak-star-20261028-231233 +3748 -0
  45. package/lib/client.js.bak-sync-20261028-220538 +3680 -0
  46. package/lib/groups.js +171 -21
  47. package/lib/index.js +141 -2
  48. package/lib/mcp.js +853 -0
  49. package/locale/en.json +2 -2
  50. package/locale/zh.json +2 -2
  51. package/package.json +87 -87
  52. package/docs/images/skill-browser-panel.png +0 -0
package/lib/groups.js CHANGED
@@ -27,6 +27,8 @@ import { categoriesMeta } from "./scan.js";
27
27
  // askModel() so a host without the package still loads this plugin (the AI
28
28
  // buttons then report "llm service unavailable" instead of breaking everything).
29
29
  var _llmMod = null;
30
+ // 记录不支持 reasoningEffort 的 provider(如 magpie 聚合网关),后续直接不传该参数。
31
+ var _noReasoning = Object.create(null);
30
32
  // llm is an OPTIONAL capability: read it through ctx.get() so the plugin still
31
33
  // loads on a host/profile that has no llm service (official rule: reserve
32
34
  // ctx.<name> for declared injections, use ctx.get() for optional services).
@@ -48,6 +50,48 @@ async function loadLlm() {
48
50
  }
49
51
 
50
52
  var STORE_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "groups.json");
53
+ // AI 分组结果缓存(按技能目录集指纹,24h 有效);只存分组结构,不含技能正文。
54
+ var CLASSIFY_CACHE_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "ai-classify-cache.json");
55
+ var CLASSIFY_CACHE_TTL = 24 * 3600 * 1000;
56
+ function cacheHash(str) {
57
+ var h = 5381;
58
+ for (var i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) | 0;
59
+ return (h >>> 0).toString(36);
60
+ }
61
+ function loadClassifyCache() {
62
+ try {
63
+ if (!existsSync(CLASSIFY_CACHE_FILE)) return {};
64
+ return JSON.parse(String(readFileSync(CLASSIFY_CACHE_FILE, "utf8")).replace(/^\uFEFF/, "")) || {};
65
+ } catch (e) { return {}; }
66
+ }
67
+ function saveClassifyCache(map) {
68
+ try {
69
+ mkdirSync(dirname(CLASSIFY_CACHE_FILE), { recursive: true });
70
+ writeFileSync(CLASSIFY_CACHE_FILE, JSON.stringify(map), "utf8");
71
+ } catch (e) { /* best-effort */ }
72
+ }
73
+ // 清除 AI 分组结果缓存(只动缓存文件,绝不碰任何技能文件)。
74
+ export function clearClassifyCache() {
75
+ try {
76
+ if (existsSync(CLASSIFY_CACHE_FILE)) writeFileSync(CLASSIFY_CACHE_FILE, "{}", "utf8");
77
+ return { ok: true };
78
+ } catch (e) { return { ok: false, error: String((e && e.message) || e) }; }
79
+ }
80
+ // 并行分批分类总开关:设 false 即退回「串行单次」原方案(回退用,单点切换)。
81
+ var PARALLEL_CLASSIFY = true;
82
+ // 合并多批结果:按「规范化大类名」归并去重(同名大类的 subs 合并)。
83
+ function mergeDomainsByName(list) {
84
+ var order = [], byKey = {};
85
+ for (var i = 0; i < list.length; i++) {
86
+ var d = list[i] || {};
87
+ var key = String(d.name || "").replace(/\s+/g, "").toLowerCase() || ("__" + i);
88
+ var t = byKey[key];
89
+ if (!t) { t = byKey[key] = { name: d.name, emoji: d.emoji, subs: [] }; order.push(key); }
90
+ var ss = Array.isArray(d.subs) ? d.subs : [];
91
+ for (var j = 0; j < ss.length; j++) t.subs.push(ss[j]);
92
+ }
93
+ return order.map(function (k) { return byKey[k]; });
94
+ }
51
95
  // Two levels (domain -> sub-group) means a 109-skill library can legitimately
52
96
  // need 10 domains x 5 sub-groups, so the cap has to be generous.
53
97
  var MAX_GROUPS = 160;
@@ -448,12 +492,14 @@ export async function resolveModelRoute(ctx, cfg) {
448
492
  return route;
449
493
  }
450
494
  var tried = [];
451
- var r2 = routeFromDefaultModel(ctx);
452
- if (r2) return r2;
453
- tried.push("agentDefaultModel");
495
+ // v2.0.9 用户要求:优先「当前对话模型」(live agent session selection),
496
+ // 其次才退 DSH 默认模型,最后目录兜底 —— 这样 AI 分组跟随你当前对话用的模型。
454
497
  var r3 = routeFromAgent(ctx);
455
498
  if (r3) return r3;
456
499
  tried.push("agent.session");
500
+ var r2 = routeFromDefaultModel(ctx);
501
+ if (r2) return r2;
502
+ tried.push("agentDefaultModel");
457
503
  var r4 = await routeFromLlmCatalog(ctx);
458
504
  if (r4) return r4;
459
505
  tried.push("llm.listProviders");
@@ -486,13 +532,14 @@ export async function describeModelRoute(ctx, cfg) {
486
532
  }
487
533
 
488
534
 
489
- async function askModel(ctx, cfg, system, userText, purpose, maxTokens) {
535
+ export async function askModel(ctx, cfg, system, userText, purpose, maxTokens, opts) {
490
536
  // resolveModelRoute is async: stage 4 may query the host's provider catalog.
491
537
  var route = await resolveModelRoute(ctx, cfg);
492
538
  var llm = await loadLlm();
493
539
  var messages = [
494
540
  llm.createUserMessage({ content: [{ type: "text", text: userText }], source: { kind: "dsh-skill-browser" } })
495
541
  ];
542
+ var o2 = opts || {};
496
543
  var options = {
497
544
  provider: route.provider,
498
545
  model: route.model,
@@ -500,9 +547,11 @@ async function askModel(ctx, cfg, system, userText, purpose, maxTokens) {
500
547
  system: system,
501
548
  maxTokens: maxTokens || 2400,
502
549
  purpose: purpose,
503
- signal: AbortSignal.timeout(120000)
550
+ signal: AbortSignal.timeout(o2.timeoutMs || 120000)
504
551
  };
505
- if (route.reasoningEffort) options.reasoningEffort = route.reasoningEffort;
552
+ // AI 分组/封面属于结构化整理任务,不需要深度推理:显式压到 low 可明显缩短等待。
553
+ if (o2.reasoningEffort && !_noReasoning[String(route.provider)]) options.reasoningEffort = String(o2.reasoningEffort);
554
+ else if (route.reasoningEffort) options.reasoningEffort = route.reasoningEffort;
506
555
  try {
507
556
  var sid = ctx.agent && ctx.agent.session && ctx.agent.session.id;
508
557
  if (sid) options.sessionId = String(sid);
@@ -518,8 +567,39 @@ async function askModel(ctx, cfg, system, userText, purpose, maxTokens) {
518
567
 
519
568
  var llmSvc = getLlm(ctx);
520
569
  if (!llmSvc) throw new Error("宿主未提供 llm 服务,AI 功能不可用");
521
- var assembler = new llm.BlockAssembler();
522
- for await (var chunk of llmSvc.stream(options)) assembler.push(chunk);
570
+
571
+ // 一次完整尝试:跑流 + 检查 finish。finish 里的 failure 也是错误来源
572
+ // (UNSUPPORTED_REASONING_EFFORT 就是从 finish 抛出的,必须一起判)。
573
+ async function attemptOnce(opts2) {
574
+ var asm = new llm.BlockAssembler();
575
+ for await (var ch of llmSvc.stream(opts2)) asm.push(ch);
576
+ var fin = asm.finish;
577
+ if (fin && fin.kind && fin.kind !== "stop") {
578
+ var fl = fin.failure;
579
+ var dt = fl ? [fl.code ? "[" + String(fl.code) + "]" : "", String(fl.message || fl)].filter(Boolean).join(" ") : "(无 failure 详情)";
580
+ var e2 = new Error("模型流异常结束(" + String(fin.kind) + "): " + dt);
581
+ e2.code = fl && fl.code ? String(fl.code) : "LLM_FINISH_" + String(fin.kind).toUpperCase();
582
+ throw e2;
583
+ }
584
+ return asm;
585
+ }
586
+
587
+ var assembler;
588
+ try {
589
+ assembler = await attemptOnce(options);
590
+ } catch (eTry) {
591
+ var sig = String((eTry && (eTry.code || eTry.message)) || eTry);
592
+ // 该 provider 不支持 reasoningEffort(实测 magpie 报 UNSUPPORTED_REASONING_EFFORT):
593
+ // 去掉参数重试,并记住这个 provider,后续调用直接不传,避免每次都白跑一轮。
594
+ if (options.reasoningEffort && /REASONING_EFFORT|reasoning.?effort|unsupported/i.test(sig)) {
595
+ _noReasoning[String(route.provider)] = true;
596
+ delete options.reasoningEffort;
597
+ if (ctx.logger && ctx.logger.warn) ctx.logger.warn('dsh-skill-browser: provider "' + route.provider + '" 不支持 reasoningEffort,已自动去掉并重试');
598
+ assembler = await attemptOnce(options);
599
+ } else {
600
+ throw eTry;
601
+ }
602
+ }
523
603
  var finish = assembler.finish;
524
604
  if (finish && finish.kind && finish.kind !== "stop") {
525
605
  // finish shape is { kind, failure } — NOT { error }.
@@ -579,6 +659,10 @@ var CLASSIFY_SYSTEM =
579
659
  // to the user first, then calls applyPlan() with mode replace|merge - "如果对 AI
580
660
  // 分组满意就替换原来的组" must be an explicit user decision, never a side effect.
581
661
  export async function aiClassify(ctx, cfg, skills, opts) {
662
+ // 【铁律·用户强制】AI 分组是纯只读流程:只读取技能元数据(frontmatter),
663
+ // 绝不修改 / 移动 / 重命名 / 删除任何技能文件(SKILL.md 或技能目录)。
664
+ // 本流程只写 ① 结果缓存 ai-classify-cache.json;分组方案不落盘(落盘由用户确认后
665
+ // 的 applyPlan 单独写 groups.json,同样不碰任何技能文件)。
582
666
  if (!getLlm(ctx)) throw new Error("宿主未提供 llm 服务,AI 功能不可用");
583
667
  var o = opts || {};
584
668
  var mode = o.mode === "merge" ? "merge" : "replace";
@@ -602,16 +686,32 @@ export async function aiClassify(ctx, cfg, skills, opts) {
602
686
  }
603
687
  }
604
688
 
689
+ // ── AI 分组结果缓存(同一批技能 + 同 mode 24h 内直接复用,避免重复调模型)──
690
+ // 指纹只用技能目录名集合,不涉及任何技能内容,因此缓存同样不读取正文。
691
+ var cacheKey = mode + ":" + cacheHash(pool.map(function (x) { return x.dir; }).sort().join("|"));
692
+ var _classifyCache = loadClassifyCache();
693
+ var _cacheHit = _classifyCache[cacheKey];
694
+ if (_cacheHit && _cacheHit.plan && (Date.now() - Number(_cacheHit.ts || 0)) < CLASSIFY_CACHE_TTL) {
695
+ if (ctx.logger && ctx.logger.info) ctx.logger.info("dsh-skill-browser: ai-classify cache hit (" + pool.length + " skills)");
696
+ return { ok: true, plan: _cacheHit.plan, mode: mode, stats: _cacheHit.stats, model: _cacheHit.model || null, cached: true };
697
+ }
698
+
699
+ // 【硬约束·用户强制】AI 分组的模型输入只允许「元数据白名单」:
700
+ // 目录名 dir + 中文名 zh + frontmatter description(各截前 60 字)+ 现分类 cat。
701
+ // 严禁读取 / 拼入 SKILL.md 正文(body / content / fullText 等)。即使上游误传
702
+ // 了正文字段,这里也一律忽略;并用行长断言兜底,防止正文被拼进 prompt。
605
703
  function buildList(arr) {
606
704
  var out = [];
607
705
  for (var i = 0; i < arr.length; i++) {
608
706
  var s = arr[i];
609
- var line = s.dir;
610
- var zh = String(s.zh || "").trim();
611
- var desc = String(s.desc || "").trim();
612
- if (zh) line += " | " + zh.slice(0, 60);
613
- else if (desc) line += " | " + desc.slice(0, 60);
707
+ var line = String(s.dir || "");
708
+ var zh = String(s.zh || "").trim().slice(0, 60);
709
+ var desc = String(s.desc || "").trim().slice(0, 60);
710
+ if (zh) line += " | " + zh;
711
+ else if (desc) line += " | " + desc;
614
712
  if (s.cat && s.cat !== "other") line += " | 现分类:" + (catLabelOf[s.cat] || s.cat);
713
+ // 兜底断言:单行只应是元数据(正常 < 160 字);超长说明混入了正文,拒绝。
714
+ if (line.length > 200) throw new Error("AI 分组输入疑似包含技能正文,已拒绝(只允许元数据)");
615
715
  out.push(line);
616
716
  }
617
717
  return out;
@@ -632,7 +732,7 @@ export async function aiClassify(ctx, cfg, skills, opts) {
632
732
  list.join("\n");
633
733
  // A full 109-skill tree easily exceeds 4k tokens; truncation would parse-fail
634
734
  // and surface as "AI 分组坏了".
635
- var res = await askModel(ctx, cfg, CLASSIFY_SYSTEM, userText, "skill-browser-classify", 8000);
735
+ var res = await askModel(ctx, cfg, CLASSIFY_SYSTEM, userText, "skill-browser-classify", 6000, { reasoningEffort: "low", timeoutMs: 150000 });
636
736
  var data = extractJson(res.text);
637
737
  var raw = data && Array.isArray(data.domains) ? data.domains : [];
638
738
  if (!raw.length && data && Array.isArray(data.groups)) raw = legacyToDomains(data.groups);
@@ -703,9 +803,50 @@ export async function aiClassify(ctx, cfg, skills, opts) {
703
803
  return out;
704
804
  }
705
805
 
706
- // Pass 1: the whole (or unassigned) catalogue.
707
- var p1 = await classifyOnce(pool, []);
708
- routeUsed = p1.route;
806
+ // Pass 1: 分批并行分类(提速)。PARALLEL_CLASSIFY=false 即退回串行单次(回退开关)。
807
+ // - pool ≥ 45 才分;每批 ~40、最多 4 批并行 Promise.all;
808
+ // - 合并时按「同名大类」归并去重(mergeDomainsByName);
809
+ // - 任一批失败 → 自动回退串行单次,保证可用性(并行只是加速,不是唯一路径)。
810
+ var ranParallel = false, ranBatches = 1;
811
+ async function classifyWhole() {
812
+ if (!PARALLEL_CLASSIFY || pool.length < 45) {
813
+ ranParallel = false; ranBatches = 1;
814
+ var one = await classifyOnce(pool, []);
815
+ return { raw: one.raw, route: one.route };
816
+ }
817
+ var BATCH = 40, nB = Math.ceil(pool.length / BATCH);
818
+ if (nB > 4) { nB = 4; BATCH = Math.ceil(pool.length / nB); }
819
+ var batches = [];
820
+ for (var bi = 0; bi < nB; bi++) batches.push(pool.slice(bi * BATCH, bi * BATCH + BATCH));
821
+ var parts = await Promise.all(batches.map(function (bt) { return classifyOnce(bt, []); }));
822
+ var route0 = null, all = [];
823
+ for (var pi = 0; pi < parts.length; pi++) {
824
+ if (parts[pi].route) route0 = parts[pi].route;
825
+ var rr0 = parts[pi].raw || [];
826
+ for (var pj = 0; pj < rr0.length; pj++) all.push(rr0[pj]);
827
+ }
828
+ ranParallel = true; ranBatches = nB;
829
+ return { raw: mergeDomainsByName(all), route: route0 };
830
+ }
831
+
832
+ var p1;
833
+ try {
834
+ p1 = await classifyWhole();
835
+ } catch (eP1) {
836
+ ranParallel = false; ranBatches = 1;
837
+ var msg1 = String((eP1 && eP1.message) || eP1);
838
+ if (ctx.logger && ctx.logger.warn) ctx.logger.warn("dsh-skill-browser: classify batch failed, fallback serial: " + msg1);
839
+ try {
840
+ var fb = await classifyOnce(pool, []);
841
+ p1 = { raw: fb.raw, route: fb.route };
842
+ } catch (eP1b) {
843
+ if (/abort|timeout|中断/i.test(String((eP1b && eP1b.message) || eP1b))) {
844
+ var fb2 = await classifyOnce(pool, []);
845
+ p1 = { raw: fb2.raw, route: fb2.route };
846
+ } else { throw eP1b; }
847
+ }
848
+ }
849
+ routeUsed = p1.route || routeUsed;
709
850
  if (!p1.raw.length) throw new Error("模型没有给出任何分组");
710
851
  absorb(p1.raw);
711
852
 
@@ -713,7 +854,8 @@ export async function aiClassify(ctx, cfg, skills, opts) {
713
854
  // Re-ask only about the leftovers so they get real semantic groups instead of
714
855
  // one "其他" bin.
715
856
  var left = leftoversOf();
716
- if (left.length && plan.domains.length) {
857
+ // 只有遗漏较多时才补第二次调用(每次调用几十秒,少一次就快一倍)。
858
+ if (left.length > 20 && plan.domains.length) {
717
859
  var names = [];
718
860
  for (var cn = 0; cn < plan.domains.length; cn++) names.push(plan.domains[cn].name);
719
861
  try {
@@ -753,13 +895,21 @@ export async function aiClassify(ctx, cfg, skills, opts) {
753
895
  subCount += plan.domains[a].subs.length;
754
896
  for (var b = 0; b < plan.domains[a].subs.length; b++) covered += plan.domains[a].subs[b].skills.length;
755
897
  }
756
- return {
898
+ var _result = {
757
899
  ok: true,
758
900
  plan: plan,
759
901
  mode: mode,
760
902
  stats: { skills: pool.length, domains: plan.domains.length, subs: subCount, covered: covered },
761
- model: routeUsed
903
+ model: routeUsed,
904
+ parallel: ranParallel,
905
+ batches: ranBatches
762
906
  };
907
+ // 写入缓存(best-effort;plan 只含分组结构,不含任何技能正文)。
908
+ try {
909
+ _classifyCache[cacheKey] = { ts: Date.now(), mode: mode, model: routeUsed, stats: _result.stats, plan: plan };
910
+ saveClassifyCache(_classifyCache);
911
+ } catch (eC) { /* ignore */ }
912
+ return _result;
763
913
  }
764
914
 
765
915
  // Turn a flat [{"name","skills":[...]}] reply into the two-level shape.
@@ -948,7 +1098,7 @@ export async function aiCover(ctx, cfg, group, sampleSkills) {
948
1098
  "分组名称:" + group.name + "\n" +
949
1099
  (sample ? "包含技能:" + sample + "\n" : "") +
950
1100
  "请为这个分组生成一张 200x200 的 SVG 封面图。";
951
- var res = await askModel(ctx, cfg, COVER_SYSTEM, userText, "skill-browser-cover", 2000);
1101
+ var res = await askModel(ctx, cfg, COVER_SYSTEM, userText, "skill-browser-cover", 2000, { reasoningEffort: "low", timeoutMs: 60000 });
952
1102
  var svg = cleanSvg(res.text);
953
1103
  var saved = setGroupCover(group.id, svg);
954
1104
  if (!saved.ok) throw new Error(saved.error || "封面保存失败");
package/lib/index.js CHANGED
@@ -45,8 +45,17 @@ var listGroups = grMod.listGroups, createGroup = grMod.createGroup,
45
45
  describeModelRoute = grMod.describeModelRoute;
46
46
  var keywordClassify = kwMod.keywordClassify;
47
47
 
48
+ // ── v2.0.0 MCP 管理子模块(同样走 RELOAD_V 热加载) ──
49
+ var mcpMod = await import("./mcp.js" + RELOAD_V);
50
+ var collectMcp = mcpMod.collectMcp, setMcpEnabled = mcpMod.setMcpEnabled,
51
+ reconnectMcp = mcpMod.reconnectMcp, recordMcpCall = mcpMod.recordMcpCall,
52
+ getMcpStats = mcpMod.getMcpStats, clearMcpStats = mcpMod.clearMcpStats,
53
+ exportMcp = mcpMod.exportMcp,
54
+ detectMcpResidue = mcpMod.detectMcpResidue, cleanMcpResidue = mcpMod.cleanMcpResidue,
55
+ scanLocalMcp = mcpMod.scanLocalMcp, mcpAiDiagnose = mcpMod.mcpAiDiagnose, mcpAiFixApply = mcpMod.mcpAiFixApply;
56
+
48
57
  var PREFIX = "/dsh-skill-browser";
49
- export var VERSION = "1.9.10";
58
+ export var VERSION = "2.0.0";
50
59
  var CACHE_TTL_MS = 30 * 1000;
51
60
  var DIAG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "diag.log");
52
61
  var CONFIG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "config.json");
@@ -278,6 +287,34 @@ export function createApi(ctx, config) {
278
287
  ctx.logger.warn("dsh-skill-browser: tools/result listener failed: " + (e && e.message));
279
288
  }
280
289
 
290
+ // ── v2.0.0 MCP 调用统计:官方 tools/result 事件,按 mcp__<server>__ 前缀归类 ──
291
+ var offMcp = null;
292
+ try {
293
+ offMcp = ctx.on("tools/result", function (exec, result) {
294
+ try {
295
+ var nm = exec && exec.name;
296
+ if (typeof nm !== "string" || nm.indexOf("mcp__") !== 0) return;
297
+ var seg = nm.split("__");
298
+ var server = seg.length >= 3 && seg[1] ? seg[1] : "(unknown)";
299
+ var ok = !(result && result.isError);
300
+ var err = "";
301
+ if (!ok && result) {
302
+ if (result.error && typeof result.error === "string") err = result.error;
303
+ else if (Array.isArray(result.content)) {
304
+ for (var ii = 0; ii < result.content.length; ii++) {
305
+ var blk = result.content[ii];
306
+ if (blk && typeof blk.text === "string") { err = blk.text; break; }
307
+ }
308
+ }
309
+ }
310
+ recordMcpCall(server, nm, ok, err, 0);
311
+ } catch (e2) { /* 统计失败绝不影响工具调用本身 */ }
312
+ });
313
+ ctx.logger.info("dsh-skill-browser: mcp call recorder armed (tools/result)");
314
+ } catch (e) {
315
+ ctx.logger.warn("dsh-skill-browser: mcp stats listener failed: " + (e && e.message));
316
+ }
317
+
281
318
  function buildPayload() {
282
319
  var skills = scanSkills(root, ZH);
283
320
  // Per-category counts, emitted in CATS order so the client renders a
@@ -603,6 +640,11 @@ function getLedgerPayload(force) {
603
640
  }
604
641
  return;
605
642
  }
643
+ if (action === "clear-classify-cache") {
644
+ var ccRes = grMod.clearClassifyCache ? grMod.clearClassifyCache() : { ok: false, error: "groups 模块不支持清缓存" };
645
+ sendJson(res, ccRes.ok ? 200 : 500, ccRes);
646
+ return;
647
+ }
606
648
  if (action === "ai-classify") {
607
649
  var skills = scanSkills(root, ZH);
608
650
  if (!skills.length) { sendJson(res, 400, { ok: false, error: "技能库为空或未找到技能" }); return; }
@@ -900,11 +942,108 @@ function getLedgerPayload(force) {
900
942
  return;
901
943
  }
902
944
 
945
+ // ── v2.0.0 MCP 管理:列表 / 状态 / 工具清单 / 统计 / 启停 / 重连 / 导出 ──
946
+ function readMcpBody(cb) {
947
+ var chunks = [];
948
+ req.on("data", function (c) { chunks.push(c); });
949
+ req.on("end", function () {
950
+ var obj = {};
951
+ try { obj = JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"); } catch (e) { obj = {}; }
952
+ cb(obj);
953
+ });
954
+ }
955
+ if (path === PREFIX + "/mcp/local-scan") {
956
+ try { sendJson(res, 200, scanLocalMcp(ctx)); }
957
+ catch (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
958
+ return;
959
+ }
960
+ if (path === PREFIX + "/mcp/ai-diagnose") {
961
+ mcpAiDiagnose(ctx).then(function (r) { sendJson(res, r && r.ok ? 200 : 500, r); })
962
+ .catch(function (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); });
963
+ return;
964
+ }
965
+ if (path === PREFIX + "/mcp/ai-fix/apply" && req.method === "POST") {
966
+ readBody(req, function (body) {
967
+ mcpAiFixApply(ctx, (body && body.actions) || []).then(function (r) { sendJson(res, 200, r); })
968
+ .catch(function (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); });
969
+ });
970
+ return;
971
+ }
972
+ if (path === PREFIX + "/mcp/servers") {
973
+ try { sendJson(res, 200, collectMcp(ctx, /(^|&)(refresh=1|t=)/.test(query))); }
974
+ catch (e) {
975
+ ctx.logger.warn("dsh-skill-browser: mcp list failed " + (e && e.message));
976
+ sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
977
+ }
978
+ return;
979
+ }
980
+ if (path === PREFIX + "/mcp/stats") {
981
+ try { sendJson(res, 200, getMcpStats({ since: qp(query, "since") })); }
982
+ catch (e) {
983
+ ctx.logger.warn("dsh-skill-browser: mcp stats failed " + (e && e.message));
984
+ sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
985
+ }
986
+ return;
987
+ }
988
+ if (path === PREFIX + "/mcp/stats/clear" && req.method === "POST") {
989
+ try { sendJson(res, 200, clearMcpStats()); }
990
+ catch (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); }
991
+ return;
992
+ }
993
+ if (path === PREFIX + "/mcp/export") {
994
+ try { sendJson(res, 200, exportMcp(ctx, { mask: /(^|&)mask=1(&|$)/.test(query) })); }
995
+ catch (e) {
996
+ ctx.logger.warn("dsh-skill-browser: mcp export failed " + (e && e.message));
997
+ sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
998
+ }
999
+ return;
1000
+ }
1001
+ if (path === PREFIX + "/mcp/residue") {
1002
+ try { sendJson(res, 200, detectMcpResidue(ctx)); }
1003
+ catch (e) {
1004
+ ctx.logger.warn("dsh-skill-browser: mcp residue detect failed " + (e && e.message));
1005
+ sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
1006
+ }
1007
+ return;
1008
+ }
1009
+ if (path === PREFIX + "/mcp/residue/clean" && req.method === "POST") {
1010
+ try {
1011
+ var cres = cleanMcpResidue(ctx);
1012
+ ctx.logger.info("dsh-skill-browser: mcp residue cleaned=" + cres.cleaned);
1013
+ sendJson(res, 200, cres);
1014
+ } catch (e) {
1015
+ ctx.logger.warn("dsh-skill-browser: mcp residue clean failed " + (e && e.message));
1016
+ sendJson(res, 500, { ok: false, error: String((e && e.message) || e) });
1017
+ }
1018
+ return;
1019
+ }
1020
+ if ((path === PREFIX + "/mcp/enable" || path === PREFIX + "/mcp/disable") && req.method === "POST") {
1021
+ var wantEnable = path.indexOf("/mcp/enable") >= 0;
1022
+ readMcpBody(function (body) {
1023
+ var r;
1024
+ try { r = setMcpEnabled(ctx, String(body.id || ""), wantEnable); }
1025
+ catch (e) { r = { ok: false, error: String((e && e.message) || e) }; }
1026
+ sendJson(res, r.ok ? 200 : 500, r);
1027
+ });
1028
+ return;
1029
+ }
1030
+ if (path === PREFIX + "/mcp/reconnect" && req.method === "POST") {
1031
+ readMcpBody(function (body) {
1032
+ Promise.resolve(reconnectMcp(ctx, String(body.id || ""))).then(function (r) {
1033
+ sendJson(res, r && r.ok ? 200 : 500, r);
1034
+ }).catch(function (e) { sendJson(res, 500, { ok: false, error: String((e && e.message) || e) }); });
1035
+ });
1036
+ return;
1037
+ }
1038
+
903
1039
  sendJson(res, 404, { ok: false, error: "unknown path" });
904
1040
  };
905
1041
 
906
1042
  return {
907
1043
  handler: handler,
908
- dispose: function () { try { if (offTools) offTools(); } catch (e) {} }
1044
+ dispose: function () {
1045
+ try { if (offTools) offTools(); } catch (e) {}
1046
+ try { if (offMcp) offMcp(); } catch (e) {}
1047
+ }
909
1048
  };
910
1049
  }