@mrrisega/dsh-remote 0.7.10-beta.3 → 0.7.12-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/agent/README.md +412 -0
- package/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
- package/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
- package/clients/dsh-remote/agent/codex.mjs +736 -0
- package/clients/dsh-remote/agent/detect.mjs +399 -0
- package/clients/dsh-remote/agent/events.mjs +511 -0
- package/clients/dsh-remote/agent/gemini.mjs +503 -0
- package/clients/dsh-remote/agent/index.mjs +339 -0
- package/clients/dsh-remote/agent/opencode.mjs +628 -0
- package/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
- package/clients/dsh-remote/agent/proc.mjs +179 -0
- package/clients/dsh-remote/agent/version.mjs +148 -0
- package/clients/dsh-remote/approval-coordinator.mjs +150 -0
- package/clients/dsh-remote/decision-poller.mjs +235 -0
- package/clients/dsh-remote/dsh-bridge.mjs +220 -1
- package/clients/dsh-remote/im-format.mjs +372 -0
- package/clients/dsh-remote/push-reporter.mjs +54 -0
- package/clients/dsh-remote/test/agent-codex.test.mjs +535 -0
- package/clients/dsh-remote/test/agent-detect.test.mjs +390 -0
- package/clients/dsh-remote/test/agent-events.test.mjs +340 -0
- package/clients/dsh-remote/test/agent-gemini.test.mjs +355 -0
- package/clients/dsh-remote/test/agent-jsonl-driver.test.mjs +165 -0
- package/clients/dsh-remote/test/agent-opencode.test.mjs +369 -0
- package/clients/dsh-remote/test/agent-permission-bridge.test.mjs +340 -0
- package/clients/dsh-remote/test/agent-proc.test.mjs +175 -0
- package/clients/dsh-remote/test/agent-runtime.test.mjs +282 -0
- package/clients/dsh-remote/test/agent-version.test.mjs +122 -0
- package/clients/dsh-remote/test/approval-coordinator.test.mjs +192 -0
- package/clients/dsh-remote/test/decision-poller.test.mjs +328 -0
- package/clients/dsh-remote/test/im-format.test.mjs +229 -0
- package/clients/dsh-remote/test/upstream-discovery.test.mjs +203 -2
- package/clients/dsh-remote/test/wecom-bot.test.mjs +1718 -0
- package/clients/dsh-remote/upstream-discovery.mjs +495 -43
- package/clients/dsh-remote/wecom-bot.mjs +2670 -0
- package/dsh-setup.mjs +95 -9
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +1377 -292
- package/packages/dsh-remote-web/lib/index.js +419 -12
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/README.md +412 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/codex.mjs +736 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/detect.mjs +399 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/events.mjs +511 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/gemini.mjs +503 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/index.mjs +339 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/opencode.mjs +628 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/proc.mjs +179 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/version.mjs +148 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/approval-coordinator.mjs +150 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/decision-poller.mjs +235 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +258 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/push-reporter.mjs +533 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/wecom-bot.mjs +2444 -0
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +18 -3
- package/packages/dsh-remote-web/test/activation-entry.test.mjs +11 -1
- package/packages/dsh-remote-web/test/announcements-ui.test.mjs +8 -3
- package/packages/dsh-remote-web/test/connect-ui.test.mjs +16 -4
- package/packages/dsh-remote-web/test/im-bots.test.mjs +400 -0
- package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
- package/packages/dsh-remote-web/test/login-fast.test.mjs +104 -0
- package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +17 -6
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +996 -0
- package/packages/dsh-remote-web/test/password-reset.test.mjs +11 -1
- package/packages/dsh-remote-web/test/plugin-link.test.mjs +78 -0
- package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +14 -5
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +69 -119
- package/packages/dsh-remote-web/test/self-manage.test.mjs +5 -1
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +42 -25
- package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
- package/packages/dsh-remote-web/test/telemetry.test.mjs +6 -4
- package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +57 -130
|
@@ -11,6 +11,7 @@ import os from "node:os";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import test from "node:test";
|
|
13
13
|
import vm from "node:vm";
|
|
14
|
+
import { openEntry } from "./lib-panel-ia.mjs";
|
|
14
15
|
import { apply } from "../lib/index.js";
|
|
15
16
|
|
|
16
17
|
const SOURCE = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
@@ -135,6 +136,7 @@ function loadPlugin(opts = {}) {
|
|
|
135
136
|
const removed = [];
|
|
136
137
|
const states = [];
|
|
137
138
|
let hook = 0;
|
|
139
|
+
let iaOpened = false;
|
|
138
140
|
|
|
139
141
|
const react = {
|
|
140
142
|
createElement(type, props, ...children) { return { type, props: props || {}, children }; },
|
|
@@ -218,7 +220,15 @@ function loadPlugin(opts = {}) {
|
|
|
218
220
|
requests,
|
|
219
221
|
removed,
|
|
220
222
|
states,
|
|
221
|
-
render() {
|
|
223
|
+
render() {
|
|
224
|
+
hook = 0;
|
|
225
|
+
let tree = registered.get("dsh-remote")({ close() {} });
|
|
226
|
+
if (!iaOpened) {
|
|
227
|
+
iaOpened = true;
|
|
228
|
+
if (openEntry(tree, opts.autoModal || "account")) { hook = 0; tree = registered.get("dsh-remote")({ close() {} }); }
|
|
229
|
+
}
|
|
230
|
+
return tree;
|
|
231
|
+
},
|
|
222
232
|
};
|
|
223
233
|
}
|
|
224
234
|
|
|
@@ -107,6 +107,59 @@ test("pluginLinkUsable:空目录/畸形链接一律判为不可用(不能只
|
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
+
// ─────────────── ①-b 拷贝形态的版本漂移:桌面端 pnpm hoisted 残留(2026-11-07 实测)───────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 背景:桌面 profile 用 pnpm `nodeLinker: hoisted`,`file:` 依赖被**复制**进 profile 的
|
|
114
|
+
* `node_modules/<id>`(不是链接)。npx 安装器只替换 `dsh-remote-web-plugin/`(file: 的源目录),
|
|
115
|
+
* 那份复制品还是上一版;相邻 beta 之间 `lib/index.js` 往往逐字节相同,只有 package.json 的版本号变了。
|
|
116
|
+
* 旧判据只看入口 ⇒ 判「已就绪」⇒ 宿主继续 import 旧 package.json:
|
|
117
|
+
* 面板版本号是旧的、`/self/update-check` 一直 outdated,用户反复 npx 都"装不上新版"。
|
|
118
|
+
*/
|
|
119
|
+
|
|
120
|
+
test("pluginLinkUsable:入口相同但 package.json 是旧版 → 判为不可用(版本号永远旧的根因)", () => {
|
|
121
|
+
const { pluginLinkUsable } = loadLinkFns();
|
|
122
|
+
const root = tmpDir();
|
|
123
|
+
try {
|
|
124
|
+
const local = makePluginDir(path.join(root, "local"));
|
|
125
|
+
fs.writeFileSync(path.join(local, "package.json"), JSON.stringify({ name: PLUGIN_ID, version: "0.7.10-beta.10" }));
|
|
126
|
+
const nm = path.join(root, "node_modules", PLUGIN_ID);
|
|
127
|
+
fs.mkdirSync(path.dirname(nm), { recursive: true });
|
|
128
|
+
|
|
129
|
+
// 与源目录完全一致的整目录拷贝 → 可用
|
|
130
|
+
fs.cpSync(local, nm, { recursive: true });
|
|
131
|
+
assert.equal(pluginLinkUsable(nm, local), true, "同版本整目录拷贝 → 可用");
|
|
132
|
+
|
|
133
|
+
// ★ 桌面端实测形态:hoisted 复制品还是上一版,入口逐字节相同
|
|
134
|
+
fs.writeFileSync(path.join(nm, "package.json"), JSON.stringify({ name: PLUGIN_ID, version: "0.7.10-beta.3" }));
|
|
135
|
+
assert.equal(pluginLinkUsable(nm, local), false,
|
|
136
|
+
"★旧版 package.json 的拷贝不得判为已就绪(否则宿主 import 旧 package.json,版本号永远旧)");
|
|
137
|
+
} finally {
|
|
138
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("ensurePluginLinked:旧版整目录拷贝会被重建,装载版本随之更新", () => {
|
|
143
|
+
const { ensurePluginLinked } = loadLinkFns();
|
|
144
|
+
const root = tmpDir();
|
|
145
|
+
try {
|
|
146
|
+
const local = makePluginDir(path.join(root, "local"));
|
|
147
|
+
fs.writeFileSync(path.join(local, "package.json"), JSON.stringify({ name: PLUGIN_ID, version: "0.7.10-beta.10" }));
|
|
148
|
+
const profile = path.join(root, "profile");
|
|
149
|
+
const nm = path.join(profile, "node_modules", PLUGIN_ID);
|
|
150
|
+
fs.mkdirSync(path.dirname(nm), { recursive: true });
|
|
151
|
+
fs.cpSync(local, nm, { recursive: true });
|
|
152
|
+
fs.writeFileSync(path.join(nm, "package.json"), JSON.stringify({ name: PLUGIN_ID, version: "0.7.10-beta.3" }));
|
|
153
|
+
|
|
154
|
+
const changed = ensurePluginLinked(profile, local);
|
|
155
|
+
assert.equal(changed, true, "旧版拷贝必须被判为需要重建(幂等判据不得放行)");
|
|
156
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(nm, "package.json"), "utf8")).version, "0.7.10-beta.10",
|
|
157
|
+
"★重建后 node_modules/<id>/package.json 必须与源目录同版");
|
|
158
|
+
} finally {
|
|
159
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
110
163
|
// ─────────────── ② 注入 Windows 的失败形态:不抛错但落盘是空壳 ───────────────
|
|
111
164
|
|
|
112
165
|
/** 造一个 fs 代理:可按需让某个策略"静默失败"(不抛错、也不产出任何东西)。 */
|
|
@@ -292,6 +345,31 @@ test("端到端:挂载失败必须中止安装,且**不写** patch(宁可
|
|
|
292
345
|
}
|
|
293
346
|
});
|
|
294
347
|
|
|
348
|
+
test("端到端:node_modules/<id> 是旧版整目录拷贝时,重装必须把版本刷到当前包(桌面端 hoisted 现场)", async () => {
|
|
349
|
+
const root = tmpDir();
|
|
350
|
+
try {
|
|
351
|
+
const profile = makeProfile(root);
|
|
352
|
+
await runInstaller(["plugin"], { DSH_PROFILE_DIR: profile });
|
|
353
|
+
|
|
354
|
+
// 复现桌面端现场:pnpm hoisted 的 file: 依赖是**整目录拷贝**,源目录升级后这份还是上一版,
|
|
355
|
+
// 且相邻 beta 的 lib/index.js 常逐字节相同 —— 只有 package.json 的版本号能说明它旧了。
|
|
356
|
+
const nm = path.join(profile, "node_modules", PLUGIN_ID);
|
|
357
|
+
fs.rmSync(nm, { recursive: true, force: true });
|
|
358
|
+
fs.cpSync(PLUGIN_SRC, nm, { recursive: true });
|
|
359
|
+
const currentPkg = JSON.parse(fs.readFileSync(path.join(PLUGIN_SRC, "package.json"), "utf8"));
|
|
360
|
+
fs.writeFileSync(path.join(nm, "package.json"),
|
|
361
|
+
JSON.stringify({ ...currentPkg, version: "0.0.0-old" }, null, 2) + "\n");
|
|
362
|
+
|
|
363
|
+
const r = await runInstaller(["plugin"], { DSH_PROFILE_DIR: profile });
|
|
364
|
+
assert.equal(r.code, 0, `重装应成功:${r.stdout}\n${r.stderr}`);
|
|
365
|
+
const after = JSON.parse(fs.readFileSync(path.join(nm, "package.json"), "utf8")).version;
|
|
366
|
+
assert.equal(after, currentPkg.version,
|
|
367
|
+
"★旧版整目录拷贝必须被重建,否则宿主继续 import 旧 package.json(面板版本号永远是旧的)");
|
|
368
|
+
} finally {
|
|
369
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
295
373
|
// ─────────────── ④ 自救入口:repair ───────────────
|
|
296
374
|
|
|
297
375
|
test("repair:空壳现场能被修好,并打印挂载诊断三行", async () => {
|
|
@@ -18,6 +18,7 @@ import assert from "node:assert/strict";
|
|
|
18
18
|
import { readFileSync } from "node:fs";
|
|
19
19
|
import test from "node:test";
|
|
20
20
|
import vm from "node:vm";
|
|
21
|
+
import { openEntry } from "./lib-panel-ia.mjs";
|
|
21
22
|
|
|
22
23
|
const SOURCE = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
23
24
|
// 复制自 community-invite-ui 的迷你 React harness 里,交流群/弹层接口仍有默认夹具,统一给一个假二维码地址。
|
|
@@ -74,6 +75,7 @@ const flush = () => new Promise((resolve) => setImmediate(resolve));
|
|
|
74
75
|
* @param {object} opts - community:/dsh-remote/community 的返回值(默认已配置二维码)
|
|
75
76
|
*/
|
|
76
77
|
function loadPlugin(opts = {}) {
|
|
78
|
+
let iaOpened = false;
|
|
77
79
|
let moduleFactory;
|
|
78
80
|
const registered = new Map();
|
|
79
81
|
const injects = new Map();
|
|
@@ -249,7 +251,12 @@ function loadPlugin(opts = {}) {
|
|
|
249
251
|
},
|
|
250
252
|
render() {
|
|
251
253
|
const root = { type: registered.get("dsh-remote"), props: { close() {} }, children: [] };
|
|
252
|
-
|
|
254
|
+
let tree = renderNode(root, "root");
|
|
255
|
+
if (!iaOpened) {
|
|
256
|
+
iaOpened = true;
|
|
257
|
+
if (openEntry(tree, opts.autoModal || "account")) tree = renderNode(root, "root");
|
|
258
|
+
}
|
|
259
|
+
return tree;
|
|
253
260
|
},
|
|
254
261
|
};
|
|
255
262
|
}
|
|
@@ -259,7 +266,7 @@ test("① 免费用户账号卡:升级 PRO 按钮仍在,但协议文案/链
|
|
|
259
266
|
const plugin = loadPlugin();
|
|
260
267
|
const tree = await plugin.settle(4);
|
|
261
268
|
// 购买入口(升级按钮)必须还在 —— 这次只删协议文案,不动购买能力
|
|
262
|
-
assert.ok(nodeWithText(tree, "
|
|
269
|
+
assert.ok(nodeWithText(tree, "升级 PRO"), "免费用户的「升级 PRO」按钮必须保留(图标换成内联 SVG,文案不再用 emoji)");
|
|
263
270
|
// 协议痕迹一个都不能有
|
|
264
271
|
assertNoAgreementInPanel(tree);
|
|
265
272
|
// 客户端也不再为了这句话去问 node 半(面板里已经没有消费者了)
|
|
@@ -271,12 +278,14 @@ test("② 会员账号卡(业主点名场景):续费会员按钮下方不
|
|
|
271
278
|
account: { plan: "pro", plan_source: "subscription", plan_ends_at: 1893456000000, invite_code: "ABC12345" },
|
|
272
279
|
});
|
|
273
280
|
const tree = await plugin.settle(4);
|
|
274
|
-
assert.ok(nodeWithText(tree, "
|
|
281
|
+
assert.ok(nodeWithText(tree, "续费会员"), "前提:会员的续费入口在(这正是业主看到那句话的位置)");
|
|
275
282
|
assertNoAgreementInPanel(tree);
|
|
276
283
|
});
|
|
277
284
|
|
|
278
285
|
test("③ 连接偏慢双路块:升级(购买)路旁也没有协议链接,协议地址零请求", async () => {
|
|
286
|
+
// 双路块在「手机访问」弹层(访问卡)里 → 打开该弹层
|
|
279
287
|
const plugin = loadPlugin({
|
|
288
|
+
autoModal: "mobile",
|
|
280
289
|
status: {
|
|
281
290
|
ok: true,
|
|
282
291
|
config: { phone: "138****0000", hasPhone: true, mode: "saas", deviceId: "dev-x" },
|
|
@@ -305,7 +314,7 @@ test("④ 未登录:没有购买入口,也没有任何协议痕迹/请求",
|
|
|
305
314
|
},
|
|
306
315
|
});
|
|
307
316
|
const tree = await plugin.settle(4);
|
|
308
|
-
assert.equal(nodeWithText(tree, "
|
|
317
|
+
assert.equal(nodeWithText(tree, "升级 PRO"), undefined, "未登录没有购买入口");
|
|
309
318
|
assertNoAgreementInPanel(tree);
|
|
310
319
|
assert.equal(plugin.agreementRequests().length, 0);
|
|
311
320
|
});
|
|
@@ -325,7 +334,7 @@ test("⑤ 源码级回归锁:client.js 里不得再残留协议渲染痕迹(
|
|
|
325
334
|
test("⑥ 只删文案、不动购买能力:点「升级 PRO」仍走既有购买流程(请求 /dsh-remote/access-key)", async () => {
|
|
326
335
|
const plugin = loadPlugin();
|
|
327
336
|
let tree = await plugin.settle(4);
|
|
328
|
-
const btn = nodeWithText(tree, "
|
|
337
|
+
const btn = nodeWithText(tree, "升级 PRO");
|
|
329
338
|
assert.ok(btn, "升级按钮必须还在");
|
|
330
339
|
btn.props.onClick();
|
|
331
340
|
await plugin.settle(2);
|
|
@@ -4,6 +4,7 @@ import assert from "node:assert/strict";
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import test from "node:test";
|
|
6
6
|
import vm from "node:vm";
|
|
7
|
+
import { openEntry } from "./lib-panel-ia.mjs";
|
|
7
8
|
|
|
8
9
|
const SOURCE = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
9
10
|
|
|
@@ -50,6 +51,7 @@ function loadPlugin(opts = {}) {
|
|
|
50
51
|
const states = [];
|
|
51
52
|
let hook = 0;
|
|
52
53
|
let sessionsCalls = 0;
|
|
54
|
+
let iaOpened = false;
|
|
53
55
|
|
|
54
56
|
const react = {
|
|
55
57
|
createElement(type, props, ...children) { return { type, props: props || {}, children }; },
|
|
@@ -153,7 +155,17 @@ function loadPlugin(opts = {}) {
|
|
|
153
155
|
requests,
|
|
154
156
|
opened,
|
|
155
157
|
states,
|
|
156
|
-
render() {
|
|
158
|
+
render() {
|
|
159
|
+
hook = 0;
|
|
160
|
+
let tree = registered.get("dsh-remote")({ close() {} });
|
|
161
|
+
// IA 重做(2026-11-07):原「📱 远程访问卡」搬进「手机访问」弹层 —— 用例按真实用户路径点开入口卡。
|
|
162
|
+
if (!iaOpened) {
|
|
163
|
+
iaOpened = true;
|
|
164
|
+
const kind = opts.autoModal || "mobile";
|
|
165
|
+
if (openEntry(tree, kind)) { hook = 0; tree = registered.get("dsh-remote")({ close() {} }); }
|
|
166
|
+
}
|
|
167
|
+
return tree;
|
|
168
|
+
},
|
|
157
169
|
};
|
|
158
170
|
}
|
|
159
171
|
|
|
@@ -166,7 +178,9 @@ test("📱 远程访问卡:bridge 在线 → 绿点文案;登录态点「生
|
|
|
166
178
|
|
|
167
179
|
// 状态行:bridge 运行 → “已连接(可远程访问)”
|
|
168
180
|
assert.ok(textHas(tree, "已连接(可远程访问)"), "bridge 在线状态文案应显示");
|
|
169
|
-
|
|
181
|
+
// A1(2026-11-07 业主截图):正文里不再重复「远程访问」标题 —— 顶部与弹层标题已经表达了。
|
|
182
|
+
assert.equal(find(tree, (n) => n.children?.includes("📱 远程访问")), undefined,
|
|
183
|
+
"正文里不得再有重复的「远程访问」卡片标题(业主截图框出的那行)");
|
|
170
184
|
|
|
171
185
|
// 未生成 key 的空态按钮 → 点击触发 /dsh-remote/access-key
|
|
172
186
|
const genBtn = find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("生成访问链接")));
|
|
@@ -191,7 +205,7 @@ test("📱 远程访问卡:bridge 在线 → 绿点文案;登录态点「生
|
|
|
191
205
|
});
|
|
192
206
|
|
|
193
207
|
test("已授权设备:展开列表(label/os/browser/时间)→ 取消配对二次确认 → 成功提示并刷新", async () => {
|
|
194
|
-
const plugin = loadPlugin();
|
|
208
|
+
const plugin = loadPlugin({ autoModal: "devices" });
|
|
195
209
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
196
210
|
let tree = plugin.render();
|
|
197
211
|
|
|
@@ -232,7 +246,7 @@ test("已授权设备:展开列表(label/os/browser/时间)→ 取消配
|
|
|
232
246
|
});
|
|
233
247
|
|
|
234
248
|
test("已授权设备行内操作:活跃行有 取消配对+删除记录,已取消行有 删除记录;删除记录二次确认 → DELETE delete {id}", async () => {
|
|
235
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 }); //
|
|
249
|
+
const plugin = loadPlugin({ sessions: SESSIONS_1, autoModal: "devices" }); // 固定列表 + 设备弹层(IA 重做:列表在弹层里)
|
|
236
250
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
237
251
|
let tree = plugin.render();
|
|
238
252
|
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
@@ -271,7 +285,7 @@ test("已授权设备行内操作:活跃行有 取消配对+删除记录,已
|
|
|
271
285
|
});
|
|
272
286
|
|
|
273
287
|
test("已授权设备:卡片底部「清理已解绑」→ 二次确认 → POST purge → 提示条数 + 刷新列表", async () => {
|
|
274
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 });
|
|
288
|
+
const plugin = loadPlugin({ sessions: SESSIONS_1, autoModal: "devices" });
|
|
275
289
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
276
290
|
let tree = plugin.render();
|
|
277
291
|
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
@@ -302,7 +316,7 @@ test("已授权设备:卡片底部「清理已解绑」→ 二次确认 → PO
|
|
|
302
316
|
});
|
|
303
317
|
|
|
304
318
|
test("升级/续费按钮:点击 → GET /dsh-remote/access-key → window.open(url)(带登录态打开)", async () => {
|
|
305
|
-
const plugin = loadPlugin();
|
|
319
|
+
const plugin = loadPlugin({ autoModal: "account" });
|
|
306
320
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
307
321
|
let tree = plugin.render();
|
|
308
322
|
|
|
@@ -410,130 +424,66 @@ test("E2EE 徽标:未登录 / host 未下发 e2ee → 不打扰(不渲染状
|
|
|
410
424
|
assert.equal(e2eeLine(oldHost.render()), undefined, "未下发 service.e2ee 时不应渲染状态行");
|
|
411
425
|
});
|
|
412
426
|
|
|
413
|
-
test("
|
|
414
|
-
|
|
415
|
-
const plugin = loadPlugin({ localStorageSeed: { "dsh-remote-marketing-more-v1": "1" } });
|
|
427
|
+
test("账号弹层平铺(A2):所有功能直接可见,不再有「更多」折叠;且不渲染档位数值", () => {
|
|
428
|
+
const plugin = loadPlugin({ autoModal: "account", localStorageSeed: { "dsh-remote-marketing-more-v1": "1" } });
|
|
416
429
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
417
|
-
|
|
430
|
+
const tree = plugin.render();
|
|
418
431
|
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
assert.equal(
|
|
423
|
-
|
|
424
|
-
tree = plugin.render();
|
|
425
|
-
const body = find(tree, (n) => n.props?.className === "dru-more-body");
|
|
426
|
-
assert.ok(body, "「更多」内容体应在 DOM 里");
|
|
427
|
-
assert.equal(body.props.hidden, false, "点击后应展开(hidden=false)");
|
|
428
|
-
|
|
429
|
-
// 【0.6.11】档位明细整块删除:标题「套餐与额度」、PRO / Pro Max 规格行、价格与结论句都不再渲染
|
|
430
|
-
assert.ok(!textHas(body, "套餐与额度"), "「套餐与额度」标题应删除");
|
|
431
|
-
assert.ok(!textHas(body, "Mbps"), "不得再渲染带宽数值(用户约定:本地面板不展示 PRO 版本流量带宽)");
|
|
432
|
-
assert.ok(!textHas(body, "GB"), "不得再渲染流量数值");
|
|
433
|
-
assert.ok(!textHas(body, "¥"), "不得再渲染价格");
|
|
434
|
-
assert.ok(!textHas(body, "以套餐页为准"), "随档位明细一起删掉的结论句不得残留");
|
|
435
|
-
// 整张账号卡同样一个规格数字都不许有
|
|
436
|
-
assert.ok(!textHas(tree, "Mbps"), "账号卡里不得出现 Mbps");
|
|
437
|
-
assert.ok(!textHas(tree, "¥"), "账号卡里不得出现价格");
|
|
438
|
-
|
|
439
|
-
// 2026-10-17 版式简化(业主):正面只留「升级/续费 + 用户反馈 + 更多」;
|
|
440
|
-
// 「带新用户换会员 / 修改密码」收进「更多」。升级入口**永远在正面**(不再随收纳状态搬家)。
|
|
441
|
-
const upsell = find(tree, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("升级 PRO")));
|
|
442
|
-
const invite = find(body, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("带新用户换会员")));
|
|
443
|
-
assert.ok(upsell, "正面应保留「🚀 升级 PRO」入口(主行动,位置稳定)");
|
|
444
|
-
assert.ok(invite, "「更多」里应保留「🎁 带新用户换会员」入口");
|
|
445
|
-
assert.equal(typeof upsell.props.onClick, "function");
|
|
446
|
-
assert.equal(typeof invite.props.onClick, "function");
|
|
447
|
-
// 正面不应再出现「带新用户换会员」(已收进「更多」)
|
|
448
|
-
const actions = find(tree, (n) => n.props?.className === "dru-actions");
|
|
449
|
-
assert.ok(actions, "应有主按钮行");
|
|
450
|
-
assert.ok(!textHas(actions, "带新用户换会员"), "带新用户入口不再占正面按钮行");
|
|
451
|
-
// 「更多」现在与主行动同排:按钮本体带 .dru-btn(原来的整宽 .dru-disclose 披露条已去掉)
|
|
452
|
-
const moreBtn = find(tree, (n) => n.props?.id === "dru-account-more");
|
|
453
|
-
assert.ok(moreBtn, "「更多」按钮应存在");
|
|
454
|
-
assert.match(String(moreBtn.props.className || ""), /dru-btn/, "「更多」必须与主行动同排(按钮样式,不再是整宽披露条)");
|
|
455
|
-
assert.doesNotMatch(String(moreBtn.props.className || ""), /dru-disclose(?!-inline)/, "不得再继承整宽披露条样式");
|
|
456
|
-
|
|
457
|
-
// 源码级:函数与其调用都删掉了(不留死代码)。
|
|
458
|
-
// ⚠️ 断言「代码形态」而不是裸标识符 —— 注释里**故意**留着「这里原本是什么、为什么删」的说明
|
|
459
|
-
// (与 settings-entry.test.mjs 的同款约定:注释里允许出现说明文字)。
|
|
460
|
-
assert.doesNotMatch(SOURCE, /function renderPlanDetail\(/, "renderPlanDetail 函数必须删除");
|
|
461
|
-
assert.doesNotMatch(SOURCE, /^\s*renderPlanDetail\(\),/m, "renderPlanDetail 的调用点必须删除");
|
|
462
|
-
assert.doesNotMatch(SOURCE, /function planSpecText\(/, "planSpecText 定义必须删除");
|
|
463
|
-
assert.doesNotMatch(SOURCE, /planSpecText\("/, "planSpecText 的调用必须删除");
|
|
464
|
-
assert.doesNotMatch(SOURCE, /function freeMbpsOf\(/, "freeMbpsOf 定义必须删除");
|
|
465
|
-
assert.doesNotMatch(SOURCE, /=\s*freeMbpsOf\(\)/, "freeMbpsOf 的调用必须删除");
|
|
466
|
-
assert.doesNotMatch(SOURCE, /var FREE_FACTS\s*=/, "FREE_FACTS 定义必须删除(只被 freeMbpsOf 使用)");
|
|
467
|
-
assert.doesNotMatch(SOURCE, /" Mbps"|" GB\/月,¥"/, "不得残留渲染规格数字的字符串拼接");
|
|
468
|
-
// 不变量仍在:planFactsOf 仍被 upgradePath 用来取档位展示名(不是死代码)
|
|
469
|
-
assert.match(SOURCE, /var name = planFactsOf\(key\)\.name;/);
|
|
470
|
-
});
|
|
432
|
+
// 不再有「更多」披露按钮 / 折叠体(业主:账号弹层不要折叠,空间足够)
|
|
433
|
+
assert.equal(find(tree, (n) => n.props?.id === "dru-account-more"), undefined,
|
|
434
|
+
"账号弹层不得再有「更多」折叠按钮");
|
|
435
|
+
assert.equal(find(tree, (n) => n.props?.className === "dru-more-body"), undefined,
|
|
436
|
+
"账号弹层不得再有「更多」折叠体");
|
|
471
437
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
assert.doesNotMatch(SOURCE, /等待服务端开启 E2EE/); // 已正式开启,不再出现“灰度等待”措辞
|
|
478
|
-
assert.match(SOURCE, /当前为普通安全连接(HTTPS)/);
|
|
479
|
-
assert.match(SOURCE, /server_disabled/);
|
|
480
|
-
assert.match(SOURCE, /params_unreachable/);
|
|
481
|
-
assert.match(SOURCE, /disabled_by_config/);
|
|
482
|
-
assert.match(SOURCE, /derive_failed/);
|
|
483
|
-
assert.match(SOURCE, /describeE2ee/);
|
|
484
|
-
assert.match(SOURCE, /dru-e2ee-line/);
|
|
485
|
-
});
|
|
438
|
+
// 各功能直接平铺(无需展开就能看到)
|
|
439
|
+
for (const label of ["用户反馈", "带新用户换会员", "修改密码"]) {
|
|
440
|
+
assert.ok(find(tree, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes(label))),
|
|
441
|
+
"应直接平铺按钮: " + label);
|
|
442
|
+
}
|
|
486
443
|
|
|
487
|
-
//
|
|
488
|
-
|
|
444
|
+
// 档位明细/数值始终不渲染(用户约定:本地面板不展示 PRO 版本流量带宽)
|
|
445
|
+
assert.ok(!textHas(tree, "套餐与额度"), "「套餐与额度」标题应删除");
|
|
446
|
+
assert.ok(!textHas(tree, "Mbps"), "不得再渲染带宽数值");
|
|
447
|
+
assert.ok(!textHas(tree, "GB"), "不得再渲染流量数值");
|
|
448
|
+
assert.ok(!textHas(tree, "¥"), "不得再渲染价格");
|
|
449
|
+
});
|
|
489
450
|
|
|
490
|
-
test("
|
|
491
|
-
const plugin = loadPlugin();
|
|
451
|
+
test("账号弹层:一屏一个主操作(只有升级/续费是 primary);退出登录在状态头(A3)", () => {
|
|
452
|
+
const plugin = loadPlugin({ autoModal: "account" });
|
|
492
453
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
493
454
|
const tree = plugin.render();
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
assert.ok(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const labels = btns.map((b) => JSON.stringify(b.children || []));
|
|
508
|
-
assert.ok(labels.some((t) => t.includes("升级 PRO") || t.includes("续费会员")), "第一个是主行动");
|
|
509
|
-
assert.ok(labels.some((t) => t.includes("用户反馈")), "用户反馈留在正面");
|
|
510
|
-
assert.ok(labels.some((t) => t.includes("更多")), "「更多」与主行动同排");
|
|
511
|
-
|
|
512
|
-
// 退出登录**不在**主按钮行里,而在账号行(.dru-user)右侧
|
|
513
|
-
const logoutInActions = btns.some((b) => (b.children || []).some((c) => c === "退出登录"));
|
|
514
|
-
assert.equal(logoutInActions, false, "退出登录不得再占主按钮行");
|
|
515
|
-
const userRow = find(tree, (n) => n.props?.className === "dru-user");
|
|
516
|
-
assert.ok(userRow, "应有账号行");
|
|
517
|
-
const logout = find(userRow, (n) => n.type === "button" && (n.children || []).includes("退出登录"));
|
|
518
|
-
assert.ok(logout, "退出登录应在账号行里(业主:放到账号那一行旁边)");
|
|
519
|
-
assert.match(String(logout.props.className || ""), /dru-user-logout/);
|
|
455
|
+
|
|
456
|
+
const primary = findAll(tree, (n) => n.type === "button" && String(n.props?.className || "").includes("dru-btn-primary"));
|
|
457
|
+
assert.ok(primary.length <= 1, `一屏最多一个主操作,实际 ${primary.length} 个 primary 按钮`);
|
|
458
|
+
|
|
459
|
+
// 登录/退出登录同一位置:状态头右侧(`.dru-ia-head-actions`)
|
|
460
|
+
const headActions = find(tree, (n) => String(n.props?.className || "").split(/\s+/).includes("dru-ia-head-actions"));
|
|
461
|
+
assert.ok(headActions, "状态头右侧应有按钮组(登录/退出登录与打开手机端同一处)");
|
|
462
|
+
assert.ok(find(headActions, (n) => n.type === "button" && (n.children || []).includes("退出登录")),
|
|
463
|
+
"已登录时状态头应有「退出登录」");
|
|
464
|
+
// 账号弹层内部不再重复一个退出登录按钮
|
|
465
|
+
const dlg = find(tree, (n) => String(n.props?.className || "").includes("dru-ia-dialog"));
|
|
466
|
+
assert.equal(find(dlg, (n) => n.type === "button" && (n.children || []).includes("退出登录")), undefined,
|
|
467
|
+
"退出登录不得在账号弹层里重复出现(同一动作只在一个位置)");
|
|
520
468
|
});
|
|
521
469
|
|
|
522
|
-
test("
|
|
470
|
+
test("未登录:状态头显示「登录 / 注册」;点「打开手机端」直接弹登录弹层并说明原因(A3/A4)", () => {
|
|
523
471
|
const plugin = loadPlugin();
|
|
524
|
-
|
|
472
|
+
// 该 harness 无 setSignedIn:直接给状态槽一个"未登录"的 st(useEffect 在本 harness 是空实现)
|
|
473
|
+
plugin.states[0] = { ok: true, config: {}, service: { running: false } };
|
|
525
474
|
let tree = plugin.render();
|
|
526
|
-
const
|
|
527
|
-
assert.ok(
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
assert.ok(
|
|
475
|
+
const headActions = find(tree, (n) => String(n.props?.className || "").split(/\s+/).includes("dru-ia-head-actions"));
|
|
476
|
+
assert.ok(headActions, "未登录也要有状态头按钮组");
|
|
477
|
+
const loginBtn = find(headActions, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("登录 / 注册")));
|
|
478
|
+
assert.ok(loginBtn, "未登录时同一位置显示「登录 / 注册」入口");
|
|
479
|
+
|
|
480
|
+
// 点状态头的「打开手机端」→ 不许静默失败:直接弹登录弹层 + 说明
|
|
481
|
+
const openPhone = find(headActions, (n) => n.type === "button" && (n.children || []).some((c) => typeof c === "string" && c.includes("打开手机端")));
|
|
482
|
+
assert.ok(openPhone, "应有「打开手机端」主按钮");
|
|
483
|
+
openPhone.props.onClick();
|
|
484
|
+
tree = plugin.settle ? plugin.render() : plugin.render();
|
|
485
|
+
assert.ok(find(tree, (n) => String(n.props?.className || "").includes("dru-ia-dialog")), "应弹出登录弹层(不是静默失败)");
|
|
486
|
+
assert.ok(textHas(tree, "登录后才能用手机访问"), "弹层里要说明「登录后才能用手机访问」");
|
|
537
487
|
});
|
|
538
488
|
|
|
539
489
|
test("★ 删除了两段解释性文案:升级带登录态说明 / 重启 DeepSeek Harness 说明", () => {
|
|
@@ -231,7 +231,11 @@ test("源码约束:浏览器半提供版本与更新卡片(含彻底卸载
|
|
|
231
231
|
assert.match(source, /一键更新/);
|
|
232
232
|
assert.match(source, /彻底卸载/);
|
|
233
233
|
assert.match(source, /dsh-remote\/self\/uninstall/);
|
|
234
|
-
|
|
234
|
+
// A7(2026-11-07 业主):彻底卸载前先弹**挽留反馈**(原因单选 + 建议),提交后继续卸载。
|
|
235
|
+
assert.match(source, /为什么要卸载/, "必须询问卸载原因(挽留反馈)");
|
|
236
|
+
assert.match(source, /我再想想/, "必须给「我再想想」的退出选项");
|
|
237
|
+
assert.match(source, /仍要卸载/, "确认按钮必须是「仍要卸载」");
|
|
238
|
+
assert.match(source, /kind: "uninstall"/, "反馈必须复用现有反馈通道(kind=uninstall)");
|
|
235
239
|
});
|
|
236
240
|
|
|
237
241
|
test("★ 用户反馈锁:已是最新版时,修复入口必须**看得出是修复入口**(不能只叫「重新检查」)", () => {
|
|
@@ -5,6 +5,7 @@ import assert from "node:assert/strict";
|
|
|
5
5
|
import { readFileSync } from "node:fs";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import vm from "node:vm";
|
|
8
|
+
import { openEntry } from "./lib-panel-ia.mjs";
|
|
8
9
|
|
|
9
10
|
const SOURCE = readFileSync(new URL("../lib/client.js", import.meta.url), "utf8");
|
|
10
11
|
|
|
@@ -59,6 +60,7 @@ function makeEl(tag) {
|
|
|
59
60
|
* localStorageSeed(预置 key/value)。
|
|
60
61
|
*/
|
|
61
62
|
function loadPlugin(opts = {}) {
|
|
63
|
+
let iaOpened = false;
|
|
62
64
|
let moduleFactory;
|
|
63
65
|
const registered = new Map();
|
|
64
66
|
const metas = new Map();
|
|
@@ -158,7 +160,16 @@ function loadPlugin(opts = {}) {
|
|
|
158
160
|
registered, metas, injects, requests, removed, states, localStorage,
|
|
159
161
|
intervals,
|
|
160
162
|
lastObserver: () => lastObserver,
|
|
161
|
-
renderSection() {
|
|
163
|
+
renderSection() {
|
|
164
|
+
hook = 0;
|
|
165
|
+
let tree = registered.get("dsh-remote")({ close() {} });
|
|
166
|
+
// IA 重做(2026-11-07):账号/退出登录在「账号」弹层里 —— 按真实用户路径点开入口卡。
|
|
167
|
+
if (!iaOpened) {
|
|
168
|
+
iaOpened = true;
|
|
169
|
+
if (openEntry(tree, opts.autoModal || "account")) { hook = 0; tree = registered.get("dsh-remote")({ close() {} }); }
|
|
170
|
+
}
|
|
171
|
+
return tree;
|
|
172
|
+
},
|
|
162
173
|
};
|
|
163
174
|
}
|
|
164
175
|
|
|
@@ -212,9 +223,10 @@ test("入口迁移:settings.section 里**只有一个**栏目(id=dsh-remote
|
|
|
212
223
|
assert.equal(plugin.metas.has("dsh-remote-wechat"), false,
|
|
213
224
|
"「🤖 微信机器人」不得再注册成独立栏目(0.6.11:搬进面板的第三个 tab「微信机器人通道」)");
|
|
214
225
|
assert.doesNotMatch(SOURCE, /dsh-remote-wechat/, "源码里也不得残留第二个栏目的 id");
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
assert.match(SOURCE, /
|
|
226
|
+
// IA 重做(2026-11-07):顶部 tab 与绿泡泡 emoji 均已删除 —— 微信通道在「通知与消息通道」弹层里,
|
|
227
|
+
// 主面板保留「通知与消息通道」入口卡(行为断言见 wechat-bot-ui.test.mjs)。
|
|
228
|
+
assert.match(SOURCE, /"通知与消息通道"/, "必须有「通知与消息通道」入口(微信/企微两条通道都在里面)");
|
|
229
|
+
assert.match(SOURCE, /"微信机器人(扫码绑定)"/, "通道弹层里必须有微信机器人一行");
|
|
218
230
|
// 槽注入次数:settings.section + shell.overlay + sidebar.footer.action(2026-10-10 加的侧栏入口)
|
|
219
231
|
assert.equal(plugin.injects.size, 3,
|
|
220
232
|
`inject 的槽名应是 settings.section + shell.overlay + sidebar.footer.action,实际:${[...plugin.injects.keys()].join(",")}`);
|
|
@@ -226,32 +238,33 @@ test("登录态账号区:无「切换账号」,有「退出登录」,头
|
|
|
226
238
|
let tree = plugin.renderSection();
|
|
227
239
|
|
|
228
240
|
// 栏目头部:标题「远程访问」+ 副文案(人在哪都能用,免公网 IP)
|
|
229
|
-
|
|
230
|
-
assert.ok(find(tree, (n) => n.children?.includes("
|
|
241
|
+
// 2026-11-07 品牌统一:顶部面板标题改为插件全名 dsh-remote-web(「远程访问」保留在副标题/栏目名里)
|
|
242
|
+
assert.ok(find(tree, (n) => n.children?.includes("dsh-remote-web")), "头部标题应为品牌全名 dsh-remote-web");
|
|
243
|
+
assert.ok(find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("远程访问"))),
|
|
244
|
+
"副标题仍说明这是「远程访问」能力");
|
|
245
|
+
assert.ok(find(tree, (n) => (n.children || []).some((c) => typeof c === "string" && c.includes("通过手机或另一台电脑远程使用同一份 dsh web,人在哪都能用(免公网 IP)"))),
|
|
246
|
+
"应渲染新副文案(人在哪都能用,免公网 IP)");
|
|
231
247
|
|
|
232
248
|
// 账号区按钮
|
|
233
249
|
assert.ok(find(tree, (n) => n.children?.includes("退出登录")), "应保留「退出登录」");
|
|
234
250
|
assert.ok(!find(tree, (n) => n.children?.includes("切换账号")), "「切换账号」按钮应移除");
|
|
235
251
|
|
|
236
|
-
// 关于 dsh-remote
|
|
237
|
-
assert.ok(find(tree, (n) => n.children?.includes("
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
"
|
|
242
|
-
"🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。",
|
|
243
|
-
];
|
|
244
|
-
for (const p of points) {
|
|
245
|
-
assert.ok(find(tree, (n) => n.children?.includes(p)), `说明卡片应含要点: ${p.slice(0, 12)}…`);
|
|
252
|
+
// 关于 dsh-remote 卡片:在「更多设置」折叠区里 —— 标题不再带 emoji(图标一律内联 SVG)
|
|
253
|
+
assert.ok(find(tree, (n) => n.children?.includes("关于 dsh-remote-web")), "应渲染「关于 dsh-remote-web」卡片标题");
|
|
254
|
+
// 价值要点仍要在(不要求逐字长文案:IA 重做后关于卡精简成结论句,避免文字堆砌)
|
|
255
|
+
const aboutBits = ["免公网 IP", "端到端加密", "匿名装机统计"];
|
|
256
|
+
for (const bit of aboutBits) {
|
|
257
|
+
assert.ok(textHas(tree, bit), "关于卡片应含要点: " + bit);
|
|
246
258
|
}
|
|
247
259
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
assert.ok(
|
|
260
|
+
// 新 IA(2026-11-07):顶部 tab 已删除 → 主面板是「状态头 + 2 列入口网格 + 文字入口」
|
|
261
|
+
for (const title of ["手机访问", "通知与消息通道", "已授权设备", "账号"]) {
|
|
262
|
+
assert.ok(find(tree, (n) => (n.children || []).includes(title)), "入口网格应有卡片: " + title);
|
|
263
|
+
}
|
|
264
|
+
assert.equal(find(tree, (n) => n.props?.className === "dru-tabs"), undefined, "主面板不得再有顶部 tab 条");
|
|
265
|
+
// 状态头:连接状态 pill(圆点 + 文案,不只靠颜色)+ 主按钮「打开手机端」
|
|
266
|
+
assert.ok(find(tree, (n) => String(n.props?.className || "").split(/\s+/).includes("dru-ia-pill")), "状态头应有连接状态 pill");
|
|
267
|
+
assert.ok(textHas(tree, "打开手机端"), "状态头应有主按钮「打开手机端」");
|
|
255
268
|
});
|
|
256
269
|
|
|
257
270
|
test("退出登录清除用户反馈线程凭据(localStorage dsh-feedback-threads)", async () => {
|
|
@@ -368,11 +381,15 @@ test("源码约束:侧栏入口走官方槽位/无浮动面板/无切换账号
|
|
|
368
381
|
assert.doesNotMatch(SOURCE, /dru-backdrop\{/);
|
|
369
382
|
assert.doesNotMatch(SOURCE, /切换账号/);
|
|
370
383
|
assert.doesNotMatch(SOURCE, /dsh-remote-panel/);
|
|
371
|
-
|
|
384
|
+
// 旧称呼「远程控制」不再作为**面板名字**出现(label/头部/菜单)。
|
|
385
|
+
// 注:2026-11-07 业主新文案「保持在线,才能用手机端远程控制这台电脑」把该词当**动词**用,
|
|
386
|
+
// 因此不再对全文做一刀切禁止;这里锁"仍然叫远程访问"这个真正的约束。
|
|
387
|
+
assert.match(SOURCE, /"远程访问"/);
|
|
388
|
+
assert.doesNotMatch(SOURCE, /"远程控制"|远程控制(/);
|
|
372
389
|
assert.match(SOURCE, /settings\.section/);
|
|
373
390
|
assert.match(SOURCE, /dsh-remote-seen-dot/);
|
|
374
391
|
assert.match(SOURCE, /fbClearThreads/);
|
|
375
|
-
assert.match(SOURCE, /关于 dsh-remote/);
|
|
392
|
+
assert.match(SOURCE, /关于 dsh-remote-web/);
|
|
376
393
|
assert.match(SOURCE, /免公网 IP/); // 关于卡 v0.5+ 价值要点
|
|
377
394
|
assert.match(SOURCE, /端到端/);
|
|
378
395
|
assert.match(SOURCE, /已授权设备/);
|