@mrrisega/dsh-remote 0.4.9 → 0.6.0-beta.0
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/README.md +2 -2
- package/clients/dsh-remote/dsh-bridge.mjs +1 -1
- package/dsh-setup.mjs +78 -42
- package/package.json +4 -4
- package/packages/{dsh-remote-ui → dsh-remote-web}/README.md +8 -4
- package/packages/{dsh-remote-ui → dsh-remote-web}/cordis.patch.yml +3 -3
- package/packages/{dsh-remote-ui → dsh-remote-web}/lib/client.js +340 -51
- package/packages/{dsh-remote-ui → dsh-remote-web}/lib/index.js +187 -32
- package/packages/{dsh-remote-ui → dsh-remote-web}/package.json +5 -5
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +211 -0
- package/packages/{dsh-remote-ui → dsh-remote-web}/test/feedback-proxy.test.mjs +1 -1
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +253 -0
- package/packages/{dsh-remote-ui → dsh-remote-web}/test/settings-entry.test.mjs +33 -14
- /package/packages/{dsh-remote-ui → dsh-remote-web}/screenshots.json +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/account-info.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/account-switch.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/register-proxy.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/self-manage.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/sms-captcha-ui.test.mjs +0 -0
- /package/packages/{dsh-remote-ui → dsh-remote-web}/test/uninstall-runtime.test.mjs +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// 📱 远程访问 UI 回归(浏览器半):一次性访问密钥卡、状态行、已授权设备管理与取消配对二次确认、
|
|
2
|
+
// 升级/续费带登录态打开。复用 settings-entry 的 vm 沙箱 + mock react 风格,只驱动真实点击/请求路径。
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import vm from "node:vm";
|
|
7
|
+
|
|
8
|
+
const SOURCE = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
9
|
+
|
|
10
|
+
/** 深度遍历:兼容任意层级的数组子节点(card()/多级数组渲染会形成多层嵌套)。 */
|
|
11
|
+
function walk(node, visit) {
|
|
12
|
+
if (node == null) return;
|
|
13
|
+
if (Array.isArray(node)) { for (const item of node) walk(item, visit); return; }
|
|
14
|
+
if (typeof node !== "object") return;
|
|
15
|
+
visit(node);
|
|
16
|
+
for (const child of node.children || []) walk(child, visit);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function find(tree, predicate) {
|
|
20
|
+
let match;
|
|
21
|
+
walk(tree, (node) => { if (!match && predicate(node)) match = node; });
|
|
22
|
+
return match;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 文本子节点包含子串(用于动态拼接文案断言)。 */
|
|
26
|
+
function textHas(tree, substr) {
|
|
27
|
+
return !!find(tree, (node) => (node.children || []).some((c) => typeof c === "string" && c.includes(substr)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NOW = Date.now();
|
|
31
|
+
const KEY_URL = "https://app.test/a/K1";
|
|
32
|
+
const SESSIONS_1 = [
|
|
33
|
+
{ id: "ms_1", label: "iPhone 15", os: "iOS", browser: "Safari", created_at: NOW - 86400000, last_seen_at: NOW - 60000, revoked_at: null },
|
|
34
|
+
{ id: "ms_2", label: "Pixel 8", os: "Android", browser: "Chrome", created_at: NOW - 3600000, last_seen_at: null, revoked_at: NOW - 1800000 },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** vm 加载 client.js 并 apply;fetch 可注入企业端同源代理响应。 */
|
|
38
|
+
function loadPlugin(opts = {}) {
|
|
39
|
+
let moduleFactory;
|
|
40
|
+
const registered = new Map();
|
|
41
|
+
const injects = new Map();
|
|
42
|
+
const requests = [];
|
|
43
|
+
const opened = [];
|
|
44
|
+
const states = [];
|
|
45
|
+
let hook = 0;
|
|
46
|
+
let sessionsCalls = 0;
|
|
47
|
+
|
|
48
|
+
const react = {
|
|
49
|
+
createElement(type, props, ...children) { return { type, props: props || {}, children }; },
|
|
50
|
+
useState(initial) {
|
|
51
|
+
const index = hook++;
|
|
52
|
+
if (!(index in states)) states[index] = initial;
|
|
53
|
+
return [states[index], (value) => { states[index] = typeof value === "function" ? value(states[index]) : value; }];
|
|
54
|
+
},
|
|
55
|
+
useEffect() {},
|
|
56
|
+
useCallback(fn) { return fn; },
|
|
57
|
+
useSyncExternalStore(_subscribe, getSnapshot) { return getSnapshot(); },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const response = (status, body) => Promise.resolve({
|
|
61
|
+
ok: status >= 200 && status < 300,
|
|
62
|
+
status,
|
|
63
|
+
text: async () => JSON.stringify(body),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const allCreated = [];
|
|
67
|
+
const makeEl = (tag) => ({
|
|
68
|
+
tag,
|
|
69
|
+
children: [],
|
|
70
|
+
style: {},
|
|
71
|
+
className: "",
|
|
72
|
+
attributes: {},
|
|
73
|
+
setAttribute(k, v) { this.attributes[k] = v; },
|
|
74
|
+
appendChild(c) { this.children.push(c); },
|
|
75
|
+
});
|
|
76
|
+
const doc = {
|
|
77
|
+
createElement(tag) { const el = makeEl(tag); allCreated.push(el); return el; },
|
|
78
|
+
head: makeEl("head"),
|
|
79
|
+
body: makeEl("body"),
|
|
80
|
+
querySelector() { return null; },
|
|
81
|
+
querySelectorAll() { return []; },
|
|
82
|
+
};
|
|
83
|
+
class MutationObserverMock { constructor() {} observe() {} disconnect() {} }
|
|
84
|
+
|
|
85
|
+
const localStorage = {
|
|
86
|
+
_store: new Map(),
|
|
87
|
+
getItem(k) { return this._store.has(k) ? this._store.get(k) : null; },
|
|
88
|
+
setItem(k, v) { this._store.set(k, String(v)); },
|
|
89
|
+
removeItem(k) { this._store.delete(k); },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sandbox = {
|
|
93
|
+
window: {
|
|
94
|
+
__ModuleLoader__: { load(spec) { moduleFactory = spec.factory; } },
|
|
95
|
+
open(url) { opened.push(url); return null; },
|
|
96
|
+
},
|
|
97
|
+
document: doc,
|
|
98
|
+
localStorage,
|
|
99
|
+
MutationObserver: MutationObserverMock,
|
|
100
|
+
navigator: { clipboard: { writeText: async () => {} } },
|
|
101
|
+
fetch(path, options = {}) {
|
|
102
|
+
requests.push({ path, method: options.method || "GET", body: options.body ? JSON.parse(options.body) : null });
|
|
103
|
+
if (path === "/dsh-remote/status") {
|
|
104
|
+
return response(200, { ok: true, config: { phone: "13800000000", deviceId: "dev-x", mode: "saas" }, service: { running: true }, remoteUrl: "https://app.test/" });
|
|
105
|
+
}
|
|
106
|
+
if (path === "/dsh-remote/access-key") {
|
|
107
|
+
return response(200, { ok: true, url: KEY_URL, key: "K1", expires_at: NOW + 30 * 60 * 1000, ttl_ms: 1800000, qr_data_url: "data:image/png;base64,AAAA" });
|
|
108
|
+
}
|
|
109
|
+
if (path === "/dsh-remote/mobile-sessions") {
|
|
110
|
+
sessionsCalls++;
|
|
111
|
+
return response(200, { ok: true, sessions: sessionsCalls === 1 ? SESSIONS_1 : [] }); // 取消配对后的刷新 → 空列表
|
|
112
|
+
}
|
|
113
|
+
if (path === "/dsh-remote/mobile-sessions/revoke") {
|
|
114
|
+
return response(200, { ok: true });
|
|
115
|
+
}
|
|
116
|
+
if (path === "/dsh-remote/account") return response(200, { ok: true, account: { phone: "13800000000", plan: "free", plan_source: "plan", invite_code: "ABC12345" } });
|
|
117
|
+
return response(200, { ok: true });
|
|
118
|
+
},
|
|
119
|
+
setInterval() { return 1; },
|
|
120
|
+
clearInterval() {},
|
|
121
|
+
setTimeout() { return 1; },
|
|
122
|
+
Set,
|
|
123
|
+
Symbol,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
vm.runInNewContext(SOURCE, sandbox);
|
|
127
|
+
const plugin = moduleFactory((name) => {
|
|
128
|
+
assert.equal(name, "react");
|
|
129
|
+
return react;
|
|
130
|
+
});
|
|
131
|
+
plugin.apply({ slots: {
|
|
132
|
+
inject(name, cb) { injects.set(name, cb); cb(); },
|
|
133
|
+
register(meta, component) { registered.set(meta.id, component); return () => {}; },
|
|
134
|
+
} });
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
registered,
|
|
138
|
+
requests,
|
|
139
|
+
opened,
|
|
140
|
+
states,
|
|
141
|
+
render() { hook = 0; return registered.get("dsh-remote")({ close() {} }); },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const flush = () => new Promise((resolve) => setImmediate(resolve));
|
|
146
|
+
|
|
147
|
+
test("📱 远程访问卡:bridge 在线 → 绿点文案;登录态点「生成访问链接」→ url/复制/直接打开/二维码/倒计时齐备", async () => {
|
|
148
|
+
const plugin = loadPlugin();
|
|
149
|
+
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
150
|
+
let tree = plugin.render();
|
|
151
|
+
|
|
152
|
+
// 状态行:bridge 运行 → “已连接(可远程访问)”
|
|
153
|
+
assert.ok(textHas(tree, "已连接(可远程访问)"), "bridge 在线状态文案应显示");
|
|
154
|
+
assert.ok(find(tree, (n) => n.children?.includes("📱 远程访问")), "应渲染「📱 远程访问」卡标题");
|
|
155
|
+
|
|
156
|
+
// 未生成 key 的空态按钮 → 点击触发 /dsh-remote/access-key
|
|
157
|
+
const genBtn = find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("生成访问链接")));
|
|
158
|
+
assert.ok(genBtn, "应提供「生成访问链接」按钮");
|
|
159
|
+
genBtn.props.onClick();
|
|
160
|
+
await flush();
|
|
161
|
+
await flush();
|
|
162
|
+
|
|
163
|
+
tree = plugin.render();
|
|
164
|
+
assert.ok(plugin.requests.some((r) => r.path === "/dsh-remote/access-key" && r.method === "GET"), "应 GET /dsh-remote/access-key");
|
|
165
|
+
// 大字地址 + 复制 + 直接打开 + 刷新按钮
|
|
166
|
+
assert.ok(find(tree, (n) => n.children?.includes(KEY_URL)), "应展示一次性访问地址");
|
|
167
|
+
assert.ok(find(tree, (n) => n.children?.includes("复制")), "应提供「复制」按钮");
|
|
168
|
+
assert.ok(find(tree, (n) => n.children?.includes("直接打开")), "应提供「直接打开」按钮");
|
|
169
|
+
assert.ok(find(tree, (n) => n.children?.includes("刷新二维码/访问链接")), "应提供手动刷新按钮");
|
|
170
|
+
// 二维码 img 直取服务端 qr_data_url(客户端不自绘)
|
|
171
|
+
const qr = find(tree, (n) => n.props?.src === "data:image/png;base64,AAAA");
|
|
172
|
+
assert.ok(qr && qr.props?.alt === "远程访问二维码", "应渲染服务端返回的二维码 <img>");
|
|
173
|
+
// 到期倒计时/有效至文案
|
|
174
|
+
assert.ok(textHas(tree, "有效至") && textHas(tree, "剩余"), "应显示「有效至 HH:MM:SS / 剩余 xx:xx」倒计时");
|
|
175
|
+
assert.ok(textHas(tree, "访问一次后失效"), "应说明一次性/30 分钟语义");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("已授权设备:展开列表(label/os/browser/时间)→ 取消配对二次确认 → 成功提示并刷新", async () => {
|
|
179
|
+
const plugin = loadPlugin();
|
|
180
|
+
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
181
|
+
let tree = plugin.render();
|
|
182
|
+
|
|
183
|
+
// 展开:按钮「已授权设备 …」→ GET /dsh-remote/mobile-sessions
|
|
184
|
+
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
185
|
+
assert.ok(openBtn, "应提供「已授权设备 N」按钮");
|
|
186
|
+
openBtn.props.onClick();
|
|
187
|
+
await flush();
|
|
188
|
+
await flush();
|
|
189
|
+
|
|
190
|
+
tree = plugin.render();
|
|
191
|
+
assert.ok(plugin.requests.some((r) => r.path === "/dsh-remote/mobile-sessions"), "展开应拉取已授权设备列表");
|
|
192
|
+
assert.ok(textHas(tree, "iPhone 15"), "列表应显示设备 label");
|
|
193
|
+
assert.ok(textHas(tree, "iOS · Safari"), "列表应显示 os/browser");
|
|
194
|
+
assert.ok(textHas(tree, "已取消配对"), "revoked 设备应带「已取消配对」标记");
|
|
195
|
+
assert.ok(textHas(tree, "首次配对"), "应显示配对时间");
|
|
196
|
+
|
|
197
|
+
// 取消配对:第一次点击进入确认态(不请求)
|
|
198
|
+
const revokeBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("取消配对")));
|
|
199
|
+
assert.ok(revokeBtn, "行尾应有「取消配对」按钮");
|
|
200
|
+
revokeBtn.props.onClick();
|
|
201
|
+
tree = plugin.render();
|
|
202
|
+
assert.ok(textHas(tree, "再点一次确认取消配对"), "二次确认文案应出现");
|
|
203
|
+
assert.ok(!plugin.requests.some((r) => r.path === "/dsh-remote/mobile-sessions/revoke"), "首次点击不应发请求");
|
|
204
|
+
|
|
205
|
+
// 第二次点击 → POST revoke {id} → 提示 + 刷新列表(空)
|
|
206
|
+
find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("再点一次确认取消配对"))).props.onClick();
|
|
207
|
+
await flush();
|
|
208
|
+
await flush();
|
|
209
|
+
await flush();
|
|
210
|
+
|
|
211
|
+
const rev = plugin.requests.find((r) => r.path === "/dsh-remote/mobile-sessions/revoke");
|
|
212
|
+
assert.ok(rev, "确认后应 POST /dsh-remote/mobile-sessions/revoke");
|
|
213
|
+
assert.deepEqual(rev.body, { id: "ms_1" });
|
|
214
|
+
tree = plugin.render();
|
|
215
|
+
assert.ok(textHas(tree, "已取消,对方需重新扫码/登录"), "应提示已取消,对方需重新扫码/登录");
|
|
216
|
+
assert.ok(textHas(tree, "暂无已授权设备(手机扫码后出现)"), "刷新后空态文案应出现");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("升级/续费按钮:点击 → GET /dsh-remote/access-key → window.open(url)(带登录态打开)", async () => {
|
|
220
|
+
const plugin = loadPlugin();
|
|
221
|
+
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
222
|
+
let tree = plugin.render();
|
|
223
|
+
|
|
224
|
+
const upgrade = find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("升级 PRO")));
|
|
225
|
+
assert.ok(upgrade, "免费用户应显示「🚀 升级 PRO」按钮");
|
|
226
|
+
upgrade.props.onClick();
|
|
227
|
+
await flush();
|
|
228
|
+
await flush();
|
|
229
|
+
|
|
230
|
+
assert.ok(plugin.requests.some((r) => r.path === "/dsh-remote/access-key"), "升级/续费应先取一次性访问 url");
|
|
231
|
+
assert.ok(plugin.opened.includes(KEY_URL), "应以 window.open 打开一次性访问地址(带登录态)");
|
|
232
|
+
assert.ok(textHas(plugin.render(), "带登录态"), "应出现「带登录态」说明文案");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("二维码缺失容错 + 未登录引导文案(源码级约束)", () => {
|
|
236
|
+
// 契约容错:qr_data_url 缺省 → 展示链接/复制/打开并说明“二维码暂不可用”,不报错
|
|
237
|
+
assert.match(SOURCE, /二维码生成中 \/ 暂不可用/);
|
|
238
|
+
assert.match(SOURCE, /qr_data_url/);
|
|
239
|
+
assert.match(SOURCE, /window\.open/);
|
|
240
|
+
// 自动刷新/状态轮询定时器与清理
|
|
241
|
+
assert.match(SOURCE, /KEY_AUTO_REFRESH_MS = 25000/);
|
|
242
|
+
assert.match(SOURCE, /STATUS_POLL_MS = 5000/);
|
|
243
|
+
assert.match(SOURCE, /clearInterval\(rotateIv\)/);
|
|
244
|
+
assert.match(SOURCE, /clearInterval\(pollIv\)/);
|
|
245
|
+
// 文案与行为关键词
|
|
246
|
+
assert.match(SOURCE, /已取消,对方需重新扫码\/登录/);
|
|
247
|
+
assert.match(SOURCE, /再点一次确认取消配对/);
|
|
248
|
+
assert.match(SOURCE, /暂无已授权设备(手机扫码后出现)/);
|
|
249
|
+
assert.match(SOURCE, /等待设备连接/);
|
|
250
|
+
assert.match(SOURCE, /已连接(可远程访问)/);
|
|
251
|
+
assert.match(SOURCE, /带登录态/);
|
|
252
|
+
assert.match(SOURCE, /通过手机或另一台电脑远程使用同一份 dsh web/);
|
|
253
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-remote-ui
|
|
1
|
+
// dsh-remote-web 浏览器半回归(2026-09 前名 dsh-remote-ui):面板入口迁移进「设置」页 settings.section 官方扩展点。
|
|
2
2
|
// 覆盖:栏目注册(id/order/label)、侧边栏入口与浮动面板移除、账号区无「切换账号」、
|
|
3
3
|
// 「关于 dsh-remote」说明卡片、退出登录/切换连接账号清理反馈线程凭据、首次安装红点引导。
|
|
4
4
|
import assert from "node:assert/strict";
|
|
@@ -23,6 +23,11 @@ function find(tree, predicate) {
|
|
|
23
23
|
return match;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** 任意文本子节点包含子串(用于动态拼接文案,如「已授权设备 N」)。 */
|
|
27
|
+
function textHas(tree, substr) {
|
|
28
|
+
return !!find(tree, (node) => (node.children || []).some((c) => typeof c === "string" && c.includes(substr)));
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
/** 极简 DOM 元素假件(够 client.js 的红点注入用)。 */
|
|
27
32
|
function makeEl(tag) {
|
|
28
33
|
return {
|
|
@@ -158,7 +163,7 @@ test("入口迁移:注册 settings.section 栏目(id/order/label),移除
|
|
|
158
163
|
// 不再注入侧边栏入口槽
|
|
159
164
|
assert.equal(plugin.injects.has("sidebar.footer.action"), false, "侧边栏入口槽不应再注入");
|
|
160
165
|
|
|
161
|
-
// settings.section 官方扩展点:id=dsh-remote、order=30(> Agent 预设 20,位于其下方)、label
|
|
166
|
+
// settings.section 官方扩展点:id=dsh-remote、order=30(> Agent 预设 20,位于其下方)、label=📱 远程访问
|
|
162
167
|
assert.ok(plugin.injects.has("settings.section"), "应注入 settings.section 扩展点");
|
|
163
168
|
const disposeSection = plugin.injects.get("settings.section")();
|
|
164
169
|
assert.equal(typeof disposeSection, "function", "register 应返回 disposer");
|
|
@@ -168,8 +173,9 @@ test("入口迁移:注册 settings.section 栏目(id/order/label),移除
|
|
|
168
173
|
assert.equal(meta.order, 30);
|
|
169
174
|
assert.equal(typeof meta.label, "function");
|
|
170
175
|
const label = meta.label();
|
|
171
|
-
assert.ok(String(label).includes("
|
|
172
|
-
assert.ok(String(label).includes("
|
|
176
|
+
assert.ok(String(label).includes("远程访问"), `栏目名应含「远程访问」,实际: ${label}`);
|
|
177
|
+
assert.ok(String(label).includes("📱"), "栏目名应带 📱 远程访问图标(手机访问语义)");
|
|
178
|
+
assert.ok(!String(label).includes("远程控制"), "栏目名不应再残留旧称「远程控制」");
|
|
173
179
|
|
|
174
180
|
// shell.overlay 仅保留满意度弹窗,浮动面板已移除
|
|
175
181
|
assert.ok(plugin.injects.has("shell.overlay"));
|
|
@@ -178,22 +184,26 @@ test("入口迁移:注册 settings.section 栏目(id/order/label),移除
|
|
|
178
184
|
assert.equal(plugin.registered.has("dsh-remote-panel"), false, "浮动配置面板不应再注册");
|
|
179
185
|
});
|
|
180
186
|
|
|
181
|
-
test("
|
|
187
|
+
test("登录态账号区:无「切换账号」,有「退出登录」,头部/关于卡文案为「远程访问」", () => {
|
|
182
188
|
const plugin = loadPlugin();
|
|
183
189
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-test" }, service: { running: false } };
|
|
184
190
|
let tree = plugin.renderSection();
|
|
185
191
|
|
|
192
|
+
// 栏目头部:标题「远程访问」+ 副文案(人在哪都能用,免公网 IP)
|
|
193
|
+
assert.ok(find(tree, (n) => n.children?.includes("远程访问")), "头部标题应为「远程访问」");
|
|
194
|
+
assert.ok(find(tree, (n) => n.children?.includes("通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)")), "应渲染新副文案(人在哪都能用,免公网 IP)");
|
|
195
|
+
|
|
186
196
|
// 账号区按钮
|
|
187
197
|
assert.ok(find(tree, (n) => n.children?.includes("退出登录")), "应保留「退出登录」");
|
|
188
198
|
assert.ok(!find(tree, (n) => n.children?.includes("切换账号")), "「切换账号」按钮应移除");
|
|
189
199
|
|
|
190
|
-
// 关于 dsh-remote 说明卡片(面板底部,
|
|
200
|
+
// 关于 dsh-remote 说明卡片(面板底部,v0.5+ 价值要点)
|
|
191
201
|
assert.ok(find(tree, (n) => n.children?.includes("📖 关于 dsh-remote")), "应渲染「关于 dsh-remote」卡片标题");
|
|
192
202
|
const points = [
|
|
193
|
-
"
|
|
194
|
-
"
|
|
195
|
-
"
|
|
196
|
-
"
|
|
203
|
+
"📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。",
|
|
204
|
+
"🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。",
|
|
205
|
+
"🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。",
|
|
206
|
+
"🛡 端到端流量保护:可选对通道做端到端加密保护,传输全程不暴露本机公网 IP(详见项目 README「安全」说明)。",
|
|
197
207
|
];
|
|
198
208
|
for (const p of points) {
|
|
199
209
|
assert.ok(find(tree, (n) => n.children?.includes(p)), `说明卡片应含要点: ${p.slice(0, 12)}…`);
|
|
@@ -202,6 +212,10 @@ test("登录态账号区:无「切换账号」,有「退出登录」,关
|
|
|
202
212
|
// 面板主体仍在(连接模式 tab 不受影响)
|
|
203
213
|
assert.ok(find(tree, (n) => n.children?.includes("☁️ 云端服务")), "云端服务 tab 应保留");
|
|
204
214
|
assert.ok(find(tree, (n) => n.children?.includes("🖥 自建服务")), "自建服务 tab 应保留");
|
|
215
|
+
|
|
216
|
+
// 📱 远程访问卡 / 📲 已授权设备卡在云端视图中渲染(bridge 未运行 → “等待设备连接”)
|
|
217
|
+
assert.ok(find(tree, (n) => n.children?.includes("等待设备连接")), "无 bridge 时应显示「等待设备连接」状态行");
|
|
218
|
+
assert.ok(textHas(tree, "已授权设备"), "应渲染「已授权设备」管理按钮");
|
|
205
219
|
});
|
|
206
220
|
|
|
207
221
|
test("退出登录清除用户反馈线程凭据(localStorage dsh-feedback-threads)", async () => {
|
|
@@ -223,7 +237,7 @@ test("首次安装红点:设置页导航栏目出现后注入,点击后写 k
|
|
|
223
237
|
const navCells = [];
|
|
224
238
|
const navCell = makeEl("button");
|
|
225
239
|
navCell.className = "VOzbGW_navCell"; // shell hashed 类名(含 navCell 子串)
|
|
226
|
-
navCell.textContent = "
|
|
240
|
+
navCell.textContent = "📱 远程访问";
|
|
227
241
|
const plugin = loadPlugin({ navCells });
|
|
228
242
|
|
|
229
243
|
// apply 时设置页未打开:观察器就位、无红点
|
|
@@ -255,7 +269,7 @@ test("红点已看过(localStorage 有 key)时不注入,重启 DSH Web 不
|
|
|
255
269
|
const navCells = [];
|
|
256
270
|
const navCell = makeEl("button");
|
|
257
271
|
navCell.className = "VOzbGW_navCell";
|
|
258
|
-
navCell.textContent = "
|
|
272
|
+
navCell.textContent = "📱 远程访问";
|
|
259
273
|
navCells.push(navCell);
|
|
260
274
|
// 预置 seen key(模拟“首次点击后重启”)
|
|
261
275
|
const plugin = loadPlugin({ navCells, localStorageSeed: { "dsh-remote-seen-dot": "1" } });
|
|
@@ -263,17 +277,22 @@ test("红点已看过(localStorage 有 key)时不注入,重启 DSH Web 不
|
|
|
263
277
|
assert.equal(navCell.children.length, 0, "已看过时不应再注入红点");
|
|
264
278
|
});
|
|
265
279
|
|
|
266
|
-
test("
|
|
280
|
+
test("源码约束:无侧边栏入口/浮动面板/切换账号;命名统一为「远程访问」;新增访问密钥/设备路由", () => {
|
|
267
281
|
// 只断言“代码形态”不存在(注释里允许出现说明文字)
|
|
268
282
|
assert.doesNotMatch(SOURCE, /slots\.inject\("sidebar\.footer\.action"/);
|
|
269
283
|
assert.doesNotMatch(SOURCE, /dru-backdrop\{/);
|
|
270
284
|
assert.doesNotMatch(SOURCE, /切换账号/);
|
|
271
285
|
assert.doesNotMatch(SOURCE, /dsh-remote-panel/);
|
|
286
|
+
assert.doesNotMatch(SOURCE, /远程控制/); // 旧称呼「远程控制」不再出现(含 label/头部/说明)
|
|
272
287
|
assert.match(SOURCE, /settings\.section/);
|
|
273
288
|
assert.match(SOURCE, /dsh-remote-seen-dot/);
|
|
274
289
|
assert.match(SOURCE, /fbClearThreads/);
|
|
275
290
|
assert.match(SOURCE, /关于 dsh-remote/);
|
|
276
|
-
assert.match(SOURCE,
|
|
291
|
+
assert.match(SOURCE, /免公网 IP/); // 关于卡 v0.5+ 价值要点
|
|
292
|
+
assert.match(SOURCE, /端到端/);
|
|
293
|
+
assert.match(SOURCE, /已授权设备/);
|
|
294
|
+
assert.match(SOURCE, /dsh-remote\/access-key/);
|
|
295
|
+
assert.match(SOURCE, /dsh-remote\/mobile-sessions/);
|
|
277
296
|
assert.match(SOURCE, /order: 30/);
|
|
278
297
|
// 清理时机:退出登录成功回调内、登录账号变化时、切换自建服务时
|
|
279
298
|
assert.match(SOURCE, /post\("\/dsh-remote\/logout"\)\.then\(function \(body\) \{[\s\S]*?fbClearThreads\(\);/);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|