@mrrisega/dsh-remote 0.6.6-beta.3 → 0.6.6
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.
|
@@ -106,7 +106,9 @@ const STYLE = `
|
|
|
106
106
|
opacity: 0; pointer-events: none;
|
|
107
107
|
transition: opacity .22s ease;
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
/* 拦截点击只认 dsh-ma-scrim-on(由 sync 按"用户意图或几何可见"决定)——
|
|
110
|
+
避免"判定说展开了、其实侧栏还在屏外"时遮罩挡住整屏。 */
|
|
111
|
+
html.dsh-ma-scrim-on div.dsh-ma-scrim,
|
|
110
112
|
html.dsh-ma-details-open div.dsh-ma-scrim { opacity: 1; pointer-events: auto; }
|
|
111
113
|
|
|
112
114
|
/* --- 顶部小鲸鱼/菜单按钮(侧栏收起时可见) --- */
|
|
@@ -114,7 +116,9 @@ const STYLE = `
|
|
|
114
116
|
position: fixed;
|
|
115
117
|
top: max(10px, env(safe-area-inset-top));
|
|
116
118
|
left: max(10px, env(safe-area-inset-left));
|
|
117
|
-
|
|
119
|
+
/* 必须高于遮罩(290):抽屉展开时遮罩会拦截整屏点击,若菜单按钮压在遮罩下面
|
|
120
|
+
就会出现"抽屉开了、按钮点不动"——用户实测反馈的正是这个。 */
|
|
121
|
+
z-index: 320;
|
|
118
122
|
width: 42px; height: 42px; padding: 0;
|
|
119
123
|
display: flex; align-items: center; justify-content: center;
|
|
120
124
|
border-radius: 12px;
|
|
@@ -216,6 +220,76 @@ const STYLE = `
|
|
|
216
220
|
}
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
/* ===== 手机端字号档位(2026-09-16 新增) =====
|
|
224
|
+
需求:4 档(最小=当前/中/大/超大),设置面板里用滑块调,**只在手机端生效**(本文件整体在
|
|
225
|
+
@media (max-width: 820px) 内,PC 完全不受影响)。
|
|
226
|
+
实现:官方组件大量用写死的 px 字号,只改 :root font-size 对它们无效;
|
|
227
|
+
因此"尽力而为地叠加"两种手段:
|
|
228
|
+
① html { font-size: 计算后的 px } —— 对用 rem 的地方生效;
|
|
229
|
+
② html { -webkit-text-size-adjust: 百分比 } —— 移动端 Safari/Chrome 对文本做整体缩放
|
|
230
|
+
(只影响文字、不影响布局,比 zoom/transform 安全得多);
|
|
231
|
+
③ 正文气泡走官方自己的变量 --dsh-content-font-size / --dsh-content-font-delta
|
|
232
|
+
(官方聊天正文就是这么取值的),换算后同步设置,保证正文一定变大。
|
|
233
|
+
适配层自己注入的 UI(.dsh-ma-*)写成固定 px,不参与缩放。 */
|
|
234
|
+
html { --dsh-ma-fs-scale: 1; font-size: calc(16px * var(--dsh-ma-fs-scale)); }
|
|
235
|
+
html[data-dsh-ma-fs="1"] { --dsh-ma-fs-scale: 1.08; -webkit-text-size-adjust: 108%; }
|
|
236
|
+
html[data-dsh-ma-fs="2"] { --dsh-ma-fs-scale: 1.18; -webkit-text-size-adjust: 118%; }
|
|
237
|
+
html[data-dsh-ma-fs="3"] { --dsh-ma-fs-scale: 1.32; -webkit-text-size-adjust: 132%; }
|
|
238
|
+
/* 注意:不要再给 body 设 font-size —— html 的字号已经缩放过了,
|
|
239
|
+
1rem 此时等于"缩放后的字号",乘上 scale 会把放大平方(1.32 → 1.74)并连累所有子元素。
|
|
240
|
+
子元素走 -webkit-text-size-adjust(文本整体缩放) + inherit 即可。 */
|
|
241
|
+
|
|
242
|
+
/* 字号设置面板:右下角浮动按钮 + 展开的小卡片(自适应、可点外部关闭) */
|
|
243
|
+
button.dsh-ma-fsbtn {
|
|
244
|
+
position: fixed;
|
|
245
|
+
right: max(10px, env(safe-area-inset-right));
|
|
246
|
+
bottom: calc(84px + env(safe-area-inset-bottom));
|
|
247
|
+
z-index: 310;
|
|
248
|
+
width: 44px; height: 44px; padding: 0;
|
|
249
|
+
display: flex; align-items: center; justify-content: center;
|
|
250
|
+
border-radius: 50%;
|
|
251
|
+
font: 600 15px/1 -apple-system, system-ui, sans-serif;
|
|
252
|
+
background: var(--dsw-alias-button-elevated-fill, rgba(255,255,255,.96));
|
|
253
|
+
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.3));
|
|
254
|
+
box-shadow: 0 2px 10px rgba(0,0,0,.18);
|
|
255
|
+
color: var(--dsw-alias-label-primary, #111);
|
|
256
|
+
cursor: pointer; touch-action: manipulation;
|
|
257
|
+
-webkit-tap-highlight-color: transparent;
|
|
258
|
+
}
|
|
259
|
+
button.dsh-ma-fsbtn:active { transform: scale(.94); }
|
|
260
|
+
div.dsh-ma-fspanel {
|
|
261
|
+
position: fixed;
|
|
262
|
+
right: max(10px, env(safe-area-inset-right));
|
|
263
|
+
bottom: calc(136px + env(safe-area-inset-bottom));
|
|
264
|
+
z-index: 311;
|
|
265
|
+
width: min(78vw, 300px);
|
|
266
|
+
box-sizing: border-box;
|
|
267
|
+
padding: 12px 14px 14px;
|
|
268
|
+
border-radius: 14px;
|
|
269
|
+
background: var(--dsw-alias-bg-elevated, #fff);
|
|
270
|
+
border: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.28));
|
|
271
|
+
box-shadow: 0 8px 28px rgba(0,0,0,.24);
|
|
272
|
+
color: var(--dsw-alias-label-primary, #111);
|
|
273
|
+
font: 400 14px/1.45 -apple-system, system-ui, sans-serif;
|
|
274
|
+
}
|
|
275
|
+
div.dsh-ma-fspanel[hidden] { display: none; }
|
|
276
|
+
div.dsh-ma-fspanel .dsh-ma-fs-title { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
|
|
277
|
+
div.dsh-ma-fspanel .dsh-ma-fs-sub { color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 12px; }
|
|
278
|
+
div.dsh-ma-fspanel .dsh-ma-fs-val { font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
279
|
+
div.dsh-ma-fspanel input[type="range"] {
|
|
280
|
+
width: 100%; margin: 10px 0 2px; height: 28px;
|
|
281
|
+
accent-color: var(--dsw-alias-brand-primary, #4d6bfe);
|
|
282
|
+
}
|
|
283
|
+
div.dsh-ma-fspanel .dsh-ma-fs-ticks {
|
|
284
|
+
display: flex; justify-content: space-between;
|
|
285
|
+
color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 11px;
|
|
286
|
+
}
|
|
287
|
+
div.dsh-ma-fspanel .dsh-ma-fs-preview {
|
|
288
|
+
margin-top: 10px; padding-top: 9px;
|
|
289
|
+
border-top: .5px solid var(--dsw-alias-border-l3, rgba(127,127,127,.22));
|
|
290
|
+
font-size: 14px;
|
|
291
|
+
}
|
|
292
|
+
|
|
219
293
|
/* 桌面宽屏(>820px):本文件无任何规则 → 对直连/桌面零影响。 */
|
|
220
294
|
`;
|
|
221
295
|
|
|
@@ -498,10 +572,11 @@ const SCRIPT = `(() => {
|
|
|
498
572
|
document.body.appendChild(scrim);
|
|
499
573
|
scrim.addEventListener("click", (e) => {
|
|
500
574
|
e.stopPropagation();
|
|
575
|
+
userWantsOpen = false; // 用户明确要关(遮罩的作用就是关抽屉)
|
|
501
576
|
const t = toggleOf();
|
|
502
|
-
if (t) { t.click();
|
|
503
|
-
/*
|
|
504
|
-
document.documentElement.classList.remove("dsh-ma-sidebar-open", "dsh-ma-details-open");
|
|
577
|
+
if (t) { t.click(); }
|
|
578
|
+
/* 兜底 + 保险:无论官方 toggle 在不在,都确保"点空白处"能关掉/不再拦截 */
|
|
579
|
+
document.documentElement.classList.remove("dsh-ma-sidebar-open", "dsh-ma-details-open", "dsh-ma-scrim-on");
|
|
505
580
|
});
|
|
506
581
|
|
|
507
582
|
/* 顶部小鲸鱼/菜单按钮 */
|
|
@@ -513,12 +588,91 @@ const SCRIPT = `(() => {
|
|
|
513
588
|
document.body.appendChild(hamburger);
|
|
514
589
|
hamburger.addEventListener("click", (e) => {
|
|
515
590
|
e.stopPropagation(); e.preventDefault();
|
|
591
|
+
const nowOpen = document.documentElement.classList.contains("dsh-ma-sidebar-open");
|
|
592
|
+
// 先登记用户意图:即便官方 toggle 不存在、或几何判定一时不满足,这次展开也必须成立
|
|
593
|
+
userWantsOpen = !nowOpen;
|
|
516
594
|
const t = toggleOf();
|
|
517
|
-
if (t) { t.click();
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
595
|
+
if (t && !nowOpen) { t.click(); } // 官方 toggle 存在 → 交给官方(state 一致)
|
|
596
|
+
else if (t && nowOpen) { t.click(); }
|
|
597
|
+
sync(); // 立刻按意图生效(不依赖官方属性回调)
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
/* ===== 手机端字号档位(2026-09-16 新增) =====
|
|
601
|
+
4 档:0=最小(当前) / 1=中 / 2=大 / 3=超大;滑块调;localStorage 记住;只作用于手机端
|
|
602
|
+
(样式在 @media (max-width:820px) 内,PC 不受影响)。
|
|
603
|
+
实现见 STYLE 里 "手机端字号档位" 段:改 :root 字号 + -webkit-text-size-adjust,
|
|
604
|
+
并同步官方正文变量(--dsh-content-font-size),保证聊天正文一定跟着变。 */
|
|
605
|
+
const FS_KEY = "dsh-ma-font-scale";
|
|
606
|
+
const FS_LEVELS = [
|
|
607
|
+
{ name: "最小", scale: 1.00, content: 14 },
|
|
608
|
+
{ name: "中", scale: 1.08, content: 15 },
|
|
609
|
+
{ name: "大", scale: 1.18, content: 16.5 },
|
|
610
|
+
{ name: "超大", scale: 1.32, content: 18.5 }
|
|
611
|
+
];
|
|
612
|
+
const readFsLevel = () => {
|
|
613
|
+
try {
|
|
614
|
+
const raw = window.localStorage && window.localStorage.getItem(FS_KEY);
|
|
615
|
+
const i = Number(raw);
|
|
616
|
+
return Number.isInteger(i) && i >= 0 && i < FS_LEVELS.length ? i : 0;
|
|
617
|
+
} catch (e) { return 0; }
|
|
618
|
+
};
|
|
619
|
+
const applyFsLevel = (level) => {
|
|
620
|
+
const L = FS_LEVELS[level] || FS_LEVELS[0];
|
|
621
|
+
const root = document.documentElement;
|
|
622
|
+
if (level === 0) root.removeAttribute("data-dsh-ma-fs");
|
|
623
|
+
else root.setAttribute("data-dsh-ma-fs", String(level));
|
|
624
|
+
// 官方聊天正文:直接喂它自己的变量(它就是这样取值的)
|
|
625
|
+
try {
|
|
626
|
+
root.style.setProperty("--dsh-content-font-size", L.content + "px");
|
|
627
|
+
root.style.setProperty("--dsh-content-font-delta", (L.content - 14) + "px");
|
|
628
|
+
} catch (e2) { /* 忽略 */ }
|
|
629
|
+
return L;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const fsPanel = document.createElement("div");
|
|
633
|
+
fsPanel.className = "dsh-ma-fspanel";
|
|
634
|
+
fsPanel.hidden = true;
|
|
635
|
+
fsPanel.innerHTML =
|
|
636
|
+
'<div class="dsh-ma-fs-title">显示字号</div>' +
|
|
637
|
+
'<div class="dsh-ma-fs-sub">仅本机手机端生效,电脑端不受影响 · 当前 <span class="dsh-ma-fs-val">最小</span></div>' +
|
|
638
|
+
'<input type="range" min="0" max="3" step="1" value="0" aria-label="显示字号">' +
|
|
639
|
+
'<div class="dsh-ma-fs-ticks"><span>最小</span><span>中</span><span>大</span><span>超大</span></div>' +
|
|
640
|
+
'<div class="dsh-ma-fs-preview">预览:手机远程控制,随手可用。</div>';
|
|
641
|
+
document.body.appendChild(fsPanel);
|
|
642
|
+
const fsBtn = document.createElement("button");
|
|
643
|
+
fsBtn.type = "button";
|
|
644
|
+
fsBtn.className = "dsh-ma-fsbtn";
|
|
645
|
+
fsBtn.setAttribute("aria-label", "显示字号");
|
|
646
|
+
fsBtn.textContent = "字";
|
|
647
|
+
document.body.appendChild(fsBtn);
|
|
648
|
+
|
|
649
|
+
const fsRange = fsPanel.querySelector('input[type="range"]');
|
|
650
|
+
const fsVal = fsPanel.querySelector(".dsh-ma-fs-val");
|
|
651
|
+
const fsPreview = fsPanel.querySelector(".dsh-ma-fs-preview");
|
|
652
|
+
const setLevel = (level, persist) => {
|
|
653
|
+
const L = applyFsLevel(level);
|
|
654
|
+
if (fsVal) fsVal.textContent = L.name;
|
|
655
|
+
if (fsRange && Number(fsRange.value) !== level) fsRange.value = String(level);
|
|
656
|
+
if (fsPreview) fsPreview.style.fontSize = L.content + "px";
|
|
657
|
+
if (persist) { try { window.localStorage && window.localStorage.setItem(FS_KEY, String(level)); } catch (e3) { /* 忽略 */ } }
|
|
658
|
+
};
|
|
659
|
+
setLevel(readFsLevel(), false);
|
|
660
|
+
|
|
661
|
+
if (fsRange) {
|
|
662
|
+
fsRange.addEventListener("input", (e) => { e.stopPropagation(); setLevel(Number(fsRange.value), true); });
|
|
663
|
+
fsRange.addEventListener("click", (e) => e.stopPropagation());
|
|
664
|
+
fsRange.addEventListener("touchstart", (e) => e.stopPropagation(), { passive: true });
|
|
665
|
+
}
|
|
666
|
+
fsBtn.addEventListener("click", (e) => {
|
|
667
|
+
e.stopPropagation(); e.preventDefault();
|
|
668
|
+
fsPanel.hidden = !fsPanel.hidden;
|
|
669
|
+
});
|
|
670
|
+
// 点面板外部关闭(面板内的事件都 stopPropagation)
|
|
671
|
+
document.addEventListener("click", (e) => {
|
|
672
|
+
if (fsPanel.hidden) return;
|
|
673
|
+
const t = e.target;
|
|
674
|
+
if (t === fsBtn || (t && t.closest && t.closest(".dsh-ma-fspanel, .dsh-ma-fsbtn"))) return;
|
|
675
|
+
fsPanel.hidden = true;
|
|
522
676
|
});
|
|
523
677
|
|
|
524
678
|
/* 侧栏是否**真的**滑进了视口。这是"遮罩能不能拦截点击"的最后一道保险:
|
|
@@ -530,11 +684,19 @@ const SCRIPT = `(() => {
|
|
|
530
684
|
catch (e2) { return false; }
|
|
531
685
|
};
|
|
532
686
|
const sidebarInView = () => inView(sidebar || document.querySelector(".dsh-ma-sidebar"));
|
|
533
|
-
/*
|
|
687
|
+
/* 状态来源(按优先级):
|
|
688
|
+
① 用户意图 userWantsOpen —— 点菜单按钮打开 / 点遮罩关闭,期间**不被几何判定推翻**;
|
|
689
|
+
② 官方 frame 的 data 属性 —— 用户没表达意图时(初始、官方自己切换)以它为准。
|
|
690
|
+
⚠️ 上一版把"几何判定"直接作用在展开 class 上,等于「侧栏还没滑进来就不许展开」:
|
|
691
|
+
点菜单按钮加 class → 动画未开始、rect 仍在屏外 → sync 立刻把 class 摘掉 → 抽屉永远打不开。
|
|
692
|
+
现在只让几何判定决定**遮罩能否拦截点击**(最后一道保险),不再否决用户的展开意图。 */
|
|
693
|
+
let userWantsOpen = null; // null=未表达意图(跟随官方); true/false=用户已明确开/关
|
|
534
694
|
const sync = () => {
|
|
535
|
-
const open = expanded();
|
|
536
|
-
|
|
537
|
-
|
|
695
|
+
const open = userWantsOpen !== null ? userWantsOpen : expanded();
|
|
696
|
+
document.documentElement.classList.toggle("dsh-ma-sidebar-open", open);
|
|
697
|
+
/* 只给遮罩加闸:即使判定/几何有偏差,也绝不让"看不见的遮罩"拦住整屏点击 */
|
|
698
|
+
const scrimOn = open && (userWantsOpen === true || sidebarInView());
|
|
699
|
+
document.documentElement.classList.toggle("dsh-ma-scrim-on", scrimOn);
|
|
538
700
|
/* 汉堡按钮**始终可见**:以前判定为"展开"时会把它 display:none 隐藏,
|
|
539
701
|
于是判定一旦出错(或遮罩因异常常亮),用户既点不动内容、也没有任何入口 —— 死局。
|
|
540
702
|
保持可见 + 点它可开可关(见其 click 处理),任何异常状态下都留一条出路。 */
|
|
@@ -567,8 +729,10 @@ const SCRIPT = `(() => {
|
|
|
567
729
|
const row = t.closest('[role="treeitem"][aria-selected]');
|
|
568
730
|
const isNew = t.closest('[aria-label*="新建会话"], [aria-label*="New session" i]');
|
|
569
731
|
if (!row && !isNew) return;
|
|
732
|
+
userWantsOpen = false;
|
|
570
733
|
const togg = toggleOf();
|
|
571
734
|
if (togg) setTimeout(() => { try { togg.click(); } catch (e5) { /* ignore */ } }, 0);
|
|
735
|
+
setTimeout(sync, 0);
|
|
572
736
|
} catch (e6) { /* 忽略 */ }
|
|
573
737
|
});
|
|
574
738
|
}
|
|
@@ -35,7 +35,8 @@ class El {
|
|
|
35
35
|
this.children = [];
|
|
36
36
|
this._cls = new Set();
|
|
37
37
|
this._listeners = new Map();
|
|
38
|
-
this.style = {};
|
|
38
|
+
this.style = { setProperty: (k, v) => { if (!this._cssVars) this._cssVars = {}; this._cssVars[k] = v; }, removeProperty: (k) => { if (this._cssVars) delete this._cssVars[k]; } };
|
|
39
|
+
this._cssVars = {};
|
|
39
40
|
this._rect = { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0 };
|
|
40
41
|
this.classList = {
|
|
41
42
|
add: (...cs) => cs.forEach((c) => this._cls.add(c)),
|
|
@@ -44,18 +45,47 @@ class El {
|
|
|
44
45
|
toggle: (c, on) => { if (on === undefined) { this._cls.has(c) ? this._cls.delete(c) : this._cls.add(c); } else if (on) this._cls.add(c); else this._cls.delete(c); return this._cls.has(c); },
|
|
45
46
|
};
|
|
46
47
|
}
|
|
48
|
+
// 极简 innerHTML:只认本用例需要的那点结构(class / input[type=range] / 文本)
|
|
49
|
+
set innerHTML(html) {
|
|
50
|
+
this._html = String(html);
|
|
51
|
+
this.children = [];
|
|
52
|
+
for (const m of String(html).matchAll(/<(\w+)([^>]*)>/g)) {
|
|
53
|
+
const tag = m[1];
|
|
54
|
+
const attrs = m[2] || "";
|
|
55
|
+
const el = new El(tag);
|
|
56
|
+
const cls = /class="([^"]*)"/.exec(attrs);
|
|
57
|
+
if (cls) el.className = cls[1];
|
|
58
|
+
const type = /type="([^"]*)"/.exec(attrs);
|
|
59
|
+
if (type) el.attrs.type = type[1];
|
|
60
|
+
this.children.push(el);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
get innerHTML() { return this._html || ""; }
|
|
64
|
+
get textContent() { return this._text || ""; }
|
|
65
|
+
set textContent(v) { this._text = String(v); }
|
|
47
66
|
get className() { return [...this._cls].join(" "); }
|
|
48
67
|
set className(v) { this._cls = new Set(String(v).split(/\s+/).filter(Boolean)); }
|
|
49
68
|
getAttribute(n) { return n in this.attrs ? this.attrs[n] : null; }
|
|
50
69
|
setAttribute(n, v) { this.attrs[n] = String(v); }
|
|
70
|
+
removeAttribute(n) { delete this.attrs[n]; }
|
|
71
|
+
prepend() {} focus() {} blur() {}
|
|
51
72
|
hasAttribute(n) { return n in this.attrs; }
|
|
52
73
|
appendChild(c) { c.parent = this; this.children.push(c); return c; }
|
|
53
74
|
addEventListener(t, fn) { (this._listeners.get(t) || this._listeners.set(t, []).get(t)).push(fn); }
|
|
54
75
|
removeEventListener() {}
|
|
55
76
|
dispatch(t, ev = {}) { for (const fn of this._listeners.get(t) || []) fn({ stopPropagation() {}, preventDefault() {}, ...ev }); }
|
|
56
77
|
getBoundingClientRect() { return this._rect; }
|
|
57
|
-
querySelector() {
|
|
58
|
-
|
|
78
|
+
querySelector(sel) {
|
|
79
|
+
const s2 = String(sel);
|
|
80
|
+
const match = (el) => {
|
|
81
|
+
if (s2.includes('input[type="range"]')) return el.tagName === "INPUT" && el.attrs.type === "range";
|
|
82
|
+
if (s2.startsWith(".")) return el._cls.has(s2.slice(1));
|
|
83
|
+
if (s2.startsWith("input")) return el.tagName === "INPUT";
|
|
84
|
+
return false;
|
|
85
|
+
};
|
|
86
|
+
return this.children.find(match) || null;
|
|
87
|
+
}
|
|
88
|
+
querySelectorAll(sel) { const one = this.querySelector(sel); return one ? [one] : []; }
|
|
59
89
|
matches() { return false; }
|
|
60
90
|
click() { this._clicked = (this._clicked || 0) + 1; this.dispatch("click"); }
|
|
61
91
|
get isConnected() { return true; }
|
|
@@ -114,7 +144,7 @@ function harness({ collapsedAttr = undefined, detailsAttr = undefined, sidebarRe
|
|
|
114
144
|
innerWidth: width,
|
|
115
145
|
innerHeight: 800,
|
|
116
146
|
devicePixelRatio: 1,
|
|
117
|
-
localStorage: {
|
|
147
|
+
localStorage: { _d: {}, getItem(k) { return k in this._d ? this._d[k] : null; }, setItem(k, v) { this._d[k] = String(v); }, removeItem(k) { delete this._d[k]; } },
|
|
118
148
|
addEventListener() {},
|
|
119
149
|
removeEventListener() {},
|
|
120
150
|
getComputedStyle: () => ({ getPropertyValue: () => "" }),
|
|
@@ -148,6 +178,8 @@ const ADAPTER_DEBUG = process.env.DSH_ADAPTER_DEBUG === "1";
|
|
|
148
178
|
function runAdapter(env) {
|
|
149
179
|
try {
|
|
150
180
|
vm.runInContext(adapterScript(), env.ctx, { timeout: 5000 });
|
|
181
|
+
// 让脚本内 setTimeout(0) 的收尾逻辑跑一轮
|
|
182
|
+
for (const fn of env._pendingTimers || []) { try { fn(); } catch { /* ignore */ } }
|
|
151
183
|
} catch (e) {
|
|
152
184
|
if (ADAPTER_DEBUG) console.error("ADAPTER THREW:", e && e.message);
|
|
153
185
|
throw e;
|
|
@@ -161,20 +193,41 @@ const findHamburger = (env) => env.body.children.find((c) => c._cls.has("dsh-ma-
|
|
|
161
193
|
|
|
162
194
|
// ── 用例 ─────────────────────────────────────────────────────────────────────
|
|
163
195
|
|
|
164
|
-
test("运行时:官方属性带值写法 =false
|
|
165
|
-
// 官方若输出 data-sidebar-collapsed="
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
196
|
+
test("运行时:官方属性带值写法 =false 时遮罩不得拦截点击(用户报的整屏阴影)", () => {
|
|
197
|
+
// 官方若输出 data-sidebar-collapsed="true"(折叠),旧实现 hasAttribute 只看存在性;
|
|
198
|
+
// 带值写法下必须按值判断 —— 否则"折叠态"被当成"展开",遮罩常亮拦住整屏点击。
|
|
199
|
+
const env = runAdapter(harness({ collapsedAttr: "true", sidebarRect: { left: 0, right: 300, top: 0, bottom: 800, width: 300, height: 800 } }));
|
|
200
|
+
assert.ok(!env.html.classList.contains("dsh-ma-scrim-on"), "折叠态 → 遮罩不得拦截点击");
|
|
201
|
+
assert.ok(!env.html.classList.contains("dsh-ma-sidebar-open"), "折叠态 → 不应处于展开态");
|
|
202
|
+
// 对照:=false 属"未折叠",是展开态(但遮罩仍只在几何可见时拦截)
|
|
203
|
+
const env2 = runAdapter(harness({ collapsedAttr: "false", sidebarRect: { left: -300, right: 0, top: 0, bottom: 800, width: 300, height: 800 } }));
|
|
204
|
+
assert.ok(!env2.html.classList.contains("dsh-ma-scrim-on"), "侧栏在屏外 → 遮罩绝不拦截");
|
|
169
205
|
});
|
|
170
206
|
|
|
171
|
-
test("
|
|
172
|
-
// 属性"存在且无值"在旧实现里就是展开;这里再叠加"侧栏几何不在视口内"(left:0/right:0)→ 仍不得拦截
|
|
207
|
+
test("运行时:侧栏没真的滑进视口时,遮罩不得拦截点击(几何兜底只管遮罩,不否决展开)", () => {
|
|
173
208
|
const env = runAdapter(harness({ collapsedAttr: "", sidebarRect: { left: 0, right: 0, top: 0, bottom: 800, width: 0, height: 800 } }));
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
assert.ok(
|
|
209
|
+
// 判定/几何可能出偏差,但"看不见的遮罩拦住整屏点击"必须不可能发生
|
|
210
|
+
assert.ok(!env.html.classList.contains("dsh-ma-scrim-on"),
|
|
211
|
+
"侧栏未滑入视口 → 遮罩必须保持不拦截(dsh-ma-scrim-on 不生效)");
|
|
212
|
+
assert.ok(findScrim(env), "遮罩元素本身仍应创建(关闭态只是不拦截)");
|
|
213
|
+
// ⚠️ 注意:展开 class 不再由几何否决 —— 否则点菜单按钮后动画还没开始、rect 仍在屏外,
|
|
214
|
+
// sync 会把 class 立刻摘掉,抽屉永远打不开(这正是 0.6.6-beta.3 的回归)。
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("运行时:点菜单按钮必须能真的打开抽屉(上一版的回归点)", () => {
|
|
218
|
+
// 复现现场:侧栏初始在屏外(translateX(-103%) → rect 仍在外侧),点汉堡
|
|
219
|
+
// 从**收起态**开始(collapsedAttr="true" = 折叠),侧栏几何仍在屏外(动画未开始)
|
|
220
|
+
const env = runAdapter(harness({ collapsedAttr: "true", sidebarRect: { left: -300, right: 0, top: 0, bottom: 800, width: 300, height: 800 }, withToggle: false }));
|
|
221
|
+
assert.ok(!env.html.classList.contains("dsh-ma-sidebar-open"), "前提:初始为收起态");
|
|
222
|
+
const ham = findHamburger(env);
|
|
223
|
+
assert.ok(ham, "菜单按钮应存在");
|
|
224
|
+
assert.equal(ham.style.display, "", "菜单按钮不得被隐藏");
|
|
225
|
+
ham.dispatch("click");
|
|
226
|
+
assert.ok(env.html.classList.contains("dsh-ma-sidebar-open"),
|
|
227
|
+
"点菜单按钮后必须处于展开态(不能被几何判定立刻摘掉)—— 这正是上一版回归的点");
|
|
228
|
+
// 再点一次应能关掉
|
|
229
|
+
ham.dispatch("click");
|
|
230
|
+
assert.ok(!env.html.classList.contains("dsh-ma-sidebar-open"), "再次点击应关闭");
|
|
178
231
|
});
|
|
179
232
|
|
|
180
233
|
test("运行时:侧栏真的滑入视口(right>8 且有宽度)时才允许拦截点击", () => {
|
|
@@ -206,3 +259,49 @@ test("运行时:能拿到官方 toggle 时,点遮罩仍走官方 toggle(
|
|
|
206
259
|
scrim.dispatch("click");
|
|
207
260
|
assert.ok(env.toggle._clicked >= 1, "应点击官方 toggle");
|
|
208
261
|
});
|
|
262
|
+
|
|
263
|
+
// ── 字号档位(2026-09-16 需求:4 档滑块、仅手机端生效) ──────────────────────
|
|
264
|
+
|
|
265
|
+
const findFsBtn = (env) => env.body.children.find((c) => c._cls.has("dsh-ma-fsbtn"));
|
|
266
|
+
const findFsPanel = (env) => env.body.children.find((c) => c._cls.has("dsh-ma-fspanel"));
|
|
267
|
+
|
|
268
|
+
test("字号:菜单/字号按钮与面板都应创建(面板默认收起)", () => {
|
|
269
|
+
const env = runAdapter(harness({}));
|
|
270
|
+
const btn = findFsBtn(env);
|
|
271
|
+
const panel = findFsPanel(env);
|
|
272
|
+
assert.ok(btn, "应有字号按钮");
|
|
273
|
+
assert.ok(panel, "应有字号面板");
|
|
274
|
+
assert.equal(panel.hidden, true, "面板默认收起(点按钮才展开)");
|
|
275
|
+
assert.ok((panel.querySelector ? panel : {}).querySelector === undefined || true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("字号:默认档 = 最小(不加缩放属性,保持当前大小)", () => {
|
|
279
|
+
const env = runAdapter(harness({}));
|
|
280
|
+
assert.equal(env.html.getAttribute("data-dsh-ma-fs"), null, "默认不应带缩放属性");
|
|
281
|
+
assert.equal(env.win.localStorage.getItem("dsh-ma-font-scale"), null);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("字号:滑块改档 → 属性/正文变量/持久化都生效", () => {
|
|
285
|
+
const env = runAdapter(harness({}));
|
|
286
|
+
const panel = findFsPanel(env);
|
|
287
|
+
const range = panel && panel.querySelector && panel.querySelector('input[type="range"]');
|
|
288
|
+
assert.ok(range, "面板里应有 range 滑块");
|
|
289
|
+
// 打到「大」(level 2)
|
|
290
|
+
range.value = "2";
|
|
291
|
+
range.dispatch("input");
|
|
292
|
+
assert.equal(env.html.getAttribute("data-dsh-ma-fs"), "2", "应写入缩放属性(CSS 据此放大字号)");
|
|
293
|
+
assert.equal(env.win.localStorage.getItem("dsh-ma-font-scale"), "2", "应持久化到 localStorage");
|
|
294
|
+
const contentVar = env.html.style["--dsh-content-font-size"] || env.html._cssVars && env.html._cssVars["--dsh-content-font-size"];
|
|
295
|
+
assert.ok(contentVar, "应同步官方正文变量 --dsh-content-font-size(聊天正文才会变大)");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("字号:恢复「最小」时移除缩放属性(回到当前大小)", () => {
|
|
299
|
+
const env = runAdapter(harness({}));
|
|
300
|
+
const panel = findFsPanel(env);
|
|
301
|
+
const range = panel.querySelector('input[type="range"]');
|
|
302
|
+
range.value = "3"; range.dispatch("input");
|
|
303
|
+
assert.equal(env.html.getAttribute("data-dsh-ma-fs"), "3");
|
|
304
|
+
range.value = "0"; range.dispatch("input");
|
|
305
|
+
assert.equal(env.html.getAttribute("data-dsh-ma-fs"), null, "level 0 = 最小 → 不应留缩放属性");
|
|
306
|
+
assert.equal(env.win.localStorage.getItem("dsh-ma-font-scale"), "0");
|
|
307
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.6
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -1812,7 +1812,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
1812
1812
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
1813
1813
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
1814
1814
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
1815
|
-
const PLUGIN_VERSION = "0.6.6
|
|
1815
|
+
const PLUGIN_VERSION = "0.6.6";
|
|
1816
1816
|
const UPDATE_LOG = ".dsh-update.log";
|
|
1817
1817
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
1818
1818
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-web",
|
|
3
|
-
"version": "0.6.6
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|