@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.
Files changed (68) hide show
  1. package/clients/dsh-remote/agent/README.md +412 -0
  2. package/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
  3. package/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
  4. package/clients/dsh-remote/agent/codex.mjs +736 -0
  5. package/clients/dsh-remote/agent/detect.mjs +399 -0
  6. package/clients/dsh-remote/agent/events.mjs +511 -0
  7. package/clients/dsh-remote/agent/gemini.mjs +503 -0
  8. package/clients/dsh-remote/agent/index.mjs +339 -0
  9. package/clients/dsh-remote/agent/opencode.mjs +628 -0
  10. package/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
  11. package/clients/dsh-remote/agent/proc.mjs +179 -0
  12. package/clients/dsh-remote/agent/version.mjs +148 -0
  13. package/clients/dsh-remote/approval-coordinator.mjs +150 -0
  14. package/clients/dsh-remote/decision-poller.mjs +235 -0
  15. package/clients/dsh-remote/dsh-bridge.mjs +214 -1
  16. package/clients/dsh-remote/push-reporter.mjs +54 -0
  17. package/clients/dsh-remote/test/agent-codex.test.mjs +535 -0
  18. package/clients/dsh-remote/test/agent-detect.test.mjs +390 -0
  19. package/clients/dsh-remote/test/agent-events.test.mjs +340 -0
  20. package/clients/dsh-remote/test/agent-gemini.test.mjs +355 -0
  21. package/clients/dsh-remote/test/agent-jsonl-driver.test.mjs +165 -0
  22. package/clients/dsh-remote/test/agent-opencode.test.mjs +369 -0
  23. package/clients/dsh-remote/test/agent-permission-bridge.test.mjs +340 -0
  24. package/clients/dsh-remote/test/agent-proc.test.mjs +175 -0
  25. package/clients/dsh-remote/test/agent-runtime.test.mjs +282 -0
  26. package/clients/dsh-remote/test/agent-version.test.mjs +122 -0
  27. package/clients/dsh-remote/test/approval-coordinator.test.mjs +192 -0
  28. package/clients/dsh-remote/test/decision-poller.test.mjs +328 -0
  29. package/clients/dsh-remote/test/wecom-bot.test.mjs +1438 -0
  30. package/clients/dsh-remote/wecom-bot.mjs +2444 -0
  31. package/dsh-setup.mjs +18 -3
  32. package/package.json +2 -2
  33. package/packages/dsh-remote-web/lib/client.js +1048 -223
  34. package/packages/dsh-remote-web/lib/index.js +299 -2
  35. package/packages/dsh-remote-web/package.json +1 -1
  36. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/README.md +412 -0
  37. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
  38. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
  39. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/codex.mjs +736 -0
  40. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/detect.mjs +399 -0
  41. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/events.mjs +511 -0
  42. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/gemini.mjs +503 -0
  43. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/index.mjs +339 -0
  44. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/opencode.mjs +628 -0
  45. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
  46. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/proc.mjs +179 -0
  47. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/version.mjs +148 -0
  48. package/packages/dsh-remote-web/runtime/clients/dsh-remote/approval-coordinator.mjs +150 -0
  49. package/packages/dsh-remote-web/runtime/clients/dsh-remote/decision-poller.mjs +235 -0
  50. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +258 -0
  51. package/packages/dsh-remote-web/runtime/clients/dsh-remote/push-reporter.mjs +533 -0
  52. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wecom-bot.mjs +2444 -0
  53. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +1 -1
  54. package/packages/dsh-remote-web/test/activation-entry.test.mjs +11 -1
  55. package/packages/dsh-remote-web/test/announcements-ui.test.mjs +8 -3
  56. package/packages/dsh-remote-web/test/connect-ui.test.mjs +16 -4
  57. package/packages/dsh-remote-web/test/im-bots.test.mjs +359 -0
  58. package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
  59. package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +13 -5
  60. package/packages/dsh-remote-web/test/panel-ia.test.mjs +798 -0
  61. package/packages/dsh-remote-web/test/password-reset.test.mjs +11 -1
  62. package/packages/dsh-remote-web/test/plugin-link.test.mjs +78 -0
  63. package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +10 -1
  64. package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +20 -8
  65. package/packages/dsh-remote-web/test/settings-entry.test.mjs +30 -21
  66. package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
  67. package/packages/dsh-remote-web/test/telemetry.test.mjs +5 -3
  68. 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() { hook = 0; return registered.get("dsh-remote")({ close() {} }); },
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
- return renderNode(root, "root");
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() { hook = 0; return registered.get("dsh-remote")({ close() {} }); },
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() { hook = 0; return registered.get("dsh-remote")({ close() {} }); },
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
- // 搬进去之后仍然存在:面板 tab 条里的第三个 tab + 微信绿泡泡图标(行为断言见 wechat-bot-ui.test.mjs)
216
- assert.match(SOURCE, /"微信机器人通道"/, "微信机器人 tab 必须在面板里");
217
- assert.match(SOURCE, /dru-wx-ico/, "微信 tab 必须带绿泡泡图标 class");
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 说明卡片(面板底部,v0.5+ 价值要点)
237
- assert.ok(find(tree, (n) => n.children?.includes("📖 关于 dsh-remote")), "应渲染「关于 dsh-remote」卡片标题");
238
- const points = [
239
- "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。",
240
- "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。",
241
- "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表。",
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
- // 面板主体仍在(连接模式 tab 不受影响)
249
- assert.ok(find(tree, (n) => n.children?.includes("☁️ 云端服务")), "云端服务 tab 应保留");
250
- assert.ok(find(tree, (n) => n.children?.includes("🖥 自建服务")), "自建服务 tab 应保留");
251
-
252
- // 📱 远程访问卡 / 📲 已授权设备卡在云端视图中渲染(bridge 未运行 → “等待设备连接”)
253
- assert.ok(find(tree, (n) => n.children?.includes("等待设备连接")), "无 bridge 时应显示「等待设备连接」状态行");
254
- assert.ok(textHas(tree, "已授权设备"), "应渲染「已授权设备」管理按钮");
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
- assert.match(source, /st !== null && !loggedIn/);
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
- const render = () => { hook = 0; return section({ close() {} }); };
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
- const aboutStart = client.indexOf('card("📖 关于 dsh-remote"');
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("\n // 【自动化】待重启时自动倒计时", aboutStart);
814
- assert.ok(aboutEnd > aboutStart, "必须能定位到「关于」卡片的结束边界(renderHome 末尾)");
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"), "切片必须真的包含「关于」卡片本体");