@mrrisega/dsh-remote 0.7.0-beta.8 → 0.7.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/clients/dsh-remote/dsh-bridge.mjs +17 -17
- package/clients/dsh-remote/dsh-events.mjs +6 -6
- package/clients/dsh-remote/e2ee-client.mjs +7 -7
- package/clients/dsh-remote/e2ee-shim-script.js +9 -9
- package/clients/dsh-remote/mobile-adapter.mjs +294 -43
- package/clients/dsh-remote/test/bridge-gzip.test.mjs +1 -1
- package/clients/dsh-remote/test/device-registration-report.test.mjs +1 -1
- package/clients/dsh-remote/test/dsh-events.test.mjs +10 -10
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +4 -4
- package/clients/dsh-remote/test/e2ee-client.test.mjs +7 -7
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +1 -1
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +1 -1
- package/clients/dsh-remote/test/harness-cookie-authority.test.mjs +4 -4
- package/clients/dsh-remote/test/machine-fingerprint.test.mjs +2 -2
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +6 -6
- package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +1 -1
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +5 -5
- package/clients/dsh-remote/test/mobile-adapter-settings.test.mjs +352 -0
- package/clients/dsh-remote/test/mobile-adapter.test.mjs +35 -0
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +1 -1
- package/clients/dsh-remote/test/wechat-channel.test.mjs +5 -5
- package/clients/dsh-remote/test/wechat-runtime.test.mjs +26 -26
- package/clients/dsh-remote/test/wechat-tier-source.test.mjs +2 -2
- package/clients/dsh-remote/wechat-channel.mjs +9 -9
- package/clients/dsh-remote/wechat-runtime.mjs +38 -38
- package/docs/telemetry.md +3 -3
- package/dsh-setup.mjs +27 -27
- package/package.json +3 -3
- package/packages/dsh-remote-web/lib/client.js +56 -56
- package/packages/dsh-remote-web/lib/fix-prompt.js +1 -1
- package/packages/dsh-remote-web/lib/index.js +380 -59
- package/packages/dsh-remote-web/lib/tarball-update.js +1 -1
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +17 -17
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +6 -6
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +7 -7
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +9 -9
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +294 -43
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +9 -9
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +38 -38
- package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +5 -5
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +27 -27
- package/packages/dsh-remote-web/test/account-switch-device.test.mjs +2 -2
- package/packages/dsh-remote-web/test/activation-single-point.test.mjs +1 -1
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +1 -1
- package/packages/dsh-remote-web/test/connect-loop.test.mjs +1 -1
- package/packages/dsh-remote-web/test/connect-ui.test.mjs +2 -2
- package/packages/dsh-remote-web/test/desktop-compat.test.mjs +4 -4
- package/packages/dsh-remote-web/test/diag-report.test.mjs +1 -1
- package/packages/dsh-remote-web/test/fix-prompt.test.mjs +2 -2
- package/packages/dsh-remote-web/test/harness-restart.test.mjs +1 -1
- package/packages/dsh-remote-web/test/install-profile-targets.test.mjs +3 -3
- package/packages/dsh-remote-web/test/installer-cli.test.mjs +1 -1
- package/packages/dsh-remote-web/test/launchd-domain.test.mjs +2 -2
- package/packages/dsh-remote-web/test/patch-activation.test.mjs +2 -2
- package/packages/dsh-remote-web/test/picker-pin.test.mjs +1 -1
- package/packages/dsh-remote-web/test/plugin-manifest.test.mjs +2 -2
- package/packages/dsh-remote-web/test/quota-absent.test.mjs +4 -4
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -2
- package/packages/dsh-remote-web/test/selfheal-restart-guard.test.mjs +420 -0
- package/packages/dsh-remote-web/test/settings-entry-placement.test.mjs +6 -6
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +2 -2
- package/packages/dsh-remote-web/test/settings-open-path.test.mjs +4 -4
- package/packages/dsh-remote-web/test/setup-output.test.mjs +11 -11
- package/packages/dsh-remote-web/test/tarball-update.test.mjs +1 -1
- package/packages/dsh-remote-web/test/telemetry.test.mjs +4 -4
- package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +2 -2
- package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +1 -1
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +7 -7
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +5 -5
- package/scripts/test-net-guard.cjs +1 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* mobile-adapter 设置面板窄屏适配回归(2026-10-13 新增)。
|
|
4
|
+
*
|
|
5
|
+
* 背景(用户反馈 + 真机 390×844 实测):
|
|
6
|
+
* 「手机端访问设置界面布局挤到一条竖线上」——官方设置弹层是**桌面两栏**
|
|
7
|
+
* (0.2.0-rc.2 实测:panel 342×796、nav 188 宽竖排、content 只剩 154px 可视宽)。
|
|
8
|
+
* 适配层此前只挂 [class~="VOzbGW_*"](0.1.2–0.1.5 的混淆前缀),
|
|
9
|
+
* 0.2.0-rc.2 重新打包后前缀变成 Dws9Sa_* → **整段 CSS 静默失效**。
|
|
10
|
+
* 用户要求的目标形态:**tab 放顶部(横排)+ 下方内容区满宽 + 弹窗按手机尺寸收敛**。
|
|
11
|
+
*
|
|
12
|
+
* 本文件锁三件事(都是"下次不会再静默坏掉"的判据):
|
|
13
|
+
* ① CSS 契约:三层识别(已知前缀 / 结构稳定类 / :has 兜底)、面板竖排、nav 横排置顶、
|
|
14
|
+
* content 满宽、通用弹层 100vw-24px + max-height + 内部滚动、规则顺序(通用弹层在前);
|
|
15
|
+
* ② 结构兜底**真的能跑**:把官方类名全部换成未知前缀(模拟下次改名),在假 DOM 上仍然
|
|
16
|
+
* 认出面板并打上 dsh-ma-settings-* 稳定类;
|
|
17
|
+
* ③ 不回归:宽屏(>820)零动作、只给**已有**节点打类(不新建任何覆盖层节点)、
|
|
18
|
+
* 认不出结构时只留一条可见 console.warn(护栏禁止全屏覆盖层)。
|
|
19
|
+
*/
|
|
20
|
+
import assert from "node:assert/strict";
|
|
21
|
+
import test from "node:test";
|
|
22
|
+
import vm from "node:vm";
|
|
23
|
+
import { injectMobileAdapter } from "../mobile-adapter.mjs";
|
|
24
|
+
|
|
25
|
+
/** 取注入结果里的真实 <script> / <style> 内容(不加壳,原样执行)。 */
|
|
26
|
+
function injected() {
|
|
27
|
+
const { html } = injectMobileAdapter("<!doctype html><html><head></head><body></body></html>");
|
|
28
|
+
const js = /<script id="[^"]*-js"[^>]*>([\s\S]*?)<\/script>/.exec(html);
|
|
29
|
+
const css = /<style id="[^"]*-css"[^>]*>([\s\S]*?)<\/style>/.exec(html);
|
|
30
|
+
assert.ok(js && css, "应能取出注入脚本与样式");
|
|
31
|
+
return { js: js[1], css: css[1] };
|
|
32
|
+
}
|
|
33
|
+
const { js: ADAPTER_JS, css: ADAPTER_CSS } = injected();
|
|
34
|
+
|
|
35
|
+
/** 只取 ≤820px 媒体块(新规则必须全部落在里面,这才谈得上"桌面零影响")。 */
|
|
36
|
+
function narrowCss() {
|
|
37
|
+
const start = ADAPTER_CSS.indexOf("@media (max-width: 820px)");
|
|
38
|
+
assert.ok(start >= 0, "应有窄屏媒体查询");
|
|
39
|
+
let depth = 0, i = ADAPTER_CSS.indexOf("{", start);
|
|
40
|
+
const from = i + 1;
|
|
41
|
+
for (; i < ADAPTER_CSS.length; i++) {
|
|
42
|
+
if (ADAPTER_CSS[i] === "{") depth++;
|
|
43
|
+
else if (ADAPTER_CSS[i] === "}") { depth--; if (depth === 0) break; }
|
|
44
|
+
}
|
|
45
|
+
return ADAPTER_CSS.slice(from, i);
|
|
46
|
+
}
|
|
47
|
+
const NARROW_CSS = narrowCss();
|
|
48
|
+
|
|
49
|
+
// ── 极简假 DOM(只实现本用例需要的那些面) ─────────────────────────────────────
|
|
50
|
+
/** 支持 `tag`、`.cls`、`[attr="v"]`、`[attr]` 与其逗号组合;不支持 `~=`(脚本里没用到)。 */
|
|
51
|
+
function matchesSel(el, sel) {
|
|
52
|
+
return String(sel).split(",").some((one) => {
|
|
53
|
+
const s = one.trim();
|
|
54
|
+
if (!s) return false;
|
|
55
|
+
const m = /^([a-zA-Z][\w-]*)?((?:\.[\w-]+)*)((?:\[[^\]]+\])*)$/.exec(s);
|
|
56
|
+
if (!m) return false;
|
|
57
|
+
if (m[1] && el.tagName !== m[1].toUpperCase()) return false;
|
|
58
|
+
for (const c of (m[2].match(/\.[\w-]+/g) || [])) if (!el._cls.has(c.slice(1))) return false;
|
|
59
|
+
for (const a of (m[3].match(/\[[^\]]+\]/g) || [])) {
|
|
60
|
+
const body = a.slice(1, -1);
|
|
61
|
+
const eq = /^([\w-]+)(?:="([^"]*)")?$/.exec(body);
|
|
62
|
+
if (!eq) return false;
|
|
63
|
+
if (!(eq[1] in el.attrs)) return false;
|
|
64
|
+
if (eq[2] !== undefined && String(el.attrs[eq[1]]).toLowerCase() !== eq[2].toLowerCase()) return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class El {
|
|
71
|
+
constructor(tag = "div", attrs = {}) {
|
|
72
|
+
this.tagName = String(tag).toUpperCase();
|
|
73
|
+
this.attrs = { ...attrs };
|
|
74
|
+
this.children = [];
|
|
75
|
+
this.parent = null;
|
|
76
|
+
this._cls = new Set(String(attrs.class || "").split(/\s+/).filter(Boolean));
|
|
77
|
+
this._rect = { left: 0, right: 0, top: 0, bottom: 0, width: 0, height: 0 };
|
|
78
|
+
this._text = "";
|
|
79
|
+
this.scrolled = 0;
|
|
80
|
+
this.createdBy = null;
|
|
81
|
+
this.classList = {
|
|
82
|
+
add: (...cs) => cs.forEach((c) => this._cls.add(c)),
|
|
83
|
+
remove: (...cs) => cs.forEach((c) => this._cls.delete(c)),
|
|
84
|
+
contains: (c) => this._cls.has(c),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
get className() { return [...this._cls].join(" "); }
|
|
88
|
+
set className(v) { this._cls = new Set(String(v).split(/\s+/).filter(Boolean)); }
|
|
89
|
+
get textContent() { return this._text; }
|
|
90
|
+
set textContent(v) { this._text = String(v); }
|
|
91
|
+
getAttribute(n) { return n in this.attrs ? this.attrs[n] : null; }
|
|
92
|
+
setAttribute(n, v) { this.attrs[n] = String(v); }
|
|
93
|
+
hasAttribute(n) { return n in this.attrs; }
|
|
94
|
+
removeAttribute(n) { delete this.attrs[n]; }
|
|
95
|
+
appendChild(c) { c.parent = this; this.children.push(c); return c; }
|
|
96
|
+
removeChild(c) { this.children = this.children.filter((x) => x !== c); c.parent = null; return c; }
|
|
97
|
+
getBoundingClientRect() { return this._rect; }
|
|
98
|
+
get parentElement() { return this.parent || null; }
|
|
99
|
+
get isConnected() { let n = this; while (n.parent) n = n.parent; return n._isRoot === true; }
|
|
100
|
+
matches(sel) { return matchesSel(this, sel); }
|
|
101
|
+
closest(sel) { let n = this; while (n) { if (n.matches(sel)) return n; n = n.parent; } return null; }
|
|
102
|
+
contains(n) { while (n) { if (n === this) return true; n = n.parent; } return false; }
|
|
103
|
+
descendants() { return this.children.flatMap((c) => [c, ...c.descendants()]); }
|
|
104
|
+
querySelector(sel) { return this.descendants().find((e) => e.matches(sel)) || null; }
|
|
105
|
+
querySelectorAll(sel) { return this.descendants().filter((e) => e.matches(sel)); }
|
|
106
|
+
addEventListener() {}
|
|
107
|
+
removeEventListener() {}
|
|
108
|
+
remove() { if (this.parent) this.parent.removeChild(this); }
|
|
109
|
+
scrollIntoView() { this.scrolled++; }
|
|
110
|
+
focus() {}
|
|
111
|
+
click() {}
|
|
112
|
+
setPointerCapture() {}
|
|
113
|
+
prepend() {}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 建一个"官方设置弹层"盒子:prefix 就是官方混淆前缀(改成未知前缀 = 模拟下次改名)。 */
|
|
117
|
+
function settingsBox(prefix, { width = 390, height = 844, withNav = true, navTag = "nav" } = {}) {
|
|
118
|
+
const rect = (l, t, w, h) => ({ left: l, top: t, width: w, height: h, right: l + w, bottom: t + h });
|
|
119
|
+
const overlay = new El("div", { class: prefix + "overlay", role: "presentation" });
|
|
120
|
+
overlay._rect = rect(0, 0, width, height);
|
|
121
|
+
const mask = new El("div", { class: prefix + "mask" });
|
|
122
|
+
const panel = new El("div", { class: prefix + "panel", role: "dialog", "aria-modal": "true" });
|
|
123
|
+
// 真实 0.2 实测:390 宽下 panel 342×796(桌面两栏),nav 188 宽竖排,content 只剩 154
|
|
124
|
+
panel._rect = rect(24, 24, Math.min(800, width - 48), height - 48);
|
|
125
|
+
const content = new El("div", { class: prefix + "content" });
|
|
126
|
+
content._rect = rect(212, 24, 154, height - 48);
|
|
127
|
+
const header = new El("div", { class: prefix + "header" });
|
|
128
|
+
const options = new El("div", { class: prefix + "options" });
|
|
129
|
+
content.appendChild(header); content.appendChild(options);
|
|
130
|
+
panel.appendChild(content);
|
|
131
|
+
const nodes = [overlay, panel, content, header, options];
|
|
132
|
+
if (withNav) {
|
|
133
|
+
const n = new El(navTag, { class: prefix + "nav" });
|
|
134
|
+
n._rect = rect(24, 24, 188, height - 48);
|
|
135
|
+
const navTitle = new El("div", { class: prefix + "navTitle" });
|
|
136
|
+
navTitle.textContent = "设置";
|
|
137
|
+
const navList = new El("div", { class: prefix + "navList" });
|
|
138
|
+
for (const [i, label] of ["通用设置", "模型", "内置插件"].entries()) {
|
|
139
|
+
const cell = new El("button", { class: prefix + "navCell" + (i === 1 ? " " + prefix + "active" : "") });
|
|
140
|
+
cell.textContent = label;
|
|
141
|
+
if (i === 1) cell.setAttribute("aria-current", "true");
|
|
142
|
+
navList.appendChild(cell);
|
|
143
|
+
nodes.push(cell);
|
|
144
|
+
}
|
|
145
|
+
n.appendChild(navTitle); n.appendChild(navList);
|
|
146
|
+
panel.children.unshift(n);
|
|
147
|
+
n.parent = panel;
|
|
148
|
+
nodes.push(n, navTitle, navList);
|
|
149
|
+
}
|
|
150
|
+
overlay.appendChild(mask); overlay.appendChild(panel);
|
|
151
|
+
return { overlay, panel, content, nodes };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 搭假环境:document/window 够跑注入脚本,且能捕获 console.warn 与 pump 定时器。 */
|
|
155
|
+
function harness({ prefix = "Dws9Sa_", width = 390, height = 844, withNav = true, navTag = "nav", withDialog = true } = {}) {
|
|
156
|
+
const html = new El("html");
|
|
157
|
+
html._isRoot = true;
|
|
158
|
+
// HOSTISH() 读 documentElement.outerHTML 找官方特征
|
|
159
|
+
html.outerHTML = '<html><head><script>window.__ModuleLoader__ = { mode: "queue" }</script>'
|
|
160
|
+
+ "<title>DeepSeek Harness</title></head><body></body></html>";
|
|
161
|
+
const body = new El("body");
|
|
162
|
+
body.parent = html;
|
|
163
|
+
const box = withDialog ? settingsBox(prefix, { width, height, withNav, navTag }) : null;
|
|
164
|
+
if (box) { body.appendChild(box.overlay); }
|
|
165
|
+
// 记录"脚本自己新建"的节点(用来断言我们没有凭空造覆盖层/面板)
|
|
166
|
+
const created = [];
|
|
167
|
+
|
|
168
|
+
const doc = {
|
|
169
|
+
documentElement: html,
|
|
170
|
+
body,
|
|
171
|
+
readyState: "complete",
|
|
172
|
+
title: "DeepSeek Harness",
|
|
173
|
+
addEventListener() {},
|
|
174
|
+
removeEventListener() {},
|
|
175
|
+
createElement(tag) { const el = new El(tag); el.createdBy = "adapter"; created.push(el); return el; },
|
|
176
|
+
createTextNode: (t) => ({ nodeValue: t, createdBy: "adapter" }),
|
|
177
|
+
getElementById: () => null,
|
|
178
|
+
querySelector: (sel) => body.querySelector(sel),
|
|
179
|
+
querySelectorAll: (sel) => body.querySelectorAll(sel),
|
|
180
|
+
};
|
|
181
|
+
const warns = [];
|
|
182
|
+
const intervals = [];
|
|
183
|
+
const timers = new Set();
|
|
184
|
+
const win = {
|
|
185
|
+
innerWidth: width,
|
|
186
|
+
innerHeight: height,
|
|
187
|
+
devicePixelRatio: 1,
|
|
188
|
+
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]; } },
|
|
189
|
+
addEventListener() {},
|
|
190
|
+
removeEventListener() {},
|
|
191
|
+
getComputedStyle: () => ({ getPropertyValue: () => "" }),
|
|
192
|
+
requestAnimationFrame: (fn) => { fn(); return 1; },
|
|
193
|
+
MutationObserver: class { observe() {} disconnect() {} },
|
|
194
|
+
ResizeObserver: class { observe() {} disconnect() {} },
|
|
195
|
+
setTimeout: (fn, ms) => { const id = setTimeout(fn, ms); try { id.unref?.(); } catch { /* 忽略 */ } timers.add(id); return id; },
|
|
196
|
+
clearTimeout: (id) => { timers.delete(id); clearTimeout(id); },
|
|
197
|
+
setInterval: (fn) => { intervals.push(fn); return intervals.length; },
|
|
198
|
+
clearInterval() {},
|
|
199
|
+
fetch: undefined,
|
|
200
|
+
};
|
|
201
|
+
const loc = { href: "https://n.risegao.cn:13443/remote/dev-x/", host: "n.risegao.cn", pathname: "/remote/dev-x/", origin: "https://n.risegao.cn:13443" };
|
|
202
|
+
win.location = loc;
|
|
203
|
+
const consoleMock = { ...console, warn: (...a) => { warns.push(a.map(String).join(" ")); }, log: () => {}, error: () => {} };
|
|
204
|
+
win.console = consoleMock; // 适配层的诊断走 window.console(镜像页里就是页面控制台)
|
|
205
|
+
const ctx = vm.createContext({
|
|
206
|
+
window: win, document: doc, console: consoleMock,
|
|
207
|
+
setTimeout: win.setTimeout, clearTimeout: win.clearTimeout,
|
|
208
|
+
setInterval: win.setInterval, clearInterval: win.clearInterval,
|
|
209
|
+
MutationObserver: win.MutationObserver, ResizeObserver: win.ResizeObserver,
|
|
210
|
+
requestAnimationFrame: win.requestAnimationFrame,
|
|
211
|
+
navigator: { userAgent: "iPhone Safari", maxTouchPoints: 5, platform: "iPhone" },
|
|
212
|
+
location: loc, localStorage: win.localStorage,
|
|
213
|
+
URL, Date, Math, JSON, Object, Array, String, Number, RegExp, Boolean, Promise, Error, isNaN, parseInt, parseFloat,
|
|
214
|
+
HTMLElement: El,
|
|
215
|
+
});
|
|
216
|
+
ctx.globalThis = ctx;
|
|
217
|
+
ctx.self = win;
|
|
218
|
+
ctx.console = consoleMock;
|
|
219
|
+
return { ctx, doc, body, html, box, warns, intervals, created, win };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 跑注入脚本 + 主动推一拍 pump(脚本用 setInterval(pump, 1000) 兜底驱动)。 */
|
|
223
|
+
function runAdapter(env) {
|
|
224
|
+
vm.runInContext(ADAPTER_JS, env.ctx, { timeout: 5000 });
|
|
225
|
+
for (const fn of env.intervals) { try { fn(); } catch { /* 忽略 */ } }
|
|
226
|
+
return env;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const has = (el, cls) => el._cls.has(cls);
|
|
230
|
+
|
|
231
|
+
// ── ① CSS 契约 ───────────────────────────────────────────────────────────────
|
|
232
|
+
test("CSS:三层识别都在(已知前缀 0.1.5/0.2 + 结构稳定类 + :has 兜底)", () => {
|
|
233
|
+
for (const s of ['[class~="VOzbGW_overlay"]', '[class~="VOzbGW_panel"]', '[class~="VOzbGW_nav"]',
|
|
234
|
+
'[class~="Dws9Sa_overlay"]', '[class~="Dws9Sa_panel"]', '[class~="Dws9Sa_nav"]',
|
|
235
|
+
'[class~="Dws9Sa_navList"]', '[class~="Dws9Sa_navCell"]', '[class~="Dws9Sa_content"]',
|
|
236
|
+
'[class~="Dws9Sa_options"]', ".dsh-ma-settings-panel", ".dsh-ma-settings-nav",
|
|
237
|
+
".dsh-ma-settings-navlist", ".dsh-ma-settings-cell", ".dsh-ma-settings-content"]) {
|
|
238
|
+
assert.ok(NARROW_CSS.includes(s), `窄屏 CSS 应包含 ${s}`);
|
|
239
|
+
}
|
|
240
|
+
assert.match(NARROW_CSS, /div\[role="dialog"\]:has\(> nav\)/, ":has() 兜底应存在");
|
|
241
|
+
assert.match(NARROW_CSS, /div\[role="dialog"\] > nav\b/, "结构选择器(直接子 nav)应存在");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("CSS:窄屏 = 顶部横向 tab 行 + 下方内容满宽(nav 与 content 上下而非左右)", () => {
|
|
245
|
+
// 面板竖排
|
|
246
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_panel"\][^{}]*\{[^}]*flex-direction:\s*column\s*!important/,
|
|
247
|
+
"面板必须竖排(nav 在上、content 在下)");
|
|
248
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_panel"\][^{}]*\{[^}]*width:\s*100%\s*!important/);
|
|
249
|
+
// nav 横排 + 整行 + 可横滑
|
|
250
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_nav"\][^{}]*\{[^}]*flex-direction:\s*row\s*!important/,
|
|
251
|
+
"nav 必须横排");
|
|
252
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_nav"\][^{}]*\{[^}]*width:\s*100%\s*!important/);
|
|
253
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_navList"\][^{}]*\{[^}]*overflow-x:\s*auto/,
|
|
254
|
+
"tab 行要能横向滚动");
|
|
255
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_navCell"\][^{}]*\{[^}]*font-size:\s*13px\s*!important/,
|
|
256
|
+
"tab 字号 ≤13px");
|
|
257
|
+
// content 满宽 + 内部滚动
|
|
258
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_content"\][^{}]*\{[^}]*width:\s*100%\s*!important/);
|
|
259
|
+
assert.match(NARROW_CSS, /\[class~="Dws9Sa_options"\][^{}]*\{[^}]*overflow-y:\s*auto/);
|
|
260
|
+
// 安全区留白
|
|
261
|
+
assert.match(NARROW_CSS, /env\(safe-area-inset-bottom\)/);
|
|
262
|
+
assert.match(NARROW_CSS, /env\(safe-area-inset-top\)/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("CSS:弹层按手机尺寸收敛(100vw-24px / max-height / 内部滚动 / 圆角收敛)", () => {
|
|
266
|
+
// 官方 dialog/alertdialog/modal + 本项目 .dru-popup-card
|
|
267
|
+
assert.match(NARROW_CSS, /\[role="dialog"\][^{}]*\{[^}]*max-width:\s*calc\(100vw - 24px\)\s*!important/);
|
|
268
|
+
assert.match(NARROW_CSS, /\[role="alertdialog"\]/);
|
|
269
|
+
assert.match(NARROW_CSS, /\[aria-modal="true"\]/);
|
|
270
|
+
assert.match(NARROW_CSS, /\.dru-popup-card/);
|
|
271
|
+
assert.match(NARROW_CSS, /max-height:\s*calc\(100dvh - 24px\)\s*!important/);
|
|
272
|
+
assert.match(NARROW_CSS, /max-height:\s*calc\(100vh - 24px\)\s*!important/, "不认 dvh 的老浏览器要有 vh 兜底");
|
|
273
|
+
assert.match(NARROW_CSS, /border-radius:\s*12px\s*!important/);
|
|
274
|
+
assert.match(NARROW_CSS, /\.dru-popup\s*\{[^}]*padding:\s*12px\s*!important/);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("CSS:通用弹层规则必须排在设置弹层规则之前(否则设置面板被 24px 边距钉住)", () => {
|
|
278
|
+
const dialogAt = NARROW_CSS.indexOf('[role="dialog"]');
|
|
279
|
+
const panelAt = NARROW_CSS.indexOf('[class~="Dws9Sa_panel"]');
|
|
280
|
+
assert.ok(dialogAt >= 0 && panelAt >= 0);
|
|
281
|
+
assert.ok(dialogAt < panelAt, "通用弹层在前、设置面板在后(同特异性靠顺序决胜)");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("CSS:新增的弹层/设置规则全部落在 ≤820px 媒体块内(宽屏零影响)", () => {
|
|
285
|
+
for (const s of ['[role="dialog"]', ".dru-popup-card", '[class~="Dws9Sa_panel"]', ".dsh-ma-settings-panel"]) {
|
|
286
|
+
assert.ok(NARROW_CSS.includes(s), `${s} 必须在窄屏媒体块内`);
|
|
287
|
+
}
|
|
288
|
+
// 媒体块之外不得出现这些选择器(整体 <style> 里只能在媒体块出现一次)
|
|
289
|
+
const outside = ADAPTER_CSS.replace(NARROW_CSS, "");
|
|
290
|
+
for (const s of ['[class~="Dws9Sa_panel"]', ".dsh-ma-settings-panel", ".dru-popup-card"]) {
|
|
291
|
+
assert.ok(!outside.includes(s), `${s} 不得出现在窄屏媒体块之外`);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── ② 结构兜底(模拟"官方再次改名") ───────────────────────────────────────────
|
|
296
|
+
test("结构兜底:官方类名全部换成未知前缀,窄屏仍能认出设置面板并打稳定类", () => {
|
|
297
|
+
const env = runAdapter(harness({ prefix: "Zz9Qq_" }));
|
|
298
|
+
const { panel, content } = env.box;
|
|
299
|
+
assert.ok(has(panel, "dsh-ma-settings-panel"), "未知前缀下也应认出面板");
|
|
300
|
+
assert.ok(has(env.box.overlay, "dsh-ma-settings-overlay"), "overlay 也应打标");
|
|
301
|
+
assert.ok(has(panel.children[0], "dsh-ma-settings-nav"), "nav 应打标");
|
|
302
|
+
assert.ok(has(content, "dsh-ma-settings-content"), "content 应打标");
|
|
303
|
+
const navList = panel.children[0].children[1];
|
|
304
|
+
assert.ok(has(navList, "dsh-ma-settings-navlist"), "tab 列表应打标");
|
|
305
|
+
for (const cell of navList.children) assert.ok(has(cell, "dsh-ma-settings-cell"), "每个 tab 都应打标");
|
|
306
|
+
assert.ok(has(navList.children[1], "dsh-ma-settings-active"), "当前 tab(aria-current/_active)应打标");
|
|
307
|
+
assert.ok(env.warns.some((w) => w.includes("结构")), "前缀改名必须留下**可见**降级诊断");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("0.2 前缀(Dws9Sa_):稳定类照打,但不报「改名」告警", () => {
|
|
311
|
+
const env = runAdapter(harness({ prefix: "Dws9Sa_" }));
|
|
312
|
+
assert.ok(has(env.box.panel, "dsh-ma-settings-panel"));
|
|
313
|
+
assert.ok(has(env.box.content, "dsh-ma-settings-content"));
|
|
314
|
+
assert.equal(env.warns.filter((w) => w.includes("混淆前缀")).length, 0, "已知前缀不该报改名告警");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("0.1.5 前缀(VOzbGW_)同样打标(老版本不许回归)", () => {
|
|
318
|
+
const env = runAdapter(harness({ prefix: "VOzbGW_" }));
|
|
319
|
+
assert.ok(has(env.box.panel, "dsh-ma-settings-panel"));
|
|
320
|
+
assert.ok(has(env.box.panel.children[0], "dsh-ma-settings-nav"));
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("结构兜底:nav 不是 <nav> 标签(官方改了标签名)时认不出 → 只留一条可见诊断,不报错", () => {
|
|
324
|
+
const env = runAdapter(harness({ prefix: "Zz9Qq_", navTag: "div" }));
|
|
325
|
+
assert.ok(!has(env.box.panel, "dsh-ma-settings-panel"), "认不出就不许乱打类(宁可不做,不可做错)");
|
|
326
|
+
assert.ok(env.warns.some((w) => w.includes("未能识别为设置面板")), "应给出可见的降级诊断");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("假 DOM 也不会被官方改名吓住:识别是幂等的,重复 pump 不重复打标/不刷屏告警", () => {
|
|
330
|
+
const env = harness({ prefix: "Zz9Qq_" });
|
|
331
|
+
runAdapter(env);
|
|
332
|
+
for (const fn of env.intervals) { try { fn(); } catch { /* 忽略 */ } }
|
|
333
|
+
const renamed = env.warns.filter((w) => w.includes("结构"));
|
|
334
|
+
assert.equal(renamed.length, 1, "同类降级诊断只报一次(不刷屏)");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── ③ 不回归 ─────────────────────────────────────────────────────────────────
|
|
338
|
+
test("宽屏(>820)零动作:不打稳定类、不告警", () => {
|
|
339
|
+
const env = runAdapter(harness({ prefix: "Zz9Qq_", width: 1280, height: 900 }));
|
|
340
|
+
assert.ok(!has(env.box.panel, "dsh-ma-settings-panel"), "宽屏不得改动设置面板");
|
|
341
|
+
assert.equal(env.warns.length, 0, "宽屏不得有任何诊断输出");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("只给**已有**节点打类:不新建任何设置/覆盖层节点(护栏:适配层不得引入全屏覆盖层)", () => {
|
|
345
|
+
const env = runAdapter(harness({ prefix: "Zz9Qq_" }));
|
|
346
|
+
const tagged = [env.box.overlay, env.box.panel, ...env.box.nodes].filter((el) =>
|
|
347
|
+
[...el._cls].some((c) => c.startsWith("dsh-ma-settings")));
|
|
348
|
+
assert.ok(tagged.length > 0);
|
|
349
|
+
for (const el of tagged) assert.notEqual(el.createdBy, "adapter", "稳定类只能打在官方已有节点上");
|
|
350
|
+
// 新建节点里不得出现任何 fixed/absolute 覆盖层(只看适配层自己造的)
|
|
351
|
+
for (const el of env.created) assert.ok(!/overlay|scrim|mask/i.test(el.className), "不得新建覆盖层节点");
|
|
352
|
+
});
|
|
@@ -101,6 +101,18 @@ test("适配层文本不含口令类敏感标记(仅结构提示)", () => {
|
|
|
101
101
|
assert.ok(!/password|secret|token\s*=/i.test(mod.ADAPTER_TAG + "")); // ADAPTER_TAG 只是 id 常量
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
test("注入体里不得出现反引号(模板字符串会被提前闭合 —— 本仓 AGENTS.md 明列的坑)", () => {
|
|
105
|
+
const r = injectMobileAdapter(OFFICIAL_HTML);
|
|
106
|
+
const cssAt = r.html.indexOf('<style id="dsh-mobile-adapter-css"');
|
|
107
|
+
const jsAt = r.html.indexOf('<script id="dsh-mobile-adapter-js"');
|
|
108
|
+
const css = r.html.slice(r.html.indexOf(">", cssAt) + 1, r.html.indexOf("</style>", cssAt));
|
|
109
|
+
const js = r.html.slice(r.html.indexOf(">", jsAt) + 1, r.html.indexOf("</script>", jsAt));
|
|
110
|
+
assert.ok(css.length > 1000 && js.length > 1000, "应取到注入体");
|
|
111
|
+
// 注入体是 JS 模板字符串拼出来的:里面再出现反引号就会提前闭合,报错信息往往毫不相干。
|
|
112
|
+
assert.equal(css.includes("`"), false, "注入样式体内出现反引号(注释里也不行)");
|
|
113
|
+
assert.equal(js.includes("`"), false, "注入脚本体内出现反引号(注释里也不行)");
|
|
114
|
+
});
|
|
115
|
+
|
|
104
116
|
test("fix2:设置区用精确类+小字号,浮层不再持久 will-change,鲸鱼协同 JS 载荷存在", () => {
|
|
105
117
|
const r = injectMobileAdapter(OFFICIAL_HTML);
|
|
106
118
|
const css = r.html.slice(r.html.indexOf('<style id="dsh-mobile-adapter-css"'), r.html.indexOf("</style>"));
|
|
@@ -122,3 +134,26 @@ test("fix2:设置区用精确类+小字号,浮层不再持久 will-change,鲸鱼
|
|
|
122
134
|
assert.ok(css.indexOf("@media (max-width: 820px)") !== -1);
|
|
123
135
|
assert.match(js, /NARROW\s*=\s*\(\)\s*=>\s*window\.innerWidth\s*<=\s*820/);
|
|
124
136
|
});
|
|
137
|
+
|
|
138
|
+
test("2026-10-13:0.2 前缀(Dws9Sa_*)与结构兜底都在(官方改名不再整段静默失效)", () => {
|
|
139
|
+
const r = injectMobileAdapter(OFFICIAL_HTML);
|
|
140
|
+
const css = r.html.slice(r.html.indexOf('<style id="dsh-mobile-adapter-css"'), r.html.indexOf("</style>"));
|
|
141
|
+
const js = r.html.slice(r.html.indexOf('<script id="dsh-mobile-adapter-js"'));
|
|
142
|
+
// ① 0.2.0-rc.2 实测前缀:VOzbGW_* → Dws9Sa_*(结构/类名一一对应,只换哈希)
|
|
143
|
+
for (const k of ["overlay", "panel", "nav", "navTitle", "navList", "navCell", "content", "header", "close", "options"]) {
|
|
144
|
+
assert.match(css, new RegExp(`\\[class~="VOzbGW_${k}"\\]`), `0.1.5 前缀钩子 ${k} 必须保留`);
|
|
145
|
+
assert.match(css, new RegExp(`\\[class~="Dws9Sa_${k}"\\]`), `0.2 前缀钩子 ${k} 必须并挂`);
|
|
146
|
+
}
|
|
147
|
+
// ② 结构兜底:JS 打稳定类 + CSS 认稳定类 + :has() 纯 CSS 兜底
|
|
148
|
+
assert.match(js, /maTagSettingsDialog/);
|
|
149
|
+
assert.match(js, /dsh-ma-settings-panel/);
|
|
150
|
+
assert.match(css, /\.dsh-ma-settings-panel/);
|
|
151
|
+
assert.match(css, /div\[role="dialog"\]:has\(> nav\)/);
|
|
152
|
+
// ③ 可见的降级诊断(不得引入覆盖层):console.warn 走适配层既有通道
|
|
153
|
+
assert.match(js, /\[dsh-mobile-adapter\]/);
|
|
154
|
+
assert.match(js, /window\.console\.warn|console\.warn/);
|
|
155
|
+
// ④ 通用弹层按手机尺寸收敛
|
|
156
|
+
assert.match(css, /max-width:\s*calc\(100vw - 24px\)\s*!important/);
|
|
157
|
+
assert.match(css, /max-height:\s*calc\(100dvh - 24px\)\s*!important/);
|
|
158
|
+
assert.match(css, /\.dru-popup-card/);
|
|
159
|
+
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 上游(dsh web)地址发现契约测试。
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* 背景(两起真实故障,见 upstream-discovery.mjs 顶部注释):
|
|
6
6
|
* ① `dsh web --port 8090` / `--port 0`(系统分配端口)→ 写死 3080 的探测永远失败;
|
|
7
7
|
* ② **DSH Desktop**(Electron 壳 `dsh-plugin-desktop`)默认 43120、占用则 +1,插件 0.6.10
|
|
8
8
|
* 实测「面板永久停在 starting、设备从未登记」——watcher 探 3080 探不到就永不派生 bridge。
|
|
@@ -1768,7 +1768,7 @@ test("完成模板:头一行按 reason 区分,completed 不许读成失败", ()
|
|
|
1768
1768
|
});
|
|
1769
1769
|
|
|
1770
1770
|
test("★ 微信不渲染 Markdown:模型产出的表格/加粗/链接必须先转成纯文本", () => {
|
|
1771
|
-
//
|
|
1771
|
+
// 用户原话:「现在的表格形式看起来很奇怪,整个格式在微信上没有做过任何兼容,体验很差很差」。
|
|
1772
1772
|
// 根因:微信**不渲染** Markdown(腾讯自己的插件在出站路径主动剥离),所以模型的表格会以
|
|
1773
1773
|
// 字面竖线、加粗会以字面星号出现在气泡里。这儿锁住"发送前必须已转换"。
|
|
1774
1774
|
const md = [
|
|
@@ -1847,7 +1847,7 @@ test("完成模板:结论要**尽量完整**(未超上限不截断),超长才显
|
|
|
1847
1847
|
// 结论**独立成段**(不再挤在 "结论:" 前缀后面) —— 压成一行正是"密密麻麻"的来源
|
|
1848
1848
|
assert.match(withSummary.text, /—— 结论 ——\n结论正文/, "结论要单独成段、原样展示");
|
|
1849
1849
|
|
|
1850
|
-
// ★
|
|
1850
|
+
// ★ 用户约定(2026-09-22):「结论被大量截断了,加长一些,更完整地展示 —— 这可能是用户关注的内容」
|
|
1851
1851
|
// 旧上限 400 连一条正常的修复总结(实测 190+ 字)都快保不住。
|
|
1852
1852
|
const fits = "长".repeat(1200);
|
|
1853
1853
|
const kept = formatCompletion({ title: "T", reason: "completed", summary: fits });
|
|
@@ -1967,10 +1967,10 @@ test("危险工具:真阴性(普通的读/查/测试/构建**不是**破坏性)"
|
|
|
1967
1967
|
}
|
|
1968
1968
|
});
|
|
1969
1969
|
|
|
1970
|
-
test("危险工具:看不到内容时**不再**降级(
|
|
1970
|
+
test("危险工具:看不到内容时**不再**降级(用户拍板:DSH 自身有权限控制,别过严),且绝不抛", () => {
|
|
1971
1971
|
// ⚠️ 2026-09-22 行为变更:原先「能跑命令 + 完全看不到内容 → 判破坏性」。
|
|
1972
1972
|
// 那条规则会把**绝大多数正常 Bash 审批**降级成"只能回电脑确认"(DSH 的审批节点
|
|
1973
|
-
// 本来就常常没有命令原文)
|
|
1973
|
+
// 本来就常常没有命令原文),过严且伤体验。用户明确安全边界可以松一点。
|
|
1974
1974
|
// 现在**只看内容**:内容为空 → 不拦(保留一步回执)。
|
|
1975
1975
|
assert.equal(isDestructiveTool("Bash"), false, "看不到内容不再降级");
|
|
1976
1976
|
assert.equal(isDestructiveTool("Bash", {}), false);
|
|
@@ -2045,7 +2045,7 @@ test("入站分类:纯函数 + 全量(不改入参、怪输入也不抛)", () =>
|
|
|
2045
2045
|
});
|
|
2046
2046
|
|
|
2047
2047
|
test("★ 回归:换绑必须清掉旧 token 的冷却(否则用户刚绑上就一小时不能聊天)", async () => {
|
|
2048
|
-
// 实测 bug(
|
|
2048
|
+
// 实测 bug(用户撞到):旧 token 撞 -14 → 冷却 1 小时 → 用户解绑 + 重新绑定拿到**新** token,
|
|
2049
2049
|
// 却仍被旧 token 的冷却挡着 → "无法跟机器人聊天",而面板只说"通知会延迟"。
|
|
2050
2050
|
// 新 token 的会话与旧 token 的超时毫无关系,冷却必须清。
|
|
2051
2051
|
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "wc-cooldown-"));
|