dsh-skill-browser 1.5.9 → 1.6.1

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