@nimoxie/dsh-ui-tweaks 0.1.2 → 0.1.3
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/DEVELOPING.md +90 -88
- package/LICENSE +21 -21
- package/README.md +67 -67
- package/cordis.patch.yml +7 -7
- package/lib/client.js +1064 -1056
- package/lib/index.js +99 -99
- package/package.json +60 -60
package/lib/client.js
CHANGED
|
@@ -1,1056 +1,1064 @@
|
|
|
1
|
-
/* dsh-ui-tweaks browser half.
|
|
2
|
-
*
|
|
3
|
-
* 三个独立模块,任一失败都不得拖垮其它(各自 guard):
|
|
4
|
-
* 1. tablefix —— 宽表格越界修复(纯 CSS 覆盖,见 README「宽表格修复」)
|
|
5
|
-
* 2. scrollbar —— 对话区左侧自绘滚动条(从 dsh-web-shell 移植,选择器适配 0.2.x)
|
|
6
|
-
* 3. updates —— 插件更新检查面板(不含 dsh 本体;remote.pluginManager/pluginInventory)
|
|
7
|
-
*
|
|
8
|
-
* 装载机制:shell 的 window.__ModuleLoader__.load + factory(require);
|
|
9
|
-
* apply(ctx) 由客户端 cordis 上下文调用(同 dsh-model-organizer 的结构)。
|
|
10
|
-
*/
|
|
11
|
-
window.__ModuleLoader__.load({
|
|
12
|
-
id: "@nimoxie/dsh-ui-tweaks",
|
|
13
|
-
factory: (require) => {
|
|
14
|
-
const module = { exports: {} };
|
|
15
|
-
const exports = module.exports;
|
|
16
|
-
|
|
17
|
-
const BUILD = "0.1.
|
|
18
|
-
|
|
19
|
-
/* 每个模块的注册各套一层 guard:失败只记 warn(座位/模块失败模式是「静默消失」,
|
|
20
|
-
必须主动吭声;但不能让一个模块的异常打断同文件里其它模块的 apply)。 */
|
|
21
|
-
function guard(name, fn) {
|
|
22
|
-
try {
|
|
23
|
-
return fn();
|
|
24
|
-
} catch (e) {
|
|
25
|
-
try { console.warn("[dsh-ui-tweaks:" + BUILD + "]", name, e); } catch (_) {}
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/* ======================================================================
|
|
31
|
-
* 模块 1:宽表格越界修复(对 dsh 本体的只读样式补丁)
|
|
32
|
-
*
|
|
33
|
-
* 缺陷(0.2.0-rc.1 实测仍在):@deepseek-ai/dsh-client-ui-chat 给
|
|
34
|
-
* .md-table-wide(≥4 列且不在引用块内)做了「出血」布局(width 加宽 +
|
|
35
|
-
* margin-left 负值左移 + padding-left 推回),而 primitives 的
|
|
36
|
-
* .tableScroll{max-width:100%} 管不住内部 width:max-content 的表格,
|
|
37
|
-
* overflow-x:hidden 之下内容直接被裁(实测右沿超出可视区约 280px)。
|
|
38
|
-
*
|
|
39
|
-
* 修法:放弃出血,收敛回正文列 —— 同时中和 width / margin-left /
|
|
40
|
-
* padding-left 三件套并放开 overflow-x(三者缺一不可,只改宽度会留下
|
|
41
|
-
* 负 margin,只去 margin 不约束宽度则继续越界)。
|
|
42
|
-
*
|
|
43
|
-
* 防失效设计:
|
|
44
|
-
* ① 选择器用属性前缀 [class*="tableScroll"](CSS modules「哈希_原名」,
|
|
45
|
-
* dsh 换版本改哈希也不失效,绝不硬编码 hash);
|
|
46
|
-
* ② 全部声明带 !important 压过 dsh 自己的规则;
|
|
47
|
-
* ③ 单一 <style id> + 存在性检查,删除节点即完全回滚。
|
|
48
|
-
* ==================================================================== */
|
|
49
|
-
function installTableWidthFix() {
|
|
50
|
-
var STYLE_ID = "dshuiw-table-width-fix";
|
|
51
|
-
if (document.getElementById(STYLE_ID)) { return; }
|
|
52
|
-
var st = document.createElement("style");
|
|
53
|
-
st.id = STYLE_ID;
|
|
54
|
-
st.setAttribute("data-dshuiw", "table-width-fix");
|
|
55
|
-
st.textContent =
|
|
56
|
-
"[class*=\"tableScroll\"]{" +
|
|
57
|
-
"width:100%!important;" +
|
|
58
|
-
"max-width:100%!important;" +
|
|
59
|
-
"margin-left:0!important;" +
|
|
60
|
-
"padding-left:0!important;" +
|
|
61
|
-
"padding-bottom:0!important;" +
|
|
62
|
-
"overflow-x:auto!important;" +
|
|
63
|
-
"}" +
|
|
64
|
-
/* 内部表格:允许按内容排布(保留列宽语义),宽度交给包裹层约束 */
|
|
65
|
-
"[class*=\"tableScroll\"]>table{" +
|
|
66
|
-
"max-width:none!important;" +
|
|
67
|
-
"min-width:max-content;" +
|
|
68
|
-
"}" +
|
|
69
|
-
/* 滚动条尽量细、贴近 dsh 的视觉语言 */
|
|
70
|
-
"[class*=\"tableScroll\"]{scrollbar-width:thin}";
|
|
71
|
-
(document.head || document.documentElement).appendChild(st);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/* ======================================================================
|
|
75
|
-
* 模块 2:对话区左侧自绘滚动条(自绘拇指)
|
|
76
|
-
*
|
|
77
|
-
* 从 dsh-web-shell 的注入脚本移植(该实现经过 A/B 实测):
|
|
78
|
-
* · 宿主挂 document.body + position:fixed —— 挂进 React 管的容器会被
|
|
79
|
-
* 「加载更早」等重渲染抹掉;fixed 还让滚动区自己滚时宿主纹丝不动。
|
|
80
|
-
* · 拖动 = PointerCapture 下的纯函数:sp.scrollTop 由拇指位置一步算出;
|
|
81
|
-
* pointermove 同步写 + scroll 捕获段回纠 + rAF 兜底(三层防御)。
|
|
82
|
-
* · 「加载更早」后 dsh 会钉阅读锚点,只有 wheel/pointerdown 等读者意图
|
|
83
|
-
* 事件会解除 —— 宿主在滚动区外,pointerdown 到不了 dsh 的监听器,
|
|
84
|
-
* 所以按下/滚轮时补发一个零位移 synthetic wheel(notifyReaderIntent)。
|
|
85
|
-
* · 只在「会话视图」显示:scrollport + 右侧调宽条 + 有实际高度三者齐备
|
|
86
|
-
* 才保留宿主,切到插件页等非会话页自动移除。
|
|
87
|
-
* · 高度裁到输入框(composerSeat,sticky 浮在底部)上沿 −4px。
|
|
88
|
-
*
|
|
89
|
-
* 0.2.x 选择器适配(0.1.x 的 data-dsh-part 标记已被 dsh 全部移除):
|
|
90
|
-
* 滚动区 [data-conversation-scroll]
|
|
91
|
-
* 调宽条 [class*="widthHandle"][data-side="right"]
|
|
92
|
-
* 输入框 [class*="composerSeat"]
|
|
93
|
-
* ==================================================================== */
|
|
94
|
-
function installLeftScrollbar() {
|
|
95
|
-
if (window.__DSHUIW_LEFT_SB__) { return; }
|
|
96
|
-
window.__DSHUIW_LEFT_SB__ = true;
|
|
97
|
-
var HOST_ID = "dshuiw-scrollbar-host";
|
|
98
|
-
|
|
99
|
-
function findScroller() {
|
|
100
|
-
return document.querySelector('[data-dsh-part="scrollport"]') ||
|
|
101
|
-
document.querySelector("[data-conversation-scroll]");
|
|
102
|
-
}
|
|
103
|
-
function findRightHandle() {
|
|
104
|
-
var hs = document.querySelectorAll('[data-dsh-part="resize-handle"]');
|
|
105
|
-
for (var i = 0; i < hs.length; i++) {
|
|
106
|
-
if (hs[i].getAttribute("data-side") === "right") { return hs[i]; }
|
|
107
|
-
}
|
|
108
|
-
/* dsh 0.2.x:宽度调整条 = widthHandle 类 + data-side(无 data-dsh-part) */
|
|
109
|
-
return document.querySelector('[class*="widthHandle"][data-side="right"]');
|
|
110
|
-
}
|
|
111
|
-
function findComposer() {
|
|
112
|
-
var c = document.querySelector('[data-dsh-part="composer-input"]');
|
|
113
|
-
if (c) { return c.closest('[class*="composerSeat"]') || c; }
|
|
114
|
-
return document.querySelector('[class*="composerSeat"]');
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
var sp = null, handle = null, host = null, track = null, thumb = null;
|
|
118
|
-
var composerEl = null, ro = null;
|
|
119
|
-
if (window.ResizeObserver) {
|
|
120
|
-
ro = new ResizeObserver(function () { ensure(); });
|
|
121
|
-
}
|
|
122
|
-
var dragging = false, dragOffsetY = 0;
|
|
123
|
-
var curThumbH = 40, curThumbTop = 0, curTrackH = 0;
|
|
124
|
-
var trRect = null;
|
|
125
|
-
var lastPointerY = 0, dragRaf = 0;
|
|
126
|
-
|
|
127
|
-
/* 「加载更早」、切会话等操作可能换掉 scrollport 节点:交互点先校验节点
|
|
128
|
-
身份,变了就重绑,否则写入落在旧节点上 —— 拖动没反应、松手弹回。 */
|
|
129
|
-
function rebindIfScrollerChanged() {
|
|
130
|
-
if (sp && findScroller() !== sp) { ensure(); return true; }
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function visibleHeight() {
|
|
135
|
-
if (!sp) { return 0; }
|
|
136
|
-
var c = findComposer();
|
|
137
|
-
if (!c) { return sp.clientHeight; }
|
|
138
|
-
var cr = c.getBoundingClientRect();
|
|
139
|
-
var sr = sp.getBoundingClientRect();
|
|
140
|
-
if (cr.top >= sr.bottom - 2) { return sp.clientHeight; }
|
|
141
|
-
var v = cr.top - sr.top - 4;
|
|
142
|
-
return (v > 80 && v < sp.clientHeight) ? v : sp.clientHeight;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function build() {
|
|
146
|
-
host = document.createElement("div");
|
|
147
|
-
host.id = HOST_ID;
|
|
148
|
-
host.style.width = "14px";
|
|
149
|
-
var sr = host.attachShadow({ mode: "open" });
|
|
150
|
-
sr.innerHTML = "<style>" +
|
|
151
|
-
":host{position:fixed;z-index:6}" +
|
|
152
|
-
".track{position:absolute;top:0;right:0;width:100%;height:100%;" +
|
|
153
|
-
"user-select:none;-webkit-user-select:none;touch-action:none}" +
|
|
154
|
-
".track[hidden]{display:none}" +
|
|
155
|
-
".thumb{position:absolute;right:0;width:100%;background:transparent}" +
|
|
156
|
-
".thumb::after{content:\"\";position:absolute;top:0;bottom:0;right:3px;width:7px;border-radius:4px;" +
|
|
157
|
-
"background:rgba(128,128,128,.4);" +
|
|
158
|
-
"background:color-mix(in srgb,currentColor 32%,transparent);" +
|
|
159
|
-
"transition:width .12s,right .12s,background .12s}" +
|
|
160
|
-
".track:hover .thumb::after,.thumb.drag::after{right:2px;width:9px;" +
|
|
161
|
-
"background:rgba(128,128,128,.62);" +
|
|
162
|
-
"background:color-mix(in srgb,currentColor 55%,transparent)}" +
|
|
163
|
-
"</style><div class=\"track\" hidden><div class=\"thumb\"></div></div>";
|
|
164
|
-
track = sr.querySelector(".track");
|
|
165
|
-
thumb = sr.querySelector(".thumb");
|
|
166
|
-
wireEvents();
|
|
167
|
-
document.body.appendChild(host);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/* thumb 几何:高 = 轨道 × 可见比,位置 = scrollTop / scrollHeight */
|
|
171
|
-
function render() {
|
|
172
|
-
if (!sp || !track || !thumb) { return; }
|
|
173
|
-
track.hidden = false;
|
|
174
|
-
var range = sp.scrollHeight - sp.clientHeight;
|
|
175
|
-
var th = track.clientHeight;
|
|
176
|
-
if (range <= 0 || th <= 0) { track.hidden = true; return; }
|
|
177
|
-
curTrackH = th;
|
|
178
|
-
curThumbH = Math.round(th * sp.clientHeight / sp.scrollHeight);
|
|
179
|
-
if (curThumbH < 28) { curThumbH = 28; }
|
|
180
|
-
if (curThumbH > th) { curThumbH = th; }
|
|
181
|
-
curThumbTop = Math.round(th * sp.scrollTop / sp.scrollHeight);
|
|
182
|
-
if (curThumbTop > th - curThumbH) { curThumbTop = th - curThumbH; }
|
|
183
|
-
if (curThumbTop < 0) { curThumbTop = 0; }
|
|
184
|
-
if (thumb.style.height !== curThumbH + "px") { thumb.style.height = curThumbH + "px"; }
|
|
185
|
-
if (thumb.style.top !== curThumbTop + "px") { thumb.style.top = curThumbTop + "px"; }
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* 补发零位移 wheel 作纯阅读意图:解除 dsh 的阅读锚点保留(stopPreserving),
|
|
189
|
-
否则「加载更早」后拖动会被判成程序化滚动、松手弹回锚点。 */
|
|
190
|
-
function notifyReaderIntent() {
|
|
191
|
-
if (!sp) { return; }
|
|
192
|
-
var targets = [sp];
|
|
193
|
-
try {
|
|
194
|
-
var outer = sp.closest("[data-conversation-scroll]");
|
|
195
|
-
if (outer && outer !== sp) { targets.push(outer); }
|
|
196
|
-
} catch (e) {}
|
|
197
|
-
for (var i = 0; i < targets.length; i++) {
|
|
198
|
-
try {
|
|
199
|
-
targets[i].dispatchEvent(new WheelEvent("wheel", { deltaY: 0, bubbles: false, cancelable: true }));
|
|
200
|
-
} catch (e) {}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/* 三层防御共用的纯函数:pointermove 同步写 + scroll 捕获段回纠 + rAF 兜底 */
|
|
205
|
-
function applyDrag() {
|
|
206
|
-
if (!dragging || !sp || !track || !trRect) { return; }
|
|
207
|
-
var top = lastPointerY - dragOffsetY - trRect.top;
|
|
208
|
-
var maxTop = curTrackH - curThumbH;
|
|
209
|
-
if (maxTop <= 0) { return; }
|
|
210
|
-
if (top < 0) { top = 0; }
|
|
211
|
-
if (top > maxTop) { top = maxTop; }
|
|
212
|
-
var want = top / curTrackH * sp.scrollHeight;
|
|
213
|
-
if (Math.abs(sp.scrollTop - want) > 0.5) { sp.scrollTop = want; }
|
|
214
|
-
render();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function wireEvents() {
|
|
218
|
-
track.addEventListener("pointerdown", function (e) {
|
|
219
|
-
if (!sp || !track) { return; }
|
|
220
|
-
rebindIfScrollerChanged();
|
|
221
|
-
if (!sp || !track) { return; }
|
|
222
|
-
trRect = track.getBoundingClientRect();
|
|
223
|
-
var tR = thumb.getBoundingClientRect();
|
|
224
|
-
if (e.clientY >= tR.top && e.clientY <= tR.bottom) {
|
|
225
|
-
dragging = true;
|
|
226
|
-
dragOffsetY = e.clientY - tR.top;
|
|
227
|
-
} else {
|
|
228
|
-
/* 点轨道 = 标准翻页:拇指中心跳到点击处,顺势进入拖动 */
|
|
229
|
-
var top0 = e.clientY - trRect.top - curThumbH / 2;
|
|
230
|
-
var maxTop0 = curTrackH - curThumbH;
|
|
231
|
-
if (top0 < 0) { top0 = 0; }
|
|
232
|
-
if (top0 > maxTop0) { top0 = maxTop0; }
|
|
233
|
-
sp.scrollTop = top0 / curTrackH * sp.scrollHeight;
|
|
234
|
-
curThumbTop = top0;
|
|
235
|
-
dragging = true;
|
|
236
|
-
dragOffsetY = e.clientY - (trRect.top + curThumbTop);
|
|
237
|
-
}
|
|
238
|
-
lastPointerY = e.clientY;
|
|
239
|
-
notifyReaderIntent();
|
|
240
|
-
thumb.classList.add("drag");
|
|
241
|
-
try { track.setPointerCapture(e.pointerId); } catch (err) {}
|
|
242
|
-
e.preventDefault();
|
|
243
|
-
render();
|
|
244
|
-
startDragTick();
|
|
245
|
-
});
|
|
246
|
-
track.addEventListener("pointermove", function (e) {
|
|
247
|
-
if (!dragging || !sp || !track || !trRect) { return; }
|
|
248
|
-
lastPointerY = e.clientY;
|
|
249
|
-
if (rebindIfScrollerChanged()) { trRect = track.getBoundingClientRect(); }
|
|
250
|
-
applyDrag();
|
|
251
|
-
});
|
|
252
|
-
function release(e) {
|
|
253
|
-
if (!dragging) { return; }
|
|
254
|
-
dragging = false;
|
|
255
|
-
trRect = null;
|
|
256
|
-
if (dragRaf) { cancelAnimationFrame(dragRaf); dragRaf = 0; }
|
|
257
|
-
thumb.classList.remove("drag");
|
|
258
|
-
try { track.releasePointerCapture(e.pointerId); } catch (err) {}
|
|
259
|
-
}
|
|
260
|
-
function startDragTick() {
|
|
261
|
-
if (dragRaf) { cancelAnimationFrame(dragRaf); }
|
|
262
|
-
var tick = function () {
|
|
263
|
-
if (!dragging) { dragRaf = 0; return; }
|
|
264
|
-
rebindIfScrollerChanged();
|
|
265
|
-
applyDrag();
|
|
266
|
-
dragRaf = requestAnimationFrame(tick);
|
|
267
|
-
};
|
|
268
|
-
dragRaf = requestAnimationFrame(tick);
|
|
269
|
-
}
|
|
270
|
-
track.addEventListener("pointerup", release);
|
|
271
|
-
track.addEventListener("pointercancel", release);
|
|
272
|
-
/* 悬停在条上时滚轮转发给真容器(同样补发阅读意图) */
|
|
273
|
-
host.addEventListener("wheel", function (e) {
|
|
274
|
-
if (!sp) { return; }
|
|
275
|
-
rebindIfScrollerChanged();
|
|
276
|
-
if (!sp) { return; }
|
|
277
|
-
e.preventDefault();
|
|
278
|
-
notifyReaderIntent();
|
|
279
|
-
sp.scrollTop += e.deltaY;
|
|
280
|
-
render();
|
|
281
|
-
}, { passive: false });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function place() {
|
|
285
|
-
if (!host || !sp) { return; }
|
|
286
|
-
var q = sp.getBoundingClientRect();
|
|
287
|
-
var top = Math.round(q.top), h = Math.round(visibleHeight());
|
|
288
|
-
var edge = handle ? handle.getBoundingClientRect().left : q.right;
|
|
289
|
-
var left = Math.round(edge - host.offsetWidth);
|
|
290
|
-
if (host.style.top !== top + "px") { host.style.top = top + "px"; }
|
|
291
|
-
if (host.style.height !== h + "px") { host.style.height = h + "px"; }
|
|
292
|
-
if (host.style.left !== left + "px") { host.style.left = left + "px"; }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/* scroll 捕获段监听(scroll 不冒泡但下传捕获段),对节点替换天然免疫;
|
|
296
|
-
拖动期间还负责把非本脚本的 scrollTop 写动立刻纠正回来。 */
|
|
297
|
-
function onDocScroll(e) {
|
|
298
|
-
if (!sp || e.target !== sp) { return; }
|
|
299
|
-
if (dragging) { applyDrag(); } else { render(); }
|
|
300
|
-
}
|
|
301
|
-
document.addEventListener("scroll", onDocScroll, { capture: true, passive: true });
|
|
302
|
-
|
|
303
|
-
function ensure() {
|
|
304
|
-
var found = findScroller();
|
|
305
|
-
if (found !== sp) { sp = found; }
|
|
306
|
-
var hFound = findRightHandle();
|
|
307
|
-
if (hFound !== handle) {
|
|
308
|
-
if (handle) { try { handle.removeEventListener("pointermove", place); } catch (e) {} }
|
|
309
|
-
handle = hFound;
|
|
310
|
-
if (handle) { try { handle.addEventListener("pointermove", place, { passive: true }); } catch (e) {} }
|
|
311
|
-
}
|
|
312
|
-
/* 只在会话视图显示:scrollport + 调宽条 + 实际高度三者齐备;
|
|
313
|
-
否则移除宿主(切到插件页等非会话页不留残影)。 */
|
|
314
|
-
var spVisible = !!(sp && sp.isConnected && sp.getBoundingClientRect().height > 0);
|
|
315
|
-
if (!spVisible || !handle) {
|
|
316
|
-
if (host) {
|
|
317
|
-
try { host.remove(); } catch (e) {}
|
|
318
|
-
host = null; track = null; thumb = null;
|
|
319
|
-
dragging = false;
|
|
320
|
-
dragOffsetY = 0;
|
|
321
|
-
if (dragRaf) { cancelAnimationFrame(dragRaf); dragRaf = 0; }
|
|
322
|
-
}
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
if (!host || !host.isConnected) {
|
|
326
|
-
if (host) { try { host.remove(); } catch (e) {} }
|
|
327
|
-
host = null; track = null; thumb = null;
|
|
328
|
-
build();
|
|
329
|
-
}
|
|
330
|
-
if (ro) {
|
|
331
|
-
try { ro.observe(sp); } catch (e) {}
|
|
332
|
-
var ce = findComposer();
|
|
333
|
-
if (ce !== composerEl) {
|
|
334
|
-
if (composerEl) { try { ro.unobserve(composerEl); } catch (e) {} }
|
|
335
|
-
composerEl = ce;
|
|
336
|
-
if (composerEl) { try { ro.observe(composerEl); } catch (e) {} }
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
place();
|
|
340
|
-
render();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
ensure();
|
|
344
|
-
/* 700ms 兜底轮询:补挂 / 高度 / 落点;不用全文档 MutationObserver
|
|
345
|
-
(流式输出每帧都有 mutation,ensure 读 rect 会强制布局)。 */
|
|
346
|
-
setInterval(ensure, 700);
|
|
347
|
-
window.addEventListener("resize", ensure);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/* ======================================================================
|
|
351
|
-
* 模块 3:插件更新检查面板(不含 dsh 本体 —— 桌面端自带更新器,
|
|
352
|
-
* web 端本体升级走 npm,都不归这个插件管)
|
|
353
|
-
*
|
|
354
|
-
* 全部走官方 remote API,不自建 host 路由:
|
|
355
|
-
* remote.pluginManager.listBundles() → 已装插件包(name/version/installed)
|
|
356
|
-
* remote.pluginManager.installBundle(spec, {enabled, requestId}) → 更新
|
|
357
|
-
* remote.pluginManager.waitForInstall(requestId) → 安装进度轮询
|
|
358
|
-
* remote.pluginManager.cancelInstall(requestId) → 取消
|
|
359
|
-
* registry 比对从浏览器直连(registry.npmjs.org 带 CORS *)。
|
|
360
|
-
*
|
|
361
|
-
* 更新严格串行(pnpm 对 profile 的 package.json 有文件锁,并发只会在锁上
|
|
362
|
-
* 排队);安装 spec 用面板解析出的**精确版本**(`name@版本`,不是 @latest):
|
|
363
|
-
* pnpm 11 的 minimumReleaseAge 门禁只拦「最新版本」解析,显式版本会自动
|
|
364
|
-
* 写入 profile 的 pnpm-workspace.yaml 的 minimumReleaseAgeExclude 并放行。
|
|
365
|
-
* 装完仍回读实际版本兜底(registry 与 pnpm 解析可能不一致),版本没变
|
|
366
|
-
* 才是「暂缓更新」。
|
|
367
|
-
* ==================================================================== */
|
|
368
|
-
function installPluginUpdates(ctx) {
|
|
369
|
-
if (window.__DSHUIW_UPDATES__) { return; }
|
|
370
|
-
window.__DSHUIW_UPDATES__ = true;
|
|
371
|
-
var BTN_ID = "dshuiw-updates-btn-host";
|
|
372
|
-
var PANEL_ID = "dshuiw-updates-panel-host";
|
|
373
|
-
var POS_KEY = "dshuiw-updates-panel-pos";
|
|
374
|
-
|
|
375
|
-
/* ---- semver 比较(rc 比数字/字母、正式版 > 预发布版;字符串比会把
|
|
376
|
-
rc.9 判成大于 rc.10,必须自己实现)---- */
|
|
377
|
-
function cmpSemver(a, b) {
|
|
378
|
-
function parse(v) {
|
|
379
|
-
var m = String(v == null ? "" : v).trim().replace(/^v/, "")
|
|
380
|
-
.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
381
|
-
if (!m) { return null; }
|
|
382
|
-
return { core: [+m[1], +m[2], +m[3]], pre: m[4] ? m[4].split(".") : null };
|
|
383
|
-
}
|
|
384
|
-
var pa = parse(a), pb = parse(b);
|
|
385
|
-
if (!pa || !pb) { return String(a) === String(b) ? 0 : (String(a) < String(b) ? -1 : 1); }
|
|
386
|
-
for (var i = 0; i < 3; i++) {
|
|
387
|
-
if (pa.core[i] !== pb.core[i]) { return pa.core[i] < pb.core[i] ? -1 : 1; }
|
|
388
|
-
}
|
|
389
|
-
var ha = pa.pre === null, hb = pb.pre === null;
|
|
390
|
-
if (ha && hb) { return 0; }
|
|
391
|
-
if (ha) { return 1; }
|
|
392
|
-
if (hb) { return -1; }
|
|
393
|
-
for (var j = 0; j < Math.max(pa.pre.length, pb.pre.length); j++) {
|
|
394
|
-
var x = pa.pre[j], y = pb.pre[j];
|
|
395
|
-
if (x === undefined) { return -1; }
|
|
396
|
-
if (y === undefined) { return 1; }
|
|
397
|
-
var xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
|
|
398
|
-
if (xn && yn) { var d = +x - +y; if (d) { return d < 0 ? -1 : 1; } }
|
|
399
|
-
else if (xn) { return -1; }
|
|
400
|
-
else if (yn) { return 1; }
|
|
401
|
-
else if (x !== y) { return x < y ? -1 : 1; }
|
|
402
|
-
}
|
|
403
|
-
return 0;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function uuid() {
|
|
407
|
-
try { if (crypto && crypto.randomUUID) { return crypto.randomUUID(); } } catch (e) {}
|
|
408
|
-
return "req-" + Date.now() + "-" + Math.random().toString(36).slice(2);
|
|
409
|
-
}
|
|
410
|
-
function esc(s) {
|
|
411
|
-
return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
|
|
412
|
-
return { "&": "&", "<": "<", ">": ">", "\"": """ }[c];
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/* ---- remote 服务解析:ctx.inject 的 scope 优先(缓存复用),超时兜底直读 ctx ---- */
|
|
417
|
-
var savedScope = null;
|
|
418
|
-
var got = null; // checkNow 的服务句柄缓存({pm})
|
|
419
|
-
function pickService(scope, dotted) {
|
|
420
|
-
var cands = [];
|
|
421
|
-
var s = scope || savedScope;
|
|
422
|
-
if (s) {
|
|
423
|
-
cands.push(s[dotted]);
|
|
424
|
-
var root = dotted.split(".")[0];
|
|
425
|
-
if (s[root]) { cands.push(s[root][dotted.split(".")[1]]); }
|
|
426
|
-
}
|
|
427
|
-
cands.push(ctx && ctx[dotted]);
|
|
428
|
-
if (ctx) {
|
|
429
|
-
var root2 = dotted.split(".")[0];
|
|
430
|
-
if (ctx[root2]) { cands.push(ctx[root2][dotted.split(".")[1]]); }
|
|
431
|
-
}
|
|
432
|
-
for (var i = 0; i < cands.length; i++) { if (cands[i]) { return cands[i]; } }
|
|
433
|
-
return null;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function fetchLatest(name) {
|
|
437
|
-
var path = String(name).split("/").map(encodeURIComponent).join("/");
|
|
438
|
-
return fetch("https://registry.npmjs.org/" + path + "/latest", { cache: "no-store" })
|
|
439
|
-
.then(function (r) {
|
|
440
|
-
if (r.status === 404) {
|
|
441
|
-
/* npm 上没有这个包:本地直装(link:/file:)或尚未发布 —— 不是「检查失败」。
|
|
442
|
-
官方 listBundles 不透传安装来源,404 就是本地包的唯一可靠签名。 */
|
|
443
|
-
var e404 = new Error("npm 上查不到此包(未发布或本地直装)");
|
|
444
|
-
e404.notFound = true;
|
|
445
|
-
throw e404;
|
|
446
|
-
}
|
|
447
|
-
if (!r.ok) { throw new Error("HTTP " + r.status); }
|
|
448
|
-
return r.json();
|
|
449
|
-
})
|
|
450
|
-
.then(function (d) { return d && d.version ? String(d.version) : null; });
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/* 非 registry 直装(link:/file:/git+/github:/http)没有可比的 registry 版本。
|
|
454
|
-
* 来源判定主路径是 host 半区的 /api/ui-tweaks/local-plugins(读 profile
|
|
455
|
-
* 依赖声明):本地路径(link:/file:)与仓库/URL 直装(git/github/http)分列,
|
|
456
|
-
* host 未重启/路由不可用时退回 registry 404 签名。 */
|
|
457
|
-
function fetchLocalNames(cb) {
|
|
458
|
-
fetch("/api/ui-tweaks/local-plugins", { cache: "no-store" })
|
|
459
|
-
.then(function (r) { if (!r.ok) { throw new Error("HTTP " + r.status); } return r.json(); })
|
|
460
|
-
.then(function (d) {
|
|
461
|
-
cb({
|
|
462
|
-
local: d && d.ok && Array.isArray(d.local) ? d.local : [],
|
|
463
|
-
remote: d && d.ok && Array.isArray(d.remote) ? d.remote : []
|
|
464
|
-
});
|
|
465
|
-
})
|
|
466
|
-
.catch(function () { cb({ local: [], remote: [] }); });
|
|
467
|
-
}
|
|
468
|
-
function isLocalSrc(src) { return /^(link:|file:)/.test(String(src || "")); }
|
|
469
|
-
function isRemoteSrc(src) { return /^(git[+:@]|github:|git:|http)/.test(String(src || "")); }
|
|
470
|
-
|
|
471
|
-
/* ---- 面板状态 ---- */
|
|
472
|
-
var rows = []; // {name, version, latest, state, why}
|
|
473
|
-
// state: pending | checking | current | update | local | remote | error | queuing | busy | done | unchanged
|
|
474
|
-
var queue = []; // 待更新名单(串行队列)
|
|
475
|
-
var busyName = ""; // 正在更新的那个
|
|
476
|
-
var busyReq = "";
|
|
477
|
-
var cancelling = false;
|
|
478
|
-
var checking = false;
|
|
479
|
-
var listeners = []; // 面板重绘订阅(面板未开时也维护状态)
|
|
480
|
-
|
|
481
|
-
function emit() { for (var i = 0; i < listeners.length; i++) { try { listeners[i](); } catch (e) {} } }
|
|
482
|
-
|
|
483
|
-
function setRow(name, patch) {
|
|
484
|
-
for (var i = 0; i < rows.length; i++) {
|
|
485
|
-
if (rows[i].name === name) { for (var k in patch) { rows[i][k] = patch[k]; } break; }
|
|
486
|
-
}
|
|
487
|
-
emit();
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/* 404 签名兜底:npm 上没有此包 → 按来源分「本地链接」/「Git 直装」;
|
|
491
|
-
host 半区未重启时走到这里。 */
|
|
492
|
-
function notFoundState(row) {
|
|
493
|
-
return isLocalSrc(row.src) ? "local" : (isRemoteSrc(row.src) ? "remote" : "local");
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function checkNow() {
|
|
497
|
-
if (checking) { return; }
|
|
498
|
-
checking = true;
|
|
499
|
-
rows = [];
|
|
500
|
-
emit();
|
|
501
|
-
var pm = null;
|
|
502
|
-
// 服务句柄在回调里现取(ctx.inject 服务齐了才回调,可能异步;waitScope 轮询等待)
|
|
503
|
-
if (!got) {
|
|
504
|
-
ctx.inject(["remote", "remote.pluginManager", "remote.pluginInventory"], function (s) {
|
|
505
|
-
savedScope = s;
|
|
506
|
-
got = { pm: pickService(s, "remote.pluginManager") };
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
var started = Date.now();
|
|
510
|
-
function waitScope() {
|
|
511
|
-
if (!got && Date.now() - started < 8000) { setTimeout(waitScope, 150); return; }
|
|
512
|
-
if (!got || !got.pm) {
|
|
513
|
-
rows = [{ name: "(插件清单服务不可用)", version: "", latest: "", state: "error", why: "remote.pluginManager 未就绪;请升级 dsh 或稍后重试" }];
|
|
514
|
-
checking = false; emit(); return;
|
|
515
|
-
}
|
|
516
|
-
pm = got.pm;
|
|
517
|
-
Promise.all([pm.listBundles(), pm.listPlugins()])
|
|
518
|
-
.then(function (r) {
|
|
519
|
-
var bundles = (r[0] && r[0].ok && r[0].value) || [];
|
|
520
|
-
var plugins = (r[1] && r[1].ok && r[1].value) || [];
|
|
521
|
-
if (!bundles.length && plugins.length) { bundles = plugins; }
|
|
522
|
-
rows = bundles.map(function (b) {
|
|
523
|
-
var name = b.name || b.id || "";
|
|
524
|
-
var ver = b.version != null ? String(b.version) : (b.installedVersion != null ? String(b.installedVersion) : "");
|
|
525
|
-
var src = typeof b.installed === "string" ? b.installed : (typeof b.source === "string" ? b.source : "");
|
|
526
|
-
return {
|
|
527
|
-
name: name, version: ver, latest: "",
|
|
528
|
-
state: isLocalSrc(src) ? "local" : (isRemoteSrc(src) ? "remote" : (ver ? "checking" : "unknown")),
|
|
529
|
-
src: src, why: ""
|
|
530
|
-
};
|
|
531
|
-
}).filter(function (x) { return x.name; })
|
|
532
|
-
/* 官方 @deepseek-ai 插件由 dsh 本体更新流程负责,直接从检查列表里剔除 */
|
|
533
|
-
.filter(function (x) { return !/^@deepseek-ai\//.test(x.name); });
|
|
534
|
-
checking = false;
|
|
535
|
-
emit();
|
|
536
|
-
// 先向 host 半区要非 registry 直装名单,命中的行直接标「本地链接」或「Git 直装」
|
|
537
|
-
// 不进比对 —— 已发布到 npm 的本地/仓库源(如 @nimoxie/dsh-model-organizer、
|
|
538
|
-
// dsh-ego-browser)靠它认出;host 未重启(无路由)时名单为空,退回
|
|
539
|
-
// registry 404 签名判定。
|
|
540
|
-
fetchLocalNames(function (srcs) {
|
|
541
|
-
var localSet = {}, remoteSet = {};
|
|
542
|
-
(srcs.local || []).forEach(function (n) { localSet[n] = true; });
|
|
543
|
-
(srcs.remote || []).forEach(function (n) { remoteSet[n] = true; });
|
|
544
|
-
rows.forEach(function (row) {
|
|
545
|
-
if (localSet[row.name] && row.state === "checking") {
|
|
546
|
-
row.state = "local";
|
|
547
|
-
row.why = "profile 依赖声明为本地路径,不参与 registry 比对";
|
|
548
|
-
} else if (remoteSet[row.name] && row.state === "checking") {
|
|
549
|
-
row.state = "remote";
|
|
550
|
-
row.why = "profile 依赖声明为 git/URL 直装,不参与 registry 比对";
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
emit();
|
|
554
|
-
// 逐个查 registry(串行,别对 registry 打并发)
|
|
555
|
-
var chain = Promise.resolve();
|
|
556
|
-
rows.forEach(function (row) {
|
|
557
|
-
if (row.state !== "checking") { return; }
|
|
558
|
-
chain = chain.then(function () {
|
|
559
|
-
return fetchLatest(row.name).then(function (v) {
|
|
560
|
-
row.latest = v || "";
|
|
561
|
-
row.state = v && cmpSemver(v, row.version) > 0 ? "update" : "current";
|
|
562
|
-
emit();
|
|
563
|
-
}).catch(function (e) {
|
|
564
|
-
row.state = e && e.notFound ? notFoundState(row) : "error";
|
|
565
|
-
row.why = String(e && e.message || e);
|
|
566
|
-
emit();
|
|
567
|
-
});
|
|
568
|
-
});
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
})
|
|
572
|
-
.catch(function (e) {
|
|
573
|
-
rows = [{ name: "(读取插件清单失败)", version: "", latest: "", state: "error", why: String(e && e.message || e) }];
|
|
574
|
-
checking = false; emit();
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
waitScope();
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/* ---- 更新执行:串行队列;装完回读实际版本判定真实结果 ---- */
|
|
581
|
-
function enqueue(name) {
|
|
582
|
-
if (busyName) {
|
|
583
|
-
if (queue.indexOf(name) < 0 && name !== busyName) { queue.push(name); setRow(name, { state: "queuing" }); }
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
queue.push(name);
|
|
587
|
-
pump();
|
|
588
|
-
}
|
|
589
|
-
function pump() {
|
|
590
|
-
var name = queue.shift();
|
|
591
|
-
if (!name) { return; }
|
|
592
|
-
startOne(name);
|
|
593
|
-
}
|
|
594
|
-
function startOne(name) {
|
|
595
|
-
busyName = name;
|
|
596
|
-
busyReq = uuid();
|
|
597
|
-
setRow(name, { state: "busy" });
|
|
598
|
-
emit();
|
|
599
|
-
var pm = pickService(null, "remote.pluginManager");
|
|
600
|
-
if (!pm) { finishOne(name, false, "remote.pluginManager 不可用"); return; }
|
|
601
|
-
var settled = false;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
var
|
|
715
|
-
if (
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
"
|
|
731
|
-
"
|
|
732
|
-
"
|
|
733
|
-
"
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
".
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
"
|
|
740
|
-
".
|
|
741
|
-
".
|
|
742
|
-
"
|
|
743
|
-
".
|
|
744
|
-
".
|
|
745
|
-
"
|
|
746
|
-
".
|
|
747
|
-
".
|
|
748
|
-
".
|
|
749
|
-
".tag
|
|
750
|
-
"
|
|
751
|
-
"
|
|
752
|
-
".
|
|
753
|
-
".
|
|
754
|
-
".
|
|
755
|
-
"
|
|
756
|
-
".
|
|
757
|
-
".
|
|
758
|
-
"
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
document.addEventListener("
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
if (
|
|
814
|
-
|
|
815
|
-
if (
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
if (t.classList
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
if (!
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
return
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
try {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
try {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
try {
|
|
1028
|
-
var
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1
|
+
/* dsh-ui-tweaks browser half.
|
|
2
|
+
*
|
|
3
|
+
* 三个独立模块,任一失败都不得拖垮其它(各自 guard):
|
|
4
|
+
* 1. tablefix —— 宽表格越界修复(纯 CSS 覆盖,见 README「宽表格修复」)
|
|
5
|
+
* 2. scrollbar —— 对话区左侧自绘滚动条(从 dsh-web-shell 移植,选择器适配 0.2.x)
|
|
6
|
+
* 3. updates —— 插件更新检查面板(不含 dsh 本体;remote.pluginManager/pluginInventory)
|
|
7
|
+
*
|
|
8
|
+
* 装载机制:shell 的 window.__ModuleLoader__.load + factory(require);
|
|
9
|
+
* apply(ctx) 由客户端 cordis 上下文调用(同 dsh-model-organizer 的结构)。
|
|
10
|
+
*/
|
|
11
|
+
window.__ModuleLoader__.load({
|
|
12
|
+
id: "@nimoxie/dsh-ui-tweaks",
|
|
13
|
+
factory: (require) => {
|
|
14
|
+
const module = { exports: {} };
|
|
15
|
+
const exports = module.exports;
|
|
16
|
+
|
|
17
|
+
const BUILD = "0.1.2";
|
|
18
|
+
|
|
19
|
+
/* 每个模块的注册各套一层 guard:失败只记 warn(座位/模块失败模式是「静默消失」,
|
|
20
|
+
必须主动吭声;但不能让一个模块的异常打断同文件里其它模块的 apply)。 */
|
|
21
|
+
function guard(name, fn) {
|
|
22
|
+
try {
|
|
23
|
+
return fn();
|
|
24
|
+
} catch (e) {
|
|
25
|
+
try { console.warn("[dsh-ui-tweaks:" + BUILD + "]", name, e); } catch (_) {}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ======================================================================
|
|
31
|
+
* 模块 1:宽表格越界修复(对 dsh 本体的只读样式补丁)
|
|
32
|
+
*
|
|
33
|
+
* 缺陷(0.2.0-rc.1 实测仍在):@deepseek-ai/dsh-client-ui-chat 给
|
|
34
|
+
* .md-table-wide(≥4 列且不在引用块内)做了「出血」布局(width 加宽 +
|
|
35
|
+
* margin-left 负值左移 + padding-left 推回),而 primitives 的
|
|
36
|
+
* .tableScroll{max-width:100%} 管不住内部 width:max-content 的表格,
|
|
37
|
+
* overflow-x:hidden 之下内容直接被裁(实测右沿超出可视区约 280px)。
|
|
38
|
+
*
|
|
39
|
+
* 修法:放弃出血,收敛回正文列 —— 同时中和 width / margin-left /
|
|
40
|
+
* padding-left 三件套并放开 overflow-x(三者缺一不可,只改宽度会留下
|
|
41
|
+
* 负 margin,只去 margin 不约束宽度则继续越界)。
|
|
42
|
+
*
|
|
43
|
+
* 防失效设计:
|
|
44
|
+
* ① 选择器用属性前缀 [class*="tableScroll"](CSS modules「哈希_原名」,
|
|
45
|
+
* dsh 换版本改哈希也不失效,绝不硬编码 hash);
|
|
46
|
+
* ② 全部声明带 !important 压过 dsh 自己的规则;
|
|
47
|
+
* ③ 单一 <style id> + 存在性检查,删除节点即完全回滚。
|
|
48
|
+
* ==================================================================== */
|
|
49
|
+
function installTableWidthFix() {
|
|
50
|
+
var STYLE_ID = "dshuiw-table-width-fix";
|
|
51
|
+
if (document.getElementById(STYLE_ID)) { return; }
|
|
52
|
+
var st = document.createElement("style");
|
|
53
|
+
st.id = STYLE_ID;
|
|
54
|
+
st.setAttribute("data-dshuiw", "table-width-fix");
|
|
55
|
+
st.textContent =
|
|
56
|
+
"[class*=\"tableScroll\"]{" +
|
|
57
|
+
"width:100%!important;" +
|
|
58
|
+
"max-width:100%!important;" +
|
|
59
|
+
"margin-left:0!important;" +
|
|
60
|
+
"padding-left:0!important;" +
|
|
61
|
+
"padding-bottom:0!important;" +
|
|
62
|
+
"overflow-x:auto!important;" +
|
|
63
|
+
"}" +
|
|
64
|
+
/* 内部表格:允许按内容排布(保留列宽语义),宽度交给包裹层约束 */
|
|
65
|
+
"[class*=\"tableScroll\"]>table{" +
|
|
66
|
+
"max-width:none!important;" +
|
|
67
|
+
"min-width:max-content;" +
|
|
68
|
+
"}" +
|
|
69
|
+
/* 滚动条尽量细、贴近 dsh 的视觉语言 */
|
|
70
|
+
"[class*=\"tableScroll\"]{scrollbar-width:thin}";
|
|
71
|
+
(document.head || document.documentElement).appendChild(st);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ======================================================================
|
|
75
|
+
* 模块 2:对话区左侧自绘滚动条(自绘拇指)
|
|
76
|
+
*
|
|
77
|
+
* 从 dsh-web-shell 的注入脚本移植(该实现经过 A/B 实测):
|
|
78
|
+
* · 宿主挂 document.body + position:fixed —— 挂进 React 管的容器会被
|
|
79
|
+
* 「加载更早」等重渲染抹掉;fixed 还让滚动区自己滚时宿主纹丝不动。
|
|
80
|
+
* · 拖动 = PointerCapture 下的纯函数:sp.scrollTop 由拇指位置一步算出;
|
|
81
|
+
* pointermove 同步写 + scroll 捕获段回纠 + rAF 兜底(三层防御)。
|
|
82
|
+
* · 「加载更早」后 dsh 会钉阅读锚点,只有 wheel/pointerdown 等读者意图
|
|
83
|
+
* 事件会解除 —— 宿主在滚动区外,pointerdown 到不了 dsh 的监听器,
|
|
84
|
+
* 所以按下/滚轮时补发一个零位移 synthetic wheel(notifyReaderIntent)。
|
|
85
|
+
* · 只在「会话视图」显示:scrollport + 右侧调宽条 + 有实际高度三者齐备
|
|
86
|
+
* 才保留宿主,切到插件页等非会话页自动移除。
|
|
87
|
+
* · 高度裁到输入框(composerSeat,sticky 浮在底部)上沿 −4px。
|
|
88
|
+
*
|
|
89
|
+
* 0.2.x 选择器适配(0.1.x 的 data-dsh-part 标记已被 dsh 全部移除):
|
|
90
|
+
* 滚动区 [data-conversation-scroll]
|
|
91
|
+
* 调宽条 [class*="widthHandle"][data-side="right"]
|
|
92
|
+
* 输入框 [class*="composerSeat"]
|
|
93
|
+
* ==================================================================== */
|
|
94
|
+
function installLeftScrollbar() {
|
|
95
|
+
if (window.__DSHUIW_LEFT_SB__) { return; }
|
|
96
|
+
window.__DSHUIW_LEFT_SB__ = true;
|
|
97
|
+
var HOST_ID = "dshuiw-scrollbar-host";
|
|
98
|
+
|
|
99
|
+
function findScroller() {
|
|
100
|
+
return document.querySelector('[data-dsh-part="scrollport"]') ||
|
|
101
|
+
document.querySelector("[data-conversation-scroll]");
|
|
102
|
+
}
|
|
103
|
+
function findRightHandle() {
|
|
104
|
+
var hs = document.querySelectorAll('[data-dsh-part="resize-handle"]');
|
|
105
|
+
for (var i = 0; i < hs.length; i++) {
|
|
106
|
+
if (hs[i].getAttribute("data-side") === "right") { return hs[i]; }
|
|
107
|
+
}
|
|
108
|
+
/* dsh 0.2.x:宽度调整条 = widthHandle 类 + data-side(无 data-dsh-part) */
|
|
109
|
+
return document.querySelector('[class*="widthHandle"][data-side="right"]');
|
|
110
|
+
}
|
|
111
|
+
function findComposer() {
|
|
112
|
+
var c = document.querySelector('[data-dsh-part="composer-input"]');
|
|
113
|
+
if (c) { return c.closest('[class*="composerSeat"]') || c; }
|
|
114
|
+
return document.querySelector('[class*="composerSeat"]');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
var sp = null, handle = null, host = null, track = null, thumb = null;
|
|
118
|
+
var composerEl = null, ro = null;
|
|
119
|
+
if (window.ResizeObserver) {
|
|
120
|
+
ro = new ResizeObserver(function () { ensure(); });
|
|
121
|
+
}
|
|
122
|
+
var dragging = false, dragOffsetY = 0;
|
|
123
|
+
var curThumbH = 40, curThumbTop = 0, curTrackH = 0;
|
|
124
|
+
var trRect = null;
|
|
125
|
+
var lastPointerY = 0, dragRaf = 0;
|
|
126
|
+
|
|
127
|
+
/* 「加载更早」、切会话等操作可能换掉 scrollport 节点:交互点先校验节点
|
|
128
|
+
身份,变了就重绑,否则写入落在旧节点上 —— 拖动没反应、松手弹回。 */
|
|
129
|
+
function rebindIfScrollerChanged() {
|
|
130
|
+
if (sp && findScroller() !== sp) { ensure(); return true; }
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function visibleHeight() {
|
|
135
|
+
if (!sp) { return 0; }
|
|
136
|
+
var c = findComposer();
|
|
137
|
+
if (!c) { return sp.clientHeight; }
|
|
138
|
+
var cr = c.getBoundingClientRect();
|
|
139
|
+
var sr = sp.getBoundingClientRect();
|
|
140
|
+
if (cr.top >= sr.bottom - 2) { return sp.clientHeight; }
|
|
141
|
+
var v = cr.top - sr.top - 4;
|
|
142
|
+
return (v > 80 && v < sp.clientHeight) ? v : sp.clientHeight;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function build() {
|
|
146
|
+
host = document.createElement("div");
|
|
147
|
+
host.id = HOST_ID;
|
|
148
|
+
host.style.width = "14px";
|
|
149
|
+
var sr = host.attachShadow({ mode: "open" });
|
|
150
|
+
sr.innerHTML = "<style>" +
|
|
151
|
+
":host{position:fixed;z-index:6}" +
|
|
152
|
+
".track{position:absolute;top:0;right:0;width:100%;height:100%;" +
|
|
153
|
+
"user-select:none;-webkit-user-select:none;touch-action:none}" +
|
|
154
|
+
".track[hidden]{display:none}" +
|
|
155
|
+
".thumb{position:absolute;right:0;width:100%;background:transparent}" +
|
|
156
|
+
".thumb::after{content:\"\";position:absolute;top:0;bottom:0;right:3px;width:7px;border-radius:4px;" +
|
|
157
|
+
"background:rgba(128,128,128,.4);" +
|
|
158
|
+
"background:color-mix(in srgb,currentColor 32%,transparent);" +
|
|
159
|
+
"transition:width .12s,right .12s,background .12s}" +
|
|
160
|
+
".track:hover .thumb::after,.thumb.drag::after{right:2px;width:9px;" +
|
|
161
|
+
"background:rgba(128,128,128,.62);" +
|
|
162
|
+
"background:color-mix(in srgb,currentColor 55%,transparent)}" +
|
|
163
|
+
"</style><div class=\"track\" hidden><div class=\"thumb\"></div></div>";
|
|
164
|
+
track = sr.querySelector(".track");
|
|
165
|
+
thumb = sr.querySelector(".thumb");
|
|
166
|
+
wireEvents();
|
|
167
|
+
document.body.appendChild(host);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* thumb 几何:高 = 轨道 × 可见比,位置 = scrollTop / scrollHeight */
|
|
171
|
+
function render() {
|
|
172
|
+
if (!sp || !track || !thumb) { return; }
|
|
173
|
+
track.hidden = false;
|
|
174
|
+
var range = sp.scrollHeight - sp.clientHeight;
|
|
175
|
+
var th = track.clientHeight;
|
|
176
|
+
if (range <= 0 || th <= 0) { track.hidden = true; return; }
|
|
177
|
+
curTrackH = th;
|
|
178
|
+
curThumbH = Math.round(th * sp.clientHeight / sp.scrollHeight);
|
|
179
|
+
if (curThumbH < 28) { curThumbH = 28; }
|
|
180
|
+
if (curThumbH > th) { curThumbH = th; }
|
|
181
|
+
curThumbTop = Math.round(th * sp.scrollTop / sp.scrollHeight);
|
|
182
|
+
if (curThumbTop > th - curThumbH) { curThumbTop = th - curThumbH; }
|
|
183
|
+
if (curThumbTop < 0) { curThumbTop = 0; }
|
|
184
|
+
if (thumb.style.height !== curThumbH + "px") { thumb.style.height = curThumbH + "px"; }
|
|
185
|
+
if (thumb.style.top !== curThumbTop + "px") { thumb.style.top = curThumbTop + "px"; }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* 补发零位移 wheel 作纯阅读意图:解除 dsh 的阅读锚点保留(stopPreserving),
|
|
189
|
+
否则「加载更早」后拖动会被判成程序化滚动、松手弹回锚点。 */
|
|
190
|
+
function notifyReaderIntent() {
|
|
191
|
+
if (!sp) { return; }
|
|
192
|
+
var targets = [sp];
|
|
193
|
+
try {
|
|
194
|
+
var outer = sp.closest("[data-conversation-scroll]");
|
|
195
|
+
if (outer && outer !== sp) { targets.push(outer); }
|
|
196
|
+
} catch (e) {}
|
|
197
|
+
for (var i = 0; i < targets.length; i++) {
|
|
198
|
+
try {
|
|
199
|
+
targets[i].dispatchEvent(new WheelEvent("wheel", { deltaY: 0, bubbles: false, cancelable: true }));
|
|
200
|
+
} catch (e) {}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* 三层防御共用的纯函数:pointermove 同步写 + scroll 捕获段回纠 + rAF 兜底 */
|
|
205
|
+
function applyDrag() {
|
|
206
|
+
if (!dragging || !sp || !track || !trRect) { return; }
|
|
207
|
+
var top = lastPointerY - dragOffsetY - trRect.top;
|
|
208
|
+
var maxTop = curTrackH - curThumbH;
|
|
209
|
+
if (maxTop <= 0) { return; }
|
|
210
|
+
if (top < 0) { top = 0; }
|
|
211
|
+
if (top > maxTop) { top = maxTop; }
|
|
212
|
+
var want = top / curTrackH * sp.scrollHeight;
|
|
213
|
+
if (Math.abs(sp.scrollTop - want) > 0.5) { sp.scrollTop = want; }
|
|
214
|
+
render();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function wireEvents() {
|
|
218
|
+
track.addEventListener("pointerdown", function (e) {
|
|
219
|
+
if (!sp || !track) { return; }
|
|
220
|
+
rebindIfScrollerChanged();
|
|
221
|
+
if (!sp || !track) { return; }
|
|
222
|
+
trRect = track.getBoundingClientRect();
|
|
223
|
+
var tR = thumb.getBoundingClientRect();
|
|
224
|
+
if (e.clientY >= tR.top && e.clientY <= tR.bottom) {
|
|
225
|
+
dragging = true;
|
|
226
|
+
dragOffsetY = e.clientY - tR.top;
|
|
227
|
+
} else {
|
|
228
|
+
/* 点轨道 = 标准翻页:拇指中心跳到点击处,顺势进入拖动 */
|
|
229
|
+
var top0 = e.clientY - trRect.top - curThumbH / 2;
|
|
230
|
+
var maxTop0 = curTrackH - curThumbH;
|
|
231
|
+
if (top0 < 0) { top0 = 0; }
|
|
232
|
+
if (top0 > maxTop0) { top0 = maxTop0; }
|
|
233
|
+
sp.scrollTop = top0 / curTrackH * sp.scrollHeight;
|
|
234
|
+
curThumbTop = top0;
|
|
235
|
+
dragging = true;
|
|
236
|
+
dragOffsetY = e.clientY - (trRect.top + curThumbTop);
|
|
237
|
+
}
|
|
238
|
+
lastPointerY = e.clientY;
|
|
239
|
+
notifyReaderIntent();
|
|
240
|
+
thumb.classList.add("drag");
|
|
241
|
+
try { track.setPointerCapture(e.pointerId); } catch (err) {}
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
render();
|
|
244
|
+
startDragTick();
|
|
245
|
+
});
|
|
246
|
+
track.addEventListener("pointermove", function (e) {
|
|
247
|
+
if (!dragging || !sp || !track || !trRect) { return; }
|
|
248
|
+
lastPointerY = e.clientY;
|
|
249
|
+
if (rebindIfScrollerChanged()) { trRect = track.getBoundingClientRect(); }
|
|
250
|
+
applyDrag();
|
|
251
|
+
});
|
|
252
|
+
function release(e) {
|
|
253
|
+
if (!dragging) { return; }
|
|
254
|
+
dragging = false;
|
|
255
|
+
trRect = null;
|
|
256
|
+
if (dragRaf) { cancelAnimationFrame(dragRaf); dragRaf = 0; }
|
|
257
|
+
thumb.classList.remove("drag");
|
|
258
|
+
try { track.releasePointerCapture(e.pointerId); } catch (err) {}
|
|
259
|
+
}
|
|
260
|
+
function startDragTick() {
|
|
261
|
+
if (dragRaf) { cancelAnimationFrame(dragRaf); }
|
|
262
|
+
var tick = function () {
|
|
263
|
+
if (!dragging) { dragRaf = 0; return; }
|
|
264
|
+
rebindIfScrollerChanged();
|
|
265
|
+
applyDrag();
|
|
266
|
+
dragRaf = requestAnimationFrame(tick);
|
|
267
|
+
};
|
|
268
|
+
dragRaf = requestAnimationFrame(tick);
|
|
269
|
+
}
|
|
270
|
+
track.addEventListener("pointerup", release);
|
|
271
|
+
track.addEventListener("pointercancel", release);
|
|
272
|
+
/* 悬停在条上时滚轮转发给真容器(同样补发阅读意图) */
|
|
273
|
+
host.addEventListener("wheel", function (e) {
|
|
274
|
+
if (!sp) { return; }
|
|
275
|
+
rebindIfScrollerChanged();
|
|
276
|
+
if (!sp) { return; }
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
notifyReaderIntent();
|
|
279
|
+
sp.scrollTop += e.deltaY;
|
|
280
|
+
render();
|
|
281
|
+
}, { passive: false });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function place() {
|
|
285
|
+
if (!host || !sp) { return; }
|
|
286
|
+
var q = sp.getBoundingClientRect();
|
|
287
|
+
var top = Math.round(q.top), h = Math.round(visibleHeight());
|
|
288
|
+
var edge = handle ? handle.getBoundingClientRect().left : q.right;
|
|
289
|
+
var left = Math.round(edge - host.offsetWidth);
|
|
290
|
+
if (host.style.top !== top + "px") { host.style.top = top + "px"; }
|
|
291
|
+
if (host.style.height !== h + "px") { host.style.height = h + "px"; }
|
|
292
|
+
if (host.style.left !== left + "px") { host.style.left = left + "px"; }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* scroll 捕获段监听(scroll 不冒泡但下传捕获段),对节点替换天然免疫;
|
|
296
|
+
拖动期间还负责把非本脚本的 scrollTop 写动立刻纠正回来。 */
|
|
297
|
+
function onDocScroll(e) {
|
|
298
|
+
if (!sp || e.target !== sp) { return; }
|
|
299
|
+
if (dragging) { applyDrag(); } else { render(); }
|
|
300
|
+
}
|
|
301
|
+
document.addEventListener("scroll", onDocScroll, { capture: true, passive: true });
|
|
302
|
+
|
|
303
|
+
function ensure() {
|
|
304
|
+
var found = findScroller();
|
|
305
|
+
if (found !== sp) { sp = found; }
|
|
306
|
+
var hFound = findRightHandle();
|
|
307
|
+
if (hFound !== handle) {
|
|
308
|
+
if (handle) { try { handle.removeEventListener("pointermove", place); } catch (e) {} }
|
|
309
|
+
handle = hFound;
|
|
310
|
+
if (handle) { try { handle.addEventListener("pointermove", place, { passive: true }); } catch (e) {} }
|
|
311
|
+
}
|
|
312
|
+
/* 只在会话视图显示:scrollport + 调宽条 + 实际高度三者齐备;
|
|
313
|
+
否则移除宿主(切到插件页等非会话页不留残影)。 */
|
|
314
|
+
var spVisible = !!(sp && sp.isConnected && sp.getBoundingClientRect().height > 0);
|
|
315
|
+
if (!spVisible || !handle) {
|
|
316
|
+
if (host) {
|
|
317
|
+
try { host.remove(); } catch (e) {}
|
|
318
|
+
host = null; track = null; thumb = null;
|
|
319
|
+
dragging = false;
|
|
320
|
+
dragOffsetY = 0;
|
|
321
|
+
if (dragRaf) { cancelAnimationFrame(dragRaf); dragRaf = 0; }
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!host || !host.isConnected) {
|
|
326
|
+
if (host) { try { host.remove(); } catch (e) {} }
|
|
327
|
+
host = null; track = null; thumb = null;
|
|
328
|
+
build();
|
|
329
|
+
}
|
|
330
|
+
if (ro) {
|
|
331
|
+
try { ro.observe(sp); } catch (e) {}
|
|
332
|
+
var ce = findComposer();
|
|
333
|
+
if (ce !== composerEl) {
|
|
334
|
+
if (composerEl) { try { ro.unobserve(composerEl); } catch (e) {} }
|
|
335
|
+
composerEl = ce;
|
|
336
|
+
if (composerEl) { try { ro.observe(composerEl); } catch (e) {} }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
place();
|
|
340
|
+
render();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
ensure();
|
|
344
|
+
/* 700ms 兜底轮询:补挂 / 高度 / 落点;不用全文档 MutationObserver
|
|
345
|
+
(流式输出每帧都有 mutation,ensure 读 rect 会强制布局)。 */
|
|
346
|
+
setInterval(ensure, 700);
|
|
347
|
+
window.addEventListener("resize", ensure);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ======================================================================
|
|
351
|
+
* 模块 3:插件更新检查面板(不含 dsh 本体 —— 桌面端自带更新器,
|
|
352
|
+
* web 端本体升级走 npm,都不归这个插件管)
|
|
353
|
+
*
|
|
354
|
+
* 全部走官方 remote API,不自建 host 路由:
|
|
355
|
+
* remote.pluginManager.listBundles() → 已装插件包(name/version/installed)
|
|
356
|
+
* remote.pluginManager.installBundle(spec, {enabled, requestId}) → 更新
|
|
357
|
+
* remote.pluginManager.waitForInstall(requestId) → 安装进度轮询
|
|
358
|
+
* remote.pluginManager.cancelInstall(requestId) → 取消
|
|
359
|
+
* registry 比对从浏览器直连(registry.npmjs.org 带 CORS *)。
|
|
360
|
+
*
|
|
361
|
+
* 更新严格串行(pnpm 对 profile 的 package.json 有文件锁,并发只会在锁上
|
|
362
|
+
* 排队);安装 spec 用面板解析出的**精确版本**(`name@版本`,不是 @latest):
|
|
363
|
+
* pnpm 11 的 minimumReleaseAge 门禁只拦「最新版本」解析,显式版本会自动
|
|
364
|
+
* 写入 profile 的 pnpm-workspace.yaml 的 minimumReleaseAgeExclude 并放行。
|
|
365
|
+
* 装完仍回读实际版本兜底(registry 与 pnpm 解析可能不一致),版本没变
|
|
366
|
+
* 才是「暂缓更新」。
|
|
367
|
+
* ==================================================================== */
|
|
368
|
+
function installPluginUpdates(ctx) {
|
|
369
|
+
if (window.__DSHUIW_UPDATES__) { return; }
|
|
370
|
+
window.__DSHUIW_UPDATES__ = true;
|
|
371
|
+
var BTN_ID = "dshuiw-updates-btn-host";
|
|
372
|
+
var PANEL_ID = "dshuiw-updates-panel-host";
|
|
373
|
+
var POS_KEY = "dshuiw-updates-panel-pos";
|
|
374
|
+
|
|
375
|
+
/* ---- semver 比较(rc 比数字/字母、正式版 > 预发布版;字符串比会把
|
|
376
|
+
rc.9 判成大于 rc.10,必须自己实现)---- */
|
|
377
|
+
function cmpSemver(a, b) {
|
|
378
|
+
function parse(v) {
|
|
379
|
+
var m = String(v == null ? "" : v).trim().replace(/^v/, "")
|
|
380
|
+
.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
381
|
+
if (!m) { return null; }
|
|
382
|
+
return { core: [+m[1], +m[2], +m[3]], pre: m[4] ? m[4].split(".") : null };
|
|
383
|
+
}
|
|
384
|
+
var pa = parse(a), pb = parse(b);
|
|
385
|
+
if (!pa || !pb) { return String(a) === String(b) ? 0 : (String(a) < String(b) ? -1 : 1); }
|
|
386
|
+
for (var i = 0; i < 3; i++) {
|
|
387
|
+
if (pa.core[i] !== pb.core[i]) { return pa.core[i] < pb.core[i] ? -1 : 1; }
|
|
388
|
+
}
|
|
389
|
+
var ha = pa.pre === null, hb = pb.pre === null;
|
|
390
|
+
if (ha && hb) { return 0; }
|
|
391
|
+
if (ha) { return 1; }
|
|
392
|
+
if (hb) { return -1; }
|
|
393
|
+
for (var j = 0; j < Math.max(pa.pre.length, pb.pre.length); j++) {
|
|
394
|
+
var x = pa.pre[j], y = pb.pre[j];
|
|
395
|
+
if (x === undefined) { return -1; }
|
|
396
|
+
if (y === undefined) { return 1; }
|
|
397
|
+
var xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
|
|
398
|
+
if (xn && yn) { var d = +x - +y; if (d) { return d < 0 ? -1 : 1; } }
|
|
399
|
+
else if (xn) { return -1; }
|
|
400
|
+
else if (yn) { return 1; }
|
|
401
|
+
else if (x !== y) { return x < y ? -1 : 1; }
|
|
402
|
+
}
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function uuid() {
|
|
407
|
+
try { if (crypto && crypto.randomUUID) { return crypto.randomUUID(); } } catch (e) {}
|
|
408
|
+
return "req-" + Date.now() + "-" + Math.random().toString(36).slice(2);
|
|
409
|
+
}
|
|
410
|
+
function esc(s) {
|
|
411
|
+
return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
|
|
412
|
+
return { "&": "&", "<": "<", ">": ">", "\"": """ }[c];
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/* ---- remote 服务解析:ctx.inject 的 scope 优先(缓存复用),超时兜底直读 ctx ---- */
|
|
417
|
+
var savedScope = null;
|
|
418
|
+
var got = null; // checkNow 的服务句柄缓存({pm})
|
|
419
|
+
function pickService(scope, dotted) {
|
|
420
|
+
var cands = [];
|
|
421
|
+
var s = scope || savedScope;
|
|
422
|
+
if (s) {
|
|
423
|
+
cands.push(s[dotted]);
|
|
424
|
+
var root = dotted.split(".")[0];
|
|
425
|
+
if (s[root]) { cands.push(s[root][dotted.split(".")[1]]); }
|
|
426
|
+
}
|
|
427
|
+
cands.push(ctx && ctx[dotted]);
|
|
428
|
+
if (ctx) {
|
|
429
|
+
var root2 = dotted.split(".")[0];
|
|
430
|
+
if (ctx[root2]) { cands.push(ctx[root2][dotted.split(".")[1]]); }
|
|
431
|
+
}
|
|
432
|
+
for (var i = 0; i < cands.length; i++) { if (cands[i]) { return cands[i]; } }
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function fetchLatest(name) {
|
|
437
|
+
var path = String(name).split("/").map(encodeURIComponent).join("/");
|
|
438
|
+
return fetch("https://registry.npmjs.org/" + path + "/latest", { cache: "no-store" })
|
|
439
|
+
.then(function (r) {
|
|
440
|
+
if (r.status === 404) {
|
|
441
|
+
/* npm 上没有这个包:本地直装(link:/file:)或尚未发布 —— 不是「检查失败」。
|
|
442
|
+
官方 listBundles 不透传安装来源,404 就是本地包的唯一可靠签名。 */
|
|
443
|
+
var e404 = new Error("npm 上查不到此包(未发布或本地直装)");
|
|
444
|
+
e404.notFound = true;
|
|
445
|
+
throw e404;
|
|
446
|
+
}
|
|
447
|
+
if (!r.ok) { throw new Error("HTTP " + r.status); }
|
|
448
|
+
return r.json();
|
|
449
|
+
})
|
|
450
|
+
.then(function (d) { return d && d.version ? String(d.version) : null; });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* 非 registry 直装(link:/file:/git+/github:/http)没有可比的 registry 版本。
|
|
454
|
+
* 来源判定主路径是 host 半区的 /api/ui-tweaks/local-plugins(读 profile
|
|
455
|
+
* 依赖声明):本地路径(link:/file:)与仓库/URL 直装(git/github/http)分列,
|
|
456
|
+
* host 未重启/路由不可用时退回 registry 404 签名。 */
|
|
457
|
+
function fetchLocalNames(cb) {
|
|
458
|
+
fetch("/api/ui-tweaks/local-plugins", { cache: "no-store" })
|
|
459
|
+
.then(function (r) { if (!r.ok) { throw new Error("HTTP " + r.status); } return r.json(); })
|
|
460
|
+
.then(function (d) {
|
|
461
|
+
cb({
|
|
462
|
+
local: d && d.ok && Array.isArray(d.local) ? d.local : [],
|
|
463
|
+
remote: d && d.ok && Array.isArray(d.remote) ? d.remote : []
|
|
464
|
+
});
|
|
465
|
+
})
|
|
466
|
+
.catch(function () { cb({ local: [], remote: [] }); });
|
|
467
|
+
}
|
|
468
|
+
function isLocalSrc(src) { return /^(link:|file:)/.test(String(src || "")); }
|
|
469
|
+
function isRemoteSrc(src) { return /^(git[+:@]|github:|git:|http)/.test(String(src || "")); }
|
|
470
|
+
|
|
471
|
+
/* ---- 面板状态 ---- */
|
|
472
|
+
var rows = []; // {name, version, latest, state, why}
|
|
473
|
+
// state: pending | checking | current | update | local | remote | error | queuing | busy | done | unchanged
|
|
474
|
+
var queue = []; // 待更新名单(串行队列)
|
|
475
|
+
var busyName = ""; // 正在更新的那个
|
|
476
|
+
var busyReq = "";
|
|
477
|
+
var cancelling = false;
|
|
478
|
+
var checking = false;
|
|
479
|
+
var listeners = []; // 面板重绘订阅(面板未开时也维护状态)
|
|
480
|
+
|
|
481
|
+
function emit() { for (var i = 0; i < listeners.length; i++) { try { listeners[i](); } catch (e) {} } }
|
|
482
|
+
|
|
483
|
+
function setRow(name, patch) {
|
|
484
|
+
for (var i = 0; i < rows.length; i++) {
|
|
485
|
+
if (rows[i].name === name) { for (var k in patch) { rows[i][k] = patch[k]; } break; }
|
|
486
|
+
}
|
|
487
|
+
emit();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/* 404 签名兜底:npm 上没有此包 → 按来源分「本地链接」/「Git 直装」;
|
|
491
|
+
host 半区未重启时走到这里。 */
|
|
492
|
+
function notFoundState(row) {
|
|
493
|
+
return isLocalSrc(row.src) ? "local" : (isRemoteSrc(row.src) ? "remote" : "local");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function checkNow() {
|
|
497
|
+
if (checking) { return; }
|
|
498
|
+
checking = true;
|
|
499
|
+
rows = [];
|
|
500
|
+
emit();
|
|
501
|
+
var pm = null;
|
|
502
|
+
// 服务句柄在回调里现取(ctx.inject 服务齐了才回调,可能异步;waitScope 轮询等待)
|
|
503
|
+
if (!got) {
|
|
504
|
+
ctx.inject(["remote", "remote.pluginManager", "remote.pluginInventory"], function (s) {
|
|
505
|
+
savedScope = s;
|
|
506
|
+
got = { pm: pickService(s, "remote.pluginManager") };
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
var started = Date.now();
|
|
510
|
+
function waitScope() {
|
|
511
|
+
if (!got && Date.now() - started < 8000) { setTimeout(waitScope, 150); return; }
|
|
512
|
+
if (!got || !got.pm) {
|
|
513
|
+
rows = [{ name: "(插件清单服务不可用)", version: "", latest: "", state: "error", why: "remote.pluginManager 未就绪;请升级 dsh 或稍后重试" }];
|
|
514
|
+
checking = false; emit(); return;
|
|
515
|
+
}
|
|
516
|
+
pm = got.pm;
|
|
517
|
+
Promise.all([pm.listBundles(), pm.listPlugins()])
|
|
518
|
+
.then(function (r) {
|
|
519
|
+
var bundles = (r[0] && r[0].ok && r[0].value) || [];
|
|
520
|
+
var plugins = (r[1] && r[1].ok && r[1].value) || [];
|
|
521
|
+
if (!bundles.length && plugins.length) { bundles = plugins; }
|
|
522
|
+
rows = bundles.map(function (b) {
|
|
523
|
+
var name = b.name || b.id || "";
|
|
524
|
+
var ver = b.version != null ? String(b.version) : (b.installedVersion != null ? String(b.installedVersion) : "");
|
|
525
|
+
var src = typeof b.installed === "string" ? b.installed : (typeof b.source === "string" ? b.source : "");
|
|
526
|
+
return {
|
|
527
|
+
name: name, version: ver, latest: "",
|
|
528
|
+
state: isLocalSrc(src) ? "local" : (isRemoteSrc(src) ? "remote" : (ver ? "checking" : "unknown")),
|
|
529
|
+
src: src, why: ""
|
|
530
|
+
};
|
|
531
|
+
}).filter(function (x) { return x.name; })
|
|
532
|
+
/* 官方 @deepseek-ai 插件由 dsh 本体更新流程负责,直接从检查列表里剔除 */
|
|
533
|
+
.filter(function (x) { return !/^@deepseek-ai\//.test(x.name); });
|
|
534
|
+
checking = false;
|
|
535
|
+
emit();
|
|
536
|
+
// 先向 host 半区要非 registry 直装名单,命中的行直接标「本地链接」或「Git 直装」
|
|
537
|
+
// 不进比对 —— 已发布到 npm 的本地/仓库源(如 @nimoxie/dsh-model-organizer、
|
|
538
|
+
// dsh-ego-browser)靠它认出;host 未重启(无路由)时名单为空,退回
|
|
539
|
+
// registry 404 签名判定。
|
|
540
|
+
fetchLocalNames(function (srcs) {
|
|
541
|
+
var localSet = {}, remoteSet = {};
|
|
542
|
+
(srcs.local || []).forEach(function (n) { localSet[n] = true; });
|
|
543
|
+
(srcs.remote || []).forEach(function (n) { remoteSet[n] = true; });
|
|
544
|
+
rows.forEach(function (row) {
|
|
545
|
+
if (localSet[row.name] && row.state === "checking") {
|
|
546
|
+
row.state = "local";
|
|
547
|
+
row.why = "profile 依赖声明为本地路径,不参与 registry 比对";
|
|
548
|
+
} else if (remoteSet[row.name] && row.state === "checking") {
|
|
549
|
+
row.state = "remote";
|
|
550
|
+
row.why = "profile 依赖声明为 git/URL 直装,不参与 registry 比对";
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
emit();
|
|
554
|
+
// 逐个查 registry(串行,别对 registry 打并发)
|
|
555
|
+
var chain = Promise.resolve();
|
|
556
|
+
rows.forEach(function (row) {
|
|
557
|
+
if (row.state !== "checking") { return; }
|
|
558
|
+
chain = chain.then(function () {
|
|
559
|
+
return fetchLatest(row.name).then(function (v) {
|
|
560
|
+
row.latest = v || "";
|
|
561
|
+
row.state = v && cmpSemver(v, row.version) > 0 ? "update" : "current";
|
|
562
|
+
emit();
|
|
563
|
+
}).catch(function (e) {
|
|
564
|
+
row.state = e && e.notFound ? notFoundState(row) : "error";
|
|
565
|
+
row.why = String(e && e.message || e);
|
|
566
|
+
emit();
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
})
|
|
572
|
+
.catch(function (e) {
|
|
573
|
+
rows = [{ name: "(读取插件清单失败)", version: "", latest: "", state: "error", why: String(e && e.message || e) }];
|
|
574
|
+
checking = false; emit();
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
waitScope();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* ---- 更新执行:串行队列;装完回读实际版本判定真实结果 ---- */
|
|
581
|
+
function enqueue(name) {
|
|
582
|
+
if (busyName) {
|
|
583
|
+
if (queue.indexOf(name) < 0 && name !== busyName) { queue.push(name); setRow(name, { state: "queuing" }); }
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
queue.push(name);
|
|
587
|
+
pump();
|
|
588
|
+
}
|
|
589
|
+
function pump() {
|
|
590
|
+
var name = queue.shift();
|
|
591
|
+
if (!name) { return; }
|
|
592
|
+
startOne(name);
|
|
593
|
+
}
|
|
594
|
+
function startOne(name) {
|
|
595
|
+
busyName = name;
|
|
596
|
+
busyReq = uuid();
|
|
597
|
+
setRow(name, { state: "busy" });
|
|
598
|
+
emit();
|
|
599
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
600
|
+
if (!pm) { finishOne(name, false, "remote.pluginManager 不可用"); return; }
|
|
601
|
+
var settled = false;
|
|
602
|
+
/* 总看门狗:无论卡在 fetchLatest / installBundle / 轮询哪一段,240 秒未结束就判超时并请求取消 */
|
|
603
|
+
var watchdog = setTimeout(function () {
|
|
604
|
+
finish(false, "更新超时(240 秒未结束)");
|
|
605
|
+
try { var pm2 = pickService(null, "remote.pluginManager"); if (pm2 && pm2.cancelInstall && busyReq) { pm2.cancelInstall(busyReq); } } catch (err) {}
|
|
606
|
+
}, 240000);
|
|
607
|
+
function finish(ok, why) {
|
|
608
|
+
if (settled) { return; }
|
|
609
|
+
settled = true;
|
|
610
|
+
clearTimeout(watchdog);
|
|
611
|
+
finishOne(name, ok, why || "");
|
|
612
|
+
}
|
|
613
|
+
/* 安装 spec 用面板已解析的精确版本(`name@版本`)而不是 @latest:
|
|
614
|
+
* pnpm 11 的 minimumReleaseAge 门禁只拦「最新版本」解析 —— 发布不足
|
|
615
|
+
* 24h 的新版会被静默跳过、回落到旧版;显式版本则会触发 pnpm 的
|
|
616
|
+
* minimumReleaseAgeExclude 自动豁免并照常安装(社区分叉版
|
|
617
|
+
* @linxin666/dsh-client-ui-plugin-manager 的 update 正是这样:先
|
|
618
|
+
* fetchManifest 拿 registry 的 version,再按 `id@版本` 安装)。
|
|
619
|
+
* 行内没解析过(没点过检查)则现场查 registry,仍失败退回 @latest。 */
|
|
620
|
+
var row = null;
|
|
621
|
+
for (var i = 0; i < rows.length; i++) { if (rows[i].name === name) { row = rows[i]; break; } }
|
|
622
|
+
var specPromise = (row && row.latest)
|
|
623
|
+
? Promise.resolve(name + "@" + row.latest)
|
|
624
|
+
: fetchLatest(name)
|
|
625
|
+
.then(function (v) { return v ? name + "@" + v : name + "@latest"; })
|
|
626
|
+
.catch(function () { return name + "@latest"; });
|
|
627
|
+
specPromise.then(function (spec) {
|
|
628
|
+
try {
|
|
629
|
+
pm.installBundle(spec, { enabled: true, requestId: busyReq })
|
|
630
|
+
.then(function (res) {
|
|
631
|
+
if (!res || res.ok === false) {
|
|
632
|
+
finish(false, (res && res.failure && (res.failure.message || res.failure.reason)) || (res && res.message) || "安装请求被拒");
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
/* 轮询进度;终态形态未知,多形状都认,240s 兜底 */
|
|
636
|
+
var t0 = Date.now();
|
|
637
|
+
function poll() {
|
|
638
|
+
var done = false;
|
|
639
|
+
try {
|
|
640
|
+
var p = pm.waitForInstall ? pm.waitForInstall(busyReq) : null;
|
|
641
|
+
Promise.resolve(p).then(function (st) {
|
|
642
|
+
if (!st) { return; }
|
|
643
|
+
var app = st.application;
|
|
644
|
+
if (app === "applied" || app === "restart-required") { done = true; finish(true, app === "restart-required" ? "已应用,重启桌面端后生效" : ""); return; }
|
|
645
|
+
if (app === "failed") { done = true; finish(false, "安装失败" + (st.failedAt ? "(" + st.failedAt + ")" : "")); return; }
|
|
646
|
+
if (app === "cancelled") { done = true; finish(false, "已取消"); return; }
|
|
647
|
+
if (st.stage) { setRow(name, { state: "busy", why: "阶段:" + st.stage }); emit(); }
|
|
648
|
+
}).catch(function () { /* 轮询失败不作为终态 */ });
|
|
649
|
+
} catch (e) {}
|
|
650
|
+
if (!done && !settled && Date.now() - t0 < 240000) { setTimeout(poll, 900); }
|
|
651
|
+
else if (!settled && Date.now() - t0 >= 240000) { finish(false, "更新超时(240 秒未结束)"); }
|
|
652
|
+
}
|
|
653
|
+
poll();
|
|
654
|
+
})
|
|
655
|
+
.catch(function (e) { finish(false, String(e && e.message || e)); });
|
|
656
|
+
} catch (e) { finish(false, String(e && e.message || e)); }
|
|
657
|
+
});
|
|
658
|
+
specPromise.catch(function (e) { finish(false, "解析安装版本失败:" + String(e && e.message || e)); });
|
|
659
|
+
}
|
|
660
|
+
/* 装完回读清单:版本变了才算真的装上——安装走精确版本后 pnpm 已不会因
|
|
661
|
+
* minimumReleaseAge 拦新版本,但如果 registry 与 pnpm 解析不一致(如
|
|
662
|
+
* profile 配了镜像源),仍可能装回旧版,所以回读判据保留作最后兜底。
|
|
663
|
+
* 只用 pluginManager.listBundles —— pluginInventory 没有 listBundles 方法
|
|
664
|
+
* (只有 list()),之前先取 inventory 导致 guard 落到 reject,面板上的
|
|
665
|
+
* 「安装已发起,但回读版本失败」假阳性就是它。装完瞬间清单可能短暂
|
|
666
|
+
* 不可用,重试 3 次(间隔 1.5s)再下结论。 */
|
|
667
|
+
function finishOne(name, ok, why) {
|
|
668
|
+
var before = null;
|
|
669
|
+
for (var i = 0; i < rows.length; i++) { if (rows[i].name === name) { before = rows[i].version; break; } }
|
|
670
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
671
|
+
function done() {
|
|
672
|
+
busyName = ""; busyReq = ""; cancelling = false;
|
|
673
|
+
emit();
|
|
674
|
+
pump();
|
|
675
|
+
}
|
|
676
|
+
function attempt(n) {
|
|
677
|
+
if (!pm || !pm.listBundles) {
|
|
678
|
+
setRow(name, ok ? { state: "unconfirmed", why: "安装已发起;清单服务不可用,未能回读版本" } : { state: "error", why: why || "更新失败" });
|
|
679
|
+
done();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
Promise.resolve(pm.listBundles()).then(function (r) {
|
|
683
|
+
var bundles = (r && r.ok && r.value) || [];
|
|
684
|
+
var ver = "";
|
|
685
|
+
for (var i = 0; i < bundles.length; i++) {
|
|
686
|
+
if (bundles[i].name === name) { ver = bundles[i].version != null ? String(bundles[i].version) : ""; break; }
|
|
687
|
+
}
|
|
688
|
+
if (!bundles.length && n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
|
|
689
|
+
if (ver) {
|
|
690
|
+
if (before && ver === before) {
|
|
691
|
+
setRow(name, { state: "unchanged", why: "版本未变化:可能 registry 解析与 pnpm 实际安装不一致(如配置了镜像源),也可能检查时远端版本信息有误" });
|
|
692
|
+
} else {
|
|
693
|
+
setRow(name, { version: ver, state: "current", latest: ver, why: "" });
|
|
694
|
+
}
|
|
695
|
+
} else if (ok) {
|
|
696
|
+
setRow(name, { state: "unconfirmed", why: "安装已发起,但清单里没读到它;点击重查确认" });
|
|
697
|
+
} else {
|
|
698
|
+
setRow(name, { state: "error", why: why || "更新失败" });
|
|
699
|
+
}
|
|
700
|
+
done();
|
|
701
|
+
}).catch(function () {
|
|
702
|
+
if (n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
|
|
703
|
+
setRow(name, ok ? { state: "unconfirmed", why: "安装已发起,但回读版本失败;点击重查确认" } : { state: "error", why: why || "更新失败" });
|
|
704
|
+
done();
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
attempt(0);
|
|
708
|
+
}
|
|
709
|
+
function requestCancel() {
|
|
710
|
+
if (!busyName || cancelling) { return; }
|
|
711
|
+
cancelling = true;
|
|
712
|
+
queue = [];
|
|
713
|
+
rows.forEach(function (r) { if (r.state === "queuing") { r.state = r.latest && cmpSemver(r.latest, r.version) > 0 ? "update" : "current"; } });
|
|
714
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
715
|
+
try { if (pm && pm.cancelInstall && busyReq) { pm.cancelInstall(busyReq); } } catch (e) {}
|
|
716
|
+
setRow(busyName, { state: "error", why: "已请求取消" });
|
|
717
|
+
emit();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/* ---- 面板 UI:挂在 body 的 Shadow DOM(fixed 浮层不能放进 flex 工具栏)---- */
|
|
721
|
+
function panelWrap() {
|
|
722
|
+
var host = document.getElementById(PANEL_ID);
|
|
723
|
+
if (host && host.shadowRoot) { return host.shadowRoot.querySelector(".wrap"); }
|
|
724
|
+
if (host) { host.remove(); }
|
|
725
|
+
host = document.createElement("div");
|
|
726
|
+
host.id = PANEL_ID;
|
|
727
|
+
document.body.appendChild(host);
|
|
728
|
+
var sr = host.attachShadow({ mode: "open" });
|
|
729
|
+
sr.innerHTML = "<style>" +
|
|
730
|
+
".wrap{position:fixed;top:56px;right:16px;z-index:2147483000;width:344px;display:flex;" +
|
|
731
|
+
"flex-direction:column;max-height:64vh;overflow:hidden;border-radius:12px;" +
|
|
732
|
+
"border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.32));" +
|
|
733
|
+
"background:var(--dsw-alias-bg-base,#1b1d23);color:var(--dsw-alias-label-primary,#e8e9ec);" +
|
|
734
|
+
"box-shadow:0 10px 30px rgba(0,0,0,.34);" +
|
|
735
|
+
"font:13px/1.55 system-ui,-apple-system,\"Microsoft YaHei UI\",sans-serif}" +
|
|
736
|
+
".wrap[hidden]{display:none}" +
|
|
737
|
+
".hd{display:flex;align-items:center;gap:8px;padding:10px 12px;flex:0 0 auto;" +
|
|
738
|
+
"border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.22));" +
|
|
739
|
+
"cursor:move;user-select:none;-webkit-user-select:none}" +
|
|
740
|
+
".hd b{flex:1;font-size:13px;font-weight:600}" +
|
|
741
|
+
".x{cursor:pointer;opacity:.55;font-size:17px;line-height:1;padding:0 3px}" +
|
|
742
|
+
".x:hover{opacity:1}" +
|
|
743
|
+
".rows{flex:1 1 auto;min-height:0;overflow:auto}" +
|
|
744
|
+
".row{display:flex;align-items:center;gap:8px;padding:7px 12px;" +
|
|
745
|
+
"border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.12))}" +
|
|
746
|
+
".row:last-child{border-bottom:0}" +
|
|
747
|
+
".nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
|
|
748
|
+
".ver{opacity:.6;font-size:12px;white-space:nowrap;font-variant-numeric:tabular-nums}" +
|
|
749
|
+
".tag{font-size:11px;padding:1px 7px;border-radius:999px;white-space:nowrap;" +
|
|
750
|
+
"border:1px solid transparent;flex:0 0 auto;cursor:not-allowed}" +
|
|
751
|
+
".tag.update{background:rgba(255,138,0,.16);color:#f0a35e;border-color:rgba(255,138,0,.32)}" +
|
|
752
|
+
".tag.current{background:rgba(62,207,142,.14);color:#4fbf8b;border-color:rgba(62,207,142,.28)}" +
|
|
753
|
+
".tag.other{background:rgba(128,128,128,.14);opacity:.85}" +
|
|
754
|
+
".tag.busy{opacity:.6}" +
|
|
755
|
+
".tag.error{background:rgba(255,80,80,.14);color:#e07070;border-color:rgba(255,80,80,.28)}" +
|
|
756
|
+
".tag.clickable{cursor:pointer}" +
|
|
757
|
+
".tag.clickable:hover{filter:brightness(1.15)}" +
|
|
758
|
+
".allbtn{cursor:pointer;font-size:12px;padding:2px 9px;border-radius:6px;flex:0 0 auto;" +
|
|
759
|
+
"border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4))}" +
|
|
760
|
+
".allbtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14))}" +
|
|
761
|
+
".allbtn[hidden]{display:none}" +
|
|
762
|
+
".recheck{cursor:pointer;font-size:12px;padding:2px 9px;border-radius:6px;flex:0 0 auto;" +
|
|
763
|
+
"border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));opacity:.85}" +
|
|
764
|
+
".recheck:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14));opacity:1}" +
|
|
765
|
+
".msg{padding:10px 12px;white-space:pre-wrap;word-break:break-word;opacity:.85}" +
|
|
766
|
+
"</style><div class=\"wrap\" hidden></div>";
|
|
767
|
+
var wrap = sr.querySelector(".wrap");
|
|
768
|
+
|
|
769
|
+
/* 拖拽(记 localStorage,重载回原位) */
|
|
770
|
+
try {
|
|
771
|
+
var saved = localStorage.getItem(POS_KEY);
|
|
772
|
+
if (saved) {
|
|
773
|
+
var a = saved.split(",");
|
|
774
|
+
if (isFinite(+a[0]) && isFinite(+a[1])) { wrap.style.left = a[0] + "px"; wrap.style.top = a[1] + "px"; wrap.style.right = "auto"; }
|
|
775
|
+
}
|
|
776
|
+
} catch (e) {}
|
|
777
|
+
sr.addEventListener("pointerdown", function (e) {
|
|
778
|
+
var t = e.target;
|
|
779
|
+
if (!t || !t.closest || !t.closest(".hd") || t.closest(".x") || t.closest(".allbtn")) { return; }
|
|
780
|
+
var r = wrap.getBoundingClientRect();
|
|
781
|
+
var offX = e.clientX - r.left, offY = e.clientY - r.top;
|
|
782
|
+
e.preventDefault();
|
|
783
|
+
function clamp(x, y, w, h) {
|
|
784
|
+
return { x: Math.min(Math.max(0, x), Math.max(0, window.innerWidth - w)),
|
|
785
|
+
y: Math.min(Math.max(0, y), Math.max(0, window.innerHeight - h)) };
|
|
786
|
+
}
|
|
787
|
+
function onMove(ev) {
|
|
788
|
+
var w = wrap.getBoundingClientRect().width || 344, h = wrap.getBoundingClientRect().height || 220;
|
|
789
|
+
var p = clamp(ev.clientX - offX, ev.clientY - offY, w, h);
|
|
790
|
+
wrap.style.left = p.x + "px"; wrap.style.top = p.y + "px"; wrap.style.right = "auto";
|
|
791
|
+
try { localStorage.setItem(POS_KEY, p.x + "," + p.y); } catch (e2) {}
|
|
792
|
+
}
|
|
793
|
+
function onUp() {
|
|
794
|
+
window.removeEventListener("pointermove", onMove);
|
|
795
|
+
window.removeEventListener("pointerup", onUp);
|
|
796
|
+
window.removeEventListener("pointercancel", onUp);
|
|
797
|
+
}
|
|
798
|
+
window.addEventListener("pointermove", onMove);
|
|
799
|
+
window.addEventListener("pointerup", onUp);
|
|
800
|
+
window.addEventListener("pointercancel", onUp);
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
/* 点击外部收起(捕获段,按下即收) */
|
|
804
|
+
document.addEventListener("pointerdown", function (e) {
|
|
805
|
+
var w = document.getElementById(PANEL_ID);
|
|
806
|
+
if (w !== host) { return; }
|
|
807
|
+
var cur = host.shadowRoot.querySelector(".wrap");
|
|
808
|
+
if (!cur || cur.hidden) { return; }
|
|
809
|
+
if (host.contains(e.target)) { return; }
|
|
810
|
+
cur.hidden = true;
|
|
811
|
+
}, true);
|
|
812
|
+
document.addEventListener("keydown", function (e) {
|
|
813
|
+
if (e.key !== "Escape") { return; }
|
|
814
|
+
var cur = host.shadowRoot.querySelector(".wrap");
|
|
815
|
+
if (cur) { cur.hidden = true; }
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
/* 行内点击委托:有新版本 → 入队;更新中/等待 → 取消;失败 → 重试 */
|
|
819
|
+
wrap.addEventListener("click", function (e) {
|
|
820
|
+
var t = e.target;
|
|
821
|
+
if (!t || !t.classList) { return; }
|
|
822
|
+
if (t.classList.contains("x")) { wrap.hidden = true; return; }
|
|
823
|
+
if (t.classList.contains("recheck")) { if (!checking) { checkNow(); } return; }
|
|
824
|
+
if (t.classList.contains("reverify")) { if (!checking) { checkNow(); } return; }
|
|
825
|
+
if (t.classList.contains("allbtn")) {
|
|
826
|
+
rows.forEach(function (r) { if (r.state === "update" && queue.indexOf(r.name) < 0 && r.name !== busyName) { enqueue(r.name); } });
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (t.classList.contains("act")) {
|
|
830
|
+
var nm = t.getAttribute("data-n");
|
|
831
|
+
if (nm) { enqueue(nm); }
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (t.classList.contains("cancel")) { requestCancel(); return; }
|
|
835
|
+
if (t.classList.contains("retry")) {
|
|
836
|
+
var nm2 = t.getAttribute("data-n");
|
|
837
|
+
checkOne(nm2);
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
/* 状态 → 行渲染订阅 */
|
|
842
|
+
listeners.push(function () { paint(); });
|
|
843
|
+
return wrap;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function checkOne(name) {
|
|
847
|
+
for (var i = 0; i < rows.length; i++) {
|
|
848
|
+
if (rows[i].name === name) {
|
|
849
|
+
var cur = rows[i];
|
|
850
|
+
cur.state = "checking"; cur.why = "";
|
|
851
|
+
emit();
|
|
852
|
+
fetchLatest(name).then(function (v) {
|
|
853
|
+
var r = null;
|
|
854
|
+
for (var j = 0; j < rows.length; j++) { if (rows[j].name === name) { r = rows[j]; break; } }
|
|
855
|
+
if (!r) { return; }
|
|
856
|
+
r.latest = v || "";
|
|
857
|
+
r.state = v && cmpSemver(v, r.version) > 0 ? "update" : "current";
|
|
858
|
+
emit();
|
|
859
|
+
}).catch(function (e) {
|
|
860
|
+
setRow(name, e && e.notFound ? { state: notFoundState(cur), why: String(e.message) } : { state: "error", why: String(e && e.message || e) });
|
|
861
|
+
});
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
var TAG_TEXT = {
|
|
868
|
+
checking: "比对中 …", current: "已是最新", update: "有新版本",
|
|
869
|
+
local: "本地链接", remote: "Git 直装", unknown: "无法比对", error: "检查失败", queuing: "等待更新",
|
|
870
|
+
busy: "更新中 …", unchanged: "暂缓更新", unconfirmed: "已发起·未确认"
|
|
871
|
+
};
|
|
872
|
+
function tagHtml(r) {
|
|
873
|
+
var cls = "tag other", act = "", title = "";
|
|
874
|
+
if (r.state === "update") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"点击更新到最新版\""; }
|
|
875
|
+
else if (r.state === "current") { cls = "tag current"; }
|
|
876
|
+
else if (r.state === "error") { cls = "tag error retry clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "检查失败") + "(点击重查)\""; }
|
|
877
|
+
else if (r.state === "local") { title = " title=\"" + esc(r.src || r.why || "本地直装") + ";没有可比的 registry 版本\""; }
|
|
878
|
+
else if (r.state === "remote") { title = " title=\"" + esc(r.src || r.why || "Git/URL 直装") + ";没有可比的 registry 版本\""; }
|
|
879
|
+
else if (r.state === "unknown") { title = " title=\"" + esc(r.why || "插件清单未提供已装版本") + ",因此无法与 registry 比对\""; }
|
|
880
|
+
else if (r.state === "queuing" || r.state === "busy") { cls = "tag other cancel clickable"; title = " title=\"点击取消" + (r.state === "busy" ? "本次更新" : "排队") + "\""; }
|
|
881
|
+
else if (r.state === "unchanged") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "") + "(点击重试)\""; }
|
|
882
|
+
else if (r.state === "unconfirmed") { cls = "tag other reverify clickable"; title = " title=\"" + esc(r.why || "") + "(点击重查)\""; }
|
|
883
|
+
return "<span class=\"" + cls + "\"" + act + title + ">" + esc(TAG_TEXT[r.state] || "未比对") + "</span>";
|
|
884
|
+
}
|
|
885
|
+
function paint() {
|
|
886
|
+
var wrap = panelWrap();
|
|
887
|
+
var upd = 0;
|
|
888
|
+
rows.forEach(function (r) { if (r.state === "update") { upd++; } });
|
|
889
|
+
var allBtn = upd ? "<span class=\"allbtn\">全部更新(" + upd + ")</span>" : "<span class=\"allbtn\" hidden></span>";
|
|
890
|
+
var body;
|
|
891
|
+
if (!rows.length) {
|
|
892
|
+
body = "<div class=\"msg\">" + (checking ? "正在读取插件清单…" : "点上方「重新检查」开始比对") + "</div>";
|
|
893
|
+
} else {
|
|
894
|
+
body = rows.map(function (r) {
|
|
895
|
+
var ver = r.latest && (r.state === "update" || r.state === "unchanged") ? r.version + " → " + r.latest : r.version;
|
|
896
|
+
return "<div class=\"row\">" +
|
|
897
|
+
"<span class=\"nm\" title=\"" + esc(r.name) + "\">" + esc(r.name) + "</span>" +
|
|
898
|
+
"<span class=\"ver\" title=\"" + esc(ver) + "\">" + esc(ver) + "</span>" +
|
|
899
|
+
tagHtml(r) + "</div>";
|
|
900
|
+
}).join("");
|
|
901
|
+
}
|
|
902
|
+
var html = "<div class=\"hd\"><b>插件更新 · " + (busyName ? "正在更新 " + esc(busyName) + " …" : (checking ? "检查中…" : (rows.length ? upd + " 个插件有新版本" : "插件更新检查"))) + "</b>" +
|
|
903
|
+
"<span class=\"recheck\" title=\"重新读取插件清单并比对 registry\">重新检查</span>" + allBtn +
|
|
904
|
+
"<span class=\"x\" title=\"关闭\">×</span></div><div class=\"rows\">" + body + "</div>";
|
|
905
|
+
if (wrap.innerHTML !== html) {
|
|
906
|
+
var keep = wrap.querySelector(".rows");
|
|
907
|
+
var st = keep ? keep.scrollTop : 0;
|
|
908
|
+
wrap.innerHTML = html;
|
|
909
|
+
var nk = wrap.querySelector(".rows");
|
|
910
|
+
if (nk && st) { nk.scrollTop = st; }
|
|
911
|
+
}
|
|
912
|
+
wrap.hidden = false;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/* ---- 入口按钮:挂官方插件页顶栏(刷新按钮左侧),样式克隆「添加
|
|
916
|
+
插件」按钮。官方类名是 CSS-module hash(不硬编码),按钮在官方
|
|
917
|
+
页面渲染后由 findToolbar 实时克隆出来,换版本也不破。刷新按钮
|
|
918
|
+
与添加按钮同处一个 toolbar 容器,用文案「添加插件/Add plugin」
|
|
919
|
+
定位该容器,再插到容器首位(刷新按钮之前),保持既有更新面板。
|
|
920
|
+
React 重渲染会抹掉手插节点,MO + 轮询兜底补挂。---- */
|
|
921
|
+
function mountButton(openPanel) {
|
|
922
|
+
function findToolbar() {
|
|
923
|
+
var panel = document.querySelector('[data-plugin-panel]');
|
|
924
|
+
if (!panel) { return null; }
|
|
925
|
+
var header = panel.querySelector("header");
|
|
926
|
+
if (!header) { return null; }
|
|
927
|
+
var add = null;
|
|
928
|
+
var btns = header.querySelectorAll("button");
|
|
929
|
+
for (var i = 0; i < btns.length; i++) {
|
|
930
|
+
var t = String(btns[i].textContent || "").trim();
|
|
931
|
+
/* 「添加插件」在安装进行中会变成「查看安装任务」,两种都要认 */
|
|
932
|
+
if (t === "添加插件" || t === "Add plugin" || t === "查看安装任务" || t === "View installation") { add = btns[i]; break; }
|
|
933
|
+
}
|
|
934
|
+
if (!add || !add.parentElement) { return null; }
|
|
935
|
+
return { toolbar: add.parentElement, addBtn: add };
|
|
936
|
+
}
|
|
937
|
+
function mount() {
|
|
938
|
+
var found = findToolbar();
|
|
939
|
+
var host = document.getElementById(BTN_ID);
|
|
940
|
+
if (host) {
|
|
941
|
+
/* 按钮已在 DOM:若工具栏在而按钮被挤到别处(React 重渲染),归位 */
|
|
942
|
+
if (found && host.parentElement !== found.toolbar) {
|
|
943
|
+
found.toolbar.insertBefore(host, found.toolbar.firstElementChild);
|
|
944
|
+
}
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
if (!found) { return false; }
|
|
948
|
+
var b = found.addBtn.cloneNode(false);
|
|
949
|
+
b.id = BTN_ID;
|
|
950
|
+
b.disabled = false;
|
|
951
|
+
b.textContent = "检查更新";
|
|
952
|
+
b.title = "比对全部已装插件与 npm registry 的版本(不含 dsh 本体)";
|
|
953
|
+
b.addEventListener("click", openPanel);
|
|
954
|
+
found.toolbar.insertBefore(b, found.toolbar.firstElementChild);
|
|
955
|
+
return true;
|
|
956
|
+
}
|
|
957
|
+
mount();
|
|
958
|
+
try {
|
|
959
|
+
var mo = new MutationObserver(function () { mount(); });
|
|
960
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
961
|
+
} catch (e) {}
|
|
962
|
+
setInterval(mount, 2000);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
mountButton(function () { paint(); if (!rows.length && !checking) { checkNow(); } });
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/* ======================================================================
|
|
969
|
+
* 模块 4:动画修复(把 prefers-reduced-motion 强制成 no-preference)
|
|
970
|
+
*
|
|
971
|
+
* 背景:Windows「动画效果」关闭时 Chromium 对外报告 prefers-reduced-motion:
|
|
972
|
+
* reduce,dsh 前端 17 个包共 40 处 @media (prefers-reduced-motion: reduce)
|
|
973
|
+
* 规则把动画全关(圈圈不转、流光消失)。dsh-web-shell 用 CDP
|
|
974
|
+
* Emulation.setEmulatedMedia 修复,但桌面端默认没有调试端口,CDP 进不去。
|
|
975
|
+
*
|
|
976
|
+
* 页面内等价修法(无需调试端口,web/桌面通用):
|
|
977
|
+
* 1) CSSOM 清除 —— 实测 dsh 的 CSS 全部经 <style> 标签注入(0 处
|
|
978
|
+
* adoptedStyleSheets),同源样式表能从 document.styleSheets 遍历
|
|
979
|
+
* cssRules;把 reduce 的 media 块整条删除后,基础动画声明(都在
|
|
980
|
+
* 非 media 规则里)立即生效 —— 效果与 CDP 覆盖等价。
|
|
981
|
+
* 2) matchMedia 包装 —— chat 包有 3 处 JS 门控(滚动平滑/跳转)读
|
|
982
|
+
* matchMedia;包一层按 no-preference 应答,顺带覆盖未来新增门控。
|
|
983
|
+
* CSS 分包是懒加载的,新 <style> 注入由 MutationObserver + 轮询兜底重扫;
|
|
984
|
+
* 换肤等对既有 style 节点的重写也靠轮询兜回来。
|
|
985
|
+
* ==================================================================== */
|
|
986
|
+
function installAnimationFix() {
|
|
987
|
+
if (window.__DSHUIW_ANIM__) { return; }
|
|
988
|
+
window.__DSHUIW_ANIM__ = true;
|
|
989
|
+
var REDUCE_RE = /prefers-reduced-motion\s*:\s*reduce/i;
|
|
990
|
+
var purged = 0;
|
|
991
|
+
|
|
992
|
+
/* 递归删 reduce media 块;owner 是持有 deleteRule 的一层(sheet 或外层规则) */
|
|
993
|
+
function purge(ruleList, owner) {
|
|
994
|
+
for (var i = ruleList.length - 1; i >= 0; i--) {
|
|
995
|
+
var r = null;
|
|
996
|
+
try { r = ruleList[i]; } catch (e) { continue; }
|
|
997
|
+
if (!r) { continue; }
|
|
998
|
+
if (r.media && REDUCE_RE.test(r.media.mediaText || "")) {
|
|
999
|
+
try { owner.deleteRule(i); purged++; } catch (e) {}
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
try { if (r.cssRules && r.cssRules.length) { purge(r.cssRules, r); } } catch (e) {}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
function scanAll() {
|
|
1006
|
+
var sheets = [];
|
|
1007
|
+
try { sheets = Array.prototype.slice.call(document.styleSheets); } catch (e) {}
|
|
1008
|
+
try { if (document.adoptedStyleSheets) { sheets = sheets.concat(Array.prototype.slice.call(document.adoptedStyleSheets)); } } catch (e) {}
|
|
1009
|
+
for (var i = 0; i < sheets.length; i++) {
|
|
1010
|
+
try {
|
|
1011
|
+
var sh = sheets[i];
|
|
1012
|
+
if (sh && sh.cssRules && sh.cssRules.length) { purge(sh.cssRules, sh); }
|
|
1013
|
+
} catch (e) {} // 跨域 <link> 读 cssRules 会抛,静默跳过
|
|
1014
|
+
}
|
|
1015
|
+
return purged;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
var first = scanAll();
|
|
1019
|
+
try { console.info("[dsh-ui-tweaks:" + BUILD + "] 动画修复:已清除 " + first + " 条 prefers-reduced-motion: reduce 规则"); } catch (e) {}
|
|
1020
|
+
|
|
1021
|
+
/* 懒加载分包/换肤重写样式 → 防抖重扫;2s 轮询兜底(纯内存枚举,开销可忽略) */
|
|
1022
|
+
var timer = 0;
|
|
1023
|
+
function schedule() {
|
|
1024
|
+
if (timer) { return; }
|
|
1025
|
+
timer = setTimeout(function () { timer = 0; scanAll(); }, 200);
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
var mo = new MutationObserver(schedule);
|
|
1029
|
+
mo.observe(document.head, { childList: true, subtree: true });
|
|
1030
|
+
} catch (e) {}
|
|
1031
|
+
setInterval(scanAll, 2000);
|
|
1032
|
+
|
|
1033
|
+
/* matchMedia 包装:一律按 no-preference 应答(覆盖 JS 门控)。
|
|
1034
|
+
注意要在应用渲染前生效 —— 本插件 apply 在合成期执行,早于挂载。 */
|
|
1035
|
+
try {
|
|
1036
|
+
var nativeMM = window.matchMedia ? window.matchMedia.bind(window) : null;
|
|
1037
|
+
if (nativeMM) {
|
|
1038
|
+
window.matchMedia = function (q) {
|
|
1039
|
+
var mql = nativeMM(q);
|
|
1040
|
+
try {
|
|
1041
|
+
var qs = String(q || "");
|
|
1042
|
+
if (/prefers-reduced-motion/i.test(qs)) {
|
|
1043
|
+
var fake = !/:\s*reduce/i.test(qs); // reduce→false;no-preference/裸→true
|
|
1044
|
+
Object.defineProperty(mql, "matches", { value: fake, configurable: true });
|
|
1045
|
+
}
|
|
1046
|
+
} catch (e) {}
|
|
1047
|
+
return mql;
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
} catch (e) {}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function apply(ctx) {
|
|
1054
|
+
try { window.__DSHUIW_BUILD__ = BUILD; } catch (e) {}
|
|
1055
|
+
guard("tablefix", installTableWidthFix);
|
|
1056
|
+
guard("scrollbar", installLeftScrollbar);
|
|
1057
|
+
guard("updates", function () { installPluginUpdates(ctx); });
|
|
1058
|
+
guard("anim", installAnimationFix);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
module.exports = { name: "ui-tweaks", apply: apply, inject: ["remote", "remote.pluginManager", "remote.pluginInventory"] };
|
|
1062
|
+
return module.exports;
|
|
1063
|
+
}
|
|
1064
|
+
});
|