dsh-skill-browser 1.5.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 ADDED
@@ -0,0 +1,941 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-skill-browser",
3
+ factory: function (require) {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ // v1.5.4(2026-09-05 用户要求):技能列表每页末尾追加的
8
+ // 「💡 悬浮球显隐设置」提示卡一并移除——每页末尾不再出现任何
9
+ // 非技能卡片,列表只展示技能本身。
10
+ // v1.5.3(2026-09-05 用户要求):
11
+ // ① DSH 设置页「悬浮球导航」新增「🎯 一键恢复悬浮球默认位置」按钮,
12
+ // 防止悬浮球被拖出屏幕外卡住;点击立即回右下角默认位。
13
+ // ② 推荐插件位收敛为仅面板 ⚙ 设置展开区(技能库位置下方)一处;
14
+ // 技能列表每页末尾与 DSH 设置页不再显示推荐广告(用户反馈心烦)。
15
+ // v1.2.1 — 纯 DOM 实现,移除 React/slots 依赖(__ModuleLoader__ 环境
16
+ // 下 require("react") 不可用,导致整个 client 模块加载失败)。
17
+ // 设置页开关用 DOM 注入实现(你之前看到"位置对了"就靠这个)。
18
+
19
+ var S = {
20
+ mounted: false,
21
+ open: false,
22
+ panelBuilt: false,
23
+ view: "skills", // "skills" | "ledger"
24
+ data: null,
25
+ ledger: null,
26
+ ledgerDetail: null,
27
+ loading: false,
28
+ error: "",
29
+ cat: "all",
30
+ sub: "",
31
+ q: "",
32
+ detail: null
33
+ };
34
+
35
+ var BALL_ID = "dsh-skill-browser-ball";
36
+ var PANEL_ID = "dsh-skill-browser-panel";
37
+ var GHOST_KEY = "dsh-skill-browser-ghost";
38
+ var BALL_POS_KEY = "dsh-skill-browser-ball-pos";
39
+ var BALL_VIS_KEY = "dsh-skill-browser-ball-visible";
40
+ var FONT_BALL_VIS_KEY = "dsh-font-ball-visible";
41
+
42
+ // ---- helpers ----
43
+ function esc(s) {
44
+ return String(s == null ? "" : s)
45
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
46
+ }
47
+ function $(id) { return document.getElementById(id); }
48
+ function apiBase() { return "/dsh-skill-browser"; }
49
+ function mountEl() { return document.documentElement || document.body; }
50
+
51
+ function catMeta(id) {
52
+ if (!S.data) return null;
53
+ var cs = S.data.categories || [];
54
+ for (var i = 0; i < cs.length; i++) if (cs[i].id === id) return cs[i];
55
+ return null;
56
+ }
57
+ function catLabel(id) {
58
+ if (id === "all") return "全部";
59
+ var m = catMeta(id);
60
+ return m ? (m.icon + " " + m.label) : id;
61
+ }
62
+ function subLabel(catId, subId) {
63
+ var m = catMeta(catId);
64
+ if (!m || !m.subs) return "";
65
+ for (var i = 0; i < m.subs.length; i++) if (m.subs[i].id === subId) return m.subs[i].label;
66
+ return subId;
67
+ }
68
+
69
+ function filtered() {
70
+ if (!S.data) return [];
71
+ var q = S.q.trim().toLowerCase();
72
+ var out = [];
73
+ var sk = S.data.skills || [];
74
+ for (var i = 0; i < sk.length; i++) {
75
+ var s = sk[i];
76
+ if (S.cat !== "all" && s.cat !== S.cat) continue;
77
+ if (S.sub && s.sub !== S.sub) continue;
78
+ if (q) {
79
+ var hay = (s.dir + " " + s.name + " " + (s.zh || "") + " " + (s.desc || "")).toLowerCase();
80
+ if (hay.indexOf(q) < 0) continue;
81
+ }
82
+ out.push(s);
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function readStore(key, def) {
88
+ try { var v = localStorage.getItem(key); return v == null ? def : v === "1"; } catch (e) { return def; }
89
+ }
90
+ function writeStore(key, val) {
91
+ try { localStorage.setItem(key, val ? "1" : "0"); } catch (e) {}
92
+ }
93
+ function isGhost() {
94
+ try { return localStorage.getItem(GHOST_KEY) === "1"; } catch (e) { return false; }
95
+ }
96
+
97
+ // ---- data ----
98
+ function load(refresh) {
99
+ S.loading = true; S.error = ""; paint();
100
+ fetch(apiBase() + "/skills" + (refresh ? "?refresh=1" : ""))
101
+ .then(function (r) { return r.json(); })
102
+ .then(function (d) {
103
+ if (!d || !d.ok) throw new Error((d && d.error) || "bad response");
104
+ S.data = d;
105
+ if (S.cat !== "all" && !catMeta(S.cat)) { S.cat = "all"; S.sub = ""; }
106
+ S.loading = false; paint();
107
+ })
108
+ .catch(function (e) {
109
+ S.loading = false;
110
+ S.error = "加载失败:" + (e && e.message ? e.message : e);
111
+ paint();
112
+ });
113
+ }
114
+
115
+ function loadLedger() {
116
+ if (S.ledger) return;
117
+ S.loading = true; S.error = ""; paint();
118
+ fetch(apiBase() + "/ledger")
119
+ .then(function (r) { return r.json(); })
120
+ .then(function (d) {
121
+ if (!d || !d.ok) throw new Error((d && d.error) || "bad response");
122
+ S.ledger = d;
123
+ S.loading = false; paint();
124
+ })
125
+ .catch(function (e) {
126
+ S.loading = false;
127
+ S.error = "台账加载失败:" + (e && e.message ? e.message : e);
128
+ paint();
129
+ });
130
+ }
131
+
132
+ function loadLedgerRefresh() {
133
+ S.ledger = null;
134
+ loadLedger();
135
+ }
136
+
137
+ // ---- 运行时配置(技能库位置 / 自动台账开关)----
138
+ // 目录选择:POST /pick-dir,由宿主端 spawn powershell -STA 弹 Windows
139
+ // FolderBrowserDialog(系统原生对话框)。不依赖 Electron renderer 的
140
+ // File API:Electron 32+ 已移除 File.path,webUtils 又依赖 preload 暴露,
141
+ // webkitdirectory 方案实测拿不到可靠真实路径(两次尝试均失败)。
142
+ function browseForDirectory() {
143
+ var st = $("dsb-set-status");
144
+ if (st) st.textContent = "正在打开系统目录选择框…(请在弹出的窗口中选择;若被遮挡请看任务栏)";
145
+ fetch(apiBase() + "/pick-dir", { method: "POST" })
146
+ .then(function (r) { return r.json(); })
147
+ .then(function (d) {
148
+ if (d && d.ok && d.path) {
149
+ var inp = $("dsb-set-root");
150
+ if (inp) inp.value = d.path;
151
+ saveConfig({ skillsDir: d.path }); // 选完即保存
152
+ } else if (d && d.ok) {
153
+ if (st) st.textContent = "已取消选择;可手动在输入框粘贴绝对路径后点「保存」";
154
+ } else {
155
+ if (st) st.textContent = "选择失败:" + ((d && d.error) || "未知错误") + ";可手动在输入框粘贴绝对路径";
156
+ }
157
+ })
158
+ .catch(function (e) {
159
+ if (st) st.textContent = "选择失败:" + (e && e.message ? e.message : e) + ";可手动在输入框粘贴绝对路径";
160
+ });
161
+ }
162
+ function loadConfig(cb) {
163
+ fetch(apiBase() + "/config")
164
+ .then(function (r) { return r.json(); })
165
+ .then(function (d) { if (d && d.ok && cb) cb(d); })
166
+ .catch(function () {});
167
+ }
168
+ function saveConfig(patch) {
169
+ var el = $("dsb-set-status");
170
+ fetch(apiBase() + "/config", {
171
+ method: "POST",
172
+ headers: { "content-type": "application/json" },
173
+ body: JSON.stringify(patch || {})
174
+ })
175
+ .then(function (r) { return r.json().catch(function () { throw new Error("HTTP " + r.status); }); })
176
+ .then(function (d) {
177
+ if (d && d.ok) {
178
+ S.data = null; S.ledger = null; // 根路径可能变了,强制重扫
179
+ if (S.open) { load(true); if (S.view === "ledger") loadLedgerRefresh(); }
180
+ paintConfigStatus(d);
181
+ } else {
182
+ if (el) el.textContent = "保存失败:" + ((d && d.error) || "未知错误") + "(宿主版本可能过旧,需重启 DSH)";
183
+ }
184
+ })
185
+ .catch(function (e) {
186
+ if (el) el.textContent = "保存失败:" + (e && e.message ? e.message : e) + "(宿主版本可能过旧,需重启 DSH)";
187
+ });
188
+ }
189
+ function paintConfigStatus(cfg) {
190
+ var el = $("dsb-set-status"), inp = $("dsb-set-root"), lcb = $("dsb-set-ledger");
191
+ if (!el) return;
192
+ var srcMap = { "settings-override": "设置页指定", "env": "环境变量", "config": "插件配置", "auto-detect": "自动识别", "none": "未找到" };
193
+ el.textContent = "当前技能库:" + (cfg.root || "未找到") + "(" + (srcMap[cfg.source] || cfg.source) + ")· 台账:" +
194
+ (cfg.ledgerExists ? "已存在" : (cfg.ledgerPath ? "将自动创建" : "—"));
195
+ if (inp && document.activeElement !== inp) inp.value = (cfg.runtime && cfg.runtime.skillsDir) || "";
196
+ if (lcb) lcb.checked = cfg.ledgerEnabled !== false;
197
+ }
198
+
199
+ function openDetail(dir) {
200
+ var box = $("dsb-detail-body"); if (box) box.textContent = "加载中…";
201
+ S.detail = { dir: dir, content: "" };
202
+ paint();
203
+ fetch(apiBase() + "/skill?dir=" + encodeURIComponent(dir))
204
+ .then(function (r) { return r.json(); })
205
+ .then(function (d) {
206
+ if (!S.detail || S.detail.dir !== dir) return;
207
+ S.detail.content = (d && d.ok) ? d.content : ("读取失败:" + ((d && d.error) || ""));
208
+ var box2 = $("dsb-detail-body");
209
+ if (box2) box2.textContent = S.detail.content;
210
+ })
211
+ .catch(function (e) {
212
+ var box3 = $("dsb-detail-body");
213
+ if (box3) box3.textContent = "读取失败:" + (e && e.message ? e.message : e);
214
+ });
215
+ }
216
+
217
+ function openLedgerDetail(skill) {
218
+ var box = $("dsb-detail-body"); if (box) box.textContent = "加载中…";
219
+ S.ledgerDetail = { skill: skill, detail: "" };
220
+ paint();
221
+ fetch(apiBase() + "/ledger-detail?skill=" + encodeURIComponent(skill))
222
+ .then(function (r) { return r.json(); })
223
+ .then(function (d) {
224
+ if (!S.ledgerDetail || S.ledgerDetail.skill !== skill) return;
225
+ var parts = [];
226
+ if (d && d.ok) {
227
+ if (d.detail) parts.push("【人工台账失败明细】\n" + d.detail);
228
+ if (d.autoEntries && d.autoEntries.length) {
229
+ var lines = ["【自动登记记录】共 " + d.autoEntries.length + " 条(最新在前)"];
230
+ var list = d.autoEntries.slice().reverse();
231
+ for (var i = 0; i < list.length; i++) {
232
+ var x = list[i];
233
+ lines.push("- " + String(x.ts || "").replace("T", " ").slice(0, 19) + " " + (x.ok ? "✅ 生效" : "❌ 未生效") + "(" + (x.kind || "auto") + ")" + (x.note || x.error ? " — " + (x.note || x.error) : ""));
234
+ }
235
+ parts.push(lines.join("\n"));
236
+ }
237
+ if (!parts.length) parts.push("(无失败明细记录)");
238
+ } else {
239
+ parts.push("读取失败:" + ((d && d.error) || ""));
240
+ }
241
+ S.ledgerDetail.detail = parts.join("\n\n");
242
+ var box2 = $("dsb-detail-body");
243
+ if (box2) box2.textContent = S.ledgerDetail.detail;
244
+ })
245
+ .catch(function (e) {
246
+ var box3 = $("dsb-detail-body");
247
+ if (box3) box3.textContent = "读取失败:" + (e && e.message ? e.message : e);
248
+ });
249
+ }
250
+
251
+ // ---- 字体插件球:纯 CSS 永久统一样式与显隐 ----
252
+ // 不再轮询检查:用全局 CSS 规则 + body 属性驱动,font-enhancer 重建按钮
253
+ // 多少次样式都自动生效(CSS 幂等)。不动 font-enhancer 一行代码。
254
+ function applyFontBallVisibility() {
255
+ // 属性设在 html 上(CSS 规则驱动)
256
+ var el = document.documentElement || document.body;
257
+ el.setAttribute("data-dsb-font-hidden", readStore(FONT_BALL_VIS_KEY, true) ? "0" : "1");
258
+ // 内联样式兜底(font-enhancer 的 display:block !important 若因时机问题未覆盖)
259
+ var t = document.getElementById("dsh-fe-toggle");
260
+ if (t) t.style.setProperty("display", readStore(FONT_BALL_VIS_KEY, true) ? "" : "none", "important");
261
+ }
262
+ function applyBallVisibility() {
263
+ var el = document.documentElement || document.body;
264
+ el.setAttribute("data-dsb-ball-hidden", readStore(BALL_VIS_KEY, true) ? "0" : "1");
265
+ }
266
+
267
+ // ---- panel ----
268
+ var PANEL_CSS =
269
+ '#dsh-skill-browser-panel{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);' +
270
+ 'width:min(940px,94vw);height:min(660px,88vh);display:flex;flex-direction:column;' +
271
+ 'background:rgba(24,26,34,.97);color:#e8eaf2;border:1px solid rgba(255,255,255,.1);border-radius:14px;' +
272
+ 'box-shadow:0 12px 48px rgba(0,0,0,.5);z-index:2147481100;overflow:hidden;font-family:inherit}' +
273
+ '#dsb-built{display:flex;flex-direction:column;min-height:0;flex:1}' +
274
+ '.dsb-head{display:flex;align-items:center;justify-content:space-between;padding:12px 16px 6px}' +
275
+ '.dsb-title{font-size:16px;font-weight:600}' +
276
+ '.dsb-headbtns{display:flex;gap:6px}' +
277
+ '.dsb-btn{background:rgba(255,255,255,.08);color:#dfe3ee;border:1px solid rgba(255,255,255,.12);border-radius:8px;' +
278
+ 'padding:4px 10px;font-size:12px;cursor:pointer}' +
279
+ '.dsb-btn:hover{background:rgba(255,255,255,.16)}' +
280
+ '.dsb-subtitle{padding:0 16px 6px;font-size:11px;color:#9aa1b5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
281
+ '.dsb-settings{display:none;padding:6px 16px;gap:6px;flex-direction:column;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.06);margin-bottom:4px}' +
282
+ '.dsb-set-row{display:flex;align-items:center;gap:8px;font-size:12.5px;cursor:pointer;padding:2px 0}' +
283
+ '.dsb-set-row input{accent-color:#5b6cff}' +
284
+ '.dsb-set-hint{font-size:11px;color:#9aa1b5}' +
285
+ '.dsb-set-rec{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12.5px;padding:3px 0}' +
286
+ '.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}' +
287
+ '.dsb-set-rec a:hover{background:rgba(255,210,122,.15);border-color:rgba(255,210,122,.4)}' +
288
+ '.dsb-q{margin:4px 16px 8px;padding:7px 12px;border-radius:9px;border:1px solid rgba(255,255,255,.14);' +
289
+ 'background:rgba(255,255,255,.06);color:#eef;font-size:13px;outline:none}' +
290
+ '.dsb-q:focus{border-color:#5b8cff}' +
291
+ '.dsb-chips{display:flex;flex-wrap:wrap;gap:6px;padding:0 16px 6px}' +
292
+ '.dsb-chip,.dsb-schip{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#cdd3e3;' +
293
+ 'border-radius:999px;padding:3px 10px;font-size:12px;cursor:pointer;white-space:nowrap}' +
294
+ '.dsb-chip:hover,.dsb-schip:hover{background:rgba(255,255,255,.14)}' +
295
+ '.dsb-chip-on{background:linear-gradient(135deg,#5b6cff,#2fa8ff);color:#fff;border-color:transparent}' +
296
+ '.dsb-n{opacity:.75;font-size:11px;margin-left:2px}' +
297
+ '.dsb-subs{display:none;flex-wrap:wrap;gap:6px;padding:0 16px 6px;border-top:1px dashed rgba(255,255,255,.08);padding-top:8px;margin:0 0 2px}' +
298
+ '.dsb-cards{flex:1;min-height:0;overflow-y:auto;display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));' +
299
+ 'gap:8px;padding:6px 16px 14px;align-content:start}' +
300
+ '.dsb-card{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);border-radius:10px;' +
301
+ 'padding:10px 12px;cursor:pointer;transition:background .12s}' +
302
+ '.dsb-card:hover{background:rgba(120,140,255,.14);border-color:rgba(120,140,255,.4)}' +
303
+ '.dsb-card-name{font-family:ui-monospace,Consolas,monospace;font-size:12px;color:#8fc7ff;word-break:break-all}' +
304
+ '.dsb-card-zh{font-size:12.5px;color:#dfe3ee;margin-top:5px;line-height:1.5;' +
305
+ 'display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}' +
306
+ '.dsb-card-tag{font-size:10.5px;color:#8b93a8;margin-top:6px}' +
307
+ '.dsb-empty{grid-column:1/-1;text-align:center;color:#9aa1b5;padding:40px 0;font-size:13px;line-height:1.7}' +
308
+ '.dsb-detail{position:absolute;inset:0;background:rgba(24,26,34,.99);flex-direction:column;z-index:2}' +
309
+ '.dsb-detail-body{flex:1;min-height:0;overflow:auto;margin:0;padding:6px 16px 16px;' +
310
+ 'font-family:ui-monospace,Consolas,monospace;font-size:12px;line-height:1.65;color:#cfd6e8;white-space:pre-wrap;word-break:break-word}' +
311
+ '.dsb-tabs{display:flex;gap:4px;margin:0 8px}' +
312
+ '.dsb-tab{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#cdd3e3;' +
313
+ 'border-radius:8px;padding:4px 12px;font-size:12px;cursor:pointer}' +
314
+ '.dsb-tab:hover{background:rgba(255,255,255,.14)}' +
315
+ '.dsb-tab-on{background:linear-gradient(135deg,#5b6cff,#2fa8ff);color:#fff;border-color:transparent}' +
316
+ '.dsb-ledger{flex:1;min-height:0;display:flex;flex-direction:column;padding:0 16px 14px}' +
317
+ '.dsb-ledger-sum{display:flex;gap:10px;padding:4px 0 10px}' +
318
+ '.dsb-lsum-card{flex:1;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.09);border-radius:10px;' +
319
+ 'padding:10px;text-align:center}' +
320
+ '.dsb-lsum-card b{display:block;font-size:22px;color:#8fc7ff}' +
321
+ '.dsb-lsum-card span{font-size:11px;color:#9aa1b5}' +
322
+ '.dsb-lsum-fail b{color:#ff9d9d}' +
323
+ '.dsb-ledger-table{flex:1;min-height:0;overflow-y:auto}' +
324
+ '.dsb-ltable{display:flex;flex-direction:column;gap:4px}' +
325
+ '.dsb-lrow{display:grid;grid-template-columns:34px 1fr 44px 44px 92px 1.2fr;gap:6px;align-items:center;' +
326
+ 'background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);border-radius:8px;padding:7px 10px;' +
327
+ 'font-size:12px;cursor:pointer;transition:background .12s}' +
328
+ '.dsb-lrow:hover{background:rgba(120,140,255,.14)}' +
329
+ '.dsb-lhead{background:transparent;border-color:transparent;cursor:default;font-size:11px;color:#8b93a8}' +
330
+ '.dsb-lc-skill{font-family:ui-monospace,Consolas,monospace;color:#8fc7ff;word-break:break-all}' +
331
+ '.dsb-lc-fail{color:#ff9d9d;font-weight:700;text-align:center}' +
332
+ '.dsb-lc-rate{text-align:center}' +
333
+ '.dsb-lv-high{color:#ff7d7d;font-weight:700}' +
334
+ '.dsb-lv-mid{color:#ffb35c;font-weight:700}' +
335
+ '.dsb-lv-low{color:#ffd27a}' +
336
+ '.dsb-lst-done{color:#6fd48a}' +
337
+ '.dsb-lst-warn{color:#ffb35c}' +
338
+ '.dsb-lst-ok{color:#9aa1b5}';
339
+
340
+ function buildPanel() {
341
+ if (S.panelBuilt) return;
342
+ var style = document.createElement("style");
343
+ style.id = "dsb-panel-style";
344
+ style.textContent = PANEL_CSS;
345
+ document.head.appendChild(style);
346
+
347
+ var panel = document.createElement("div");
348
+ panel.id = PANEL_ID;
349
+ panel.innerHTML =
350
+ '<div class="dsb-head">' +
351
+ '<div class="dsb-title">🧩 技能浏览器</div>' +
352
+ '<div class="dsb-tabs">' +
353
+ '<button id="dsb-tab-skills" class="dsb-tab" data-view="skills">技能浏览</button>' +
354
+ '<button id="dsb-tab-ledger" class="dsb-tab" data-view="ledger">📋 失效台账</button>' +
355
+ '</div>' +
356
+ '<div class="dsb-headbtns">' +
357
+ '<button id="dsb-setting" class="dsb-btn" title="开关设置">⚙</button>' +
358
+ '<button id="dsb-refresh" class="dsb-btn" title="重新扫描技能目录">↻ 刷新</button>' +
359
+ '<button id="dsb-close" class="dsb-btn" title="关闭">×</button>' +
360
+ '</div>' +
361
+ '</div>' +
362
+ '<div id="dsb-subtitle" class="dsb-subtitle"></div>' +
363
+ '<div id="dsb-settings" class="dsb-settings" style="display:none">' +
364
+ '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-ball" /> 技能浏览器悬浮球(🧩)</label>' +
365
+ '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-font" /> 字体插件悬浮球(🔤)</label>' +
366
+ '<div class="dsb-set-row" style="flex-wrap:wrap">' +
367
+ '<span style="min-width:70px">技能库位置</span>' +
368
+ '<input id="dsb-set-root" type="text" placeholder="留空=自动识别(~/.dsh/skills 等)" style="flex:1;min-width:220px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);border-radius:6px;color:#eef;font-size:12px;padding:4px 8px;outline:none" />' +
369
+ '<button id="dsb-set-root-browse" class="dsb-btn" title="打开系统文件夹选择框">📁 选择</button>' +
370
+ '<button id="dsb-set-root-save" class="dsb-btn">保存</button>' +
371
+ '</div>' +
372
+ '<label class="dsb-set-row"><input type="checkbox" id="dsb-set-ledger" /> 失效台账自动登记(记录每次技能工具调用结果)</label>' +
373
+ '<span id="dsb-set-status" class="dsb-set-hint">当前技能库:识别中…</span>' +
374
+ '<span class="dsb-set-hint">💡 悬浮球显隐也可在 <b>DSH 设置页 → 「悬浮球导航」分区</b> 中控制;悬浮球可拖动;清单面板始终居中</span>' +
375
+ '<div style="padding-top:4px;border-top:1px dashed rgba(255,255,255,.08);margin-top:4px"></div>' +
376
+ '<div class="dsb-set-rec">' +
377
+ '<span>🖼️ 照片显示器 — 聊天图片灯箱(放大/下载/浏览)</span>' +
378
+ '<a href="https://github.com/loyalchiiina/dsh-chat-image-lightbox" target="_blank" rel="noopener">⭐ Star</a>' +
379
+ '</div>' +
380
+ '<div class="dsb-set-rec">' +
381
+ '<span>🔤 字体插件 — 区域自定义字体/颜色/字号</span>' +
382
+ '<a href="https://github.com/loyalchiiina/dsh-font-enhancer" target="_blank" rel="noopener">⭐ Star</a>' +
383
+ '</div>' +
384
+ '</div>' +
385
+ '<input id="dsb-q" class="dsb-q" type="text" placeholder="🔍 搜索技能名 / 中文简介 / 描述…" />' +
386
+ '<div id="dsb-chips" class="dsb-chips"></div>' +
387
+ '<div id="dsb-subs" class="dsb-subs"></div>' +
388
+ '<div id="dsb-cards" class="dsb-cards"></div>' +
389
+ '<div id="dsb-ledger" class="dsb-ledger" style="display:none">' +
390
+ '<div id="dsb-ledger-sum" class="dsb-ledger-sum"></div>' +
391
+ '<div id="dsb-ledger-table" class="dsb-ledger-table"></div>' +
392
+ '</div>' +
393
+ '<div id="dsb-detail" class="dsb-detail" style="display:none">' +
394
+ '<div class="dsb-head">' +
395
+ '<div id="dsb-detail-title" class="dsb-title" style="font-size:14px"></div>' +
396
+ '<div class="dsb-headbtns">' +
397
+ '<button id="dsb-copy" class="dsb-btn" title="复制全文">复制</button>' +
398
+ '<button id="dsb-back" class="dsb-btn">← 返回</button>' +
399
+ '</div>' +
400
+ '</div>' +
401
+ '<pre id="dsb-detail-body" class="dsb-detail-body"></pre>' +
402
+ '</div>';
403
+ mountEl().appendChild(panel);
404
+
405
+ $("dsb-close").addEventListener("click", function () { closePanel(); });
406
+ $("dsb-refresh").addEventListener("click", function () { if (S.view === "ledger") loadLedgerRefresh(); else load(true); });
407
+ $("dsb-setting").addEventListener("click", function () {
408
+ var box = $("dsb-settings");
409
+ if (!box) return;
410
+ var show = box.style.display !== "block";
411
+ box.style.display = show ? "block" : "none";
412
+ if (show) { syncSettingsUI(); loadConfig(paintConfigStatus); }
413
+ });
414
+ var ballCb = $("dsb-set-ball"), fontCb = $("dsb-set-font");
415
+ if (ballCb) ballCb.addEventListener("change", function () {
416
+ writeStore(BALL_VIS_KEY, ballCb.checked);
417
+ applyBallVisibility();
418
+ if (!ballCb.checked && S.open) closePanel();
419
+ });
420
+ if (fontCb) fontCb.addEventListener("change", function () {
421
+ writeStore(FONT_BALL_VIS_KEY, fontCb.checked);
422
+ applyFontBallVisibility();
423
+ });
424
+ var ledgerCb = $("dsb-set-ledger");
425
+ if (ledgerCb) ledgerCb.addEventListener("change", function () {
426
+ saveConfig({ ledgerEnabled: ledgerCb.checked });
427
+ });
428
+ var rootSave = $("dsb-set-root-save");
429
+ if (rootSave) rootSave.addEventListener("click", function () {
430
+ var inp = $("dsb-set-root");
431
+ if (inp) saveConfig({ skillsDir: inp.value });
432
+ });
433
+ var rootBrowse = $("dsb-set-root-browse");
434
+ if (rootBrowse) rootBrowse.addEventListener("click", function () { browseForDirectory(); });
435
+ $("dsb-q").addEventListener("input", function () { S.q = this.value; S.sub = ""; paintChips(); paintSubs(); paintCards(); });
436
+ $("dsb-back").addEventListener("click", function () { S.detail = null; S.ledgerDetail = null; paint(); });
437
+ $("dsb-tab-skills").addEventListener("click", function () { S.view = "skills"; S.detail = null; S.ledgerDetail = null; paint(); });
438
+ $("dsb-tab-ledger").addEventListener("click", function () { S.view = "ledger"; S.detail = null; if (!S.ledger) loadLedger(); else paint(); });
439
+ $("dsb-ledger-table").addEventListener("click", function (ev) {
440
+ var el = ev.target.closest("[data-lskill]");
441
+ if (!el) return;
442
+ openLedgerDetail(el.getAttribute("data-lskill"));
443
+ });
444
+ $("dsb-chips").addEventListener("click", function (ev) {
445
+ var el = ev.target.closest("[data-cat]");
446
+ if (!el) return;
447
+ S.cat = el.getAttribute("data-cat");
448
+ S.sub = "";
449
+ paintChips(); paintSubs(); paintCards();
450
+ });
451
+ $("dsb-subs").addEventListener("click", function (ev) {
452
+ var el = ev.target.closest("[data-sub]");
453
+ if (!el) return;
454
+ S.sub = el.getAttribute("data-sub");
455
+ paintSubs(); paintCards();
456
+ });
457
+ $("dsb-cards").addEventListener("click", function (ev) {
458
+ var ext = ev.target.closest("[data-ext-url]");
459
+ if (ext) {
460
+ try { window.open(ext.getAttribute("data-ext-url"), "_blank", "noopener"); } catch (e2) {}
461
+ return;
462
+ }
463
+ var el = ev.target.closest("[data-dir]");
464
+ if (!el) return;
465
+ openDetail(el.getAttribute("data-dir"));
466
+ });
467
+
468
+ S.panelBuilt = true;
469
+ }
470
+
471
+ function closePanel() {
472
+ S.open = false;
473
+ S.detail = null;
474
+ var p = $(PANEL_ID);
475
+ if (p && p.parentNode) p.parentNode.removeChild(p);
476
+ var st = $("dsb-panel-style");
477
+ if (st && st.parentNode) st.parentNode.removeChild(st);
478
+ S.panelBuilt = false;
479
+ }
480
+
481
+ function openPanel() {
482
+ buildPanel();
483
+ S.open = true;
484
+ paint();
485
+ if (!S.data && !S.loading) load(false);
486
+ }
487
+
488
+ function paint() {
489
+ var panel = $(PANEL_ID);
490
+ if (!panel || !S.panelBuilt) return;
491
+ panel.style.display = S.open ? "flex" : "none";
492
+ if (!S.open) return;
493
+ var isLedger = S.view === "ledger";
494
+ var q = $("dsb-q"), chips = $("dsb-chips"), subs = $("dsb-subs"), cards = $("dsb-cards"), lg = $("dsb-ledger");
495
+ if (q) q.style.display = isLedger ? "none" : "";
496
+ if (chips) chips.style.display = isLedger ? "none" : "";
497
+ if (subs) subs.style.display = isLedger ? "none" : "";
498
+ if (cards) cards.style.display = isLedger ? "none" : "";
499
+ if (lg) lg.style.display = isLedger ? "block" : "none";
500
+ var t1 = $("dsb-tab-skills"), t2 = $("dsb-tab-ledger");
501
+ if (t1) t1.classList.toggle("dsb-tab-on", !isLedger);
502
+ if (t2) t2.classList.toggle("dsb-tab-on", isLedger);
503
+ paintHeader();
504
+ if (isLedger) paintLedger();
505
+ else { paintChips(); paintSubs(); paintCards(); }
506
+ paintDetail();
507
+ }
508
+
509
+ function paintHeader() {
510
+ var sub = $("dsb-subtitle");
511
+ if (!sub) return;
512
+ if (S.loading && !S.data) { sub.textContent = "扫描技能目录中…"; return; }
513
+ if (S.error) { sub.innerHTML = '<span style="color:#ff8f8f">' + esc(S.error) + '</span>'; return; }
514
+ if (!S.data) return;
515
+ sub.textContent = "共 " + S.data.total + " 个技能 · " + (S.data.root || "?") +
516
+ " · 生成于 " + String(S.data.generatedAt || "").replace("T", " ").slice(0, 16);
517
+ }
518
+
519
+ function paintChips() {
520
+ var box = $("dsb-chips"); if (!box) return;
521
+ if (!S.data) { box.innerHTML = ""; return; }
522
+ var cs = S.data.categories || [];
523
+ var counts = {};
524
+ var sk = S.data.skills || [];
525
+ for (var i = 0; i < sk.length; i++) counts[sk[i].cat] = (counts[sk[i].cat] || 0) + 1;
526
+ var html = chip("all", "🧩 全部", S.data.total, S.cat === "all");
527
+ for (var j = 0; j < cs.length; j++) {
528
+ var c = cs[j];
529
+ html += chip(c.id, (c.icon || "") + " " + esc(c.label), counts[c.id] || 0, S.cat === c.id);
530
+ }
531
+ box.innerHTML = html;
532
+ }
533
+ function chip(id, label, count, active) {
534
+ return '<button class="dsb-chip' + (active ? " dsb-chip-on" : "") + '" data-cat="' + esc(id) + '">' +
535
+ esc(label) + ' <span class="dsb-n">' + count + "</span></button>";
536
+ }
537
+
538
+ function paintSubs() {
539
+ var box = $("dsb-subs"); if (!box) return;
540
+ var m = catMeta(S.cat);
541
+ if (!m || !m.subs || !m.subs.length || S.cat === "all") { box.innerHTML = ""; box.style.display = "none"; return; }
542
+ box.style.display = "flex";
543
+ var counts = {};
544
+ var sk = S.data.skills || [];
545
+ var q = S.q.trim().toLowerCase();
546
+ for (var i = 0; i < sk.length; i++) {
547
+ var s = sk[i];
548
+ if (s.cat !== S.cat) continue;
549
+ if (q && (s.dir + " " + s.name + " " + (s.zh || "") + " " + (s.desc || "")).toLowerCase().indexOf(q) < 0) continue;
550
+ counts[s.sub || "_"] = (counts[s.sub || "_"] || 0) + 1;
551
+ }
552
+ var total = 0, k;
553
+ for (k in counts) total += counts[k];
554
+ var html = '<button class="dsb-schip' + (S.sub === "" ? " dsb-chip-on" : "") + '" data-sub="">全部 ' + total + "</button>";
555
+ for (var j = 0; j < m.subs.length; j++) {
556
+ var sb = m.subs[j];
557
+ var n = counts[sb.id] || 0;
558
+ if (!n) continue;
559
+ html += '<button class="dsb-schip' + (S.sub === sb.id ? " dsb-chip-on" : "") + '" data-sub="' + esc(sb.id) + '">' +
560
+ esc(sb.label) + " " + n + "</button>";
561
+ }
562
+ box.innerHTML = html;
563
+ }
564
+
565
+ function paintCards() {
566
+ var box = $("dsb-cards"); if (!box) return;
567
+ if (S.error && !S.data) {
568
+ box.innerHTML = '<div class="dsb-empty">' + esc(S.error) + '<br/><br/><button class="dsb-btn" id="dsb-retry">重试</button></div>';
569
+ var r = $("dsb-retry");
570
+ if (r) r.addEventListener("click", function () { load(true); });
571
+ return;
572
+ }
573
+ if (!S.data) { box.innerHTML = '<div class="dsb-empty">加载中…</div>'; return; }
574
+ var list = filtered();
575
+ if (!list.length) { box.innerHTML = '<div class="dsb-empty">没有匹配的技能</div>'; return; }
576
+ var html = "";
577
+ for (var i = 0; i < list.length; i++) {
578
+ var s = list[i];
579
+ var tag = s.sub ? esc(subLabel(s.cat, s.sub)) : esc(catLabel(s.cat));
580
+ var brief = s.zh || (s.desc ? String(s.desc).slice(0, 90) + (s.desc.length > 90 ? "…" : "") : "(无描述)");
581
+ html += '<div class="dsb-card" data-dir="' + esc(s.dir) + '" title="点击查看 SKILL.md 全文">' +
582
+ '<div class="dsb-card-name">' + esc(s.dir) + "</div>" +
583
+ '<div class="dsb-card-zh">' + esc(brief) + "</div>" +
584
+ '<div class="dsb-card-tag">' + tag + "</div>" +
585
+ "</div>";
586
+ }
587
+ // v1.5.3(用户要求):技能列表每页末尾不再追加推荐插件广告卡,
588
+ // 推荐位只保留在面板 ⚙ 设置展开区(技能库位置下方)一处。
589
+ // v1.5.4(用户要求):「💡 悬浮球显隐设置」提示卡也一并移除,
590
+ // 每页末尾不再追加任何非技能卡片,列表只展示技能本身。
591
+ box.innerHTML = html;
592
+ }
593
+
594
+ function ledgerLevel(fails) {
595
+ if (fails >= 4) return { cls: "dsb-lv-high", label: "高" };
596
+ if (fails >= 2) return { cls: "dsb-lv-mid", label: "中" };
597
+ return { cls: "dsb-lv-low", label: "低" };
598
+ }
599
+
600
+ function paintLedger() {
601
+ var sum = $("dsb-ledger-sum"), tbl = $("dsb-ledger-table");
602
+ if (!sum || !tbl) return;
603
+ if (S.loading && !S.ledger) { sum.innerHTML = '<div class="dsb-empty">台账加载中…</div>'; tbl.innerHTML = ""; return; }
604
+ if (S.error && !S.ledger) { sum.innerHTML = '<div class="dsb-empty">' + esc(S.error) + '</div>'; tbl.innerHTML = ""; return; }
605
+ if (!S.ledger) return;
606
+ var l = S.ledger;
607
+ var totalSkills = (S.data && S.data.total) || 0;
608
+ var autoRecent = l.autoRecent || [];
609
+ var autoFails = autoRecent.filter(function (x) { return !x.ok; }).length;
610
+ var autoTotal = l.autoTotal || 0;
611
+ var autoFailTotal = l.autoFailTotal || 0;
612
+ sum.innerHTML =
613
+ '<div class="dsb-lsum-card"><b>' + totalSkills + '</b><span>技能总数</span></div>' +
614
+ '<div class="dsb-lsum-card dsb-lsum-fail"><b>' + l.totalFailSkills + '</b><span>人工·失败技能</span></div>' +
615
+ '<div class="dsb-lsum-card dsb-lsum-fail"><b>' + l.totalFails + '</b><span>人工·累计失败</span></div>' +
616
+ '<div class="dsb-lsum-card"><b>' + autoTotal + '</b><span>自动·调用总数</span></div>' +
617
+ (autoFailTotal ? '<div class="dsb-lsum-card dsb-lsum-fail"><b>' + autoFailTotal + '</b><span>自动·失败</span></div>' : '');
618
+ var rows = (l.entries || []).filter(function (e) { return e.fails > 0; });
619
+ var html = '<div style="display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;margin:2px 0 6px">' +
620
+ '<span>👤 人工台账</span>' +
621
+ '<span style="font-weight:400;font-size:11px;color:#9aa1b5">技能生效-失效台账.md 统计表(人工维护 + 历史登记)</span>' +
622
+ '</div>' +
623
+ '<div class="dsb-ltable">' +
624
+ '<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>';
625
+ if (!rows.length) {
626
+ 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>';
627
+ }
628
+ for (var i = 0; i < rows.length; i++) {
629
+ var e = rows[i];
630
+ var lv = ledgerLevel(e.fails);
631
+ var status = e.status || "";
632
+ var statusCls = /待强化|⚠/.test(status) ? "dsb-lst-warn" : /已强化/.test(status) ? "dsb-lst-done" : "dsb-lst-ok";
633
+ html += '<div class="dsb-lrow" data-lskill="' + esc(e.skill) + '" title="点击查看失败明细">' +
634
+ '<span class="dsb-lc-rank">' + esc(e.rank || "—") + '</span>' +
635
+ '<span class="dsb-lc-skill">' + esc(e.skill) + '</span>' +
636
+ '<span class="dsb-lc-fail">' + e.fails + '</span>' +
637
+ '<span class="dsb-lc-rate"><span class="' + lv.cls + '">' + lv.label + '</span></span>' +
638
+ '<span class="dsb-lc-date">' + esc(e.lastDate || "—") + '</span>' +
639
+ '<span class="dsb-lc-status ' + statusCls + '">' + esc(status) + '</span>' +
640
+ '</div>';
641
+ }
642
+ html += '</div>';
643
+ // 自动登记流水(与人工台账分开的独立区块,最新在上)
644
+ var auto = l.autoRecent || [];
645
+ 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)">' +
646
+ '<span>🤖 自动登记</span>' +
647
+ '<span style="font-weight:400;font-size:11px;color:#9aa1b5">dsh-skill-browser 插件监听 skill 工具调用结果自动记录 · 近 ' + auto.length + ' 条,最新在前</span>' +
648
+ '</div>';
649
+ if (!auto.length) {
650
+ html += '<div class="dsb-empty" style="padding:14px 0">暂无自动记录 — 插件会记录每次技能工具调用结果(成功✅ / 失败❌)</div>';
651
+ } else {
652
+ html += '<div class="dsb-ltable">';
653
+ for (var a = 0; a < auto.length; a++) {
654
+ var x = auto[a];
655
+ var mark = x.ok ? "✅" : "❌";
656
+ var cls = x.ok ? "dsb-lst-done" : "dsb-lst-warn";
657
+ var note = x.note || x.error || "";
658
+ html += '<div class="dsb-lrow" style="font-size:11.5px' + (x.ok ? ';opacity:.62' : '') + '"' + (x.ok ? '' : ' data-lskill="' + esc(x.skill) + '" title="点击查看失败详情"') + '>' +
659
+ '<span class="dsb-lc-date" style="width:110px;flex:none">' + esc(String(x.ts || "").replace("T", " ").slice(5, 16)) + '</span>' +
660
+ '<span class="' + cls + '" style="width:22px;flex:none;text-align:center">' + mark + '</span>' +
661
+ '<span class="dsb-lc-skill" style="flex:1">' + esc(x.skill) + '</span>' +
662
+ '<span style="width:64px;flex:none;color:#8b93a8;font-size:10.5px">' + esc(x.kind === "manual-report" ? "人工上报" : "工具结果") + '</span>' +
663
+ '<span style="flex:2;color:#9aa1b5;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(x.kind || "") + (note ? " — " + esc(note.slice(0, 80)) : "") + '</span>' +
664
+ '</div>';
665
+ }
666
+ html += '</div>';
667
+ }
668
+ tbl.innerHTML = html;
669
+ }
670
+
671
+ function paintDetail() {
672
+ var d = $("dsb-detail"); if (!d) return;
673
+ var isLedgerDetail = S.view === "ledger" && S.ledgerDetail;
674
+ if (S.detail || isLedgerDetail) {
675
+ d.style.display = "flex";
676
+ var t = $("dsb-detail-title");
677
+ if (t) {
678
+ if (isLedgerDetail) t.textContent = "📋 " + S.ledgerDetail.skill + " — 失败明细";
679
+ else t.textContent = "📄 " + S.detail.dir + " / SKILL.md";
680
+ }
681
+ var b = $("dsb-detail-body");
682
+ if (b && !b.textContent) b.textContent = "加载中…";
683
+ } else {
684
+ d.style.display = "none";
685
+ }
686
+ }
687
+
688
+ function syncSettingsUI() {
689
+ var ballCb = $("dsb-set-ball"), fontCb = $("dsb-set-font");
690
+ if (ballCb) ballCb.checked = readStore(BALL_VIS_KEY, true);
691
+ if (fontCb) fontCb.checked = readStore(FONT_BALL_VIS_KEY, true);
692
+ }
693
+
694
+ // ---- 球体样式 + 字体球统一样式 + 显隐规则(纯 CSS,永久生效)----
695
+ var CSS = [
696
+ /* 技能球 */
697
+ '#' + BALL_ID + '{position:fixed;right:26px;bottom:118px;width:46px;height:46px;border-radius:50%;' +
698
+ 'background:linear-gradient(135deg,#6a5cff,#2fb7ff);color:#fff;font-size:22px;display:flex;align-items:center;justify-content:center;' +
699
+ 'cursor:grab;z-index:2147481000;box-shadow:0 4px 16px rgba(40,80,255,.35);user-select:none;touch-action:none;transition:transform .15s ease}',
700
+ '#' + BALL_ID + ':hover{transform:scale(1.08)}',
701
+ '#' + BALL_ID + '.dsb-ghost{width:12px!important;height:12px!important;font-size:0!important;right:10px!important;bottom:10px!important;opacity:.3}',
702
+ '#' + BALL_ID + '.dsb-ghost:hover{opacity:.8}',
703
+ /* 字体球统一样式:与技能球同尺寸,暖色渐变,🔤 图标(纯 CSS,幂等,不动 font-enhancer) */
704
+ '#dsh-fe-root #dsh-fe-toggle{width:46px!important;height:46px!important;' +
705
+ 'line-height:46px!important;font-size:0!important;color:transparent!important;' +
706
+ 'display:flex!important;align-items:center!important;justify-content:center!important;' +
707
+ 'background:linear-gradient(135deg,#ff8c42,#ffd27a)!important;' +
708
+ 'box-shadow:0 4px 16px rgba(255,140,66,.35)!important}',
709
+ '#dsh-fe-root #dsh-fe-toggle::after{content:"🔤";font-size:20px;color:#fff;display:block;line-height:1}',
710
+ /* 显隐开关:取消勾选=彻底隐藏球(display:none),勾选=显示原版 46px 大球 */
711
+ '[data-dsb-ball-hidden="1"] #' + BALL_ID + '{display:none!important}',
712
+ '[data-dsb-font-hidden="1"] #dsh-fe-toggle{display:none!important}'
713
+ ].join('\n');
714
+
715
+ function restoreBallPos(ball) {
716
+ try {
717
+ var raw = localStorage.getItem(BALL_POS_KEY);
718
+ if (!raw) return;
719
+ var p = JSON.parse(raw);
720
+ if (typeof p.left === "number" && typeof p.top === "number") {
721
+ ball.style.left = p.left + "px";
722
+ ball.style.top = p.top + "px";
723
+ ball.style.right = "auto";
724
+ ball.style.bottom = "auto";
725
+ }
726
+ } catch (e) {}
727
+ }
728
+ function saveBallPos(ball) {
729
+ try {
730
+ localStorage.setItem(BALL_POS_KEY, JSON.stringify({ left: ball.offsetLeft, top: ball.offsetTop }));
731
+ } catch (e) {}
732
+ }
733
+
734
+ // v1.5.3 一键恢复默认位置:清位置记忆与收起/隐藏状态,球立即回右下角默认位。
735
+ // 供 DSH 设置页「悬浮球导航」分区按钮调用(球可能被拖出屏幕外卡住,无法点回)。
736
+ function resetBallToDefault() {
737
+ try { localStorage.removeItem(BALL_POS_KEY); } catch (e) {}
738
+ try { writeStore(BALL_VIS_KEY, true); } catch (e) {}
739
+ try { writeStore(GHOST_KEY, false); } catch (e) {}
740
+ var b = $(BALL_ID);
741
+ if (b) {
742
+ b.style.left = ""; b.style.top = "";
743
+ b.style.right = "26px"; b.style.bottom = "118px";
744
+ b.classList.remove("dsb-ghost");
745
+ }
746
+ try { applyBallVisibility(); } catch (e) {}
747
+ try { console.log("[dsh-skill-browser] ball reset to default position (bottom-right)"); } catch (e) {}
748
+ }
749
+
750
+ function buildBall() {
751
+ if ($(BALL_ID)) return;
752
+ var style = document.createElement("style");
753
+ style.id = "dsb-ball-style";
754
+ style.textContent = CSS;
755
+ document.head.appendChild(style);
756
+
757
+ var ball = document.createElement("div");
758
+ ball.id = BALL_ID;
759
+ ball.title = "技能浏览器(点击开关面板,可拖动,Alt+点击 收起成小圆点)";
760
+ ball.textContent = "🧩";
761
+
762
+ var drag = { active: false, moved: false, sx: 0, sy: 0, ox: 0, oy: 0 };
763
+ ball.addEventListener("pointerdown", function (e) {
764
+ drag.active = true; drag.moved = false;
765
+ drag.sx = e.clientX; drag.sy = e.clientY;
766
+ var r = ball.getBoundingClientRect();
767
+ drag.ox = r.left; drag.oy = r.top;
768
+ ball.style.transition = "none";
769
+ try { ball.setPointerCapture(e.pointerId); } catch (e2) {}
770
+ });
771
+ ball.addEventListener("pointermove", function (e) {
772
+ if (!drag.active) return;
773
+ var dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
774
+ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) drag.moved = true;
775
+ ball.style.left = (drag.ox + dx) + "px";
776
+ ball.style.top = (drag.oy + dy) + "px";
777
+ ball.style.right = "auto";
778
+ ball.style.bottom = "auto";
779
+ });
780
+ function endDrag() {
781
+ if (!drag.active) return;
782
+ drag.active = false;
783
+ ball.style.transition = "";
784
+ if (drag.moved) saveBallPos(ball);
785
+ }
786
+ ball.addEventListener("pointerup", endDrag);
787
+ ball.addEventListener("pointercancel", endDrag);
788
+
789
+ ball.addEventListener("click", function (ev) {
790
+ if (drag.moved) { drag.moved = false; return; }
791
+ if (ev.altKey) {
792
+ var ghost = !isGhost();
793
+ writeStore(GHOST_KEY, ghost);
794
+ if (ghost) closePanel();
795
+ paintGhost();
796
+ return;
797
+ }
798
+ if (S.open) closePanel();
799
+ else openPanel();
800
+ });
801
+ mountEl().appendChild(ball);
802
+ restoreBallPos(ball);
803
+ paintGhost();
804
+ applyBallVisibility();
805
+ }
806
+
807
+ function paintGhost() {
808
+ var b = $(BALL_ID);
809
+ if (!b) return;
810
+ if (isGhost()) b.classList.add("dsb-ghost");
811
+ else b.classList.remove("dsb-ghost");
812
+ }
813
+
814
+ // ---- DSH 设置界面:settings.section 区块(纯函数组件)----
815
+ // DIAG 版:组件被调用时上报 React 状态到宿主 /diag,用于定位内容为空原因。
816
+ function diagReport(info) {
817
+ try {
818
+ fetch(apiBase() + "/diag", {
819
+ method: "POST",
820
+ headers: { "content-type": "application/json" },
821
+ body: JSON.stringify(info)
822
+ }).catch(function () {});
823
+ } catch (e) {}
824
+ }
825
+
826
+ function SettingsSection(props) {
827
+ // 组件渲染即上报:React 可用性 + props 键
828
+ try {
829
+ diagReport({
830
+ view: "settings-section-render",
831
+ reactType: typeof React,
832
+ reactKeys: React ? Object.keys(React).slice(0, 12) : [],
833
+ propsKeys: props ? Object.keys(props) : []
834
+ });
835
+ } catch (e) {}
836
+
837
+ // 若 React 不可用则返回 null(内容空,但至少诊断日志能告诉我们原因)
838
+ if (!React) return null;
839
+
840
+ var rows = [];
841
+ function mkToggle(id, label, onChange) {
842
+ return React.createElement("label", { key: id, style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", cursor: "pointer" } },
843
+ React.createElement("input", { type: "checkbox", id: id, onChange: onChange }),
844
+ React.createElement("span", null, label));
845
+ }
846
+ rows.push(React.createElement("div", { key: "head", style: { fontWeight: "600", marginBottom: "4px" } }, "悬浮球导航"));
847
+ rows.push(mkToggle("dsb-set-ball", "技能浏览器悬浮球(🧩)", function (e) {
848
+ writeStore(BALL_VIS_KEY, e.target.checked);
849
+ applyBallVisibility();
850
+ if (!e.target.checked && S.open) closePanel();
851
+ }));
852
+ rows.push(mkToggle("dsb-set-font", "字体插件悬浮球(🔤)", function (e) {
853
+ writeStore(FONT_BALL_VIS_KEY, e.target.checked);
854
+ applyFontBallVisibility();
855
+ }));
856
+ rows.push(React.createElement("div", { key: "hint", style: { fontSize: "12px", color: "#8b93a8" } },
857
+ "悬浮球可拖动;清单面板始终居中;本分区就在 DSH 设置中,随时可改悬浮球显隐"));
858
+ // ── 一键恢复默认位置(v1.5.3 用户要求):悬浮球被拖出屏幕外卡住时,
859
+ // 点此按钮立即回到默认右下角 (right:26px bottom:118px) 46px 常规大小,
860
+ // 并顺带恢复显示、解除收起小圆点状态。无需重启。──
861
+ rows.push(React.createElement("button", { key: "reset-pos",
862
+ onClick: function () { try { resetBallToDefault(); } catch (e) {} },
863
+ style: { alignSelf: "flex-start", fontSize: "13px", cursor: "pointer",
864
+ background: "rgba(255,210,122,.12)", border: "1px solid rgba(255,210,122,.35)",
865
+ borderRadius: "6px", padding: "6px 12px", color: "#ffd27a" }
866
+ }, "🎯 一键恢复悬浮球默认位置(右下角)"));
867
+ return React.createElement("div", { "data-dsb-slot": "1", style: { display: "flex", flexDirection: "column", gap: "8px", padding: "8px 0" } }, rows);
868
+ }
869
+
870
+ function registerSettings(ctx) {
871
+ if (!ctx || !ctx.slots || !React) return false;
872
+ // 直接调用(官方 dsh-im/archive-manager 均如此,不用 ctx.effect 包裹)
873
+ try {
874
+ ctx.slots.inject("settings.section", function () {
875
+ return ctx.slots.register({
876
+ name: "settings.section",
877
+ id: "dsh-skill-browser",
878
+ order: 22,
879
+ label: function () { return "悬浮球导航"; },
880
+ inject: function () { return {}; }
881
+ }, SettingsSection);
882
+ });
883
+ return true;
884
+ } catch (e) {
885
+ try { console.warn("[dsh-skill-browser] settings registration failed", e); } catch (e2) {}
886
+ return false;
887
+ }
888
+ }
889
+
890
+ // React for the settings.section slot (dsh-im 同款,__ModuleLoader__ 提供 react)
891
+ var React = null;
892
+ try {
893
+ var _react = require("react");
894
+ React = (_react && _react.default) || _react;
895
+ } catch (e) { React = null; }
896
+ // 兜底:若 require 拿不到,从全局 window 上找(部分环境挂 window.React)
897
+ if (!React) { try { React = window.React || null; } catch (e) {} }
898
+
899
+ // ---- 旧状态迁移:v1.2.x 之前用户测试勾选可能在 localStorage 留下
900
+ // "隐藏球"标记,导致球不显示且无法通过界面恢复。升级时强制重置一次。----
901
+ function migrateOldState() {
902
+ try {
903
+ var ver = localStorage.getItem("dsh-skill-browser-cfg-ver");
904
+ if (ver !== "3") {
905
+ localStorage.setItem(BALL_VIS_KEY, "1"); // 球恢复显示
906
+ localStorage.setItem(FONT_BALL_VIS_KEY, "1"); // 字体球恢复显示
907
+ localStorage.removeItem(GHOST_KEY);
908
+ // v3 复位(2026-09-05 用户要求"恢复原状"):同时清除拖拽位置记忆与
909
+ // 收起小圆点状态,让悬浮球回到默认右下角 (right:26px bottom:118px) 常规大小。
910
+ localStorage.removeItem(BALL_POS_KEY);
911
+ localStorage.setItem("dsh-skill-browser-cfg-ver", "3");
912
+ }
913
+ } catch (e) {}
914
+ }
915
+
916
+ // inject 必须导出为纯数组(同 archive-manager/dsh-im:cordis loader 读取
917
+ // exports.inject 判定向 client ctx 注入哪些服务;函数形式曾被验证不生效)。
918
+ // 目录选择走原生 <input type="file" webkitdirectory>,无需 remote seam。
919
+ exports.inject = ["slots", "locale"];
920
+ exports.apply = function (ctx) {
921
+ // 设置分区每次 apply 都重新注册(DSH 热重载会释放 effect 导致分区消失,
922
+ // 不设 mounted 守卫;重复注册由 slots 系统去重/替换)
923
+ try { migrateOldState(); } catch (e) {}
924
+ try { registerSettings(ctx); } catch (e) {}
925
+ if (S.mounted) return;
926
+ S.mounted = true;
927
+ try { buildBall(); } catch (e) {
928
+ try { console.warn("[dsh-skill-browser] mount failed", e); } catch (e2) {}
929
+ return;
930
+ }
931
+ try {
932
+ // 纯 CSS 显隐(html 属性驱动),无需轮询
933
+ applyFontBallVisibility();
934
+ applyBallVisibility();
935
+ } catch (e) {}
936
+ try { console.log("[dsh-skill-browser] client mounted (v1.4.1 slot-only)"); } catch (e3) {}
937
+ };
938
+
939
+ return module.exports;
940
+ }
941
+ });