@mrrisega/dsh-remote 0.6.0-beta.11 → 0.6.0-beta.2
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 +0 -30
- package/clients/dsh-remote/dsh-bridge.mjs +5 -305
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +178 -467
- package/packages/dsh-remote-web/lib/index.js +4 -115
- package/packages/dsh-remote-web/test/access-key-sessions.test.mjs +11 -81
- package/packages/dsh-remote-web/test/account-switch.test.mjs +1 -1
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -174
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +5 -55
- package/packages/dsh-remote-web/test/uninstall-runtime.test.mjs +2 -2
- package/clients/dsh-remote/e2ee-client.mjs +0 -711
- package/clients/dsh-remote/e2ee-shim-script.js +0 -697
- package/clients/dsh-remote/e2ee-shim.mjs +0 -158
- package/clients/dsh-remote/mobile-adapter.mjs +0 -541
- package/clients/dsh-remote/test/bridge-html-inject.test.mjs +0 -110
- package/clients/dsh-remote/test/e2ee-bridge.test.mjs +0 -630
- package/clients/dsh-remote/test/e2ee-client.test.mjs +0 -365
- package/clients/dsh-remote/test/e2ee-shim.test.mjs +0 -507
- package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +0 -200
- package/clients/dsh-remote/test/lib-e2ee-shim.mjs +0 -59
- package/clients/dsh-remote/test/lib-native-e2ee.mjs +0 -92
- package/clients/dsh-remote/test/mobile-adapter.test.mjs +0 -124
- package/packages/dsh-remote-web/test/e2ee-state.test.mjs +0 -141
- package/packages/dsh-remote-web/test/feedback-account-mine.test.mjs +0 -70
- package/packages/dsh-remote-web/test/password-reset.test.mjs +0 -429
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
// - 读写配置目录下 .dsh-config.json(0600)
|
|
5
5
|
// - 查询/启停 bridge(launchctl,plist 缺失时自动生成,逻辑与 dsh-setup.mjs 一致)
|
|
6
6
|
// - 代理 relay API(captcha / register / login / public-config),直连、不走系统代理;
|
|
7
|
-
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke
|
|
8
|
-
// DELETE …/:id、POST …/purge,Bearer)供面板「📱 远程访问」卡使用
|
|
7
|
+
// 另代理企业端一次性访问密钥 / 授权设备(/api/auth-key、/api/mobile-sessions、…/revoke,Bearer)供面板「📱 远程访问」卡使用
|
|
9
8
|
// - 自管理 self*(版本可见 / 新版检测 / 一键在线更新 / 彻底卸载):插件市场没有更新卸载按钮,
|
|
10
9
|
// 面板内即官方管理入口;更新=后台 npx 按 dist-tag(默认 latest,DSH_UPDATE_TAG 可切 beta/alpha)(幂等补齐运行环境并重启 bridge);
|
|
11
10
|
// 彻底卸载=profile 插件清理(uninstallSelf)+ 运行时清理(uninstallRuntime:停 bridge 自启动 /
|
|
@@ -763,13 +762,13 @@ async function proxyCreateAccessKey(relayDir, res) {
|
|
|
763
762
|
}
|
|
764
763
|
|
|
765
764
|
/**
|
|
766
|
-
* GET /dsh-remote/mobile-sessions → 已授权设备列表(企业端
|
|
765
|
+
* GET /dsh-remote/mobile-sessions → 已授权设备列表(企业端 POST /api/mobile-sessions,Bearer)。
|
|
767
766
|
*/
|
|
768
767
|
async function proxyMobileSessions(relayDir, res) {
|
|
769
768
|
const token = await relayToken(relayDir).catch(() => "");
|
|
770
769
|
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
771
770
|
const r = await relayFetch(relayDir, "/api/mobile-sessions", {
|
|
772
|
-
method: "
|
|
771
|
+
method: "POST",
|
|
773
772
|
headers: { authorization: `Bearer ${token}` },
|
|
774
773
|
});
|
|
775
774
|
const d = flattenRelayBody(r);
|
|
@@ -803,78 +802,8 @@ async function proxyRevokeMobileSession(relayDir, req, res) {
|
|
|
803
802
|
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
804
803
|
}
|
|
805
804
|
|
|
806
|
-
/**
|
|
807
|
-
* DELETE /dsh-remote/mobile-sessions/delete(body {id})→ 删除本机该设备的授权记录
|
|
808
|
-
* (企业端 DELETE /api/mobile-sessions/:id,Bearer:本人整行删除并拉黑 jti)。
|
|
809
|
-
*/
|
|
810
|
-
async function proxyDeleteMobileSession(relayDir, req, res) {
|
|
811
|
-
const body = await readJsonBody(req);
|
|
812
|
-
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
813
|
-
const id = String(body.id ?? "").trim();
|
|
814
|
-
if (!id) return sendJson(res, 400, { ok: false, error: "缺少参数 id(会话 ID)" });
|
|
815
|
-
const token = await relayToken(relayDir).catch(() => "");
|
|
816
|
-
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
817
|
-
const r = await relayFetch(relayDir, `/api/mobile-sessions/${encodeURIComponent(id)}`, {
|
|
818
|
-
method: "DELETE",
|
|
819
|
-
headers: { authorization: `Bearer ${token}` },
|
|
820
|
-
});
|
|
821
|
-
const d = flattenRelayBody(r);
|
|
822
|
-
const ok = !!(r.ok && d.ok !== false);
|
|
823
|
-
if (!ok) {
|
|
824
|
-
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
825
|
-
}
|
|
826
|
-
return sendJson(res, 200, { ok: true, relayStatus: (r && r.status) || 200 });
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
/**
|
|
830
|
-
* POST /dsh-remote/mobile-sessions/purge → 清理本人所有已解绑(revoked)记录
|
|
831
|
-
* (企业端 POST /api/mobile-sessions/purge,Bearer)。
|
|
832
|
-
*/
|
|
833
|
-
async function proxyPurgeMobileSessions(relayDir, res) {
|
|
834
|
-
const token = await relayToken(relayDir).catch(() => "");
|
|
835
|
-
if (!token) return sendJson(res, 401, notLoggedInJson());
|
|
836
|
-
const r = await relayFetch(relayDir, "/api/mobile-sessions/purge", {
|
|
837
|
-
method: "POST",
|
|
838
|
-
headers: { authorization: `Bearer ${token}` },
|
|
839
|
-
});
|
|
840
|
-
const d = flattenRelayBody(r);
|
|
841
|
-
const ok = !!(r.ok && d.ok !== false);
|
|
842
|
-
if (!ok) {
|
|
843
|
-
return sendJson(res, (r && r.status) || 502, { ok: false, error: relayErrorMessage(r), relayStatus: (r && r.status) || 0 });
|
|
844
|
-
}
|
|
845
|
-
const removed = Number.isInteger(d.removed) ? d.removed : null;
|
|
846
|
-
const payload = { ok: true, relayStatus: (r && r.status) || 200 };
|
|
847
|
-
if (removed !== null) payload.removed = removed;
|
|
848
|
-
return sendJson(res, 200, payload);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
805
|
// ---------- 综合状态 ----------
|
|
852
806
|
|
|
853
|
-
/**
|
|
854
|
-
* 读取 bridge 写入的 E2EE 开关状态(<relayDir>/.e2ee-state.json,写入方见
|
|
855
|
-
* clients/dsh-remote/e2ee-client.mjs 的 writeE2eeStateFile:{enabled, reason, profile, epoch, caps, at})。
|
|
856
|
-
* 归一化后只透出固定展示字段(enabled/reason/profile/epoch/caps),at 等调试字段不外泄;
|
|
857
|
-
* 文件缺失/损坏一律视为「未启用(明文回退)」——面板据此显示普通安全连接。
|
|
858
|
-
*/
|
|
859
|
-
export function readE2eeStateFile(relayDir) {
|
|
860
|
-
const none = () => ({ enabled: false, reason: "no_state_file", profile: "", epoch: 0, caps: [] });
|
|
861
|
-
try {
|
|
862
|
-
const f = join(relayDir, ".e2ee-state.json");
|
|
863
|
-
if (!existsSync(f)) return none();
|
|
864
|
-
const s = JSON.parse(readFileSync(f, "utf8"));
|
|
865
|
-
if (!s || typeof s !== "object") return none();
|
|
866
|
-
return {
|
|
867
|
-
enabled: s.enabled === true,
|
|
868
|
-
reason: typeof s.reason === "string" && s.reason ? s.reason : "",
|
|
869
|
-
profile: typeof s.profile === "string" ? s.profile : "",
|
|
870
|
-
epoch: Number.isInteger(s.epoch) && s.epoch >= 0 ? s.epoch : 0,
|
|
871
|
-
caps: Array.isArray(s.caps) ? s.caps.filter((c) => typeof c === "string") : [],
|
|
872
|
-
};
|
|
873
|
-
} catch {
|
|
874
|
-
return none();
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
807
|
async function composeStatus(relayDir) {
|
|
879
808
|
const cfg = loadConfig(relayDir);
|
|
880
809
|
const launchd = launchdStatus();
|
|
@@ -909,9 +838,6 @@ async function composeStatus(relayDir) {
|
|
|
909
838
|
manual,
|
|
910
839
|
running: launchd.running || manual.bridge.length > 0,
|
|
911
840
|
bindError,
|
|
912
|
-
// E2EE 开关状态(bridge 写 .e2ee-state.json;文件缺失 = 未启用明文):
|
|
913
|
-
// {enabled, reason, profile, epoch, caps},供面板「📱 远程访问」卡展示加密状态。
|
|
914
|
-
e2ee: readE2eeStateFile(relayDir),
|
|
915
841
|
},
|
|
916
842
|
host: hostname(),
|
|
917
843
|
};
|
|
@@ -1020,7 +946,7 @@ const PLUGIN_ID = "dsh-remote-web";
|
|
|
1020
946
|
const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
|
|
1021
947
|
const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
|
|
1022
948
|
/** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
|
|
1023
|
-
const PLUGIN_VERSION = "0.6.0-beta.
|
|
949
|
+
const PLUGIN_VERSION = "0.6.0-beta.2";
|
|
1024
950
|
const UPDATE_LOG = ".dsh-update.log";
|
|
1025
951
|
const UPDATE_MARKER = ".dsh-update-running";
|
|
1026
952
|
|
|
@@ -1296,22 +1222,6 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1296
1222
|
await proxyRevokeMobileSession(relayDir, req, res);
|
|
1297
1223
|
},
|
|
1298
1224
|
},
|
|
1299
|
-
// 删除已授权设备记录(整行删除并拉黑 jti;企业端 DELETE /api/mobile-sessions/:id,Bearer)
|
|
1300
|
-
{
|
|
1301
|
-
method: "DELETE",
|
|
1302
|
-
path: "/dsh-remote/mobile-sessions/delete",
|
|
1303
|
-
handler: async (req, res) => {
|
|
1304
|
-
await proxyDeleteMobileSession(relayDir, req, res);
|
|
1305
|
-
},
|
|
1306
|
-
},
|
|
1307
|
-
// 清理本人全部已解绑(revoked)记录(企业端 POST /api/mobile-sessions/purge,Bearer)
|
|
1308
|
-
{
|
|
1309
|
-
method: "POST",
|
|
1310
|
-
path: "/dsh-remote/mobile-sessions/purge",
|
|
1311
|
-
handler: async (_req, res) => {
|
|
1312
|
-
await proxyPurgeMobileSessions(relayDir, res);
|
|
1313
|
-
},
|
|
1314
|
-
},
|
|
1315
1225
|
{
|
|
1316
1226
|
method: "POST",
|
|
1317
1227
|
path: "/dsh-remote/config",
|
|
@@ -1391,27 +1301,6 @@ function registerRoutes(ctx, relayDir) {
|
|
|
1391
1301
|
sendJson(res, r.status || 502, { ok: r.ok, status: r.status, body: r.body });
|
|
1392
1302
|
},
|
|
1393
1303
|
},
|
|
1394
|
-
{
|
|
1395
|
-
method: "POST",
|
|
1396
|
-
path: "/dsh-remote/password/reset",
|
|
1397
|
-
handler: async (req, res) => {
|
|
1398
|
-
// 公开 POST /api/password/reset(无需 Bearer):短信验证码重置密码。
|
|
1399
|
-
// 成功 → 企业端使该账号全部授权设备/会话失效(含 E2EE 派生口令)。
|
|
1400
|
-
const body = await readJsonBody(req);
|
|
1401
|
-
if (body.__parseError) return sendJson(res, 400, { ok: false, error: "JSON 解析失败" });
|
|
1402
|
-
const phone = String(body.phone ?? "").trim();
|
|
1403
|
-
const smsCode = String(body.sms_code ?? "").trim();
|
|
1404
|
-
const newPassword = String(body.new_password ?? body.password ?? "");
|
|
1405
|
-
if (!phone || !smsCode || !newPassword) return sendJson(res, 400, { ok: false, error: "手机号、短信验证码与新密码必填" });
|
|
1406
|
-
const r = await relayFetch(relayDir, "/api/password/reset", {
|
|
1407
|
-
method: "POST",
|
|
1408
|
-
headers: { "content-type": "application/json" },
|
|
1409
|
-
body: JSON.stringify({ phone, sms_code: smsCode, new_password: newPassword }),
|
|
1410
|
-
});
|
|
1411
|
-
// 透传企业端 ok/error(成功 200 {ok:true};失败保留 status 与错误体)
|
|
1412
|
-
sendJson(res, r.status || 502, { ok: r.ok, status: r.status, body: r.body });
|
|
1413
|
-
},
|
|
1414
|
-
},
|
|
1415
1304
|
{
|
|
1416
1305
|
method: "GET",
|
|
1417
1306
|
path: "/dsh-remote/captcha",
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// 插件 node 半新增代理回归:/dsh-remote/access-key | mobile-sessions | mobile-sessions/revoke
|
|
2
|
-
//
|
|
3
|
-
// (企业端 E1 契约:POST /api/auth-key、GET /api/mobile-sessions、POST /api/mobile-sessions/:id/revoke、
|
|
4
|
-
// DELETE /api/mobile-sessions/:id、POST /api/mobile-sessions/purge,Bearer device-login JWT)
|
|
2
|
+
// (企业端 E1 契约:POST /api/auth-key、POST /api/mobile-sessions、POST /api/mobile-sessions/:id/revoke,Bearer device-login JWT)
|
|
5
3
|
// 覆盖:Bearer 透传、字段扁平化、未登录 401、qr 缺失容错、上游错误透传。
|
|
6
4
|
import assert from "node:assert/strict";
|
|
7
5
|
import http from "node:http";
|
|
@@ -32,7 +30,7 @@ function startFakeRelay(opts = {}) {
|
|
|
32
30
|
if (!opts.noQr) body.qr_data_url = QR;
|
|
33
31
|
return send(200, body);
|
|
34
32
|
}
|
|
35
|
-
if (
|
|
33
|
+
if (req.method === "POST" && url.pathname === "/api/mobile-sessions") {
|
|
36
34
|
return send(200, {
|
|
37
35
|
ok: true,
|
|
38
36
|
sessions: [
|
|
@@ -43,9 +41,6 @@ function startFakeRelay(opts = {}) {
|
|
|
43
41
|
}
|
|
44
42
|
if (req.method === "POST" && url.pathname === "/api/mobile-sessions/ms_1/revoke") return send(200, { ok: true });
|
|
45
43
|
if (req.method === "POST" && url.pathname === "/api/mobile-sessions/ghost/revoke") return send(404, { error: { message: "not_found" } });
|
|
46
|
-
if (req.method === "DELETE" && url.pathname === "/api/mobile-sessions/ms_1") return send(200, { ok: true });
|
|
47
|
-
if (req.method === "DELETE" && url.pathname === "/api/mobile-sessions/ghost") return send(404, { error: { message: "not_found" } });
|
|
48
|
-
if (req.method === "POST" && url.pathname === "/api/mobile-sessions/purge") return send(200, { ok: true, removed: 2 });
|
|
49
44
|
send(404, { error: { code: "not_found" } });
|
|
50
45
|
});
|
|
51
46
|
return new Promise((resolve) => srv.listen(0, "127.0.0.1", () => resolve({ srv, seen, port: srv.address().port })));
|
|
@@ -122,7 +117,7 @@ test("mobile-sessions 路由:列表透传,Bearer 已带上", async () => {
|
|
|
122
117
|
assert.equal(r.sessions[0].label, "iPhone 15");
|
|
123
118
|
assert.equal(r.sessions[0].os, "iOS");
|
|
124
119
|
const up = relay.seen.find((s) => s.path === "/api/mobile-sessions");
|
|
125
|
-
assert.equal(up.method, "
|
|
120
|
+
assert.equal(up.method, "POST");
|
|
126
121
|
assert.equal(up.authorization, "Bearer jwt-abc");
|
|
127
122
|
} finally {
|
|
128
123
|
host.close();
|
|
@@ -160,66 +155,7 @@ test("mobile-sessions/revoke 路由:body {id} 转发到 /api/mobile-sessions/:
|
|
|
160
155
|
}
|
|
161
156
|
});
|
|
162
157
|
|
|
163
|
-
test("
|
|
164
|
-
const relay = await startFakeRelay();
|
|
165
|
-
const { host, base, tempDir } = await bootRelay(relay.port);
|
|
166
|
-
try {
|
|
167
|
-
const r = await (await fetch(`${base}/dsh-remote/mobile-sessions/delete`, {
|
|
168
|
-
method: "DELETE",
|
|
169
|
-
headers: { "content-type": "application/json" },
|
|
170
|
-
body: JSON.stringify({ id: "ms_1" }),
|
|
171
|
-
})).json();
|
|
172
|
-
assert.equal(r.ok, true);
|
|
173
|
-
const up = relay.seen.find((s) => s.path === "/api/mobile-sessions/ms_1" && s.method === "DELETE");
|
|
174
|
-
assert.ok(up, "应转发到企业端 DELETE /api/mobile-sessions/ms_1");
|
|
175
|
-
assert.equal(up.authorization, "Bearer jwt-abc");
|
|
176
|
-
|
|
177
|
-
// 缺 id → 400;不存在的会话 → 透传 404 文案
|
|
178
|
-
const bad = await (await fetch(`${base}/dsh-remote/mobile-sessions/delete`, {
|
|
179
|
-
method: "DELETE",
|
|
180
|
-
headers: { "content-type": "application/json" },
|
|
181
|
-
body: JSON.stringify({}),
|
|
182
|
-
})).json();
|
|
183
|
-
assert.equal(bad.ok, false);
|
|
184
|
-
assert.match(String(bad.error), /id/);
|
|
185
|
-
|
|
186
|
-
const ghost = await (await fetch(`${base}/dsh-remote/mobile-sessions/delete`, {
|
|
187
|
-
method: "DELETE",
|
|
188
|
-
headers: { "content-type": "application/json" },
|
|
189
|
-
body: JSON.stringify({ id: "ghost" }),
|
|
190
|
-
})).json();
|
|
191
|
-
assert.equal(ghost.ok, false);
|
|
192
|
-
assert.match(String(ghost.error), /not_found/);
|
|
193
|
-
} finally {
|
|
194
|
-
host.close();
|
|
195
|
-
relay.srv.close();
|
|
196
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test("mobile-sessions/purge 路由:POST → 企业端 POST /api/mobile-sessions/purge,removed 透传", async () => {
|
|
201
|
-
const relay = await startFakeRelay();
|
|
202
|
-
const { host, base, tempDir } = await bootRelay(relay.port);
|
|
203
|
-
try {
|
|
204
|
-
const r = await (await fetch(`${base}/dsh-remote/mobile-sessions/purge`, {
|
|
205
|
-
method: "POST",
|
|
206
|
-
headers: { "content-type": "application/json" },
|
|
207
|
-
body: JSON.stringify({}),
|
|
208
|
-
})).json();
|
|
209
|
-
assert.equal(r.ok, true);
|
|
210
|
-
assert.equal(r.removed, 2, "企业端返回的 removed 清理条数应透传");
|
|
211
|
-
const up = relay.seen.find((s) => s.path === "/api/mobile-sessions/purge");
|
|
212
|
-
assert.ok(up, "应转发到 /api/mobile-sessions/purge");
|
|
213
|
-
assert.equal(up.method, "POST");
|
|
214
|
-
assert.equal(up.authorization, "Bearer jwt-abc");
|
|
215
|
-
} finally {
|
|
216
|
-
host.close();
|
|
217
|
-
relay.srv.close();
|
|
218
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test("未登录(无账号配置)→ 五条路由统一 401 提示登录,不请求企业端", async () => {
|
|
158
|
+
test("未登录(无账号配置)→ 三条路由统一 401 提示登录,不请求企业端", async () => {
|
|
223
159
|
const relay = await startFakeRelay();
|
|
224
160
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-aks-401-"));
|
|
225
161
|
await writeFile(path.join(tempDir, ".dsh-config.json"), JSON.stringify({ api_url: `http://127.0.0.1:${relay.port}` }));
|
|
@@ -243,19 +179,13 @@ test("未登录(无账号配置)→ 五条路由统一 401 提示登录,
|
|
|
243
179
|
assert.equal((await (await fetch(`${base}${p}`, { method }))).status, 401, `${p} 应返回 401`);
|
|
244
180
|
assert.match(String(r.error), /尚未登录/);
|
|
245
181
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
headers: { "content-type": "application/json" },
|
|
254
|
-
body: JSON.stringify({ id: "ms_1" }),
|
|
255
|
-
})).json();
|
|
256
|
-
assert.equal(r.ok, false);
|
|
257
|
-
assert.match(String(r.error), /尚未登录/);
|
|
258
|
-
}
|
|
182
|
+
const rev = await (await fetch(`${base}/dsh-remote/mobile-sessions/revoke`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: { "content-type": "application/json" },
|
|
185
|
+
body: JSON.stringify({ id: "ms_1" }),
|
|
186
|
+
})).json();
|
|
187
|
+
assert.equal(rev.ok, false);
|
|
188
|
+
assert.match(String(rev.error), /尚未登录/);
|
|
259
189
|
assert.ok(!relay.seen.some((s) => s.path.startsWith("/api/auth-key") || s.path.startsWith("/api/mobile-sessions")), "未登录不应请求企业端");
|
|
260
190
|
} finally {
|
|
261
191
|
host.close();
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import { apply } from "../lib/index.js";
|
|
8
8
|
|
|
9
|
-
test("切换账号时轮换设备身份并重启 bridge",
|
|
9
|
+
test("切换账号时轮换设备身份并重启 bridge", async () => {
|
|
10
10
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "dsh-account-switch-"));
|
|
11
11
|
const fakeHome = path.join(tempDir, "home");
|
|
12
12
|
const fakeBin = path.join(tempDir, "bin");
|
|
@@ -22,12 +22,6 @@ function find(tree, predicate) {
|
|
|
22
22
|
return match;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function findAll(tree, predicate) {
|
|
26
|
-
const out = [];
|
|
27
|
-
walk(tree, (node) => { if (predicate(node)) out.push(node); });
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
25
|
/** 文本子节点包含子串(用于动态拼接文案断言)。 */
|
|
32
26
|
function textHas(tree, substr) {
|
|
33
27
|
return !!find(tree, (node) => (node.children || []).some((c) => typeof c === "string" && c.includes(substr)));
|
|
@@ -114,19 +108,11 @@ function loadPlugin(opts = {}) {
|
|
|
114
108
|
}
|
|
115
109
|
if (path === "/dsh-remote/mobile-sessions") {
|
|
116
110
|
sessionsCalls++;
|
|
117
|
-
|
|
118
|
-
const list = opts.sessions !== undefined ? opts.sessions : (sessionsCalls === 1 ? SESSIONS_1 : []);
|
|
119
|
-
return response(200, { ok: true, sessions: list });
|
|
111
|
+
return response(200, { ok: true, sessions: sessionsCalls === 1 ? SESSIONS_1 : [] }); // 取消配对后的刷新 → 空列表
|
|
120
112
|
}
|
|
121
113
|
if (path === "/dsh-remote/mobile-sessions/revoke") {
|
|
122
114
|
return response(200, { ok: true });
|
|
123
115
|
}
|
|
124
|
-
if (path === "/dsh-remote/mobile-sessions/delete") {
|
|
125
|
-
return response(200, { ok: true });
|
|
126
|
-
}
|
|
127
|
-
if (path === "/dsh-remote/mobile-sessions/purge") {
|
|
128
|
-
return response(200, { ok: true, removed: 1 });
|
|
129
|
-
}
|
|
130
116
|
if (path === "/dsh-remote/account") return response(200, { ok: true, account: { phone: "13800000000", plan: "free", plan_source: "plan", invite_code: "ABC12345" } });
|
|
131
117
|
return response(200, { ok: true });
|
|
132
118
|
},
|
|
@@ -186,7 +172,7 @@ test("📱 远程访问卡:bridge 在线 → 绿点文案;登录态点「生
|
|
|
186
172
|
assert.ok(qr && qr.props?.alt === "远程访问二维码", "应渲染服务端返回的二维码 <img>");
|
|
187
173
|
// 到期倒计时/有效至文案
|
|
188
174
|
assert.ok(textHas(tree, "有效至") && textHas(tree, "剩余"), "应显示「有效至 HH:MM:SS / 剩余 xx:xx」倒计时");
|
|
189
|
-
assert.ok(textHas(tree, "
|
|
175
|
+
assert.ok(textHas(tree, "访问一次后失效"), "应说明一次性/30 分钟语义");
|
|
190
176
|
});
|
|
191
177
|
|
|
192
178
|
test("已授权设备:展开列表(label/os/browser/时间)→ 取消配对二次确认 → 成功提示并刷新", async () => {
|
|
@@ -230,76 +216,6 @@ test("已授权设备:展开列表(label/os/browser/时间)→ 取消配
|
|
|
230
216
|
assert.ok(textHas(tree, "暂无已授权设备(手机扫码后出现)"), "刷新后空态文案应出现");
|
|
231
217
|
});
|
|
232
218
|
|
|
233
|
-
test("已授权设备行内操作:活跃行有 取消配对+删除记录,已取消行有 删除记录;删除记录二次确认 → DELETE delete {id}", async () => {
|
|
234
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 }); // 固定列表:操作后重拉仍保留,便于断言行内按钮
|
|
235
|
-
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
236
|
-
let tree = plugin.render();
|
|
237
|
-
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
238
|
-
openBtn.props.onClick();
|
|
239
|
-
await flush();
|
|
240
|
-
await flush();
|
|
241
|
-
tree = plugin.render();
|
|
242
|
-
|
|
243
|
-
// 行内按钮:活跃行(ms_1)应有「取消配对」+「删除记录」;已取消行(ms_2)只应有「删除记录」
|
|
244
|
-
const delBtns = findAll(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("删除记录")));
|
|
245
|
-
assert.ok(delBtns.length >= 2, "每行(含已取消/历史)都应有「删除记录」按钮");
|
|
246
|
-
assert.ok(find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("取消配对"))),
|
|
247
|
-
"活跃行应有「取消配对」按钮");
|
|
248
|
-
// 底部有「清理已解绑」入口(卡片底部)
|
|
249
|
-
assert.ok(textHas(tree, "清理已解绑"), "卡片底部应提供「清理已解绑」");
|
|
250
|
-
|
|
251
|
-
// 删除 ms_1:第一次点击进入确认态(不请求)
|
|
252
|
-
delBtns[0].props.onClick();
|
|
253
|
-
tree = plugin.render();
|
|
254
|
-
assert.ok(textHas(tree, "再点一次确认删除记录"), "删除记录需二次确认");
|
|
255
|
-
assert.ok(!plugin.requests.some((r) => r.path === "/dsh-remote/mobile-sessions/delete"), "首次点击不应发 DELETE");
|
|
256
|
-
|
|
257
|
-
// 第二次点击 → DELETE /dsh-remote/mobile-sessions/delete {id} → 提示 + 刷新列表
|
|
258
|
-
const listBefore = plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length;
|
|
259
|
-
find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("再点一次确认删除记录"))).props.onClick();
|
|
260
|
-
await flush();
|
|
261
|
-
await flush();
|
|
262
|
-
await flush();
|
|
263
|
-
const del = plugin.requests.find((r) => r.path === "/dsh-remote/mobile-sessions/delete");
|
|
264
|
-
assert.ok(del, "确认后应 DELETE /dsh-remote/mobile-sessions/delete");
|
|
265
|
-
assert.equal(del.method, "DELETE");
|
|
266
|
-
assert.deepEqual(del.body, { id: "ms_1" }, "行内删除应带上该行 session id");
|
|
267
|
-
assert.ok(plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length > listBefore, "删除后应重拉设备列表");
|
|
268
|
-
tree = plugin.render();
|
|
269
|
-
assert.ok(textHas(tree, "已删除该设备的记录"), "删除成功应有可读提示");
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("已授权设备:卡片底部「清理已解绑」→ 二次确认 → POST purge → 提示条数 + 刷新列表", async () => {
|
|
273
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 });
|
|
274
|
-
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
275
|
-
let tree = plugin.render();
|
|
276
|
-
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
277
|
-
openBtn.props.onClick();
|
|
278
|
-
await flush();
|
|
279
|
-
await flush();
|
|
280
|
-
tree = plugin.render();
|
|
281
|
-
|
|
282
|
-
// 底部 purge 按钮:带 1 条已解绑计数
|
|
283
|
-
const purgeBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("清理已解绑")));
|
|
284
|
-
assert.ok(purgeBtn, "卡片底部应有「清理已解绑」按钮");
|
|
285
|
-
purgeBtn.props.onClick();
|
|
286
|
-
tree = plugin.render();
|
|
287
|
-
assert.ok(textHas(tree, "再点一次确认清理已解绑"), "清理已解绑需二次确认");
|
|
288
|
-
assert.ok(!plugin.requests.some((r) => r.path === "/dsh-remote/mobile-sessions/purge"), "首次点击不应发请求");
|
|
289
|
-
|
|
290
|
-
const listBefore = plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length;
|
|
291
|
-
find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("再点一次确认清理已解绑"))).props.onClick();
|
|
292
|
-
await flush();
|
|
293
|
-
await flush();
|
|
294
|
-
await flush();
|
|
295
|
-
const purge = plugin.requests.find((r) => r.path === "/dsh-remote/mobile-sessions/purge");
|
|
296
|
-
assert.ok(purge, "确认后应 POST /dsh-remote/mobile-sessions/purge");
|
|
297
|
-
assert.equal(purge.method, "POST");
|
|
298
|
-
assert.ok(plugin.requests.filter((r) => r.path === "/dsh-remote/mobile-sessions").length > listBefore, "清理后应重拉设备列表");
|
|
299
|
-
tree = plugin.render();
|
|
300
|
-
assert.ok(textHas(tree, "已清理 1 条已解绑记录"), "应提示清理条数(可读)");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
219
|
test("升级/续费按钮:点击 → GET /dsh-remote/access-key → window.open(url)(带登录态打开)", async () => {
|
|
304
220
|
const plugin = loadPlugin();
|
|
305
221
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
@@ -334,92 +250,4 @@ test("二维码缺失容错 + 未登录引导文案(源码级约束)", () =>
|
|
|
334
250
|
assert.match(SOURCE, /已连接(可远程访问)/);
|
|
335
251
|
assert.match(SOURCE, /带登录态/);
|
|
336
252
|
assert.match(SOURCE, /通过手机或另一台电脑远程使用同一份 dsh web/);
|
|
337
|
-
// 已授权设备管理(delete/purge 代理路由与按钮)
|
|
338
|
-
assert.match(SOURCE, /dsh-remote\/mobile-sessions\/delete/);
|
|
339
|
-
assert.match(SOURCE, /dsh-remote\/mobile-sessions\/purge/);
|
|
340
|
-
assert.match(SOURCE, /删除记录/);
|
|
341
|
-
assert.match(SOURCE, /清理已解绑/);
|
|
342
|
-
assert.match(SOURCE, /再点一次确认删除记录/);
|
|
343
|
-
assert.match(SOURCE, /再点一次确认清理已解绑/);
|
|
344
|
-
// 文案精简:二维码说明压成一句、右侧长段压成一句
|
|
345
|
-
assert.match(SOURCE, /扫码即进入,30 分钟有效、用一次即失效。/);
|
|
346
|
-
assert.match(SOURCE, /打开链接\/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。/);
|
|
347
|
-
assert.doesNotMatch(SOURCE, /每次生成的链接 30 分钟有效、访问一次后失效/);
|
|
348
|
-
assert.doesNotMatch(SOURCE, /手机上打开链接点「进入」即可像在本机一样使用 dsh web/);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// ---------- 端到端加密(E2EE,Phase-5)状态徽标 ----------
|
|
352
|
-
|
|
353
|
-
const E2EE_STATE = {
|
|
354
|
-
enabled: { enabled: true, reason: "ok", profile: "pbkdf2-sha256-600k", epoch: 1, caps: ["e2ee-v2"] },
|
|
355
|
-
serverDisabled: { enabled: false, reason: "server_disabled", profile: "", epoch: 0, caps: [] },
|
|
356
|
-
paramsUnreachable: { enabled: false, reason: "params_unreachable", profile: "", epoch: 0, caps: [] },
|
|
357
|
-
localDisabled: { enabled: false, reason: "disabled_by_config", profile: "", epoch: 0, caps: [] },
|
|
358
|
-
deriveFailed: { enabled: false, reason: "derive_failed", profile: "", epoch: 0, caps: [] },
|
|
359
|
-
unknownReason: { enabled: false, reason: "brand_new_reason_x", profile: "", epoch: 0, caps: [] },
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
function stateWith(service) {
|
|
363
|
-
return { config: { phone: "13800000000", deviceId: "dev-x", mode: "saas" }, service, remoteUrl: "https://app.test/" };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function e2eeLine(tree) {
|
|
367
|
-
return find(tree, (n) => n.props && typeof n.props.className === "string" && n.props.className.indexOf("dru-e2ee-line") === 0);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
test("E2EE 徽标:已启用 → “🔒 端到端加密已启用(手机解锁后生效)”(service.e2ee 字段驱动)", () => {
|
|
371
|
-
const plugin = loadPlugin();
|
|
372
|
-
plugin.states[0] = stateWith({ running: true, e2ee: E2EE_STATE.enabled });
|
|
373
|
-
const tree = plugin.render();
|
|
374
|
-
const line = e2eeLine(tree);
|
|
375
|
-
assert.ok(line, "启用态应在「📱 远程访问」卡渲染加密状态行");
|
|
376
|
-
assert.match(String(line.props.className), /ok/, "启用态状态行应为绿色(ok)标记");
|
|
377
|
-
assert.ok(textHas(tree, "🔒 端到端加密已启用"), "应显示“端到端加密已启用”徽标文案");
|
|
378
|
-
assert.ok(textHas(tree, "手机解锁后生效"), "应提示“手机解锁后生效”(bridge 就绪、手机解锁后方生效)");
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
test("E2EE 徽标:未启用原因映射可读文案(服务端关闭 / 参数不可达 / 本地关闭 / 改密 / 未知兜底)", () => {
|
|
382
|
-
const cases = [
|
|
383
|
-
[E2EE_STATE.serverDisabled, "端到端加密暂不可用(当前为普通安全连接 HTTPS)"],
|
|
384
|
-
[E2EE_STATE.paramsUnreachable, "当前为普通安全连接(HTTPS)"],
|
|
385
|
-
[E2EE_STATE.localDisabled, "当前为普通安全连接(HTTPS)"],
|
|
386
|
-
[E2EE_STATE.deriveFailed, "账号密码已变更"],
|
|
387
|
-
[E2EE_STATE.unknownReason, "当前为普通安全连接(HTTPS)"], // 未知 reason → 兜底
|
|
388
|
-
];
|
|
389
|
-
for (const [e2ee, text] of cases) {
|
|
390
|
-
const plugin = loadPlugin();
|
|
391
|
-
plugin.states[0] = stateWith({ running: true, e2ee });
|
|
392
|
-
const tree = plugin.render();
|
|
393
|
-
assert.ok(textHas(tree, text), `reason=${e2ee.reason} 应映射为可读文案: ${text}`);
|
|
394
|
-
const line = e2eeLine(tree);
|
|
395
|
-
assert.ok(line && !/ ok/.test(String(line.props.className)), `reason=${e2ee.reason} 非启用态不应带 ok 标记`);
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("E2EE 徽标:未登录 / host 未下发 e2ee → 不打扰(不渲染状态行)", () => {
|
|
400
|
-
// 未登录(config 无 phone)即便 bridge 上报 enabled 也不打扰
|
|
401
|
-
const anon = loadPlugin();
|
|
402
|
-
anon.states[0] = { config: { deviceId: "dev-x", mode: "saas" }, service: { running: true, e2ee: E2EE_STATE.enabled } };
|
|
403
|
-
assert.equal(e2eeLine(anon.render()), undefined, "未登录不应渲染 E2EE 状态行");
|
|
404
|
-
assert.ok(!textHas(anon.render(), "手机解锁后生效"), "未登录不应出现加密徽标文案");
|
|
405
|
-
|
|
406
|
-
// 已登录但 service 无 e2ee(旧 host / 未下发)→ 不渲染
|
|
407
|
-
const oldHost = loadPlugin();
|
|
408
|
-
oldHost.states[0] = stateWith({ running: true });
|
|
409
|
-
assert.equal(e2eeLine(oldHost.render()), undefined, "未下发 service.e2ee 时不应渲染状态行");
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
test("E2EE 徽标(源码级约束):client 含徽标字段/文案与 reason 映射表", () => {
|
|
413
|
-
assert.match(SOURCE, /service\.e2ee/);
|
|
414
|
-
assert.match(SOURCE, /\.e2ee-state\.json/);
|
|
415
|
-
assert.match(SOURCE, /🔒 端到端加密已启用(手机解锁后生效)/);
|
|
416
|
-
assert.match(SOURCE, /端到端加密暂不可用(当前为普通安全连接 HTTPS)/);
|
|
417
|
-
assert.doesNotMatch(SOURCE, /等待服务端开启 E2EE/); // 已正式开启,不再出现“灰度等待”措辞
|
|
418
|
-
assert.match(SOURCE, /当前为普通安全连接(HTTPS)/);
|
|
419
|
-
assert.match(SOURCE, /server_disabled/);
|
|
420
|
-
assert.match(SOURCE, /params_unreachable/);
|
|
421
|
-
assert.match(SOURCE, /disabled_by_config/);
|
|
422
|
-
assert.match(SOURCE, /derive_failed/);
|
|
423
|
-
assert.match(SOURCE, /describeE2ee/);
|
|
424
|
-
assert.match(SOURCE, /dru-e2ee-line/);
|
|
425
253
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-remote-web 浏览器半回归(2026-09 前名 dsh-remote-ui):面板入口迁移进「设置」页 settings.section 官方扩展点。
|
|
2
|
-
// 覆盖:栏目注册(id/order/label
|
|
3
|
-
//
|
|
2
|
+
// 覆盖:栏目注册(id/order/label)、侧边栏入口与浮动面板移除、账号区无「切换账号」、
|
|
3
|
+
// 「关于 dsh-remote」说明卡片、退出登录/切换连接账号清理反馈线程凭据、首次安装红点引导。
|
|
4
4
|
import assert from "node:assert/strict";
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import test from "node:test";
|
|
@@ -87,7 +87,6 @@ function loadPlugin(opts = {}) {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
const allCreated = [];
|
|
90
|
-
const intervals = [];
|
|
91
90
|
const doc = {
|
|
92
91
|
createElement(tag) { const el = makeEl(tag); allCreated.push(el); return el; },
|
|
93
92
|
head: makeEl("head"),
|
|
@@ -96,10 +95,7 @@ function loadPlugin(opts = {}) {
|
|
|
96
95
|
const cls = sel.charAt(0) === "." ? sel.slice(1) : "";
|
|
97
96
|
return allCreated.find((el) => el.className === cls) || null;
|
|
98
97
|
},
|
|
99
|
-
querySelectorAll(sel) {
|
|
100
|
-
if (sel === "button") return opts.navCells || [];
|
|
101
|
-
return [];
|
|
102
|
-
},
|
|
98
|
+
querySelectorAll(sel) { return sel === "button" ? (opts.navCells || []) : []; },
|
|
103
99
|
};
|
|
104
100
|
|
|
105
101
|
let lastObserver = null;
|
|
@@ -133,7 +129,7 @@ function loadPlugin(opts = {}) {
|
|
|
133
129
|
return response(200, { body: { ok: true } });
|
|
134
130
|
},
|
|
135
131
|
navigator: { clipboard: { writeText: async () => {} } },
|
|
136
|
-
setInterval(
|
|
132
|
+
setInterval() { return 1; },
|
|
137
133
|
clearInterval() {},
|
|
138
134
|
setTimeout() { return 1; },
|
|
139
135
|
Set,
|
|
@@ -156,7 +152,6 @@ function loadPlugin(opts = {}) {
|
|
|
156
152
|
|
|
157
153
|
return {
|
|
158
154
|
registered, metas, injects, requests, removed, states, localStorage,
|
|
159
|
-
intervals,
|
|
160
155
|
lastObserver: () => lastObserver,
|
|
161
156
|
renderSection() { hook = 0; return registered.get("dsh-remote")({ close() {} }); },
|
|
162
157
|
};
|
|
@@ -208,7 +203,7 @@ test("登录态账号区:无「切换账号」,有「退出登录」,头
|
|
|
208
203
|
"📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。",
|
|
209
204
|
"🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。",
|
|
210
205
|
"🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。",
|
|
211
|
-
"🛡
|
|
206
|
+
"🛡 端到端流量保护:可选对通道做端到端加密保护,传输全程不暴露本机公网 IP(详见项目 README「安全」说明)。",
|
|
212
207
|
];
|
|
213
208
|
for (const p of points) {
|
|
214
209
|
assert.ok(find(tree, (n) => n.children?.includes(p)), `说明卡片应含要点: ${p.slice(0, 12)}…`);
|
|
@@ -282,51 +277,6 @@ test("红点已看过(localStorage 有 key)时不注入,重启 DSH Web 不
|
|
|
282
277
|
assert.equal(navCell.children.length, 0, "已看过时不应再注入红点");
|
|
283
278
|
});
|
|
284
279
|
|
|
285
|
-
test("红点只挂在设置页「远程访问」栏目导航(navCell):普通按钮即使文本含「远程访问」也不注入,不遮挡官方入口", () => {
|
|
286
|
-
const navCells = [];
|
|
287
|
-
const officialCell = makeEl("button");
|
|
288
|
-
officialCell.className = "VOzbGW_navCell"; // shell hashed 类名(含 navCell 子串)=设置页栏目导航
|
|
289
|
-
officialCell.textContent = "📱 远程访问";
|
|
290
|
-
const plainBtn = makeEl("button");
|
|
291
|
-
plainBtn.className = "some-other-btn"; // 非 navCell(官方侧栏/内容区按钮):即使含「远程访问」也不得注入
|
|
292
|
-
plainBtn.textContent = "远程访问";
|
|
293
|
-
const plugin = loadPlugin({ navCells });
|
|
294
|
-
|
|
295
|
-
navCells.push(officialCell, plainBtn);
|
|
296
|
-
plugin.lastObserver().cb([]);
|
|
297
|
-
|
|
298
|
-
assert.equal(officialCell.children.length, 1, "官方设置页「远程访问」栏目按钮应注入红点");
|
|
299
|
-
assert.equal(officialCell.children[0].className, "dru-reddot");
|
|
300
|
-
assert.equal(plainBtn.children.length, 0, "非栏目导航按钮不得注入红点(不遮挡官方按钮)");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
// ---------- 入口收敛:不再向官方侧栏注入任何按钮 / 导航点击机器 ----------
|
|
304
|
-
// 用户从官方「设置」进入 → 点 settings.section「远程访问」栏目(官方扩展点,天然不冲突),
|
|
305
|
-
// 官方「设置」按钮保持原样、不被遮挡。旧 Bug A 侧栏注入实现(容器限定点击流 + 独立按钮 +
|
|
306
|
-
// 红点 + openRemoteSettings/__dshRemoteNav)已整段移除,红点只保留在设置页栏目导航 navCell 上。
|
|
307
|
-
|
|
308
|
-
test("源码约束:侧栏注入按钮/导航点击机器已移除,官方「设置」不被抢、红点仅保留在设置页栏目导航", () => {
|
|
309
|
-
// 不再存在:侧栏 DOM 注入 / 导航容器点击 / 暴露给宿主与自动化的 open 入口
|
|
310
|
-
assert.doesNotMatch(SOURCE, /injectSidebarRemoteEntry/);
|
|
311
|
-
assert.doesNotMatch(SOURCE, /dru-nav-remote/);
|
|
312
|
-
assert.doesNotMatch(SOURCE, /dsh-remote-nav-seen/);
|
|
313
|
-
assert.doesNotMatch(SOURCE, /openRemoteSettings/);
|
|
314
|
-
assert.doesNotMatch(SOURCE, /clickNavToken/);
|
|
315
|
-
assert.doesNotMatch(SOURCE, /remoteSectionVisible/);
|
|
316
|
-
assert.doesNotMatch(SOURCE, /__dshRemoteNav/);
|
|
317
|
-
assert.doesNotMatch(SOURCE, /data-dru-remote/);
|
|
318
|
-
assert.doesNotMatch(SOURCE, /NAV_ENTRY_ID/);
|
|
319
|
-
assert.doesNotMatch(SOURCE, /NAV_CONTAINER_SEL/);
|
|
320
|
-
assert.doesNotMatch(SOURCE, /NAV_TEXT_MAX/);
|
|
321
|
-
assert.doesNotMatch(SOURCE, /dru-fab/);
|
|
322
|
-
// 官方按钮不被克隆 / 包装 / 挪位(克隆曾继承官方事件委托与热区 → Bug A)
|
|
323
|
-
assert.doesNotMatch(SOURCE, /cloneNode/);
|
|
324
|
-
// 唯一保留的 DOM 注入 = 设置页栏目导航红点(.dru-reddot,navCell 右上角,pointer-events:none)
|
|
325
|
-
assert.match(SOURCE, /dru-reddot/);
|
|
326
|
-
assert.match(SOURCE, /navCell/);
|
|
327
|
-
assert.match(SOURCE, /dsh-remote-seen-dot/);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
280
|
test("源码约束:无侧边栏入口/浮动面板/切换账号;命名统一为「远程访问」;新增访问密钥/设备路由", () => {
|
|
331
281
|
// 只断言“代码形态”不存在(注释里允许出现说明文字)
|
|
332
282
|
assert.doesNotMatch(SOURCE, /slots\.inject\("sidebar\.footer\.action"/);
|
|
@@ -77,7 +77,7 @@ async function plantInstallation(tempHome, opts = {}) {
|
|
|
77
77
|
return { relayDir, profile };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
test("彻底卸载:配置目录整目录清空(含账号/密钥/固化运行时),不误伤 profile 其它条目与家目录其它数据",
|
|
80
|
+
test("彻底卸载:配置目录整目录清空(含账号/密钥/固化运行时),不误伤 profile 其它条目与家目录其它数据", async () => {
|
|
81
81
|
const tempHome = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-rt-"));
|
|
82
82
|
const prevHome = process.env.HOME;
|
|
83
83
|
process.env.HOME = tempHome;
|
|
@@ -220,7 +220,7 @@ test("DSH_RELAY_SKIP_SERVICE 开关:置位时即便存在自启动 plist 也
|
|
|
220
220
|
}
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
test("自然跳过(无开关):HOME 有 plist 但服务未运行 → 删除 plist 但不发 launchctl bootout(PATH 假命令接管)",
|
|
223
|
+
test("自然跳过(无开关):HOME 有 plist 但服务未运行 → 删除 plist 但不发 launchctl bootout(PATH 假命令接管)", async () => {
|
|
224
224
|
const tempHome = await mkdtemp(path.join(os.tmpdir(), "dsh-ui-rt-natural-"));
|
|
225
225
|
const prevHome = process.env.HOME;
|
|
226
226
|
const prevPath = process.env.PATH;
|