dsh-skill-browser 1.6.1 → 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,15 +48,103 @@ 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";
40
144
  var BALL_POS_KEY = "dsh-skill-browser-ball-pos";
41
145
  var BALL_VIS_KEY = "dsh-skill-browser-ball-visible";
42
146
  var FONT_BALL_VIS_KEY = "dsh-font-ball-visible";
147
+ var TODO_BALL_VIS_KEY = "dsh-tfb-ball-visible";
43
148
 
44
149
  // ---- helpers ----
45
150
  function esc(s) {
@@ -104,6 +209,7 @@ window.__ModuleLoader__.load({
104
209
  .then(function (d) {
105
210
  if (!d || !d.ok) throw new Error((d && d.error) || "bad response");
106
211
  S.data = d;
212
+ closeBatchMenu(); // v1.7.2: 数据重扫后确认菜单里的 N 已过期,直接关闭
107
213
  if (S.cat !== "all" && !catMeta(S.cat)) { S.cat = "all"; S.sub = ""; }
108
214
  S.loading = false; paint();
109
215
  })
@@ -260,6 +366,11 @@ window.__ModuleLoader__.load({
260
366
  // 内联样式兜底(font-enhancer 的 display:block !important 若因时机问题未覆盖)
261
367
  var t = document.getElementById("dsh-fe-toggle");
262
368
  if (t) t.style.setProperty("display", readStore(FONT_BALL_VIS_KEY, true) ? "" : "none", "important");
369
+ // Todo 悬浮球(dsh-todo-float-ball)显隐:同字体球模式——共享其
370
+ // localStorage 键(dsh-tfb-ball-visible),属性+内联双兜底,
371
+ // 不动 todo 插件一行代码(CSS 幂等,重建后仍生效)。
372
+ var tfb = document.getElementById("dsh-tfb-root");
373
+ if (tfb) tfb.style.setProperty("display", readStore(TODO_BALL_VIS_KEY, true) ? "" : "none", "important");
263
374
  }
264
375
  function applyBallVisibility() {
265
376
  var el = document.documentElement || document.body;
@@ -287,6 +398,12 @@ window.__ModuleLoader__.load({
287
398
  '.dsb-set-rec{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px;padding:3px 0}' +
288
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}' +
289
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}' +
290
407
  '.dsb-q{margin:4px 16px 8px;padding:7px 12px;border-radius:9px;border:1px solid rgba(255,255,255,.14);' +
291
408
  'background:rgba(255,255,255,.06);color:#eef;font-size:13px;outline:none}' +
292
409
  '.dsb-q:focus{border-color:#5b8cff}' +
@@ -337,7 +454,82 @@ window.__ModuleLoader__.load({
337
454
  '.dsb-lv-low{color:#ffd27a}' +
338
455
  '.dsb-lst-done{color:#6fd48a}' +
339
456
  '.dsb-lst-warn{color:#ffb35c}' +
340
- '.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}';
341
533
 
342
534
  function buildPanel() {
343
535
  if (S.panelBuilt) return;
@@ -374,17 +566,29 @@ window.__ModuleLoader__.load({
374
566
  '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-ledger" /> 失效台账自动登记(记录每次技能工具调用结果)</label>' +
375
567
  '<span id="dsb-set-status" class="dsb-set-hint">当前技能库:识别中…</span>' +
376
568
  '<span class="dsb-set-hint">💡 悬浮球显隐也可在 <b>DSH 设置页 → 「悬浮球导航」分区</b> 中控制;悬浮球可拖动;清单面板始终居中</span>' +
377
- '<div style="padding-top:4px;border-top:1px dashed rgba(255,255,255,.08);margin-top:4px"></div>' +
378
- '<div class="dsb-set-rec">' +
379
- '<span>🖼️ 照片显示器 — 聊天图片灯箱(放大/下载/浏览)</span>' +
380
- '<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>' +
578
+ '</div>' +
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>' +
381
583
  '</div>' +
382
- '<div class="dsb-set-rec">' +
383
- '<span>🔤 字体插件 — 区域自定义字体/颜色/字号</span>' +
384
- '<a href="https://github.com/loyalchiiina/dsh-font-enhancer" target="_blank" rel="noopener">⭐ Star</a>' +
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>' +
385
588
  '</div>' +
386
589
  '</div>' +
387
590
  '<input id="dsb-q" class="dsb-q" type="text" placeholder="🔍 搜索技能名 / 中文简介 / 描述…" />' +
591
+ '<div id="dsb-sortbar" class="dsb-sortbar" style="display:none"></div>' +
388
592
  '<div id="dsb-chips" class="dsb-chips"></div>' +
389
593
  '<div id="dsb-subs" class="dsb-subs"></div>' +
390
594
  '<div id="dsb-cards" class="dsb-cards"></div>' +
@@ -404,6 +608,36 @@ window.__ModuleLoader__.load({
404
608
  '</div>';
405
609
  mountEl().appendChild(panel);
406
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
+
407
641
  $("dsb-close").addEventListener("click", function () { closePanel(); });
408
642
  $("dsb-refresh").addEventListener("click", function () { if (S.view === "ledger") loadLedgerRefresh(); else load(true); });
409
643
  $("dsb-setting").addEventListener("click", function () {
@@ -448,7 +682,7 @@ window.__ModuleLoader__.load({
448
682
  if (!el) return;
449
683
  S.cat = el.getAttribute("data-cat");
450
684
  S.sub = "";
451
- paintChips(); paintSubs(); paintCards();
685
+ paintChips(); paintSubs(); paintCards(); paintSortbar();
452
686
  });
453
687
  $("dsb-subs").addEventListener("click", function (ev) {
454
688
  var el = ev.target.closest("[data-sub]");
@@ -456,12 +690,46 @@ window.__ModuleLoader__.load({
456
690
  S.sub = el.getAttribute("data-sub");
457
691
  paintSubs(); paintCards();
458
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
+ });
459
718
  $("dsb-cards").addEventListener("click", function (ev) {
460
719
  var ext = ev.target.closest("[data-ext-url]");
461
720
  if (ext) {
462
721
  try { window.open(ext.getAttribute("data-ext-url"), "_blank", "noopener"); } catch (e2) {}
463
722
  return;
464
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
+ }
465
733
  var el = ev.target.closest("[data-dir]");
466
734
  if (!el) return;
467
735
  openDetail(el.getAttribute("data-dir"));
@@ -473,6 +741,7 @@ window.__ModuleLoader__.load({
473
741
  function closePanel() {
474
742
  S.open = false;
475
743
  S.detail = null;
744
+ S.batchMenuScope = null; // v1.7.2: 面板随 DOM 一起销毁,同步清确认菜单状态
476
745
  var p = $(PANEL_ID);
477
746
  if (p && p.parentNode) p.parentNode.removeChild(p);
478
747
  var st = $("dsb-panel-style");
@@ -493,18 +762,19 @@ window.__ModuleLoader__.load({
493
762
  panel.style.display = S.open ? "flex" : "none";
494
763
  if (!S.open) return;
495
764
  var isLedger = S.view === "ledger";
496
- 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");
497
766
  if (q) q.style.display = isLedger ? "none" : "";
498
767
  if (chips) chips.style.display = isLedger ? "none" : "";
499
768
  if (subs) subs.style.display = isLedger ? "none" : "";
500
769
  if (cards) cards.style.display = isLedger ? "none" : "";
770
+ if (sortbar) sortbar.style.display = isLedger ? "none" : "";
501
771
  if (lg) lg.style.display = isLedger ? "block" : "none";
502
772
  var t1 = $("dsb-tab-skills"), t2 = $("dsb-tab-ledger");
503
773
  if (t1) t1.classList.toggle("dsb-tab-on", !isLedger);
504
774
  if (t2) t2.classList.toggle("dsb-tab-on", isLedger);
505
775
  paintHeader();
506
776
  if (isLedger) paintLedger();
507
- else { paintChips(); paintSubs(); paintCards(); }
777
+ else { paintSortbar(); paintChips(); paintSubs(); paintCards(); }
508
778
  paintDetail();
509
779
  }
510
780
 
@@ -514,7 +784,12 @@ window.__ModuleLoader__.load({
514
784
  if (S.loading && !S.data) { sub.textContent = "扫描技能目录中…"; return; }
515
785
  if (S.error) { sub.innerHTML = '<span style="color:#ff8f8f">' + esc(S.error) + '</span>'; return; }
516
786
  if (!S.data) return;
517
- 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 || "?") +
518
793
  " · 生成于 " + String(S.data.generatedAt || "").replace("T", " ").slice(0, 16);
519
794
  }
520
795
 
@@ -564,6 +839,319 @@ window.__ModuleLoader__.load({
564
839
  box.innerHTML = html;
565
840
  }
566
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
+ }
567
1155
  function paintCards() {
568
1156
  var box = $("dsb-cards"); if (!box) return;
569
1157
  if (S.error && !S.data) {
@@ -574,22 +1162,36 @@ window.__ModuleLoader__.load({
574
1162
  }
575
1163
  if (!S.data) { box.innerHTML = '<div class="dsb-empty">加载中…</div>'; return; }
576
1164
  var list = filtered();
1165
+ // v1.7.1 四态排序:名称↑(默认)/名称↓/时间↓(最新在前)/时间↑(最旧在前)
1166
+ list = sortList(list);
577
1167
  if (!list.length) { box.innerHTML = '<div class="dsb-empty">没有匹配的技能</div>'; return; }
578
1168
  var html = "";
579
1169
  for (var i = 0; i < list.length; i++) {
580
1170
  var s = list[i];
581
1171
  var tag = s.sub ? esc(subLabel(s.cat, s.sub)) : esc(catLabel(s.cat));
582
1172
  var brief = s.zh || (s.desc ? String(s.desc).slice(0, 90) + (s.desc.length > 90 ? "…" : "") : "(无描述)");
583
- html += '<div class="dsb-card" data-dir="' + esc(s.dir) + '" title="点击查看 SKILL.md 全文">' +
584
- '<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>" +
585
1179
  '<div class="dsb-card-zh">' + esc(brief) + "</div>" +
586
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>" +
587
1189
  "</div>";
588
1190
  }
589
- // v1.5.3(用户要求):技能列表每页末尾不再追加推荐插件广告卡,
590
- // 推荐位只保留在面板 ⚙ 设置展开区(技能库位置下方)一处。
591
- // v1.5.4(用户要求):「💡 悬浮球显隐设置」提示卡也一并移除,
592
- // 每页末尾不再追加任何非技能卡片,列表只展示技能本身。
1191
+ // ── v1.7.3/v1.7.4 用户指令:广告卡从技能清单彻底移除 ──
1192
+ // 技能列表/分类末尾恢复为纯技能卡列表(原 v1.7.2 追加在列表尾部的
1193
+ // 3 张推荐卡——🖼️照片显示器/🔤字体插件/💡悬浮球说明——已整体迁往
1194
+ // 设置面板 #dsb-settings 广告区,见 buildPanel 内注释)。
593
1195
  box.innerHTML = html;
594
1196
  }
595
1197
 
@@ -739,12 +1341,17 @@ window.__ModuleLoader__.load({
739
1341
  function isFontEnhancerInstalled() {
740
1342
  try { return !!document.getElementById("dsh-fe-toggle"); } catch (e) { return false; }
741
1343
  }
1344
+ // Todo 球安装探测(兼容):dsh-todo-float-ball 未装时隐藏其开关,避免无效控件
1345
+ function isTodoBallInstalled() {
1346
+ try { return !!document.getElementById("dsh-tfb-root"); } catch (e) { return false; }
1347
+ }
742
1348
 
743
1349
  // v1.5.3 一键恢复默认位置:清位置记忆与收起/隐藏状态,球立即回右下角默认位。
744
1350
  // 供 DSH 设置页「悬浮球导航」分区按钮调用(球可能被拖出屏幕外卡住,无法点回)。
745
1351
  // v1.5.7 推荐位安装徽标:探测三个插件的 client 运行状态,注入「已安装/未安装」徽标。
746
1352
  function recBadgeInstalled(id) {
747
1353
  if (id === "font") return !!document.getElementById("dsh-fe-toggle");
1354
+ if (id === "todoball") return isTodoBallInstalled();
748
1355
  if (id === "skillbrowser") return !!document.getElementById(BALL_ID);
749
1356
  return false; // lightbox 用异步 fetch 判定,不走这里
750
1357
  }
@@ -803,7 +1410,18 @@ window.__ModuleLoader__.load({
803
1410
  feBall.style.right = "18px"; feBall.style.bottom = "18px";
804
1411
  }
805
1412
  } catch (e) {}
806
- try { console.log("[dsh-skill-browser] reset: skill ball + font ball -> default positions (paired bottom-right)"); } catch (e) {}
1413
+ // v0.7.2 三球联动:🎯 复位时顺带把 Todo 球归位(右下角 right:18px bottom:18px)。
1414
+ // 同字体球模式:只清位置记忆 + DOM 归位,不动 todo 插件内部实现。
1415
+ try { localStorage.removeItem("dsh-todo-float-ball-pos"); } catch (e) {}
1416
+ try {
1417
+ var tfbHost = document.getElementById("dsh-tfb-root");
1418
+ var tfb = tfbHost && tfbHost.shadowRoot ? tfbHost.shadowRoot.querySelector(".tfb-ball") : null;
1419
+ if (tfb) {
1420
+ tfb.style.left = "auto"; tfb.style.top = "auto";
1421
+ tfb.style.right = "18px"; tfb.style.bottom = "18px";
1422
+ }
1423
+ } catch (e) {}
1424
+ try { console.log("[dsh-skill-browser] reset: skill ball + font ball + todo ball -> default positions (paired bottom-right)"); } catch (e) {}
807
1425
  }
808
1426
 
809
1427
  function buildBall() {
@@ -897,31 +1515,39 @@ window.__ModuleLoader__.load({
897
1515
  if (!React) return null;
898
1516
 
899
1517
  var rows = [];
900
- function mkToggle(id, label, onChange) {
901
- return React.createElement("label", { key: id, style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: "pointer" } },
902
- React.createElement("input", { type: "checkbox", id: id, onChange: onChange }),
903
- React.createElement("span", null, label));
1518
+ // 未安装的球:开关仍显示但置灰+标注,保证"单独装任何一个,分区界面一致"(生态规格)
1519
+ function mkToggle(id, label, onChange, installed) {
1520
+ var on = installed !== false;
1521
+ return React.createElement("label", { key: id, style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: on ? "pointer" : "not-allowed", opacity: on ? 1 : .5 } },
1522
+ React.createElement("input", { type: "checkbox", id: id, disabled: !on, onChange: onChange }),
1523
+ React.createElement("span", null, label + (on ? "" : "(未安装)")));
904
1524
  }
905
1525
  rows.push(React.createElement("div", { key: "head", style: { fontWeight: "600", marginBottom: "4px" } }, "悬浮球导航"));
906
1526
  rows.push(mkToggle("dsb-set-ball", "技能浏览器悬浮球(🧩)", function (e) {
907
1527
  writeStore(BALL_VIS_KEY, e.target.checked);
908
1528
  applyBallVisibility();
909
1529
  if (!e.target.checked && S.open) closePanel();
910
- }));
1530
+ }, true));
911
1531
  rows.push(mkToggle("dsb-set-font", "字体插件悬浮球(🔤)", function (e) {
912
1532
  writeStore(FONT_BALL_VIS_KEY, e.target.checked);
913
1533
  applyFontBallVisibility();
914
- }));
1534
+ }, isFontEnhancerInstalled()));
1535
+ rows.push(mkToggle("dsb-set-todo", "Todo 悬浮球(📋)", function (e) {
1536
+ writeStore(TODO_BALL_VIS_KEY, e.target.checked);
1537
+ if (typeof applyTodoBallVisibility === "function") applyTodoBallVisibility();
1538
+ var el = document.getElementById("dsh-tfb-root");
1539
+ if (el) el.style.setProperty("display", e.target.checked ? "" : "none", "important");
1540
+ }, isTodoBallInstalled()));
915
1541
  rows.push(React.createElement("div", { key: "hint", style: { fontSize: "12px", color: "#8b93a8" } },
916
1542
  "悬浮球可拖动;清单面板始终居中;本分区就在 DSH 设置中,随时可改悬浮球显隐"));
917
1543
  // ── 安装与更新须知(v1.5.7:24h 供应链政策 + 推荐清单说明)──
918
1544
  rows.push(React.createElement("div", { key: "notice-block", style: { marginTop: "12px", padding: "10px 12px", background: "rgba(255,255,255,.03)", border: "1px solid rgba(255,255,255,.06)", borderRadius: "10px", fontSize: "12px", color: "#9aa1b5", lineHeight: "1.7" } },
919
1545
  React.createElement("div", { style: { fontWeight: "600", color: "#cfd4dc", marginBottom: "4px" } }, "📌 安装与更新须知"),
920
- React.createElement("div", null, "1. 本分区由「技能浏览器」提供;若同时安装了字体插件,两个悬浮球的显隐都在这里控制。"),
1546
+ React.createElement("div", null, "1. 本分区由「技能浏览器」提供;技能球 / 字体球 / Todo 球三个悬浮球的显隐都在这里控制(新装默认全部显示)。"),
921
1547
  React.createElement("div", null, "2. 推荐插件列表实时显示「已安装 / 未安装」状态:绿色「已安装」= 可直接使用;红色「未安装」= 点击 ⭐ Star 前往仓库,或到插件市场搜索安装。"),
922
1548
  React.createElement("div", { style: { color: "#e0b0b0" } }, "3. 【重要】安装或更新插件时,若报错含 minimumReleaseAge 或 ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION:这不是插件坏了,是 DSH 对刚发布 npm 包的 24 小时安全观察期(防止刚发布的投毒包)。"),
923
1549
  React.createElement("div", { style: { color: "#e0b0b0" } }, "4. 遇到 24h 报错的处理:等一天再安装/更新即可自动通过,插件本体没有任何问题。插件作者连发多版时窗口期会顺延一天。"),
924
- React.createElement("div", null, "5. 悬浮球可拖动换位置;拖到屏幕外找不到时,点上方 🎯 按钮一键复位(检测到字体插件时双球一起归位)。")
1550
+ React.createElement("div", null, "5. 悬浮球可拖动换位置;拖到屏幕外找不到时,点 🎯 按钮所有悬浮球一起回到右下角。Todo 球的皮肤在展开其面板后点 🎨 切换(星云/石墨/蓝宝石)。")
925
1551
  ));
926
1552
  // ── 一键恢复默认位置(v1.5.3 用户要求):悬浮球被拖出屏幕外卡住时,
927
1553
  // 点此按钮立即回到默认右下角 (right:26px bottom:118px) 46px 常规大小,
@@ -931,7 +1557,7 @@ window.__ModuleLoader__.load({
931
1557
  style: { alignSelf: "flex-start", fontSize: "13px", cursor: "pointer",
932
1558
  background: "rgba(255,210,122,.12)", border: "1px solid rgba(255,210,122,.35)",
933
1559
  borderRadius: "6px", padding: "6px 12px", color: "#ffd27a" }
934
- }, isFontEnhancerInstalled() ? "🎯 一键恢复悬浮球默认位置(右下角,双球)" : "🎯 一键恢复悬浮球默认位置(右下角)"));
1560
+ }, "🎯 所有悬浮球回到右下角"));
935
1561
  // ── 推荐插件(v1.5.7 统一三条清单:字体/技能/照片,探测式安装徽标)──
936
1562
  rows.push(React.createElement("div", { key: "rec-head", style: { fontWeight: "600", marginTop: "12px", marginBottom: "4px", borderTop: "1px solid rgba(255,255,255,.08)", paddingTop: "10px" } }, "推荐插件"));
937
1563
  function recRow(key, recId, icon, text, repo) {
@@ -939,9 +1565,14 @@ window.__ModuleLoader__.load({
939
1565
  React.createElement("span", null, icon + " " + text),
940
1566
  React.createElement("a", { href: "https://github.com/" + repo, target: "_blank", rel: "noopener", style: { textDecoration: "none", fontSize: "12px", background: "rgba(255,255,255,.06)", border: "1px solid rgba(255,255,255,.12)", borderRadius: "6px", padding: "2px 8px", color: "#ffd27a", cursor: "pointer", whiteSpace: "nowrap" } }, "⭐ Star"));
941
1567
  }
1568
+ // ── 推荐插件(v1.5.7 统一清单:字体/技能/照片/Todo,探测式安装徽标)──
1569
+ // Todo 悬浮球皮肤切换(三皮肤):事件委托到本分区容器——React 合成
1570
+ // 事件与 ref 均不可靠(slot 重渲染会重建 DOM),委托到容器上永不失效。
1571
+ // 点击写 localStorage + 直调 todo 插件的 __dshtfbApplyTheme 钩子。
942
1572
  rows.push(recRow("rec-font", "font", "🔤", "字体插件 — 区域自定义字体/颜色/字号", "loyalchiiina/dsh-font-enhancer"));
943
1573
  rows.push(recRow("rec-skillbrowser", "skillbrowser", "🧩", "技能浏览器 — 悬浮球面板浏览共享技能库 + 失效台账", "loyalchiiina/dsh-skill-browser"));
944
- rows.push(recRow("rec-lightbox", "lightbox", "🖼️", "照片显示器 — 聊天图片灯箱(放大/下载/浏览)", "loyalchiiina/dsh-chat-image-lightbox"));return React.createElement("div", { "data-dsb-slot": "1", style: { display: "flex", flexDirection: "column", gap: "8px", padding: "8px 0" } }, rows);
1574
+ rows.push(recRow("rec-lightbox", "lightbox", "🖼️", "照片显示器 — 聊天图片灯箱(放大/下载/浏览)", "loyalchiiina/dsh-chat-image-lightbox"));
1575
+ rows.push(recRow("rec-todoball", "todoball", "📋", "Todo 悬浮球 — todo_write 进度常驻监控 + 多会话固定 + 三皮肤(面板内 🎨 切换)", "loyalchiiina/dsh-todo-float-ball"));return React.createElement("div", { "data-dsb-slot": "1", style: { display: "flex", flexDirection: "column", gap: "8px", padding: "8px 0" } }, rows);
945
1576
  }
946
1577
 
947
1578
  function registerSettings(ctx) {
@@ -1010,7 +1641,7 @@ window.__ModuleLoader__.load({
1010
1641
  applyFontBallVisibility();
1011
1642
  applyBallVisibility();
1012
1643
  } catch (e) {}
1013
- 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) {}
1014
1645
  setTimeout(applyRecBadges, 1200);
1015
1646
  setInterval(applyRecBadges, 5000);
1016
1647
  };