@nimoxie/dsh-ui-tweaks 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{DEVELOPING.md → DEVELOPMENT.md} +100 -88
- package/LICENSE +21 -21
- package/README.md +70 -68
- package/cordis.patch.yml +7 -7
- package/lib/client.js +1162 -1056
- package/lib/index.js +99 -99
- package/package.json +60 -60
package/lib/client.js
CHANGED
|
@@ -1,1056 +1,1162 @@
|
|
|
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
|
|
478
|
-
var
|
|
479
|
-
var
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
function
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
var
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
.filter(function (x) { return
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
row.
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
row.
|
|
565
|
-
row.
|
|
566
|
-
emit();
|
|
567
|
-
})
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
var
|
|
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
|
-
|
|
715
|
-
|
|
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
|
-
|
|
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
|
-
function
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
var
|
|
800
|
-
if (
|
|
801
|
-
if (
|
|
802
|
-
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
if (
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
if (
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
/*
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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.4";
|
|
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 busyBefore = ""; // 本次安装前该行的版本(回读比对基准,不受中途重查影响)
|
|
478
|
+
var busyLatest = ""; // 本次安装解析出的目标版本(取消后退回「有新版本」要用)
|
|
479
|
+
var cancelling = false;
|
|
480
|
+
var checking = false;
|
|
481
|
+
var listeners = []; // 面板重绘订阅(面板未开时也维护状态)
|
|
482
|
+
|
|
483
|
+
function emit() { for (var i = 0; i < listeners.length; i++) { try { listeners[i](); } catch (e) {} } }
|
|
484
|
+
|
|
485
|
+
function setRow(name, patch) {
|
|
486
|
+
for (var i = 0; i < rows.length; i++) {
|
|
487
|
+
if (rows[i].name === name) { for (var k in patch) { rows[i][k] = patch[k]; } break; }
|
|
488
|
+
}
|
|
489
|
+
emit();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* 404 签名兜底:npm 上没有此包 → 按来源分「本地链接」/「Git 直装」;
|
|
493
|
+
host 半区未重启时走到这里。 */
|
|
494
|
+
function notFoundState(row) {
|
|
495
|
+
return isLocalSrc(row.src) ? "local" : (isRemoteSrc(row.src) ? "remote" : "local");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function checkNow() {
|
|
499
|
+
if (checking) { return; }
|
|
500
|
+
checking = true;
|
|
501
|
+
rows = [];
|
|
502
|
+
emit();
|
|
503
|
+
var pm = null;
|
|
504
|
+
// 服务句柄在回调里现取(ctx.inject 服务齐了才回调,可能异步;waitScope 轮询等待)
|
|
505
|
+
if (!got) {
|
|
506
|
+
ctx.inject(["remote", "remote.pluginManager", "remote.pluginInventory"], function (s) {
|
|
507
|
+
savedScope = s;
|
|
508
|
+
got = { pm: pickService(s, "remote.pluginManager") };
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
var started = Date.now();
|
|
512
|
+
function waitScope() {
|
|
513
|
+
if (!got && Date.now() - started < 8000) { setTimeout(waitScope, 150); return; }
|
|
514
|
+
if (!got || !got.pm) {
|
|
515
|
+
rows = [{ name: "(插件清单服务不可用)", version: "", latest: "", state: "error", why: "remote.pluginManager 未就绪;请升级 dsh 或稍后重试" }];
|
|
516
|
+
checking = false; emit(); return;
|
|
517
|
+
}
|
|
518
|
+
pm = got.pm;
|
|
519
|
+
Promise.all([pm.listBundles(), pm.listPlugins()])
|
|
520
|
+
.then(function (r) {
|
|
521
|
+
var bundles = (r[0] && r[0].ok && r[0].value) || [];
|
|
522
|
+
var plugins = (r[1] && r[1].ok && r[1].value) || [];
|
|
523
|
+
if (!bundles.length && plugins.length) { bundles = plugins; }
|
|
524
|
+
rows = bundles.map(function (b) {
|
|
525
|
+
var name = b.name || b.id || "";
|
|
526
|
+
var ver = b.version != null ? String(b.version) : (b.installedVersion != null ? String(b.installedVersion) : "");
|
|
527
|
+
var src = typeof b.installed === "string" ? b.installed : (typeof b.source === "string" ? b.source : "");
|
|
528
|
+
return {
|
|
529
|
+
name: name, version: ver, latest: "",
|
|
530
|
+
state: isLocalSrc(src) ? "local" : (isRemoteSrc(src) ? "remote" : (ver ? "checking" : "unknown")),
|
|
531
|
+
src: src, enabled: b.enabled !== false, why: ""
|
|
532
|
+
};
|
|
533
|
+
}).filter(function (x) { return x.name; })
|
|
534
|
+
/* 官方 @deepseek-ai 插件由 dsh 本体更新流程负责,直接从检查列表里剔除 */
|
|
535
|
+
.filter(function (x) { return !/^@deepseek-ai\//.test(x.name); });
|
|
536
|
+
/* 与在飞的安装对齐(见 reconcileInstallStates):重查不得抹掉「更新中/等待更新」 */
|
|
537
|
+
reconcileInstallStates();
|
|
538
|
+
checking = false;
|
|
539
|
+
emit();
|
|
540
|
+
// 先向 host 半区要非 registry 直装名单,命中的行直接标「本地链接」或「Git 直装」
|
|
541
|
+
// 不进比对 —— 已发布到 npm 的本地/仓库源(如 @nimoxie/dsh-model-organizer、
|
|
542
|
+
// dsh-ego-browser)靠它认出;host 未重启(无路由)时名单为空,退回
|
|
543
|
+
// registry 404 签名判定。
|
|
544
|
+
fetchLocalNames(function (srcs) {
|
|
545
|
+
var localSet = {}, remoteSet = {};
|
|
546
|
+
(srcs.local || []).forEach(function (n) { localSet[n] = true; });
|
|
547
|
+
(srcs.remote || []).forEach(function (n) { remoteSet[n] = true; });
|
|
548
|
+
rows.forEach(function (row) {
|
|
549
|
+
if (localSet[row.name] && row.state === "checking") {
|
|
550
|
+
row.state = "local";
|
|
551
|
+
row.why = "profile 依赖声明为本地路径,不参与 registry 比对";
|
|
552
|
+
} else if (remoteSet[row.name] && row.state === "checking") {
|
|
553
|
+
row.state = "remote";
|
|
554
|
+
row.why = "profile 依赖声明为 git/URL 直装,不参与 registry 比对";
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
emit();
|
|
558
|
+
// 逐个查 registry(串行,别对 registry 打并发)
|
|
559
|
+
var chain = Promise.resolve();
|
|
560
|
+
rows.forEach(function (row) {
|
|
561
|
+
if (row.state !== "checking") { return; }
|
|
562
|
+
chain = chain.then(function () {
|
|
563
|
+
return fetchLatest(row.name).then(function (v) {
|
|
564
|
+
row.latest = v || "";
|
|
565
|
+
row.state = v && cmpSemver(v, row.version) > 0 ? "update" : "current";
|
|
566
|
+
emit();
|
|
567
|
+
}).catch(function (e) {
|
|
568
|
+
row.state = e && e.notFound ? notFoundState(row) : "error";
|
|
569
|
+
row.why = String(e && e.message || e);
|
|
570
|
+
emit();
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
})
|
|
576
|
+
.catch(function (e) {
|
|
577
|
+
rows = [{ name: "(读取插件清单失败)", version: "", latest: "", state: "error", why: String(e && e.message || e) }];
|
|
578
|
+
checking = false; emit();
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
waitScope();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* ---- 更新执行:串行队列 ------------------------------------------------
|
|
585
|
+
* installBundle 是「等到装完才返回」的调用:返回信封的 .value 就是终态对象
|
|
586
|
+
* (application / stage / target / error / packageResult / pendingBuilds …),
|
|
587
|
+
* 官方桌面端 UI 也是 await 它之后直接拿 .value 定案。
|
|
588
|
+
* waitForInstall 只用于**响应丢失后的恢复**:请求不在活动中时返回 null,
|
|
589
|
+
* 且「已完成的结果不保留」。把它当进度轮询器(旧实现)必然永远读到 null、
|
|
590
|
+
* 一路空转到 240s 看门狗 —— 这就是「更新总是超时」的真因。
|
|
591
|
+
* 正常路径只读 res.value;仅当信封本身失败(res.ok === false,结果未知)才用
|
|
592
|
+
* waitForInstall 兜一次,仍为 null 就如实报「结果未知」。
|
|
593
|
+
* ------------------------------------------------------------------ */
|
|
594
|
+
function findRow(name) {
|
|
595
|
+
for (var i = 0; i < rows.length; i++) { if (rows[i].name === name) { return rows[i]; } }
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
function msgOf(e) { return String((e && e.message) || e); }
|
|
599
|
+
|
|
600
|
+
/* 重新检查后把在飞状态盖回新行:否则「更新中/等待更新」会被抹成「有新版本」,
|
|
601
|
+
点它会撞上 enqueue 的 busyName 保护而毫无反应(表现为「重新检查后无法更新」),
|
|
602
|
+
真正在装的那一项也失去取消入口。busy / queuing 行不参与 registry 比对。 */
|
|
603
|
+
function reconcileInstallStates() {
|
|
604
|
+
if (busyName) { var b = findRow(busyName); if (b) { b.state = "busy"; b.why = ""; } }
|
|
605
|
+
for (var i = 0; i < queue.length; i++) {
|
|
606
|
+
var q = findRow(queue[i]);
|
|
607
|
+
if (q) { q.state = "queuing"; q.why = ""; }
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
var ERROR_CODE_TEXT = {
|
|
612
|
+
"management-required": "该 profile 由桌面端独占管理,不能从这里安装",
|
|
613
|
+
"unaddressable": "插件地址无法解析",
|
|
614
|
+
"unknown-plugin": "未知插件",
|
|
615
|
+
"invalid-spec": "安装 spec 非法",
|
|
616
|
+
"ambiguous-install": "目标不明确,无法确定要装哪个包",
|
|
617
|
+
"not-bundle": "该包不是 dsh 组合包(缺少 bundle patch)",
|
|
618
|
+
"not-removable": "该包不可卸载",
|
|
619
|
+
"stop-profile": "profile 正在停止,请稍后重试",
|
|
620
|
+
"bundle-in-use": "插件正在使用中,无法替换",
|
|
621
|
+
"stale-approval": "构建脚本授权已失效,需重新授权",
|
|
622
|
+
"incompatible-version": "版本与当前 dsh 不兼容",
|
|
623
|
+
"operation-error": "安装命令执行失败(pnpm 非零退出)"
|
|
624
|
+
};
|
|
625
|
+
/* 失败原因必须落到面板上:旧实现只回一句「安装失败」,把 pnpm 的真实报错
|
|
626
|
+
(如 ERR_PNPM_UNEXPECTED_STORE)连同 logPath 一起丢掉,用户只看到「超时」。 */
|
|
627
|
+
function failureText(st) {
|
|
628
|
+
var parts = [];
|
|
629
|
+
var code = st && st.error && st.error.code;
|
|
630
|
+
parts.push("安装失败" + (code ? ":" + (ERROR_CODE_TEXT[code] || code) : ""));
|
|
631
|
+
var diag = st && st.error && st.error.diagnostic;
|
|
632
|
+
if (diag) { parts.push(String(diag)); }
|
|
633
|
+
var pr = st && st.packageResult;
|
|
634
|
+
if (pr) {
|
|
635
|
+
if (pr.kind) { parts.push("分类:" + pr.kind); }
|
|
636
|
+
if (pr.exitCode) { parts.push("pnpm 退出码 " + pr.exitCode); }
|
|
637
|
+
var out = String(pr.output || "").trim();
|
|
638
|
+
if (out) { parts.push("输出:" + out.slice(-800)); }
|
|
639
|
+
if (pr.logPath) { parts.push("日志:" + pr.logPath); }
|
|
640
|
+
}
|
|
641
|
+
var pend = st && st.pendingBuilds;
|
|
642
|
+
if (pend && pend.length) { parts.push("待授权的构建脚本:" + pend.join("、") + "(需在官方插件页授权后重试)"); }
|
|
643
|
+
if (st && st.failedAt) { parts.push("失败环节:" + st.failedAt); }
|
|
644
|
+
return parts.join(" | ");
|
|
645
|
+
}
|
|
646
|
+
function lostReplyText(res) {
|
|
647
|
+
var m = res && res.error && res.error.message;
|
|
648
|
+
return "安装请求未返回结果" + (m ? ":" + String(m) : "");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function enqueue(name) {
|
|
652
|
+
if (name === busyName) { return; } // 正在装它(行内已是「更新中」态)
|
|
653
|
+
if (busyName) {
|
|
654
|
+
if (queue.indexOf(name) < 0) { queue.push(name); setRow(name, { state: "queuing" }); }
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
queue.push(name);
|
|
658
|
+
pump();
|
|
659
|
+
}
|
|
660
|
+
function pump() {
|
|
661
|
+
if (busyName) { return; }
|
|
662
|
+
var name = queue.shift();
|
|
663
|
+
if (!name) { return; }
|
|
664
|
+
startOne(name);
|
|
665
|
+
}
|
|
666
|
+
/* 排队项取消:只把它自己摘出队列,其余照旧等待。 */
|
|
667
|
+
function cancelQueued(name) {
|
|
668
|
+
var i = queue.indexOf(name);
|
|
669
|
+
if (i < 0) { return; }
|
|
670
|
+
queue.splice(i, 1);
|
|
671
|
+
setRow(name, { state: "update", why: "" });
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function startOne(name) {
|
|
675
|
+
var row = findRow(name);
|
|
676
|
+
busyName = name;
|
|
677
|
+
busyBefore = row ? row.version : "";
|
|
678
|
+
busyLatest = (row && row.latest) || "";
|
|
679
|
+
busyReq = uuid();
|
|
680
|
+
setRow(name, { state: "busy", why: "" });
|
|
681
|
+
emit();
|
|
682
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
683
|
+
if (!pm) { finishOne(name, "failed", "remote.pluginManager 不可用"); return; }
|
|
684
|
+
var settled = false;
|
|
685
|
+
/* 看门狗只作兜底:正常路径一次调用就定案,不等它。 */
|
|
686
|
+
var watchdog = setTimeout(function () {
|
|
687
|
+
finish("unknown", "更新超时(240 秒未结束);结果未知,请点「重新检查」确认");
|
|
688
|
+
try { var pm2 = pickService(null, "remote.pluginManager"); if (pm2 && pm2.cancelInstall && busyReq) { pm2.cancelInstall(busyReq); } } catch (e) {}
|
|
689
|
+
}, 240000);
|
|
690
|
+
function finish(kind, why) {
|
|
691
|
+
if (settled) { return; }
|
|
692
|
+
settled = true;
|
|
693
|
+
clearTimeout(watchdog);
|
|
694
|
+
finishOne(name, kind, why || "");
|
|
695
|
+
}
|
|
696
|
+
function settle(st) {
|
|
697
|
+
if (!st || typeof st !== "object") { finish("unknown", "安装未返回可判定的结果"); return; }
|
|
698
|
+
var app = st.application;
|
|
699
|
+
if (app === "applied") { finish("applied", ""); return; }
|
|
700
|
+
if (app === "restart-required") { finish("applied", "已应用,重启桌面端后生效"); return; }
|
|
701
|
+
if (app === "cancelled") { finish("cancelled", "已取消"); return; }
|
|
702
|
+
if (app === "overridden") { finish("failed", "已被另一次安装覆盖,请重试"); return; }
|
|
703
|
+
if (app === "failed") { finish("failed", failureText(st)); return; }
|
|
704
|
+
finish("unknown", "安装返回了无法识别的结果(application=" + String(app) + ")");
|
|
705
|
+
}
|
|
706
|
+
function lostReply(res) {
|
|
707
|
+
var pm2 = pickService(null, "remote.pluginManager");
|
|
708
|
+
var wait = pm2 && pm2.waitForInstall;
|
|
709
|
+
if (!wait) { finish("unknown", lostReplyText(res) + ",且无法恢复,请点「重新检查」确认"); return; }
|
|
710
|
+
Promise.resolve(wait.call(pm2, busyReq)).then(function (r) {
|
|
711
|
+
var st = r && r.ok ? r.value : null;
|
|
712
|
+
if (st) { settle(st); return; }
|
|
713
|
+
finish("unknown", lostReplyText(res) + ";该请求已不在活动中,结果未知,请点「重新检查」确认");
|
|
714
|
+
}).catch(function () {
|
|
715
|
+
finish("unknown", lostReplyText(res) + ";恢复结果失败,请点「重新检查」确认");
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
/* 安装 spec 用面板已解析的精确版本(`name@版本`),不用 @latest:pnpm 11 的
|
|
719
|
+
minimumReleaseAge 只拦「最新版本」解析,显式版本会触发
|
|
720
|
+
minimumReleaseAgeExclude 自动豁免并照常安装。行内没解析过则现场查 registry。 */
|
|
721
|
+
var specPromise = busyLatest
|
|
722
|
+
? Promise.resolve(name + "@" + busyLatest)
|
|
723
|
+
: fetchLatest(name)
|
|
724
|
+
.then(function (v) { return v ? name + "@" + v : name + "@latest"; })
|
|
725
|
+
.catch(function () { return name + "@latest"; });
|
|
726
|
+
/* 保留原本的启用选择:不再一律 enabled:true(那会把用户手动禁用的插件重新启用)。 */
|
|
727
|
+
var enabled = !(row && row.enabled === false);
|
|
728
|
+
specPromise.then(function (spec) {
|
|
729
|
+
return pm.installBundle(spec, { enabled: enabled, requestId: busyReq });
|
|
730
|
+
}).then(function (res) {
|
|
731
|
+
if (!res || res.ok === false) { lostReply(res); return; }
|
|
732
|
+
settle(res.value);
|
|
733
|
+
}).catch(function (e) { finish("failed", "安装请求失败:" + msgOf(e)); });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/* 终态落定:回读清单核对真实版本(registry 解析与 pnpm 实际安装可能不一致,
|
|
737
|
+
如镜像源),再按 kind 决定文案。kind: applied | failed | cancelled | unknown。
|
|
738
|
+
回读只用 pluginManager.listBundles(pluginInventory 只有 list(),没有
|
|
739
|
+
listBundles);装完瞬间清单可能短暂不可用,重试 3 次(1.5s 间隔)再下结论。 */
|
|
740
|
+
function finishOne(name, kind, why) {
|
|
741
|
+
var before = busyBefore;
|
|
742
|
+
var latest = busyLatest;
|
|
743
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
744
|
+
function done() {
|
|
745
|
+
busyName = ""; busyReq = ""; busyBefore = ""; busyLatest = "";
|
|
746
|
+
cancelling = false;
|
|
747
|
+
emit();
|
|
748
|
+
pump();
|
|
749
|
+
}
|
|
750
|
+
function judge(ver) {
|
|
751
|
+
if (ver && ver !== before) {
|
|
752
|
+
setRow(name, { version: ver, state: "current", latest: ver, why: "" });
|
|
753
|
+
} else if (kind === "cancelled") {
|
|
754
|
+
/* 取消不是失败:该行仍有新版本,退回可再次更新的状态。 */
|
|
755
|
+
setRow(name, { state: "update", latest: latest, why: "" });
|
|
756
|
+
} else if (kind === "failed") {
|
|
757
|
+
setRow(name, { state: "fail", why: why || "更新失败" });
|
|
758
|
+
} else if (kind === "applied" && ver) {
|
|
759
|
+
setRow(name, { state: "unchanged", why: "安装已应用,但版本未变化:可能 registry 与 pnpm 实际解析不一致(如配置了镜像源)" });
|
|
760
|
+
} else {
|
|
761
|
+
setRow(name, { state: "unconfirmed", why: why || "未能回读版本;点击重查确认" });
|
|
762
|
+
}
|
|
763
|
+
done();
|
|
764
|
+
}
|
|
765
|
+
function attempt(n) {
|
|
766
|
+
if (!pm || !pm.listBundles) { judge(""); return; }
|
|
767
|
+
Promise.resolve(pm.listBundles()).then(function (r) {
|
|
768
|
+
var bundles = (r && r.ok && r.value) || [];
|
|
769
|
+
if (!bundles.length && n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
|
|
770
|
+
var ver = "";
|
|
771
|
+
for (var i = 0; i < bundles.length; i++) {
|
|
772
|
+
if (bundles[i].name === name) { ver = bundles[i].version != null ? String(bundles[i].version) : ""; break; }
|
|
773
|
+
}
|
|
774
|
+
judge(ver);
|
|
775
|
+
}).catch(function () {
|
|
776
|
+
if (n < 3) { setTimeout(function () { attempt(n + 1); }, 1500); return; }
|
|
777
|
+
judge("");
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
attempt(0);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* 取消:排队项只摘自己(cancelQueued);在装项请求取消**本次**,队列保留,
|
|
784
|
+
等它落定后 done() → pump() 自然接着装下一项。
|
|
785
|
+
旧实现把整个 queue 清空,于是「等待更新」的项全部退回「有新版本」——
|
|
786
|
+
这就是「取消一项后,等待的项不再开始、反而变回有新版本」的真因。 */
|
|
787
|
+
function requestCancel() {
|
|
788
|
+
if (!busyName || cancelling) { return; }
|
|
789
|
+
cancelling = true;
|
|
790
|
+
var name = busyName;
|
|
791
|
+
setRow(name, { state: "busy", why: "正在取消 …" });
|
|
792
|
+
emit();
|
|
793
|
+
var pm = pickService(null, "remote.pluginManager");
|
|
794
|
+
if (!pm || !pm.cancelInstall || !busyReq) {
|
|
795
|
+
setRow(name, { state: "busy", why: "取消服务不可用,仍在等待它自行结束" });
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
Promise.resolve(pm.cancelInstall(busyReq)).then(function (r) {
|
|
799
|
+
var status = r && r.ok && r.value ? r.value.status : "";
|
|
800
|
+
if (status === "too-late") { setRow(name, { state: "busy", why: "已进入应用阶段,取消不了,正在等它结束" }); }
|
|
801
|
+
else if (status === "not-running") { setRow(name, { state: "busy", why: "该请求已不在运行,等待结果回传" }); }
|
|
802
|
+
/* status === "cancelled":安装调用会以 application:"cancelled" 落定,不在此改状态 */
|
|
803
|
+
}).catch(function (e) {
|
|
804
|
+
setRow(name, { state: "busy", why: "取消请求失败:" + msgOf(e) + ";仍在等待它结束" });
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/* ---- 面板 UI:挂在 body 的 Shadow DOM(fixed 浮层不能放进 flex 工具栏)---- */
|
|
809
|
+
function panelWrap() {
|
|
810
|
+
var host = document.getElementById(PANEL_ID);
|
|
811
|
+
if (host && host.shadowRoot) { return host.shadowRoot.querySelector(".wrap"); }
|
|
812
|
+
if (host) { host.remove(); }
|
|
813
|
+
host = document.createElement("div");
|
|
814
|
+
host.id = PANEL_ID;
|
|
815
|
+
document.body.appendChild(host);
|
|
816
|
+
var sr = host.attachShadow({ mode: "open" });
|
|
817
|
+
sr.innerHTML = "<style>" +
|
|
818
|
+
".wrap{position:fixed;top:56px;right:16px;z-index:2147483000;width:344px;display:flex;" +
|
|
819
|
+
"flex-direction:column;max-height:64vh;overflow:hidden;border-radius:12px;" +
|
|
820
|
+
"border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.32));" +
|
|
821
|
+
"background:var(--dsw-alias-bg-base,#1b1d23);color:var(--dsw-alias-label-primary,#e8e9ec);" +
|
|
822
|
+
"box-shadow:0 10px 30px rgba(0,0,0,.34);" +
|
|
823
|
+
"font:13px/1.55 system-ui,-apple-system,\"Microsoft YaHei UI\",sans-serif}" +
|
|
824
|
+
".wrap[hidden]{display:none}" +
|
|
825
|
+
".hd{display:flex;align-items:center;gap:8px;padding:10px 12px;flex:0 0 auto;" +
|
|
826
|
+
"border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.22));" +
|
|
827
|
+
"cursor:move;user-select:none;-webkit-user-select:none}" +
|
|
828
|
+
".hd b{flex:1;font-size:13px;font-weight:600}" +
|
|
829
|
+
".x{cursor:pointer;opacity:.55;font-size:17px;line-height:1;padding:0 3px}" +
|
|
830
|
+
".x:hover{opacity:1}" +
|
|
831
|
+
".rows{flex:1 1 auto;min-height:0;overflow:auto}" +
|
|
832
|
+
".rowwrap{border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.12))}" +
|
|
833
|
+
".rowwrap:last-child{border-bottom:0}" +
|
|
834
|
+
".row{display:flex;align-items:center;gap:8px;padding:7px 12px}" +
|
|
835
|
+
".why{padding:0 12px 8px;font-size:12px;line-height:1.45;opacity:.7;word-break:break-word;white-space:pre-wrap}" +
|
|
836
|
+
".nm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}" +
|
|
837
|
+
".ver{opacity:.6;font-size:12px;white-space:nowrap;font-variant-numeric:tabular-nums}" +
|
|
838
|
+
".tag{font-size:11px;padding:1px 7px;border-radius:999px;white-space:nowrap;" +
|
|
839
|
+
"border:1px solid transparent;flex:0 0 auto;cursor:not-allowed}" +
|
|
840
|
+
".tag.update{background:rgba(255,138,0,.16);color:#f0a35e;border-color:rgba(255,138,0,.32)}" +
|
|
841
|
+
".tag.current{background:rgba(62,207,142,.14);color:#4fbf8b;border-color:rgba(62,207,142,.28)}" +
|
|
842
|
+
".tag.other{background:rgba(128,128,128,.14);opacity:.85}" +
|
|
843
|
+
".tag.busy{opacity:.6}" +
|
|
844
|
+
".tag.error{background:rgba(255,80,80,.14);color:#e07070;border-color:rgba(255,80,80,.28)}" +
|
|
845
|
+
".tag.clickable{cursor:pointer}" +
|
|
846
|
+
".tag.clickable:hover{filter:brightness(1.15)}" +
|
|
847
|
+
".allbtn{cursor:pointer;font-size:12px;padding:2px 9px;border-radius:6px;flex:0 0 auto;" +
|
|
848
|
+
"border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4))}" +
|
|
849
|
+
".allbtn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14))}" +
|
|
850
|
+
".allbtn[hidden]{display:none}" +
|
|
851
|
+
".recheck{cursor:pointer;font-size:12px;padding:2px 9px;border-radius:6px;flex:0 0 auto;" +
|
|
852
|
+
"border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.4));opacity:.85}" +
|
|
853
|
+
".recheck:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14));opacity:1}" +
|
|
854
|
+
".msg{padding:10px 12px;white-space:pre-wrap;word-break:break-word;opacity:.85}" +
|
|
855
|
+
"</style><div class=\"wrap\" hidden></div>";
|
|
856
|
+
var wrap = sr.querySelector(".wrap");
|
|
857
|
+
|
|
858
|
+
/* 拖拽(记 localStorage,重载回原位) */
|
|
859
|
+
try {
|
|
860
|
+
var saved = localStorage.getItem(POS_KEY);
|
|
861
|
+
if (saved) {
|
|
862
|
+
var a = saved.split(",");
|
|
863
|
+
if (isFinite(+a[0]) && isFinite(+a[1])) { wrap.style.left = a[0] + "px"; wrap.style.top = a[1] + "px"; wrap.style.right = "auto"; }
|
|
864
|
+
}
|
|
865
|
+
} catch (e) {}
|
|
866
|
+
sr.addEventListener("pointerdown", function (e) {
|
|
867
|
+
var t = e.target;
|
|
868
|
+
if (!t || !t.closest || !t.closest(".hd") || t.closest(".x") || t.closest(".allbtn")) { return; }
|
|
869
|
+
var r = wrap.getBoundingClientRect();
|
|
870
|
+
var offX = e.clientX - r.left, offY = e.clientY - r.top;
|
|
871
|
+
e.preventDefault();
|
|
872
|
+
function clamp(x, y, w, h) {
|
|
873
|
+
return { x: Math.min(Math.max(0, x), Math.max(0, window.innerWidth - w)),
|
|
874
|
+
y: Math.min(Math.max(0, y), Math.max(0, window.innerHeight - h)) };
|
|
875
|
+
}
|
|
876
|
+
function onMove(ev) {
|
|
877
|
+
var w = wrap.getBoundingClientRect().width || 344, h = wrap.getBoundingClientRect().height || 220;
|
|
878
|
+
var p = clamp(ev.clientX - offX, ev.clientY - offY, w, h);
|
|
879
|
+
wrap.style.left = p.x + "px"; wrap.style.top = p.y + "px"; wrap.style.right = "auto";
|
|
880
|
+
try { localStorage.setItem(POS_KEY, p.x + "," + p.y); } catch (e2) {}
|
|
881
|
+
}
|
|
882
|
+
function onUp() {
|
|
883
|
+
window.removeEventListener("pointermove", onMove);
|
|
884
|
+
window.removeEventListener("pointerup", onUp);
|
|
885
|
+
window.removeEventListener("pointercancel", onUp);
|
|
886
|
+
}
|
|
887
|
+
window.addEventListener("pointermove", onMove);
|
|
888
|
+
window.addEventListener("pointerup", onUp);
|
|
889
|
+
window.addEventListener("pointercancel", onUp);
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
/* 点击外部收起(捕获段,按下即收) */
|
|
893
|
+
document.addEventListener("pointerdown", function (e) {
|
|
894
|
+
var w = document.getElementById(PANEL_ID);
|
|
895
|
+
if (w !== host) { return; }
|
|
896
|
+
var cur = host.shadowRoot.querySelector(".wrap");
|
|
897
|
+
if (!cur || cur.hidden) { return; }
|
|
898
|
+
if (host.contains(e.target)) { return; }
|
|
899
|
+
cur.hidden = true;
|
|
900
|
+
}, true);
|
|
901
|
+
document.addEventListener("keydown", function (e) {
|
|
902
|
+
if (e.key !== "Escape") { return; }
|
|
903
|
+
var cur = host.shadowRoot.querySelector(".wrap");
|
|
904
|
+
if (cur) { cur.hidden = true; }
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
/* 行内点击委托:有新版本 → 入队;更新中/等待 → 取消;失败 → 重试 */
|
|
908
|
+
wrap.addEventListener("click", function (e) {
|
|
909
|
+
var t = e.target;
|
|
910
|
+
if (!t || !t.classList) { return; }
|
|
911
|
+
if (t.classList.contains("x")) { wrap.hidden = true; return; }
|
|
912
|
+
if (t.classList.contains("recheck")) { if (!checking) { checkNow(); } return; }
|
|
913
|
+
if (t.classList.contains("reverify")) { if (!checking) { checkNow(); } return; }
|
|
914
|
+
if (t.classList.contains("allbtn")) {
|
|
915
|
+
rows.forEach(function (r) { if (r.state === "update" && queue.indexOf(r.name) < 0 && r.name !== busyName) { enqueue(r.name); } });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
if (t.classList.contains("act")) {
|
|
919
|
+
var nm = t.getAttribute("data-n");
|
|
920
|
+
if (nm) { enqueue(nm); }
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (t.classList.contains("cancel")) {
|
|
924
|
+
var nmc = t.getAttribute("data-n");
|
|
925
|
+
if (nmc && nmc !== busyName) { cancelQueued(nmc); } else { requestCancel(); }
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
if (t.classList.contains("retry")) {
|
|
929
|
+
var nm2 = t.getAttribute("data-n");
|
|
930
|
+
checkOne(nm2);
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
/* 状态 → 行渲染订阅 */
|
|
935
|
+
listeners.push(function () { paint(); });
|
|
936
|
+
return wrap;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function checkOne(name) {
|
|
940
|
+
for (var i = 0; i < rows.length; i++) {
|
|
941
|
+
if (rows[i].name === name) {
|
|
942
|
+
var cur = rows[i];
|
|
943
|
+
cur.state = "checking"; cur.why = "";
|
|
944
|
+
emit();
|
|
945
|
+
fetchLatest(name).then(function (v) {
|
|
946
|
+
var r = null;
|
|
947
|
+
for (var j = 0; j < rows.length; j++) { if (rows[j].name === name) { r = rows[j]; break; } }
|
|
948
|
+
if (!r) { return; }
|
|
949
|
+
r.latest = v || "";
|
|
950
|
+
r.state = v && cmpSemver(v, r.version) > 0 ? "update" : "current";
|
|
951
|
+
emit();
|
|
952
|
+
}).catch(function (e) {
|
|
953
|
+
setRow(name, e && e.notFound ? { state: notFoundState(cur), why: String(e.message) } : { state: "error", why: String(e && e.message || e) });
|
|
954
|
+
});
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
var TAG_TEXT = {
|
|
961
|
+
checking: "比对中 …", current: "已是最新", update: "有新版本",
|
|
962
|
+
local: "本地链接", remote: "Git 直装", unknown: "无法比对", error: "检查失败", queuing: "等待更新",
|
|
963
|
+
busy: "更新中 …", unchanged: "暂缓更新", unconfirmed: "已发起·未确认", fail: "更新失败"
|
|
964
|
+
};
|
|
965
|
+
function tagHtml(r) {
|
|
966
|
+
var cls = "tag other", act = "", title = "";
|
|
967
|
+
if (r.state === "update") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"点击更新到最新版\""; }
|
|
968
|
+
else if (r.state === "current") { cls = "tag current"; }
|
|
969
|
+
else if (r.state === "error") { cls = "tag error retry clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "检查失败") + "(点击重查)\""; }
|
|
970
|
+
else if (r.state === "local") { title = " title=\"" + esc(r.src || r.why || "本地直装") + ";没有可比的 registry 版本\""; }
|
|
971
|
+
else if (r.state === "remote") { title = " title=\"" + esc(r.src || r.why || "Git/URL 直装") + ";没有可比的 registry 版本\""; }
|
|
972
|
+
else if (r.state === "unknown") { title = " title=\"" + esc(r.why || "插件清单未提供已装版本") + ",因此无法与 registry 比对\""; }
|
|
973
|
+
else if (r.state === "queuing") { cls = "tag other cancel clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"点击取消排队\""; }
|
|
974
|
+
else if (r.state === "busy") { cls = "tag other cancel clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why ? r.why + ";" : "") + "点击取消本次更新\""; }
|
|
975
|
+
else if (r.state === "fail") { cls = "tag error act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "更新失败") + "(点击重试)\""; }
|
|
976
|
+
else if (r.state === "unchanged") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "") + "(点击重试)\""; }
|
|
977
|
+
else if (r.state === "unconfirmed") { cls = "tag other reverify clickable"; title = " title=\"" + esc(r.why || "") + "(点击重查)\""; }
|
|
978
|
+
return "<span class=\"" + cls + "\"" + act + title + ">" + esc(TAG_TEXT[r.state] || "未比对") + "</span>";
|
|
979
|
+
}
|
|
980
|
+
function paint() {
|
|
981
|
+
var wrap = panelWrap();
|
|
982
|
+
var upd = 0;
|
|
983
|
+
rows.forEach(function (r) { if (r.state === "update") { upd++; } });
|
|
984
|
+
var allBtn = upd ? "<span class=\"allbtn\">全部更新(" + upd + ")</span>" : "<span class=\"allbtn\" hidden></span>";
|
|
985
|
+
var body;
|
|
986
|
+
if (!rows.length) {
|
|
987
|
+
body = "<div class=\"msg\">" + (checking ? "正在读取插件清单…" : "点上方「重新检查」开始比对") + "</div>";
|
|
988
|
+
} else {
|
|
989
|
+
body = rows.map(function (r) {
|
|
990
|
+
var showLatest = r.state === "update" || r.state === "unchanged" || r.state === "fail";
|
|
991
|
+
var ver = r.latest && showLatest ? r.version + " → " + r.latest : r.version;
|
|
992
|
+
var sub = r.why && (r.state === "fail" || r.state === "unchanged" || r.state === "unconfirmed" || r.state === "error")
|
|
993
|
+
? "<div class=\"why\">" + esc(r.why) + "</div>" : "";
|
|
994
|
+
return "<div class=\"rowwrap\"><div class=\"row\">" +
|
|
995
|
+
"<span class=\"nm\" title=\"" + esc(r.name) + "\">" + esc(r.name) + "</span>" +
|
|
996
|
+
"<span class=\"ver\" title=\"" + esc(ver) + "\">" + esc(ver) + "</span>" +
|
|
997
|
+
tagHtml(r) + "</div>" + sub + "</div>";
|
|
998
|
+
}).join("");
|
|
999
|
+
}
|
|
1000
|
+
var html = "<div class=\"hd\"><b>插件更新 · " + (busyName ? "正在更新 " + esc(busyName) + " …" : (checking ? "检查中…" : (rows.length ? upd + " 个插件有新版本" : "插件更新检查"))) + "</b>" +
|
|
1001
|
+
"<span class=\"recheck\" title=\"重新读取插件清单并比对 registry\">重新检查</span>" + allBtn +
|
|
1002
|
+
"<span class=\"x\" title=\"关闭\">×</span></div><div class=\"rows\">" + body + "</div>";
|
|
1003
|
+
if (wrap.innerHTML !== html) {
|
|
1004
|
+
var keep = wrap.querySelector(".rows");
|
|
1005
|
+
var st = keep ? keep.scrollTop : 0;
|
|
1006
|
+
wrap.innerHTML = html;
|
|
1007
|
+
var nk = wrap.querySelector(".rows");
|
|
1008
|
+
if (nk && st) { nk.scrollTop = st; }
|
|
1009
|
+
}
|
|
1010
|
+
wrap.hidden = false;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/* ---- 入口按钮:挂官方插件页顶栏(刷新按钮左侧),样式克隆「添加
|
|
1014
|
+
插件」按钮。官方类名是 CSS-module hash(不硬编码),按钮在官方
|
|
1015
|
+
页面渲染后由 findToolbar 实时克隆出来,换版本也不破。刷新按钮
|
|
1016
|
+
与添加按钮同处一个 toolbar 容器,用文案「添加插件/Add plugin」
|
|
1017
|
+
定位该容器,再插到容器首位(刷新按钮之前),保持既有更新面板。
|
|
1018
|
+
React 重渲染会抹掉手插节点,MO + 轮询兜底补挂。---- */
|
|
1019
|
+
function mountButton(openPanel) {
|
|
1020
|
+
function findToolbar() {
|
|
1021
|
+
var panel = document.querySelector('[data-plugin-panel]');
|
|
1022
|
+
if (!panel) { return null; }
|
|
1023
|
+
var header = panel.querySelector("header");
|
|
1024
|
+
if (!header) { return null; }
|
|
1025
|
+
var add = null;
|
|
1026
|
+
var btns = header.querySelectorAll("button");
|
|
1027
|
+
for (var i = 0; i < btns.length; i++) {
|
|
1028
|
+
var t = String(btns[i].textContent || "").trim();
|
|
1029
|
+
/* 「添加插件」在安装进行中会变成「查看安装任务」,两种都要认 */
|
|
1030
|
+
if (t === "添加插件" || t === "Add plugin" || t === "查看安装任务" || t === "View installation") { add = btns[i]; break; }
|
|
1031
|
+
}
|
|
1032
|
+
if (!add || !add.parentElement) { return null; }
|
|
1033
|
+
return { toolbar: add.parentElement, addBtn: add };
|
|
1034
|
+
}
|
|
1035
|
+
function mount() {
|
|
1036
|
+
var found = findToolbar();
|
|
1037
|
+
var host = document.getElementById(BTN_ID);
|
|
1038
|
+
if (host) {
|
|
1039
|
+
/* 按钮已在 DOM:若工具栏在而按钮被挤到别处(React 重渲染),归位 */
|
|
1040
|
+
if (found && host.parentElement !== found.toolbar) {
|
|
1041
|
+
found.toolbar.insertBefore(host, found.toolbar.firstElementChild);
|
|
1042
|
+
}
|
|
1043
|
+
return true;
|
|
1044
|
+
}
|
|
1045
|
+
if (!found) { return false; }
|
|
1046
|
+
var b = found.addBtn.cloneNode(false);
|
|
1047
|
+
b.id = BTN_ID;
|
|
1048
|
+
b.disabled = false;
|
|
1049
|
+
b.textContent = "检查更新";
|
|
1050
|
+
b.title = "比对全部已装插件与 npm registry 的版本(不含 dsh 本体)";
|
|
1051
|
+
b.addEventListener("click", openPanel);
|
|
1052
|
+
found.toolbar.insertBefore(b, found.toolbar.firstElementChild);
|
|
1053
|
+
return true;
|
|
1054
|
+
}
|
|
1055
|
+
mount();
|
|
1056
|
+
try {
|
|
1057
|
+
var mo = new MutationObserver(function () { mount(); });
|
|
1058
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
1059
|
+
} catch (e) {}
|
|
1060
|
+
setInterval(mount, 2000);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
mountButton(function () { paint(); if (!rows.length && !checking) { checkNow(); } });
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/* ======================================================================
|
|
1067
|
+
* 模块 4:动画修复(把 prefers-reduced-motion 强制成 no-preference)
|
|
1068
|
+
*
|
|
1069
|
+
* 背景:Windows「动画效果」关闭时 Chromium 对外报告 prefers-reduced-motion:
|
|
1070
|
+
* reduce,dsh 前端 17 个包共 40 处 @media (prefers-reduced-motion: reduce)
|
|
1071
|
+
* 规则把动画全关(圈圈不转、流光消失)。dsh-web-shell 用 CDP
|
|
1072
|
+
* Emulation.setEmulatedMedia 修复,但桌面端默认没有调试端口,CDP 进不去。
|
|
1073
|
+
*
|
|
1074
|
+
* 页面内等价修法(无需调试端口,web/桌面通用):
|
|
1075
|
+
* 1) CSSOM 清除 —— 实测 dsh 的 CSS 全部经 <style> 标签注入(0 处
|
|
1076
|
+
* adoptedStyleSheets),同源样式表能从 document.styleSheets 遍历
|
|
1077
|
+
* cssRules;把 reduce 的 media 块整条删除后,基础动画声明(都在
|
|
1078
|
+
* 非 media 规则里)立即生效 —— 效果与 CDP 覆盖等价。
|
|
1079
|
+
* 2) matchMedia 包装 —— chat 包有 3 处 JS 门控(滚动平滑/跳转)读
|
|
1080
|
+
* matchMedia;包一层按 no-preference 应答,顺带覆盖未来新增门控。
|
|
1081
|
+
* CSS 分包是懒加载的,新 <style> 注入由 MutationObserver + 轮询兜底重扫;
|
|
1082
|
+
* 换肤等对既有 style 节点的重写也靠轮询兜回来。
|
|
1083
|
+
* ==================================================================== */
|
|
1084
|
+
function installAnimationFix() {
|
|
1085
|
+
if (window.__DSHUIW_ANIM__) { return; }
|
|
1086
|
+
window.__DSHUIW_ANIM__ = true;
|
|
1087
|
+
var REDUCE_RE = /prefers-reduced-motion\s*:\s*reduce/i;
|
|
1088
|
+
var purged = 0;
|
|
1089
|
+
|
|
1090
|
+
/* 递归删 reduce media 块;owner 是持有 deleteRule 的一层(sheet 或外层规则) */
|
|
1091
|
+
function purge(ruleList, owner) {
|
|
1092
|
+
for (var i = ruleList.length - 1; i >= 0; i--) {
|
|
1093
|
+
var r = null;
|
|
1094
|
+
try { r = ruleList[i]; } catch (e) { continue; }
|
|
1095
|
+
if (!r) { continue; }
|
|
1096
|
+
if (r.media && REDUCE_RE.test(r.media.mediaText || "")) {
|
|
1097
|
+
try { owner.deleteRule(i); purged++; } catch (e) {}
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
try { if (r.cssRules && r.cssRules.length) { purge(r.cssRules, r); } } catch (e) {}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function scanAll() {
|
|
1104
|
+
var sheets = [];
|
|
1105
|
+
try { sheets = Array.prototype.slice.call(document.styleSheets); } catch (e) {}
|
|
1106
|
+
try { if (document.adoptedStyleSheets) { sheets = sheets.concat(Array.prototype.slice.call(document.adoptedStyleSheets)); } } catch (e) {}
|
|
1107
|
+
for (var i = 0; i < sheets.length; i++) {
|
|
1108
|
+
try {
|
|
1109
|
+
var sh = sheets[i];
|
|
1110
|
+
if (sh && sh.cssRules && sh.cssRules.length) { purge(sh.cssRules, sh); }
|
|
1111
|
+
} catch (e) {} // 跨域 <link> 读 cssRules 会抛,静默跳过
|
|
1112
|
+
}
|
|
1113
|
+
return purged;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
var first = scanAll();
|
|
1117
|
+
try { console.info("[dsh-ui-tweaks:" + BUILD + "] 动画修复:已清除 " + first + " 条 prefers-reduced-motion: reduce 规则"); } catch (e) {}
|
|
1118
|
+
|
|
1119
|
+
/* 懒加载分包/换肤重写样式 → 防抖重扫;2s 轮询兜底(纯内存枚举,开销可忽略) */
|
|
1120
|
+
var timer = 0;
|
|
1121
|
+
function schedule() {
|
|
1122
|
+
if (timer) { return; }
|
|
1123
|
+
timer = setTimeout(function () { timer = 0; scanAll(); }, 200);
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
var mo = new MutationObserver(schedule);
|
|
1127
|
+
mo.observe(document.head, { childList: true, subtree: true });
|
|
1128
|
+
} catch (e) {}
|
|
1129
|
+
setInterval(scanAll, 2000);
|
|
1130
|
+
|
|
1131
|
+
/* matchMedia 包装:一律按 no-preference 应答(覆盖 JS 门控)。
|
|
1132
|
+
注意要在应用渲染前生效 —— 本插件 apply 在合成期执行,早于挂载。 */
|
|
1133
|
+
try {
|
|
1134
|
+
var nativeMM = window.matchMedia ? window.matchMedia.bind(window) : null;
|
|
1135
|
+
if (nativeMM) {
|
|
1136
|
+
window.matchMedia = function (q) {
|
|
1137
|
+
var mql = nativeMM(q);
|
|
1138
|
+
try {
|
|
1139
|
+
var qs = String(q || "");
|
|
1140
|
+
if (/prefers-reduced-motion/i.test(qs)) {
|
|
1141
|
+
var fake = !/:\s*reduce/i.test(qs); // reduce→false;no-preference/裸→true
|
|
1142
|
+
Object.defineProperty(mql, "matches", { value: fake, configurable: true });
|
|
1143
|
+
}
|
|
1144
|
+
} catch (e) {}
|
|
1145
|
+
return mql;
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
} catch (e) {}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function apply(ctx) {
|
|
1152
|
+
try { window.__DSHUIW_BUILD__ = BUILD; } catch (e) {}
|
|
1153
|
+
guard("tablefix", installTableWidthFix);
|
|
1154
|
+
guard("scrollbar", installLeftScrollbar);
|
|
1155
|
+
guard("updates", function () { installPluginUpdates(ctx); });
|
|
1156
|
+
guard("anim", installAnimationFix);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
module.exports = { name: "ui-tweaks", apply: apply, inject: ["remote", "remote.pluginManager", "remote.pluginInventory"] };
|
|
1160
|
+
return module.exports;
|
|
1161
|
+
}
|
|
1162
|
+
});
|