dsh-skill-browser 1.8.6 → 1.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -169,7 +169,35 @@ window.__ModuleLoader__.load({
169
169
  sortMode: "name-asc", // v1.7.1 四态:"name-asc" | "name-desc" | "mtime-desc" | "mtime-asc"
170
170
  toggling: false, // v1.7.0: toggle 请求进行中(防重复点击)
171
171
  toastTimer: null, // v1.7.1: toast 自动消失定时器句柄
172
- batchMenuScope: null // v1.7.2: 批量确认菜单当前作用域("global" 或分类 id)
172
+ batchMenuScope: null, // v1.7.2: 批量确认菜单当前作用域("global" 或分类 id)
173
+ hotBusy: false, // v1.9.4: 热重载进行中,防重复点击
174
+ // ── v1.9.0 视图分组(纯视图层,不移动任何技能文件)──
175
+ groups: [], // [{id,name,emoji,color,cover,ai,createdAt,members:[dir]}]
176
+ gassign: {}, // { skillDir: groupId }
177
+ group: "all", // "all" | "ungrouped" | <groupId>
178
+ groupsLoaded: false,
179
+ groupBusy: "", // "" | "ai-classify" | "ai-cover:<gid>"
180
+ moveMenuDir: null, // 当前打开"移动到分组"菜单的技能目录
181
+ // ── v1.9.3 两级分组(大类 → 子类)──
182
+ gplan: null,
183
+ gplanMode: "", // AI 产出的待确认方案 {domains:[{name,emoji,subs:[{name,emoji,skills:[]}]}]}
184
+ gplanBusy: false, // 方案确认弹层是否已打开
185
+ gsub: "", // 当前选中大类下再选中的子类 id(""=看整个大类)
186
+ gmode: "tree", // v1.9.8 分组栏显示模式:"tree" 树状(大类一行+常驻子类)| "flat" 平铺(原样)
187
+ modelInfo: null, // v1.9.9 AI 模型探针结果 {ok,provider,model,source,error};null=还没探
188
+ newParent: "", // "新建分组"时选定的上级大类 id(""=建大类)
189
+ // ── v1.9.1 调用台账看板(起止日期 + 粒度 + 排行)──
190
+ lstat: null, // GET /ledger/stats 聚合结果
191
+ lfrom: "", // 起始日期 YYYY-MM-DD(空 = 不限)
192
+ lto: "", // 结束日期 YYYY-MM-DD(空 = 不限)
193
+ lgran: "day", // day | week | month | year
194
+ lsort: "calls", // calls | fails | rate | last | name
195
+ lstatLoading: false,
196
+ lstatErr: "",
197
+ lstatShowFlow: false, // 原始流水是否展开
198
+ // ── v1.9.6 渲染分片(避免一次性渲染上千节点卡顿)──
199
+ cardLimit: 60, // 技能卡片已渲染条数(滚动到底自动追加)
200
+ lflowLimit: 120 // 原始流水已渲染条数
173
201
  };
174
202
 
175
203
  // v1.7.1 排序四态循环的唯一权威定义(点击按钮沿数组循环)。
@@ -237,7 +265,125 @@ window.__ModuleLoader__.load({
237
265
  aggMenuTitleCat: "分类批量启停 — 仅作用于当前分类",
238
266
  aggMenuStat: "{scope} · 共 {n} 项 · 已停用 {x} 项",
239
267
  aggCatStat: "停用 {x}/{y}",
240
- aggCancelText: "取消"
268
+ aggCancelText: "取消",
269
+ // ── v1.9.0 新增:版本号 / 视图分组 / AI 分组与封面 ──
270
+ versionLabel: "版本",
271
+ versionNone: "暂未编版本号",
272
+ versionTip: "该技能 SKILL.md frontmatter 与 package.json 都没有声明 version",
273
+ groupsTitle: "我的分组",
274
+ groupsHint: "分组只是视图归档,不会移动任何技能文件",
275
+ groupNew: "+ 新建分组",
276
+ groupAi: "🤖 AI 自动分组",
277
+ groupAll: "全部",
278
+ groupUngrouped: "未分组",
279
+ groupEmpty: "还没有分组,点「+ 新建分组」或「🤖 AI 自动分组」",
280
+ groupNamePrompt: "分组名称",
281
+ groupNamePlaceholder: "例如:Fluent 仿真、逆向工具、文档处理",
282
+ groupDeleteConfirm: "删除分组「{name}」?技能文件不会受影响,只是取消归档。",
283
+ groupCreated: "已创建分组:{name}",
284
+ groupRenamed: "已重命名:{name}",
285
+ groupDeleted: "已删除分组:{name}",
286
+ groupAssignTip: "放进这个分组(只改视图,不动文件)",
287
+ groupMove: "移动",
288
+ groupMoveTitle: "移动到分组",
289
+ groupMoveNone: "移出分组",
290
+ groupAssigned: "已把 {name} 移入「{group}」",
291
+ groupUnassigned: "已把 {name} 移出分组",
292
+ groupAiRunning: "AI 正在分析技能库并分组,请稍候…",
293
+ groupAiDone: "AI 已建 {n} 个分组",
294
+ groupAiFail: "AI 分组失败:{err}",
295
+ groupCover: "AI 生成封面",
296
+ groupCoverRunning: "正在生成封面…",
297
+ groupCoverDone: "封面已生成",
298
+ groupCoverFail: "封面生成失败:{err}",
299
+ groupRename: "重命名",
300
+ groupDelete: "删除分组",
301
+ groupManageTitle: "分组管理",
302
+ aiNoLlm: "AI 不可用:宿主未提供 llm 服务",
303
+ hotReload: "热重载",
304
+ hotReloadTip: "热重载:重新加载插件代码,不重启桌面端",
305
+ hotReloadRunning: "正在热重载…",
306
+ hotReloadDone: "热重载完成(v{v} · 第 {n} 代)",
307
+ hotReloadFail: "热重载失败:{e}",
308
+ hotReloadRefresh: "代码已换新,正在刷新界面…",
309
+ cardMore: "加载更多(还有 {n} 条)",
310
+ detailHint: "点开才动态加载全文,列表只做静态摘要",
311
+ // ── v1.9.1 调用台账看板 ──
312
+ lstatTitle: "调用台账",
313
+ lstatQuickToday: "今天",
314
+ lstatQuick7: "近7天",
315
+ lstatQuick30: "近30天",
316
+ lstatQuickMonth: "本月",
317
+ lstatQuickPrevMonth: "上月",
318
+ lstatQuickYear: "今年",
319
+ lstatQuickAll: "全部",
320
+ lstatFrom: "起",
321
+ lstatTo: "止",
322
+ lstatGran: "粒度",
323
+ lstatGranDay: "日",
324
+ lstatGranWeek: "周",
325
+ lstatGranMonth: "月",
326
+ lstatGranYear: "年",
327
+ lstatCalls: "调用",
328
+ lstatSkills: "活跃技能",
329
+ lstatDays: "活跃天数",
330
+ lstatRate: "成功率",
331
+ lstatAvg: "日均",
332
+ lstatTrend: "调用趋势",
333
+ lstatRank: "技能调用排行",
334
+ lstatColRank: "#",
335
+ lstatColSkill: "技能",
336
+ lstatColVer: "版本",
337
+ lstatColCalls: "调用",
338
+ lstatColOk: "成功",
339
+ lstatColFail: "失败",
340
+ lstatColRate: "成功率",
341
+ lstatColLast: "最后调用",
342
+ lstatSortTip: "点表头切换排序",
343
+ lstatEmptyRange: "该区间没有调用记录",
344
+ lstatEmptyAll: "还没有任何调用记录 —— 用一次技能后这里就会出现数据",
345
+ lstatLoading: "台账统计加载中…",
346
+ lstatFail: "台账统计加载失败:{err}",
347
+ lstatDataRange: "数据区间 {from} ~ {to}",
348
+ lstatNoData: "无数据",
349
+ lstatManual: "人工台账",
350
+ lstatAuto: "自动登记流水",
351
+ lstatAutoTip: "插件监听 skill 工具调用结果自动记录 · 最近 30 条",
352
+ lstatCollapse: "展开原始流水",
353
+ lstatExpand: "收起原始流水",
354
+ lstatTopN: "仅显示前 {n}",
355
+ // ── v1.9.3:两级分组(总-分)与 AI 方案确认 ──
356
+ groupSubsOf: "子类",
357
+ groupWhole: "整个大类",
358
+ groupNewSub: "在此大类下新建子类",
359
+ groupCurTag: "当前",
360
+ groupKidsN: "{n} 子类",
361
+ groupKw: "⚡ 关键词分组",
362
+ groupKwTip: "本地关键词规则直接分组:零 token、毫秒级、结果固定",
363
+ groupAiTip: "调用模型重新归纳分组:慢,但更懂语义",
364
+ modelUsing: "AI 分组将使用:{model}",
365
+ modelSourceDefault: "DSH 默认模型",
366
+ modelSourceAgent: "当前对话模型",
367
+ modelSourceConfig: "插件配置",
368
+ modelSourceCatalog: "宿主可用模型",
369
+ modelNone: "AI 不可用:没找到任何可用模型,请到 DSH 设置 → 模型 选一个默认模型",
370
+ modelProbeTip: "点一下检测 AI 分组会用哪个模型(不会真的调模型)",
371
+ groupPlanTitleKw: "⚡ 关键词分组方案(确认后才会写入)",
372
+ groupPlanTitle: "🤖 AI 分组方案(确认后才会写入)",
373
+ groupPlanStat: "共 {d} 个大类 · {s} 个子类 · 覆盖 {n} 个技能",
374
+ groupPlanReplace: "替换:清掉旧的 AI 分组,按此方案重建",
375
+ groupPlanMerge: "追加:保留现有分组,只补未归类的技能",
376
+ groupApplyRunning: "正在写入分组…",
377
+ groupApplyDone: "已应用:{d} 个大类 / {s} 个子类",
378
+ groupApplyFail: "应用分组失败:{err}",
379
+ groupSubCreated: "子类已创建:{name}",
380
+ groupSubPrompt: "在「{p}」下新建子类,输入名称:",
381
+ groupSubFail: "新建子类失败",
382
+ // ── v1.9.8:分组栏双显示模式(树状 / 平铺)──
383
+ groupModeTree: "🌳 树状",
384
+ groupModeFlat: "▦ 平铺",
385
+ groupModeTreeTip: "树状:每个大类一行,子类常驻缩进挂在下层,层层展开一眼看全",
386
+ groupModeFlatTip: "平铺:大类排成卡片,点开才显示子类(v1.9.3 原样)"
241
387
  };
242
388
  function T(key) {
243
389
  var ui = (S.data && S.data.ui) || null;
@@ -299,6 +445,15 @@ window.__ModuleLoader__.load({
299
445
  var s = sk[i];
300
446
  if (S.cat !== "all" && s.cat !== S.cat) continue;
301
447
  if (S.sub && s.sub !== S.sub) continue;
448
+ // v1.9.0 分组过滤(纯视图层):只决定"这张卡现在显不显示",
449
+ // 与技能目录在磁盘上的位置毫无关系。
450
+ // v1.9.3 两级:选中大类时显示该大类 + 它全部子类的技能;再选子类则只显示子类。
451
+ if (S.group !== "all") {
452
+ var gid = S.gassign[s.dir] || "";
453
+ if (S.group === "ungrouped") { if (gid) continue; }
454
+ else if (S.gsub) { if (gid !== S.gsub) continue; }
455
+ else if (!inDomain(S.group, gid)) continue;
456
+ }
302
457
  if (q) {
303
458
  var hay = (s.dir + " " + s.name + " " + (s.zh || "") + " " + (s.desc || "")).toLowerCase();
304
459
  if (hay.indexOf(q) < 0) continue;
@@ -327,8 +482,13 @@ window.__ModuleLoader__.load({
327
482
  if (!d || !d.ok) throw new Error((d && d.error) || "bad response");
328
483
  S.data = d;
329
484
  closeBatchMenu(); // v1.7.2: 数据重扫后确认菜单里的 N 已过期,直接关闭
485
+ closeMoveMenu();
330
486
  if (S.cat !== "all" && !catMeta(S.cat)) { S.cat = "all"; S.sub = ""; }
331
487
  S.loading = false; paint();
488
+ // v1.9.0 分组与技能清单一起首载(并行失败不影响技能列表显示)
489
+ if (!S.groupsLoaded) loadGroups(function () { paint(); });
490
+ // v1.9.9 首载时顺便探一次模型路由(不调模型,只读配置,失败静默)
491
+ if (!S.modelInfo) loadModelInfo(false);
332
492
  })
333
493
  .catch(function (e) {
334
494
  S.loading = false;
@@ -337,6 +497,383 @@ window.__ModuleLoader__.load({
337
497
  });
338
498
  }
339
499
 
500
+ // ---- v1.9.0 视图分组:加载 + 操作(全部走 host /groups,只动 groups.json)----
501
+ function loadGroups(cb) {
502
+ fetch(apiBase() + "/groups")
503
+ .then(function (r) { return r.json(); })
504
+ .then(function (d) {
505
+ if (d && d.ok) {
506
+ S.groups = d.groups || [];
507
+ S.gassign = d.assign || {};
508
+ S.groupsLoaded = true;
509
+ // 分组被删/换根后,当前选中的分组可能已不存在 → 回落到"全部"
510
+ if (S.group !== "all" && S.group !== "ungrouped") {
511
+ var still = false;
512
+ for (var i = 0; i < S.groups.length; i++) if (S.groups[i].id === S.group) { still = true; break; }
513
+ if (!still) { S.group = "all"; S.gsub = ""; }
514
+ }
515
+ // 子类被删/被合并后清掉二级选中,否则列表会空掉
516
+ if (S.gsub) {
517
+ var subStill = false;
518
+ for (var j2 = 0; j2 < S.groups.length; j2++) if (S.groups[j2].id === S.gsub) { subStill = true; break; }
519
+ if (!subStill) S.gsub = "";
520
+ }
521
+ }
522
+ if (cb) cb(d);
523
+ })
524
+ .catch(function (e) {
525
+ try { console.warn("[dsh-skill-browser] groups load failed: " + (e && e.message)); } catch (e2) {}
526
+ if (cb) cb(null);
527
+ });
528
+ }
529
+
530
+ function groupById(id) {
531
+ for (var i = 0; i < S.groups.length; i++) if (S.groups[i].id === id) return S.groups[i];
532
+ return null;
533
+ }
534
+ function groupOfDir(dir) { return S.gassign[dir] || ""; }
535
+ // ── v1.9.3 两级分组工具 ──
536
+ // A domain is any group whose parent is empty; a sub-group points at one.
537
+ function isDomain(g) { return !!g && !g.parent; }
538
+ function topGroups() {
539
+ var out = [];
540
+ for (var i = 0; i < S.groups.length; i++) if (isDomain(S.groups[i])) out.push(S.groups[i]);
541
+ return out;
542
+ }
543
+ function subsOf(id) {
544
+ var out = [];
545
+ for (var i = 0; i < S.groups.length; i++) if (S.groups[i].parent === id) out.push(S.groups[i]);
546
+ return out;
547
+ }
548
+ // Which domain owns this group id (the id itself when it is already a domain).
549
+ function domainOf(id) {
550
+ var g = groupById(id);
551
+ if (!g) return "";
552
+ return g.parent || g.id;
553
+ }
554
+ // "Does skill assigned to skillGid belong under the selected domain?"
555
+ // Skills may sit directly on the domain (no finer sub) - that counts too.
556
+ function inDomain(domainId, skillGid) {
557
+ if (!skillGid) return false;
558
+ if (skillGid === domainId) return true;
559
+ var g = groupById(skillGid);
560
+ return !!(g && g.parent === domainId);
561
+ }
562
+ // Total skill count under a domain = its own members + every sub-group's.
563
+ function domainCount(g) {
564
+ if (!g) return 0;
565
+ var n = (g.members || []).length;
566
+ var kids = subsOf(g.id);
567
+ for (var i = 0; i < kids.length; i++) n += (kids[i].members || []).length;
568
+ return n;
569
+ }
570
+ function groupCoverStyle(g) {
571
+ if (!g || !g.cover) return "";
572
+ try {
573
+ // encodeURIComponent leaves ' alone, which would break out of url('...').
574
+ var uri = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(g.cover).replace(/'/g, "%27");
575
+ return ' style="background-image:url(&quot;' + uri + '&quot;)"';
576
+ } catch (e) { return ""; }
577
+ }
578
+
579
+ // v1.9.9 AI 模型探针:把后端解析出的路由来源翻译成文案键。
580
+ function modelSourceKey(src) {
581
+ if (src === "config") return "modelSourceConfig";
582
+ if (src === "agent-session") return "modelSourceAgent";
583
+ if (src === "llm-catalog") return "modelSourceCatalog";
584
+ return "modelSourceDefault";
585
+ }
586
+ // 探针只在用户主动点、或第一次打开面板时跑一次(不调模型,只读配置)。
587
+ function loadModelInfo(force) {
588
+ if (S.modelInfo && !force) return;
589
+ postGroups({ action: "model-info" }, function (d) {
590
+ S.modelInfo = d && typeof d === "object"
591
+ ? { ok: !!d.ok, provider: d.provider || "", model: d.model || "", source: d.source || "", error: d.error || "" }
592
+ : { ok: false, provider: "", model: "", source: "", error: "" };
593
+ paintGroups();
594
+ });
595
+ }
596
+
597
+ function postGroups(body, cb) {
598
+ fetch(apiBase() + "/groups", {
599
+ method: "POST",
600
+ headers: { "content-type": "application/json" },
601
+ body: JSON.stringify(body || {})
602
+ })
603
+ .then(function (r) { return r.json().catch(function () { throw new Error("HTTP " + r.status); }); })
604
+ .then(function (d) { if (cb) cb(d); })
605
+ .catch(function (e) { if (cb) cb({ ok: false, error: String((e && e.message) || e) }); });
606
+ }
607
+
608
+ function createGroupFlow() {
609
+ var name = "";
610
+ try { name = window.prompt(T("groupNamePrompt"), ""); } catch (e) { name = ""; }
611
+ if (name == null) return;
612
+ name = String(name).trim();
613
+ if (!name) return;
614
+ postGroups({ action: "create", name: name }, function (d) {
615
+ if (d && d.ok) {
616
+ showToast("ok", TF("groupCreated", { name: name }));
617
+ loadGroups(function () { S.group = (d.group && d.group.id) || S.group; paint(); });
618
+ } else {
619
+ showToast("err", (d && d.error) || "创建失败");
620
+ }
621
+ });
622
+ }
623
+
624
+ // v1.9.3 在大类下新建子类
625
+ function createSubGroupFlow(parentId) {
626
+ var par = groupById(parentId);
627
+ if (!par) return;
628
+ var name = "";
629
+ try { name = window.prompt(TF("groupSubPrompt", { p: par.name }), ""); } catch (e) { return; }
630
+ if (name == null) return;
631
+ name = String(name).trim();
632
+ if (!name) return;
633
+ postGroups({ action: "create", name: name, parent: parentId }, function (d) {
634
+ if (d && d.ok) {
635
+ showToast("ok", TF("groupSubCreated", { name: name }));
636
+ loadGroups(function () { S.group = parentId; S.gsub = (d.group && d.group.id) || ""; paint(); });
637
+ } else {
638
+ showToast("err", (d && d.error) || T("groupSubFail"));
639
+ }
640
+ });
641
+ }
642
+
643
+ function renameGroupFlow(id) {
644
+ var g = groupById(id);
645
+ if (!g) return;
646
+ var name = "";
647
+ try { name = window.prompt(T("groupNamePrompt"), g.name); } catch (e) { return; }
648
+ if (name == null) return;
649
+ name = String(name).trim();
650
+ if (!name || name === g.name) return;
651
+ postGroups({ action: "rename", id: id, name: name }, function (d) {
652
+ if (d && d.ok) {
653
+ showToast("ok", TF("groupRenamed", { name: name }));
654
+ loadGroups(function () { paint(); });
655
+ } else {
656
+ showToast("err", (d && d.error) || "重命名失败");
657
+ }
658
+ });
659
+ }
660
+
661
+ function deleteGroupFlow(id) {
662
+ var g = groupById(id);
663
+ if (!g) return;
664
+ var ok = false;
665
+ try { ok = window.confirm(TF("groupDeleteConfirm", { name: g.name })); } catch (e) { ok = false; }
666
+ if (!ok) return;
667
+ postGroups({ action: "delete", id: id }, function (d) {
668
+ if (d && d.ok) {
669
+ showToast("ok", TF("groupDeleted", { name: g.name }));
670
+ if (S.group === id) S.group = "all";
671
+ loadGroups(function () { paint(); });
672
+ } else {
673
+ showToast("err", (d && d.error) || "删除失败");
674
+ }
675
+ });
676
+ }
677
+
678
+ // v1.9.7: 关键词快速分组 —— 纯本地正则,零 token、毫秒级、结果确定。
679
+ // 与 AI 分组共用同一个方案弹层与 applyPlan 落盘路径。
680
+ function kwClassifyFlow() {
681
+ if (S.groupBusy) return;
682
+ S.groupBusy = "kw-classify";
683
+ paintGroups();
684
+ postGroups({ action: "kw-classify" }, function (d) {
685
+ S.groupBusy = "";
686
+ if (d && d.ok) {
687
+ S.gplan = d.plan || null;
688
+ S.gplanMode = "keyword";
689
+ paintGroups();
690
+ openPlanMenu();
691
+ } else {
692
+ showToast("err", TF("groupAiFail", { err: String((d && d.error) || "unknown") }));
693
+ paintGroups();
694
+ }
695
+ });
696
+ }
697
+ // v1.9.3: AI 只产出「大类→子类」两级方案,**不落盘**。
698
+ // 用户在弹层里看清楚、选好 replace/merge,才真正调用 ai-apply。
699
+ function aiClassifyFlow() {
700
+ if (S.groupBusy) return;
701
+ S.groupBusy = "ai-classify";
702
+ paintGroups();
703
+ showToast("ok", T("groupAiRunning"));
704
+ postGroups({ action: "ai-classify" }, function (d) {
705
+ S.groupBusy = "";
706
+ if (d && d.ok) {
707
+ S.gplan = d.plan || null;
708
+ S.gplanMode = "ai";
709
+ paintGroups();
710
+ openPlanMenu();
711
+ } else {
712
+ var aiErr = String((d && d.error) || "unknown");
713
+ if (/llm|dsh-llm|服务/i.test(aiErr)) aiErr = T("aiNoLlm");
714
+ showToast("err", TF("groupAiFail", { err: aiErr }));
715
+ paintGroups();
716
+ }
717
+ });
718
+ }
719
+
720
+ // 方案确认弹层:展示「大类 → 子类(N 项)」树,并让用户选替换还是追加。
721
+ function openPlanMenu() {
722
+ var menu = $("dsb-move"), mask = $("dsb-move-mask"), panel = $(PANEL_ID);
723
+ if (!menu || !mask || !panel) return;
724
+ var plan = S.gplan;
725
+ if (!plan || !plan.domains || !plan.domains.length) { showToast("err", T("groupAiFail").replace("{err}", "方案为空")); return; }
726
+ var total = 0, domN = 0, subN = 0;
727
+ var rows = "";
728
+ for (var i = 0; i < plan.domains.length; i++) {
729
+ var dm = plan.domains[i] || {};
730
+ var subs = dm.subs || [];
731
+ var dn = 0;
732
+ for (var k = 0; k < subs.length; k++) dn += (subs[k].skills || []).length;
733
+ total += dn; domN++; subN += subs.length;
734
+ var subTxt = [];
735
+ for (var m = 0; m < subs.length; m++) {
736
+ subTxt.push(esc((subs[m].emoji || "📁") + " " + subs[m].name) + '<span class="dsb-gn">' + (subs[m].skills || []).length + "</span>");
737
+ }
738
+ rows += '<div class="dsb-plan-dom">' +
739
+ '<div class="dsb-plan-dname">' + esc((dm.emoji || "📂") + " " + dm.name) + '<span class="dsb-gn">' + dn + " 项</span></div>" +
740
+ '<div class="dsb-plan-subs">' + subTxt.join("") + "</div>" +
741
+ "</div>";
742
+ }
743
+ var planTitle = S.gplanMode === "keyword" ? T("groupPlanTitleKw") : T("groupPlanTitle");
744
+ var html = '<div class="dsb-bmenu-title">' + esc(planTitle) + "</div>" +
745
+ '<div class="dsb-plan-stat">' + esc(TF("groupPlanStat", { d: domN, s: subN, n: total })) + "</div>" +
746
+ '<div class="dsb-plan-list">' + rows + "</div>" +
747
+ '<button class="dsb-bmenu-item dsb-bmenu-danger" data-plan-act="replace"><span>' + esc(T("groupPlanReplace")) + "</span></button>" +
748
+ '<button class="dsb-bmenu-item" data-plan-act="merge"><span>' + esc(T("groupPlanMerge")) + "</span></button>" +
749
+ '<button class="dsb-bmenu-cancel" data-plan-act="__cancel">' + esc(T("aggCancelText")) + "</button>";
750
+ menu.className = "dsb-bmenu dsb-plan-menu";
751
+ menu.innerHTML = html;
752
+ mask.style.display = "block";
753
+ menu.style.display = "flex";
754
+ S.gplanBusy = true;
755
+ var pr = panel.getBoundingClientRect();
756
+ menu.style.left = "12px";
757
+ menu.style.top = "40px";
758
+ menu.style.maxHeight = Math.max(200, pr.height - 80) + "px";
759
+ }
760
+ function closePlanMenu() {
761
+ S.gplanBusy = false;
762
+ var menu = $("dsb-move"), mask = $("dsb-move-mask");
763
+ if (menu) { menu.style.display = "none"; menu.className = "dsb-bmenu"; menu.style.maxHeight = ""; }
764
+ if (mask) mask.style.display = "none";
765
+ }
766
+ // 用户点了「替换」或「追加」→ 才真正落盘
767
+ function applyPlanFlow(mode) {
768
+ var plan = S.gplan;
769
+ if (!plan) { closePlanMenu(); return; }
770
+ S.groupBusy = "ai-apply";
771
+ paintGroups();
772
+ showToast("ok", T("groupApplyRunning"));
773
+ postGroups({ action: "ai-apply", plan: plan, mode: mode }, function (d) {
774
+ S.groupBusy = "";
775
+ closePlanMenu();
776
+ if (d && d.ok) {
777
+ S.gplan = null;
778
+ S.group = "all"; S.gsub = "";
779
+ showToast("ok", TF("groupApplyDone", { d: d.domains || 0, s: d.subs || 0 }));
780
+ loadGroups(function () { paint(); });
781
+ } else {
782
+ var apErr = String((d && d.error) || "unknown");
783
+ if (/llm|dsh-llm|服务/i.test(apErr)) apErr = T("aiNoLlm");
784
+ showToast("err", TF("groupApplyFail", { err: apErr }));
785
+ paintGroups();
786
+ }
787
+ });
788
+ }
789
+
790
+ function aiCoverFlow(id) {
791
+ if (S.groupBusy) return;
792
+ S.groupBusy = "ai-cover:" + id;
793
+ paintGroups();
794
+ showToast("ok", T("groupCoverRunning"));
795
+ postGroups({ action: "ai-cover", id: id }, function (d) {
796
+ S.groupBusy = "";
797
+ if (d && d.ok) {
798
+ showToast("ok", T("groupCoverDone"));
799
+ loadGroups(function () { paint(); });
800
+ } else {
801
+ var cvErr = String((d && d.error) || "unknown");
802
+ if (/llm|dsh-llm|服务/i.test(cvErr)) cvErr = T("aiNoLlm");
803
+ showToast("err", TF("groupCoverFail", { err: cvErr }));
804
+ paintGroups();
805
+ }
806
+ });
807
+ }
808
+
809
+ // 移动技能到分组(只改 groups.json 的 assign 映射,不碰技能目录)
810
+ function assignFlow(dir, gid) {
811
+ closeMoveMenu();
812
+ postGroups({ action: "assign", dir: dir, gid: gid || "" }, function (d) {
813
+ if (d && d.ok) {
814
+ if (gid) {
815
+ var g = groupById(gid);
816
+ showToast("ok", TF("groupAssigned", { name: dir, group: g ? g.name : gid }));
817
+ } else {
818
+ showToast("ok", TF("groupUnassigned", { name: dir }));
819
+ }
820
+ loadGroups(function () { paint(); });
821
+ } else {
822
+ showToast("err", (d && d.error) || "移动失败");
823
+ }
824
+ });
825
+ }
826
+
827
+ // "移动到分组"浮层:复用 #dsb-bmenu 那套遮罩/锚定逻辑,独立菜单体
828
+ // v1.9.3 两级:先列大类(可整体选),下面缩进列它的子类。
829
+ function openMoveMenu(anchor, dir) {
830
+ var menu = $("dsb-move"), mask = $("dsb-move-mask"), panel = $(PANEL_ID);
831
+ if (!menu || !mask || !panel || !anchor) return;
832
+ var cur = groupOfDir(dir);
833
+ var html = '<div class="dsb-bmenu-title">' + esc(T("groupMoveTitle")) + '<br/>' + esc(dir) + "</div>";
834
+ html += '<button class="dsb-bmenu-item' + (cur ? "" : " dsb-bmenu-danger") + '" data-move-gid=""><span>' +
835
+ esc(T("groupMoveNone")) + "</span></button>";
836
+ var doms = topGroups();
837
+ for (var i = 0; i < doms.length; i++) {
838
+ var g = doms[i];
839
+ html += '<button class="dsb-bmenu-item dsb-move-dom' + (g.id === cur ? " dsb-bmenu-danger" : "") +
840
+ '" data-move-gid="' + esc(g.id) + '"><span>' + esc((g.emoji || "📂") + " " + g.name) +
841
+ (g.id === cur ? ' <span class="dsb-bmenu-sub">' + esc(T("groupCurTag")) + "</span>" : "") + "</span></button>";
842
+ var kids = subsOf(g.id);
843
+ for (var k = 0; k < kids.length; k++) {
844
+ var sg = kids[k];
845
+ html += '<button class="dsb-bmenu-item dsb-move-sub' + (sg.id === cur ? " dsb-bmenu-danger" : "") +
846
+ '" data-move-gid="' + esc(sg.id) + '"><span>└ ' + esc((sg.emoji || "📁") + " " + sg.name) +
847
+ (sg.id === cur ? ' <span class="dsb-bmenu-sub">' + esc(T("groupCurTag")) + "</span>" : "") + "</span></button>";
848
+ }
849
+ }
850
+ if (!doms.length) {
851
+ html += '<div class="dsb-bmenu-sub" style="padding:4px 6px">' + esc(T("groupEmpty")) + "</div>";
852
+ }
853
+ html += '<button class="dsb-bmenu-cancel" data-move-gid="__cancel">' + esc(T("aggCancelText")) + "</button>";
854
+ menu.className = "dsb-bmenu";
855
+ menu.innerHTML = html;
856
+ mask.style.display = "block";
857
+ menu.style.display = "flex";
858
+ S.moveMenuDir = dir;
859
+ var ar = anchor.getBoundingClientRect();
860
+ var pr = panel.getBoundingClientRect();
861
+ var left = ar.left - pr.left;
862
+ var top = ar.bottom - pr.top + 6;
863
+ var mw = menu.offsetWidth, mh = menu.offsetHeight;
864
+ var pw = panel.clientWidth, ph = panel.clientHeight;
865
+ if (left + mw > pw - 8) left = Math.max(8, pw - mw - 8);
866
+ if (top + mh > ph - 8) top = Math.max(8, ar.top - pr.top - mh - 6);
867
+ menu.style.left = left + "px";
868
+ menu.style.top = top + "px";
869
+ }
870
+ function closeMoveMenu() {
871
+ S.moveMenuDir = null;
872
+ var menu = $("dsb-move"), mask = $("dsb-move-mask");
873
+ if (menu) menu.style.display = "none";
874
+ if (mask) mask.style.display = "none";
875
+ }
876
+
340
877
  function loadLedger() {
341
878
  if (S.ledger) return;
342
879
  S.loading = true; S.error = ""; paint();
@@ -356,9 +893,66 @@ window.__ModuleLoader__.load({
356
893
 
357
894
  function loadLedgerRefresh() {
358
895
  S.ledger = null;
896
+ S.lstat = null;
359
897
  loadLedger();
360
898
  }
361
899
 
900
+ // ── v1.9.1 调用台账统计(服务端聚合,前端只画)──
901
+ function loadStats() {
902
+ var qs = [];
903
+ if (S.lfrom) qs.push("from=" + encodeURIComponent(S.lfrom));
904
+ if (S.lto) qs.push("to=" + encodeURIComponent(S.lto));
905
+ qs.push("gran=" + encodeURIComponent(S.lgran || "day"));
906
+ qs.push("sort=" + encodeURIComponent(S.lsort || "calls"));
907
+ qs.push("top=200");
908
+ S.lstatLoading = true;
909
+ paintLedger();
910
+ fetch(apiBase() + "/ledger/stats?" + qs.join("&"))
911
+ .then(function (r) { return r.json(); })
912
+ .then(function (d) {
913
+ S.lstatLoading = false;
914
+ if (!d || !d.ok) throw new Error((d && d.error) || "bad response");
915
+ S.lstat = d;
916
+ paintLedger();
917
+ })
918
+ .catch(function (e) {
919
+ S.lstatLoading = false;
920
+ S.lstatErr = String((e && e.message) || e);
921
+ paintLedger();
922
+ });
923
+ }
924
+
925
+ // 本地日期工具(避免时区漂移:全部用本地 Date 取 Y-M-D)
926
+ function ymd(d) {
927
+ var p = function (n) { return n < 10 ? "0" + n : String(n); };
928
+ return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate());
929
+ }
930
+ function todayYmd() { return ymd(new Date()); }
931
+ function shiftYmd(base, days) {
932
+ var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(base || ""));
933
+ if (!m) return "";
934
+ var d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
935
+ d.setDate(d.getDate() + days);
936
+ return ymd(d);
937
+ }
938
+ // 快捷区间:返回 {from,to,gran}
939
+ function quickRange(key) {
940
+ var now = new Date();
941
+ var y = now.getFullYear(), mo = now.getMonth();
942
+ var first = ymd(new Date(y, mo, 1));
943
+ if (key === "today") return { from: todayYmd(), to: todayYmd(), gran: "day" };
944
+ if (key === "7") return { from: shiftYmd(todayYmd(), -6), to: todayYmd(), gran: "day" };
945
+ if (key === "30") return { from: shiftYmd(todayYmd(), -29), to: todayYmd(), gran: "day" };
946
+ if (key === "month") return { from: first, to: todayYmd(), gran: "day" };
947
+ if (key === "prev") {
948
+ var pf = ymd(new Date(y, mo - 1, 1));
949
+ var pt = ymd(new Date(y, mo, 0));
950
+ return { from: pf, to: pt, gran: "day" };
951
+ }
952
+ if (key === "year") return { from: y + "-01-01", to: todayYmd(), gran: "month" };
953
+ return { from: "", to: "", gran: "day" };
954
+ }
955
+
362
956
  // ---- 运行时配置(技能库位置 / 自动台账开关)----
363
957
  // 目录选择:POST /pick-dir,由宿主端 spawn powershell -STA 弹 Windows
364
958
  // FolderBrowserDialog(系统原生对话框)。不依赖 Electron renderer 的
@@ -451,7 +1045,11 @@ window.__ModuleLoader__.load({
451
1045
  if (d && d.ok) {
452
1046
  if (d.detail) parts.push("【人工台账失败明细】\n" + d.detail);
453
1047
  if (d.autoEntries && d.autoEntries.length) {
454
- var lines = ["【自动登记记录】共 " + d.autoEntries.length + " 条(最新在前)"];
1048
+ var tot = (d.autoTotal == null ? d.autoEntries.length : d.autoTotal);
1049
+ var shownN = d.autoEntries.length;
1050
+ var head = "【自动登记记录】共 " + tot + " 条";
1051
+ if (tot > shownN) head += "(仅显示最近 " + shownN + " 条)";
1052
+ var lines = [head + "(最新在前)"];
455
1053
  var list = d.autoEntries.slice().reverse();
456
1054
  for (var i = 0; i < list.length; i++) {
457
1055
  var x = list[i];
@@ -499,8 +1097,19 @@ window.__ModuleLoader__.load({
499
1097
  '#dsh-skill-browser-panel{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);' +
500
1098
  'width:min(940px,94vw);height:min(660px,88vh);display:flex;flex-direction:column;' +
501
1099
  'background:rgba(24,26,34,.97);color:#e8eaf2;border:1px solid rgba(255,255,255,.1);border-radius:14px;' +
502
- 'box-shadow:0 12px 48px rgba(0,0,0,.5);z-index:2147481100;overflow:hidden;font-family:inherit}' +
1100
+ 'box-shadow:0 12px 48px rgba(0,0,0,.5);z-index:2147483000;overflow:hidden;font-family:inherit}' +
503
1101
  '#dsb-built{display:flex;flex-direction:column;min-height:0;flex:1}' +
1102
+ // v1.9.4 滚动条必须看得见:默认 overlay 滚动条在深色面板上几乎隐形,
1103
+ // 用户滚了也不知道滚到哪。这里给面板内所有滚动容器统一加常驻滚动条。
1104
+ '#dsh-skill-browser-panel *::-webkit-scrollbar{width:11px;height:11px}' +
1105
+ '#dsh-skill-browser-panel *::-webkit-scrollbar-track{background:var(--dsb-soft,rgba(255,255,255,.06));border-radius:6px;margin:2px}' +
1106
+ '#dsh-skill-browser-panel *::-webkit-scrollbar-thumb{background:var(--dsb-brand,rgba(143,199,255,.75));' +
1107
+ 'border-radius:6px;border:2px solid transparent;background-clip:padding-box;min-height:40px}' +
1108
+ '#dsh-skill-browser-panel *::-webkit-scrollbar-thumb:hover{background:var(--dsb-brand,#8fc7ff);background-clip:padding-box}' +
1109
+ '#dsh-skill-browser-panel *::-webkit-scrollbar-corner{background:transparent}' +
1110
+ '#dsh-skill-browser-panel .dsb-cards,#dsh-skill-browser-panel .dsb-ledger-table,' +
1111
+ '#dsh-skill-browser-panel .dsb-detail-body,#dsh-skill-browser-panel .dsb-groups{scrollbar-width:thin;' +
1112
+ 'scrollbar-color:var(--dsb-brand,rgba(143,199,255,.75)) transparent;overscroll-behavior:contain}' +
504
1113
  '.dsb-head{display:flex;align-items:center;justify-content:space-between;padding:12px 16px 6px}' +
505
1114
  '.dsb-title{font-size:16px;font-weight:600}' +
506
1115
  '.dsb-headbtns{display:flex;gap:6px}' +
@@ -541,7 +1150,7 @@ window.__ModuleLoader__.load({
541
1150
  'display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}' +
542
1151
  '.dsb-card-tag{font-size:10.5px;color:#8b93a8;margin-top:6px}' +
543
1152
  '.dsb-empty{grid-column:1/-1;text-align:center;color:#9aa1b5;padding:40px 0;font-size:13px;line-height:1.7}' +
544
- '.dsb-detail{position:absolute;inset:0;background:rgba(24,26,34,.99);flex-direction:column;z-index:2}' +
1153
+ '.dsb-detail{position:absolute;inset:0;background:rgba(24,26,34,.99);flex-direction:column;z-index:20}' +
545
1154
  '.dsb-detail-body{flex:1;min-height:0;overflow:auto;margin:0;padding:6px 16px 16px;' +
546
1155
  'font-family:ui-monospace,Consolas,monospace;font-size:12px;line-height:1.65;color:#cfd6e8;white-space:pre-wrap;word-break:break-word}' +
547
1156
  '.dsb-tabs{display:flex;gap:4px;margin:0 8px}' +
@@ -646,7 +1255,209 @@ window.__ModuleLoader__.load({
646
1255
  '#dsh-skill-browser-panel .dsb-bmenu-danger:hover{background:rgba(255,120,120,.16)}' +
647
1256
  '#dsh-skill-browser-panel .dsb-bmenu-danger .dsb-bmenu-sub{color:#ff9d9d}' +
648
1257
  '#dsh-skill-browser-panel .dsb-bmenu-cancel{background:transparent;border:none;color:#9aa1b5;font-size:12px;cursor:pointer;padding:4px 6px;text-align:center}' +
649
- '#dsh-skill-browser-panel .dsb-bmenu-cancel:hover{color:#dfe3ee}';
1258
+ '#dsh-skill-browser-panel .dsb-bmenu-cancel:hover{color:#dfe3ee}' +
1259
+ '#dsh-skill-browser-panel .dsb-lc-date{font-size:10.5px;color:#8b93a8;white-space:nowrap}' +
1260
+ '#dsh-skill-browser-panel .dsb-lc-rank{font-size:10.5px;color:#8b93a8;text-align:right;white-space:nowrap}' +
1261
+ '#dsh-skill-browser-panel .dsb-lc-status{font-size:10.5px;color:#ffb3b3;white-space:nowrap}' +
1262
+ /* ── v1.9.2:版本号徽章(语义色,全部皮肤可读)── */
1263
+ '#dsh-skill-browser-panel .dsb-ver{display:inline-block;margin-left:6px;font-size:10px;font-family:ui-monospace,Consolas,monospace;' +
1264
+ 'color:var(--dsb-ok,#9ff5c8);background:var(--dsb-okbg,rgba(80,220,150,.12));border:1px solid var(--dsb-okline,rgba(80,220,150,.3));border-radius:999px;padding:0 6px;vertical-align:middle}' +
1265
+ '#dsh-skill-browser-panel .dsb-ver-none{color:var(--dsb-dim,#8b93a8);background:var(--chipBg,rgba(255,255,255,.05));border-color:var(--pbd,rgba(255,255,255,.12))}' +
1266
+ /* ── v1.9.8:分组栏改为「树形统一管理」──
1267
+ 原来大类是一排小卡片 flex-wrap 挨着、子类只在选中后才另起一行,视觉上零零散散。
1268
+ 现在:顶部一条工具条 + 下面每个大类一整行,子类**常驻**缩进挂在大类下方(带竖引导线),
1269
+ 一眼就能看出「哪个大类下面有哪几个子类」。 */
1270
+ '#dsh-skill-browser-panel .dsb-groups{display:flex;flex-direction:column;gap:6px;padding:0 16px 8px;' +
1271
+ 'max-height:220px;overflow-y:auto;overflow-x:hidden}' +
1272
+ '#dsh-skill-browser-panel .dsb-gbar{display:flex;align-items:center;gap:5px;flex-wrap:wrap}' +
1273
+ '#dsh-skill-browser-panel .dsb-gsp{flex:1 1 auto;min-width:0}' +
1274
+ /* v1.9.9 模型探针徽标:文字色一律继承父级(工具条=--itm),不写死,
1275
+ 否则浅色皮肤下白字白底。 */
1276
+ '#dsh-skill-browser-panel .dsb-gmodel{font-size:10.5px;line-height:1;padding:3px 7px;' +
1277
+ 'border-radius:6px;border:1px dashed var(--dsb-line,rgba(255,255,255,.16));' +
1278
+ 'color:inherit;cursor:pointer;max-width:260px;white-space:nowrap;overflow:hidden;' +
1279
+ 'text-overflow:ellipsis;background:var(--dsb-soft,rgba(255,255,255,.04))}' +
1280
+ '#dsh-skill-browser-panel .dsb-gmodel-idle{opacity:.72}' +
1281
+ '#dsh-skill-browser-panel .dsb-gmodel-ok{border-color:var(--dsb-okline,rgba(80,220,150,.30));' +
1282
+ 'color:var(--dsb-ok,#9ff5c8);background:var(--dsb-okbg,rgba(80,220,150,.12))}' +
1283
+ '#dsh-skill-browser-panel .dsb-gmodel-bad{border-color:var(--dsb-fail,rgba(255,120,120,.55));' +
1284
+ 'color:var(--dsb-fail,#ff9d9d);background:transparent}' +
1285
+ '#dsh-skill-browser-panel .dsb-gtree{display:flex;flex-direction:column;gap:5px}' +
1286
+ '#dsh-skill-browser-panel .dsb-gitem{display:flex;flex-direction:column;gap:3px}' +
1287
+ /* 大类行:整宽一行 */
1288
+ '#dsh-skill-browser-panel .dsb-grow{position:relative;display:flex;align-items:center;gap:7px;height:32px;' +
1289
+ 'padding:0 8px;border-radius:8px;cursor:pointer;overflow:hidden;' +
1290
+ 'background:linear-gradient(135deg,var(--dsb-tile1,#39406b),var(--dsb-tile2,#232a45));' +
1291
+ 'border:1px solid var(--pbd,rgba(255,255,255,.16));background-size:cover;background-position:center}' +
1292
+ '#dsh-skill-browser-panel .dsb-grow::after{content:"";position:absolute;inset:0;' +
1293
+ 'background:var(--dsb-shade,rgba(8,10,18,.42));pointer-events:none}' +
1294
+ '#dsh-skill-browser-panel .dsb-grow>*{position:relative;z-index:1}' +
1295
+ '#dsh-skill-browser-panel .dsb-grow:hover{border-color:var(--stA,#7c8dff)}' +
1296
+ '#dsh-skill-browser-panel .dsb-grow-on{border-color:var(--stA,#7c8dff)!important;' +
1297
+ 'box-shadow:0 0 0 2px var(--stA,#7c8dff) inset}' +
1298
+ '#dsh-skill-browser-panel .dsb-gico{font-size:13px;line-height:1;flex:0 0 auto}' +
1299
+ '#dsh-skill-browser-panel .dsb-grname{font-size:12px;font-weight:600;color:var(--dsb-oncard,#fff);' +
1300
+ 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-shadow:0 1px 2px rgba(0,0,0,.55)}' +
1301
+ '#dsh-skill-browser-panel .dsb-grmeta{font-size:10px;color:var(--dsb-oncard-dim,#d6def2);' +
1302
+ 'white-space:nowrap;flex:0 0 auto;text-shadow:0 1px 2px rgba(0,0,0,.5)}' +
1303
+ '#dsh-skill-browser-panel .dsb-grsp{flex:1 1 auto;min-width:0}' +
1304
+ '#dsh-skill-browser-panel .dsb-grops{display:flex;gap:2px;opacity:0;transition:opacity .12s;flex:0 0 auto}' +
1305
+ '#dsh-skill-browser-panel .dsb-grow:hover .dsb-grops{opacity:1}' +
1306
+ '#dsh-skill-browser-panel .dsb-gop{font-size:9px;line-height:1;background:rgba(0,0,0,.62);' +
1307
+ 'border:1px solid rgba(255,255,255,.26);color:#fff;border-radius:4px;padding:1px 3px;cursor:pointer}' +
1308
+ '#dsh-skill-browser-panel .dsb-gop:hover{background:var(--stA,#5b6cff);border-color:transparent}' +
1309
+ /* 子类行:常驻、竖排、每行一个,带 ├─ / └─ 树枝连接符 */
1310
+ '#dsh-skill-browser-panel .dsb-gsubrow{display:flex;flex-direction:column;gap:1px;' +
1311
+ 'padding:1px 0 3px 14px;position:relative}' +
1312
+ '#dsh-skill-browser-panel .dsb-gline{display:flex;align-items:center;gap:0;position:relative;' +
1313
+ 'padding-left:14px;min-height:21px}' +
1314
+ '#dsh-skill-browser-panel .dsb-gconn{position:absolute;left:0;top:0;bottom:0;width:14px;' +
1315
+ 'color:var(--dsb-line,rgba(255,255,255,.30));font-family:monospace;font-size:11px;line-height:21px;' +
1316
+ 'white-space:pre;user-select:none;pointer-events:none}' +
1317
+ '#dsh-skill-browser-panel .dsb-gline::before{content:"";position:absolute;left:5px;top:0;' +
1318
+ 'bottom:0;width:1px;background:var(--dsb-line,rgba(255,255,255,.18))}' +
1319
+ '#dsh-skill-browser-panel .dsb-gline-last::before{bottom:50%}' +
1320
+ '#dsh-skill-browser-panel .dsb-gsub{display:inline-flex;align-items:center;gap:4px;height:21px;padding:0 8px;' +
1321
+ 'border-radius:6px;border:1px solid transparent;text-align:left;' +
1322
+ 'background:transparent;color:var(--dsb-dim,#9aa1b5);font-size:11px;cursor:pointer;' +
1323
+ 'white-space:nowrap;max-width:260px;overflow:hidden;text-overflow:ellipsis}' +
1324
+ '#dsh-skill-browser-panel .dsb-gsub:hover{color:var(--ptx,#dfe3ee);' +
1325
+ 'background:var(--dsb-hover,rgba(120,140,255,.16));border-color:var(--dsb-line,rgba(255,255,255,.16))}' +
1326
+ '#dsh-skill-browser-panel .dsb-gsub-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;' +
1327
+ 'color:var(--dsb-onbrand,#fff)!important;border-color:transparent!important;font-weight:600}' +
1328
+ '#dsh-skill-browser-panel .dsb-gsub .dsb-gn{font-size:9.5px;opacity:.8;text-shadow:none}' +
1329
+ '#dsh-skill-browser-panel .dsb-gnone{font-size:10px;color:var(--dsb-dim,#8b93a8);font-style:italic}' +
1330
+ // v1.9.9:大类行内部的「N 项 · N 子类」用 oncard-dim(亮字),但 --dsb-dim
1331
+ // 是按卡片底算的深/浅色,落到 tile 深底上会看不清 → 大类行内单独覆盖成
1332
+ // oncard-dim,并保证 tile 底足够暗(semanticVarsCss 里 tile 固定深色)。
1333
+ '#dsh-skill-browser-panel .dsb-grow .dsb-grmeta,' +
1334
+ '#dsh-skill-browser-panel .dsb-grow .dsb-gn,' +
1335
+ '#dsh-skill-browser-panel .dsb-grow .dsb-gnone{color:var(--dsb-oncard-dim,#d6def2)}' +
1336
+ // v1.9.9:工具条/子类按钮的文字色一律走 --dsb-dim(该变量在 semanticVarsCss
1337
+ // 里按「卡片真实底色」算到 ≥4.8:1),不再用 --itm —— 实测 --itm 在 6 款皮肤上
1338
+ // 只有 4.40~4.50:1,10~11px 小字刚好卡在 WCAG AA 线下方。
1339
+ '#dsh-skill-browser-panel .dsb-gadd{height:26px;min-width:0;border-radius:7px;border:1px dashed var(--pbd,rgba(255,255,255,.22));' +
1340
+ 'background:var(--chipBg,rgba(255,255,255,.04));color:var(--dsb-dim,#b9c2d8);font-size:11px;cursor:pointer;padding:0 8px;' +
1341
+ 'display:inline-flex;align-items:center;gap:4px;white-space:nowrap}' +
1342
+ '#dsh-skill-browser-panel .dsb-gadd:hover{color:var(--ptx,#dfe3ee);border-color:var(--stA,#7c8dff)}' +
1343
+ '#dsh-skill-browser-panel .dsb-gadd-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;' +
1344
+ 'color:var(--dsb-onbrand,#fff)!important;border-style:solid!important;border-color:transparent!important}' +
1345
+ '#dsh-skill-browser-panel .dsb-gadd-kw{border-color:var(--dsb-okline,rgba(80,220,150,.34));color:var(--dsb-ok,#9ff5c8)}' +
1346
+ '#dsh-skill-browser-panel .dsb-gbusy{opacity:.55;pointer-events:none}' +
1347
+ '#dsh-skill-browser-panel .dsb-gtitle{font-size:11px;color:var(--dsb-dim,#8b93a8);white-space:nowrap}' +
1348
+ '#dsh-skill-browser-panel .dsb-ghint{font-size:10.5px;color:var(--dsb-dim,#6f778c);white-space:nowrap;' +
1349
+ 'overflow:hidden;text-overflow:ellipsis;max-width:44%}' +
1350
+ // v1.9.9 对比度修正:数字徽标一律「继承父级文字色」,不再写死
1351
+ // oncard-dim(近白色)。原因:同一个 .dsb-gn 同时出现在工具条(面板底)、
1352
+ // 激活渐变按钮(品牌渐变底)、大类行(tile 深底)三种背景上,写死一个颜色
1353
+ // 必有一处看不清 —— 截图里「全部 111」的白字就是这么来的。
1354
+ // 继承父色后:工具条→--itm、激活按钮→--dsb-onbrand、大类行→--dsb-oncard,
1355
+ // 三处都天然达标。
1356
+ '#dsh-skill-browser-panel .dsb-gn{font-size:10px;color:inherit;opacity:.82;' +
1357
+ 'flex:0 0 auto;font-weight:500}' +
1358
+ '#dsh-skill-browser-panel .dsb-gadd-on .dsb-gn{opacity:.9}' +
1359
+ '#dsh-skill-browser-panel .dsb-gcard .dsb-gn,' +
1360
+ '#dsh-skill-browser-panel .dsb-gcard-dom .dsb-gn,' +
1361
+ '#dsh-skill-browser-panel .dsb-grow .dsb-gn{opacity:.88}' +
1362
+ /* ── v1.9.8 平铺模式(原样保留,用户可一键切回)── */
1363
+ '#dsh-skill-browser-panel .dsb-gflat{display:flex;flex-wrap:wrap;gap:5px;align-items:center}' +
1364
+ '#dsh-skill-browser-panel .dsb-gcard,#dsh-skill-browser-panel .dsb-gcard-dom{position:relative;display:flex;align-items:center;gap:5px;' +
1365
+ 'min-width:0;max-width:190px;height:30px;padding:0 9px;border-radius:8px;cursor:pointer;overflow:hidden;' +
1366
+ 'background:linear-gradient(135deg,var(--dsb-tile1,#39406b),var(--dsb-tile2,#232a45));' +
1367
+ 'border:1px solid var(--pbd,rgba(255,255,255,.18));background-size:cover;background-position:center}' +
1368
+ '#dsh-skill-browser-panel .dsb-gcard::after{content:"";position:absolute;inset:0;' +
1369
+ 'background:var(--dsb-shade,rgba(8,10,18,.42));pointer-events:none}' +
1370
+ '#dsh-skill-browser-panel .dsb-gcard>*{position:relative;z-index:1}' +
1371
+ '#dsh-skill-browser-panel .dsb-gcard:hover{border-color:var(--stA,#7c8dff)}' +
1372
+ '#dsh-skill-browser-panel .dsb-gcard-on{border-color:var(--stA,#7c8dff)!important;' +
1373
+ 'box-shadow:0 0 0 2px var(--stA,#7c8dff) inset}' +
1374
+ '#dsh-skill-browser-panel .dsb-gname{font-size:11.5px;color:var(--dsb-oncard,#fff);font-weight:600;' +
1375
+ 'line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:132px;' +
1376
+ 'text-shadow:0 1px 2px rgba(0,0,0,.55)}' +
1377
+ '#dsh-skill-browser-panel .dsb-gkids{font-size:9.5px;color:var(--dsb-oncard-dim,#cfd8ea);' +
1378
+ 'margin-left:5px;opacity:.9;font-weight:400}' +
1379
+ '#dsh-skill-browser-panel .dsb-gops{position:absolute;top:1px;right:1px;display:flex;gap:1px;' +
1380
+ 'opacity:0;transition:opacity .12s}' +
1381
+ '#dsh-skill-browser-panel .dsb-gcard:hover .dsb-gops{opacity:1}' +
1382
+ '#dsh-skill-browser-panel .dsb-gsubs{display:flex;flex-wrap:wrap;gap:5px;width:100%;align-items:center;' +
1383
+ 'padding:4px 0 0;margin-top:2px;border-top:1px dashed var(--dsb-line,rgba(255,255,255,.1))}' +
1384
+ /* ── v1.9.3:AI 分组方案确认弹层 ── */
1385
+ '#dsh-skill-browser-panel .dsb-plan-menu{min-width:0;width:calc(100% - 24px);left:12px;overflow-y:auto}' +
1386
+ '#dsh-skill-browser-panel .dsb-plan-stat{font-size:10.5px;color:var(--dsb-dim,#8b93a8);padding:2px 8px 6px}' +
1387
+ '#dsh-skill-browser-panel .dsb-plan-list{max-height:52vh;overflow-y:auto;padding:2px 4px 6px;display:flex;' +
1388
+ 'flex-direction:column;gap:5px}' +
1389
+ '#dsh-skill-browser-panel .dsb-plan-dom{background:var(--dsb-soft,rgba(255,255,255,.04));' +
1390
+ 'border:1px solid var(--dsb-line,rgba(255,255,255,.08));border-radius:8px;padding:5px 7px}' +
1391
+ '#dsh-skill-browser-panel .dsb-plan-dname{font-size:11.5px;font-weight:600;color:var(--dsb-brand,#8fc7ff);' +
1392
+ 'display:flex;align-items:center;gap:6px}' +
1393
+ '#dsh-skill-browser-panel .dsb-plan-subs{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}' +
1394
+ '#dsh-skill-browser-panel .dsb-plan-subs>span,#dsh-skill-browser-panel .dsb-plan-subs{font-size:10.5px}' +
1395
+ '#dsh-skill-browser-panel .dsb-plan-subs>span{color:var(--dsb-dim,#b9c2d8);background:var(--chipBg,rgba(255,255,255,.06));' +
1396
+ 'border-radius:6px;padding:1px 6px;display:inline-flex;align-items:center;gap:4px}' +
1397
+ /* 移动菜单里的层级缩进 */
1398
+ '#dsh-skill-browser-panel .dsb-move-dom{font-weight:600}' +
1399
+ '#dsh-skill-browser-panel .dsb-move-sub{padding-left:18px;font-size:11px;opacity:.92}' +
1400
+ /* 卡片上的"移动"按钮 */
1401
+ '#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);' +
1402
+ 'border-radius:6px;font-size:10.5px;padding:2px 7px;cursor:pointer;white-space:nowrap}' +
1403
+ '#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)}' +
1404
+ '#dsh-skill-browser-panel .dsb-card-foot{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:6px}' +
1405
+ '#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}' +
1406
+ /* ── v1.9.1:调用台账看板(全部改用语义变量,浅色皮肤同样可读)── */
1407
+ '#dsh-skill-browser-panel .dsb-lbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:0 0 8px}' +
1408
+ '#dsh-skill-browser-panel .dsb-lq{background:var(--chipBg,rgba(255,255,255,.06));border:1px solid var(--pbd,rgba(255,255,255,.12));color:var(--dsb-dim,#cdd3e3);' +
1409
+ 'border-radius:999px;padding:3px 10px;font-size:11.5px;cursor:pointer;white-space:nowrap}' +
1410
+ '#dsh-skill-browser-panel .dsb-lq:hover{background:var(--dsb-hover,rgba(255,255,255,.14));color:var(--ptx,#fff)}' +
1411
+ '#dsh-skill-browser-panel .dsb-lq-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff));color:var(--dsb-onbrand,#fff);border-color:transparent}' +
1412
+ '#dsh-skill-browser-panel .dsb-ldate{background:var(--chipBg,rgba(255,255,255,.06));border:1px solid var(--pbd,rgba(255,255,255,.12));color:var(--ptx,#dfe3ee);' +
1413
+ 'border-radius:6px;padding:2px 6px;font-size:11.5px;font-family:ui-monospace,Consolas,monospace;color-scheme:var(--dsb-scheme,dark)}' +
1414
+ '#dsh-skill-browser-panel .dsb-lsep{width:1px;height:16px;background:var(--pbd,rgba(255,255,255,.14));margin:0 3px}' +
1415
+ '#dsh-skill-browser-panel .dsb-lhint{font-size:10.5px;color:var(--dsb-dim,#6f778c);margin-left:auto;white-space:nowrap}' +
1416
+ '#dsh-skill-browser-panel .dsb-lcards{display:flex;gap:8px;padding:0 0 10px;flex-wrap:wrap}' +
1417
+ '#dsh-skill-browser-panel .dsb-lcard{flex:1;min-width:88px;background:var(--chipBg,rgba(255,255,255,.05));border:1px solid var(--pbd,rgba(255,255,255,.09));' +
1418
+ 'border-radius:10px;padding:8px 10px;text-align:center}' +
1419
+ '#dsh-skill-browser-panel .dsb-lcard b{display:block;font-size:20px;line-height:1.2;color:var(--dsb-brand-strong,#8fc7ff)}' +
1420
+ '#dsh-skill-browser-panel .dsb-lcard span{font-size:10.5px;color:var(--dsb-dim,#9aa1b5)}' +
1421
+ '#dsh-skill-browser-panel .dsb-lcard-fail b{color:var(--dsb-fail,#ff9d9d)}' +
1422
+ '#dsh-skill-browser-panel .dsb-lcard-rate b{color:var(--dsb-ok,#9ff5c8)}' +
1423
+ '#dsh-skill-browser-panel .dsb-ltrend{display:flex;align-items:flex-end;gap:2px;height:76px;padding:6px 8px;margin-bottom:10px;' +
1424
+ 'background:var(--dsb-soft,rgba(255,255,255,.03));border:1px solid var(--dsb-line,rgba(255,255,255,.07));border-radius:10px;overflow-x:auto}' +
1425
+ '#dsh-skill-browser-panel .dsb-lbarcol{flex:1;min-width:5px;display:flex;flex-direction:column;justify-content:flex-end;' +
1426
+ 'height:100%;position:relative;cursor:default}' +
1427
+ '#dsh-skill-browser-panel .dsb-lbarfill{background:linear-gradient(180deg,var(--dsb-bar1,#5b8cff),var(--dsb-bar2,#2fa8ff));border-radius:2px 2px 0 0;min-height:1px}' +
1428
+ '#dsh-skill-browser-panel .dsb-lbarfill-zero{background:var(--dsb-line,rgba(255,255,255,.08))}' +
1429
+ '#dsh-skill-browser-panel .dsb-lbarfill-fail{background:linear-gradient(180deg,var(--dsb-fail,#ff7d7d),var(--dsb-fail,#ff5c5c))}' +
1430
+ '#dsh-skill-browser-panel .dsb-lbarcol:hover .dsb-lbarfill{filter:brightness(1.25)}' +
1431
+ '#dsh-skill-browser-panel .dsb-lrank{display:flex;flex-direction:column;gap:3px}' +
1432
+ '#dsh-skill-browser-panel .dsb-lrrow{display:grid;grid-template-columns:30px minmax(0,1fr) 54px 46px 38px 38px 56px 58px;gap:6px;' +
1433
+ 'align-items:center;background:var(--dsb-soft,rgba(255,255,255,.04));border:1px solid var(--dsb-line,rgba(255,255,255,.07));border-radius:8px;' +
1434
+ 'padding:6px 9px;font-size:11.5px;transition:background .12s}' +
1435
+ '#dsh-skill-browser-panel .dsb-lrrow:hover{background:var(--dsb-hover,rgba(120,140,255,.14))}' +
1436
+ '#dsh-skill-browser-panel .dsb-lrhead{background:var(--dsb-surface,#1a1d26);' +
1437
+ 'border-color:transparent transparent var(--dsb-line,rgba(255,255,255,.12));' +
1438
+ 'font-size:10.5px;color:var(--dsb-dim,#8b93a8);' +
1439
+ 'padding:3px 9px;position:sticky;top:0;z-index:3;box-shadow:0 2px 6px rgba(0,0,0,.10)}' +
1440
+ '#dsh-skill-browser-panel .dsb-lrhead span{cursor:pointer;user-select:none;white-space:nowrap}' +
1441
+ '#dsh-skill-browser-panel .dsb-lrhead span:hover{color:var(--ptx,#dfe3ee)}' +
1442
+ '#dsh-skill-browser-panel .dsb-lr-sorton{color:var(--dsb-brand,#8fc7ff)!important;font-weight:700}' +
1443
+ '#dsh-skill-browser-panel .dsb-lrk{color:var(--dsb-dim,#8b93a8);text-align:right;font-family:ui-monospace,Consolas,monospace}' +
1444
+ '#dsh-skill-browser-panel .dsb-lrname{font-family:ui-monospace,Consolas,monospace;color:var(--dsb-brand,#8fc7ff);overflow:hidden;' +
1445
+ 'text-overflow:ellipsis;white-space:nowrap}' +
1446
+ '#dsh-skill-browser-panel .dsb-lrver{font-size:10px;color:var(--dsb-ok,#9ff5c8);font-family:ui-monospace,Consolas,monospace;' +
1447
+ 'text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
1448
+ '#dsh-skill-browser-panel .dsb-lrver-none{color:var(--dsb-dim,#6f778c)}' +
1449
+ '#dsh-skill-browser-panel .dsb-lrn{text-align:right;font-family:ui-monospace,Consolas,monospace;color:var(--ptx,#dfe3ee)}' +
1450
+ '#dsh-skill-browser-panel .dsb-lrf{text-align:right;font-family:ui-monospace,Consolas,monospace;color:var(--dsb-fail,#ff9d9d)}' +
1451
+ '#dsh-skill-browser-panel .dsb-lrr{text-align:right;font-family:ui-monospace,Consolas,monospace;color:var(--ptx,#dfe3ee)}' +
1452
+ '#dsh-skill-browser-panel .dsb-lrdate{text-align:right;font-size:10.5px;color:var(--dsb-dim,#8b93a8);font-family:ui-monospace,Consolas,monospace}' +
1453
+ '#dsh-skill-browser-panel .dsb-lrbar{grid-column:1/-1;height:3px;border-radius:2px;background:var(--dsb-line,rgba(255,255,255,.07));overflow:hidden}' +
1454
+ '#dsh-skill-browser-panel .dsb-lrbar i{display:block;height:100%;border-radius:2px;background:linear-gradient(90deg,var(--dsb-bar1,#5b8cff),var(--dsb-bar2,#2fa8ff))}' +
1455
+ '#dsh-skill-browser-panel .dsb-lsec{display:flex;align-items:center;gap:8px;font-size:11.5px;font-weight:600;color:var(--ptx,#dfe3ee);' +
1456
+ 'margin:12px 0 6px;padding-top:9px;border-top:1px solid var(--dsb-line,rgba(255,255,255,.08))}' +
1457
+ '#dsh-skill-browser-panel .dsb-lsec span.sub{font-weight:400;font-size:10.5px;color:var(--dsb-dim,#8b93a8)}' +
1458
+ '#dsh-skill-browser-panel .dsb-ldetail-toggle{background:var(--chipBg,rgba(255,255,255,.06));border:1px solid var(--pbd,rgba(255,255,255,.12));' +
1459
+ 'color:var(--dsb-dim,#b9c2d8);border-radius:6px;padding:2px 8px;font-size:10.5px;cursor:pointer}' +
1460
+ '#dsh-skill-browser-panel .dsb-ldetail-toggle:hover{background:var(--dsb-hover,rgba(120,140,255,.24));color:var(--ptx,#fff)}';
650
1461
 
651
1462
  function buildPanel() {
652
1463
  if (S.panelBuilt) return;
@@ -663,11 +1474,12 @@ window.__ModuleLoader__.load({
663
1474
  '<div class="dsb-title">🧩 技能浏览器</div>' +
664
1475
  '<div class="dsb-tabs">' +
665
1476
  '<button id="dsb-tab-skills" class="dsb-tab" data-view="skills">技能浏览</button>' +
666
- '<button id="dsb-tab-ledger" class="dsb-tab" data-view="ledger">📋 失效台账</button>' +
1477
+ '<button id="dsb-tab-ledger" class="dsb-tab" data-view="ledger">📊 调用台账</button>' +
667
1478
  '</div>' +
668
1479
  '<div class="dsb-headbtns">' +
669
1480
  '<button id="dsb-skin-btn" class="dsb-btn" title="切换皮肤(三球同步 · 128款)">🎨</button>' +
670
1481
  '<button id="dsb-setting" class="dsb-btn" title="开关设置">⚙</button>' +
1482
+ '<button id="dsb-hot" class="dsb-btn" title="' + esc(T("hotReloadTip")) + '">⚡</button>' +
671
1483
  '<button id="dsb-refresh" class="dsb-btn" title="重新扫描技能目录">↻ 刷新</button>' +
672
1484
  '<button id="dsb-close" class="dsb-btn" title="关闭">×</button>' +
673
1485
  '</div>' +
@@ -713,13 +1525,19 @@ window.__ModuleLoader__.load({
713
1525
  '</div>' +
714
1526
  '</div>' +
715
1527
  '<input id="dsb-q" class="dsb-q" type="text" placeholder="🔍 搜索技能名 / 中文简介 / 描述…" />' +
1528
+ '<div id="dsb-groups" class="dsb-groups"></div>' +
716
1529
  '<div id="dsb-sortbar" class="dsb-sortbar" style="display:none"></div>' +
717
1530
  '<div id="dsb-chips" class="dsb-chips"></div>' +
718
1531
  '<div id="dsb-subs" class="dsb-subs"></div>' +
719
1532
  '<div id="dsb-cards" class="dsb-cards"></div>' +
720
1533
  '<div id="dsb-ledger" class="dsb-ledger" style="display:none">' +
721
- '<div id="dsb-ledger-sum" class="dsb-ledger-sum"></div>' +
722
- '<div id="dsb-ledger-table" class="dsb-ledger-table"></div>' +
1534
+ '<div id="dsb-lbar" class="dsb-lbar"></div>' +
1535
+ '<div id="dsb-lcards" class="dsb-lcards"></div>' +
1536
+ '<div id="dsb-ltrend" class="dsb-ltrend"></div>' +
1537
+ '<div id="dsb-ledger-table" class="dsb-ledger-table">' +
1538
+ '<div id="dsb-lrank" class="dsb-lrank"></div>' +
1539
+ '<div id="dsb-ledger-sum" class="dsb-ledger-sum"></div>' +
1540
+ '</div>' +
723
1541
  '</div>' +
724
1542
  '<div id="dsb-detail" class="dsb-detail" style="display:none">' +
725
1543
  '<div class="dsb-head">' +
@@ -763,8 +1581,92 @@ window.__ModuleLoader__.load({
763
1581
  });
764
1582
  bmenuMask.addEventListener("click", function () { closeBatchMenu(); });
765
1583
 
1584
+ // v1.9.0 "移动到分组"浮层(与批量确认菜单同款遮罩/锚定,独立菜单体)
1585
+ var moveMask = document.createElement("div");
1586
+ moveMask.id = "dsb-move-mask";
1587
+ moveMask.className = "dsb-bmenu-mask";
1588
+ moveMask.style.display = "none";
1589
+ panel.appendChild(moveMask);
1590
+ var moveMenu = document.createElement("div");
1591
+ moveMenu.id = "dsb-move";
1592
+ moveMenu.className = "dsb-bmenu";
1593
+ moveMenu.style.display = "none";
1594
+ panel.appendChild(moveMenu);
1595
+ moveMenu.addEventListener("click", function (ev) {
1596
+ var pa = ev.target.closest("[data-plan-act]");
1597
+ if (pa) {
1598
+ var pact = pa.getAttribute("data-plan-act");
1599
+ if (pact === "replace") applyPlanFlow("replace");
1600
+ else if (pact === "merge") applyPlanFlow("merge");
1601
+ else closePlanMenu();
1602
+ return;
1603
+ }
1604
+ var it = ev.target.closest("[data-move-gid]");
1605
+ if (!it) return;
1606
+ var gid = it.getAttribute("data-move-gid");
1607
+ var dir = S.moveMenuDir;
1608
+ if (gid === "__cancel" || !dir) { closeMoveMenu(); return; }
1609
+ assignFlow(dir, gid);
1610
+ });
1611
+ moveMask.addEventListener("click", function () {
1612
+ if (S.gplanBusy) closePlanMenu(); else closeMoveMenu();
1613
+ });
1614
+
1615
+ // v1.9.0 分组栏交互:选分组 / 新建 / AI 自动分组 / 重命名 / 删分组 / 生成封面
1616
+ $("dsb-groups").addEventListener("click", function (ev) {
1617
+ var op = ev.target.closest("[data-gop]");
1618
+ if (op) {
1619
+ var act = op.getAttribute("data-gop");
1620
+ var gid = op.getAttribute("data-gid") || "";
1621
+ ev.stopPropagation();
1622
+ if (act === "cover") aiCoverFlow(gid);
1623
+ else if (act === "rename") renameGroupFlow(gid);
1624
+ else if (act === "del") deleteGroupFlow(gid);
1625
+ else if (act === "sub") createSubGroupFlow(gid);
1626
+ return;
1627
+ }
1628
+ // v1.9.9 模型探针徽标:点一下重新探测(不调模型,只读配置)
1629
+ var mo = ev.target.closest("[data-gmodel]");
1630
+ if (mo) {
1631
+ loadModelInfo(true);
1632
+ return;
1633
+ }
1634
+ // v1.9.8 显示模式切换:树状 / 平铺
1635
+ var gm = ev.target.closest("[data-gmode]");
1636
+ if (gm) {
1637
+ S.gmode = gm.getAttribute("data-gmode") === "flat" ? "flat" : "tree";
1638
+ try { localStorage.setItem("dsh-skill-browser-gmode", S.gmode); } catch (e) {}
1639
+ paintGroups();
1640
+ return;
1641
+ }
1642
+ var add = ev.target.closest("[data-gadd]");
1643
+ if (add) {
1644
+ var how = add.getAttribute("data-gadd");
1645
+ if (how === "kw") kwClassifyFlow();
1646
+ else if (how === "ai") aiClassifyFlow();
1647
+ else createGroupFlow();
1648
+ return;
1649
+ }
1650
+ // 二级:子类筛选
1651
+ var gs = ev.target.closest("[data-gsub]");
1652
+ if (gs) {
1653
+ S.gsub = gs.getAttribute("data-gsub") || "";
1654
+ paintGroups(); paintCards();
1655
+ return;
1656
+ }
1657
+ var card = ev.target.closest("[data-gsel]");
1658
+ if (card) {
1659
+ S.group = card.getAttribute("data-gsel") || "all";
1660
+ S.gsub = "";
1661
+ paintGroups(); paintCards();
1662
+ }
1663
+ });
1664
+
766
1665
  $("dsb-close").addEventListener("click", function () { closePanel(); });
767
1666
  $("dsb-refresh").addEventListener("click", function () { if (S.view === "ledger") loadLedgerRefresh(); else load(true); });
1667
+
1668
+ // ── v1.9.4 热加载:重新加载插件代码,不重启桌面端 ──────────────
1669
+ $("dsb-hot").addEventListener("click", function () { hotReloadFlow(); });
768
1670
  $("dsb-setting").addEventListener("click", function () {
769
1671
  var box = $("dsb-settings");
770
1672
  if (!box) return;
@@ -794,14 +1696,56 @@ window.__ModuleLoader__.load({
794
1696
  var rootBrowse = $("dsb-set-root-browse");
795
1697
  if (rootBrowse) rootBrowse.addEventListener("click", function () { browseForDirectory(); });
796
1698
  $("dsb-q").addEventListener("input", function () { S.q = this.value; S.sub = ""; paintChips(); paintSubs(); paintCards(); });
1699
+ // v1.9.6 技能列表滚到底自动追加一批(无感分页,避免一次性渲染上千节点)
1700
+ $("dsb-cards").addEventListener("scroll", function () {
1701
+ var b = this;
1702
+ if (!S.data || S.detail) return;
1703
+ if (b.scrollTop + b.clientHeight < b.scrollHeight - 120) return;
1704
+ var total = sortList(filtered()).length;
1705
+ if (S.cardLimit >= total) return;
1706
+ S.cardLimit += 60;
1707
+ paintCards();
1708
+ }, { passive: true });
797
1709
  $("dsb-back").addEventListener("click", function () { S.detail = null; S.ledgerDetail = null; paint(); });
798
1710
  $("dsb-tab-skills").addEventListener("click", function () { S.view = "skills"; S.detail = null; S.ledgerDetail = null; paint(); });
799
- $("dsb-tab-ledger").addEventListener("click", function () { S.view = "ledger"; S.detail = null; if (!S.ledger) loadLedger(); else paint(); });
1711
+ $("dsb-tab-ledger").addEventListener("click", function () {
1712
+ S.view = "ledger"; S.detail = null;
1713
+ if (!S.ledger) loadLedger(); else paint();
1714
+ if (!S.lstat) loadStats();
1715
+ });
800
1716
  $("dsb-ledger-table").addEventListener("click", function (ev) {
1717
+ // 排行表头排序
1718
+ var sEl = ev.target.closest("[data-lsort]");
1719
+ if (sEl) { S.lsort = sEl.getAttribute("data-lsort"); loadStats(); return; }
1720
+ // 展开/收起原始流水
1721
+ var fEl = ev.target.closest("[data-lflow]");
1722
+ if (fEl) { S.lstatShowFlow = !S.lstatShowFlow; paintLedger(); return; }
1723
+ // v1.9.6 原始流水分片"加载更多"
1724
+ var mo2 = ev.target.closest("[data-more]");
1725
+ if (mo2 && mo2.getAttribute("data-more") === "flow") {
1726
+ S.lflowLimit += 120; paintLedger(); return;
1727
+ }
801
1728
  var el = ev.target.closest("[data-lskill]");
802
1729
  if (!el) return;
803
1730
  openLedgerDetail(el.getAttribute("data-lskill"));
804
1731
  });
1732
+ // v1.9.1 台账工具栏:快捷区间 / 起止日期 / 粒度
1733
+ $("dsb-lbar").addEventListener("click", function (ev) {
1734
+ var q1 = ev.target.closest("[data-lq]");
1735
+ if (q1) {
1736
+ var rr = quickRange(q1.getAttribute("data-lq"));
1737
+ S.lfrom = rr.from; S.lto = rr.to; S.lgran = rr.gran;
1738
+ loadStats();
1739
+ return;
1740
+ }
1741
+ var g1 = ev.target.closest("[data-lgran]");
1742
+ if (g1) { S.lgran = g1.getAttribute("data-lgran"); loadStats(); return; }
1743
+ });
1744
+ $("dsb-lbar").addEventListener("change", function (ev) {
1745
+ var id = ev.target && ev.target.id;
1746
+ if (id === "dsb-lfrom") { S.lfrom = ev.target.value || ""; loadStats(); }
1747
+ else if (id === "dsb-lto") { S.lto = ev.target.value || ""; loadStats(); }
1748
+ });
805
1749
  $("dsb-chips").addEventListener("click", function (ev) {
806
1750
  var el = ev.target.closest("[data-cat]");
807
1751
  if (!el) return;
@@ -846,6 +1790,13 @@ window.__ModuleLoader__.load({
846
1790
  try { window.open(ext.getAttribute("data-ext-url"), "_blank", "noopener"); } catch (e2) {}
847
1791
  return;
848
1792
  }
1793
+ // v1.9.0 "移动到分组"按钮:点它开浮层,不打开详情
1794
+ var mv = ev.target.closest("[data-move]");
1795
+ if (mv) {
1796
+ ev.stopPropagation();
1797
+ openMoveMenu(mv, mv.getAttribute("data-move"));
1798
+ return;
1799
+ }
849
1800
  // v1.7.0 单技能启用/停用开关:点开关不打开详情(closest 先命中 tgl 按钮)
850
1801
  var tgl = ev.target.closest("[data-tgl]");
851
1802
  if (tgl) {
@@ -855,6 +1806,14 @@ window.__ModuleLoader__.load({
855
1806
  toggleSkill(d1, toDisabled);
856
1807
  return;
857
1808
  }
1809
+ // v1.9.6 分片"加载更多"
1810
+ var mo = ev.target.closest("[data-more]");
1811
+ if (mo) {
1812
+ var mk = mo.getAttribute("data-more");
1813
+ if (mk === "cards") { S.cardLimit += 60; paintCards(); }
1814
+ else if (mk === "flow") { S.lflowLimit += 120; paintLedger(); }
1815
+ return;
1816
+ }
858
1817
  var el = ev.target.closest("[data-dir]");
859
1818
  if (!el) return;
860
1819
  openDetail(el.getAttribute("data-dir"));
@@ -867,6 +1826,7 @@ window.__ModuleLoader__.load({
867
1826
  S.open = false;
868
1827
  S.detail = null;
869
1828
  S.batchMenuScope = null; // v1.7.2: 面板随 DOM 一起销毁,同步清确认菜单状态
1829
+ S.moveMenuDir = null; // v1.9.0: 同上,清"移动到分组"浮层状态
870
1830
  var p = $(PANEL_ID);
871
1831
  if (p && p.parentNode) p.parentNode.removeChild(p);
872
1832
  var st = $("dsb-panel-style");
@@ -887,27 +1847,197 @@ window.__ModuleLoader__.load({
887
1847
  panel.style.display = S.open ? "flex" : "none";
888
1848
  if (!S.open) return;
889
1849
  var isLedger = S.view === "ledger";
890
- var q = $("dsb-q"), chips = $("dsb-chips"), subs = $("dsb-subs"), cards = $("dsb-cards"), lg = $("dsb-ledger"), sortbar = $("dsb-sortbar");
1850
+ var q = $("dsb-q"), chips = $("dsb-chips"), subs = $("dsb-subs"), cards = $("dsb-cards"), lg = $("dsb-ledger"), sortbar = $("dsb-sortbar"), groups = $("dsb-groups");
891
1851
  if (q) q.style.display = isLedger ? "none" : "";
1852
+ if (groups) groups.style.display = isLedger ? "none" : "";
892
1853
  if (chips) chips.style.display = isLedger ? "none" : "";
893
1854
  if (subs) subs.style.display = isLedger ? "none" : "";
894
1855
  if (cards) cards.style.display = isLedger ? "none" : "";
895
1856
  if (sortbar) sortbar.style.display = isLedger ? "none" : "";
896
- if (lg) lg.style.display = isLedger ? "block" : "none";
1857
+ if (lg) lg.style.display = isLedger ? "flex" : "none";
897
1858
  var t1 = $("dsb-tab-skills"), t2 = $("dsb-tab-ledger");
898
1859
  if (t1) t1.classList.toggle("dsb-tab-on", !isLedger);
899
1860
  if (t2) t2.classList.toggle("dsb-tab-on", isLedger);
900
1861
  paintHeader();
901
1862
  if (isLedger) paintLedger();
902
- else { paintSortbar(); paintChips(); paintSubs(); paintCards(); }
1863
+ else { paintGroups(); paintSortbar(); paintChips(); paintSubs(); paintCards(); }
903
1864
  paintDetail();
904
1865
  }
905
1866
 
1867
+ // v1.9.0 分组栏 / v1.9.3 两级:全部 / 未分组 / 大类(含子类计数)
1868
+ // 选中大类后,下方再出一条子类栏(点"整个大类"看全部,点某个子类只看它)
1869
+ // ── v1.9.8 分组栏:两种显示模式 ──
1870
+ // tree(默认):每个大类独占一行,子类**常驻**缩进挂在该行下方(竖引导线),
1871
+ // 一眼看出「哪个大类下有哪些子类」,像文件树。
1872
+ // flat(原样):大类一排小卡片 flex-wrap,选中大类后才另起一行显示子类。
1873
+ // 工具条两种模式共用:全部 / 未分组 / ⚡关键词分组 / 🤖AI自动分组 / +新建 / 切换模式。
1874
+ function paintGroups() {
1875
+ var box = $("dsb-groups"); if (!box) return;
1876
+ if (!S.data) { box.innerHTML = ""; return; }
1877
+ var sk = S.data.skills || [];
1878
+ var total = sk.length;
1879
+ var ungrouped = 0;
1880
+ for (var i = 0; i < sk.length; i++) { if (!S.gassign[sk[i].dir]) ungrouped++; }
1881
+ var doms = topGroups();
1882
+ var treeMode = S.gmode !== "flat";
1883
+
1884
+ // ① 工具条
1885
+ var bar = '<div class="dsb-gbar">';
1886
+ bar += '<span class="dsb-gtitle" title="' + esc(T("groupsHint")) + '">' + esc(T("groupsTitle")) + "</span>";
1887
+ bar += '<button class="dsb-gadd' + (S.group === "all" ? " dsb-gadd-on" : "") + '" data-gsel="all">' +
1888
+ esc(T("groupAll")) + ' <span class="dsb-gn">' + total + "</span></button>";
1889
+ bar += '<button class="dsb-gadd' + (S.group === "ungrouped" ? " dsb-gadd-on" : "") + '" data-gsel="ungrouped">' +
1890
+ esc(T("groupUngrouped")) + ' <span class="dsb-gn">' + ungrouped + "</span></button>";
1891
+ bar += '<span class="dsb-gsp"></span>';
1892
+ bar += '<button class="dsb-gadd' + (treeMode ? " dsb-gadd-on" : "") + '" data-gmode="tree" title="' +
1893
+ esc(T("groupModeTreeTip")) + '">' + esc(T("groupModeTree")) + "</button>";
1894
+ bar += '<button class="dsb-gadd' + (treeMode ? "" : " dsb-gadd-on") + '" data-gmode="flat" title="' +
1895
+ esc(T("groupModeFlatTip")) + '">' + esc(T("groupModeFlat")) + "</button>";
1896
+ bar += '<button class="dsb-gadd dsb-gadd-kw' + (S.groupBusy === "kw-classify" ? " dsb-gbusy" : "") +
1897
+ '" data-gadd="kw" title="' + esc(T("groupKwTip")) + '">' +
1898
+ esc(S.groupBusy === "kw-classify" ? T("processingText") : T("groupKw")) + "</button>";
1899
+ bar += '<button class="dsb-gadd' + (S.groupBusy === "ai-classify" ? " dsb-gbusy" : "") +
1900
+ '" data-gadd="ai" title="' + esc(T("groupAiTip")) + '">' +
1901
+ esc(S.groupBusy === "ai-classify" ? T("processingText") : T("groupAi")) + "</button>";
1902
+ bar += '<button class="dsb-gadd" data-gadd="new">' + esc(T("groupNew")) + "</button>";
1903
+ // v1.9.9 模型探针:一眼看出 AI 分组会调哪个模型(零配置可用性自检)
1904
+ var mi = S.modelInfo;
1905
+ var miText, miCls, miTip;
1906
+ if (!mi) {
1907
+ miText = "🔍 " + esc(T("modelProbeTip"));
1908
+ miCls = " dsb-gmodel-idle";
1909
+ miTip = T("modelProbeTip");
1910
+ } else if (mi.ok) {
1911
+ miText = "🤖 " + esc(TF("modelUsing", { model: mi.model })) +
1912
+ (mi.source && mi.source !== "config" ? ' <span class="dsb-gn">' + esc(T(modelSourceKey(mi.source))) + "</span>" : "");
1913
+ miCls = " dsb-gmodel-ok";
1914
+ miTip = (mi.provider || "") + " / " + (mi.model || "") + "(" + (mi.source || "") + ")";
1915
+ } else {
1916
+ miText = "⚠ " + esc(T("modelNone"));
1917
+ miCls = " dsb-gmodel-bad";
1918
+ miTip = mi.error || T("modelNone");
1919
+ }
1920
+ bar += '<span class="dsb-gsp"></span>';
1921
+ bar += '<span class="dsb-gmodel' + miCls + '" data-gmodel="1" title="' + esc(miTip) + '">' + miText + "</span>";
1922
+ bar += "</div>";
1923
+
1924
+ var body = "";
1925
+ if (treeMode) {
1926
+ // ② 树状:每个大类一行 + 常驻子类行
1927
+ body = '<div class="dsb-gtree">';
1928
+ for (var j = 0; j < doms.length; j++) {
1929
+ var g = doms[j];
1930
+ var kids = subsOf(g.id);
1931
+ var n = domainCount(g);
1932
+ var on = S.group === g.id;
1933
+ var busy = S.groupBusy === "ai-cover:" + g.id;
1934
+ body += '<div class="dsb-gitem">';
1935
+ body += '<div class="dsb-grow' + (on ? " dsb-grow-on" : "") + (busy ? " dsb-gbusy" : "") +
1936
+ '" data-gsel="' + esc(g.id) + '"' + groupCoverStyle(g) + ' title="' +
1937
+ esc(g.name + " · " + n + " 项" + (kids.length ? " · " + kids.length + " 个子类" : "")) + '">' +
1938
+ '<span class="dsb-gico">' + esc(g.emoji || "📂") + "</span>" +
1939
+ '<span class="dsb-grname">' + esc(g.name) + "</span>" +
1940
+ '<span class="dsb-grmeta">' + n + " 项" + (kids.length ? " · " + kids.length + " 子类" : "") + "</span>" +
1941
+ '<span class="dsb-grsp"></span>' +
1942
+ '<span class="dsb-grops">' +
1943
+ '<button class="dsb-gop" data-gop="cover" data-gid="' + esc(g.id) + '" title="' + esc(T("groupCover")) + '">🎨</button>' +
1944
+ '<button class="dsb-gop" data-gop="sub" data-gid="' + esc(g.id) + '" title="' + esc(T("groupNewSub")) + '">+</button>' +
1945
+ '<button class="dsb-gop" data-gop="rename" data-gid="' + esc(g.id) + '" title="' + esc(T("groupRename")) + '">✏️</button>' +
1946
+ '<button class="dsb-gop" data-gop="del" data-gid="' + esc(g.id) + '" title="' + esc(T("groupDelete")) + '">🗑</button>' +
1947
+ "</span>" +
1948
+ "</div>";
1949
+ // 子类行常驻:选中该大类时把「整个大类」作为第一个可选项。
1950
+ // 用 ├─ / └─ 连接符做成真正的树枝,每行一个子类,层层展开。
1951
+ if (kids.length || on) {
1952
+ var rows = [];
1953
+ if (on) rows.push({ id: "", label: T("groupWhole"), emoji: "", n: n, tip: T("groupWhole") });
1954
+ for (var k = 0; k < kids.length; k++) {
1955
+ var sg = kids[k];
1956
+ rows.push({
1957
+ id: sg.id,
1958
+ label: sg.name,
1959
+ emoji: sg.emoji || "📁",
1960
+ n: (sg.members || []).length,
1961
+ tip: sg.name + " · " + (sg.members || []).length + " 项"
1962
+ });
1963
+ }
1964
+ body += '<div class="dsb-gsubrow">';
1965
+ for (var r = 0; r < rows.length; r++) {
1966
+ var it = rows[r];
1967
+ var last = r === rows.length - 1;
1968
+ var sOn = it.id ? S.gsub === it.id : (!S.gsub && on);
1969
+ body += '<div class="dsb-gline' + (last ? " dsb-gline-last" : "") + '">' +
1970
+ '<span class="dsb-gconn">' + (last ? "└─" : "├─") + "</span>" +
1971
+ '<button class="dsb-gsub' + (sOn ? " dsb-gsub-on" : "") +
1972
+ '" data-gsub="' + esc(it.id) + '" title="' + esc(it.tip) + '">' +
1973
+ (it.emoji ? esc(it.emoji) + " " : "") + esc(it.label) +
1974
+ ' <span class="dsb-gn">' + it.n + "</span>" +
1975
+ "</button>" +
1976
+ "</div>";
1977
+ }
1978
+ body += "</div>";
1979
+ }
1980
+ body += "</div>";
1981
+ }
1982
+ body += "</div>";
1983
+ if (!doms.length) {
1984
+ body = '<div class="dsb-gtree"><span class="dsb-gtitle">' + esc(T("groupEmpty")) + "</span></div>";
1985
+ }
1986
+ } else {
1987
+ // ③ 平铺(原样)
1988
+ body = '<div class="dsb-gflat">';
1989
+ for (var m = 0; m < doms.length; m++) {
1990
+ var gf = doms[m];
1991
+ var kidsF = subsOf(gf.id);
1992
+ var nF = domainCount(gf);
1993
+ var onF = S.group === gf.id;
1994
+ var busyF = S.groupBusy === "ai-cover:" + gf.id;
1995
+ body += '<div class="dsb-gcard dsb-gcard-dom' + (onF ? " dsb-gcard-on" : "") + (busyF ? " dsb-gbusy" : "") +
1996
+ '" data-gsel="' + esc(gf.id) + '"' + groupCoverStyle(gf) + ' title="' +
1997
+ esc(gf.name + " · " + nF + " 项" + (kidsF.length ? " · " + kidsF.length + " 个子类" : "")) + '">' +
1998
+ '<div class="dsb-gname">' + esc((gf.emoji || "📂") + " " + gf.name) +
1999
+ (kidsF.length ? '<span class="dsb-gkids">' + kidsF.length + " 子类</span>" : "") + "</div>" +
2000
+ '<div class="dsb-gn">' + nF + " 项</div>" +
2001
+ '<div class="dsb-gops">' +
2002
+ '<button class="dsb-gop" data-gop="cover" data-gid="' + esc(gf.id) + '" title="' + esc(T("groupCover")) + '">🎨</button>' +
2003
+ '<button class="dsb-gop" data-gop="sub" data-gid="' + esc(gf.id) + '" title="' + esc(T("groupNewSub")) + '">+</button>' +
2004
+ '<button class="dsb-gop" data-gop="rename" data-gid="' + esc(gf.id) + '" title="' + esc(T("groupRename")) + '">✏️</button>' +
2005
+ '<button class="dsb-gop" data-gop="del" data-gid="' + esc(gf.id) + '" title="' + esc(T("groupDelete")) + '">🗑</button>' +
2006
+ "</div>" +
2007
+ "</div>";
2008
+ }
2009
+ body += "</div>";
2010
+ if (!doms.length) body += '<span class="dsb-gtitle">' + esc(T("groupEmpty")) + "</span>";
2011
+ // 平铺模式:选中大类后才显示子类栏
2012
+ if (S.group !== "all" && S.group !== "ungrouped") {
2013
+ var cur = groupById(S.group);
2014
+ if (cur && isDomain(cur)) {
2015
+ var kids2 = subsOf(cur.id);
2016
+ if (kids2.length) {
2017
+ body += '<div class="dsb-gsubs"><span class="dsb-gtitle">└ ' + esc(T("groupSubsOf")) + "</span>";
2018
+ body += '<button class="dsb-gadd' + (S.gsub ? "" : " dsb-gadd-on") + '" data-gsub="">' +
2019
+ esc(T("groupWhole")) + ' <span class="dsb-gn">' + domainCount(cur) + "</span></button>";
2020
+ for (var q = 0; q < kids2.length; q++) {
2021
+ var sg2 = kids2[q];
2022
+ var sn2 = (sg2.members || []).length;
2023
+ body += '<button class="dsb-gadd' + (S.gsub === sg2.id ? " dsb-gadd-on" : "") +
2024
+ '" data-gsub="' + esc(sg2.id) + '" title="' + esc(sg2.name + " · " + sn2 + " 项") + '">' +
2025
+ esc((sg2.emoji || "📁") + " " + sg2.name) + ' <span class="dsb-gn">' + sn2 + "</span></button>";
2026
+ }
2027
+ body += "</div>";
2028
+ }
2029
+ }
2030
+ }
2031
+ }
2032
+
2033
+ box.innerHTML = bar + body;
2034
+ }
2035
+
906
2036
  function paintHeader() {
907
2037
  var sub = $("dsb-subtitle");
908
2038
  if (!sub) return;
909
2039
  if (S.loading && !S.data) { sub.textContent = "扫描技能目录中…"; return; }
910
- if (S.error) { sub.innerHTML = '<span style="color:#ff8f8f">' + esc(S.error) + '</span>'; return; }
2040
+ if (S.error) { sub.innerHTML = '<span style="color:var(--dsb-fail,#ff8f8f)">' + esc(S.error) + '</span>'; return; }
911
2041
  if (!S.data) return;
912
2042
  var offCount = 0;
913
2043
  var sk = S.data.skills || [];
@@ -1071,6 +2201,48 @@ window.__ModuleLoader__.load({
1071
2201
  // v1.7.1:面板内 toast 轻提示(右上角浮层,2 秒自动消失)。
1072
2202
  // kind="ok" 绿 / "err" 红;新提示覆盖旧提示并重置 2 秒计时;
1073
2203
  // 面板未构建/已关闭时元素不存在,静默跳过。
2204
+ // ── v1.9.4 热加载 ────────────────────────────────────────────
2205
+ // host 半:POST /reload → 宿主用 ?v=<ts> 重新 import 自己的模块,
2206
+ // 换掉正在服务的 handler(gen 自增),并让 client-modules 重读
2207
+ // client.js;随后本函数刷新页面,界面即是最新代码。
2208
+ // 不重启桌面端、不中断会话。
2209
+ function hotReloadFlow() {
2210
+ if (S.hotBusy) return;
2211
+ S.hotBusy = true;
2212
+ showToast("ok", T("hotReloadRunning"));
2213
+ var done = false;
2214
+ function finish(kind, text, refresh) {
2215
+ if (done) return;
2216
+ done = true;
2217
+ S.hotBusy = false;
2218
+ showToast(kind, text);
2219
+ if (refresh) setTimeout(function () { try { location.reload(); } catch (e) {} }, 700);
2220
+ }
2221
+ fetch(apiBase() + "/reload", { method: "POST" })
2222
+ .then(function (r) { return r.json(); })
2223
+ .then(function (d) {
2224
+ if (!d || d.ok !== true) throw new Error((d && d.error) || "reload rejected");
2225
+ // 轮询 /rev 等宿主半真的换新(gen 变了),再刷新界面
2226
+ var tries = 0;
2227
+ (function poll() {
2228
+ tries += 1;
2229
+ fetch(apiBase() + "/rev")
2230
+ .then(function (r) { return r.json(); })
2231
+ .then(function (v) {
2232
+ if (v && v.gen > d.gen) {
2233
+ finish("ok", TF("hotReloadDone", { v: v.version || "?", n: v.gen }), true);
2234
+ } else if (tries < 25) {
2235
+ setTimeout(poll, 200);
2236
+ } else {
2237
+ finish("err", TF("hotReloadFail", { e: "超时:宿主半未换新(可点 ↻ 刷新数据)" }), false);
2238
+ }
2239
+ })
2240
+ .catch(function () { if (tries < 25) setTimeout(poll, 200); else finish("err", TF("hotReloadFail", { e: "轮询失败" }), false); });
2241
+ })();
2242
+ })
2243
+ .catch(function (e) { finish("err", TF("hotReloadFail", { e: (e && e.message) || String(e) }), false); });
2244
+ }
2245
+
1074
2246
  function showToast(kind, text) {
1075
2247
  var el = $("dsb-toast");
1076
2248
  if (!el) return;
@@ -1287,23 +2459,43 @@ window.__ModuleLoader__.load({
1287
2459
  }
1288
2460
  if (!S.data) { box.innerHTML = '<div class="dsb-empty">加载中…</div>'; return; }
1289
2461
  var list = filtered();
2462
+ // v1.9.6 过滤条件变化时重置分片游标(否则切换分类后只看到旧的 60 条)
2463
+ var sig = S.cat + "|" + S.sub + "|" + S.q + "|" + S.sortMode + "|" + S.group + "|" + S.gsub + "|" + list.length;
2464
+ if (S.cardSig !== sig) { S.cardSig = sig; S.cardLimit = 60; }
1290
2465
  // v1.7.1 四态排序:名称↑(默认)/名称↓/时间↓(最新在前)/时间↑(最旧在前)
1291
2466
  list = sortList(list);
1292
2467
  if (!list.length) { box.innerHTML = '<div class="dsb-empty">没有匹配的技能</div>'; return; }
2468
+ // v1.9.6 分片:只渲染前 cardLimit 张,滚动到底再追加,避免一次性生成上千节点
2469
+ var shown = list.slice(0, S.cardLimit);
2470
+ var restN = list.length - shown.length;
1293
2471
  var html = "";
1294
- for (var i = 0; i < list.length; i++) {
1295
- var s = list[i];
2472
+ for (var i = 0; i < shown.length; i++) {
2473
+ var s = shown[i];
1296
2474
  var tag = s.sub ? esc(subLabel(s.cat, s.sub)) : esc(catLabel(s.cat));
1297
2475
  var brief = s.zh || (s.desc ? String(s.desc).slice(0, 90) + (s.desc.length > 90 ? "…" : "") : "(无描述)");
1298
2476
  var off = !!s.disabled;
1299
2477
  var mt = fmtTime(s.mtime);
2478
+ // v1.9.0 版本号:有 version 显示绿色徽章,没有显示灰色「暂未编版本号」
2479
+ var ver = String(s.version == null ? "" : s.version).trim();
2480
+ var verHtml = ver
2481
+ ? '<span class="dsb-ver" title="' + esc(T("versionLabel")) + " " + esc(ver) + '">v' + esc(ver) + "</span>"
2482
+ : '<span class="dsb-ver dsb-ver-none" title="' + esc(T("versionTip")) + '">' + esc(T("versionNone")) + "</span>";
2483
+ // v1.9.0 当前所属分组(视图层标签,与磁盘位置无关)
2484
+ var gid = groupOfDir(s.dir);
2485
+ var g = gid ? groupById(gid) : null;
2486
+ var gTag = g ? '<div class="dsb-card-gtag">📁 ' + esc(g.name) + "</div>" : "";
1300
2487
  // v1.7.1 滑动开关:.dsb-sw-on=开启(绿轨+滑块右),无=停用(灰轨+滑块左)。
1301
2488
  // 状态唯一来源是 s.disabled;title 提示点击后的动作方向(off→点击启用)。
1302
2489
  html += '<div class="dsb-card' + (off ? " dsb-card-off" : "") + '" data-dir="' + esc(s.dir) + '" title="' + esc(off ? T("cardDisabledTitle") : "点击查看 SKILL.md 全文") + '">' +
1303
- '<div class="dsb-card-name">' + esc(s.dir) + (off ? '<span class="dsb-badge-off">' + esc(T("disabledBadge")) + '</span>' : "") + "</div>" +
2490
+ '<div class="dsb-card-name">' + esc(s.dir) + verHtml + (off ? '<span class="dsb-badge-off">' + esc(T("disabledBadge")) + '</span>' : "") + "</div>" +
1304
2491
  '<div class="dsb-card-zh">' + esc(brief) + "</div>" +
1305
2492
  '<div class="dsb-card-tag">' + tag + "</div>" +
1306
2493
  (mt ? '<div class="dsb-card-mtime">' + esc(T("mtimeLabel")) + " " + esc(mt) + "</div>" : "") +
2494
+ '<div class="dsb-card-foot">' +
2495
+ gTag +
2496
+ '<button class="dsb-movebtn" data-move="' + esc(s.dir) + '" title="' + esc(T("groupAssignTip")) + '">' +
2497
+ "📁 " + esc(T("groupMove")) + "</button>" +
2498
+ "</div>" +
1307
2499
  '<div style="margin-top:6px">' +
1308
2500
  '<label class="dsb-switch' + (off ? "" : " dsb-sw-on") + '" data-tgl="' + esc(s.dir) + '" data-to="' + (off ? "0" : "1") +
1309
2501
  '" title="' + esc(off ? T("toggleEnableTip") : T("toggleDisableTip")) + '"' + (S.toggling ? ' aria-disabled="true"' : "") + '>' +
@@ -1317,7 +2509,13 @@ window.__ModuleLoader__.load({
1317
2509
  // 技能列表/分类末尾恢复为纯技能卡列表(原 v1.7.2 追加在列表尾部的
1318
2510
  // 3 张推荐卡——🖼️照片显示器/🔤字体插件/💡悬浮球说明——已整体迁往
1319
2511
  // 设置面板 #dsb-settings 广告区,见 buildPanel 内注释)。
2512
+ if (restN > 0) {
2513
+ html += '<div class="dsb-empty" style="grid-column:1/-1;padding:10px 0">' +
2514
+ '<button class="dsb-btn" data-more="cards">' + esc(TF("cardMore", { n: restN })) + "</button></div>";
2515
+ }
2516
+ var keepTop = box.scrollTop;
1320
2517
  box.innerHTML = html;
2518
+ if (keepTop) box.scrollTop = keepTop;
1321
2519
  }
1322
2520
 
1323
2521
  function ledgerLevel(fails) {
@@ -1326,75 +2524,215 @@ window.__ModuleLoader__.load({
1326
2524
  return { cls: "dsb-lv-low", label: "低" };
1327
2525
  }
1328
2526
 
2527
+ // ── v1.9.1 调用台账看板:工具栏 → 指标卡 → 趋势柱 → 排行榜 → 原始流水 ──
1329
2528
  function paintLedger() {
1330
- var sum = $("dsb-ledger-sum"), tbl = $("dsb-ledger-table");
1331
- if (!sum || !tbl) return;
1332
- if (S.loading && !S.ledger) { sum.innerHTML = '<div class="dsb-empty">台账加载中…</div>'; tbl.innerHTML = ""; return; }
1333
- if (S.error && !S.ledger) { sum.innerHTML = '<div class="dsb-empty">' + esc(S.error) + '</div>'; tbl.innerHTML = ""; return; }
1334
- if (!S.ledger) return;
1335
- var l = S.ledger;
1336
- var totalSkills = (S.data && S.data.total) || 0;
1337
- var autoRecent = l.autoRecent || [];
1338
- var autoFails = autoRecent.filter(function (x) { return !x.ok; }).length;
1339
- var autoTotal = l.autoTotal || 0;
1340
- var autoFailTotal = l.autoFailTotal || 0;
1341
- sum.innerHTML =
1342
- '<div class="dsb-lsum-card"><b>' + totalSkills + '</b><span>技能总数</span></div>' +
1343
- '<div class="dsb-lsum-card dsb-lsum-fail"><b>' + l.totalFailSkills + '</b><span>人工·失败技能</span></div>' +
1344
- '<div class="dsb-lsum-card dsb-lsum-fail"><b>' + l.totalFails + '</b><span>人工·累计失败</span></div>' +
1345
- '<div class="dsb-lsum-card"><b>' + autoTotal + '</b><span>自动·调用总数</span></div>' +
1346
- (autoFailTotal ? '<div class="dsb-lsum-card dsb-lsum-fail"><b>' + autoFailTotal + '</b><span>自动·失败</span></div>' : '');
1347
- var rows = (l.entries || []).filter(function (e) { return e.fails > 0; });
1348
- var html = '<div style="display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;margin:2px 0 6px">' +
1349
- '<span>👤 人工台账</span>' +
1350
- '<span style="font-weight:400;font-size:11px;color:#9aa1b5">技能生效-失效台账.md 统计表(人工维护 + 历史登记)</span>' +
1351
- '</div>' +
1352
- '<div class="dsb-ltable">' +
1353
- '<div class="dsb-lrow dsb-lhead"><span class="dsb-lc-rank">#</span><span class="dsb-lc-skill">技能名</span><span class="dsb-lc-fail">失败</span><span class="dsb-lc-rate">等级</span><span class="dsb-lc-date">最近失败</span><span class="dsb-lc-status">强化状态</span></div>';
1354
- if (!rows.length) {
1355
- html += '<div class="dsb-lrow"><span class="dsb-lc-skill" style="opacity:.7;line-height:1.8">暂无人工登记。<br/>💡 技能触发失败时,在对话里说「<b>触发失败,登记</b>」或「<b>登记失败:技能名 原因</b>」,AI 会自动写入台账;<br/>插件也会自动记录每次技能调用(见下方 🤖 自动登记)。</span></div>';
2529
+ var bar = $("dsb-lbar"), cards = $("dsb-lcards"), trend = $("dsb-ltrend"),
2530
+ rank = $("dsb-lrank"), sum = $("dsb-ledger-sum");
2531
+ if (!bar || !cards || !trend || !rank || !sum) return;
2532
+
2533
+ // ---- 1) 工具栏:快捷区间 + 起止日期 + 粒度 ----
2534
+ var QUICK = [
2535
+ { k: "today", t: T("lstatQuickToday") },
2536
+ { k: "7", t: T("lstatQuick7") },
2537
+ { k: "30", t: T("lstatQuick30") },
2538
+ { k: "month", t: T("lstatQuickMonth") },
2539
+ { k: "prev", t: T("lstatQuickPrevMonth") },
2540
+ { k: "year", t: T("lstatQuickYear") },
2541
+ { k: "all", t: T("lstatQuickAll") }
2542
+ ];
2543
+ var gh = "";
2544
+ for (var qi = 0; qi < QUICK.length; qi++) {
2545
+ gh += '<button class="dsb-lq" data-lq="' + QUICK[qi].k + '">' + esc(QUICK[qi].t) + "</button>";
2546
+ }
2547
+ var dFirst = (S.lstat && S.lstat.dataFirst) || "";
2548
+ var dLast = (S.lstat && S.lstat.dataLast) || "";
2549
+ var rangeHint = dFirst ? TF("lstatDataRange", { from: dFirst, to: dLast }) : T("lstatNoData");
2550
+ bar.innerHTML = gh +
2551
+ '<span class="dsb-lsep"></span>' +
2552
+ '<span style="font-size:10.5px;color:var(--dsb-dim,#8b93a8)">' + esc(T("lstatFrom")) + "</span>" +
2553
+ '<input id="dsb-lfrom" class="dsb-ldate" type="date" value="' + esc(S.lfrom) + '" />' +
2554
+ '<span style="font-size:10.5px;color:var(--dsb-dim,#8b93a8)">' + esc(T("lstatTo")) + "</span>" +
2555
+ '<input id="dsb-lto" class="dsb-ldate" type="date" value="' + esc(S.lto) + '" />' +
2556
+ '<span class="dsb-lsep"></span>' +
2557
+ '<span style="font-size:10.5px;color:var(--dsb-dim,#8b93a8)">' + esc(T("lstatGran")) + "</span>" +
2558
+ '<button class="dsb-lq' + (S.lgran === "day" ? " dsb-lq-on" : "") + '" data-lgran="day">' + esc(T("lstatGranDay")) + "</button>" +
2559
+ '<button class="dsb-lq' + (S.lgran === "week" ? " dsb-lq-on" : "") + '" data-lgran="week">' + esc(T("lstatGranWeek")) + "</button>" +
2560
+ '<button class="dsb-lq' + (S.lgran === "month" ? " dsb-lq-on" : "") + '" data-lgran="month">' + esc(T("lstatGranMonth")) + "</button>" +
2561
+ '<button class="dsb-lq' + (S.lgran === "year" ? " dsb-lq-on" : "") + '" data-lgran="year">' + esc(T("lstatGranYear")) + "</button>" +
2562
+ '<span class="dsb-lhint">' + esc(rangeHint) + "</span>";
2563
+
2564
+ // ---- 加载/错误态 ----
2565
+ if (S.lstatLoading && !S.lstat) {
2566
+ cards.innerHTML = "";
2567
+ trend.innerHTML = '<div class="dsb-empty" style="width:100%">' + esc(T("lstatLoading")) + "</div>";
2568
+ rank.innerHTML = "";
2569
+ sum.innerHTML = "";
2570
+ return;
1356
2571
  }
1357
- for (var i = 0; i < rows.length; i++) {
1358
- var e = rows[i];
1359
- var lv = ledgerLevel(e.fails);
1360
- var status = e.status || "";
1361
- var statusCls = /待强化|⚠/.test(status) ? "dsb-lst-warn" : /已强化/.test(status) ? "dsb-lst-done" : "dsb-lst-ok";
1362
- html += '<div class="dsb-lrow" data-lskill="' + esc(e.skill) + '" title="点击查看失败明细">' +
1363
- '<span class="dsb-lc-rank">' + esc(e.rank || "—") + '</span>' +
1364
- '<span class="dsb-lc-skill">' + esc(e.skill) + '</span>' +
1365
- '<span class="dsb-lc-fail">' + e.fails + '</span>' +
1366
- '<span class="dsb-lc-rate"><span class="' + lv.cls + '">' + lv.label + '</span></span>' +
1367
- '<span class="dsb-lc-date">' + esc(e.lastDate || "—") + '</span>' +
1368
- '<span class="dsb-lc-status ' + statusCls + '">' + esc(status) + '</span>' +
1369
- '</div>';
2572
+ if (S.lstatErr && !S.lstat) {
2573
+ cards.innerHTML = "";
2574
+ trend.innerHTML = "";
2575
+ rank.innerHTML = '<div class="dsb-empty">' + esc(TF("lstatFail", { err: S.lstatErr })) + "</div>";
2576
+ sum.innerHTML = "";
2577
+ return;
1370
2578
  }
1371
- html += '</div>';
1372
- // 自动登记流水(与人工台账分开的独立区块,最新在上)
1373
- var auto = l.autoRecent || [];
1374
- html += '<div style="display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;margin:14px 0 6px;padding-top:10px;border-top:1px solid rgba(255,255,255,.08)">' +
1375
- '<span>🤖 自动登记</span>' +
1376
- '<span style="font-weight:400;font-size:11px;color:#9aa1b5">dsh-skill-browser 插件监听 skill 工具调用结果自动记录 · 近 ' + auto.length + ' 条,最新在前</span>' +
1377
- '</div>';
1378
- if (!auto.length) {
1379
- html += '<div class="dsb-empty" style="padding:14px 0">暂无自动记录 — 插件会记录每次技能工具调用结果(成功✅ / 失败❌)</div>';
2579
+ if (!S.lstat) { loadStats(); return; }
2580
+
2581
+ var st = S.lstat;
2582
+ var tt = st.totals || {};
2583
+
2584
+ // ---- 2) 指标卡 ----
2585
+ cards.innerHTML =
2586
+ '<div class="dsb-lcard"><b>' + (tt.calls || 0) + "</b><span>" + esc(T("lstatCalls")) + "</span></div>" +
2587
+ '<div class="dsb-lcard"><b>' + (tt.skills || 0) + "</b><span>" + esc(T("lstatSkills")) + "</span></div>" +
2588
+ '<div class="dsb-lcard"><b>' + (tt.days || 0) + "</b><span>" + esc(T("lstatDays")) + "</span></div>" +
2589
+ '<div class="dsb-lcard dsb-lcard-rate"><b>' + (tt.rate || 0) + "%</b><span>" + esc(T("lstatRate")) + "</span></div>" +
2590
+ '<div class="dsb-lcard' + (tt.fail ? " dsb-lcard-fail" : "") + '"><b>' + (tt.fail || 0) + "</b><span>" + esc(T("lstatColFail")) + "</span></div>" +
2591
+ '<div class="dsb-lcard"><b>' + (tt.avgPerDay || 0) + "</b><span>" + esc(T("lstatAvg")) + "</span></div>";
2592
+
2593
+ // ---- 3) 趋势柱(纯 CSS,无外部图表库)----
2594
+ var bk = st.buckets || [];
2595
+ if (!bk.length) {
2596
+ trend.innerHTML = '<div class="dsb-empty" style="width:100%">' +
2597
+ esc(tt.calls ? T("lstatEmptyRange") : (st.dataLast ? T("lstatEmptyRange") : T("lstatEmptyAll"))) + "</div>";
1380
2598
  } else {
1381
- html += '<div class="dsb-ltable">';
1382
- for (var a = 0; a < auto.length; a++) {
1383
- var x = auto[a];
1384
- var mark = x.ok ? "✅" : "❌";
1385
- var cls = x.ok ? "dsb-lst-done" : "dsb-lst-warn";
1386
- var note = x.note || x.error || "";
1387
- html += '<div class="dsb-lrow" style="font-size:11.5px' + (x.ok ? ';opacity:.62' : '') + '"' + (x.ok ? '' : ' data-lskill="' + esc(x.skill) + '" title="点击查看失败详情"') + '>' +
1388
- '<span class="dsb-lc-date" style="width:110px;flex:none">' + esc(String(x.ts || "").replace("T", " ").slice(5, 16)) + '</span>' +
1389
- '<span class="' + cls + '" style="width:22px;flex:none;text-align:center">' + mark + '</span>' +
1390
- '<span class="dsb-lc-skill" style="flex:1">' + esc(x.skill) + '</span>' +
1391
- '<span style="width:64px;flex:none;color:#8b93a8;font-size:10.5px">' + esc(x.kind === "manual-report" ? "人工上报" : "工具结果") + '</span>' +
1392
- '<span style="flex:2;color:#9aa1b5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(x.kind || "") + (note ? " — " + esc(note.slice(0, 80)) : "") + '</span>' +
1393
- '</div>';
2599
+ var maxC = 0;
2600
+ for (var bi = 0; bi < bk.length; bi++) if (bk[bi].calls > maxC) maxC = bk[bi].calls;
2601
+ if (maxC <= 0) maxC = 1;
2602
+ var th = "";
2603
+ for (var bj = 0; bj < bk.length; bj++) {
2604
+ var b = bk[bj];
2605
+ var pct = Math.round((b.calls / maxC) * 100);
2606
+ var cls = b.calls === 0 ? " dsb-lbarfill-zero" : (b.fail > 0 && b.fail === b.calls ? " dsb-lbarfill-fail" : "");
2607
+ var tip = b.label + " · " + b.calls + " " + T("lstatCalls") + (b.fail ? " · " + b.fail + " " + T("lstatColFail") : "");
2608
+ th += '<div class="dsb-lbarcol" title="' + esc(tip) + '">' +
2609
+ '<div class="dsb-lbarfill' + cls + '" style="height:' + pct + '%"></div></div>';
2610
+ }
2611
+ trend.innerHTML = th;
2612
+ }
2613
+
2614
+ // ---- 4) 排行榜 ----
2615
+ var ranks = st.ranks || [];
2616
+ var SORTS = [
2617
+ { k: "calls", t: T("lstatColCalls") },
2618
+ { k: "fails", t: T("lstatColFail") },
2619
+ { k: "rate", t: T("lstatColRate") },
2620
+ { k: "last", t: T("lstatColLast") },
2621
+ { k: "name", t: T("lstatColSkill") }
2622
+ ];
2623
+ var rh = '<div class="dsb-lsec"><span>📈 ' + esc(T("lstatRank")) + "</span>" +
2624
+ '<span class="sub">' + esc(T("lstatSortTip")) + "</span></div>" +
2625
+ '<div class="dsb-lrrow dsb-lrhead">' +
2626
+ '<span class="dsb-lrk">' + esc(T("lstatColRank")) + "</span>" +
2627
+ '<span data-lsort="name" class="' + (S.lsort === "name" ? "dsb-lr-sorton" : "") + '">' + esc(T("lstatColSkill")) + "</span>" +
2628
+ '<span style="text-align:center">' + esc(T("lstatColVer")) + "</span>" +
2629
+ '<span data-lsort="calls" style="text-align:right" class="' + (S.lsort === "calls" ? "dsb-lr-sorton" : "") + '">' + esc(T("lstatColCalls")) + "</span>" +
2630
+ '<span style="text-align:right">' + esc(T("lstatColOk")) + "</span>" +
2631
+ '<span data-lsort="fails" style="text-align:right" class="' + (S.lsort === "fails" ? "dsb-lr-sorton" : "") + '">' + esc(T("lstatColFail")) + "</span>" +
2632
+ '<span data-lsort="rate" style="text-align:right" class="' + (S.lsort === "rate" ? "dsb-lr-sorton" : "") + '">' + esc(T("lstatColRate")) + "</span>" +
2633
+ '<span data-lsort="last" style="text-align:right" class="' + (S.lsort === "last" ? "dsb-lr-sorton" : "") + '">' + esc(T("lstatColLast")) + "</span>" +
2634
+ "</div>";
2635
+
2636
+ if (!ranks.length) {
2637
+ rh += '<div class="dsb-empty" style="padding:16px 0">' +
2638
+ esc(st.dataLast ? T("lstatEmptyRange") : T("lstatEmptyAll")) + "</div>";
2639
+ } else {
2640
+ var maxR = ranks[0] && ranks[0].calls ? ranks[0].calls : 1;
2641
+ // 版本号来自 /skills 的 skills[].version
2642
+ var vmap = {};
2643
+ var sks = (S.data && S.data.skills) || [];
2644
+ for (var vi = 0; vi < sks.length; vi++) vmap[sks[vi].dir] = sks[vi].version || "";
2645
+
2646
+ for (var ri = 0; ri < ranks.length; ri++) {
2647
+ var x = ranks[ri];
2648
+ var ver = String(vmap[x.skill] == null ? "" : vmap[x.skill]).trim();
2649
+ var verHtml = ver
2650
+ ? '<span class="dsb-lrver" title="' + esc(T("versionLabel")) + " " + esc(ver) + '">v' + esc(ver) + "</span>"
2651
+ : '<span class="dsb-lrver dsb-lrver-none" title="' + esc(T("versionTip")) + '">' + esc(T("versionNone")) + "</span>";
2652
+ var lastTxt = String(x.last || "").replace("T", " ").slice(5, 16);
2653
+ var w = Math.max(2, Math.round((x.calls / maxR) * 100));
2654
+ rh += '<div class="dsb-lrrow"' + (x.fail ? ' data-lskill="' + esc(x.skill) + '" title="点击查看失败明细"' : "") + ">" +
2655
+ '<span class="dsb-lrk">' + x.rank + "</span>" +
2656
+ '<span class="dsb-lrname" title="' + esc(x.skill) + '">' + esc(x.skill) + "</span>" +
2657
+ verHtml +
2658
+ '<span class="dsb-lrn">' + x.calls + "</span>" +
2659
+ '<span class="dsb-lrn">' + x.ok + "</span>" +
2660
+ '<span class="dsb-lrf">' + (x.fail || 0) + "</span>" +
2661
+ '<span class="dsb-lrr" style="color:' + (x.rate >= 95 ? "var(--dsb-ok,#9ff5c8)" : x.rate >= 80 ? "var(--dsb-warn,#ffd27a)" : "var(--dsb-fail,#ff9d9d)") + '">' + x.rate + "%</span>" +
2662
+ '<span class="dsb-lrdate">' + esc(lastTxt || "—") + "</span>" +
2663
+ '<div class="dsb-lrbar"><i style="width:' + w + '%"></i></div>' +
2664
+ "</div>";
2665
+ }
2666
+ }
2667
+ rank.innerHTML = rh;
2668
+
2669
+ // ---- 5) 原始流水(人工台账 + 自动登记,折叠)----
2670
+ var l = S.ledger || {};
2671
+ var rows = (l.entries || []).filter(function (e) { return e.fails > 0; });
2672
+ var auto = l.autoRecent || [];
2673
+ var flowOpen = !!S.lstatShowFlow;
2674
+ var fh = '<div class="dsb-lsec"><span>🗂 ' + esc(T("lstatManual")) + "</span>" +
2675
+ '<span class="sub">技能生效-失效台账.md(人工维护 + 历史登记)</span>' +
2676
+ '<button class="dsb-ldetail-toggle" data-lflow="1" style="margin-left:auto">' +
2677
+ esc(flowOpen ? T("lstatExpand") : T("lstatCollapse")) + "</button></div>";
2678
+
2679
+ if (flowOpen) {
2680
+ fh += '<div class="dsb-ltable">' +
2681
+ '<div class="dsb-lrow dsb-lhead"><span class="dsb-lc-rank">#</span><span class="dsb-lc-skill">技能名</span>' +
2682
+ '<span class="dsb-lc-fail">失败</span><span class="dsb-lc-rate">等级</span>' +
2683
+ '<span class="dsb-lc-date">最近失败</span><span class="dsb-lc-status">强化状态</span></div>';
2684
+ if (!rows.length) {
2685
+ fh += '<div class="dsb-lrow"><span class="dsb-lc-skill" style="opacity:.7;line-height:1.8">' +
2686
+ "暂无人工登记。<br/>💡 技能触发失败时,在对话里说「<b>触发失败,登记</b>」或「<b>登记失败:技能名 原因</b>」,AI 会自动写入台账。</span></div>";
2687
+ }
2688
+ for (var i = 0; i < rows.length; i++) {
2689
+ var e = rows[i];
2690
+ var lv = ledgerLevel(e.fails);
2691
+ var status = e.status || "";
2692
+ var statusCls = /待强化|⚠/.test(status) ? "dsb-lst-warn" : /已强化/.test(status) ? "dsb-lst-done" : "dsb-lst-ok";
2693
+ fh += '<div class="dsb-lrow" data-lskill="' + esc(e.skill) + '" title="点击查看失败明细">' +
2694
+ '<span class="dsb-lc-rank">' + esc(e.rank || "—") + "</span>" +
2695
+ '<span class="dsb-lc-skill">' + esc(e.skill) + "</span>" +
2696
+ '<span class="dsb-lc-fail">' + e.fails + "</span>" +
2697
+ '<span class="dsb-lc-rate"><span class="' + lv.cls + '">' + lv.label + "</span></span>" +
2698
+ '<span class="dsb-lc-date">' + esc(e.lastDate || "—") + "</span>" +
2699
+ '<span class="dsb-lc-status ' + statusCls + '">' + esc(status) + "</span>" +
2700
+ "</div>";
2701
+ }
2702
+ fh += "</div>";
2703
+
2704
+ fh += '<div class="dsb-lsec"><span>🤖 ' + esc(T("lstatAuto")) + '</span><span class="sub">' +
2705
+ esc(T("lstatAutoTip")) + " · " + auto.length + " 条</span></div>";
2706
+ if (!auto.length) {
2707
+ fh += '<div class="dsb-empty" style="padding:12px 0">暂无自动记录 — 插件会记录每次技能工具调用结果(成功✅ / 失败❌)</div>';
2708
+ } else {
2709
+ fh += '<div class="dsb-ltable">';
2710
+ var autoShown = auto.slice(0, S.lflowLimit);
2711
+ var autoRest = auto.length - autoShown.length;
2712
+ for (var a = 0; a < autoShown.length; a++) {
2713
+ var y = autoShown[a];
2714
+ var mark = y.ok ? "✅" : "❌";
2715
+ var cls2 = y.ok ? "dsb-lst-done" : "dsb-lst-warn";
2716
+ var note = y.note || y.error || "";
2717
+ fh += '<div class="dsb-lrow" style="font-size:11.5px' + (y.ok ? ";opacity:.62" : "") + '"' +
2718
+ (y.ok ? "" : ' data-lskill="' + esc(y.skill) + '" title="点击查看失败详情"') + ">" +
2719
+ '<span class="dsb-lc-date" style="width:110px;flex:none">' + esc(String(y.ts || "").replace("T", " ").slice(5, 16)) + "</span>" +
2720
+ '<span class="' + cls2 + '" style="width:22px;flex:none;text-align:center">' + mark + "</span>" +
2721
+ '<span class="dsb-lc-skill" style="flex:1">' + esc(y.skill) + "</span>" +
2722
+ '<span style="width:64px;flex:none;color:var(--dsb-dim,#8b93a8);font-size:10.5px">' +
2723
+ esc(y.kind === "manual-report" ? "人工上报" : "工具结果") + "</span>" +
2724
+ '<span style="flex:2;color:var(--dsb-dim,#9aa1b5);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
2725
+ esc(y.kind || "") + (note ? " — " + esc(String(note).slice(0, 80)) : "") + "</span>" +
2726
+ "</div>";
2727
+ }
2728
+ if (autoRest > 0) {
2729
+ fh += '<div class="dsb-empty" style="padding:10px 0">' +
2730
+ '<button class="dsb-btn" data-more="flow">' + esc(TF("cardMore", { n: autoRest })) + "</button></div>";
2731
+ }
2732
+ fh += "</div>";
1394
2733
  }
1395
- html += '</div>';
1396
2734
  }
1397
- tbl.innerHTML = html;
2735
+ sum.innerHTML = fh;
1398
2736
  }
1399
2737
 
1400
2738
  function paintDetail() {
@@ -1424,7 +2762,7 @@ window.__ModuleLoader__.load({
1424
2762
  // ---- 球体样式 + 字体球统一样式 + 显隐规则(纯 CSS,永久生效)----
1425
2763
  var CSS = [
1426
2764
  /* 技能球 */
1427
- '#' + BALL_ID + '{position:fixed;right:74px;bottom:18px;width:46px;height:46px;border-radius:50%;background:radial-gradient(circle at 50% 60%, rgba(255,255,255,.98) 0%, rgba(214,232,247,.9) 9%, rgba(111,158,232,.7) 18%, rgba(165,46,255,.5) 32%, rgba(206,44,203,.35) 46%, rgba(10,14,30,.9) 66%, rgba(4,6,14,.99) 100%);box-shadow:0 0 22px rgba(111,158,232,.45),0 0 4px rgba(206,44,203,.3),0 4px 18px rgba(0,0,0,.55),inset 0 1px 3px rgba(255,255,255,.4),inset 0 -8px 16px rgba(206,44,203,.14),inset 0 0 14px rgba(80,120,255,.2);color:#ffffff;font-size:15px;font-weight:800;display:flex;align-items:center;justify-content:center;text-shadow:0 0 4px rgba(255,255,255,.95),0 0 10px rgba(160,200,255,.9),0 0 20px rgba(120,90,255,.7),0 2px 4px rgba(10,20,60,.8);filter:drop-shadow(0 0 6px rgba(180,210,255,.8));animation:dsbGlow 2.4s ease-in-out infinite;cursor:grab;z-index:2147481000;user-select:none;touch-action:none;animation:orbBreathe 3.2s ease-in-out infinite;}',
2765
+ '#' + BALL_ID + '{position:fixed;right:74px;bottom:18px;width:46px;height:46px;border-radius:50%;background:radial-gradient(circle at 50% 60%, rgba(255,255,255,.98) 0%, rgba(214,232,247,.9) 9%, rgba(111,158,232,.7) 18%, rgba(165,46,255,.5) 32%, rgba(206,44,203,.35) 46%, rgba(10,14,30,.9) 66%, rgba(4,6,14,.99) 100%);box-shadow:0 0 22px rgba(111,158,232,.45),0 0 4px rgba(206,44,203,.3),0 4px 18px rgba(0,0,0,.55),inset 0 1px 3px rgba(255,255,255,.4),inset 0 -8px 16px rgba(206,44,203,.14),inset 0 0 14px rgba(80,120,255,.2);color:#ffffff;font-size:15px;font-weight:800;display:flex;align-items:center;justify-content:center;text-shadow:0 0 4px rgba(255,255,255,.95),0 0 10px rgba(160,200,255,.9),0 0 20px rgba(120,90,255,.7),0 2px 4px rgba(10,20,60,.8);filter:drop-shadow(0 0 6px rgba(180,210,255,.8));animation:dsbGlow 2.4s ease-in-out infinite;cursor:grab;z-index:2147483600;user-select:none;touch-action:none;animation:orbBreathe 3.2s ease-in-out infinite;}',
1428
2766
  '#' + BALL_ID + ':hover{transform:scale(1.08)}',
1429
2767
  '#' + BALL_ID + '::before{content:"";position:absolute;inset:1px;border-radius:50%;z-index:1;background:radial-gradient(ellipse 85% 26% at 50% 60%, rgba(255,255,255,1) 0%, rgba(235,245,255,.85) 18%, rgba(160,200,255,.5) 38%, rgba(190,120,255,.35) 55%, transparent 75%),radial-gradient(ellipse 70% 18% at 45% 42%, rgba(111,158,232,.55) 0%, transparent 70%),radial-gradient(ellipse 70% 18% at 55% 74%, rgba(206,44,203,.4) 0%, transparent 70%);filter:blur(1px);animation:bandFlow 4.5s ease-in-out infinite;}',
1430
2768
  '#' + BALL_ID + '::after{content:"";position:absolute;inset:-1px;border-radius:50%;z-index:1;background:conic-gradient(from 200deg, rgba(255,255,255,.55), rgba(140,180,255,.35), rgba(206,44,203,.4), rgba(255,255,255,.15), rgba(111,158,232,.4), rgba(255,255,255,.55));-webkit-mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);mask:radial-gradient(circle, transparent 63%, #000 66%, #000 72%, transparent 76%);animation:rimSpin 7s linear infinite;filter:blur(.5px);}',
@@ -1633,31 +2971,248 @@ window.__ModuleLoader__.load({
1633
2971
  if (s.rim) css += "\n" + RIM + "{content:\"\"}";
1634
2972
  return css;
1635
2973
  }
2974
+ // ---- v1.9.2: semantic colour vars so every new UI follows the active skin ----
2975
+ // A skin only exposes brand hues (--stA/--ptx/...). Success/failure/warning and
2976
+ // soft-surface colours need their own palette, and that palette must flip on
2977
+ // light skins (04 全息幻彩 / 08 极地冰川 / 28 月白纸墨 ...) — otherwise the
2978
+ // success green and the muted greys wash out to near-white on a near-white panel.
2979
+ function skinColorLum(c) {
2980
+ var s = String(c == null ? "" : c).trim(), r = 0, g = 0, b = 0, m;
2981
+ if ((m = /^#([0-9a-f]{3})$/i.exec(s))) {
2982
+ r = parseInt(m[1].charAt(0) + m[1].charAt(0), 16);
2983
+ g = parseInt(m[1].charAt(1) + m[1].charAt(1), 16);
2984
+ b = parseInt(m[1].charAt(2) + m[1].charAt(2), 16);
2985
+ } else if ((m = /^#([0-9a-f]{6})$/i.exec(s))) {
2986
+ r = parseInt(m[1].slice(0, 2), 16);
2987
+ g = parseInt(m[1].slice(2, 4), 16);
2988
+ b = parseInt(m[1].slice(4, 6), 16);
2989
+ } else if ((m = /^hsla?\(\s*(-?[\d.]+)(?:deg)?[,\s]+([\d.]+)%[,\s]+([\d.]+)%/i.exec(s))) {
2990
+ // the 100 hue-derived skins emit hsl()/hsla() strings, so parse those too —
2991
+ // otherwise every derived skin falls through to the hard-coded default.
2992
+ var hh = ((parseFloat(m[1]) % 360) + 360) % 360;
2993
+ var ss = parseFloat(m[2]) / 100, ll = parseFloat(m[3]) / 100;
2994
+ var cc = (1 - Math.abs(2 * ll - 1)) * ss;
2995
+ var xx = cc * (1 - Math.abs(((hh / 60) % 2) - 1));
2996
+ var mm = ll - cc / 2;
2997
+ var rr = 0, gg = 0, bb = 0;
2998
+ if (hh < 60) { rr = cc; gg = xx; }
2999
+ else if (hh < 120) { rr = xx; gg = cc; }
3000
+ else if (hh < 180) { gg = cc; bb = xx; }
3001
+ else if (hh < 240) { gg = xx; bb = cc; }
3002
+ else if (hh < 300) { rr = xx; bb = cc; }
3003
+ else { rr = cc; bb = xx; }
3004
+ r = (rr + mm) * 255; g = (gg + mm) * 255; b = (bb + mm) * 255;
3005
+ } else if ((m = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i.exec(s))) {
3006
+ r = parseFloat(m[1]); g = parseFloat(m[2]); b = parseFloat(m[3]);
3007
+ } else {
3008
+ return null;
3009
+ }
3010
+ function lin(v) { v = v / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }
3011
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
3012
+ }
3013
+ function skinIsLight(id) {
3014
+ var s = SKIN && SKIN.BY_ID ? SKIN.BY_ID[id] : null;
3015
+ var v = (s && s.vars) || {};
3016
+ var lum = skinColorLum(v.pbgA);
3017
+ if (lum == null) lum = skinColorLum(v.bgA);
3018
+ return lum != null && lum > 0.45;
3019
+ }
3020
+ // ---- v1.9.2 对比度工具:解析成 rgb / 混合 / 对比度 / 前景色挑选 ----
3021
+ // skinColorLum 只给亮度,算对比度需要两个颜色的 rgb,所以这里补一套。
3022
+ function skinRgb(c) {
3023
+ var s = String(c == null ? "" : c).trim(), m;
3024
+ if ((m = /^#([0-9a-f]{3})$/i.exec(s))) {
3025
+ return [parseInt(m[1].charAt(0) + m[1].charAt(0), 16), parseInt(m[1].charAt(1) + m[1].charAt(1), 16), parseInt(m[1].charAt(2) + m[1].charAt(2), 16), 1];
3026
+ }
3027
+ if ((m = /^#([0-9a-f]{6})$/i.exec(s))) {
3028
+ return [parseInt(m[1].slice(0, 2), 16), parseInt(m[1].slice(2, 4), 16), parseInt(m[1].slice(4, 6), 16), 1];
3029
+ }
3030
+ if ((m = /^hsla?\(\s*(-?[\d.]+)(?:deg)?[,\s]+([\d.]+)%[,\s]+([\d.]+)%(?:[,\s/]+([\d.]+))?/i.exec(s))) {
3031
+ var hh = ((parseFloat(m[1]) % 360) + 360) % 360, ss = parseFloat(m[2]) / 100, ll = parseFloat(m[3]) / 100;
3032
+ var aa = m[4] == null ? 1 : parseFloat(m[4]);
3033
+ var cc = (1 - Math.abs(2 * ll - 1)) * ss, xx = cc * (1 - Math.abs(((hh / 60) % 2) - 1)), mm = ll - cc / 2;
3034
+ var rr = 0, gg = 0, bb = 0;
3035
+ if (hh < 60) { rr = cc; gg = xx; } else if (hh < 120) { rr = xx; gg = cc; }
3036
+ else if (hh < 180) { gg = cc; bb = xx; } else if (hh < 240) { gg = xx; bb = cc; }
3037
+ else if (hh < 300) { rr = xx; bb = cc; } else { rr = cc; bb = xx; }
3038
+ return [Math.round((rr + mm) * 255), Math.round((gg + mm) * 255), Math.round((bb + mm) * 255), aa];
3039
+ }
3040
+ if ((m = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+))?/i.exec(s))) {
3041
+ return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3]), m[4] == null ? 1 : parseFloat(m[4])];
3042
+ }
3043
+ return null;
3044
+ }
3045
+ function skinLumRgb(rgb) {
3046
+ function lin(v) { v = v / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }
3047
+ return 0.2126 * lin(rgb[0]) + 0.7152 * lin(rgb[1]) + 0.0722 * lin(rgb[2]);
3048
+ }
3049
+ function skinCr(a, b) {
3050
+ var l1 = skinLumRgb(a), l2 = skinLumRgb(b);
3051
+ var hi = Math.max(l1, l2), lo = Math.min(l1, l2);
3052
+ return (hi + 0.05) / (lo + 0.05);
3053
+ }
3054
+ function skinHex(rgb) {
3055
+ function h(v) { v = Math.round(Math.max(0, Math.min(255, v))); return (v < 16 ? "0" : "") + v.toString(16); }
3056
+ return "#" + h(rgb[0]) + h(rgb[1]) + h(rgb[2]);
3057
+ }
3058
+ function skinMix(a, b, t) {
3059
+ return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t, 1];
3060
+ }
3061
+ // 挑一个「在给定底色上一定读得清」的前景色:
3062
+ // 候选按顺序试,第一个对比度达标的直接用(保住皮肤原味);
3063
+ // 全都不达标,就把第一候选往最后一个候选(通常是正文色)方向混,
3064
+ // 逐档加深/提亮直到达标 —— 这样浅色皮肤的品牌亮色会变成同色相的深色版,
3065
+ // 而不是直接丢掉颜色变成灰字。
3066
+ function skinFg(cands, bgStr, min) {
3067
+ var bg = skinRgb(bgStr), i, rgb;
3068
+ if (!bg) return (cands && cands[0]) || "#e8eaf2";
3069
+ for (i = 0; i < cands.length; i++) {
3070
+ if (!cands[i]) continue;
3071
+ rgb = skinRgb(cands[i]);
3072
+ if (rgb && skinCr(rgb, bg) >= min) return cands[i];
3073
+ }
3074
+ var base = skinRgb(cands[0] || "");
3075
+ var target = skinRgb(cands[cands.length - 1] || "");
3076
+ if (!base || !target) return (cands && cands[0]) || "#e8eaf2";
3077
+ for (var t = 0.1; t <= 1.0001; t += 0.1) {
3078
+ var mixed = skinMix(base, target, t);
3079
+ if (skinCr(mixed, bg) >= min) return skinHex(mixed);
3080
+ }
3081
+ return skinHex(target);
3082
+ }
3083
+ function semanticVarsCss(P, id) {
3084
+ var s = SKIN && SKIN.BY_ID ? SKIN.BY_ID[id] : null;
3085
+ var v = (s && s.vars) || {};
3086
+ var light = skinIsLight(id);
3087
+ var d;
3088
+ if (light) {
3089
+ d = {
3090
+ "--dsb-ok": "#0f7a45", "--dsb-okbg": "rgba(15,122,69,.10)", "--dsb-okline": "rgba(15,122,69,.34)",
3091
+ "--dsb-fail": "#b3261e", "--dsb-warn": "#8a5a00",
3092
+ "--dsb-soft": "rgba(0,0,0,.035)", "--dsb-line": "rgba(0,0,0,.11)",
3093
+ "--dsb-hover": "rgba(60,80,190,.13)", "--dsb-shade": "rgba(8,10,18,.46)",
3094
+ "--dsb-tile1": "#3a4370", "--dsb-tile2": "#232a45",
3095
+ "--dsb-oncard": "#ffffff", "--dsb-oncard-dim": "rgba(255,255,255,.86)"
3096
+ };
3097
+ } else {
3098
+ d = {
3099
+ "--dsb-ok": "#9ff5c8", "--dsb-okbg": "rgba(80,220,150,.12)", "--dsb-okline": "rgba(80,220,150,.30)",
3100
+ "--dsb-fail": "#ff9d9d", "--dsb-warn": "#ffd27a",
3101
+ "--dsb-soft": "rgba(255,255,255,.04)", "--dsb-line": "rgba(255,255,255,.08)",
3102
+ "--dsb-hover": "rgba(120,140,255,.16)", "--dsb-shade": "rgba(8,10,18,.42)",
3103
+ "--dsb-tile1": "#3a4370", "--dsb-tile2": "#232a45",
3104
+ "--dsb-oncard": "#ffffff", "--dsb-oncard-dim": "rgba(255,255,255,.86)"
3105
+ };
3106
+ }
3107
+ // native <input type="date"> picks its picker chrome from color-scheme;
3108
+ // without this a light skin still renders a dark calendar popup.
3109
+ d["--dsb-scheme"] = light ? "light" : "dark";
3110
+ // The trend bars are non-text graphics, so 3:1 is the bar — but the raw accent
3111
+ // pair still fails on light skins (08 极地冰川: 2.80 / 2.79 on #eef7fd). Walk
3112
+ // the same-hue-darken ladder so the chart stays visible in every skin.
3113
+ d["--dsb-bar1"] = skinFg([v.stA || (light ? "#4a4f8a" : "#5b8cff"), light ? "#3a3a3a" : "#eaf0ff"], v.pbgA || v.bgA || "#181a22", 3.2);
3114
+ d["--dsb-bar2"] = skinFg([v.stP || v.stD || (light ? "#2f6fa8" : "#2fa8ff"), light ? "#3a3a3a" : "#eaf0ff"], v.pbgA || v.bgA || "#181a22", 3.2);
3115
+ // --dsb-dim: the muted small-print colour. Skins ship --hintC for this role,
3116
+ // but on several of them it only reaches 3.0–4.0:1 against the panel — below
3117
+ // WCAG AA for 10–11px text. Walk hintC -> itm -> ptx and keep the first that
3118
+ // clears the bar, so the muted tone stays muted without becoming unreadable.
3119
+ var bg = v.pbgA || v.bgA;
3120
+ // Small print also sits on cards/rows, which overlay --chipBg on the panel and
3121
+ // are therefore a slightly different colour. Measure against THAT (the harder
3122
+ // background) with a 4.8 margin, otherwise 8 skins landed at 4.17–4.49:1.
3123
+ var cardRgb = skinRgb(bg) || [24, 26, 34, 1];
3124
+ var chipRgb = skinRgb(v.chipBg);
3125
+ if (chipRgb) cardRgb = skinMix(cardRgb, chipRgb, chipRgb[3]);
3126
+ var cardHex = skinHex(cardRgb);
3127
+ // --dsb-surface: the OPAQUE colour rows/cards actually render as (panel bg
3128
+ // with --chipBg composited on top). Sticky table headers must paint this
3129
+ // instead of "transparent", otherwise scrolled rows show through the header
3130
+ // text — the "冻结栏透明、字重叠" the user reported.
3131
+ d["--dsb-surface"] = cardHex;
3132
+ var cands = [v.hintC, v.itm, v.ptx, light ? "#3a3a3a" : "#d8dde8"];
3133
+ d["--dsb-dim"] = skinFg(cands, cardHex, 4.8);
3134
+ // --dsb-brand: the brand accent used as TEXT (skill name, rank name, sort-on
3135
+ // label). On 08 极地冰川 (stA=#f06a5d on #eef7fd) and 04 全息幻彩
3136
+ // (stA=#e0559f on #f2f0ff) the raw accent lands at 2.6–2.9:1 — the exact
3137
+ // "字看不见" the user hit. Fall back to itm/ptx, then to a same-hue dark mix.
3138
+ d["--dsb-brand"] = skinFg([v.stA, v.itm, v.ptx, light ? "#1b2230" : "#eaf0ff"], cardHex, 4.8);
3139
+ // --dsb-brand-strong: same idea for the 20px metric numbers (large text, 3:1).
3140
+ d["--dsb-brand-strong"] = skinFg([v.stA, v.ptx, light ? "#1b2230" : "#eaf0ff"], cardHex, 3.2);
3141
+ // v1.9.9: ok/fail/warn are also used as 10–11px TEXT (model probe badge,
3142
+ // 「N 子类」 counters, ledger status labels), not just as 3:1 graphics.
3143
+ // The fixed palettes above clear 4.5:1 on the panel but 6 light skins
3144
+ // landed at 4.35–4.50 on the card surface, so re-walk them against the
3145
+ // card with a 4.8 margin. Keeps the hue, only deepens it as needed.
3146
+ d["--dsb-ok"] = skinFg([d["--dsb-ok"], light ? "#0b5c33" : "#c8ffe4"], cardHex, 4.8);
3147
+ d["--dsb-fail"] = skinFg([d["--dsb-fail"], light ? "#8c1d16" : "#ffd0d0"], cardHex, 4.8);
3148
+ d["--dsb-warn"] = skinFg([d["--dsb-warn"], light ? "#6b4400" : "#ffe6b0"], cardHex, 4.8);
3149
+ // --dsb-onbrand: label colour ON the accent gradient (active chip/tab/sort
3150
+ // button). White is only right when the gradient is dark; on light skins
3151
+ // (pale gradient) it has to flip to near-black or the label vanishes.
3152
+ //
3153
+ // v1.9.9: also expose a SAFE gradient pair. 8 skins (062 十样锦 3.03:1,
3154
+ // 029 朱砂 3.05, 063 海棠 3.08 …) had a mid-tone gradient where neither
3155
+ // white nor near-black reached 4.5:1 — 11px bold labels are NOT "large
3156
+ // text", so 3:1 is not enough. We darken the gradient itself (mix toward
3157
+ // #12151c) until white text clears 4.6:1, then emit --dsb-onbrand-bg1/2.
3158
+ // Every active-state background uses those instead of the raw stA/stD.
3159
+ var g1 = skinRgb(v.stA) || [91, 108, 255, 1], g2 = skinRgb(v.stP || v.stD) || g1;
3160
+ var gmid = skinMix(g1, g2, 0.5);
3161
+ var whiteOnMid = skinCr([255, 255, 255], gmid);
3162
+ var darkOnMid = skinCr([18, 21, 28], gmid);
3163
+ if (whiteOnMid >= 4.6) {
3164
+ d["--dsb-onbrand"] = "#ffffff";
3165
+ d["--dsb-onbrand-bg1"] = skinHex(g1);
3166
+ d["--dsb-onbrand-bg2"] = skinHex(g2);
3167
+ } else if (darkOnMid >= 4.6) {
3168
+ d["--dsb-onbrand"] = "#12151c";
3169
+ d["--dsb-onbrand-bg1"] = skinHex(g1);
3170
+ d["--dsb-onbrand-bg2"] = skinHex(g2);
3171
+ } else {
3172
+ // Neither extreme works → darken the gradient step by step.
3173
+ var ink = [18, 21, 28, 1], a1 = g1, a2 = g2, t = 0;
3174
+ for (t = 0.1; t <= 0.95; t += 0.05) {
3175
+ a1 = skinMix(g1, ink, t); a2 = skinMix(g2, ink, t);
3176
+ if (skinCr([255, 255, 255], skinMix(a1, a2, 0.5)) >= 4.6) break;
3177
+ }
3178
+ d["--dsb-onbrand"] = "#ffffff";
3179
+ d["--dsb-onbrand-bg1"] = skinHex(a1);
3180
+ d["--dsb-onbrand-bg2"] = skinHex(a2);
3181
+ }
3182
+ var out = P + "{";
3183
+ for (var k in d) { if (Object.prototype.hasOwnProperty.call(d, k)) out += k + ":" + d[k] + ";"; }
3184
+ return out + "}";
3185
+ }
1636
3186
  function skinPanelCss() {
1637
3187
  if (!SKIN) return "";
1638
3188
  var P = "#dsh-skill-browser-panel";
1639
3189
  var id = SKIN.getId();
1640
3190
  var css = SKIN.varsCss(P, id);
3191
+ css += "\n" + semanticVarsCss(P, id);
3192
+ if (skinIsLight(id)) {
3193
+ // native date pickers and the group tiles need the matching scheme
3194
+ css += "\n" + P + " .dsb-ldate{color-scheme:light!important}";
3195
+ }
1641
3196
  css += "\n" + P + "{background:linear-gradient(180deg,var(--pbgA,rgba(24,26,34,.97)),var(--pbgB,rgba(13,15,21,.98)))!important;color:var(--ptx,#e8eaf2)!important;border:1px solid var(--pbd,rgba(255,255,255,.1))!important}";
1642
3197
  css += "\n" + P + " .dsb-btn{background:var(--chipBg,rgba(255,255,255,.08))!important;color:var(--ptx,#dfe3ee)!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important}";
1643
- css += "\n" + P + " .dsb-subtitle{color:var(--itm,#9aa1b5)!important}";
3198
+ css += "\n" + P + " .dsb-subtitle{color:var(--dsb-dim,#9aa1b5)!important}";
1644
3199
  css += "\n" + P + " .dsb-q{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.14))!important;color:var(--ptx,#eef)!important}";
1645
3200
  css += "\n" + P + " .dsb-q:focus{border-color:var(--stA,#5b8cff)!important}";
1646
3201
  css += "\n" + P + " .dsb-chip{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.1))!important;color:var(--ptx,#cdd3e3)!important}";
1647
3202
  css += "\n" + P + " .dsb-schip{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.1))!important;color:var(--ptx,#cdd3e3)!important}";
1648
- css += "\n" + P + " .dsb-chip-on{background:linear-gradient(135deg,var(--stA,#5b6cff),var(--stD,#2fa8ff))!important;color:#fff!important;border-color:transparent!important}";
1649
- css += "\n" + P + " .dsb-tab-on{background:linear-gradient(135deg,var(--stA,#5b6cff),var(--stD,#2fa8ff))!important;color:#fff!important;border-color:transparent!important}";
1650
- css += "\n" + P + " .dsb-sortbtn-on{background:linear-gradient(135deg,var(--stA,#5b6cff),var(--stD,#2fa8ff))!important;color:#fff!important;border-color:transparent!important}";
3203
+ css += "\n" + P + " .dsb-chip-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;color:var(--dsb-onbrand,#fff)!important;border-color:transparent!important}";
3204
+ css += "\n" + P + " .dsb-tab-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;color:var(--dsb-onbrand,#fff)!important;border-color:transparent!important}";
3205
+ css += "\n" + P + " .dsb-sortbtn-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;color:var(--dsb-onbrand,#fff)!important;border-color:transparent!important}";
1651
3206
  css += "\n" + P + " .dsb-card{background:var(--chipBg,rgba(255,255,255,.05))!important;border:1px solid var(--pbd,rgba(255,255,255,.08))!important}";
1652
- css += "\n" + P + " .dsb-card-name{color:var(--stA,#8fc7ff)!important}";
1653
- css += "\n" + P + " .dsb-lc-skill{color:var(--stA,#8fc7ff)!important}";
3207
+ css += "\n" + P + " .dsb-card-name{color:var(--dsb-brand,#8fc7ff)!important}";
3208
+ css += "\n" + P + " .dsb-lc-skill{color:var(--dsb-brand,#8fc7ff)!important}";
1654
3209
  css += "\n" + P + " .dsb-card-zh{color:var(--ptx,#dfe3ee)!important}";
1655
- css += "\n" + P + " .dsb-card-tag{color:var(--hintC,#8b93a8)!important}";
1656
- css += "\n" + P + " .dsb-card-mtime{color:var(--hintC,#8b93a8)!important}";
1657
- css += "\n" + P + " .dsb-lsum-card span{color:var(--hintC,#8b93a8)!important}";
1658
- css += "\n" + P + " .dsb-lhead{color:var(--hintC,#8b93a8)!important}";
1659
- css += "\n" + P + " .dsb-batch-status{color:var(--hintC,#8b93a8)!important}";
1660
- css += "\n" + P + " .dsb-lsum-card b{color:var(--stA,#8fc7ff)!important}";
3210
+ css += "\n" + P + " .dsb-card-tag{color:var(--dsb-dim,#8b93a8)!important}";
3211
+ css += "\n" + P + " .dsb-card-mtime{color:var(--dsb-dim,#8b93a8)!important}";
3212
+ css += "\n" + P + " .dsb-lsum-card span{color:var(--dsb-dim,#8b93a8)!important}";
3213
+ css += "\n" + P + " .dsb-lhead{color:var(--dsb-dim,#8b93a8)!important}";
3214
+ css += "\n" + P + " .dsb-batch-status{color:var(--dsb-dim,#8b93a8)!important}";
3215
+ css += "\n" + P + " .dsb-lsum-card b{color:var(--dsb-brand,#8fc7ff)!important}";
1661
3216
  css += "\n" + P + " .dsb-lsum-card{background:var(--chipBg,rgba(255,255,255,.05))!important;border:1px solid var(--pbd,rgba(255,255,255,.09))!important}";
1662
3217
  css += "\n" + P + " .dsb-lrow{background:var(--chipBg,rgba(255,255,255,.05))!important;border:1px solid var(--pbd,rgba(255,255,255,.09))!important}";
1663
3218
  css += "\n" + P + " .dsb-detail{background:linear-gradient(180deg,var(--pbgA,rgba(24,26,34,.99)),var(--pbgB,rgba(13,15,21,.99)))!important}";
@@ -1679,32 +3234,89 @@ window.__ModuleLoader__.load({
1679
3234
  // ---- uncovered classes: make every remaining .dsb-* follow the skin vars ----
1680
3235
  // 修复:详情正文/tab/排序按钮/开关/空态/提示/批量菜单在浅色主题(04/08/28)下不可读。
1681
3236
  css += "\n" + P + " .dsb-detail-body{color:var(--ptx,#cfd6e8)!important;background:transparent!important}";
1682
- css += "\n" + P + " .dsb-tab{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.1))!important;color:var(--itm,#cdd3e3)!important}";
3237
+ css += "\n" + P + " .dsb-tab{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.1))!important;color:var(--dsb-dim,#cdd3e3)!important}";
1683
3238
  css += "\n" + P + " .dsb-tab:hover{background:var(--pbgB,rgba(255,255,255,.14))!important;color:var(--ptx,#e6e6e6)!important}";
1684
- css += "\n" + P + " .dsb-sortbtn{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--itm,#cdd3e3)!important}";
3239
+ css += "\n" + P + " .dsb-sortbtn{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--dsb-dim,#cdd3e3)!important}";
1685
3240
  css += "\n" + P + " .dsb-sortbtn:hover{background:var(--pbgB,rgba(255,255,255,.14))!important;color:var(--ptx,#e6e6e6)!important}";
1686
- css += "\n" + P + " .dsb-tgl{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--itm,#cdd3e3)!important}";
3241
+ css += "\n" + P + " .dsb-tgl{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--dsb-dim,#cdd3e3)!important}";
1687
3242
  css += "\n" + P + " .dsb-tgl:hover{background:var(--pbgB,rgba(255,255,255,.16))!important;color:var(--ptx,#e6e6e6)!important}";
1688
- css += "\n" + P + " .dsb-tgl-on{color:var(--itD,#9fe0b5)!important;border-color:var(--itD,#9fe0b5)!important}";
1689
- css += "\n" + P + " .dsb-batchbtn{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--itm,#cdd3e3)!important}";
3243
+ css += "\n" + P + " .dsb-tgl-on{color:var(--dsb-ok,#9fe0b5)!important;border-color:var(--dsb-ok,#9fe0b5)!important}";
3244
+ css += "\n" + P + " .dsb-batchbtn{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--dsb-dim,#cdd3e3)!important}";
1690
3245
  css += "\n" + P + " .dsb-batchbtn:hover{background:var(--pbgB,rgba(255,255,255,.14))!important;color:var(--ptx,#e6e6e6)!important}";
1691
- css += "\n" + P + " .dsb-empty{color:var(--hintC,#9aa1b5)!important}";
1692
- css += "\n" + P + " .dsb-set-hint{color:var(--hintC,#9aa1b5)!important}";
1693
- css += "\n" + P + " .dsb-sw-label{color:var(--hintC,#9aa1b5)!important}";
1694
- css += "\n" + P + " .dsb-switch.dsb-sw-on .dsb-sw-label{color:var(--itD,#9fe0b5)!important}";
3246
+ css += "\n" + P + " .dsb-empty{color:var(--dsb-dim,#9aa1b5)!important}";
3247
+ css += "\n" + P + " .dsb-set-hint{color:var(--dsb-dim,#9aa1b5)!important}";
3248
+ css += "\n" + P + " .dsb-sw-label{color:var(--dsb-dim,#9aa1b5)!important}";
3249
+ css += "\n" + P + " .dsb-switch.dsb-sw-on .dsb-sw-label{color:var(--dsb-ok,#9fe0b5)!important}";
1695
3250
  css += "\n" + P + " .dsb-sw-thumb{background:var(--ptx,#fff)!important;box-shadow:0 1px 3px rgba(0,0,0,.35)!important}";
1696
3251
  css += "\n" + P + " .dsb-sw-sep{background:var(--pbd,rgba(255,255,255,.14))!important}";
1697
3252
  css += "\n" + P + " .dsb-head .dsb-title{color:var(--ptx,#e8eaf2)!important}";
1698
3253
  css += "\n" + P + " .dsb-card-off{opacity:.5!important}";
1699
- css += "\n" + P + " .dsb-badge-off{color:var(--stA,#ffd27a)!important;background:var(--chipBg,rgba(255,210,122,.12))!important;border:1px solid var(--pbd,rgba(255,210,122,.35))!important}";
3254
+ css += "\n" + P + " .dsb-badge-off{color:var(--dsb-warn,#ffd27a)!important;background:var(--chipBg,rgba(255,210,122,.12))!important;border:1px solid var(--pbd,rgba(255,210,122,.35))!important}";
1700
3255
  css += "\n" + P + " .dsb-bmenu{background:linear-gradient(180deg,var(--pbgA,#232633),var(--pbgB,#1b1d22))!important;border:1px solid var(--pbd,rgba(255,255,255,.16))!important;box-shadow:0 10px 32px rgba(0,0,0,.35)!important}";
1701
- css += "\n" + P + " .dsb-bmenu-title{color:var(--hintC,#9aa1b5)!important}";
1702
- css += "\n" + P + " .dsb-bmenu-sub{color:var(--hintC,#9aa1b5)!important}";
3256
+ css += "\n" + P + " .dsb-bmenu-title{color:var(--dsb-dim,#9aa1b5)!important}";
3257
+ css += "\n" + P + " .dsb-bmenu-sub{color:var(--dsb-dim,#9aa1b5)!important}";
1703
3258
  css += "\n" + P + " .dsb-bmenu-item{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--ptx,#dfe3ee)!important}";
1704
3259
  css += "\n" + P + " .dsb-bmenu-item:hover{background:var(--pbgB,rgba(120,140,255,.18))!important;border-color:var(--stA,#5b8cff)!important}";
1705
- css += "\n" + P + " .dsb-bmenu-cancel{color:var(--hintC,#9aa1b5)!important}";
3260
+ css += "\n" + P + " .dsb-bmenu-cancel{color:var(--dsb-dim,#9aa1b5)!important}";
1706
3261
  css += "\n" + P + " .dsb-bmenu-cancel:hover{color:var(--ptx,#dfe3ee)!important}";
1707
3262
  css += "\n" + P + " .dsb-toast{box-shadow:0 6px 22px var(--pbd,rgba(0,0,0,.45))!important}";
3263
+ // ---- v1.9.2: 剩余裸硬编码色 + 台账/分组新类统一走语义变量 ----
3264
+ // 语义变量由 semanticVarsCss() 按皮肤明暗给值,浅色皮肤自动切深色系,
3265
+ // 所以这里只需把每一处颜色指向变量,不再关心具体皮肤。
3266
+ css += "\n" + P + " .dsb-set-rec a{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--dsb-warn,#ffd27a)!important}";
3267
+ css += "\n" + P + " .dsb-set-promo .p-name{color:var(--dsb-warn,#ffd27a)!important}";
3268
+ css += "\n" + P + " .dsb-set-promo .p-tag{color:var(--dsb-dim,#9aa1b5)!important}";
3269
+ css += "\n" + P + " .dsb-lsum-fail b{color:var(--dsb-fail,#ff9d9d)!important}";
3270
+ css += "\n" + P + " .dsb-lc-fail{color:var(--dsb-fail,#ff9d9d)!important}";
3271
+ css += "\n" + P + " .dsb-lv-low{color:var(--dsb-warn,#ffd27a)!important}";
3272
+ css += "\n" + P + " .dsb-lst-ok{color:var(--dsb-dim,#9aa1b5)!important}";
3273
+ css += "\n" + P + " .dsb-batch-fail{color:var(--dsb-fail,#ff9d9d)!important}";
3274
+ css += "\n" + P + " .dsb-sw-half .dsb-sw-label{color:var(--dsb-warn,#ffd27a)!important}";
3275
+ css += "\n" + P + " .dsb-bmenu-danger .dsb-bmenu-sub{color:var(--dsb-fail,#ff9d9d)!important}";
3276
+ css += "\n" + P + " .dsb-lc-date{color:var(--dsb-dim,#8b93a8)!important}";
3277
+ css += "\n" + P + " .dsb-lc-rank{color:var(--dsb-dim,#8b93a8)!important}";
3278
+ // 台账看板
3279
+ css += "\n" + P + " .dsb-lq{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--dsb-dim,#9aa1b5)!important}";
3280
+ css += "\n" + P + " .dsb-lq:hover{color:var(--ptx,#dfe3ee)!important;border-color:var(--stA,#7c8dff)!important}";
3281
+ css += "\n" + P + " .dsb-lq-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;color:var(--dsb-onbrand,#fff)!important;border-color:transparent!important}";
3282
+ css += "\n" + P + " .dsb-ldate{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--ptx,#dfe3ee)!important}";
3283
+ css += "\n" + P + " .dsb-lhint{color:var(--dsb-dim,#8b93a8)!important}";
3284
+ css += "\n" + P + " .dsb-lcard{background:var(--chipBg,rgba(255,255,255,.05))!important;border:1px solid var(--pbd,rgba(255,255,255,.09))!important}";
3285
+ css += "\n" + P + " .dsb-lcard b{color:var(--ptx,#eef1f8)!important}";
3286
+ css += "\n" + P + " .dsb-lcard span{color:var(--dsb-dim,#9aa1b5)!important}";
3287
+ css += "\n" + P + " .dsb-lcard-rate b{color:var(--dsb-ok,#9ff5c8)!important}";
3288
+ css += "\n" + P + " .dsb-lcard-fail b{color:var(--dsb-fail,#ff9d9d)!important}";
3289
+ css += "\n" + P + " .dsb-ltrend{background:var(--dsb-soft,rgba(255,255,255,.03))!important;border:1px solid var(--dsb-line,rgba(255,255,255,.07))!important}";
3290
+ css += "\n" + P + " .dsb-lbarcol{background:transparent!important}";
3291
+ css += "\n" + P + " .dsb-lrrow{background:var(--chipBg,rgba(255,255,255,.04))!important;border:1px solid var(--dsb-line,rgba(255,255,255,.07))!important;color:var(--ptx,#dfe3ee)!important}";
3292
+ css += "\n" + P + " .dsb-lrhead{background:var(--dsb-surface,#1a1d26)!important;border-color:transparent transparent var(--dsb-line,rgba(255,255,255,.12))!important;color:var(--dsb-dim,#8b93a8)!important;box-shadow:0 2px 6px rgba(0,0,0,.10)!important}";
3293
+ css += "\n" + P + " .dsb-lrname{color:var(--ptx,#eef1f8)!important}";
3294
+ css += "\n" + P + " .dsb-lrn{color:var(--dsb-dim,#c6cee2)!important}";
3295
+ css += "\n" + P + " .dsb-lrf{color:var(--dsb-fail,#ff9d9d)!important}";
3296
+ css += "\n" + P + " .dsb-lrr{color:var(--dsb-ok,#9ff5c8)!important}";
3297
+ css += "\n" + P + " .dsb-lrdate{color:var(--dsb-dim,#8b93a8)!important}";
3298
+ css += "\n" + P + " .dsb-lsec{color:var(--dsb-dim,#b9c2d8)!important}";
3299
+ css += "\n" + P + " .dsb-ldetail-toggle{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.12))!important;color:var(--dsb-dim,#b9c2d8)!important}";
3300
+ // 分组栏
3301
+ css += "\n" + P + " .dsb-gadd{background:var(--chipBg,rgba(255,255,255,.04))!important;border:1px dashed var(--pbd,rgba(255,255,255,.22))!important;color:var(--dsb-dim,#9aa1b5)!important}";
3302
+ css += "\n" + P + " .dsb-gadd:hover{color:var(--ptx,#dfe3ee)!important;border-color:var(--stA,#7c8dff)!important}";
3303
+ css += "\n" + P + " .dsb-gadd-on{background:linear-gradient(135deg,var(--dsb-onbrand-bg1,#5b6cff),var(--dsb-onbrand-bg2,#2fa8ff))!important;color:var(--dsb-onbrand,#fff)!important;border-style:solid!important;border-color:transparent!important}";
3304
+ css += "\n" + P + " .dsb-movebtn{background:var(--chipBg,rgba(255,255,255,.06))!important;border:1px solid var(--pbd,rgba(255,255,255,.14))!important;color:var(--dsb-dim,#b9c2d8)!important}";
3305
+ css += "\n" + P + " .dsb-movebtn:hover{color:var(--ptx,#eef1f8)!important;border-color:var(--stA,#7c8dff)!important}";
3306
+ css += "\n" + P + " .dsb-card-gtag{color:var(--dsb-brand,#8fc7ff)!important}";
3307
+ css += "\n" + P + " .dsb-ver{color:var(--dsb-ok,#9ff5c8)!important;background:var(--dsb-okbg,rgba(80,220,150,.12))!important;border:1px solid var(--dsb-okline,rgba(80,220,150,.3))!important}";
3308
+ css += "\n" + P + " .dsb-ver-none{color:var(--dsb-dim,#8b93a8)!important;background:var(--chipBg,rgba(255,255,255,.05))!important;border-color:var(--pbd,rgba(255,255,255,.12))!important}";
3309
+ css += "\n" + P + " .dsb-gtitle{color:var(--dsb-dim,#8b93a8)!important}";
3310
+ css += "\n" + P + " .dsb-ghint{color:var(--dsb-dim,#6f778c)!important}";
3311
+ // 台账旧表的状态色 + 设置区底色:这些是 1.8.x 就有的类,同样不能留裸色
3312
+ css += "\n" + P + " .dsb-lv-high{color:var(--dsb-fail,#ff7d7d)!important}";
3313
+ css += "\n" + P + " .dsb-lv-mid{color:var(--dsb-warn,#ffb35c)!important}";
3314
+ css += "\n" + P + " .dsb-lst-done{color:var(--dsb-ok,#6fd48a)!important}";
3315
+ css += "\n" + P + " .dsb-lst-warn{color:var(--dsb-warn,#ffb35c)!important}";
3316
+ css += "\n" + P + " .dsb-lc-status{color:var(--dsb-fail,#ffb3b3)!important}";
3317
+ css += "\n" + P + " .dsb-batchbtn-warn{color:var(--dsb-fail,#ffb3b3)!important;border-color:var(--dsb-fail,#ff7878)!important;background:var(--dsb-soft,rgba(255,120,120,.15))!important}";
3318
+ css += "\n" + P + " .dsb-settings{background:var(--dsb-soft,rgba(255,255,255,.04))!important;border-color:var(--dsb-line,rgba(255,255,255,.08))!important}";
3319
+ css += "\n" + P + " .dsb-subs{border-color:var(--dsb-line,rgba(255,255,255,.08))!important}";
1708
3320
  return css;
1709
3321
  }
1710
3322
  // 原版皮肤(仅本球):flag 置位时清空皮肤覆盖样式,技能球回到原始星云紫。
@@ -1806,7 +3418,7 @@ window.__ModuleLoader__.load({
1806
3418
  var id = row.getAttribute("data-skin-id");
1807
3419
  if (id) { setSkinClassic(false); SKIN.setSkin(id); }
1808
3420
  }, true);
1809
- var SKINMENU_CSS = ".dsb-skinmenu,.fe-skinmenu{display:none;max-height:46vh;overflow-y:auto;border:1px solid rgba(255,255,255,.12);border-radius:12px;background:rgba(10,13,20,.98);padding:6px;z-index:5;box-shadow:0 14px 40px rgba(0,0,0,.5)}\n.dsb-skinmenu.open,.fe-skinmenu.open{display:block}\n.dsb-skinmenu .cap,.fe-skinmenu .cap{font-size:10.5px;font-weight:700;color:#8b93a8;padding:6px 8px 2px;letter-spacing:1px}\n.dsb-skinmenu .row,.fe-skinmenu .row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:8px;cursor:pointer;font-size:12px;color:#dfe4ec;font-family:'Segoe UI','Microsoft YaHei','PingFang SC',system-ui,sans-serif}\n.dsb-skinmenu .row:hover,.fe-skinmenu .row:hover{background:rgba(255,255,255,.08)}\n.dsb-skinmenu .row.cur,.fe-skinmenu .row.cur{color:var(--stA,#ffd27a);font-weight:600}\n.dsb-skinmenu .dot,.fe-skinmenu .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;border:1px solid rgba(255,255,255,.2)}\n.dsb-skinmenu .chk,.fe-skinmenu .chk{width:14px;text-align:center;flex-shrink:0}";
3421
+ var SKINMENU_CSS = ".dsb-skinmenu,.fe-skinmenu{display:none;max-height:46vh;overflow-y:auto;border:1px solid rgba(255,255,255,.12);border-radius:12px;background:rgba(10,13,20,.98);padding:6px;z-index:5;box-shadow:0 14px 40px rgba(0,0,0,.5)}\n.dsb-skinmenu.open,.fe-skinmenu.open{display:block}\n.dsb-skinmenu .cap,.fe-skinmenu .cap{font-size:10.5px;font-weight:700;color:#8b93a8;padding:6px 8px 2px;letter-spacing:1px}\n.dsb-skinmenu .row,.fe-skinmenu .row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:8px;cursor:pointer;font-size:12px;color:#dfe4ec;font-family:'Segoe UI','Microsoft YaHei','PingFang SC',system-ui,sans-serif}\n.dsb-skinmenu .row:hover,.fe-skinmenu .row:hover{background:rgba(255,255,255,.08)}\n.dsb-skinmenu .row.cur,.fe-skinmenu .row.cur{color:var(--dsb-warn,#ffd27a);font-weight:600}\n.dsb-skinmenu .dot,.fe-skinmenu .dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;border:1px solid rgba(255,255,255,.2)}\n.dsb-skinmenu .chk,.fe-skinmenu .chk{width:14px;text-align:center;flex-shrink:0}";
1810
3422
  function ensureSkinMenuMargin() { var m = skinMenuEl(); if (m) m.style.margin = SKINMENU_MARGIN; }
1811
3423
 
1812
3424
  if (SKIN) {
@@ -1991,6 +3603,9 @@ window.__ModuleLoader__.load({
1991
3603
  localStorage.removeItem(BALL_POS_KEY);
1992
3604
  localStorage.setItem("dsh-skill-browser-cfg-ver", "4");
1993
3605
  }
3606
+ // v1.9.8 分组栏显示模式:恢复上次选择(tree / flat)
3607
+ var gm = localStorage.getItem("dsh-skill-browser-gmode");
3608
+ S.gmode = gm === "flat" ? "flat" : "tree";
1994
3609
  } catch (e) {}
1995
3610
  }
1996
3611
 
@@ -2022,4 +3637,4 @@ window.__ModuleLoader__.load({
2022
3637
 
2023
3638
  return module.exports;
2024
3639
  }
2025
- });
3640
+ });