@mrrisega/dsh-remote 0.7.10-beta.3 → 0.7.11-beta.3
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 +214 -1
- 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/wecom-bot.test.mjs +1438 -0
- package/clients/dsh-remote/wecom-bot.mjs +2444 -0
- package/dsh-setup.mjs +18 -3
- package/package.json +2 -2
- package/packages/dsh-remote-web/lib/client.js +1048 -223
- package/packages/dsh-remote-web/lib/index.js +299 -2
- 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 +1 -1
- 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 +359 -0
- package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
- package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +13 -5
- package/packages/dsh-remote-web/test/panel-ia.test.mjs +798 -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 +10 -1
- package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +20 -8
- package/packages/dsh-remote-web/test/settings-entry.test.mjs +30 -21
- package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
- package/packages/dsh-remote-web/test/telemetry.test.mjs +5 -3
- 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
|
}
|
|
@@ -276,7 +283,9 @@ test("② 会员账号卡(业主点名场景):续费会员按钮下方不
|
|
|
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" },
|
|
@@ -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
|
|
|
@@ -191,7 +203,7 @@ test("📱 远程访问卡:bridge 在线 → 绿点文案;登录态点「生
|
|
|
191
203
|
});
|
|
192
204
|
|
|
193
205
|
test("已授权设备:展开列表(label/os/browser/时间)→ 取消配对二次确认 → 成功提示并刷新", async () => {
|
|
194
|
-
const plugin = loadPlugin();
|
|
206
|
+
const plugin = loadPlugin({ autoModal: "devices" });
|
|
195
207
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
196
208
|
let tree = plugin.render();
|
|
197
209
|
|
|
@@ -232,7 +244,7 @@ test("已授权设备:展开列表(label/os/browser/时间)→ 取消配
|
|
|
232
244
|
});
|
|
233
245
|
|
|
234
246
|
test("已授权设备行内操作:活跃行有 取消配对+删除记录,已取消行有 删除记录;删除记录二次确认 → DELETE delete {id}", async () => {
|
|
235
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 }); //
|
|
247
|
+
const plugin = loadPlugin({ sessions: SESSIONS_1, autoModal: "devices" }); // 固定列表 + 设备弹层(IA 重做:列表在弹层里)
|
|
236
248
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
237
249
|
let tree = plugin.render();
|
|
238
250
|
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
@@ -271,7 +283,7 @@ test("已授权设备行内操作:活跃行有 取消配对+删除记录,已
|
|
|
271
283
|
});
|
|
272
284
|
|
|
273
285
|
test("已授权设备:卡片底部「清理已解绑」→ 二次确认 → POST purge → 提示条数 + 刷新列表", async () => {
|
|
274
|
-
const plugin = loadPlugin({ sessions: SESSIONS_1 });
|
|
286
|
+
const plugin = loadPlugin({ sessions: SESSIONS_1, autoModal: "devices" });
|
|
275
287
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
276
288
|
let tree = plugin.render();
|
|
277
289
|
const openBtn = find(tree, (n) => typeof n.props?.onClick === "function" && (n.children || []).some((c) => typeof c === "string" && c.includes("已授权设备")));
|
|
@@ -302,7 +314,7 @@ test("已授权设备:卡片底部「清理已解绑」→ 二次确认 → PO
|
|
|
302
314
|
});
|
|
303
315
|
|
|
304
316
|
test("升级/续费按钮:点击 → GET /dsh-remote/access-key → window.open(url)(带登录态打开)", async () => {
|
|
305
|
-
const plugin = loadPlugin();
|
|
317
|
+
const plugin = loadPlugin({ autoModal: "account" });
|
|
306
318
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
307
319
|
let tree = plugin.render();
|
|
308
320
|
|
|
@@ -412,7 +424,7 @@ test("E2EE 徽标:未登录 / host 未下发 e2ee → 不打扰(不渲染状
|
|
|
412
424
|
|
|
413
425
|
test("「更多」只收纳营销入口:档位规格数字(Mbps / GB / 价格)不再出现在账号卡里", () => {
|
|
414
426
|
// 收纳态(localStorage 里记着用户关掉过引导)=「更多」里会出现那两条折叠入口的场景
|
|
415
|
-
const plugin = loadPlugin({ localStorageSeed: { "dsh-remote-marketing-more-v1": "1" } });
|
|
427
|
+
const plugin = loadPlugin({ autoModal: "account", localStorageSeed: { "dsh-remote-marketing-more-v1": "1" } });
|
|
416
428
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
417
429
|
let tree = plugin.render();
|
|
418
430
|
|
|
@@ -488,7 +500,7 @@ test("E2EE 徽标(源码级约束):client 含徽标字段/文案与 reason
|
|
|
488
500
|
// 动线:账号(我是谁 / 退出)→ 主行动(升级或续费)→ 次要(反馈)→ 更多(其它入口)。
|
|
489
501
|
|
|
490
502
|
test("★ 正面按钮收敛为三个:升级/续费 + 用户反馈 + 更多(不再一排按钮疯狂换行)", () => {
|
|
491
|
-
const plugin = loadPlugin();
|
|
503
|
+
const plugin = loadPlugin({ autoModal: "account" });
|
|
492
504
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
493
505
|
const tree = plugin.render();
|
|
494
506
|
// 面板里可能有多处 .dru-actions(访问密钥卡等);账号卡那一处用「更多」按钮定位。
|
|
@@ -520,7 +532,7 @@ test("★ 正面按钮收敛为三个:升级/续费 + 用户反馈 + 更多(
|
|
|
520
532
|
});
|
|
521
533
|
|
|
522
534
|
test("★ 次要入口全部在「更多」里:带新用户 / 修改密码 / 加入交流群(展开后可见)", () => {
|
|
523
|
-
const plugin = loadPlugin();
|
|
535
|
+
const plugin = loadPlugin({ autoModal: "account" });
|
|
524
536
|
plugin.states[0] = { config: { phone: "13800000000", deviceId: "dev-x" }, service: { running: true } };
|
|
525
537
|
let tree = plugin.render();
|
|
526
538
|
const toggle = find(tree, (n) => n.props?.id === "dru-account-more");
|
|
@@ -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(",")}`);
|
|
@@ -233,25 +245,22 @@ test("登录态账号区:无「切换账号」,有「退出登录」,头
|
|
|
233
245
|
assert.ok(find(tree, (n) => n.children?.includes("退出登录")), "应保留「退出登录」");
|
|
234
246
|
assert.ok(!find(tree, (n) => n.children?.includes("切换账号")), "「切换账号」按钮应移除");
|
|
235
247
|
|
|
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)}…`);
|
|
248
|
+
// 关于 dsh-remote 卡片:在「更多设置」折叠区里 —— 标题不再带 emoji(图标一律内联 SVG)
|
|
249
|
+
assert.ok(find(tree, (n) => n.children?.includes("关于 dsh-remote")), "应渲染「关于 dsh-remote」卡片标题");
|
|
250
|
+
// 价值要点仍要在(不要求逐字长文案:IA 重做后关于卡精简成结论句,避免文字堆砌)
|
|
251
|
+
const aboutBits = ["免公网 IP", "端到端加密", "匿名装机统计"];
|
|
252
|
+
for (const bit of aboutBits) {
|
|
253
|
+
assert.ok(textHas(tree, bit), "关于卡片应含要点: " + bit);
|
|
246
254
|
}
|
|
247
255
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
assert.ok(
|
|
256
|
+
// 新 IA(2026-11-07):顶部 tab 已删除 → 主面板是「状态头 + 2 列入口网格 + 文字入口」
|
|
257
|
+
for (const title of ["手机访问", "通知与消息通道", "已授权设备", "账号"]) {
|
|
258
|
+
assert.ok(find(tree, (n) => (n.children || []).includes(title)), "入口网格应有卡片: " + title);
|
|
259
|
+
}
|
|
260
|
+
assert.equal(find(tree, (n) => n.props?.className === "dru-tabs"), undefined, "主面板不得再有顶部 tab 条");
|
|
261
|
+
// 状态头:连接状态 pill(圆点 + 文案,不只靠颜色)+ 主按钮「打开手机端」
|
|
262
|
+
assert.ok(find(tree, (n) => String(n.props?.className || "").split(/\s+/).includes("dru-ia-pill")), "状态头应有连接状态 pill");
|
|
263
|
+
assert.ok(textHas(tree, "打开手机端"), "状态头应有主按钮「打开手机端」");
|
|
255
264
|
});
|
|
256
265
|
|
|
257
266
|
test("退出登录清除用户反馈线程凭据(localStorage dsh-feedback-threads)", async () => {
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import test from "node:test";
|
|
4
4
|
import vm from "node:vm";
|
|
5
|
+
import { openEntry } from "./lib-panel-ia.mjs";
|
|
5
6
|
|
|
6
7
|
function walk(node, visit) {
|
|
7
8
|
if (!node || typeof node !== "object") return;
|
|
@@ -24,7 +25,9 @@ test("账号状态未返回前只显示加载态且不暴露配置路径", () =>
|
|
|
24
25
|
assert.match(source, /正在读取远控状态/);
|
|
25
26
|
assert.match(source, /st\.config\.deviceId/);
|
|
26
27
|
assert.doesNotMatch(source, /configPath/);
|
|
27
|
-
|
|
28
|
+
// IA 重做(2026-11-07):登录不再隐藏功能 —— 未登录时主面板照常显示,入口卡写「登录后可同步」。
|
|
29
|
+
// (旧的 `st !== null && !loggedIn` 内联三元结构已随账号卡迁入弹层而消失,改锁新契约。)
|
|
30
|
+
assert.match(source, /登录后可同步/);
|
|
28
31
|
});
|
|
29
32
|
|
|
30
33
|
test("短信防刷要求图形验证码时,注册面板展示验证码并随重试提交", async () => {
|
|
@@ -97,7 +100,17 @@ test("短信防刷要求图形验证码时,注册面板展示验证码并随
|
|
|
97
100
|
// 面板入口已迁入设置页 settings.section 栏目(id=dsh-remote),不再有侧边栏入口/浮动面板
|
|
98
101
|
const section = registered.get("dsh-remote");
|
|
99
102
|
assert.ok(section, "settings.section 栏目组件应已注册");
|
|
100
|
-
|
|
103
|
+
let iaOpened = false;
|
|
104
|
+
const render = () => {
|
|
105
|
+
hook = 0;
|
|
106
|
+
let tree = section({ close() {} });
|
|
107
|
+
// IA 重做(2026-11-07):登录/注册在「账号」弹层里 —— 按真实用户路径点开入口卡。
|
|
108
|
+
if (!iaOpened) {
|
|
109
|
+
iaOpened = true;
|
|
110
|
+
if (openEntry(tree, "account")) { hook = 0; tree = section({ close() {} }); }
|
|
111
|
+
}
|
|
112
|
+
return tree;
|
|
113
|
+
};
|
|
101
114
|
|
|
102
115
|
states[0] = { config: { phone: "", deviceId: "dev-test" }, service: { running: false, plistExists: true } };
|
|
103
116
|
let tree = render();
|
|
@@ -808,10 +808,12 @@ test("面板可见性:关于卡片写明「匿名统计 + 关闭方式」,
|
|
|
808
808
|
// 0.6.18 面板重排(「加入交流群」搬进账号卡)之后更是会变成空串 → 断言全部平凡通过。
|
|
809
809
|
// 这是"静默失守":比变红更危险(它看起来一直是绿的)。
|
|
810
810
|
// 现在取片边界是「关于卡片开头 → renderHome 结尾」,并**显式断言切片非空且像样**。
|
|
811
|
-
|
|
811
|
+
// IA 重做(2026-11-07):关于卡不再用 card() 助手(标题图标改成内联 SVG,禁止 emoji)→
|
|
812
|
+
// 取片锚点改成"关于卡标题 → 弹层段开头",仍然要求切片非空且真的包含卡片本体。
|
|
813
|
+
const aboutStart = client.indexOf('h("h3", null, "关于 dsh-remote")');
|
|
812
814
|
assert.ok(aboutStart > 0, "必须能定位到「关于」卡片的开头");
|
|
813
|
-
const aboutEnd = client.indexOf("
|
|
814
|
-
assert.ok(aboutEnd > aboutStart, "
|
|
815
|
+
const aboutEnd = client.indexOf("// ── 弹层内容", aboutStart);
|
|
816
|
+
assert.ok(aboutEnd > aboutStart, "必须能定位到「关于」卡片的结束边界(弹层段开始之前)");
|
|
815
817
|
const about = client.slice(aboutStart, aboutEnd);
|
|
816
818
|
assert.ok(about.length > 400, "「关于」卡片切片不能是空串/碎片(空串会让下面四条断言全部平凡通过)");
|
|
817
819
|
assert.ok(about.includes("关于 dsh-remote"), "切片必须真的包含「关于」卡片本体");
|