dsh-skill-browser 1.6.2 → 1.7.4

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
@@ -6,17 +6,34 @@ window.__ModuleLoader__.load({
6
6
 
7
7
  // v1.5.5(2026-09-06):版本号对齐修复——host 端 index.js 的 VERSION
8
8
  // 常量此前停在 1.5.1 未随 package.json 同步,现统一为 1.5.5。
9
- // v1.5.4(2026-09-05 用户要求):技能列表每页末尾追加的
10
- // 「💡 悬浮球显隐设置」提示卡一并移除——每页末尾不再出现任何
11
- // 非技能卡片,列表只展示技能本身。
12
- // v1.5.3(2026-09-05 用户要求):
13
- // ① DSH 设置页「悬浮球导航」新增「🎯 一键恢复悬浮球默认位置」按钮,
14
- // 防止悬浮球被拖出屏幕外卡住;点击立即回右下角默认位。
15
- // ② 推荐插件位收敛为仅面板 ⚙ 设置展开区(技能库位置下方)一处;
16
- // 技能列表每页末尾与 DSH 设置页不再显示推荐广告(用户反馈心烦)。
17
- // v1.2.1 — 纯 DOM 实现,移除 React/slots 依赖(__ModuleLoader__ 环境
18
- // 下 require("react") 不可用,导致整个 client 模块加载失败)。
19
- // 设置页开关用 DOM 注入实现(你之前看到"位置对了"就靠这个)。
9
+ // v1.7.0(2026-09-11):三功能升级——
10
+ // ① 技能卡片显示 SKILL.md 最新修改时间(yyyy-MM-dd HH:mm);
11
+ // ② 排序切换:按名称(默认)↔ 按修改时间降序(总清单与每个分类内);
12
+ // ③ 技能启用/停用:单技能开关 + 分类级全启用/全停用,走宿主
13
+ // POST /toggle 与 /toggle-batch(frontmatter disable-model-invocation
14
+ // 行级改写,SKILL.md 先自动备份),停用卡片灰显 + 徽章。
15
+ // v1.7.1(2026-09-11):操作反馈 UI 升级(用户实测反馈:开关无视觉状态、
16
+ // 操作无反馈,误点"全停用"80 个技能事后才发现)——
17
+ // ① 单技能开关换成滑动开关(胶囊轨道 34×18,开启=滑块右移+绿色#27ae60,
18
+ // 停用=滑块左移+灰色#555,transition .2s;状态按 skill.disabled 渲染,
19
+ // 点击乐观更新,API 失败回滚);
20
+ // ② 面板内右上角 toast 轻提示(2 秒自动消失,成功绿/失败红):单开关
21
+ // "已停用/已启用 <技能名>"、批量"已停用/启用 N 项"、部分失败
22
+ // "成功 X 项,失败 Y 项";批量进行中按钮显示"处理中…"防重入;
23
+ // ③ 排序升级四态循环:名称↑ → 名称↓ → 时间↓(最新在前)→ 时间↑(最旧在前),
24
+ // 单按钮显示当前态(SORT_ORDER 为唯一权威,legacy name/mtime 自动迁移)。
25
+ // v1.7.2(2026-09-11):三级滑块开关体系(样式统一胶囊滑块,聚合态半开样式)——
26
+ // ① 全局总开关(排序按钮旁,标签"全部技能"):状态=全部技能聚合
27
+ // (全启用→右绿 / 全停用→左灰 / 混合→居中半开 .dsb-sw-half + title"部分停用");
28
+ // 点击弹面板内确认菜单(非 window.confirm):"全部启用 (N 项)" /
29
+ // "全部停用 (N 项)" + 取消,停用项带醒目危险提示;确认后
30
+ // POST /toggle-batch body={dirs:[全量目录]}(不走 category,最稳)。
31
+ // ② 分类级滑块(替换 v1.7.0 排序栏内"全启用/全停用"两个按钮):小一号
32
+ // 胶囊滑块 + 小字"停用 x/y",聚合逻辑同全局;确认后
33
+ // POST /toggle-batch body={category:"<cat-id>"}。
34
+ // ③ 联动:任一层 toggle 成功后重算全局滑块 + 分类滑块 + 卡片
35
+ // (卡片走 patchCardsDisabled 局部更新类名/徽章,不整列表 innerHTML
36
+ // 重建,避免闪烁);聚合态唯一数据源 = S.data.skills 的 disabled 字段。
20
37
 
21
38
  var S = {
22
39
  mounted: false,
@@ -31,9 +48,96 @@ window.__ModuleLoader__.load({
31
48
  cat: "all",
32
49
  sub: "",
33
50
  q: "",
34
- detail: null
51
+ detail: null,
52
+ sortMode: "name-asc", // v1.7.1 四态:"name-asc" | "name-desc" | "mtime-desc" | "mtime-asc"
53
+ toggling: false, // v1.7.0: toggle 请求进行中(防重复点击)
54
+ toastTimer: null, // v1.7.1: toast 自动消失定时器句柄
55
+ batchMenuScope: null // v1.7.2: 批量确认菜单当前作用域("global" 或分类 id)
35
56
  };
36
57
 
58
+ // v1.7.1 排序四态循环的唯一权威定义(点击按钮沿数组循环)。
59
+ // asc=升序(旧/前→新/后,字符串 localeCompare、时间小→大),
60
+ // desc=降序(新在前/名称 Z→A)。
61
+ var SORT_ORDER = ["name-asc", "name-desc", "mtime-desc", "mtime-asc"];
62
+ function nextSortMode(m) {
63
+ var i = SORT_ORDER.indexOf(m);
64
+ if (i < 0) i = 0;
65
+ return SORT_ORDER[(i + 1) % SORT_ORDER.length];
66
+ }
67
+ // v1.7.0 老会话只存过 "name"/"mtime" 两态,读到旧值自动迁移到等价新态
68
+ function normalizeSortMode(m) {
69
+ if (m === "name") return "name-asc";
70
+ if (m === "mtime") return "mtime-desc";
71
+ return (SORT_ORDER.indexOf(m) >= 0) ? m : "name-asc";
72
+ }
73
+
74
+ // v1.7.1 文案兜底:优先用宿主 /skills 下发的 ui 常量(lib/zh.js UI_TEXT),
75
+ // 老宿主无该字段时用这里的同文兜底。
76
+ var UI_FALLBACK = {
77
+ sortByName: "按名称",
78
+ sortByMtime: "按修改时间",
79
+ sortToggleTitle: "切换排序方式(名称 ↔ 修改时间降序)",
80
+ mtimeLabel: "修改于",
81
+ disabledBadge: "已停用",
82
+ cardDisabledTitle: "已停用(模型技能目录中已摘除)· 点击卡片查看全文",
83
+ toggleEnableTip: "启用后技能恢复进模型技能目录",
84
+ toggleDisableTip: "停用后技能从模型技能目录摘除(SKILL.md 会先自动备份)",
85
+ batchEnable: "全启用",
86
+ batchDisable: "全停用",
87
+ batchBtnTitle: "对该分类下全部技能执行启用/停用",
88
+ batchConfirmDisable: "确定停用该分类下全部技能?(每个 SKILL.md 都会先自动备份,可再次全启用恢复)",
89
+ batchConfirmEnable: "确定启用该分类下全部技能?",
90
+ toggling: "执行中…",
91
+ toggleFailPrefix: "失败",
92
+ toggleDoneSuffix: "完成",
93
+ toggleEnable: "启用",
94
+ toggleDisable: "停用",
95
+ disabledCountSuffix: "项已停用",
96
+ // ── v1.7.1 新增:排序四态 / 滑动开关 / toast ──
97
+ sortNameAsc: "名称↑",
98
+ sortNameDesc: "名称↓",
99
+ sortMtimeDesc: "时间↓",
100
+ sortMtimeAsc: "时间↑",
101
+ sortCycleTitle: "排序(点击循环):名称↑ → 名称↓ → 时间↓(最新在前)→ 时间↑(最旧在前)",
102
+ switchOnText: "开",
103
+ switchOffText: "关",
104
+ toastDisabled: "已停用 {name}",
105
+ toastEnabled: "已启用 {name}",
106
+ toastBatchDisabled: "已停用 {n} 项",
107
+ toastBatchEnabled: "已启用 {n} 项",
108
+ toastBatchPartial: "成功 {x} 项,失败 {y} 项",
109
+ toastToggleFail: "操作失败:{err}",
110
+ processingText: "处理中…",
111
+ // ── v1.7.2 新增:三级滑块开关(全局总开关 / 分类聚合滑块 / 确认菜单)──
112
+ aggAllLabel: "全部技能",
113
+ aggMixedTitle: "部分停用(已停用 {x}/{y})· 点击选择批量启停",
114
+ aggAllOnTitle: "全部启用 · 点击选择批量启停",
115
+ aggAllOffTitle: "全部停用 · 点击选择批量启停",
116
+ aggEnableN: "全部启用 ({n} 项)",
117
+ aggDisableN: "全部停用 ({n} 项)",
118
+ aggDangerText: "将停用全部 {n} 项技能,新会话将不可用这些技能",
119
+ aggMenuTitleGlobal: "全局批量启停 — 作用于技能库全部技能",
120
+ aggMenuTitleCat: "分类批量启停 — 仅作用于当前分类",
121
+ aggMenuStat: "{scope} · 共 {n} 项 · 已停用 {x} 项",
122
+ aggCatStat: "停用 {x}/{y}",
123
+ aggCancelText: "取消"
124
+ };
125
+ function T(key) {
126
+ var ui = (S.data && S.data.ui) || null;
127
+ return (ui && typeof ui[key] === "string" && ui[key]) || UI_FALLBACK[key] || key;
128
+ }
129
+ // v1.7.1 简易占位替换(字符串模板 {name}/{n}/{x}/{y}/{err}),纯 JS 实现
130
+ function TF(key, vars) {
131
+ var t = T(key) || "";
132
+ vars = vars || {};
133
+ for (var k in vars) {
134
+ if (Object.prototype.hasOwnProperty.call(vars, k)) {
135
+ t = String(t).split("{" + k + "}").join(String(vars[k]));
136
+ }
137
+ }
138
+ return t;
139
+ }
140
+
37
141
  var BALL_ID = "dsh-skill-browser-ball";
38
142
  var PANEL_ID = "dsh-skill-browser-panel";
39
143
  var GHOST_KEY = "dsh-skill-browser-ghost";
@@ -105,6 +209,7 @@ window.__ModuleLoader__.load({
105
209
  .then(function (d) {
106
210
  if (!d || !d.ok) throw new Error((d && d.error) || "bad response");
107
211
  S.data = d;
212
+ closeBatchMenu(); // v1.7.2: 数据重扫后确认菜单里的 N 已过期,直接关闭
108
213
  if (S.cat !== "all" && !catMeta(S.cat)) { S.cat = "all"; S.sub = ""; }
109
214
  S.loading = false; paint();
110
215
  })
@@ -293,6 +398,12 @@ window.__ModuleLoader__.load({
293
398
  '.dsb-set-rec{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px;padding:3px 0}' +
294
399
  '.dsb-set-rec a{text-decoration:none;font-size:12px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:6px;padding:2px 8px;color:#ffd27a;cursor:pointer;white-space:nowrap}' +
295
400
  '.dsb-set-rec a:hover{background:rgba(255,210,122,.15);border-color:rgba(255,210,122,.4)}' +
401
+ // v1.7.4 设置面板金边推广卡(广告卡自技能清单迁入):稍窄适配面板宽度
402
+ '.dsb-set-promo{border:1px solid rgba(255,210,122,.35);border-radius:8px;padding:6px 10px;margin-top:6px;background:rgba(255,210,122,.05);cursor:pointer;transition:background .15s}' +
403
+ '.dsb-set-promo:hover{background:rgba(255,210,122,.12)}' +
404
+ '.dsb-set-promo .p-name{font-size:12.5px;font-weight:600;color:#ffd27a}' +
405
+ '.dsb-set-promo .p-desc{font-size:11.5px;color:#c6cbdb;line-height:1.5;margin-top:2px;white-space:normal}' +
406
+ '.dsb-set-promo .p-tag{font-size:10.5px;color:#9aa1b5;margin-top:2px}' +
296
407
  '.dsb-q{margin:4px 16px 8px;padding:7px 12px;border-radius:9px;border:1px solid rgba(255,255,255,.14);' +
297
408
  'background:rgba(255,255,255,.06);color:#eef;font-size:13px;outline:none}' +
298
409
  '.dsb-q:focus{border-color:#5b8cff}' +
@@ -343,7 +454,82 @@ window.__ModuleLoader__.load({
343
454
  '.dsb-lv-low{color:#ffd27a}' +
344
455
  '.dsb-lst-done{color:#6fd48a}' +
345
456
  '.dsb-lst-warn{color:#ffb35c}' +
346
- '.dsb-lst-ok{color:#9aa1b5}';
457
+ '.dsb-lst-ok{color:#9aa1b5}' +
458
+ /* ── v1.7.0:排序切换 / 批量启停 / 单技能开关 / 停用视觉 ── */
459
+ '.dsb-sortbar{display:flex;align-items:center;gap:8px;padding:0 16px 6px}' +
460
+ '.dsb-sortbtn{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);color:#cdd3e3;' +
461
+ 'border-radius:999px;padding:3px 12px;font-size:12px;cursor:pointer;white-space:nowrap}' +
462
+ '.dsb-sortbtn:hover{background:rgba(255,255,255,.14)}' +
463
+ '.dsb-sortbtn-on{background:linear-gradient(135deg,#5b6cff,#2fa8ff);color:#fff;border-color:transparent}' +
464
+ '.dsb-batchbtn{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);color:#cdd3e3;' +
465
+ 'border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer;white-space:nowrap;margin-left:6px}' +
466
+ '.dsb-batchbtn:hover{background:rgba(255,255,255,.14)}' +
467
+ '.dsb-batchbtn-warn{color:#ffb3b3;border-color:rgba(255,120,120,.35)}' +
468
+ '.dsb-batchbtn-warn:hover{background:rgba(255,120,120,.15)}' +
469
+ '.dsb-batchbtn[disabled],.dsb-tgl[disabled]{opacity:.5;cursor:default}' +
470
+ '.dsb-batch-status{font-size:11px;color:#9aa1b5}' +
471
+ '.dsb-batch-fail{color:#ff9d9d}' +
472
+ '.dsb-card-off{opacity:.45;filter:grayscale(.7)}' +
473
+ '.dsb-card-off:hover{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.08)}' +
474
+ '.dsb-badge-off{display:inline-block;font-size:10px;color:#ffd27a;background:rgba(255,210,122,.12);' +
475
+ 'border:1px solid rgba(255,210,122,.35);border-radius:6px;padding:0 6px;margin-left:6px;vertical-align:1px}' +
476
+ '.dsb-card-mtime{font-size:10.5px;color:#8b93a8;margin-top:3px}' +
477
+ '.dsb-tgl{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);color:#cdd3e3;' +
478
+ 'border-radius:6px;padding:2px 8px;font-size:11px;cursor:pointer;white-space:nowrap}' +
479
+ '.dsb-tgl:hover{background:rgba(255,255,255,.16)}' +
480
+ '.dsb-tgl-on{color:#9fe0b5;border-color:rgba(120,220,150,.35)}' +
481
+ /* ── v1.7.1:滑动开关(胶囊轨道 34×18 + 圆形滑块,乐观更新)──
482
+ 结构:<label class="dsb-switch" data-tgl=dir data-to=0|1>
483
+ <span class="dsb-sw-track"><span class="dsb-sw-thumb"></span></span>
484
+ <span class="dsb-sw-label">开/关</span></label>
485
+ 开启态:.dsb-sw-on 轨道绿 #27ae60、滑块右移;停用态:轨道灰 #555、滑块左移。 */
486
+ '#dsh-skill-browser-panel .dsb-switch{display:inline-flex;align-items:center;gap:7px;cursor:pointer;user-select:none}' +
487
+ '#dsh-skill-browser-panel .dsb-switch[disabled]{opacity:.5;cursor:default}' +
488
+ '#dsh-skill-browser-panel .dsb-sw-track{position:relative;display:inline-block;width:34px;height:18px;' +
489
+ 'border-radius:999px;background:#555;transition:background .2s;flex:none}' +
490
+ '#dsh-skill-browser-panel .dsb-sw-thumb{position:absolute;top:2px;left:2px;width:14px;height:14px;' +
491
+ 'border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.4);transition:left .2s}' +
492
+ '#dsh-skill-browser-panel .dsb-switch.dsb-sw-on .dsb-sw-track{background:#27ae60}' +
493
+ '#dsh-skill-browser-panel .dsb-switch.dsb-sw-on .dsb-sw-thumb{left:18px}' +
494
+ '#dsh-skill-browser-panel .dsb-sw-label{font-size:11px;color:#9aa1b5;white-space:nowrap}' +
495
+ '#dsh-skill-browser-panel .dsb-switch.dsb-sw-on .dsb-sw-label{color:#9fe0b5}' +
496
+ /* ── v1.7.1:面板内右上角 toast 轻提示(2s 自动消失,成功绿/失败红)── */
497
+ '#dsh-skill-browser-panel .dsb-toast{position:absolute;top:54px;right:16px;z-index:50;max-width:60%;' +
498
+ 'padding:8px 14px;border-radius:9px;font-size:12.5px;line-height:1.5;white-space:nowrap;overflow:hidden;' +
499
+ 'text-overflow:ellipsis;box-shadow:0 6px 22px rgba(0,0,0,.45);opacity:0;transform:translateY(-6px);' +
500
+ 'transition:opacity .2s,transform .2s;pointer-events:none}' +
501
+ '#dsh-skill-browser-panel .dsb-toast.dsb-toast-show{opacity:1;transform:translateY(0)}' +
502
+ '#dsh-skill-browser-panel .dsb-toast-ok{background:rgba(39,174,96,.95);color:#fff;border:1px solid rgba(255,255,255,.25)}' +
503
+ '#dsh-skill-browser-panel .dsb-toast-err{background:rgba(192,57,43,.95);color:#fff;border:1px solid rgba(255,255,255,.25)}' +
504
+ /* ── v1.7.2:三级滑块开关(聚合态半开 / 小号分类滑块 / 面板内确认菜单)──
505
+ 半开态 .dsb-sw-half:轨道左半灰右半绿 + 滑块居中,标签暖黄;
506
+ 全启用=右绿(复用 .dsb-sw-on),全停用=左灰(无附加类)。 */
507
+ '#dsh-skill-browser-panel .dsb-sw-half .dsb-sw-track{background:linear-gradient(90deg,#555 0%,#555 50%,#27ae60 50%,#27ae60 100%)}' +
508
+ '#dsh-skill-browser-panel .dsb-sw-half .dsb-sw-thumb{left:10px}' +
509
+ '#dsh-skill-browser-panel .dsb-sw-half .dsb-sw-label{color:#ffd27a}' +
510
+ '#dsh-skill-browser-panel .dsb-switch.dsb-sw-agg[aria-disabled]{opacity:.5;cursor:default}' +
511
+ '#dsh-skill-browser-panel .dsb-sw-sep{width:1px;height:16px;background:rgba(255,255,255,.14);margin:0 2px;flex:none}' +
512
+ /* 分类级小一号胶囊滑块(轨道 26×14,滑块 10px) */
513
+ '#dsh-skill-browser-panel .dsb-sw-sm .dsb-sw-track{width:26px;height:14px}' +
514
+ '#dsh-skill-browser-panel .dsb-sw-sm .dsb-sw-thumb{width:10px;height:10px;top:2px;left:2px}' +
515
+ '#dsh-skill-browser-panel .dsb-switch.dsb-sw-sm.dsb-sw-on .dsb-sw-thumb{left:14px}' +
516
+ '#dsh-skill-browser-panel .dsb-switch.dsb-sw-sm.dsb-sw-half .dsb-sw-thumb{left:8px}' +
517
+ /* 面板内批量确认菜单(透明遮罩点击关闭 + 菜单浮层,非 window.confirm) */
518
+ '#dsh-skill-browser-panel .dsb-bmenu-mask{position:absolute;inset:0;z-index:58;background:transparent}' +
519
+ '#dsh-skill-browser-panel .dsb-bmenu{position:absolute;z-index:59;min-width:230px;max-width:78%;' +
520
+ 'background:#232633;border:1px solid rgba(255,255,255,.16);border-radius:10px;padding:6px;' +
521
+ 'box-shadow:0 10px 32px rgba(0,0,0,.55);display:none;flex-direction:column;gap:4px}' +
522
+ '#dsh-skill-browser-panel .dsb-bmenu-title{font-size:11px;color:#9aa1b5;padding:2px 6px 4px;line-height:1.5}' +
523
+ '#dsh-skill-browser-panel .dsb-bmenu-item{display:flex;flex-direction:column;align-items:flex-start;gap:2px;' +
524
+ 'background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);color:#dfe3ee;border-radius:8px;' +
525
+ 'padding:6px 10px;font-size:12.5px;cursor:pointer;text-align:left;width:100%}' +
526
+ '#dsh-skill-browser-panel .dsb-bmenu-item:hover{background:rgba(120,140,255,.18)}' +
527
+ '#dsh-skill-browser-panel .dsb-bmenu-sub{font-size:10.5px;color:#9aa1b5;font-weight:400;line-height:1.5;white-space:normal}' +
528
+ '#dsh-skill-browser-panel .dsb-bmenu-danger{color:#ffb3b3;border-color:rgba(255,120,120,.4)}' +
529
+ '#dsh-skill-browser-panel .dsb-bmenu-danger:hover{background:rgba(255,120,120,.16)}' +
530
+ '#dsh-skill-browser-panel .dsb-bmenu-danger .dsb-bmenu-sub{color:#ff9d9d}' +
531
+ '#dsh-skill-browser-panel .dsb-bmenu-cancel{background:transparent;border:none;color:#9aa1b5;font-size:12px;cursor:pointer;padding:4px 6px;text-align:center}' +
532
+ '#dsh-skill-browser-panel .dsb-bmenu-cancel:hover{color:#dfe3ee}';
347
533
 
348
534
  function buildPanel() {
349
535
  if (S.panelBuilt) return;
@@ -380,17 +566,29 @@ window.__ModuleLoader__.load({
380
566
  '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-ledger" /> 失效台账自动登记(记录每次技能工具调用结果)</label>' +
381
567
  '<span id="dsb-set-status" class="dsb-set-hint">当前技能库:识别中…</span>' +
382
568
  '<span class="dsb-set-hint">💡 悬浮球显隐也可在 <b>DSH 设置页 → 「悬浮球导航」分区</b> 中控制;悬浮球可拖动;清单面板始终居中</span>' +
383
- '<div style="padding-top:4px;border-top:1px dashed rgba(255,255,255,.08);margin-top:4px"></div>' +
384
- '<div class="dsb-set-rec">' +
385
- '<span>🖼️ 照片显示器 — 聊天图片灯箱(放大/下载/浏览)</span>' +
386
- '<a href="https://github.com/loyalchiiina/dsh-chat-image-lightbox" target="_blank" rel="noopener">⭐ Star</a>' +
569
+ // ── v1.7.4 广告区(用户指定落位:设置面板 hint 之后)──
570
+ // hint 文案与原"💡悬浮球显隐设置"说明卡内容重复,二选一保留 hint。
571
+ // 3 张金边推广卡(.dsb-set-promo,rgba(255,210,122,.35)),前两张
572
+ // data-ext-url 点击直达 GitHub(委托见 buildPanel dsb-settings 绑定)。
573
+ // 不带 data-dir/data-tgl:不参与排序/计数/开关逻辑。
574
+ '<div class="dsb-set-promo" data-ext-url="https://github.com/loyalchiiina/dsh-chat-image-lightbox" title="点击打开 GitHub 仓库">' +
575
+ '<div class="p-name">🖼️ 推荐 · 照片显示器</div>' +
576
+ '<div class="p-desc">聊天图片灯箱:多图自动网格、全屏放大、高清下载——本插件悬浮球导航的好搭档</div>' +
577
+ '<div class="p-tag">dsh-chat-image-lightbox · 点击访问 ⭐ Star</div>' +
387
578
  '</div>' +
388
- '<div class="dsb-set-rec">' +
389
- '<span>🔤 字体插件 — 区域自定义字体/颜色/字号</span>' +
390
- '<a href="https://github.com/loyalchiiina/dsh-font-enhancer" target="_blank" rel="noopener">⭐ Star</a>' +
579
+ '<div class="dsb-set-promo" data-ext-url="https://github.com/loyalchiiina/dsh-font-enhancer" title="点击打开 GitHub 仓库">' +
580
+ '<div class="p-name">🔤 推荐 · 字体插件</div>' +
581
+ '<div class="p-desc">界面分区自定义字体/颜色/字号,自带字体球与一键装字体——和技能球并排两个悬浮球</div>' +
582
+ '<div class="p-tag">dsh-font-enhancer · 点击访问 ⭐ Star</div>' +
583
+ '</div>' +
584
+ '<div class="dsb-set-promo" data-ext-url="https://github.com/loyalchiiina/dsh-todo-float-ball" title="点击打开 GitHub 仓库">' +
585
+ '<div class="p-name">📋 推荐 · Todo 悬浮球</div>' +
586
+ '<div class="p-desc">任务进度悬浮球:AI 的任务清单常驻屏幕,实时显示进行中步骤——多步长任务不错过进度(v0.9.0 已上 npm)</div>' +
587
+ '<div class="p-tag">dsh-todo-float-ball · 点击访问 ⭐ Star</div>' +
391
588
  '</div>' +
392
589
  '</div>' +
393
590
  '<input id="dsb-q" class="dsb-q" type="text" placeholder="🔍 搜索技能名 / 中文简介 / 描述…" />' +
591
+ '<div id="dsb-sortbar" class="dsb-sortbar" style="display:none"></div>' +
394
592
  '<div id="dsb-chips" class="dsb-chips"></div>' +
395
593
  '<div id="dsb-subs" class="dsb-subs"></div>' +
396
594
  '<div id="dsb-cards" class="dsb-cards"></div>' +
@@ -410,6 +608,36 @@ window.__ModuleLoader__.load({
410
608
  '</div>';
411
609
  mountEl().appendChild(panel);
412
610
 
611
+ // v1.7.1 toast 轻提示容器:绝对定位在面板右上角(CSS .dsb-toast),初始隐藏
612
+ var toastHost = document.createElement("div");
613
+ toastHost.id = "dsb-toast";
614
+ toastHost.className = "dsb-toast";
615
+ panel.appendChild(toastHost);
616
+
617
+ // v1.7.2 三级滑块的批量确认菜单(面板内浮层,替代 window.confirm):
618
+ // 透明遮罩(点击任意处关闭)+ 菜单浮层(锚定在所点滑块下方,越界自动翻上来)
619
+ var bmenuMask = document.createElement("div");
620
+ bmenuMask.id = "dsb-bmenu-mask";
621
+ bmenuMask.className = "dsb-bmenu-mask";
622
+ bmenuMask.style.display = "none";
623
+ panel.appendChild(bmenuMask);
624
+ var bmenu = document.createElement("div");
625
+ bmenu.id = "dsb-bmenu";
626
+ bmenu.className = "dsb-bmenu";
627
+ bmenu.style.display = "none";
628
+ panel.appendChild(bmenu);
629
+ bmenu.addEventListener("click", function (ev) {
630
+ var it = ev.target.closest("[data-bmenu-act]");
631
+ if (!it) return;
632
+ var act = it.getAttribute("data-bmenu-act");
633
+ var scope = S.batchMenuScope;
634
+ closeBatchMenu();
635
+ if (!scope || act === "cancel") return;
636
+ if (act === "enable") batchToggle(scope, false);
637
+ else if (act === "disable") batchToggle(scope, true);
638
+ });
639
+ bmenuMask.addEventListener("click", function () { closeBatchMenu(); });
640
+
413
641
  $("dsb-close").addEventListener("click", function () { closePanel(); });
414
642
  $("dsb-refresh").addEventListener("click", function () { if (S.view === "ledger") loadLedgerRefresh(); else load(true); });
415
643
  $("dsb-setting").addEventListener("click", function () {
@@ -454,7 +682,7 @@ window.__ModuleLoader__.load({
454
682
  if (!el) return;
455
683
  S.cat = el.getAttribute("data-cat");
456
684
  S.sub = "";
457
- paintChips(); paintSubs(); paintCards();
685
+ paintChips(); paintSubs(); paintCards(); paintSortbar();
458
686
  });
459
687
  $("dsb-subs").addEventListener("click", function (ev) {
460
688
  var el = ev.target.closest("[data-sub]");
@@ -462,12 +690,46 @@ window.__ModuleLoader__.load({
462
690
  S.sub = el.getAttribute("data-sub");
463
691
  paintSubs(); paintCards();
464
692
  });
693
+ // v1.7.0 排序切换 + v1.7.1 四态循环 + 分类级批量启停(事件委托到排序栏容器)
694
+ $("dsb-sortbar").addEventListener("click", function (ev) {
695
+ var sb = ev.target.closest("[data-sort]");
696
+ if (sb) {
697
+ // v1.7.1 四态循环:点击沿 SORT_ORDER 前进一步(名称↑→名称↓→时间↓→时间↑)
698
+ S.sortMode = nextSortMode(sb.getAttribute("data-sort") || S.sortMode);
699
+ paintSortbar(); paintCards();
700
+ return;
701
+ }
702
+ // v1.7.2 聚合滑块(全局 data-bagg="global" / 分类 data-bagg="<cat-id>"):
703
+ // 点击弹出面板内确认菜单(openBatchMenu),不再走 window.confirm
704
+ var agg = ev.target.closest("[data-bagg]");
705
+ if (agg) {
706
+ openBatchMenu(agg, agg.getAttribute("data-bagg") || "global");
707
+ return;
708
+ }
709
+ });
710
+ // v1.7.4 设置面板推广卡(data-ext-url)点击直达 GitHub:
711
+ // 广告卡迁入 #dsb-settings 后,原 #dsb-cards 委托覆盖不到,此处补一条同款委托。
712
+ $("dsb-settings").addEventListener("click", function (ev) {
713
+ var ext = ev.target.closest("[data-ext-url]");
714
+ if (ext) {
715
+ try { window.open(ext.getAttribute("data-ext-url"), "_blank", "noopener"); } catch (e2) {}
716
+ }
717
+ });
465
718
  $("dsb-cards").addEventListener("click", function (ev) {
466
719
  var ext = ev.target.closest("[data-ext-url]");
467
720
  if (ext) {
468
721
  try { window.open(ext.getAttribute("data-ext-url"), "_blank", "noopener"); } catch (e2) {}
469
722
  return;
470
723
  }
724
+ // v1.7.0 单技能启用/停用开关:点开关不打开详情(closest 先命中 tgl 按钮)
725
+ var tgl = ev.target.closest("[data-tgl]");
726
+ if (tgl) {
727
+ if (S.toggling) return;
728
+ var d1 = tgl.getAttribute("data-tgl");
729
+ var toDisabled = tgl.getAttribute("data-to") === "1";
730
+ toggleSkill(d1, toDisabled);
731
+ return;
732
+ }
471
733
  var el = ev.target.closest("[data-dir]");
472
734
  if (!el) return;
473
735
  openDetail(el.getAttribute("data-dir"));
@@ -479,6 +741,7 @@ window.__ModuleLoader__.load({
479
741
  function closePanel() {
480
742
  S.open = false;
481
743
  S.detail = null;
744
+ S.batchMenuScope = null; // v1.7.2: 面板随 DOM 一起销毁,同步清确认菜单状态
482
745
  var p = $(PANEL_ID);
483
746
  if (p && p.parentNode) p.parentNode.removeChild(p);
484
747
  var st = $("dsb-panel-style");
@@ -499,18 +762,19 @@ window.__ModuleLoader__.load({
499
762
  panel.style.display = S.open ? "flex" : "none";
500
763
  if (!S.open) return;
501
764
  var isLedger = S.view === "ledger";
502
- var q = $("dsb-q"), chips = $("dsb-chips"), subs = $("dsb-subs"), cards = $("dsb-cards"), lg = $("dsb-ledger");
765
+ var q = $("dsb-q"), chips = $("dsb-chips"), subs = $("dsb-subs"), cards = $("dsb-cards"), lg = $("dsb-ledger"), sortbar = $("dsb-sortbar");
503
766
  if (q) q.style.display = isLedger ? "none" : "";
504
767
  if (chips) chips.style.display = isLedger ? "none" : "";
505
768
  if (subs) subs.style.display = isLedger ? "none" : "";
506
769
  if (cards) cards.style.display = isLedger ? "none" : "";
770
+ if (sortbar) sortbar.style.display = isLedger ? "none" : "";
507
771
  if (lg) lg.style.display = isLedger ? "block" : "none";
508
772
  var t1 = $("dsb-tab-skills"), t2 = $("dsb-tab-ledger");
509
773
  if (t1) t1.classList.toggle("dsb-tab-on", !isLedger);
510
774
  if (t2) t2.classList.toggle("dsb-tab-on", isLedger);
511
775
  paintHeader();
512
776
  if (isLedger) paintLedger();
513
- else { paintChips(); paintSubs(); paintCards(); }
777
+ else { paintSortbar(); paintChips(); paintSubs(); paintCards(); }
514
778
  paintDetail();
515
779
  }
516
780
 
@@ -520,7 +784,12 @@ window.__ModuleLoader__.load({
520
784
  if (S.loading && !S.data) { sub.textContent = "扫描技能目录中…"; return; }
521
785
  if (S.error) { sub.innerHTML = '<span style="color:#ff8f8f">' + esc(S.error) + '</span>'; return; }
522
786
  if (!S.data) return;
523
- sub.textContent = "共 " + S.data.total + " 个技能 · " + (S.data.root || "?") +
787
+ var offCount = 0;
788
+ var sk = S.data.skills || [];
789
+ for (var i = 0; i < sk.length; i++) { if (sk[i].disabled) offCount++; }
790
+ sub.textContent = "共 " + S.data.total + " 个技能" +
791
+ (offCount ? "(" + offCount + " 已停用)" : "") +
792
+ " · " + (S.data.root || "?") +
524
793
  " · 生成于 " + String(S.data.generatedAt || "").replace("T", " ").slice(0, 16);
525
794
  }
526
795
 
@@ -570,6 +839,319 @@ window.__ModuleLoader__.load({
570
839
  box.innerHTML = html;
571
840
  }
572
841
 
842
+ // ── v1.7.2 三级滑块:聚合态计算与渲染 ──
843
+ // aggCount:统计(可选按分类过滤)技能总数与已停用数——唯一数据源是
844
+ // S.data.skills 的 disabled 字段(内存数据,不额外请求)。
845
+ function aggCount(list, catFilter) {
846
+ var total = 0, off = 0;
847
+ for (var i = 0; i < list.length; i++) {
848
+ if (catFilter && list[i].cat !== catFilter) continue;
849
+ total++;
850
+ if (list[i].disabled) off++;
851
+ }
852
+ return { total: total, off: off };
853
+ }
854
+ // aggState:"on"=全部启用(右绿)| "off"=全部停用(左灰)| "half"=混合(居中半开)
855
+ function aggState(a) {
856
+ if (!a.total) return "off";
857
+ if (!a.off) return "on";
858
+ if (a.off >= a.total) return "off";
859
+ return "half";
860
+ }
861
+ // aggSwitchHtml:胶囊滑块 HTML(样式复用 .dsb-switch;state=on 加 dsb-sw-on、
862
+ // half 加 dsb-sw-half;label 可空——分类滑块不带文字标签,旁边小字显示 停用 x/y)
863
+ function aggSwitchHtml(state, attrs, title, label, small) {
864
+ var cls = "dsb-switch dsb-sw-agg" + (small ? " dsb-sw-sm" : "") +
865
+ (state === "on" ? " dsb-sw-on" : "") + (state === "half" ? " dsb-sw-half" : "");
866
+ return '<label class="' + cls + '" ' + attrs + ' title="' + esc(title || "") + '"' +
867
+ (S.toggling ? ' aria-disabled="true"' : "") + ">" +
868
+ '<span class="dsb-sw-track"><span class="dsb-sw-thumb"></span></span>' +
869
+ (label ? '<span class="dsb-sw-label">' + esc(label) + "</span>" : "") +
870
+ "</label>";
871
+ }
872
+ // ── v1.7.0 排序栏 + v1.7.1 四态循环按钮 + v1.7.2 三级滑块聚合开关 ──
873
+ // 排序单按钮显示当前排序态(点击循环);排序按钮右侧=全局总开关("全部技能");
874
+ // 选中具体分类时再追加分类级小滑块 + 小字"停用 x/y"。
875
+ function sortLabel(m) {
876
+ if (m === "name-asc") return T("sortNameAsc");
877
+ if (m === "name-desc") return T("sortNameDesc");
878
+ if (m === "mtime-asc") return T("sortMtimeAsc");
879
+ if (m === "mtime-desc") return T("sortMtimeDesc");
880
+ return T("sortNameAsc");
881
+ }
882
+ function paintSortbar() {
883
+ var box = $("dsb-sortbar"); if (!box) return;
884
+ var m = normalizeSortMode(S.sortMode);
885
+ S.sortMode = m;
886
+ var btns =
887
+ '<button class="dsb-sortbtn dsb-sortbtn-on" data-sort="' + m + '" title="' + esc(T("sortCycleTitle")) + '">' +
888
+ esc(sortLabel(m)) + '</button>';
889
+ // v1.7.2 全局总开关(最重要):标签"全部技能",状态=所有技能聚合
890
+ // (全启用→右绿 / 全停用→左灰 / 混合→居中半开+title"部分停用");
891
+ // 点击任意态弹面板内确认菜单 openBatchMenu
892
+ var sk = (S.data && S.data.skills) || [];
893
+ var ga = aggCount(sk, null);
894
+ var gs = aggState(ga);
895
+ var gTitle = gs === "half" ? TF("aggMixedTitle", { x: ga.off, y: ga.total })
896
+ : (gs === "on" ? T("aggAllOnTitle") : T("aggAllOffTitle"));
897
+ var html = btns + aggSwitchHtml(gs, 'data-bagg="global"', gTitle, T("aggAllLabel"), false);
898
+ // v1.7.2 分类级滑块:替换 v1.7.0 的"全启用/全停用"两个按钮——小一号胶囊
899
+ // 滑块 + 旁边小字"停用 x/y"(该分类已停用数/总数),聚合逻辑同全局
900
+ if (S.cat !== "all") {
901
+ var ca = aggCount(sk, S.cat);
902
+ var cs = aggState(ca);
903
+ var cTitle = catLabel(S.cat) + " · " + (cs === "half" ? TF("aggMixedTitle", { x: ca.off, y: ca.total })
904
+ : (cs === "on" ? T("aggAllOnTitle") : T("aggAllOffTitle")));
905
+ html += '<span class="dsb-sw-sep"></span>' +
906
+ aggSwitchHtml(cs, 'data-bagg="' + esc(S.cat) + '"', cTitle, "", true) +
907
+ '<span class="dsb-batch-status">' + esc(TF("aggCatStat", { x: ca.off, y: ca.total })) + '</span>';
908
+ }
909
+ // v1.7.1 保留:批量进行中显示"处理中…"(滑块同时 aria-disabled 防重入)
910
+ if (S.toggling) html += '<span class="dsb-batch-status">' + esc(T("processingText")) + '</span>';
911
+ box.innerHTML = html;
912
+ }
913
+
914
+ // v1.7.1:统一排序(总清单与分类内共用),四态口径——
915
+ // name-asc:名称升序(A→Z,localeCompare);name-desc:名称降序(Z→A)
916
+ // mtime-desc:修改时间新→旧(最新在前);mtime-asc:旧→新(最旧在前)
917
+ // 同名/同时间的次序键用 dir 兜底,保证排序稳定可复现
918
+ function sortList(list) {
919
+ var m = normalizeSortMode(S.sortMode);
920
+ var out = list.slice();
921
+ if (m === "name-desc" || m === "name-asc") {
922
+ out.sort(function (a, b) {
923
+ var c = String(a.dir || "").localeCompare(String(b.dir || ""));
924
+ return m === "name-asc" ? c : -c;
925
+ });
926
+ } else {
927
+ out.sort(function (a, b) {
928
+ var d = (Number(b.mtime) || 0) - (Number(a.mtime) || 0);
929
+ if (!d) d = String(a.dir || "").localeCompare(String(b.dir || ""));
930
+ return m === "mtime-desc" ? d : -d;
931
+ });
932
+ }
933
+ return out;
934
+ }
935
+
936
+ // v1.7.0:毫秒时间戳 → yyyy-MM-dd HH:mm(本地时区;非法/0 返回空)
937
+ function fmtTime(ms) {
938
+ var n = Number(ms);
939
+ if (!isFinite(n) || n <= 0) return "";
940
+ var d = new Date(n);
941
+ if (isNaN(d.getTime())) return "";
942
+ function p2(x) { return (x < 10 ? "0" : "") + x; }
943
+ return d.getFullYear() + "-" + p2(d.getMonth() + 1) + "-" + p2(d.getDate()) + " " + p2(d.getHours()) + ":" + p2(d.getMinutes());
944
+ }
945
+
946
+ // v1.7.1:面板内 toast 轻提示(右上角浮层,2 秒自动消失)。
947
+ // kind="ok" 绿 / "err" 红;新提示覆盖旧提示并重置 2 秒计时;
948
+ // 面板未构建/已关闭时元素不存在,静默跳过。
949
+ function showToast(kind, text) {
950
+ var el = $("dsb-toast");
951
+ if (!el) return;
952
+ el.className = "dsb-toast " + (kind === "err" ? "dsb-toast-err" : "dsb-toast-ok");
953
+ el.textContent = text;
954
+ el.classList.add("dsb-toast-show");
955
+ if (S.toastTimer) { try { clearTimeout(S.toastTimer); } catch (e0) {} S.toastTimer = null; }
956
+ S.toastTimer = setTimeout(function () {
957
+ el.classList.remove("dsb-toast-show");
958
+ S.toastTimer = null;
959
+ }, 2000);
960
+ }
961
+
962
+ // v1.7.1:单技能启用/停用(乐观更新)→ 点击瞬间先翻转滑块/灰显/徽章,
963
+ // 再 POST /toggle;API 返回失败则回滚到请求前状态 + 红 toast。
964
+ // 解决用户实测"点停用看不到任何变化"的问题:点击即有视觉反馈。
965
+ function toggleSkill(dir, disable) {
966
+ if (S.toggling) return;
967
+ S.toggling = true;
968
+ var sk = (S.data && S.data.skills) || [];
969
+ var prev = null;
970
+ for (var i = 0; i < sk.length; i++) {
971
+ if (sk[i].dir === dir) { prev = !!sk[i].disabled; sk[i].disabled = !!disable; break; }
972
+ }
973
+ paintSortbar(); paintCards(); // 乐观重绘:滑块位置/颜色、卡片灰显、徽章立即变化
974
+ fetch(apiBase() + "/toggle", {
975
+ method: "POST",
976
+ headers: { "content-type": "application/json" },
977
+ body: JSON.stringify({ dir: dir, disable: disable })
978
+ })
979
+ .then(function (r) { return r.json().catch(function () { throw new Error("HTTP " + r.status); }); })
980
+ .then(function (d) {
981
+ S.toggling = false;
982
+ if (d && d.ok) {
983
+ ctxLogger("toggle " + dir + " -> " + (disable ? "disabled" : "enabled") + (d.backup ? " (backup ok)" : ""));
984
+ showToast("ok", disable ? TF("toastDisabled", { name: dir }) : TF("toastEnabled", { name: dir }));
985
+ } else {
986
+ rollbackToggle(dir, prev, (d && d.error) || "unknown");
987
+ }
988
+ paintSortbar(); paintCards(); paintHeader(); // v1.7.2: paintHeader 同步副标题"已停用"计数
989
+ })
990
+ .catch(function (e) {
991
+ S.toggling = false;
992
+ rollbackToggle(dir, prev, (e && e.message ? e.message : e));
993
+ paintSortbar(); paintCards(); paintHeader();
994
+ });
995
+ }
996
+ // v1.7.1:toggle 失败回滚到请求前状态 + 红 toast(prevState 为请求前的 disabled 值)
997
+ function rollbackToggle(dir, prevState, err) {
998
+ var sk2 = (S.data && S.data.skills) || [];
999
+ for (var i = 0; i < sk2.length; i++) {
1000
+ if (sk2[i].dir === dir) {
1001
+ if (prevState != null) sk2[i].disabled = prevState;
1002
+ break;
1003
+ }
1004
+ }
1005
+ showToast("err", TF("toastToggleFail", { err: err }));
1006
+ try { console.warn("[dsh-skill-browser] toggle " + dir + " failed: " + err); } catch (e) {}
1007
+ }
1008
+
1009
+ // v1.7.0:分类级全启用/全停用 → POST /toggle-batch
1010
+ // v1.7.1:进行中显示"处理中…"(防重入);完成后 toast 汇报"已停用/已启用 N 项"
1011
+ // (N=成功数),部分失败改报"成功 X 项,失败 Y 项"。
1012
+ // v1.7.2:升级为三级滑块统一批量执行入口 batchToggle(scope, disable)——
1013
+ // scope="global" → body={dirs:[全量技能目录]}(不用 category,直接传全量最稳);
1014
+ // scope=<cat-id> → body={category:"<cat-id>"};
1015
+ // 确认交互已上移到面板内确认菜单(openBatchMenu,替代 window.confirm),
1016
+ // 本函数只负责执行与三层联动刷新:①按 results 回写内存 disabled 字段;
1017
+ // ②patchCardsDisabled 局部刷新受影响卡片(不整列 innerHTML 重建,防闪烁);
1018
+ // ③paintSortbar 重算全局+分类聚合滑块与"停用 x/y"小字;④paintHeader 刷副标题。
1019
+ function batchToggle(scope, disable) {
1020
+ if (S.toggling) return;
1021
+ var isGlobal = scope === "global";
1022
+ var sk = (S.data && S.data.skills) || [];
1023
+ var dirs = [];
1024
+ for (var i = 0; i < sk.length; i++) {
1025
+ if (isGlobal || sk[i].cat === scope) dirs.push(sk[i].dir);
1026
+ }
1027
+ if (!dirs.length) return;
1028
+ S.toggling = true;
1029
+ paintSortbar(); // 聚合滑块进入"处理中…"半透明态(aria-disabled 防重入)
1030
+ var body = isGlobal ? { dirs: dirs, disable: disable } : { category: scope, disable: disable };
1031
+ fetch(apiBase() + "/toggle-batch", {
1032
+ method: "POST",
1033
+ headers: { "content-type": "application/json" },
1034
+ body: JSON.stringify(body)
1035
+ })
1036
+ .then(function (r) { return r.json().catch(function () { throw new Error("HTTP " + r.status); }); })
1037
+ .then(function (d) {
1038
+ S.toggling = false;
1039
+ if (d && d.ok) {
1040
+ var byDir = {};
1041
+ var changed = {};
1042
+ var res = d.results || [];
1043
+ for (var i2 = 0; i2 < res.length; i2++) {
1044
+ byDir[res[i2].dir] = res[i2];
1045
+ if (res[i2].ok) changed[res[i2].dir] = !!disable;
1046
+ }
1047
+ for (var j = 0; j < sk.length; j++) {
1048
+ var r2 = byDir[sk[j].dir];
1049
+ if (r2 && r2.ok) sk[j].disabled = !!disable;
1050
+ }
1051
+ patchCardsDisabled(changed); // ③卡片局部更新
1052
+ paintHeader(); // ④副标题"已停用"计数
1053
+ var okN = d.okCount || 0, fails = d.failCount || 0;
1054
+ if (fails) {
1055
+ showToast(fails >= okN ? "err" : "ok", TF("toastBatchPartial", { x: okN, y: fails }));
1056
+ } else {
1057
+ showToast("ok", TF(disable ? "toastBatchDisabled" : "toastBatchEnabled", { n: okN }));
1058
+ }
1059
+ ctxLogger("batch " + (isGlobal ? "global" : scope) + ": ok=" + okN + " fail=" + fails);
1060
+ } else {
1061
+ showToast("err", TF("toastToggleFail", { err: (d && d.error) || "unknown" }));
1062
+ try { console.warn("[dsh-skill-browser] batch failed: " + ((d && d.error) || "unknown")); } catch (e) {}
1063
+ }
1064
+ paintSortbar(); // ①②聚合滑块 + "停用 x/y" 小字重算刷新
1065
+ })
1066
+ .catch(function (e) {
1067
+ S.toggling = false;
1068
+ showToast("err", TF("toastToggleFail", { err: (e && e.message ? e.message : e) }));
1069
+ paintSortbar();
1070
+ try { console.warn("[dsh-skill-browser] batch error: " + (e && e.message ? e.message : e)); } catch (e2) {}
1071
+ });
1072
+ }
1073
+
1074
+ // v1.7.2:打开批量确认菜单(面板内浮层,锚定在所点聚合滑块下方,越界上翻)。
1075
+ // scope:"global" 或分类 id;N=当前作用域内技能总数;
1076
+ // 全局停用选项带醒目危险提示"将停用全部 N 项技能,新会话将不可用这些技能"。
1077
+ function openBatchMenu(anchor, scope) {
1078
+ if (S.toggling) return;
1079
+ var menu = $("dsb-bmenu"), mask = $("dsb-bmenu-mask"), panel = $(PANEL_ID);
1080
+ if (!menu || !mask || !panel || !anchor) return;
1081
+ var isGlobal = scope === "global";
1082
+ var a = aggCount((S.data && S.data.skills) || [], isGlobal ? null : scope);
1083
+ var n = a.total;
1084
+ var scopeName = isGlobal ? T("aggAllLabel") : catLabel(scope);
1085
+ var enSub = isGlobal ? "" : T("batchConfirmEnable");
1086
+ var disSub = isGlobal ? TF("aggDangerText", { n: n }) : T("batchConfirmDisable");
1087
+ menu.innerHTML =
1088
+ '<div class="dsb-bmenu-title">' + esc(isGlobal ? T("aggMenuTitleGlobal") : T("aggMenuTitleCat")) + '<br/>' +
1089
+ esc(TF("aggMenuStat", { scope: scopeName, n: n, x: a.off })) + '</div>' +
1090
+ '<button class="dsb-bmenu-item" data-bmenu-act="enable"><span>' + esc(TF("aggEnableN", { n: n })) + '</span>' +
1091
+ (enSub ? '<span class="dsb-bmenu-sub">' + esc(enSub) + '</span>' : "") + '</button>' +
1092
+ '<button class="dsb-bmenu-item dsb-bmenu-danger" data-bmenu-act="disable"><span>' + esc(TF("aggDisableN", { n: n })) + '</span>' +
1093
+ '<span class="dsb-bmenu-sub">⚠ ' + esc(disSub) + '</span></button>' +
1094
+ '<button class="dsb-bmenu-cancel" data-bmenu-act="cancel">' + esc(T("aggCancelText")) + '</button>';
1095
+ mask.style.display = "block";
1096
+ menu.style.display = "flex";
1097
+ S.batchMenuScope = scope;
1098
+ // 锚定:滑块下方 6px;右缘/下缘越界时向左/向上收进面板
1099
+ var ar = anchor.getBoundingClientRect();
1100
+ var pr = panel.getBoundingClientRect();
1101
+ var left = ar.left - pr.left;
1102
+ var top = ar.bottom - pr.top + 6;
1103
+ var mw = menu.offsetWidth, mh = menu.offsetHeight;
1104
+ var pw = panel.clientWidth, ph = panel.clientHeight;
1105
+ if (left + mw > pw - 8) left = Math.max(8, pw - mw - 8);
1106
+ if (top + mh > ph - 8) top = Math.max(8, ar.top - pr.top - mh - 6);
1107
+ menu.style.left = left + "px";
1108
+ menu.style.top = top + "px";
1109
+ }
1110
+ function closeBatchMenu() {
1111
+ S.batchMenuScope = null;
1112
+ var menu = $("dsb-bmenu"), mask = $("dsb-bmenu-mask");
1113
+ if (menu) menu.style.display = "none";
1114
+ if (mask) mask.style.display = "none";
1115
+ }
1116
+
1117
+ // v1.7.2:批量启停成功后的卡片局部更新——只改受影响卡片的类名/开关/徽章/
1118
+ // title,不重建 #dsb-cards 的 innerHTML(避免全列表闪烁)。DOM 里找不到的
1119
+ // 目录(被搜索/分类过滤掉的卡片)直接跳过,下次全量 paintCards 自然对齐。
1120
+ function patchCardsDisabled(map) {
1121
+ var box = $("dsb-cards"); if (!box) return;
1122
+ var cards = box.querySelectorAll('.dsb-card[data-dir]');
1123
+ for (var i = 0; i < cards.length; i++) {
1124
+ var card = cards[i];
1125
+ var dir = card.getAttribute("data-dir");
1126
+ if (!Object.prototype.hasOwnProperty.call(map, dir)) continue;
1127
+ var off = !!map[dir];
1128
+ card.classList.toggle("dsb-card-off", off);
1129
+ card.title = off ? T("cardDisabledTitle") : "点击查看 SKILL.md 全文";
1130
+ var sw = card.querySelector(".dsb-switch");
1131
+ if (sw) {
1132
+ sw.classList.toggle("dsb-sw-on", !off);
1133
+ sw.setAttribute("data-to", off ? "0" : "1");
1134
+ sw.title = off ? T("toggleEnableTip") : T("toggleDisableTip");
1135
+ var lbl = sw.querySelector(".dsb-sw-label");
1136
+ if (lbl) lbl.textContent = off ? T("switchOffText") : T("switchOnText");
1137
+ }
1138
+ var nameEl = card.querySelector(".dsb-card-name");
1139
+ if (nameEl) {
1140
+ var badge = nameEl.querySelector(".dsb-badge-off");
1141
+ if (off && !badge) {
1142
+ var b = document.createElement("span");
1143
+ b.className = "dsb-badge-off";
1144
+ b.textContent = T("disabledBadge");
1145
+ nameEl.appendChild(b);
1146
+ } else if (!off && badge && badge.parentNode) {
1147
+ badge.parentNode.removeChild(badge);
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ function ctxLogger(msg) {
1153
+ try { console.log("[dsh-skill-browser] " + msg); } catch (e) {}
1154
+ }
573
1155
  function paintCards() {
574
1156
  var box = $("dsb-cards"); if (!box) return;
575
1157
  if (S.error && !S.data) {
@@ -580,22 +1162,36 @@ window.__ModuleLoader__.load({
580
1162
  }
581
1163
  if (!S.data) { box.innerHTML = '<div class="dsb-empty">加载中…</div>'; return; }
582
1164
  var list = filtered();
1165
+ // v1.7.1 四态排序:名称↑(默认)/名称↓/时间↓(最新在前)/时间↑(最旧在前)
1166
+ list = sortList(list);
583
1167
  if (!list.length) { box.innerHTML = '<div class="dsb-empty">没有匹配的技能</div>'; return; }
584
1168
  var html = "";
585
1169
  for (var i = 0; i < list.length; i++) {
586
1170
  var s = list[i];
587
1171
  var tag = s.sub ? esc(subLabel(s.cat, s.sub)) : esc(catLabel(s.cat));
588
1172
  var brief = s.zh || (s.desc ? String(s.desc).slice(0, 90) + (s.desc.length > 90 ? "…" : "") : "(无描述)");
589
- html += '<div class="dsb-card" data-dir="' + esc(s.dir) + '" title="点击查看 SKILL.md 全文">' +
590
- '<div class="dsb-card-name">' + esc(s.dir) + "</div>" +
1173
+ var off = !!s.disabled;
1174
+ var mt = fmtTime(s.mtime);
1175
+ // v1.7.1 滑动开关:.dsb-sw-on=开启(绿轨+滑块右),无=停用(灰轨+滑块左)。
1176
+ // 状态唯一来源是 s.disabled;title 提示点击后的动作方向(off→点击启用)。
1177
+ html += '<div class="dsb-card' + (off ? " dsb-card-off" : "") + '" data-dir="' + esc(s.dir) + '" title="' + esc(off ? T("cardDisabledTitle") : "点击查看 SKILL.md 全文") + '">' +
1178
+ '<div class="dsb-card-name">' + esc(s.dir) + (off ? '<span class="dsb-badge-off">' + esc(T("disabledBadge")) + '</span>' : "") + "</div>" +
591
1179
  '<div class="dsb-card-zh">' + esc(brief) + "</div>" +
592
1180
  '<div class="dsb-card-tag">' + tag + "</div>" +
1181
+ (mt ? '<div class="dsb-card-mtime">' + esc(T("mtimeLabel")) + " " + esc(mt) + "</div>" : "") +
1182
+ '<div style="margin-top:6px">' +
1183
+ '<label class="dsb-switch' + (off ? "" : " dsb-sw-on") + '" data-tgl="' + esc(s.dir) + '" data-to="' + (off ? "0" : "1") +
1184
+ '" title="' + esc(off ? T("toggleEnableTip") : T("toggleDisableTip")) + '"' + (S.toggling ? ' aria-disabled="true"' : "") + '>' +
1185
+ '<span class="dsb-sw-track"><span class="dsb-sw-thumb"></span></span>' +
1186
+ '<span class="dsb-sw-label">' + esc(off ? T("switchOffText") : T("switchOnText")) + "</span>" +
1187
+ "</label>" +
1188
+ "</div>" +
593
1189
  "</div>";
594
1190
  }
595
- // v1.5.3(用户要求):技能列表每页末尾不再追加推荐插件广告卡,
596
- // 推荐位只保留在面板 ⚙ 设置展开区(技能库位置下方)一处。
597
- // v1.5.4(用户要求):「💡 悬浮球显隐设置」提示卡也一并移除,
598
- // 每页末尾不再追加任何非技能卡片,列表只展示技能本身。
1191
+ // ── v1.7.3/v1.7.4 用户指令:广告卡从技能清单彻底移除 ──
1192
+ // 技能列表/分类末尾恢复为纯技能卡列表(原 v1.7.2 追加在列表尾部的
1193
+ // 3 张推荐卡——🖼️照片显示器/🔤字体插件/💡悬浮球说明——已整体迁往
1194
+ // 设置面板 #dsb-settings 广告区,见 buildPanel 内注释)。
599
1195
  box.innerHTML = html;
600
1196
  }
601
1197
 
@@ -1045,7 +1641,7 @@ window.__ModuleLoader__.load({
1045
1641
  applyFontBallVisibility();
1046
1642
  applyBallVisibility();
1047
1643
  } catch (e) {}
1048
- try { console.log("[dsh-skill-browser] client mounted (v1.4.1 slot-only)"); } catch (e3) {}
1644
+ try { console.log("[dsh-skill-browser] client mounted (v1.7.2: 3-level aggregate switches + batch confirm menu)"); } catch (e3) {}
1049
1645
  setTimeout(applyRecBadges, 1200);
1050
1646
  setInterval(applyRecBadges, 5000);
1051
1647
  };