dsh-skill-browser 1.9.11 → 2.0.1

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 (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +7 -0
  3. package/lib/client.js +1147 -137
  4. package/lib/groups.js +171 -21
  5. package/lib/index.js +141 -2
  6. package/lib/mcp.js +853 -0
  7. package/locale/en.json +2 -2
  8. package/locale/zh.json +2 -2
  9. package/package.json +85 -86
  10. package/lib/client.js.bak-6colimp-20261028-230915 +0 -3748
  11. package/lib/client.js.bak-auth12-20261029-020107 +0 -3749
  12. package/lib/client.js.bak-auth12b-20261029-020140 +0 -3749
  13. package/lib/client.js.bak-auth12c-20261029-020206 +0 -3749
  14. package/lib/client.js.bak-auth12d-20261029-020232 +0 -3747
  15. package/lib/client.js.bak-auto-20261028-171351 +0 -3669
  16. package/lib/client.js.bak-auto2-20261028-171422 +0 -3676
  17. package/lib/client.js.bak-auto3-20261028-171545 +0 -3676
  18. package/lib/client.js.bak-followball-20261028-224743 +0 -3720
  19. package/lib/client.js.bak-gearico-20261028-230937 +0 -3748
  20. package/lib/client.js.bak-glow-20261030 +0 -3676
  21. package/lib/client.js.bak-grid-20261028-220142 +0 -3680
  22. package/lib/client.js.bak-guide-20261029-003108 +0 -3750
  23. package/lib/client.js.bak-guide2-20261029-003213 +0 -3751
  24. package/lib/client.js.bak-guide3-20261029-003236 +0 -3763
  25. package/lib/client.js.bak-guide4-20261029-003249 +0 -3768
  26. package/lib/client.js.bak-guidefc-20261029-003535 +0 -3775
  27. package/lib/client.js.bak-guideico-20261029-013809 +0 -3776
  28. package/lib/client.js.bak-guideorder-20261029-004345 +0 -3775
  29. package/lib/client.js.bak-headbtn-20261028-232114 +0 -3750
  30. package/lib/client.js.bak-menu6col-20261028-225126 +0 -3741
  31. package/lib/client.js.bak-menu6col2-20261028-225348 +0 -3741
  32. package/lib/client.js.bak-menu6col2-20261028-225527 +0 -3741
  33. package/lib/client.js.bak-monobtn-20261028-230502 +0 -3741
  34. package/lib/client.js.bak-navsimple-20261029-015235 +0 -3786
  35. package/lib/client.js.bak-nobadge-20261029-015354 +0 -3784
  36. package/lib/client.js.bak-nodrag-20261028-170713 +0 -3651
  37. package/lib/client.js.bak-official-20261028-171240 +0 -3653
  38. package/lib/client.js.bak-official2-20261028-171304 +0 -3668
  39. package/lib/client.js.bak-renumber-20261028-215518 +0 -3681
  40. package/lib/client.js.bak-sbgap-20261029-013144 +0 -3776
  41. package/lib/client.js.bak-sbgap-20261029-013202 +0 -3776
  42. package/lib/client.js.bak-sbrowh-20261029-012307 +0 -3776
  43. package/lib/client.js.bak-sbrowh-20261029-012333 +0 -3776
  44. package/lib/client.js.bak-scope-sb-20261029-011711 +0 -3776
  45. package/lib/client.js.bak-skinclarity-20261029-011027 +0 -3776
  46. package/lib/client.js.bak-skincols-20261028-224614 +0 -3720
  47. package/lib/client.js.bak-star-20261028-231205 +0 -3748
  48. package/lib/client.js.bak-star-20261028-231233 +0 -3748
  49. package/lib/client.js.bak-sync-20261028-220538 +0 -3680
package/lib/client.js CHANGED
@@ -175,10 +175,12 @@ window.__ModuleLoader__.load({
175
175
  mounted: false,
176
176
  open: false,
177
177
  panelBuilt: false,
178
- view: "skills", // "skills" | "ledger"
178
+ view: "skills", // "skills" | "ledger" | "guide" | "mcp"
179
179
  data: null,
180
180
  ledger: null,
181
181
  ledgerDetail: null,
182
+ // ── v2.0.0 MCP 管理 ──
183
+ mcp: null, // { data, stats, loading, statsLoading, error, detailId, busy, exportText, showExport }
182
184
  loading: false,
183
185
  error: "",
184
186
  cat: "all",
@@ -186,6 +188,7 @@ window.__ModuleLoader__.load({
186
188
  q: "",
187
189
  detail: null,
188
190
  sortMode: "name-asc", // v1.7.1 四态:"name-asc" | "name-desc" | "mtime-desc" | "mtime-asc"
191
+ cardCols: (function () { try { var v = parseInt(localStorage.getItem("dsb-card-cols"), 10); return (v >= 2 && v <= 6) ? v : 0; } catch (e) { return 0; } })(), // v2.0.6 技能卡片每行列数(0=自适应)
189
192
  toggling: false, // v1.7.0: toggle 请求进行中(防重复点击)
190
193
  toastTimer: null, // v1.7.1: toast 自动消失定时器句柄
191
194
  batchMenuScope: null, // v1.7.2: 批量确认菜单当前作用域("global" 或分类 id)
@@ -196,12 +199,22 @@ window.__ModuleLoader__.load({
196
199
  group: "all", // "all" | "ungrouped" | <groupId>
197
200
  groupsLoaded: false,
198
201
  groupBusy: "", // "" | "ai-classify" | "ai-cover:<gid>"
202
+ aiStartTs: 0, // v2.0.13 AI/关键词分组计时起点
203
+ aiElapsedMs: 0, // 本次分组耗时(毫秒)
204
+ aiCached: false, // 是否命中结果缓存
205
+ aiTimer: null, // 计时器句柄
206
+ aiLastMs: (function () { try { var v = parseInt(localStorage.getItem("dsb-ai-last-ms"), 10); return v > 0 ? v : 0; } catch (e) { return 0; } })(),
207
+ aiLastCached: (function () { try { return localStorage.getItem("dsb-ai-last-cached") === "1"; } catch (e) { return false; } })(),
208
+ aiLastMode: (function () { try { return localStorage.getItem("dsb-ai-last-mode") || ""; } catch (e) { return ""; } })(),
209
+ aiLastParallel: (function () { try { return localStorage.getItem("dsb-ai-last-par") === "1"; } catch (e) { return false; } })(),
210
+ aiLastBatches: (function () { try { var v = parseInt(localStorage.getItem("dsb-ai-last-bat"), 10); return v > 0 ? v : 0; } catch (e) { return 0; } })(),
199
211
  moveMenuDir: null, // 当前打开"移动到分组"菜单的技能目录
200
212
  // ── v1.9.3 两级分组(大类 → 子类)──
201
213
  gplan: null,
202
214
  gplanMode: "", // AI 产出的待确认方案 {domains:[{name,emoji,subs:[{name,emoji,skills:[]}]}]}
203
215
  gplanBusy: false, // 方案确认弹层是否已打开
204
216
  gsub: "", // 当前选中大类下再选中的子类 id(""=看整个大类)
217
+ gFold: false, // v2.0.12 分组树子类行:true=全部收起 / false=全部展开
205
218
  gmode: "tree", // v1.9.8 分组栏显示模式:"tree" 树状(大类一行+常驻子类)| "flat" 平铺(原样)
206
219
  modelInfo: null, // v1.9.9 AI 模型探针结果 {ok,provider,model,source,error};null=还没探
207
220
  newParent: "", // "新建分组"时选定的上级大类 id(""=建大类)
@@ -470,9 +483,11 @@ window.__ModuleLoader__.load({
470
483
  if (S.group !== "all") {
471
484
  var gid = S.gassign[s.dir] || "";
472
485
  if (S.group === "ungrouped") { if (gid) continue; }
473
- else if (S.gsub) { if (gid !== S.gsub) continue; }
474
486
  else if (!inDomain(S.group, gid)) continue;
475
487
  }
488
+ // v2.0.8 修复:子类过滤独立生效。树状模式直接点子类时 S.group 仍是 all,
489
+ // 原来 gsub 判断被 S.group!=="all" 挡住 → 点了子类列表不变(平铺因先选大类才正常)。
490
+ if (S.gsub && (S.gassign[s.dir] || "") !== S.gsub) continue;
476
491
  if (q) {
477
492
  var hay = (s.dir + " " + s.name + " " + (s.zh || "") + " " + (s.desc || "")).toLowerCase();
478
493
  if (hay.indexOf(q) < 0) continue;
@@ -624,19 +639,47 @@ window.__ModuleLoader__.load({
624
639
  .catch(function (e) { if (cb) cb({ ok: false, error: String((e && e.message) || e) }); });
625
640
  }
626
641
 
642
+ // v2.0.15 Electron 不支持 window.prompt(返回 undefined)→ 新建/重命名/建子类全点不动。
643
+ // 改:面板内统一输入弹层。
644
+ function openTextPrompt(title, defVal, cb) {
645
+ var panel = $(PANEL_ID); if (!panel) return;
646
+ var old = panel.querySelector(".dsb-tprompt"); if (old && old.parentNode) old.parentNode.removeChild(old);
647
+ var wrap = document.createElement("div");
648
+ wrap.className = "dsb-tprompt";
649
+ wrap.innerHTML = '<div class="dsb-tprompt-box">' +
650
+ '<div class="dsb-tprompt-title">' + esc(title) + '</div>' +
651
+ '<input class="dsb-tprompt-inp" type="text" />' +
652
+ '<div class="dsb-tprompt-ops">' +
653
+ '<button class="dsb-btn" data-tp="cancel">取消</button>' +
654
+ '<button class="dsb-btn" data-tp="ok">确定</button>' +
655
+ '</div></div>';
656
+ panel.appendChild(wrap);
657
+ var inp = wrap.querySelector(".dsb-tprompt-inp");
658
+ inp.value = defVal || "";
659
+ setTimeout(function () { try { inp.focus(); inp.select(); } catch (e) { } }, 30);
660
+ function close() { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); }
661
+ function ok() { var v = String(inp.value || "").trim(); close(); if (cb) cb(v); }
662
+ wrap.addEventListener("click", function (ev) {
663
+ var t = ev.target.closest("[data-tp]"); if (!t) return;
664
+ if (t.getAttribute("data-tp") === "ok") ok(); else close();
665
+ });
666
+ inp.addEventListener("keydown", function (ev) {
667
+ if (ev.key === "Enter") { ev.preventDefault(); ok(); }
668
+ else if (ev.key === "Escape") { ev.preventDefault(); close(); }
669
+ });
670
+ }
627
671
  function createGroupFlow() {
628
- var name = "";
629
- try { name = window.prompt(T("groupNamePrompt"), ""); } catch (e) { name = ""; }
630
- if (name == null) return;
631
- name = String(name).trim();
632
- if (!name) return;
633
- postGroups({ action: "create", name: name }, function (d) {
634
- if (d && d.ok) {
635
- showToast("ok", TF("groupCreated", { name: name }));
636
- loadGroups(function () { S.group = (d.group && d.group.id) || S.group; paint(); });
637
- } else {
638
- showToast("err", (d && d.error) || "创建失败");
639
- }
672
+ openTextPrompt(T("groupNamePrompt"), "", function (name0) {
673
+ var name = String(name0 || "").trim();
674
+ if (!name) return;
675
+ postGroups({ action: "create", name: name }, function (d) {
676
+ if (d && d.ok) {
677
+ showToast("ok", TF("groupCreated", { name: name }));
678
+ loadGroups(function () { S.group = (d.group && d.group.id) || S.group; paint(); });
679
+ } else {
680
+ showToast("err", (d && d.error) || "创建失败");
681
+ }
682
+ });
640
683
  });
641
684
  }
642
685
 
@@ -644,36 +687,34 @@ window.__ModuleLoader__.load({
644
687
  function createSubGroupFlow(parentId) {
645
688
  var par = groupById(parentId);
646
689
  if (!par) return;
647
- var name = "";
648
- try { name = window.prompt(TF("groupSubPrompt", { p: par.name }), ""); } catch (e) { return; }
649
- if (name == null) return;
650
- name = String(name).trim();
651
- if (!name) return;
652
- postGroups({ action: "create", name: name, parent: parentId }, function (d) {
653
- if (d && d.ok) {
654
- showToast("ok", TF("groupSubCreated", { name: name }));
655
- loadGroups(function () { S.group = parentId; S.gsub = (d.group && d.group.id) || ""; paint(); });
656
- } else {
657
- showToast("err", (d && d.error) || T("groupSubFail"));
658
- }
690
+ openTextPrompt(TF("groupSubPrompt", { p: par.name }), "", function (name0) {
691
+ var name = String(name0 || "").trim();
692
+ if (!name) return;
693
+ postGroups({ action: "create", name: name, parent: parentId }, function (d) {
694
+ if (d && d.ok) {
695
+ showToast("ok", TF("groupSubCreated", { name: name }));
696
+ loadGroups(function () { S.group = parentId; S.gsub = (d.group && d.group.id) || ""; paint(); });
697
+ } else {
698
+ showToast("err", (d && d.error) || T("groupSubFail"));
699
+ }
700
+ });
659
701
  });
660
702
  }
661
703
 
662
704
  function renameGroupFlow(id) {
663
705
  var g = groupById(id);
664
706
  if (!g) return;
665
- var name = "";
666
- try { name = window.prompt(T("groupNamePrompt"), g.name); } catch (e) { return; }
667
- if (name == null) return;
668
- name = String(name).trim();
669
- if (!name || name === g.name) return;
670
- postGroups({ action: "rename", id: id, name: name }, function (d) {
671
- if (d && d.ok) {
672
- showToast("ok", TF("groupRenamed", { name: name }));
673
- loadGroups(function () { paint(); });
674
- } else {
675
- showToast("err", (d && d.error) || "重命名失败");
676
- }
707
+ openTextPrompt(T("groupNamePrompt"), g.name, function (name0) {
708
+ var name = String(name0 || "").trim();
709
+ if (!name || name === g.name) return;
710
+ postGroups({ action: "rename", id: id, name: name }, function (d) {
711
+ if (d && d.ok) {
712
+ showToast("ok", TF("groupRenamed", { name: name }));
713
+ loadGroups(function () { paint(); });
714
+ } else {
715
+ showToast("err", (d && d.error) || "重命名失败");
716
+ }
717
+ });
677
718
  });
678
719
  }
679
720
 
@@ -694,17 +735,59 @@ window.__ModuleLoader__.load({
694
735
  });
695
736
  }
696
737
 
738
+ // v2.0.13 分组计时:进行中实时刷新 AI 按钮 + 面板内进度浮窗;结束后在方案弹层显示累计用时。
739
+ function aiProgEl(create) {
740
+ var panel = $(PANEL_ID); if (!panel) return null;
741
+ var el = panel.querySelector(".dsb-ai-prog");
742
+ if (!el && create) {
743
+ el = document.createElement("div");
744
+ el.className = "dsb-ai-prog";
745
+ el.innerHTML = '<div class="dsb-ai-prog-txt"></div><div class="dsb-ai-prog-bar"><i></i></div>';
746
+ panel.appendChild(el);
747
+ }
748
+ return el;
749
+ }
750
+ function stopAiTimer() {
751
+ if (S.aiTimer) { clearInterval(S.aiTimer); S.aiTimer = null; }
752
+ var el = aiProgEl(false);
753
+ if (el && el.parentNode) el.parentNode.removeChild(el);
754
+ }
755
+ function startAiTimer() {
756
+ stopAiTimer();
757
+ var EXPECT = 40; // 预计秒数(仅用于进度提示,不参与实际超时)
758
+ S.aiTimer = setInterval(function () {
759
+ try {
760
+ if (S.groupBusy !== "ai-classify") { stopAiTimer(); return; }
761
+ var sec = (Date.now() - S.aiStartTs) / 1000;
762
+ var btn = document.querySelector('#dsh-skill-browser-panel [data-gadd="ai"]');
763
+ if (btn) btn.innerHTML = "🤖 AI 分组中… " + sec.toFixed(1) + "s";
764
+ var el = aiProgEl(true);
765
+ if (el) {
766
+ var t = el.querySelector(".dsb-ai-prog-txt");
767
+ var bar = el.querySelector(".dsb-ai-prog-bar > i");
768
+ if (t) t.textContent = "🤖 AI 分组中 · 已用 " + sec.toFixed(1) + "s / 预计约 " + EXPECT + "s";
769
+ if (bar) bar.style.width = Math.min(97, Math.round(sec / EXPECT * 100)) + "%";
770
+ }
771
+ } catch (e) { }
772
+ }, 200);
773
+ }
774
+
697
775
  // v1.9.7: 关键词快速分组 —— 纯本地正则,零 token、毫秒级、结果确定。
698
776
  // 与 AI 分组共用同一个方案弹层与 applyPlan 落盘路径。
699
777
  function kwClassifyFlow() {
700
778
  if (S.groupBusy) return;
701
779
  S.groupBusy = "kw-classify";
780
+ S.aiStartTs = Date.now(); S.aiElapsedMs = 0; S.aiCached = false;
702
781
  paintGroups();
703
782
  postGroups({ action: "kw-classify" }, function (d) {
704
783
  S.groupBusy = "";
784
+ S.aiElapsedMs = Date.now() - S.aiStartTs;
705
785
  if (d && d.ok) {
706
786
  S.gplan = d.plan || null;
707
787
  S.gplanMode = "keyword";
788
+ S.aiLastMs = S.aiElapsedMs; S.aiLastCached = false; S.aiLastMode = "keyword";
789
+ S.aiLastParallel = false; S.aiLastBatches = 1;
790
+ try { localStorage.setItem("dsb-ai-last-ms", String(S.aiLastMs)); localStorage.setItem("dsb-ai-last-cached", "0"); localStorage.setItem("dsb-ai-last-mode", "keyword"); localStorage.setItem("dsb-ai-last-par", "0"); localStorage.setItem("dsb-ai-last-bat", "1"); } catch (eL) { }
708
791
  paintGroups();
709
792
  openPlanMenu();
710
793
  } else {
@@ -718,13 +801,21 @@ window.__ModuleLoader__.load({
718
801
  function aiClassifyFlow() {
719
802
  if (S.groupBusy) return;
720
803
  S.groupBusy = "ai-classify";
804
+ S.aiStartTs = Date.now(); S.aiElapsedMs = 0; S.aiCached = false;
721
805
  paintGroups();
722
806
  showToast("ok", T("groupAiRunning"));
807
+ startAiTimer();
723
808
  postGroups({ action: "ai-classify" }, function (d) {
809
+ stopAiTimer();
724
810
  S.groupBusy = "";
811
+ S.aiElapsedMs = Date.now() - S.aiStartTs;
812
+ S.aiCached = !!(d && d.cached);
725
813
  if (d && d.ok) {
726
814
  S.gplan = d.plan || null;
727
815
  S.gplanMode = "ai";
816
+ S.aiLastMs = S.aiElapsedMs; S.aiLastCached = S.aiCached; S.aiLastMode = "ai";
817
+ S.aiLastParallel = !!(d && d.parallel); S.aiLastBatches = (d && d.batches) || 1;
818
+ try { localStorage.setItem("dsb-ai-last-ms", String(S.aiLastMs)); localStorage.setItem("dsb-ai-last-cached", S.aiLastCached ? "1" : "0"); localStorage.setItem("dsb-ai-last-mode", "ai"); localStorage.setItem("dsb-ai-last-par", S.aiLastParallel ? "1" : "0"); localStorage.setItem("dsb-ai-last-bat", String(S.aiLastBatches)); } catch (eL) { }
728
819
  paintGroups();
729
820
  openPlanMenu();
730
821
  } else {
@@ -760,11 +851,21 @@ window.__ModuleLoader__.load({
760
851
  "</div>";
761
852
  }
762
853
  var planTitle = S.gplanMode === "keyword" ? T("groupPlanTitleKw") : T("groupPlanTitle");
854
+ var timeLine = "";
855
+ if (S.gplanMode === "keyword") {
856
+ timeLine = '<div class="dsb-plan-time">⚡ 关键词分组 · 用时 ' + (Math.max(0, S.aiElapsedMs) / 1000).toFixed(2) + " 秒(不耗 token)</div>";
857
+ } else if (S.aiCached) {
858
+ timeLine = '<div class="dsb-plan-time">⚡ 命中缓存 · 0.0 秒(未调用模型)</div>';
859
+ } else if (S.aiElapsedMs > 0) {
860
+ timeLine = '<div class="dsb-plan-time">⏱ AI 分组累计用时 ' + (S.aiElapsedMs / 1000).toFixed(1) + " 秒" + (S.aiLastBatches > 1 ? (" · 并行" + S.aiLastBatches + "批") : " · 串行") + "</div>";
861
+ }
763
862
  var html = '<div class="dsb-bmenu-title">' + esc(planTitle) + "</div>" +
764
863
  '<div class="dsb-plan-stat">' + esc(TF("groupPlanStat", { d: domN, s: subN, n: total })) + "</div>" +
864
+ timeLine +
765
865
  '<div class="dsb-plan-list">' + rows + "</div>" +
766
866
  '<button class="dsb-bmenu-item dsb-bmenu-danger" data-plan-act="replace"><span>' + esc(T("groupPlanReplace")) + "</span></button>" +
767
867
  '<button class="dsb-bmenu-item" data-plan-act="merge"><span>' + esc(T("groupPlanMerge")) + "</span></button>" +
868
+ '<button class="dsb-plan-recache" data-plan-act="recache">🔄 清除缓存并重新生成</button>' +
768
869
  '<button class="dsb-bmenu-cancel" data-plan-act="__cancel">' + esc(T("aggCancelText")) + "</button>";
769
870
  menu.className = "dsb-bmenu dsb-plan-menu";
770
871
  menu.innerHTML = html;
@@ -941,6 +1042,776 @@ window.__ModuleLoader__.load({
941
1042
  });
942
1043
  }
943
1044
 
1045
+ // ═══════════════════════ v2.0.0 MCP 管理 ═══════════════════════
1046
+ function mcpEnsure() {
1047
+ if (!S.mcp) {
1048
+ var savedMode = "tree";
1049
+ try { if (localStorage.getItem("dsb-mcp-mode") === "card") savedMode = "card"; } catch (e) { }
1050
+ S.mcp = {
1051
+ data: null, stats: null, loading: false, statsLoading: false, error: "",
1052
+ detailId: null, busy: "", showExport: false, exportText: "",
1053
+ mode: savedMode, // "tree" | "card"
1054
+ expanded: {}, // { mcpId: true } 树状展开集合
1055
+ residue: null, // 残留检测结果
1056
+ showResidue: false, // 是否显示残留确认区
1057
+ cols: (function () { try { var v = parseInt(localStorage.getItem("dsb-mcp-cols"), 10); return (v >= 2 && v <= 6) ? v : 0; } catch (e) { return 0; } })() // 卡片每行列数(0=自适应)
1058
+ };
1059
+ }
1060
+ return S.mcp;
1061
+ }
1062
+
1063
+ // ── v2.0.1 工具清单中文化:内置词典(本机已知 MCP 工具精确中文名)+ 关键词兜底 ──
1064
+ var MCP_TOOL_ZH = {
1065
+ // ansys-fluent(12)
1066
+ fluent_detect_tool: "检测 Fluent 与环境",
1067
+ fluent_run_journal_tool: "运行 Fluent Journal 作业",
1068
+ fluent_job_status_tool: "查询作业状态",
1069
+ fluent_job_log_tool: "读取作业日志",
1070
+ fluent_list_jobs_tool: "列出作业",
1071
+ fluent_launch_session_tool: "启动 Fluent 会话",
1072
+ fluent_list_sessions_tool: "列出活动会话",
1073
+ fluent_session_info_tool: "查看会话详情",
1074
+ fluent_execute_scheme_tool: "执行 Scheme 表达式",
1075
+ fluent_run_tui_tool: "执行 TUI 命令",
1076
+ fluent_run_python_tool: "执行 Python 脚本",
1077
+ fluent_close_session_tool: "关闭会话",
1078
+ // windows-mcp 常见
1079
+ Snapshot: "桌面快照(截图+元素树)",
1080
+ DesktopSnapshot: "桌面快照(截图+元素树)",
1081
+ Screenshot: "屏幕截图",
1082
+ Click: "鼠标点击",
1083
+ Move: "移动鼠标 / 拖拽",
1084
+ Type: "键入文本",
1085
+ Scroll: "滚动页面",
1086
+ Shortcut: "键盘快捷键",
1087
+ Clipboard: "剪贴板读取 / 写入",
1088
+ Process: "进程列表 / 结束进程",
1089
+ Registry: "注册表读取 / 写入",
1090
+ Scrape: "抓取网页内容",
1091
+ Wait: "等待指定时长",
1092
+ WaitFor: "等待条件满足",
1093
+ Notification: "发送系统通知",
1094
+ ControlStatus: "读取桌面控制状态",
1095
+ DisplayInventory: "读取显示器信息",
1096
+ MultiEdit: "批量填充输入框",
1097
+ MultiSelect: "多选 / 多击",
1098
+ PowerShell: "执行 PowerShell 命令",
1099
+ App: "应用启动 / 管理",
1100
+ FileSystem: "文件系统读写"
1101
+ };
1102
+ // 前缀动词兜底:未在词典里的工具,按名字前缀给出中文动作词
1103
+ var MCP_VERB_ZH = [
1104
+ [/^(get|read|query|fetch|search|find|inspect|show|describe|stat)/i, "查询"],
1105
+ [/^(list|enumerate)/i, "列出"],
1106
+ [/^(set|write|update|patch|edit|put|save|config)/i, "设置"],
1107
+ [/^(create|add|insert|new|make)/i, "创建"],
1108
+ [/^(delete|remove|drop|clear|reset|purge)/i, "删除"],
1109
+ [/^(run|execute|exec|eval|invoke|call|apply|solve)/i, "执行"],
1110
+ [/^(launch|start|open|spawn|boot)/i, "启动"],
1111
+ [/^(close|stop|kill|cancel|abort|terminate)/i, "关闭"],
1112
+ [/^(export|dump|download|backup)/i, "导出"],
1113
+ [/^(import|upload|load|restore)/i, "导入"],
1114
+ [/^(click|tap|press)/i, "点击"],
1115
+ [/^(move|drag|scroll)/i, "移动"],
1116
+ [/^(type|input|fill|send)/i, "输入"],
1117
+ [/^(wait|sleep|poll)/i, "等待"],
1118
+ [/^(check|validate|verify|test|probe)/i, "检查"],
1119
+ [/^(detect|discover|scan)/i, "检测"]
1120
+ ];
1121
+ function mcpToolZh(rawName, desc) {
1122
+ var name = String(rawName || "");
1123
+ if (MCP_TOOL_ZH[name]) return MCP_TOOL_ZH[name];
1124
+ // 去掉常见后缀再试(_tool / Tool)
1125
+ var base = name.replace(/(_tool|Tool|_cmd|Cmd)$/, "");
1126
+ if (MCP_TOOL_ZH[base]) return MCP_TOOL_ZH[base];
1127
+ for (var i = 0; i < MCP_VERB_ZH.length; i++) {
1128
+ if (MCP_VERB_ZH[i][0].test(base)) {
1129
+ var tail = base.replace(MCP_VERB_ZH[i][0], "").replace(/[_\-\s]+/g, " ").trim();
1130
+ return MCP_VERB_ZH[i][1] + (tail ? " " + tail : "");
1131
+ }
1132
+ }
1133
+ return "";
1134
+ }
1135
+
1136
+ function mcpPill(phase) {
1137
+ if (phase === "active") return { cls: "dsb-pill-active", text: "已连接" };
1138
+ if (phase === "failed") return { cls: "dsb-pill-failed", text: "连接失败" };
1139
+ if (phase === "disabled") return { cls: "dsb-pill-off", text: "已停用" };
1140
+ if (phase === "loading" || phase === "pending" || phase === "unloading") return { cls: "dsb-pill-warn", text: "加载中" };
1141
+ return { cls: "dsb-pill-off", text: "未激活" };
1142
+ }
1143
+
1144
+ function fmtMoment(ts) {
1145
+ if (!ts) return "";
1146
+ var d = new Date(ts), now = new Date();
1147
+ var p = function (n) { return n < 10 ? "0" + n : String(n); };
1148
+ var hm = p(d.getHours()) + ":" + p(d.getMinutes());
1149
+ var same = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate();
1150
+ return same ? hm : (p(d.getMonth() + 1) + "-" + p(d.getDate()) + " " + hm);
1151
+ }
1152
+
1153
+ function loadMcp(force) {
1154
+ var M = mcpEnsure();
1155
+ if (M.loading) return;
1156
+ M.loading = true; M.error = "";
1157
+ paintMcp();
1158
+ var url = apiBase() + "/mcp/servers" + (force ? "?t=" + Date.now() : "");
1159
+ // 主数据与统计分开请求:一个失败绝不整块清空
1160
+ Promise.all([
1161
+ fetch(url).then(function (r) { return r.json(); }),
1162
+ fetch(apiBase() + "/mcp/stats").then(function (r) { return r.json(); }).catch(function () { return null; })
1163
+ ]).then(function (arr) {
1164
+ var d = arr[0], st = arr[1];
1165
+ M.loading = false;
1166
+ if (!d || !d.ok) throw new Error((d && d.error) || "bad response");
1167
+ M.data = d;
1168
+ if (st && st.ok) M.stats = st;
1169
+ if (!M.expandedInited) {
1170
+ M.expandedInited = true;
1171
+ M.expanded["__ROOT__"] = true;
1172
+ (d.servers || []).forEach(function (s) { M.expanded[s.id] = true; });
1173
+ }
1174
+ paintMcp();
1175
+ }).catch(function (e) {
1176
+ M.loading = false;
1177
+ M.error = String((e && e.message) || e);
1178
+ paintMcp();
1179
+ });
1180
+ }
1181
+
1182
+ function loadMcpStats() {
1183
+ var M = mcpEnsure();
1184
+ M.statsLoading = true; paintMcp();
1185
+ fetch(apiBase() + "/mcp/stats").then(function (r) { return r.json(); }).then(function (st) {
1186
+ M.statsLoading = false;
1187
+ if (st && st.ok) M.stats = st;
1188
+ paintMcp();
1189
+ }).catch(function () { M.statsLoading = false; paintMcp(); });
1190
+ }
1191
+
1192
+ function mcpDetailHtml(s, st) {
1193
+ var h = '<div class="dsb-mcp-detail">';
1194
+ if (s.tools && s.tools.length) {
1195
+ h += '<div style="margin-bottom:5px;font-weight:600">工具清单(' + s.tools.length + ')</div><div class="dsb-mcp-tools">';
1196
+ s.tools.forEach(function (t) {
1197
+ var zh = mcpToolZh(t.rawName, t.description);
1198
+ var label = zh || t.rawName;
1199
+ var sub = zh ? t.rawName : (t.description || "(无描述)");
1200
+ h += '<div class="dsb-mcp-tool" title="' + esc(t.description || "") + '"><b>' + esc(label) + '</b><span>' + esc(sub) + '</span></div>';
1201
+ });
1202
+ h += '</div>';
1203
+ } else {
1204
+ h += '<div style="color:var(--hintC,#9aa1b5)">未获取到已注册工具(可能未连接或已停用)</div>';
1205
+ }
1206
+ if (st && st.topTools && st.topTools.length) {
1207
+ h += '<div style="margin:8px 0 5px;font-weight:600">调用排行</div>';
1208
+ st.topTools.forEach(function (t) {
1209
+ h += '<div class="dsb-mcp-tool"><b>' + t.calls + '×</b><span>' + esc(String(t.tool).replace(/^mcp__[^_]+__/, "")) + '</span></div>';
1210
+ });
1211
+ }
1212
+ if (s.envKeys && s.envKeys.length) {
1213
+ h += '<div style="margin:8px 0 5px;font-weight:600">环境变量(值已打码)</div>';
1214
+ s.envKeys.forEach(function (k) {
1215
+ h += '<div class="dsb-mcp-tool"><b>' + esc(k) + '</b><span>' + esc(s.env[k]) + '</span></div>';
1216
+ });
1217
+ }
1218
+ h += '<div style="margin-top:6px;color:var(--hintC,#9aa1b5);font-size:10.5px">配置来源:' + esc(s.patchFile || "(内置层)") + '</div>';
1219
+ h += '</div>';
1220
+ return h;
1221
+ }
1222
+
1223
+ function mcpCardHtml(s, statsByName, M) {
1224
+ var st = statsByName[s.serverName] || null;
1225
+ var pill = mcpPill(s.phase);
1226
+ var target = s.transport === "stdio"
1227
+ ? (s.command + (s.args && s.args.length ? " " + s.args.join(" ") : ""))
1228
+ : s.url;
1229
+ var h = '<div class="dsb-mcp-card" data-mcp-id="' + esc(s.id) + '">';
1230
+ h += '<div class="dsb-mcp-hd"><div class="dsb-mcp-name" title="' + esc(s.serverName) + '">' + esc(s.serverName) + '</div><span class="dsb-mcp-transport">' + esc(s.transport) + '</span>' + (s.version ? '<span class="dsb-mcp-badge" title="从启动配置推断的版本">v' + esc(s.version) + '</span>' : '') + '</div>';
1231
+ h += '<div class="dsb-mcp-status"><span class="dsb-pill ' + pill.cls + '"><i class="dsb-pill-dot"></i>' + esc(pill.text) + '</span>';
1232
+ if (!s.disabled) h += '<span style="color:var(--hintC,#9aa1b5);font-size:11px">' + s.toolCount + ' 个工具</span>';
1233
+ h += '</div>';
1234
+ var tgt = target || "(未配置命令 / URL)";
1235
+ h += '<div class="dsb-mcp-target" title="' + esc(tgt) + '">' + esc(tgt.length > 90 ? tgt.slice(0, 90) + "…" : tgt) + '</div>';
1236
+ h += '<div class="dsb-mcp-summary">' + esc(s.summary) + '</div>';
1237
+ if (st) {
1238
+ h += '<div class="dsb-mcp-calls">累计调用 ' + st.calls + ' 次' + (st.fail ? '(失败 ' + st.fail + ')' : '') + (st.lastTs ? ' · 最近 ' + fmtMoment(st.lastTs) : '') + '</div>';
1239
+ } else {
1240
+ h += '<div class="dsb-mcp-calls">暂无调用记录</div>';
1241
+ }
1242
+ h += '<div class="dsb-mcp-acts">';
1243
+ h += '<label class="dsb-switch dsb-mcp-switch' + (s.disabled ? '' : ' dsb-sw-on') + '" data-stop="1" data-mcp-switch="' + esc(s.id) + '" data-on="' + (s.disabled ? '0' : '1') + '"' + (M.busy ? ' aria-disabled="true"' : '') + '>';
1244
+ h += '<span class="dsb-sw-track"><span class="dsb-sw-thumb"></span></span>';
1245
+ h += '<span class="dsb-sw-label">' + (s.disabled ? '关' : '开') + '</span>';
1246
+ h += '</label>';
1247
+ h += '<button class="dsb-mcp-btn" data-mcp-reconnect="' + esc(s.id) + '" ' + (s.disabled ? 'disabled' : '') + '>重连</button>';
1248
+ h += '<button class="dsb-mcp-btn" data-mcp-detail="' + esc(s.id) + '">' + (M.detailId === s.id ? '收起' : '详情') + '</button>';
1249
+ h += '<button class="dsb-mcp-btn" data-mcp-export="' + esc(s.id) + '">导出</button>';
1250
+ h += '</div>';
1251
+ if (M.detailId === s.id) h += mcpDetailHtml(s, st);
1252
+ h += '</div>';
1253
+ return h;
1254
+ }
1255
+
1256
+ function mcpPhaseColor(phase) {
1257
+ if (phase === "active") return "#34d399";
1258
+ if (phase === "failed") return "#f87171";
1259
+ if (phase === "loading" || phase === "pending" || phase === "unloading") return "#fbbf24";
1260
+ return "#9aa1b5";
1261
+ }
1262
+
1263
+ // v2.0.2 工具级统计取值与状态
1264
+ function mcpToolStat(ss, toolName) {
1265
+ if (!ss) return null;
1266
+ var name = String(toolName || "");
1267
+ var arr = ss.tools;
1268
+ if (arr && arr.length) {
1269
+ for (var i = 0; i < arr.length; i++) {
1270
+ var x = arr[i];
1271
+ if (x.tool === name) return x;
1272
+ if (x.tool && name && (x.tool.length >= name.length ? x.tool.slice(-name.length) === name : name.slice(-x.tool.length) === x.tool)) return x;
1273
+ }
1274
+ }
1275
+ var tt = ss.topTools || [];
1276
+ for (var j = 0; j < tt.length; j++) if (tt[j].tool === name) return { tool: tt[j].tool, calls: tt[j].calls, ok: null, fail: null, lastTs: 0 };
1277
+ return null;
1278
+ }
1279
+ function mcpToolState(ts) {
1280
+ if (!ts || !ts.calls) return { color: "#9aa1b5", text: "○ 未调用" };
1281
+ if (ts.ok == null) return { color: "#9aa1b5", text: "调用 " + ts.calls + " 次" };
1282
+ if (ts.fail === 0) return { color: "#34d399", text: "✓ 正常" };
1283
+ if (ts.ok === 0) return { color: "#f87171", text: "✗ 全失败" };
1284
+ return { color: "#fbbf24", text: "⚠ 部分失败" };
1285
+ }
1286
+
1287
+ // ── v2.0.3 MCP 故障一键送诊断 ──
1288
+ // DSH 对话框输入框是 Lexical contenteditable([data-lexical-editor="true"]),
1289
+ // 不能直接设 value,必须走 paste 事件(Lexical 自带 paste 解析)。
1290
+ function dsbFindComposer() {
1291
+ try {
1292
+ var els = document.querySelectorAll('[data-lexical-editor="true"]');
1293
+ var visible = null;
1294
+ for (var i = 0; i < els.length; i++) {
1295
+ var r = els[i].getBoundingClientRect();
1296
+ if (r.width > 40 && r.height > 10) { visible = els[i]; break; }
1297
+ }
1298
+ return visible || (els.length ? els[0] : null);
1299
+ } catch (e) { return null; }
1300
+ }
1301
+
1302
+ function dsbCopyText(text, okMsg) {
1303
+ try {
1304
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1305
+ navigator.clipboard.writeText(text).then(function () {
1306
+ showToast("ok", okMsg || "已复制到剪贴板,按 Ctrl+V 粘贴到对话框");
1307
+ }).catch(function () {
1308
+ showToast("err", "复制失败,请手动复制");
1309
+ });
1310
+ return;
1311
+ }
1312
+ } catch (e) { /* 走下面兜底 */ }
1313
+ showToast("err", "剪贴板不可用,请手动复制");
1314
+ }
1315
+
1316
+ // 三级兜底:paste 事件 → execCommand insertText → 剪贴板
1317
+ function dsbSendToComposer(text) {
1318
+ var el = dsbFindComposer();
1319
+ if (!el) {
1320
+ dsbCopyText(text, "未找到对话框输入框,已复制(Ctrl+V 粘贴到对话框)");
1321
+ return false;
1322
+ }
1323
+ try { el.focus(); } catch (e) { }
1324
+ var probe = text.slice(0, 12);
1325
+ try {
1326
+ var dt = new DataTransfer();
1327
+ dt.setData("text/plain", text);
1328
+ var ev = new ClipboardEvent("paste", { clipboardData: dt, bubbles: true, cancelable: true });
1329
+ el.dispatchEvent(ev);
1330
+ } catch (e) { /* 走下面兜底 */ }
1331
+ if ((el.textContent || "").indexOf(probe) < 0) {
1332
+ try { document.execCommand("insertText", false, text); } catch (e2) { }
1333
+ }
1334
+ if ((el.textContent || "").indexOf(probe) >= 0) {
1335
+ showToast("ok", "诊断信息已填入对话框,按回车发送给 AI");
1336
+ return true;
1337
+ }
1338
+ dsbCopyText(text, "未能填入输入框,已复制(Ctrl+V 粘贴到对话框)");
1339
+ return false;
1340
+ }
1341
+
1342
+ function buildMcpDiag(srv, tool, ts) {
1343
+ var lines = [];
1344
+ lines.push("【MCP 工具故障诊断请求】");
1345
+ if (srv) {
1346
+ lines.push("MCP 服务器:" + srv.serverName + "(" + (srv.transport || "?") + ",状态 " + (srv.phase || "?") + ")");
1347
+ var cmd = srv.transport === "stdio" ? (srv.command || "") : (srv.url || "");
1348
+ if (cmd) lines.push("启动配置:" + cmd + (srv.args && srv.args.length ? " " + srv.args.join(" ") : ""));
1349
+ }
1350
+ if (tool) {
1351
+ var zh = mcpToolZh(tool.rawName, tool.description);
1352
+ lines.push("工具:" + tool.rawName + (zh ? "(" + zh + ")" : ""));
1353
+ if (tool.description) lines.push("工具说明:" + String(tool.description).slice(0, 200));
1354
+ }
1355
+ if (ts && ts.calls) {
1356
+ lines.push("调用情况:共 " + ts.calls + " 次 · 成功 " + (ts.ok == null ? "?" : ts.ok) + " · 失败 " + (ts.fail == null ? "?" : ts.fail) + (ts.avgDur ? " · 平均耗时 " + ts.avgDur + " ms" : ""));
1357
+ if (ts.lastTs) lines.push("最近调用:" + new Date(ts.lastTs).toLocaleString());
1358
+ if (ts.lastErr) lines.push("最近错误:" + ts.lastErr);
1359
+ } else {
1360
+ lines.push("调用情况:暂无调用记录");
1361
+ }
1362
+ lines.push("");
1363
+ lines.push("请帮我分析这个 MCP 工具为什么调用失败,并给出排查与修复建议。");
1364
+ return lines.join("\n");
1365
+ }
1366
+
1367
+ function handleMcpDiag(M, key) {
1368
+ var parts = String(key || "").split("::");
1369
+ var sid = parts[0], rawName = parts.slice(1).join("::");
1370
+ var srv = null;
1371
+ (M.data && M.data.servers ? M.data.servers : []).forEach(function (x) { if (x.id === sid) srv = x; });
1372
+ var tool = null;
1373
+ if (srv) (srv.tools || []).forEach(function (t) { if (t.rawName === rawName) tool = t; });
1374
+ var ss = null;
1375
+ if (srv && M.stats && M.stats.servers) M.stats.servers.forEach(function (x) { if (x.server === srv.serverName) ss = x; });
1376
+ var ts = mcpToolStat(ss, tool ? tool.name : rawName);
1377
+ dsbSendToComposer(buildMcpDiag(srv, tool, ts));
1378
+ }
1379
+
1380
+ // v2.0.2 树状视图:MCP 节点 → 工具节点 → 工具调用指标(三级大树)
1381
+ function mcpTreeHtml(s, statsByName, M) {
1382
+ var ss = statsByName[s.serverName] || null;
1383
+ var mExp = !!M.expanded[s.id];
1384
+ var target = s.transport === "stdio" ? (s.command + (s.args && s.args.length ? " " + s.args.join(" ") : "")) : s.url;
1385
+ var h = '<div class="dsb-mcp-node" data-mcp-id="' + esc(s.id) + '">';
1386
+ h += '<div class="dsb-mcp-row" data-mcp-toggle-node="' + esc(s.id) + '" title="' + esc(target || "") + '">';
1387
+ h += '<span class="dsb-mcp-caret' + (mExp ? ' open' : '') + '"></span>';
1388
+ h += '<i class="dsb-mcp-dot" style="background:' + mcpPhaseColor(s.phase) + '"></i>';
1389
+ h += '<span class="dsb-mcp-node-name">' + esc(s.serverName) + '</span>';
1390
+ h += '<span class="dsb-mcp-transport">' + esc(s.transport) + '</span>';
1391
+ if (s.version) h += '<span class="dsb-mcp-badge" title="从启动配置推断的版本">v' + esc(s.version) + '</span>';
1392
+ h += '<span class="dsb-mcp-badge">' + (s.disabled ? "已停用" : s.toolCount + " 个工具") + '</span>';
1393
+ if (ss && ss.calls) h += '<span class="dsb-mcp-badge">累计 ' + ss.calls + ' 次</span>';
1394
+ if (ss && ss.calls) h += '<span class="dsb-mcp-toolstate" style="color:' + (ss.fail ? "#fbbf24" : "#34d399") + '">' + (ss.fail ? "⚠ " + ss.fail + " 失败" : "✓ 全成功") + '</span>';
1395
+ h += '<span style="flex:1"></span>';
1396
+ h += '<label class="dsb-switch dsb-mcp-switch' + (s.disabled ? '' : ' dsb-sw-on') + '" data-stop="1" data-mcp-switch="' + esc(s.id) + '" data-on="' + (s.disabled ? '0' : '1') + '"' + (M.busy ? ' aria-disabled="true"' : '') + ' title="' + (s.disabled ? '点击启用' : '点击停用') + '">';
1397
+ h += '<span class="dsb-sw-track"><span class="dsb-sw-thumb"></span></span>';
1398
+ h += '<span class="dsb-sw-label">' + (s.disabled ? '关' : '开') + '</span>';
1399
+ h += '</label>';
1400
+ h += '<button class="dsb-mcp-btn" data-stop="1" data-mcp-reconnect="' + esc(s.id) + '" ' + (s.disabled ? 'disabled' : '') + '>重连</button>';
1401
+ h += '<button class="dsb-mcp-btn" data-stop="1" data-mcp-export="' + esc(s.id) + '">导出</button>';
1402
+ h += '</div>';
1403
+ h += '<div class="dsb-mcp-children" style="display:' + (mExp ? 'block' : 'none') + '">';
1404
+ h += '<div class="dsb-mcp-summary">' + esc(s.summary) + '</div>';
1405
+ if (s.tools && s.tools.length) {
1406
+ h += '<div class="dsb-mcp-sub">工具清单(' + s.tools.length + ')</div>';
1407
+ s.tools.forEach(function (t) {
1408
+ var ts = mcpToolStat(ss, t.name);
1409
+ var state = mcpToolState(ts);
1410
+ var zh = mcpToolZh(t.rawName, t.description);
1411
+ var tkey = s.id + "::" + t.rawName;
1412
+ var tExp = !!M.expanded[tkey];
1413
+ h += '<div class="dsb-mcp-toolnode">';
1414
+ h += '<div class="dsb-mcp-toolrow" data-mcp-tool-node="' + esc(tkey) + '" title="' + esc(t.description || "") + '">';
1415
+ h += '<span class="dsb-mcp-caret dsb-mcp-caret-s' + (tExp ? ' open' : '') + '"></span>';
1416
+ h += '<b class="dsb-mcp-toolname">' + esc(zh || t.rawName) + '</b>';
1417
+ h += '<span class="dsb-mcp-toolen">' + esc(t.rawName) + '</span>';
1418
+ h += '<span style="flex:1"></span>';
1419
+ if (ts && ts.calls) h += '<span class="dsb-mcp-badge">' + ts.calls + ' 次</span>';
1420
+ h += '<span class="dsb-mcp-toolstate" style="color:' + state.color + '">' + state.text + '</span>';
1421
+ if (ts && ts.lastTs) h += '<span class="dsb-mcp-badge">' + fmtMoment(ts.lastTs) + '</span>';
1422
+ if (ts && ts.fail > 0) h += '<button class="dsb-mcp-btn dsb-mcp-diag" data-stop="1" data-mcp-diag="' + esc(tkey) + '" title="把这个工具的故障信息直接填进对话框,让 AI 诊断">诊断</button>';
1423
+ h += '</div>';
1424
+ h += '<div class="dsb-mcp-toolchildren" style="display:' + (tExp ? 'block' : 'none') + '">';
1425
+ if (ts && ts.ok != null) {
1426
+ h += '<div class="dsb-mcp-kv"><span>成功</span><b style="color:#34d399">' + ts.ok + '</b></div>';
1427
+ h += '<div class="dsb-mcp-kv"><span>失败</span><b style="color:#f87171">' + ts.fail + '</b></div>';
1428
+ h += '<div class="dsb-mcp-kv"><span>平均耗时</span><b>' + (ts.avgDur ? ts.avgDur + ' ms' : '—') + '</b></div>';
1429
+ if (ts.lastErr) h += '<div class="dsb-mcp-kv"><span>最近错误</span><b style="color:#f87171;font-weight:400">' + esc(ts.lastErr) + '</b></div>';
1430
+ } else {
1431
+ h += '<div class="dsb-mcp-kv"><span>状态</span><b>该工具暂无调用记录</b></div>';
1432
+ }
1433
+ h += '</div>';
1434
+ h += '</div>';
1435
+ });
1436
+ } else {
1437
+ h += '<div class="dsb-mcp-sub2">未获取到已注册工具(可能未连接或已停用)</div>';
1438
+ }
1439
+ if (s.envKeys && s.envKeys.length) {
1440
+ h += '<div class="dsb-mcp-sub">环境变量(值已打码)</div>';
1441
+ s.envKeys.forEach(function (k) {
1442
+ h += '<div class="dsb-mcp-tool"><b>' + esc(k) + '</b><span>' + esc(s.env[k]) + '</span></div>';
1443
+ });
1444
+ }
1445
+ h += '<div class="dsb-mcp-src">配置来源:' + esc(s.patchFile || "(内置层)") + '</div>';
1446
+ h += '</div>';
1447
+ h += '</div>';
1448
+ return h;
1449
+ }
1450
+
1451
+ // 根节点:全部 MCP(树根)
1452
+ function mcpTreeRootHtml(data, statsByName, M) {
1453
+ var rootExp = !!M.expanded["__ROOT__"];
1454
+ var toolSum = (data.servers || []).reduce(function (a, s) { return a + (s.toolCount || 0); }, 0);
1455
+ var totalCalls = 0;
1456
+ (data.servers || []).forEach(function (s) { var ss = statsByName[s.serverName]; if (ss) totalCalls += (ss.calls || 0); });
1457
+ var h = '<div class="dsb-mcp-node dsb-mcp-rootnode">';
1458
+ h += '<div class="dsb-mcp-row dsb-mcp-rootrow" data-mcp-toggle-node="__ROOT__">';
1459
+ h += '<span class="dsb-mcp-caret' + (rootExp ? ' open' : '') + '"></span>';
1460
+ h += '<span class="dsb-mcp-rootname">全部 MCP</span>';
1461
+ h += '<span class="dsb-mcp-badge">' + data.total + ' 个服务器</span>';
1462
+ h += '<span class="dsb-mcp-badge">已连接 ' + data.active + '</span>';
1463
+ h += '<span class="dsb-mcp-badge">工具 ' + toolSum + '</span>';
1464
+ if (totalCalls) h += '<span class="dsb-mcp-badge">累计 ' + totalCalls + ' 次</span>';
1465
+ h += '</div>';
1466
+ h += '<div class="dsb-mcp-children" style="display:' + (rootExp ? 'block' : 'none') + '">';
1467
+ (data.servers || []).forEach(function (s) { h += mcpTreeHtml(s, statsByName, M); });
1468
+ h += '</div>';
1469
+ h += '</div>';
1470
+ return h;
1471
+ }
1472
+
1473
+ var _mcpPaintTimer = null;
1474
+ // v2.0.4 性能:合并重绘(16ms 内多次请求只渲染一次),避免频繁操作时整树重排卡顿。
1475
+ function paintMcp() {
1476
+ if (_mcpPaintTimer) return;
1477
+ _mcpPaintTimer = setTimeout(function () { _mcpPaintTimer = null; paintMcpNow(); }, 16);
1478
+ }
1479
+ function paintMcpNow() {
1480
+ var host = $("dsb-mcp"); if (!host) return;
1481
+ var M = mcpEnsure();
1482
+ var h = "";
1483
+ h += '<div class="dsb-mcp-top">';
1484
+ if (M.data) {
1485
+ var toolSum = (M.data.servers || []).reduce(function (a, s) { return a + (s.toolCount || 0); }, 0);
1486
+ h += '<span class="dsb-mcp-sum">共 ' + M.data.total + ' 个 MCP · 已连接 ' + M.data.active + ' 个 · 工具 ' + toolSum + ' 个</span>';
1487
+ } else {
1488
+ h += '<span class="dsb-mcp-sum">' + (M.loading ? '加载中…' : '尚未加载') + '</span>';
1489
+ }
1490
+ h += '<span style="flex:1"></span>';
1491
+ var isTree = M.mode !== "card";
1492
+ h += '<button class="dsb-mcp-btn ' + (isTree ? 'primary' : '') + '" data-mcp-act="mode-tree">树状</button>';
1493
+ h += '<button class="dsb-mcp-btn ' + (!isTree ? 'primary' : '') + '" data-mcp-act="mode-card">卡片</button>';
1494
+ if (!isTree) {
1495
+ h += '<span style="font-size:11px;color:var(--hintC,#9aa1b5);margin-left:4px">每行</span>';
1496
+ [0, 2, 3, 4, 5, 6].forEach(function (nn) {
1497
+ h += '<button class="dsb-mcp-btn ' + (M.cols === nn ? 'primary' : '') + '" data-mcp-cols="' + nn + '" title="' + (nn ? ("每行 " + nn + " 个") : "自适应") + '">' + (nn || '自') + '</button>';
1498
+ });
1499
+ }
1500
+ if (isTree && M.data && M.data.servers && M.data.servers.length) {
1501
+ h += '<button class="dsb-mcp-btn" data-mcp-act="expandAll">全部展开</button>';
1502
+ h += '<button class="dsb-mcp-btn" data-mcp-act="collapseAll">全部收起</button>';
1503
+ }
1504
+ h += '<button class="dsb-mcp-btn" data-mcp-act="refresh">刷新</button>';
1505
+ h += '<button class="dsb-mcp-btn" data-mcp-act="exportAll">导出全部</button>';
1506
+ h += '<button class="dsb-mcp-btn" data-mcp-act="clearStats">清空统计</button>';
1507
+ h += '<button class="dsb-mcp-btn" data-mcp-act="cleanResidue" title="检测并清除指向已卸载 MCP 的残留配置">清理残留</button>';
1508
+ h += '<button class="dsb-mcp-btn" data-mcp-act="scanLocal" title="扫描本机已安装 / 已配置的全部 MCP(只读)">🔍 本机 MCP</button>';
1509
+ h += '<button class="dsb-mcp-btn" data-mcp-act="aiFix" title="AI 诊断异常的 MCP 并给出/应用修复方案">🛠 AI 修复</button>';
1510
+ h += '</div>';
1511
+
1512
+ if (M.showExport) {
1513
+ h += '<div style="margin-bottom:10px">' +
1514
+ '<div style="font-size:11.5px;color:var(--hintC,#9aa1b5);margin-bottom:5px">mcpServers 配置(可直接粘贴到 Claude Desktop / Cursor)</div>' +
1515
+ '<textarea class="dsb-mcp-export" id="dsb-mcp-export-ta" readonly>' + esc(M.exportText) + '</textarea>' +
1516
+ '<div style="margin-top:6px;display:flex;gap:6px"><button class="dsb-mcp-btn primary" data-mcp-act="copyExport">复制</button><button class="dsb-mcp-btn" data-mcp-act="closeExport">关闭</button></div></div>';
1517
+ }
1518
+
1519
+ if (M.showResidue) {
1520
+ var rl = (M.residue && M.residue.length) ? M.residue : [];
1521
+ h += '<div class="dsb-mcp-residue">';
1522
+ if (!rl.length) {
1523
+ h += '<div style="font-size:12px;color:var(--hintC,#9aa1b5);padding:4px 0">没有检测到残留,配置是干净的。</div>';
1524
+ h += '<div style="margin-top:6px"><button class="dsb-mcp-btn" data-mcp-act="cancelResidue">关闭</button></div>';
1525
+ } else {
1526
+ h += '<div style="font-weight:600;font-size:12px;margin-bottom:6px;color:#fbbf24">检测到 ' + rl.length + ' 处残留(指向已卸载的 MCP,清掉只消除日志告警,不影响其它配置)</div>';
1527
+ rl.forEach(function (x, idx) {
1528
+ h += '<div class="dsb-mcp-residue-item">' + (idx + 1) + '. ' + esc(x.reason) + ' — id: ' + esc((x.ids || []).join(", ")) + (x.pkg ? '(包 ' + esc(x.pkg) + ')' : '') + '</div>';
1529
+ });
1530
+ h += '<div style="margin-top:8px;display:flex;gap:6px"><button class="dsb-mcp-btn primary" data-mcp-act="doCleanResidue">确认清理</button><button class="dsb-mcp-btn" data-mcp-act="cancelResidue">取消</button></div>';
1531
+ }
1532
+ h += '</div>';
1533
+ }
1534
+
1535
+ if (M.error) {
1536
+ h += '<div class="dsb-mcp-empty">加载失败:' + esc(M.error) + '<br><br><button class="dsb-mcp-btn" data-mcp-act="refresh">重试</button></div>';
1537
+ } else if (!M.data) {
1538
+ h += '<div class="dsb-mcp-empty">' + (M.loading ? '正在读取运行时 MCP 配置…' : '点击「刷新」加载') + '</div>';
1539
+ } else if (!M.data.servers || !M.data.servers.length) {
1540
+ h += '<div class="dsb-mcp-empty">当前没有挂载任何 MCP 服务器<br><span style="font-size:11px">可在 profile 的 cordis.patch.yml 里以 @deepseek-ai/dsh-mcp-client 挂载</span></div>';
1541
+ } else {
1542
+ var byName = {};
1543
+ if (M.stats && M.stats.servers) M.stats.servers.forEach(function (x) { byName[x.server] = x; });
1544
+ if (M.mode === "card") {
1545
+ h += '<div class="dsb-mcp-grid"' + (M.cols ? ' style="grid-template-columns:repeat(' + M.cols + ',minmax(0,1fr))"' : '') + '>';
1546
+ M.data.servers.forEach(function (s) { h += mcpCardHtml(s, byName, M); });
1547
+ h += '</div>';
1548
+ } else {
1549
+ h += '<div class="dsb-mcp-tree">';
1550
+ h += mcpTreeRootHtml(M.data, byName, M);
1551
+ h += '</div>';
1552
+ }
1553
+ if (M.stats) {
1554
+ var today = (M.stats.hourly || []).reduce(function (a, b) { return a + b; }, 0);
1555
+ h += '<div style="font-size:11px;color:var(--hintC,#9aa1b5);padding-top:8px">近 24 小时调用 ' + today + ' 次 · 累计 ' + (M.stats.total ? M.stats.total.calls : 0) + ' 次</div>';
1556
+ }
1557
+ }
1558
+ host.innerHTML = h;
1559
+ }
1560
+
1561
+ function openMcpFixMenu(issues) {
1562
+ var panel = $(PANEL_ID); if (!panel) return;
1563
+ var old = panel.querySelector(".dsb-scan"); if (old && old.parentNode) old.parentNode.removeChild(old);
1564
+ var rows = "";
1565
+ issues.forEach(function (x) {
1566
+ rows += '<div class="dsb-scan-row"><b>' + esc(x.serverName) + '</b>' +
1567
+ ' <span class="dsb-scan-dim">' + esc(x.symptoms || "") + '</span>' +
1568
+ '<div class="dsb-scan-desc">建议:<b>' + esc(x.action) + '</b> · ' + esc(x.reason || "") + '</div></div>';
1569
+ });
1570
+ var wrap = document.createElement("div");
1571
+ wrap.className = "dsb-scan";
1572
+ wrap.innerHTML = '<div class="dsb-scan-box"><div class="dsb-scan-h"><span>🛠 MCP AI 修复 · 诊断结果</span><span class="dsb-scan-close" data-scan-close="1">✕</span></div>' +
1573
+ '<div class="dsb-scan-list">' + rows + '</div>' +
1574
+ '<div style="padding:10px 16px;border-top:1px solid rgba(255,255,255,.12);display:flex;justify-content:flex-end;gap:8px">' +
1575
+ '<button class="dsb-btn" data-scan-close="1">取消</button>' +
1576
+ '<button class="dsb-btn" data-fix-apply="1">应用这些修复</button></div></div>';
1577
+ panel.appendChild(wrap);
1578
+ wrap.addEventListener("click", function (ev) {
1579
+ if (ev.target.closest("[data-scan-close]") || ev.target === wrap) { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); return; }
1580
+ if (ev.target.closest("[data-fix-apply]")) {
1581
+ var actions = issues.map(function (x) { return { serverName: x.serverName, action: x.action }; });
1582
+ var btn = ev.target.closest("[data-fix-apply]"); btn.disabled = true; btn.textContent = "修复中…";
1583
+ fetch(apiBase() + "/mcp/ai-fix/apply", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ actions: actions }) })
1584
+ .then(function (r) { return r.json(); }).then(function (d) {
1585
+ if (wrap.parentNode) wrap.parentNode.removeChild(wrap);
1586
+ var okN = (d && d.results ? d.results.filter(function (x) { return x.ok; }).length : 0);
1587
+ var totalN = (d && d.results ? d.results.length : 0);
1588
+ showToast(okN ? "ok" : "err", "修复完成:" + okN + "/" + totalN + " 项成功");
1589
+ loadMcp(true);
1590
+ }).catch(function (e) { btn.disabled = false; btn.textContent = "应用这些修复"; showToast("err", "修复失败:" + ((e && e.message) || e)); });
1591
+ }
1592
+ });
1593
+ }
1594
+ function doMcpAiFix() {
1595
+ showToast("ok", "AI 诊断中…");
1596
+ fetch(apiBase() + "/mcp/ai-diagnose").then(function (r) { return r.json(); }).then(function (d) {
1597
+ if (!d || !d.ok) { showToast("err", "诊断失败:" + ((d && d.error) || "unknown")); return; }
1598
+ if (!d.issues || !d.issues.length) { showToast("ok", d.note || "没有发现异常的 MCP"); return; }
1599
+ openMcpFixMenu(d.issues);
1600
+ }).catch(function (e) { showToast("err", "诊断失败:" + ((e && e.message) || e)); });
1601
+ }
1602
+
1603
+ function openMcpScanMenu(rowsHtml) {
1604
+ var panel = $(PANEL_ID); if (!panel) return;
1605
+ var old = panel.querySelector(".dsb-scan"); if (old && old.parentNode) old.parentNode.removeChild(old);
1606
+ var wrap = document.createElement("div");
1607
+ wrap.className = "dsb-scan";
1608
+ wrap.innerHTML = '<div class="dsb-scan-box"><div class="dsb-scan-h"><span>🔍 本机 MCP 扫描</span><span class="dsb-scan-close" data-scan-close="1">✕</span></div>' +
1609
+ '<div class="dsb-scan-list">' + rowsHtml + '</div></div>';
1610
+ panel.appendChild(wrap);
1611
+ wrap.addEventListener("click", function (ev) {
1612
+ if (ev.target.closest("[data-scan-close]") || ev.target === wrap) { if (wrap.parentNode) wrap.parentNode.removeChild(wrap); }
1613
+ });
1614
+ }
1615
+ function doMcpScanLocal() {
1616
+ showToast("ok", "正在扫描本机 MCP…");
1617
+ fetch(apiBase() + "/mcp/local-scan").then(function (r) { return r.json(); }).then(function (d) {
1618
+ if (!d || !d.ok) { showToast("err", "扫描失败:" + ((d && d.error) || "unknown")); return; }
1619
+ var conf = d.configured || [];
1620
+ var inst = d.installed || [];
1621
+ var notConf = inst.filter(function (x) { return !x.configured; });
1622
+ var rows = "";
1623
+ rows += '<div class="dsb-scan-sec">已配置的 MCP(' + conf.length + ' 个)</div>';
1624
+ if (!conf.length) rows += '<div class="dsb-scan-dim">(无)</div>';
1625
+ conf.forEach(function (c) {
1626
+ rows += '<div class="dsb-scan-row"><b>' + esc(c.serverName) + '</b>' +
1627
+ ' <span class="dsb-scan-dim">' + esc(c.transport || "") + (c.version ? " · v" + esc(c.version) : "") + '</span>' +
1628
+ (c.disabled ? ' <span class="dsb-scan-off">已停用</span>' : ' <span class="dsb-scan-on">启用</span>') +
1629
+ ' <span class="dsb-scan-dim">工具 ' + (c.toolCount || 0) + ' 个</span></div>';
1630
+ });
1631
+ rows += '<div class="dsb-scan-sec">本机已装但未配置(' + notConf.length + ' 个)</div>';
1632
+ if (!notConf.length) rows += '<div class="dsb-scan-dim">没有发现未配置的 MCP 包</div>';
1633
+ notConf.forEach(function (x) {
1634
+ rows += '<div class="dsb-scan-row"><b>' + esc(x.name) + '</b>' +
1635
+ (x.version ? ' <span class="dsb-scan-dim">v' + esc(x.version) + '</span>' : '') +
1636
+ (x.bin ? ' <span class="dsb-scan-dim">bin: ' + esc(x.bin) + '</span>' : '') +
1637
+ '<div class="dsb-scan-desc">' + esc(x.description || "(无描述)") + '</div></div>';
1638
+ });
1639
+ openMcpScanMenu(rows);
1640
+ }).catch(function (e) { showToast("err", "扫描失败:" + ((e && e.message) || e)); });
1641
+ }
1642
+
1643
+ function handleMcpClick(ev) {
1644
+ var M = mcpEnsure();
1645
+ var b = ev.target.closest("[data-mcp-act]");
1646
+ if (b) {
1647
+ var act = b.getAttribute("data-mcp-act");
1648
+ if (act === "refresh") { loadMcp(true); return; }
1649
+ if (act === "exportAll") { doMcpExport(null); return; }
1650
+ if (act === "clearStats") { doMcpClearStats(); return; }
1651
+ if (act === "copyExport") { copyMcpExport(); return; }
1652
+ if (act === "closeExport") { M.showExport = false; paintMcp(); return; }
1653
+ if (act === "mode-tree" || act === "mode-card") {
1654
+ M.mode = (act === "mode-card") ? "card" : "tree";
1655
+ try { localStorage.setItem("dsb-mcp-mode", M.mode); } catch (e) { }
1656
+ paintMcp(); return;
1657
+ }
1658
+ if (act === "expandAll") {
1659
+ M.expanded = { "__ROOT__": true };
1660
+ (M.data && M.data.servers ? M.data.servers : []).forEach(function (s) {
1661
+ M.expanded[s.id] = true;
1662
+ (s.tools || []).forEach(function (t) { M.expanded[s.id + "::" + t.rawName] = true; });
1663
+ });
1664
+ paintMcp(); return;
1665
+ }
1666
+ if (act === "collapseAll") { M.expanded = { "__ROOT__": true }; paintMcp(); return; }
1667
+ if (act === "cleanResidue") { doDetectResidue(); return; }
1668
+ if (act === "doCleanResidue") { doCleanResidue(); return; }
1669
+ if (act === "cancelResidue") { M.showResidue = false; M.residue = null; paintMcp(); return; }
1670
+ if (act === "scanLocal") { doMcpScanLocal(); return; }
1671
+ if (act === "aiFix") { doMcpAiFix(); return; }
1672
+ }
1673
+ // 树状:点行头展开 / 收起(点带 data-stop 的操作控件时不切换)
1674
+ var nd = ev.target.closest("[data-mcp-toggle-node]");
1675
+ if (nd && !ev.target.closest("[data-stop]")) {
1676
+ var nid = nd.getAttribute("data-mcp-toggle-node");
1677
+ M.expanded[nid] = !M.expanded[nid];
1678
+ paintMcp(); return;
1679
+ }
1680
+ var mc = ev.target.closest("[data-mcp-cols]");
1681
+ if (mc) {
1682
+ M.cols = parseInt(mc.getAttribute("data-mcp-cols"), 10) || 0;
1683
+ try { localStorage.setItem("dsb-mcp-cols", String(M.cols)); } catch (e) { }
1684
+ paintMcp(); return;
1685
+ }
1686
+ var sw = ev.target.closest("[data-mcp-switch]");
1687
+ if (sw) {
1688
+ doMcpToggle(sw.getAttribute("data-mcp-switch"), sw.getAttribute("data-on") !== "1");
1689
+ return;
1690
+ }
1691
+ var dg = ev.target.closest("[data-mcp-diag]");
1692
+ if (dg) {
1693
+ handleMcpDiag(M, dg.getAttribute("data-mcp-diag"));
1694
+ return;
1695
+ }
1696
+ var tn = ev.target.closest("[data-mcp-tool-node]");
1697
+ if (tn && !ev.target.closest("[data-stop]")) {
1698
+ var tkey = tn.getAttribute("data-mcp-tool-node");
1699
+ M.expanded[tkey] = !M.expanded[tkey];
1700
+ paintMcp(); return;
1701
+ }
1702
+ var rc = ev.target.closest("[data-mcp-reconnect]");
1703
+ if (rc) { doMcpReconnect(rc.getAttribute("data-mcp-reconnect")); return; }
1704
+ var dt = ev.target.closest("[data-mcp-detail]");
1705
+ if (dt) { var id = dt.getAttribute("data-mcp-detail"); M.detailId = (M.detailId === id ? null : id); paintMcp(); return; }
1706
+ var ex = ev.target.closest("[data-mcp-export]");
1707
+ if (ex) { doMcpExport(ex.getAttribute("data-mcp-export")); return; }
1708
+ }
1709
+
1710
+ function handleMcpChange(ev) {
1711
+ var t = ev.target.closest("[data-mcp-toggle]");
1712
+ if (!t) return;
1713
+ doMcpToggle(t.getAttribute("data-mcp-toggle"), t.checked);
1714
+ }
1715
+
1716
+ function doMcpToggle(id, enable) {
1717
+ var M = mcpEnsure();
1718
+ if (M.busy) return;
1719
+ M.busy = id; paintMcp();
1720
+ fetch(apiBase() + "/mcp/" + (enable ? "enable" : "disable"), {
1721
+ method: "POST", headers: { "Content-Type": "application/json" },
1722
+ body: JSON.stringify({ id: id })
1723
+ }).then(function (r) { return r.json(); }).then(function (d) {
1724
+ M.busy = "";
1725
+ if (!d || !d.ok) { showToast("err", "操作失败:" + ((d && d.error) || "未知错误")); loadMcp(true); return; }
1726
+ showToast("ok", enable ? "已启用(配置已写入)" : "已停用(配置已写入)");
1727
+ setTimeout(function () { loadMcp(true); }, 900);
1728
+ paintMcp();
1729
+ }).catch(function (e) { M.busy = ""; showToast("err", "操作失败:" + ((e && e.message) || e)); paintMcp(); });
1730
+ }
1731
+
1732
+ function doMcpReconnect(id) {
1733
+ var M = mcpEnsure();
1734
+ if (M.busy) return;
1735
+ M.busy = id; paintMcp();
1736
+ fetch(apiBase() + "/mcp/reconnect", {
1737
+ method: "POST", headers: { "Content-Type": "application/json" },
1738
+ body: JSON.stringify({ id: id })
1739
+ }).then(function (r) { return r.json(); }).then(function (d) {
1740
+ M.busy = "";
1741
+ if (!d || !d.ok) { showToast("err", "重连失败:" + ((d && d.error) || "未知错误")); paintMcp(); return; }
1742
+ showToast("ok", "已触发重连");
1743
+ setTimeout(function () { loadMcp(true); }, 900);
1744
+ paintMcp();
1745
+ }).catch(function (e) { M.busy = ""; showToast("err", "重连失败:" + ((e && e.message) || e)); paintMcp(); });
1746
+ }
1747
+
1748
+ function doMcpClearStats() {
1749
+ var M = mcpEnsure();
1750
+ M.statsLoading = true; paintMcp();
1751
+ fetch(apiBase() + "/mcp/stats/clear", { method: "POST" }).then(function (r) { return r.json(); }).then(function (d) {
1752
+ M.statsLoading = false;
1753
+ if (d && d.ok) { M.stats = null; showToast("ok", "统计已清空"); loadMcp(true); }
1754
+ else { showToast("err", (d && d.error) || "清空失败"); paintMcp(); }
1755
+ }).catch(function (e) { M.statsLoading = false; showToast("err", "清空失败:" + ((e && e.message) || e)); paintMcp(); });
1756
+ }
1757
+
1758
+ function doMcpExport(oneId) {
1759
+ var M = mcpEnsure();
1760
+ fetch(apiBase() + "/mcp/export?mask=0").then(function (r) { return r.json(); }).then(function (d) {
1761
+ if (!d || !d.ok) { showToast("err", "导出失败:" + ((d && d.error) || "未知错误")); return; }
1762
+ var all = d.mcpServers || {};
1763
+ var out = all;
1764
+ if (oneId) {
1765
+ var s = null;
1766
+ if (M.data && M.data.servers) M.data.servers.forEach(function (x) { if (x.id === oneId) s = x; });
1767
+ var name = s ? s.serverName : oneId;
1768
+ out = {}; out[name] = all[name] || {};
1769
+ }
1770
+ M.exportText = JSON.stringify({ mcpServers: out }, null, 2);
1771
+ M.showExport = true;
1772
+ paintMcp();
1773
+ }).catch(function (e) { showToast("err", "导出失败:" + ((e && e.message) || e)); });
1774
+ }
1775
+
1776
+ function doDetectResidue() {
1777
+ var M = mcpEnsure();
1778
+ fetch(apiBase() + "/mcp/residue").then(function (r) { return r.json(); }).then(function (d) {
1779
+ if (!d || !d.ok) { showToast("err", "检测失败:" + ((d && d.error) || "未知错误")); return; }
1780
+ M.residue = d.residue || [];
1781
+ M.showResidue = true;
1782
+ paintMcp();
1783
+ }).catch(function (e) { showToast("err", "检测失败:" + ((e && e.message) || e)); });
1784
+ }
1785
+
1786
+ function doCleanResidue() {
1787
+ var M = mcpEnsure();
1788
+ fetch(apiBase() + "/mcp/residue/clean", { method: "POST" }).then(function (r) { return r.json(); }).then(function (d) {
1789
+ if (!d || !d.ok) { showToast("err", "清理失败:" + ((d && d.error) || "未知错误")); return; }
1790
+ M.showResidue = false; M.residue = null;
1791
+ showToast("ok", d.cleaned ? ("已清理 " + d.cleaned + " 处残留") : "没有残留可清理");
1792
+ loadMcp(true);
1793
+ }).catch(function (e) { showToast("err", "清理失败:" + ((e && e.message) || e)); });
1794
+ }
1795
+
1796
+ function copyMcpExport() {
1797
+ var M = mcpEnsure();
1798
+ var ta = $("dsb-mcp-export-ta");
1799
+ var txt = ta ? ta.value : (M.exportText || "");
1800
+ try {
1801
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1802
+ navigator.clipboard.writeText(txt).then(function () { showToast("ok", "已复制到剪贴板"); }).catch(function () { fallbackCopy(ta, txt); });
1803
+ } else { fallbackCopy(ta, txt); }
1804
+ } catch (e) { fallbackCopy(ta, txt); }
1805
+ }
1806
+
1807
+ function fallbackCopy(ta, txt) {
1808
+ try {
1809
+ if (ta) { ta.removeAttribute("readonly"); ta.select(); document.execCommand("copy"); ta.setAttribute("readonly", "readonly"); }
1810
+ else { var t = document.createElement("textarea"); t.value = txt; document.body.appendChild(t); t.select(); document.execCommand("copy"); document.body.removeChild(t); }
1811
+ showToast("ok", "已复制到剪贴板");
1812
+ } catch (e) { showToast("err", "复制失败,请手动选中复制"); }
1813
+ }
1814
+
944
1815
  // 本地日期工具(避免时区漂移:全部用本地 Date 取 Y-M-D)
945
1816
  function ymd(d) {
946
1817
  var p = function (n) { return n < 10 ? "0" + n : String(n); };
@@ -1062,7 +1933,7 @@ window.__ModuleLoader__.load({
1062
1933
  if (!S.ledgerDetail || S.ledgerDetail.skill !== skill) return;
1063
1934
  var parts = [];
1064
1935
  if (d && d.ok) {
1065
- if (d.detail) parts.push("【人工台账失败明细】\n" + d.detail);
1936
+ if (d.detail) parts.push("【失败明细】\n" + d.detail);
1066
1937
  if (d.autoEntries && d.autoEntries.length) {
1067
1938
  var tot = (d.autoTotal == null ? d.autoEntries.length : d.autoTotal);
1068
1939
  var shownN = d.autoEntries.length;
@@ -1162,7 +2033,7 @@ window.__ModuleLoader__.load({
1162
2033
  '.dsb-cards{flex:1;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));' +
1163
2034
  'gap:8px;padding:6px 16px 14px;align-content:start}' +
1164
2035
  '.dsb-card{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);border-radius:10px;' +
1165
- 'padding:10px 12px;cursor:pointer;transition:background .12s}' +
2036
+ 'padding:10px 12px;cursor:pointer;transition:background .12s;display:flex;flex-direction:column;height:100%;box-sizing:border-box}' +
1166
2037
  '.dsb-card:hover{background:rgba(120,140,255,.14);border-color:rgba(120,140,255,.4)}' +
1167
2038
  '.dsb-card-name{font-family:ui-monospace,Consolas,monospace;font-size:11px;color:#8fc7ff;word-break:break-all}' +
1168
2039
  '.dsb-card-zh{font-size:11.5px;color:#dfe3ee;margin-top:5px;line-height:1.5;' +
@@ -1210,7 +2081,79 @@ window.__ModuleLoader__.load({
1210
2081
  '.dsb-lst-warn{color:#ffb35c}' +
1211
2082
  '.dsb-lst-ok{color:#9aa1b5}' +
1212
2083
  /* ── v1.7.0:排序切换 / 批量启停 / 单技能开关 / 停用视觉 ── */
1213
- '.dsb-sortbar{display:flex;align-items:center;gap:8px;padding:0 16px 6px}' +
2084
+ '.dsb-sortbar{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:0 16px 6px}' +
2085
+ // ── v2.0.0 MCP 管理面板 ──
2086
+ '.dsb-mcp{flex:1;min-height:0;display:none;flex-direction:column;padding:0 16px 14px}' +
2087
+ '.dsb-mcp-top{display:flex;align-items:center;gap:8px;padding:4px 0 10px;flex-wrap:wrap}' +
2088
+ '.dsb-mcp-sum{font-size:12px;color:var(--hintC,#9aa1b5)}' +
2089
+ '.dsb-mcp-grid{flex:1;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:10px;align-content:start}' +
2090
+ // 树状视图(v2.0.1)
2091
+ '.dsb-mcp-tree{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column}' +
2092
+ '.dsb-mcp-node{border-bottom:1px solid rgba(128,138,160,.14)}' +
2093
+ '.dsb-mcp-row{display:flex;align-items:center;gap:7px;padding:7px 8px;border-radius:7px;cursor:pointer}' +
2094
+ '.dsb-mcp-row:hover{background:var(--chipBg,rgba(128,138,160,.10))}' +
2095
+ '.dsb-mcp-caret{width:14px;height:14px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;color:var(--hintC,#9aa1b5);transition:transform .15s ease;transform-origin:center}' +
2096
+ '.dsb-mcp-caret::before{content:"";width:0;height:0;border-left:5px solid currentColor;border-top:4px solid transparent;border-bottom:4px solid transparent;margin-left:1px}' +
2097
+ '.dsb-mcp-caret.open{transform:rotate(90deg)}' +
2098
+ '.dsb-mcp-dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto}' +
2099
+ '.dsb-mcp-node-name{font-size:13.5px;font-weight:700;color:var(--ptx,#e8eaf2);flex:0 0 auto}' +
2100
+ '.dsb-mcp-badge{font-size:10.5px;color:var(--hintC,#9aa1b5);background:rgba(128,138,160,.14);border:1px solid var(--pbd,rgba(128,138,160,.22));padding:1px 7px;border-radius:20px;flex:0 0 auto}' +
2101
+ '.dsb-mcp-children{margin-left:20px;padding:2px 0 8px 12px;border-left:1px dashed var(--pbd,rgba(128,138,160,.3))}' +
2102
+ '.dsb-mcp-children .dsb-mcp-summary{margin:1px 0 6px}' +
2103
+ '.dsb-mcp-sub{margin:8px 0 4px;font-weight:600;font-size:11.5px;color:var(--ptx,#e8eaf2)}' +
2104
+ '.dsb-mcp-sub2{color:var(--hintC,#9aa1b5);font-size:11px;padding:2px 0 6px}' +
2105
+ '.dsb-mcp-src{margin-top:6px;color:var(--hintC,#9aa1b5);font-size:10.5px}' +
2106
+ '.dsb-mcp-tree-tools{display:flex;flex-direction:column;gap:1px}' +
2107
+ // 三级大树:根、工具节点、工具明细、树形连线(v2.0.2)
2108
+ '.dsb-mcp-rootrow{background:rgba(91,140,255,.10);border:1px solid rgba(91,140,255,.22);border-radius:8px;margin:3px 0}' +
2109
+ '.dsb-mcp-rootname{font-size:14px;font-weight:800;color:var(--stA,#5b8cff)}' +
2110
+ '.dsb-mcp-toolnode{position:relative}' +
2111
+ '.dsb-mcp-toolrow{display:flex;align-items:center;gap:6px;padding:4px 6px;border-radius:6px;cursor:pointer}' +
2112
+ '.dsb-mcp-toolrow:hover{background:var(--chipBg,rgba(128,138,160,.10))}' +
2113
+ '.dsb-mcp-caret-s{width:11px;height:11px}' +
2114
+ '.dsb-mcp-caret-s::before{border-left-width:4px;border-top-width:3px;border-bottom-width:3px;margin-left:0}' +
2115
+ '.dsb-mcp-toolname{font-weight:600;color:var(--ptx,#e8eaf2);font-size:12px;flex:0 1 auto;max-width:52%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
2116
+ '.dsb-mcp-toolen{color:var(--hintC,#9aa1b5);font-size:10.5px;flex:0 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
2117
+ '.dsb-mcp-toolstate{font-size:10.5px;font-weight:600;flex:0 0 auto}' +
2118
+ '.dsb-mcp-switch{flex:0 0 auto}' +
2119
+ '.dsb-mcp-switch .dsb-sw-label{font-size:11px}' +
2120
+ '.dsb-mcp-diag{color:#f87171;border-color:rgba(248,113,113,.4);background:rgba(248,113,113,.12);padding:2px 8px;font-size:10.5px}' +
2121
+ '.dsb-mcp-diag:hover{background:rgba(248,113,113,.24)}' +
2122
+ '.dsb-mcp-toolchildren{margin-left:16px;padding:3px 0 4px 10px;border-left:1px dashed var(--pbd,rgba(128,138,160,.3))}' +
2123
+ '.dsb-mcp-kv{display:flex;gap:10px;font-size:11px;padding:1px 0}' +
2124
+ '.dsb-mcp-kv span{color:var(--hintC,#9aa1b5);min-width:56px}' +
2125
+ '.dsb-mcp-kv b{color:var(--ptx,#e8eaf2);font-weight:600;word-break:break-all;min-width:0}' +
2126
+ '.dsb-mcp-children>.dsb-mcp-node{position:relative}' +
2127
+ '.dsb-mcp-children>.dsb-mcp-node::before{content:"";position:absolute;left:-13px;top:16px;width:11px;height:1px;background:var(--pbd,rgba(128,138,160,.35))}' +
2128
+ '.dsb-mcp-card{background:var(--chipBg,rgba(128,138,160,.07));border:1px solid var(--pbd,rgba(128,138,160,.25));border-radius:11px;padding:11px 12px;display:flex;flex-direction:column;gap:7px}' +
2129
+ '.dsb-mcp-hd{display:flex;align-items:center;gap:7px}' +
2130
+ '.dsb-mcp-name{font-size:14px;font-weight:700;color:var(--ptx,#e8eaf2);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
2131
+ '.dsb-mcp-transport{font-size:10.5px;padding:1px 6px;border-radius:5px;background:rgba(91,140,255,.16);color:var(--stA,#5b8cff);border:1px solid rgba(91,140,255,.3);flex:0 0 auto}' +
2132
+ '.dsb-mcp-status{display:flex;align-items:center;gap:6px;font-size:11.5px}' +
2133
+ '.dsb-pill{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600;border:1px solid transparent}' +
2134
+ '.dsb-pill-dot{width:6px;height:6px;border-radius:50%;background:currentColor;flex:0 0 auto}' +
2135
+ '.dsb-pill-active{color:#34d399;background:rgba(52,211,153,.12);border-color:rgba(52,211,153,.3)}' +
2136
+ '.dsb-pill-failed{color:#f87171;background:rgba(248,113,113,.12);border-color:rgba(248,113,113,.3)}' +
2137
+ '.dsb-pill-warn{color:#fbbf24;background:rgba(251,191,36,.12);border-color:rgba(251,191,36,.3)}' +
2138
+ '.dsb-pill-off{color:#9aa1b5;background:rgba(154,161,181,.12);border-color:rgba(154,161,181,.28)}' +
2139
+ '.dsb-mcp-target{font-size:11px;color:var(--hintC,#9aa1b5);word-break:break-all;line-height:1.5}' +
2140
+ '.dsb-mcp-summary{font-size:11.5px;color:var(--ptx,#e8eaf2);line-height:1.55}' +
2141
+ '.dsb-mcp-calls{font-size:11px;color:var(--hintC,#9aa1b5)}' +
2142
+ '.dsb-mcp-acts{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:2px}' +
2143
+ '.dsb-mcp-btn{font-size:11px;padding:3px 9px;border-radius:6px;cursor:pointer;background:rgba(128,138,160,.14);border:1px solid var(--pbd,rgba(128,138,160,.3));color:var(--ptx,#e8eaf2);font-family:inherit}' +
2144
+ '.dsb-mcp-btn:hover{background:rgba(128,138,160,.26)}' +
2145
+ '.dsb-mcp-btn.primary{background:linear-gradient(135deg,#5b6cff,#2fa8ff);border-color:transparent;color:#fff}' +
2146
+ '.dsb-mcp-btn:disabled{opacity:.45;cursor:not-allowed}' +
2147
+ '.dsb-mcp-switch{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--ptx,#e8eaf2);cursor:pointer}' +
2148
+ '.dsb-mcp-detail{margin-top:4px;border-top:1px dashed var(--pbd,rgba(128,138,160,.3));padding-top:8px;font-size:11.5px}' +
2149
+ '.dsb-mcp-tools{max-height:180px;overflow:auto;display:flex;flex-direction:column;gap:3px}' +
2150
+ '.dsb-mcp-tool{display:flex;gap:8px;padding:2px 0;border-bottom:1px solid rgba(128,138,160,.1);align-items:baseline}' +
2151
+ '.dsb-mcp-tool b{font-weight:600;color:var(--stA,#5b8cff);font-size:11px;flex:0 0 auto;max-width:56%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
2152
+ '.dsb-mcp-tool span{color:var(--hintC,#9aa1b5);font-size:10.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}' +
2153
+ '.dsb-mcp-empty{font-size:12px;color:var(--hintC,#9aa1b5);padding:24px;text-align:center;line-height:1.7}' +
2154
+ '.dsb-mcp-export{width:100%;min-height:160px;background:rgba(0,0,0,.3);border:1px solid var(--pbd,rgba(128,138,160,.3));border-radius:8px;color:var(--ptx,#e8eaf2);font-family:ui-monospace,Consolas,monospace;font-size:11px;padding:8px;resize:vertical;box-sizing:border-box}' +
2155
+ '.dsb-mcp-residue{background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.3);border-radius:8px;padding:9px 11px;margin-bottom:10px}' +
2156
+ '.dsb-mcp-residue-item{font-size:11.5px;color:var(--ptx,#e8eaf2);line-height:1.6;padding:1px 0;word-break:break-all}' +
1214
2157
  '.dsb-sortbtn{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);color:#cdd3e3;' +
1215
2158
  'border-radius:999px;padding:3px 12px;font-size:12px;cursor:pointer;white-space:nowrap}' +
1216
2159
  '.dsb-sortbtn:hover{background:rgba(255,255,255,.14)}' +
@@ -1284,6 +2227,12 @@ window.__ModuleLoader__.load({
1284
2227
  '#dsh-skill-browser-panel .dsb-bmenu-danger .dsb-bmenu-sub{color:#ff9d9d}' +
1285
2228
  '#dsh-skill-browser-panel .dsb-bmenu-cancel{background:transparent;border:none;color:#9aa1b5;font-size:12px;cursor:pointer;padding:4px 6px;text-align:center}' +
1286
2229
  '#dsh-skill-browser-panel .dsb-bmenu-cancel:hover{color:#dfe3ee}' +
2230
+ // v2.0.10 AI 分组方案弹层:两个操作按钮与背景明显区分,且随皮肤(主色用 --dsb-onbrand-bg)
2231
+ '#dsh-skill-browser-panel .dsb-plan-menu .dsb-bmenu-item{padding:9px 11px!important;font-weight:600;border:1px solid var(--pbd,rgba(255,255,255,.2))!important}' +
2232
+ '#dsh-skill-browser-panel .dsb-plan-menu .dsb-bmenu-item[data-plan-act="replace"]{background:linear-gradient(135deg,rgba(248,113,113,.30),rgba(248,113,113,.14))!important;border-color:rgba(248,113,113,.55)!important;color:#ffe2e2!important}' +
2233
+ '#dsh-skill-browser-panel .dsb-plan-menu .dsb-bmenu-item[data-plan-act="replace"]:hover{background:rgba(248,113,113,.38)!important}' +
2234
+ '#dsh-skill-browser-panel .dsb-plan-menu .dsb-bmenu-item[data-plan-act="merge"]{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;border-color:transparent!important;color:#fff!important}' +
2235
+ '#dsh-skill-browser-panel .dsb-plan-menu .dsb-bmenu-item[data-plan-act="merge"]:hover{filter:brightness(1.08)}' +
1287
2236
  '#dsh-skill-browser-panel .dsb-lc-date{font-size:10.5px;color:#8b93a8;white-space:nowrap}' +
1288
2237
  '#dsh-skill-browser-panel .dsb-lc-rank{font-size:10.5px;color:#8b93a8;text-align:right;white-space:nowrap}' +
1289
2238
  '#dsh-skill-browser-panel .dsb-lc-status{font-size:10.5px;color:#ffb3b3;white-space:nowrap}' +
@@ -1373,6 +2322,11 @@ window.__ModuleLoader__.load({
1373
2322
  '#dsh-skill-browser-panel .dsb-gadd-kw{border-color:var(--dsb-okline,rgba(80,220,150,.34));color:var(--dsb-ok,#9ff5c8)}' +
1374
2323
  '#dsh-skill-browser-panel .dsb-gbusy{opacity:.55;pointer-events:none}' +
1375
2324
  '#dsh-skill-browser-panel .dsb-gtitle{font-size:10.5px;color:var(--dsb-dim,#8b93a8);white-space:nowrap}' +
2325
+ // v2.0.11 分组工具条美化:统一间距、按钮实心化、竖分隔线、选中态皮肤主色
2326
+ '#dsh-skill-browser-panel .dsb-gbar{gap:8px;row-gap:8px;padding:2px 0 8px}' +
2327
+ '#dsh-skill-browser-panel .dsb-gsep{width:1px;height:16px;background:var(--pbd,rgba(255,255,255,.18));margin:0 3px;flex:0 0 auto;border-radius:1px}' +
2328
+ '#dsh-skill-browser-panel .dsb-gbar .dsb-gadd{height:26px!important;padding:0 10px!important;border:1px solid var(--pbd,rgba(255,255,255,.2))!important;border-radius:8px!important;font-size:11px!important;line-height:1!important}' +
2329
+ '#dsh-skill-browser-panel .dsb-gbar .dsb-gadd-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;color:#fff!important;border-color:transparent!important}' +
1376
2330
  '#dsh-skill-browser-panel .dsb-ghint{font-size:10.5px;color:var(--dsb-dim,#6f778c);white-space:nowrap;' +
1377
2331
  'overflow:hidden;text-overflow:ellipsis;max-width:44%}' +
1378
2332
  // v1.9.9 对比度修正:数字徽标一律「继承父级文字色」,不再写死
@@ -1412,6 +2366,30 @@ window.__ModuleLoader__.load({
1412
2366
  /* ── v1.9.3:AI 分组方案确认弹层 ── */
1413
2367
  '#dsh-skill-browser-panel .dsb-plan-menu{min-width:0;width:calc(100% - 24px);left:12px;overflow-y:auto}' +
1414
2368
  '#dsh-skill-browser-panel .dsb-plan-stat{font-size:10.5px;color:var(--dsb-dim,#8b93a8);padding:2px 8px 6px}' +
2369
+ '#dsh-skill-browser-panel .dsb-plan-time{font-size:11px;font-weight:600;color:var(--dsb-brand,#8fc7ff);padding:0 8px 8px}' +
2370
+ '#dsh-skill-browser-panel .dsb-plan-recache{background:transparent;border:1px dashed var(--pbd,rgba(255,255,255,.22));color:var(--dsb-dim,#9aa1b5);font-size:12px;padding:6px;border-radius:8px;cursor:pointer;margin:2px 0}' +
2371
+ '#dsh-skill-browser-panel .dsb-plan-recache:hover{border-color:var(--stA,#7c8dff);color:var(--ptx,#eef)}' +
2372
+ '#dsh-skill-browser-panel .dsb-ai-last{font-size:10.5px;color:var(--dsb-dim,#8b93a8);white-space:nowrap;flex:0 0 auto}' +
2373
+ '#dsh-skill-browser-panel .dsb-tprompt{position:absolute;inset:0;background:rgba(0,0,0,.5);z-index:70;display:flex;align-items:center;justify-content:center}' +
2374
+ '#dsh-skill-browser-panel .dsb-tprompt-box{background:#1f2430;border:1px solid rgba(255,255,255,.2);border-radius:10px;padding:16px;width:min(320px,80%);box-shadow:0 12px 32px rgba(0,0,0,.55)}' +
2375
+ '#dsh-skill-browser-panel .dsb-tprompt-title{font-size:12.5px;color:#e8ecf6;margin-bottom:10px}' +
2376
+ '#dsh-skill-browser-panel .dsb-tprompt-inp{width:100%;box-sizing:border-box;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.2);border-radius:6px;color:#eef;font-size:12.5px;padding:7px 9px;outline:none}' +
2377
+ '#dsh-skill-browser-panel .dsb-tprompt-ops{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}' +
2378
+ '#dsh-skill-browser-panel .dsb-scan{position:absolute;inset:0;background:rgba(0,0,0,.55);z-index:70;display:flex;align-items:center;justify-content:center}' +
2379
+ '#dsh-skill-browser-panel .dsb-scan-box{background:#1f2430;border:1px solid rgba(255,255,255,.2);border-radius:10px;width:min(560px,90%);max-height:82%;display:flex;flex-direction:column;box-shadow:0 16px 40px rgba(0,0,0,.6)}' +
2380
+ '#dsh-skill-browser-panel .dsb-scan-h{font-size:13px;font-weight:700;color:#e8ecf6;padding:13px 16px;border-bottom:1px solid rgba(255,255,255,.12);display:flex;justify-content:space-between}' +
2381
+ '#dsh-skill-browser-panel .dsb-scan-close{cursor:pointer;color:#9aa1b5}' +
2382
+ '#dsh-skill-browser-panel .dsb-scan-list{overflow-y:auto;padding:8px 16px 16px;font-size:12px;color:#dfe3ee}' +
2383
+ '#dsh-skill-browser-panel .dsb-scan-sec{margin:12px 0 4px;font-weight:700;color:var(--dsb-brand,#8fc7ff)}' +
2384
+ '#dsh-skill-browser-panel .dsb-scan-row{padding:5px 0;border-bottom:1px dashed rgba(255,255,255,.08)}' +
2385
+ '#dsh-skill-browser-panel .dsb-scan-dim{color:#8b93a8;font-size:11px}' +
2386
+ '#dsh-skill-browser-panel .dsb-scan-desc{color:#9aa1b5;font-size:11px;margin-top:2px}' +
2387
+ '#dsh-skill-browser-panel .dsb-scan-on{color:#5fd08a;font-size:11px}' +
2388
+ '#dsh-skill-browser-panel .dsb-scan-off{color:#c9a24a;font-size:11px}' +
2389
+ '#dsh-skill-browser-panel .dsb-ai-prog{position:absolute;left:50%;bottom:16px;transform:translateX(-50%);z-index:40;width:min(340px,82%);box-sizing:border-box;background:#1f2430;border:1px solid rgba(255,255,255,.20);border-radius:10px;padding:10px 14px;box-shadow:0 10px 28px rgba(0,0,0,.55);font-size:11.5px;color:#e8ecf6}' +
2390
+ '#dsh-skill-browser-panel .dsb-ai-prog-txt{font-weight:600}' +
2391
+ '#dsh-skill-browser-panel .dsb-ai-prog-bar{height:6px;border-radius:3px;background:rgba(255,255,255,.14);margin-top:8px;overflow:hidden}' +
2392
+ '#dsh-skill-browser-panel .dsb-ai-prog-bar>i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff));transition:width .2s}' +
1415
2393
  '#dsh-skill-browser-panel .dsb-plan-list{max-height:52vh;overflow-y:auto;padding:2px 4px 6px;display:flex;' +
1416
2394
  'flex-direction:column;gap:5px}' +
1417
2395
  '#dsh-skill-browser-panel .dsb-plan-dom{background:var(--dsb-soft,rgba(255,255,255,.04));' +
@@ -1429,7 +2407,7 @@ window.__ModuleLoader__.load({
1429
2407
  '#dsh-skill-browser-panel .dsb-movebtn{background:var(--chipBg,rgba(255,255,255,.06));border:1px solid var(--pbd,rgba(255,255,255,.14));color:var(--dsb-dim,#b9c2d8);' +
1430
2408
  'border-radius:6px;font-size:10.5px;padding:2px 7px;cursor:pointer;white-space:nowrap}' +
1431
2409
  '#dsh-skill-browser-panel .dsb-movebtn:hover{background:var(--dsb-hover,rgba(120,140,255,.24));color:var(--ptx,#fff);border-color:var(--stA,#7c8dff)}' +
1432
- '#dsh-skill-browser-panel .dsb-card-foot{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:6px}' +
2410
+ '#dsh-skill-browser-panel .dsb-card-foot{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:auto;padding-top:8px}' +
1433
2411
  '#dsh-skill-browser-panel .dsb-card-gtag{font-size:10px;color:var(--dsb-brand,#8fc7ff);opacity:.95;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
1434
2412
  /* ── v1.9.1:调用台账看板(全部改用语义变量,浅色皮肤同样可读)── */
1435
2413
  '#dsh-skill-browser-panel .dsb-lbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:0 0 8px}' +
@@ -1499,11 +2477,12 @@ window.__ModuleLoader__.load({
1499
2477
  panel.id = PANEL_ID;
1500
2478
  panel.innerHTML =
1501
2479
  '<div class="dsb-head">' +
1502
- '<div class="dsb-title">技能浏览器</div>' +
2480
+ '<div class="dsb-title">技能与 MCP 浏览器</div>' +
1503
2481
  '<div class="dsb-tabs">' +
1504
2482
  '<button id="dsb-tab-skills" class="dsb-tab" data-view="skills">技能浏览</button>' +
1505
2483
  '<button id="dsb-tab-ledger" class="dsb-tab" data-view="ledger">调用台账</button>' +
1506
- '<button id="dsb-tab-guide" class="dsb-tab" data-view="guide">' + DSB_ICONS.book + '快速上手</button>' +
2484
+ '<button id="dsb-tab-mcp" class="dsb-tab" data-view="mcp">MCP</button>' +
2485
+ '<button id="dsb-tab-guide" class="dsb-tab" data-view="guide">快速上手</button>' +
1507
2486
  '</div>' +
1508
2487
  '<div class="dsb-headbtns">' +
1509
2488
  '<button id="dsb-skin-btn" class="dsb-btn" title="切换皮肤(三球同步 · 130款)">' + DSB_ICONS.palette + '</button>' +
@@ -1517,7 +2496,7 @@ window.__ModuleLoader__.load({
1517
2496
  '<div id="dsb-skin" class="dsb-skinmenu"></div>' +
1518
2497
  '<div id="dsb-subtitle" class="dsb-subtitle"></div>' +
1519
2498
  '<div id="dsb-settings" class="dsb-settings" style="display:none">' +
1520
- '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-ball" /> 技能浏览器悬浮球(🧩)</label>' +
2499
+ '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-ball" /> 技能与 MCP 浏览器悬浮球(🧩)</label>' +
1521
2500
  '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-font" /> 字体插件悬浮球(🔤)</label>' +
1522
2501
  '<div class="dsb-set-row" style="flex-wrap:wrap">' +
1523
2502
  '<span style="min-width:70px">技能库位置</span>' +
@@ -1525,12 +2504,11 @@ window.__ModuleLoader__.load({
1525
2504
  '<button id="dsb-set-root-browse" class="dsb-btn" title="打开系统文件夹选择框">📁 选择</button>' +
1526
2505
  '<button id="dsb-set-root-save" class="dsb-btn">保存</button>' +
1527
2506
  '</div>' +
1528
- '<div class="dsb-set-row" style="flex-wrap:wrap">' +
1529
- '<span style="min-width:70px">悬浮球皮肤</span>' +
1530
- '<select id="dsb-set-skin" style="flex:1;min-width:220px;background:rgba(0,0,0,.35);border:1px solid rgba(255,255,255,.14);border-radius:6px;color:inherit;font-size:12px;padding:4px 6px;outline:none"></select>' +
1531
- '<span class="dsb-set-hint">三球同步 · 128 款</span>' +
1532
- '</div>' +
1533
2507
  '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-ledger" /> 失效台账自动登记(记录每次技能工具调用结果)</label>' +
2508
+ '<div class="dsb-set-row"><span style="min-width:70px">AI 分组缓存</span>' +
2509
+ '<button id="dsb-set-clear-cache" class="dsb-btn" title="清除 AI 分组的本地结果缓存(只动缓存文件,不碰任何技能文件)">🗑 清除缓存</button>' +
2510
+ '<span id="dsb-set-cache-status" class="dsb-set-hint">命中缓存时 AI 分组秒回</span>' +
2511
+ '</div>' +
1534
2512
  '<span id="dsb-set-status" class="dsb-set-hint">当前技能库:识别中…</span>' +
1535
2513
  '<span class="dsb-set-hint">💡 悬浮球显隐也可在 <b>DSH 设置页 → 「悬浮球导航」分区</b> 中控制;悬浮球可拖动;清单面板始终居中</span>' +
1536
2514
  // ── v1.7.4 广告区(用户指定落位:设置面板 hint 之后)──
@@ -1560,10 +2538,11 @@ window.__ModuleLoader__.load({
1560
2538
  '<div id="dsb-chips" class="dsb-chips"></div>' +
1561
2539
  '<div id="dsb-subs" class="dsb-subs"></div>' +
1562
2540
  '<div id="dsb-cards" class="dsb-cards"></div>' +
2541
+ '<div id="dsb-mcp" class="dsb-mcp" style="display:none"></div>' +
1563
2542
  '<div id="dsb-guide" class="dsb-guide" style="display:none">' +
1564
2543
  '<div class="dsb-guide-inner">' +
1565
2544
  '<h3>' + DSB_ICONS.book + '快速上手</h3>' +
1566
- '<p class="dsb-guide-sub">60 秒学会用技能浏览器管理你的技能库</p>' +
2545
+ '<p class="dsb-guide-sub">60 秒学会用技能与 MCP 浏览器管理你的技能库</p>' +
1567
2546
  '<div class="dsb-guide-card">' + dsbGico('folder') + '<b>① 先指定技能库位置(第一步)</b><br>点右上角 <b>设置</b> 齿轮 → 找到「<b>技能库位置</b>」→ 点 <b>选择</b> 弹出系统文件夹框,选中你的技能包文件夹(例如 <b>E:\DSHskills</b>)→ 点 <b>保存</b>。输入框留空 = 自动识别(~/.dsh/skills 等默认位置)。<br>保存后会自动重扫,下方状态行显示「当前技能库:…」确认路径正确。</div>' +
1568
2547
  '<div class="dsb-guide-card">' + dsbGico('refresh') + '<b>② 刷新,让新技能进列表</b><br>技能文件增删改之后,点右上角 <b>刷新</b>(↻)重新扫描技能库。要让它<b>不重启 DSH 立即生效</b>,再点一下 <b>热重载</b>(⚡)。顺序永远是:<b>先选库 → 再刷新 → 最后热重载</b>。</div>' +
1569
2548
  '<div class="dsb-guide-card">' + dsbGico('search') + '<b>③ 搜索与筛选技能</b><br>顶部搜索框支持<b>技能名 / 中文简介 / 描述</b>关键字。下方分组栏:<b>我的分组</b>(树状/平铺切换)→ <b>关键词分组</b>(按触发词自动归类)→ <b>AI 自动分组</b>(调模型智能归类)。存在自定义分组时,默认分类行会自动隐藏。</div>' +
@@ -1641,6 +2620,10 @@ window.__ModuleLoader__.load({
1641
2620
  var pact = pa.getAttribute("data-plan-act");
1642
2621
  if (pact === "replace") applyPlanFlow("replace");
1643
2622
  else if (pact === "merge") applyPlanFlow("merge");
2623
+ else if (pact === "recache") {
2624
+ closePlanMenu();
2625
+ postGroups({ action: "clear-classify-cache" }, function () { aiClassifyFlow(); });
2626
+ }
1644
2627
  else closePlanMenu();
1645
2628
  return;
1646
2629
  }
@@ -1682,6 +2665,12 @@ window.__ModuleLoader__.load({
1682
2665
  paintGroups();
1683
2666
  return;
1684
2667
  }
2668
+ var gf = ev.target.closest("[data-gfold]");
2669
+ if (gf) {
2670
+ S.gFold = gf.getAttribute("data-gfold") === "1";
2671
+ paintGroups();
2672
+ return;
2673
+ }
1685
2674
  var add = ev.target.closest("[data-gadd]");
1686
2675
  if (add) {
1687
2676
  var how = add.getAttribute("data-gadd");
@@ -1694,19 +2683,23 @@ window.__ModuleLoader__.load({
1694
2683
  var gs = ev.target.closest("[data-gsub]");
1695
2684
  if (gs) {
1696
2685
  S.gsub = gs.getAttribute("data-gsub") || "";
1697
- paintGroups(); paintCards();
2686
+ paintGroups(); paintCards(); paintSortbar();
1698
2687
  return;
1699
2688
  }
1700
2689
  var card = ev.target.closest("[data-gsel]");
1701
2690
  if (card) {
1702
2691
  S.group = card.getAttribute("data-gsel") || "all";
1703
2692
  S.gsub = "";
1704
- paintGroups(); paintCards();
2693
+ paintGroups(); paintCards(); paintSortbar();
1705
2694
  }
1706
2695
  });
1707
2696
 
1708
2697
  $("dsb-close").addEventListener("click", function () { closePanel(); });
1709
- $("dsb-refresh").addEventListener("click", function () { if (S.view === "ledger") loadLedgerRefresh(); else load(true); });
2698
+ $("dsb-refresh").addEventListener("click", function () {
2699
+ if (S.view === "ledger") loadLedgerRefresh();
2700
+ else if (S.view === "mcp") loadMcp(true);
2701
+ else load(true);
2702
+ });
1710
2703
 
1711
2704
  // ── v1.9.4 热加载:重新加载插件代码,不重启桌面端 ──────────────
1712
2705
  $("dsb-hot").addEventListener("click", function () { hotReloadFlow(); });
@@ -1738,7 +2731,23 @@ window.__ModuleLoader__.load({
1738
2731
  });
1739
2732
  var rootBrowse = $("dsb-set-root-browse");
1740
2733
  if (rootBrowse) rootBrowse.addEventListener("click", function () { browseForDirectory(); });
1741
- $("dsb-q").addEventListener("input", function () { S.q = this.value; S.sub = ""; paintChips(); paintSubs(); paintCards(); });
2734
+ // v2.0.14 清除 AI 分组缓存按钮(只清缓存文件,不碰任何技能文件)
2735
+ var clearCacheBtn = $("dsb-set-clear-cache");
2736
+ if (clearCacheBtn) clearCacheBtn.addEventListener("click", function () {
2737
+ clearCacheBtn.disabled = true;
2738
+ postGroups({ action: "clear-classify-cache" }, function (d) {
2739
+ clearCacheBtn.disabled = false;
2740
+ var st = $("dsb-set-cache-status");
2741
+ if (d && d.ok) {
2742
+ if (st) st.textContent = "✅ 已清除,下次 AI 分组将重新调用模型";
2743
+ showToast("ok", "AI 分组缓存已清除");
2744
+ } else {
2745
+ if (st) st.textContent = "清除失败:" + String((d && d.error) || "unknown");
2746
+ showToast("err", "清除缓存失败");
2747
+ }
2748
+ });
2749
+ });
2750
+ $("dsb-q").addEventListener("input", function () { S.q = this.value; S.sub = ""; paintChips(); paintSubs(); paintCards(); paintSortbar(); });
1742
2751
  // v1.9.6 技能列表滚到底自动追加一批(无感分页,避免一次性渲染上千节点)
1743
2752
  $("dsb-cards").addEventListener("scroll", function () {
1744
2753
  var b = this;
@@ -1757,6 +2766,18 @@ window.__ModuleLoader__.load({
1757
2766
  if (!S.lstat) loadStats();
1758
2767
  });
1759
2768
  $("dsb-tab-guide").addEventListener("click", function () { S.view = "guide"; S.detail = null; S.ledgerDetail = null; paint(); });
2769
+ $("dsb-tab-mcp").addEventListener("click", function () {
2770
+ S.view = "mcp"; S.detail = null; S.ledgerDetail = null;
2771
+ paint();
2772
+ if (!S.mcp || !S.mcp.data) loadMcp(true);
2773
+ else if (!S.mcp.stats) loadMcpStats();
2774
+ });
2775
+ // v2.0.0 MCP 面板:事件委托(innerHTML 重绘不丢监听)
2776
+ var mcpHost = $("dsb-mcp");
2777
+ if (mcpHost) {
2778
+ mcpHost.addEventListener("click", handleMcpClick);
2779
+ mcpHost.addEventListener("change", handleMcpChange);
2780
+ }
1760
2781
  $("dsb-ledger-table").addEventListener("click", function (ev) {
1761
2782
  // 排行表头排序
1762
2783
  var sEl = ev.target.closest("[data-lsort]");
@@ -1801,7 +2822,7 @@ window.__ModuleLoader__.load({
1801
2822
  var el = ev.target.closest("[data-sub]");
1802
2823
  if (!el) return;
1803
2824
  S.sub = el.getAttribute("data-sub");
1804
- paintSubs(); paintCards();
2825
+ paintSubs(); paintCards(); paintSortbar();
1805
2826
  });
1806
2827
  // v1.7.0 排序切换 + v1.7.1 四态循环 + 分类级批量启停(事件委托到排序栏容器)
1807
2828
  $("dsb-sortbar").addEventListener("click", function (ev) {
@@ -1819,6 +2840,13 @@ window.__ModuleLoader__.load({
1819
2840
  openBatchMenu(agg, agg.getAttribute("data-bagg") || "global");
1820
2841
  return;
1821
2842
  }
2843
+ var cc = ev.target.closest("[data-cardcols]");
2844
+ if (cc) {
2845
+ S.cardCols = parseInt(cc.getAttribute("data-cardcols"), 10) || 0;
2846
+ try { localStorage.setItem("dsb-card-cols", String(S.cardCols)); } catch (e2) { }
2847
+ paintSortbar(); paintCards();
2848
+ return;
2849
+ }
1822
2850
  });
1823
2851
  // v1.7.4 设置面板推广卡(data-ext-url)点击直达 GitHub:
1824
2852
  // 广告卡迁入 #dsb-settings 后,原 #dsb-cards 委托覆盖不到,此处补一条同款委托。
@@ -1912,8 +2940,9 @@ window.__ModuleLoader__.load({
1912
2940
  if (!S.open) return;
1913
2941
  var isLedger = S.view === "ledger";
1914
2942
  var isGuide = S.view === "guide";
1915
- var q = $("dsb-q"), chips = $("dsb-chips"), subs = $("dsb-subs"), cards = $("dsb-cards"), lg = $("dsb-ledger"), sortbar = $("dsb-sortbar"), groups = $("dsb-groups"), gd = $("dsb-guide");
1916
- var skillsVis = !isLedger && !isGuide ? "" : "none";
2943
+ var isMcp = S.view === "mcp";
2944
+ var q = $("dsb-q"), chips = $("dsb-chips"), subs = $("dsb-subs"), cards = $("dsb-cards"), lg = $("dsb-ledger"), sortbar = $("dsb-sortbar"), groups = $("dsb-groups"), gd = $("dsb-guide"), mcpEl = $("dsb-mcp");
2945
+ var skillsVis = !isLedger && !isGuide && !isMcp ? "" : "none";
1917
2946
  if (q) q.style.display = skillsVis;
1918
2947
  if (groups) groups.style.display = skillsVis;
1919
2948
  if (chips) chips.style.display = skillsVis;
@@ -1922,12 +2951,15 @@ window.__ModuleLoader__.load({
1922
2951
  if (sortbar) sortbar.style.display = skillsVis;
1923
2952
  if (lg) lg.style.display = isLedger ? "flex" : "none";
1924
2953
  if (gd) gd.style.display = isGuide ? "block" : "none";
1925
- var t1 = $("dsb-tab-skills"), t2 = $("dsb-tab-ledger"), t3 = $("dsb-tab-guide");
2954
+ if (mcpEl) mcpEl.style.display = isMcp ? "flex" : "none";
2955
+ var t1 = $("dsb-tab-skills"), t2 = $("dsb-tab-ledger"), t3 = $("dsb-tab-guide"), tm = $("dsb-tab-mcp");
1926
2956
  if (t1) t1.classList.toggle("dsb-tab-on", S.view === "skills");
1927
2957
  if (t2) t2.classList.toggle("dsb-tab-on", isLedger);
1928
2958
  if (t3) t3.classList.toggle("dsb-tab-on", isGuide);
2959
+ if (tm) tm.classList.toggle("dsb-tab-on", isMcp);
1929
2960
  paintHeader();
1930
2961
  if (isLedger) paintLedger();
2962
+ else if (isMcp) paintMcp();
1931
2963
  else if (!isGuide) { paintGroups(); paintSortbar(); paintChips(); paintSubs(); paintCards(); }
1932
2964
  paintDetail();
1933
2965
  }
@@ -1953,14 +2985,19 @@ window.__ModuleLoader__.load({
1953
2985
  var bar = '<div class="dsb-gbar">';
1954
2986
  bar += '<span class="dsb-gtitle" title="' + esc(T("groupsHint")) + '">' + esc(T("groupsTitle")) + "</span>";
1955
2987
  bar += '<button class="dsb-gadd' + (S.group === "all" ? " dsb-gadd-on" : "") + '" data-gsel="all">' +
1956
- esc(T("groupAll")) + ' <span class="dsb-gn">' + total + "</span></button>";
2988
+ esc(T("groupAll")) + ' <span class="dsb-gn">' + total + " 项</span></button>";
1957
2989
  bar += '<button class="dsb-gadd' + (S.group === "ungrouped" ? " dsb-gadd-on" : "") + '" data-gsel="ungrouped">' +
1958
- esc(T("groupUngrouped")) + ' <span class="dsb-gn">' + ungrouped + "</span></button>";
1959
- bar += '<span class="dsb-gsp"></span>';
2990
+ esc(T("groupUngrouped")) + ' <span class="dsb-gn">' + ungrouped + " 项</span></button>";
2991
+ bar += '<span class="dsb-gsep"></span>';
1960
2992
  bar += '<button class="dsb-gadd' + (treeMode ? " dsb-gadd-on" : "") + '" data-gmode="tree" title="' +
1961
2993
  esc(T("groupModeTreeTip")) + '">' + esc(T("groupModeTree")) + "</button>";
1962
2994
  bar += '<button class="dsb-gadd' + (treeMode ? "" : " dsb-gadd-on") + '" data-gmode="flat" title="' +
1963
2995
  esc(T("groupModeFlatTip")) + '">' + esc(T("groupModeFlat")) + "</button>";
2996
+ bar += '<span class="dsb-gsep"></span>';
2997
+ if (treeMode) {
2998
+ bar += '<button class="dsb-gadd' + (!S.gFold ? " dsb-gadd-on" : "") + '" data-gfold="0" title="展开所有分组的子类">全部展开</button>';
2999
+ bar += '<button class="dsb-gadd' + (S.gFold ? " dsb-gadd-on" : "") + '" data-gfold="1" title="收起所有分组的子类">全部收起</button>';
3000
+ }
1964
3001
  bar += '<button class="dsb-gadd dsb-gadd-kw' + (S.groupBusy === "kw-classify" ? " dsb-gbusy" : "") +
1965
3002
  '" data-gadd="kw" title="' + esc(T("groupKwTip")) + '">' +
1966
3003
  esc(S.groupBusy === "kw-classify" ? T("processingText") : T("groupKw")) + "</button>";
@@ -1976,16 +3013,22 @@ window.__ModuleLoader__.load({
1976
3013
  miCls = " dsb-gmodel-idle";
1977
3014
  miTip = T("modelProbeTip");
1978
3015
  } else if (mi.ok) {
1979
- miText = "🤖 " + esc(TF("modelUsing", { model: mi.model })) +
1980
- (mi.source && mi.source !== "config" ? ' <span class="dsb-gn">' + esc(T(modelSourceKey(mi.source))) + "</span>" : "");
3016
+ // v2.0.9 用户要求:只显示「跟随当前对话模型」,具体模型放 title 悬浮可见。
3017
+ miText = "🤖 跟随当前对话模型 · 正常";
1981
3018
  miCls = " dsb-gmodel-ok";
1982
- miTip = (mi.provider || "") + " / " + (mi.model || "") + "(" + (mi.source || "") + ")";
3019
+ miTip = "跟随当前对话模型 · 实际:" + (mi.provider || "") + " / " + (mi.model || "") + "(" + (mi.source || "") + ")";
1983
3020
  } else {
1984
3021
  miText = "⚠ " + esc(T("modelNone"));
1985
3022
  miCls = " dsb-gmodel-bad";
1986
3023
  miTip = mi.error || T("modelNone");
1987
3024
  }
1988
3025
  bar += '<span class="dsb-gsp"></span>';
3026
+ if (S.aiLastMs > 0) {
3027
+ var lastTxt = S.aiLastCached
3028
+ ? "⚡ 上次分组:缓存 0.0s"
3029
+ : ("⏱ 上次分组:" + (S.aiLastMs / 1000).toFixed(1) + "s" + (S.aiLastBatches > 1 ? (" · 并行" + S.aiLastBatches + "批") : " · 串行"));
3030
+ bar += '<span class="dsb-ai-last" title="最近一次分组的耗时(折叠 / 切走 / 重开面板都保留)">' + esc(lastTxt) + "</span>";
3031
+ }
1989
3032
  bar += '<span class="dsb-gmodel' + miCls + '" data-gmodel="1" title="' + esc(miTip) + '">' + miText + "</span>";
1990
3033
  bar += "</div>";
1991
3034
 
@@ -2016,9 +3059,8 @@ window.__ModuleLoader__.load({
2016
3059
  "</div>";
2017
3060
  // 子类行常驻:选中该大类时把「整个大类」作为第一个可选项。
2018
3061
  // 用 ├─ / └─ 连接符做成真正的树枝,每行一个子类,层层展开。
2019
- if (kids.length || on) {
3062
+ if (kids.length && !S.gFold) {
2020
3063
  var rows = [];
2021
- if (on) rows.push({ id: "", label: T("groupWhole"), emoji: "", n: n, tip: T("groupWhole") });
2022
3064
  for (var k = 0; k < kids.length; k++) {
2023
3065
  var sg = kids[k];
2024
3066
  rows.push({
@@ -2039,7 +3081,7 @@ window.__ModuleLoader__.load({
2039
3081
  '<button class="dsb-gsub' + (sOn ? " dsb-gsub-on" : "") +
2040
3082
  '" data-gsub="' + esc(it.id) + '" title="' + esc(it.tip) + '">' +
2041
3083
  (it.emoji ? esc(it.emoji) + " " : "") + esc(it.label) +
2042
- ' <span class="dsb-gn">' + it.n + "</span>" +
3084
+ ' <span class="dsb-gn">' + it.n + " 项</span>" +
2043
3085
  "</button>" +
2044
3086
  "</div>";
2045
3087
  }
@@ -2083,14 +3125,12 @@ window.__ModuleLoader__.load({
2083
3125
  var kids2 = subsOf(cur.id);
2084
3126
  if (kids2.length) {
2085
3127
  body += '<div class="dsb-gsubs"><span class="dsb-gtitle">└ ' + esc(T("groupSubsOf")) + "</span>";
2086
- body += '<button class="dsb-gadd' + (S.gsub ? "" : " dsb-gadd-on") + '" data-gsub="">' +
2087
- esc(T("groupWhole")) + ' <span class="dsb-gn">' + domainCount(cur) + "</span></button>";
2088
3128
  for (var q = 0; q < kids2.length; q++) {
2089
3129
  var sg2 = kids2[q];
2090
3130
  var sn2 = (sg2.members || []).length;
2091
3131
  body += '<button class="dsb-gadd' + (S.gsub === sg2.id ? " dsb-gadd-on" : "") +
2092
3132
  '" data-gsub="' + esc(sg2.id) + '" title="' + esc(sg2.name + " · " + sn2 + " 项") + '">' +
2093
- esc((sg2.emoji || "📁") + " " + sg2.name) + ' <span class="dsb-gn">' + sn2 + "</span></button>";
3133
+ esc((sg2.emoji || "📁") + " " + sg2.name) + ' <span class="dsb-gn">' + sn2 + " 项</span></button>";
2094
3134
  }
2095
3135
  body += "</div>";
2096
3136
  }
@@ -2217,25 +3257,22 @@ window.__ModuleLoader__.load({
2217
3257
  // v1.7.2 全局总开关(最重要):标签"全部技能",状态=所有技能聚合
2218
3258
  // (全启用→右绿 / 全停用→左灰 / 混合→居中半开+title"部分停用");
2219
3259
  // 点击任意态弹面板内确认菜单 openBatchMenu
2220
- var sk = (S.data && S.data.skills) || [];
2221
- var ga = aggCount(sk, null);
3260
+ // v2.0.7 聚合开关作用于「当前筛选/分组显示的列表」(filtered),并显示数量
3261
+ var fl = filtered();
3262
+ var ga = aggCount(fl, null);
2222
3263
  var gs = aggState(ga);
2223
- var gTitle = gs === "half" ? TF("aggMixedTitle", { x: ga.off, y: ga.total })
2224
- : (gs === "on" ? T("aggAllOnTitle") : T("aggAllOffTitle"));
2225
- var html = btns + aggSwitchHtml(gs, 'data-bagg="global"', gTitle, T("aggAllLabel"), false);
2226
- // v1.7.2 分类级滑块:替换 v1.7.0 的"全启用/全停用"两个按钮——小一号胶囊
2227
- // 滑块 + 旁边小字"停用 x/y"(该分类已停用数/总数),聚合逻辑同全局
2228
- if (S.cat !== "all") {
2229
- var ca = aggCount(sk, S.cat);
2230
- var cs = aggState(ca);
2231
- var cTitle = catLabel(S.cat) + " · " + (cs === "half" ? TF("aggMixedTitle", { x: ca.off, y: ca.total })
2232
- : (cs === "on" ? T("aggAllOnTitle") : T("aggAllOffTitle")));
2233
- html += '<span class="dsb-sw-sep"></span>' +
2234
- aggSwitchHtml(cs, 'data-bagg="' + esc(S.cat) + '"', cTitle, "", true) +
2235
- '<span class="dsb-batch-status">' + esc(TF("aggCatStat", { x: ca.off, y: ca.total })) + '</span>';
2236
- }
3264
+ var gTitle = (gs === "half" ? TF("aggMixedTitle", { x: ga.off, y: ga.total })
3265
+ : (gs === "on" ? T("aggAllOnTitle") : T("aggAllOffTitle"))) + "(当前分组 " + ga.total + " 项)";
3266
+ var hasFilter = (S.cat !== "all") || (S.group !== "all") || !!S.sub || !!S.q.trim();
3267
+ var aggLabel = hasFilter ? ("当前 " + ga.total + " 项") : (T("aggAllLabel") + " " + ga.total + " 项");
3268
+ var html = btns + aggSwitchHtml(gs, 'data-bagg="filtered"', gTitle, aggLabel, false);
2237
3269
  // v1.7.1 保留:批量进行中显示"处理中…"(滑块同时 aria-disabled 防重入)
2238
3270
  if (S.toggling) html += '<span class="dsb-batch-status">' + esc(T("processingText")) + '</span>';
3271
+ // v2.0.6 每行列数(0=自适应)
3272
+ html += '<span class="dsb-sw-sep"></span><span class="dsb-batch-status">每行</span>';
3273
+ [0, 2, 3, 4, 5, 6].forEach(function (n) {
3274
+ html += '<button class="dsb-sortbtn' + (S.cardCols === n ? ' dsb-sortbtn-on' : '') + '" data-cardcols="' + n + '" title="' + (n ? ("每行 " + n + " 个") : "自适应") + '">' + (n || '自') + '</button>';
3275
+ });
2239
3276
  box.innerHTML = html;
2240
3277
  }
2241
3278
 
@@ -2388,16 +3425,20 @@ window.__ModuleLoader__.load({
2388
3425
  // ③paintSortbar 重算全局+分类聚合滑块与"停用 x/y"小字;④paintHeader 刷副标题。
2389
3426
  function batchToggle(scope, disable) {
2390
3427
  if (S.toggling) return;
2391
- var isGlobal = scope === "global";
2392
3428
  var sk = (S.data && S.data.skills) || [];
2393
3429
  var dirs = [];
2394
- for (var i = 0; i < sk.length; i++) {
2395
- if (isGlobal || sk[i].cat === scope) dirs.push(sk[i].dir);
3430
+ if (scope === "filtered") {
3431
+ dirs = filtered().map(function (s) { return s.dir; });
3432
+ } else {
3433
+ var isGlobal = scope === "global";
3434
+ for (var i = 0; i < sk.length; i++) {
3435
+ if (isGlobal || sk[i].cat === scope) dirs.push(sk[i].dir);
3436
+ }
2396
3437
  }
2397
3438
  if (!dirs.length) return;
2398
3439
  S.toggling = true;
2399
3440
  paintSortbar(); // 聚合滑块进入"处理中…"半透明态(aria-disabled 防重入)
2400
- var body = isGlobal ? { dirs: dirs, disable: disable } : { category: scope, disable: disable };
3441
+ var body = { dirs: dirs, disable: disable };
2401
3442
  fetch(apiBase() + "/toggle-batch", {
2402
3443
  method: "POST",
2403
3444
  headers: { "content-type": "application/json" },
@@ -2426,7 +3467,7 @@ window.__ModuleLoader__.load({
2426
3467
  } else {
2427
3468
  showToast("ok", TF(disable ? "toastBatchDisabled" : "toastBatchEnabled", { n: okN }));
2428
3469
  }
2429
- ctxLogger("batch " + (isGlobal ? "global" : scope) + ": ok=" + okN + " fail=" + fails);
3470
+ ctxLogger("batch " + (scope === "global" ? "global" : scope) + ": ok=" + okN + " fail=" + fails);
2430
3471
  } else {
2431
3472
  showToast("err", TF("toastToggleFail", { err: (d && d.error) || "unknown" }));
2432
3473
  try { console.warn("[dsh-skill-browser] batch failed: " + ((d && d.error) || "unknown")); } catch (e) {}
@@ -2448,14 +3489,15 @@ window.__ModuleLoader__.load({
2448
3489
  if (S.toggling) return;
2449
3490
  var menu = $("dsb-bmenu"), mask = $("dsb-bmenu-mask"), panel = $(PANEL_ID);
2450
3491
  if (!menu || !mask || !panel || !anchor) return;
3492
+ var isFiltered = scope === "filtered";
2451
3493
  var isGlobal = scope === "global";
2452
- var a = aggCount((S.data && S.data.skills) || [], isGlobal ? null : scope);
3494
+ var a = isFiltered ? aggCount(filtered(), null) : aggCount((S.data && S.data.skills) || [], isGlobal ? null : scope);
2453
3495
  var n = a.total;
2454
- var scopeName = isGlobal ? T("aggAllLabel") : catLabel(scope);
2455
- var enSub = isGlobal ? "" : T("batchConfirmEnable");
2456
- var disSub = isGlobal ? TF("aggDangerText", { n: n }) : T("batchConfirmDisable");
3496
+ var scopeName = isFiltered ? "当前分组" : (isGlobal ? T("aggAllLabel") : catLabel(scope));
3497
+ var enSub = (isFiltered || isGlobal) ? "" : T("batchConfirmEnable");
3498
+ var disSub = (isFiltered || isGlobal) ? TF("aggDangerText", { n: n }) : T("batchConfirmDisable");
2457
3499
  menu.innerHTML =
2458
- '<div class="dsb-bmenu-title">' + esc(isGlobal ? T("aggMenuTitleGlobal") : T("aggMenuTitleCat")) + '<br/>' +
3500
+ '<div class="dsb-bmenu-title">' + esc((isFiltered || isGlobal) ? T("aggMenuTitleGlobal") : T("aggMenuTitleCat")) + '<br/>' +
2459
3501
  esc(TF("aggMenuStat", { scope: scopeName, n: n, x: a.off })) + '</div>' +
2460
3502
  '<button class="dsb-bmenu-item" data-bmenu-act="enable"><span>' + esc(TF("aggEnableN", { n: n })) + '</span>' +
2461
3503
  (enSub ? '<span class="dsb-bmenu-sub">' + esc(enSub) + '</span>' : "") + '</button>' +
@@ -2524,6 +3566,7 @@ window.__ModuleLoader__.load({
2524
3566
  }
2525
3567
  function paintCards() {
2526
3568
  var box = $("dsb-cards"); if (!box) return;
3569
+ box.style.gridTemplateColumns = S.cardCols ? ("repeat(" + S.cardCols + ",minmax(0,1fr))") : "";
2527
3570
  if (S.error && !S.data) {
2528
3571
  box.innerHTML = '<div class="dsb-empty">' + esc(S.error) + '<br/><br/><button class="dsb-btn" id="dsb-retry">重试</button></div>';
2529
3572
  var r = $("dsb-retry");
@@ -2567,7 +3610,7 @@ window.__ModuleLoader__.load({
2567
3610
  '<div class="dsb-card-foot">' +
2568
3611
  gTag +
2569
3612
  '<button class="dsb-movebtn" data-move="' + esc(s.dir) + '" title="' + esc(T("groupAssignTip")) + '">' +
2570
- "📁 " + esc(T("groupMove")) + "</button>" +
3613
+ "🔀 " + esc(T("groupMove")) + "</button>" +
2571
3614
  "</div>" +
2572
3615
  '<div style="margin-top:6px">' +
2573
3616
  '<label class="dsb-switch' + (off ? "" : " dsb-sw-on") + '" data-tgl="' + esc(s.dir) + '" data-to="' + (off ? "0" : "1") +
@@ -2739,43 +3782,16 @@ window.__ModuleLoader__.load({
2739
3782
  }
2740
3783
  rank.innerHTML = rh;
2741
3784
 
2742
- // ---- 5) 原始流水(人工台账 + 自动登记,折叠)----
3785
+ // ---- 5) 原始流水(只保留自动登记;人工台账区块按用户要求移除)----
2743
3786
  var l = S.ledger || {};
2744
- var rows = (l.entries || []).filter(function (e) { return e.fails > 0; });
2745
3787
  var auto = l.autoRecent || [];
2746
3788
  var flowOpen = !!S.lstatShowFlow;
2747
- var fh = '<div class="dsb-lsec"><span>🗂 ' + esc(T("lstatManual")) + "</span>" +
2748
- '<span class="sub">技能生效-失效台账.md(人工维护 + 历史登记)</span>' +
3789
+ var fh = '<div class="dsb-lsec"><span>🗂 ' + esc(T("lstatAuto")) + "</span>" +
3790
+ '<span class="sub">' + esc(T("lstatAutoTip")) + " · " + auto.length + " 条</span>" +
2749
3791
  '<button class="dsb-ldetail-toggle" data-lflow="1" style="margin-left:auto">' +
2750
3792
  esc(flowOpen ? T("lstatExpand") : T("lstatCollapse")) + "</button></div>";
2751
3793
 
2752
3794
  if (flowOpen) {
2753
- fh += '<div class="dsb-ltable">' +
2754
- '<div class="dsb-lrow dsb-lhead"><span class="dsb-lc-rank">#</span><span class="dsb-lc-skill">技能名</span>' +
2755
- '<span class="dsb-lc-fail">失败</span><span class="dsb-lc-rate">等级</span>' +
2756
- '<span class="dsb-lc-date">最近失败</span><span class="dsb-lc-status">强化状态</span></div>';
2757
- if (!rows.length) {
2758
- fh += '<div class="dsb-lrow"><span class="dsb-lc-skill" style="opacity:.7;line-height:1.8">' +
2759
- "暂无人工登记。<br/>💡 技能触发失败时,在对话里说「<b>触发失败,登记</b>」或「<b>登记失败:技能名 原因</b>」,AI 会自动写入台账。</span></div>";
2760
- }
2761
- for (var i = 0; i < rows.length; i++) {
2762
- var e = rows[i];
2763
- var lv = ledgerLevel(e.fails);
2764
- var status = e.status || "";
2765
- var statusCls = /待强化|⚠/.test(status) ? "dsb-lst-warn" : /已强化/.test(status) ? "dsb-lst-done" : "dsb-lst-ok";
2766
- fh += '<div class="dsb-lrow" data-lskill="' + esc(e.skill) + '" title="点击查看失败明细">' +
2767
- '<span class="dsb-lc-rank">' + esc(e.rank || "—") + "</span>" +
2768
- '<span class="dsb-lc-skill">' + esc(e.skill) + "</span>" +
2769
- '<span class="dsb-lc-fail">' + e.fails + "</span>" +
2770
- '<span class="dsb-lc-rate"><span class="' + lv.cls + '">' + lv.label + "</span></span>" +
2771
- '<span class="dsb-lc-date">' + esc(e.lastDate || "—") + "</span>" +
2772
- '<span class="dsb-lc-status ' + statusCls + '">' + esc(status) + "</span>" +
2773
- "</div>";
2774
- }
2775
- fh += "</div>";
2776
-
2777
- fh += '<div class="dsb-lsec"><span>🤖 ' + esc(T("lstatAuto")) + '</span><span class="sub">' +
2778
- esc(T("lstatAutoTip")) + " · " + auto.length + " 条</span></div>";
2779
3795
  if (!auto.length) {
2780
3796
  fh += '<div class="dsb-empty" style="padding:12px 0">暂无自动记录 — 插件会记录每次技能工具调用结果(成功✅ / 失败❌)</div>';
2781
3797
  } else {
@@ -2829,7 +3845,7 @@ window.__ModuleLoader__.load({
2829
3845
  var ballCb = $("dsb-set-ball"), fontCb = $("dsb-set-font");
2830
3846
  if (ballCb) ballCb.checked = readStore(BALL_VIS_KEY, true);
2831
3847
  if (fontCb) fontCb.checked = readStore(FONT_BALL_VIS_KEY, true);
2832
- try { fillSkinSelect($("dsb-set-skin")); } catch (eSkin) {}
3848
+ try { } catch (eSkin) {}
2833
3849
  }
2834
3850
 
2835
3851
  // ---- 球体样式 + 字体球统一样式 + 显隐规则(纯 CSS,永久生效)----
@@ -3424,7 +4440,6 @@ window.__ModuleLoader__.load({
3424
4440
  document.head.appendChild(el); // (re)append last so overrides win the cascade
3425
4441
  ensureSkinMenuCss();
3426
4442
  refreshSkinMenu();
3427
- fillSkinSelect(document.getElementById("dsb-set-skin"));
3428
4443
  fillSkinSelect(document.getElementById("dsh-set-skin-global"));
3429
4444
  }
3430
4445
  function skinOptionsHtml() {
@@ -3450,6 +4465,7 @@ window.__ModuleLoader__.load({
3450
4465
  }
3451
4466
  // ---- todo-style grouped skin menu (panel 🎨) ----
3452
4467
  var DSB_ICONS = {
4468
+ plug: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 1.9v3.1M10 1.9v3.1"/><path d="M4.4 5h7.2v2.3a3.6 3.6 0 0 1-7.2 0z"/><path d="M8 10.9v3.2"/></svg>',
3453
4469
  palette: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 14.2A6.2 6.2 0 1 1 14.2 8c0 1.1-.9 1.8-2 1.8h-1.4c-.9 0-1.6.7-1.6 1.6 0 .4.2.8.4 1.1.3.4.4.7.4 1 0 .4-.4.7-1 .7z"/><circle cx="5.6" cy="6.2" r=".85"/><circle cx="8" cy="4.4" r=".85"/><circle cx="10.4" cy="6.2" r=".85"/></svg>',
3454
4470
  gear: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="8" cy="8" r="4.6"/><circle cx="8" cy="8" r="1.9"/><path d="M8 3.4v1.4M8 11.2v1.4M3.4 8h1.4M11.2 8h1.4M4.6 4.6l1 1M10.4 10.4l1 1M11.4 4.6l-1 1M5.6 10.4l-1 1"/></svg>',
3455
4471
  bolt: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" aria-hidden="true"><path d="M8.8 1.8 3.8 9h3.4l-.9 5.2L11.9 7H8.5z"/></svg>',
@@ -3545,12 +4561,6 @@ window.__ModuleLoader__.load({
3545
4561
  document.addEventListener("change", function (ev) {
3546
4562
  var t = ev.target;
3547
4563
  if (!t || !t.id) return;
3548
- if (t.id === "dsb-set-skin") {
3549
- if (t.value === "classic") { setSkinClassic(true); return; }
3550
- setSkinClassic(false);
3551
- SKIN.setSkin(t.value);
3552
- return;
3553
- }
3554
4564
  if (t.id === "dsh-set-skin-global") {
3555
4565
  // 全局三球统一换肤:清掉本球与字体球的「原版」标记
3556
4566
  try { localStorage.removeItem(SKIN_CLASSIC_KEY); } catch (e) {}
@@ -3595,7 +4605,7 @@ window.__ModuleLoader__.load({
3595
4605
  React.createElement("span", null, label + (on ? "" : "(未安装)")));
3596
4606
  }
3597
4607
  rows.push(React.createElement("div", { key: "head", style: { fontWeight: "600", fontSize: "13px", marginBottom: "2px" } }, "悬浮球"));
3598
- rows.push(mkToggle("dsb-set-ball", "技能浏览器", function (e) {
4608
+ rows.push(mkToggle("dsb-set-ball", "技能与 MCP 浏览器", function (e) {
3599
4609
  writeStore(BALL_VIS_KEY, e.target.checked);
3600
4610
  applyBallVisibility();
3601
4611
  if (!e.target.checked && S.open) closePanel();