@mrrisega/dsh-remote 0.7.0-beta.8 → 0.7.1-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.
Files changed (71) hide show
  1. package/clients/dsh-remote/dsh-bridge.mjs +17 -17
  2. package/clients/dsh-remote/dsh-events.mjs +6 -6
  3. package/clients/dsh-remote/e2ee-client.mjs +7 -7
  4. package/clients/dsh-remote/e2ee-shim-script.js +9 -9
  5. package/clients/dsh-remote/mobile-adapter.mjs +294 -43
  6. package/clients/dsh-remote/test/bridge-gzip.test.mjs +1 -1
  7. package/clients/dsh-remote/test/device-registration-report.test.mjs +1 -1
  8. package/clients/dsh-remote/test/dsh-events.test.mjs +10 -10
  9. package/clients/dsh-remote/test/e2ee-bridge.test.mjs +4 -4
  10. package/clients/dsh-remote/test/e2ee-client.test.mjs +7 -7
  11. package/clients/dsh-remote/test/e2ee-shim.test.mjs +1 -1
  12. package/clients/dsh-remote/test/e2ee-wecrypto-parity.test.mjs +1 -1
  13. package/clients/dsh-remote/test/harness-cookie-authority.test.mjs +4 -4
  14. package/clients/dsh-remote/test/machine-fingerprint.test.mjs +2 -2
  15. package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +6 -6
  16. package/clients/dsh-remote/test/mobile-adapter-image.test.mjs +1 -1
  17. package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +5 -5
  18. package/clients/dsh-remote/test/mobile-adapter-settings.test.mjs +352 -0
  19. package/clients/dsh-remote/test/mobile-adapter.test.mjs +35 -0
  20. package/clients/dsh-remote/test/upstream-discovery.test.mjs +1 -1
  21. package/clients/dsh-remote/test/wechat-channel.test.mjs +5 -5
  22. package/clients/dsh-remote/test/wechat-runtime.test.mjs +26 -26
  23. package/clients/dsh-remote/test/wechat-tier-source.test.mjs +2 -2
  24. package/clients/dsh-remote/wechat-channel.mjs +9 -9
  25. package/clients/dsh-remote/wechat-runtime.mjs +38 -38
  26. package/docs/telemetry.md +3 -3
  27. package/dsh-setup.mjs +27 -27
  28. package/package.json +3 -3
  29. package/packages/dsh-remote-web/lib/client.js +56 -56
  30. package/packages/dsh-remote-web/lib/fix-prompt.js +1 -1
  31. package/packages/dsh-remote-web/lib/index.js +380 -59
  32. package/packages/dsh-remote-web/lib/tarball-update.js +1 -1
  33. package/packages/dsh-remote-web/package.json +1 -1
  34. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +17 -17
  35. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-events.mjs +6 -6
  36. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-client.mjs +7 -7
  37. package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +9 -9
  38. package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +294 -43
  39. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-channel.mjs +9 -9
  40. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wechat-runtime.mjs +38 -38
  41. package/packages/dsh-remote-web/runtime/clients/dsh-web/native.html +5 -5
  42. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +27 -27
  43. package/packages/dsh-remote-web/test/account-switch-device.test.mjs +2 -2
  44. package/packages/dsh-remote-web/test/activation-single-point.test.mjs +1 -1
  45. package/packages/dsh-remote-web/test/announcements-ui.test.mjs +1 -1
  46. package/packages/dsh-remote-web/test/connect-loop.test.mjs +1 -1
  47. package/packages/dsh-remote-web/test/connect-ui.test.mjs +2 -2
  48. package/packages/dsh-remote-web/test/desktop-compat.test.mjs +4 -4
  49. package/packages/dsh-remote-web/test/diag-report.test.mjs +1 -1
  50. package/packages/dsh-remote-web/test/fix-prompt.test.mjs +2 -2
  51. package/packages/dsh-remote-web/test/harness-restart.test.mjs +1 -1
  52. package/packages/dsh-remote-web/test/install-profile-targets.test.mjs +3 -3
  53. package/packages/dsh-remote-web/test/installer-cli.test.mjs +1 -1
  54. package/packages/dsh-remote-web/test/launchd-domain.test.mjs +2 -2
  55. package/packages/dsh-remote-web/test/patch-activation.test.mjs +2 -2
  56. package/packages/dsh-remote-web/test/picker-pin.test.mjs +1 -1
  57. package/packages/dsh-remote-web/test/plugin-manifest.test.mjs +2 -2
  58. package/packages/dsh-remote-web/test/quota-absent.test.mjs +4 -4
  59. package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +2 -2
  60. package/packages/dsh-remote-web/test/selfheal-restart-guard.test.mjs +420 -0
  61. package/packages/dsh-remote-web/test/settings-entry-placement.test.mjs +6 -6
  62. package/packages/dsh-remote-web/test/settings-entry.test.mjs +2 -2
  63. package/packages/dsh-remote-web/test/settings-open-path.test.mjs +4 -4
  64. package/packages/dsh-remote-web/test/setup-output.test.mjs +11 -11
  65. package/packages/dsh-remote-web/test/tarball-update.test.mjs +1 -1
  66. package/packages/dsh-remote-web/test/telemetry.test.mjs +4 -4
  67. package/packages/dsh-remote-web/test/uninstall-safety.test.mjs +2 -2
  68. package/packages/dsh-remote-web/test/watcher-upstream-port.test.mjs +1 -1
  69. package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +7 -7
  70. package/packages/dsh-remote-web/test/windows-compat.test.mjs +5 -5
  71. package/scripts/test-net-guard.cjs +1 -1
@@ -339,12 +339,12 @@ test("★ handleCommand 的字段是 replyText:回 /help 必须真的发出帮
339
339
  test("★ 分层:免费档 = 通知+审批,付费档才给遥控;未知档位按最低档", () => {
340
340
  const { capabilitiesFor, WECHAT_CAPABILITY_TABLE, grantsCap, limitsFor } = rtmod;
341
341
  const free = capabilitiesFor("free");
342
- // 免费档(业主口径):收得到通知、点得了审批、看得到状态、停得下任务 —— 但**遥控不了**。
342
+ // 免费档(用户约定):收得到通知、点得了审批、看得到状态、停得下任务 —— 但**遥控不了**。
343
343
  for (const cap of ["notify", "approve", "status", "stop"]) {
344
344
  assert.ok(free.includes(cap), `免费档必须有 ${cap}`);
345
345
  }
346
346
  for (const cap of ["assign", "sessions", "summary"]) {
347
- assert.ok(!free.includes(cap), `免费档**不得**有 ${cap}(业主:那是付费能力)`);
347
+ assert.ok(!free.includes(cap), `免费档**不得**有 ${cap}(用户:那是付费能力)`);
348
348
  }
349
349
  // 付费档 = 免费档超集 + 遥控
350
350
  const pro = capabilitiesFor("pro");
@@ -355,7 +355,7 @@ test("★ 分层:免费档 = 通知+审批,付费档才给遥控;未知档位按
355
355
  assert.ok(grantsCap(pro, cap), `付费档必须有 ${cap}`);
356
356
  }
357
357
 
358
- // ★★ 业主 2026-09-22 拍板的核心不变量:免费**能续接已有会话、不能开新会话**
358
+ // ★★ 用户 2026-09-22 拍板的核心不变量:免费**能续接已有会话、不能开新会话**
359
359
  assert.ok(grantsCap(free, "assign.continue"), "免费档必须能「继续已有会话」(回复即续接)");
360
360
  assert.ok(!grantsCap(free, "assign.new"), "★免费档**不得**能开新会话(那是会员能力的差异点)");
361
361
  assert.ok(!grantsCap(free, "sessions"), "免费档不得能切换会话");
@@ -368,7 +368,7 @@ test("★ 分层:免费档 = 通知+审批,付费档才给遥控;未知档位按
368
368
  assert.ok(!grantsCap(["assign"], "constructor"), "前缀规则不能被开头相同的无关词命中");
369
369
  assert.ok(!grantsCap(free, "steer") && !grantsCap(["whatever"], "assign.new"), "未知能力不授权");
370
370
 
371
- // 额度(业主:「免费给每月 N 条消息额度,超出再引导升级」;付费档不限)
371
+ // 额度(用户:「免费给每月 N 条消息额度,超出再引导升级」;付费档不限)
372
372
  assert.ok(Number(limitsFor("free").messages_per_month) > 0, "免费档必须有每月消息额度");
373
373
  assert.equal(Number(limitsFor("pro").messages_per_month || 0), 0, "付费档不应限量(0 = 不限)");
374
374
  assert.equal(Number(limitsFor("pro_max").messages_per_month || 0), 0, "最高档同样不限量");
@@ -402,7 +402,7 @@ test("★ 分层:免费档被挡且回复里带 App 链接;付费档放行;未
402
402
  const freeText = lastText(ilink.state);
403
403
  assert.match(freeText, /会员功能/, `要说清为什么被拒,实际:${freeText}`);
404
404
  assert.ok(freeText.includes(APP_URL),
405
- `★回复必须带上可点击的 App 链接(业主:免费用户要去做别的,就给他访问 App 的链接),实际:${freeText}`);
405
+ `★回复必须带上可点击的 App 链接(用户:免费用户要去做别的,就给他访问 App 的链接),实际:${freeText}`);
406
406
  assert.match(freeText, /\/help/, "还要告诉他现在能做什么,而不是死路一条");
407
407
 
408
408
  // ② 付费档:同一句话必须放行
@@ -543,9 +543,9 @@ test("★ v2:有当前会话时纯文本进当前会话(不是每次开新的)",
543
543
  }
544
544
  });
545
545
 
546
- test("★ v2 安全边界(业主拍板放宽):看不到内容照给一键回执,只看得出破坏性才降级", async () => {
546
+ test("★ v2 安全边界(用户拍板放宽):看不到内容照给一键回执,只看得出破坏性才降级", async () => {
547
547
  // 背景:DSH 的审批节点常常只下发 toolName、没有命令原文。原先「看不到内容也算破坏性」
548
- // 会把绝大多数正常 Bash 审批都降级成"只能回电脑确认" —— 过严。业主明确:DSH 自身有
548
+ // 会把绝大多数正常 Bash 审批都降级成"只能回电脑确认" —— 过严。用户明确:DSH 自身有
549
549
  // 权限控制,安全边界可以松一点。所以现在**只看内容**。
550
550
  const ilink = await fakeIlink();
551
551
  try {
@@ -647,7 +647,7 @@ test("★ v2:完成推送带会话名称 + 结论,而不只是「任务已停止
647
647
  });
648
648
 
649
649
  test("★ 0.6.15:完成推送必须把「当前会话」切到刚跑完的那个(否则回话会发进上一次的会话)", async () => {
650
- // 业主实测:B 任务跑完推了结论,他在微信里直接回一句"再改一下",那条消息却发给了
650
+ // 用户实测:B 任务跑完推了结论,他在微信里直接回一句"再改一下",那条消息却发给了
651
651
  // **上一次的当前会话 A** —— A 平白多了一条指令,B 永远收不到。
652
652
  const ilink = await fakeIlink();
653
653
  try {
@@ -819,7 +819,7 @@ test("★★ 日报发送失败时**不能**把当天标记为已发(否则当
819
819
  });
820
820
 
821
821
  test("★ 日报:晚上发(不是早上)、内容是「今天干了啥」、带品牌露出", async () => {
822
- // 业主:「每天早上发『今日干了啥事儿』,这肯定不太对劲。应该是晚上发,比如晚上 6 点,发『今天干了啥』」
822
+ // 用户:「每天早上发『今日干了啥事儿』,这肯定不太对劲。应该是晚上发,比如晚上 6 点,发『今天干了啥』」
823
823
  // 且一天只发一次(它兼作 24h 推送窗口的心跳,早晚各一次就太频繁了)。
824
824
  assert.equal(rtmod.DEFAULT_DIGEST_HOUR, 18, "日报时刻必须是晚上 18 点,不能是早上 9 点");
825
825
  const ilink = await fakeIlink();
@@ -847,7 +847,7 @@ test("★ 日报:晚上发(不是早上)、内容是「今天干了啥」、带
847
847
 
848
848
  test("★★ 会员到期提醒:提前 2 天 / 1 天 / 到期当天各提醒一次,同一天不重复", async () => {
849
849
  // 这条**以前根本不存在**:notifyMembershipExpiring() 只有定义、全仓零调用方 ——
850
- // 业主以为"提前两天发、提前一天也发",实际一次都没发过。这里锁住补上的触发点。
850
+ // 用户以为"提前两天发、提前一天也发",实际一次都没发过。这里锁住补上的触发点。
851
851
  const ilink = await fakeIlink();
852
852
  try {
853
853
  const DAY = 86_400_000;
@@ -888,7 +888,7 @@ test("★★ 会员到期提醒:提前 2 天 / 1 天 / 到期当天各提醒一
888
888
  }
889
889
  });
890
890
 
891
- test("★★ A:免费档「能续接已有会话、不能开新会话」(业主拍板的分级)", async () => {
891
+ test("★★ A:免费档「能续接已有会话、不能开新会话」(用户拍板的分级)", async () => {
892
892
  const ilink = await fakeIlink();
893
893
  try {
894
894
  // ① 免费 + **没有**当前会话 → 纯文本等于开新任务 → 必须被拒
@@ -906,7 +906,7 @@ test("★★ A:免费档「能续接已有会话、不能开新会话」(业主
906
906
  b.rt.currentSessionTitle = "已有任务";
907
907
  await b.rt.handleInbound({ from_user_id: "u", item_list: [{ type: 1, text_item: { text: "继续修" } }] });
908
908
  const prompted = subB.calls.filter((c) => c.m === "promptSession");
909
- assert.equal(prompted.length, 1, "★免费 + 有当前会话:必须能继续聊(业主要的就是这个)");
909
+ assert.equal(prompted.length, 1, "★免费 + 有当前会话:必须能继续聊(用户要的就是这个)");
910
910
  assert.equal(prompted[0].text, "继续修", "内容要原样下发");
911
911
 
912
912
  // ③ 免费仍不能开新会话、不能切换会话
@@ -993,7 +993,7 @@ test("★★ 权益包:服务端下发的 caps/limits 覆盖内置表(后台改
993
993
  });
994
994
 
995
995
  test("★★ 多条待审批:回数字必须答**最早那条**(先进先出),答完把下一条顶到底部", async () => {
996
- // 业主报的真 bug:「如果需要用户回应多条消息,而用户一开始只看到第一条审批消息,
996
+ // 用户报的真 bug:「如果需要用户回应多条消息,而用户一开始只看到第一条审批消息,
997
997
  // 回应一个数字之后,流程就结束了。后面的几条消息没有让用户继续回应,直接提示为『未回应』」
998
998
  // 成因:旧实现按**栈顶(最新)**匹配数字,而微信是**从上往下读**的、消息位置一发出去就固定
999
999
  // → 用户看着第 1 条回「1」,系统答了最后一条,第 1 条永远没人回应,DSH 最终标它「未回应」。
@@ -1535,7 +1535,7 @@ test("★ 多审批 + 提醒混排:回执仍按 FIFO 走审批(提醒不参与
1535
1535
  // 原先这条路径透传上游的静态 HELP_TEXT —— 那份清单把 /new、/ls、/use、/summary 与
1536
1536
  // "直接发一句话"一并列成"你能用的"。免费用户敲错一个字,拿到的就是这张**会员清单**,
1537
1537
  // 照着做再吃一次拒绝,于是"打错命令"被理解成"这功能坏了"。
1538
- // 业主红线:话术与权限必须一致、不多承诺。
1538
+ // 用户红线:话术与权限必须一致、不多承诺。
1539
1539
  // ---------------------------------------------------------------------------
1540
1540
 
1541
1541
  test("★ 免费用户敲未知指令:要回显命令 + 给**分层**帮助,不得把会员指令列成『你能用的』", async () => {
@@ -1719,7 +1719,7 @@ test("★ /help 必须逐行分隔:付费档也不许退化成一坨(无换行/
1719
1719
  });
1720
1720
 
1721
1721
  // ---------------------------------------------------------------------------
1722
- // 日报**重复推送**的根治(2026-09-23 业主实测「每日简报推送了两次」)
1722
+ // 日报**重复推送**的根治(2026-09-23 用户实测「每日简报推送了两次」)
1723
1723
  //
1724
1724
  // 根因:`#maybeDigest` 是「读 state → 等网络发送 → 写 state」的**跨 await 读-改-写**。
1725
1725
  // 发送要等网络,这段窗口里第二个执行者会读到"今天还没发" → 两条都发出去。
@@ -1798,7 +1798,7 @@ test("★ 日报占位一天一个,换天时会清掉旧占位(不攒文件)", a
1798
1798
  });
1799
1799
 
1800
1800
  // ---------------------------------------------------------------------------
1801
- // 出站必须回带 context_token(2026-09-23 业主实测「没收到微信通道」)
1801
+ // 出站必须回带 context_token(2026-09-23 用户实测「没收到微信通道」)
1802
1802
  //
1803
1803
  // 平台要求出站消息带回「这条会话的上下文令牌」,而它只出现在**入站消息**里
1804
1804
  // (官方实现:messaging/send.js 的 buildTextMessageReq → `msg.context_token`;
@@ -1831,7 +1831,7 @@ test("★ 没有 context_token 时**完全不带这个键**(而不是空串)", a
1831
1831
  await rt.handleInbound({ from_user_id: "user-1", item_list: [{ type: 1, text_item: { text: "/status" } }] });
1832
1832
  const last = ilink.state.sends.at(-1);
1833
1833
  assert.ok(!("context_token" in last.msg),
1834
- "★拿不到令牌时不得塞空串(与官方 `contextToken ?? undefined` 同口径)");
1834
+ "★拿不到令牌时不得塞空串(与官方 `contextToken ?? undefined` 同约定)");
1835
1835
  } finally {
1836
1836
  await ilink.close();
1837
1837
  }
@@ -1887,14 +1887,14 @@ test("★ extractContextToken 防御式:形状不认识一律当没有,绝不猜
1887
1887
  assert.equal(extractContextToken({ contextToken: "CT-2" }), "CT-2", "兼容驼峰写法");
1888
1888
  });
1889
1889
 
1890
- // ═════════════ 2026-09-25 事故:完成推送"消失" + 回话跑进另一个会话 ═════════════
1890
+ // ═════════════ 2026-09-25 故障:完成推送"消失" + 回话跑进另一个会话 ═════════════
1891
1891
  //
1892
- // 现场(业主):任务跑完一条微信都没收到;他在微信里回了一句,结果**发进了上一个会话**。
1892
+ // 现场(用户):任务跑完一条微信都没收到;他在微信里回了一句,结果**发进了上一个会话**。
1893
1893
  // 日志证据:48 条 `sendmessage 失败:sendMessage: ret=-2 errmsg=prepare failed`
1894
1894
  // —— 平台侧发送是**间歇性**失败的,而旧实现失败即丢弃:那条"任务跑完了"永远消失了。
1895
1895
  // 状态证据:`current_session_id` 停在几天前的旧会话上(因为切换只在推送成功后发生)。
1896
1896
 
1897
- test("★ 事故锁:主动推送失败**不能消失** —— 入队,并在下一次入站时补发(带「补发」标记)", async () => {
1897
+ test("★ 回归锁:主动推送失败**不能消失** —— 入队,并在下一次入站时补发(带「补发」标记)", async () => {
1898
1898
  let failSends = true;
1899
1899
  const ilink = await fakeIlink({
1900
1900
  onSendMessage: (_state, reply) => (failSends
@@ -1926,7 +1926,7 @@ test("★ 事故锁:主动推送失败**不能消失** —— 入队,并在
1926
1926
  }
1927
1927
  });
1928
1928
 
1929
- test("★ 事故锁:静音(/quiet)也要记下「最近完成的任务」,回话不能跑进上一个会话", async () => {
1929
+ test("★ 回归锁:静音(/quiet)也要记下「最近完成的任务」,回话不能跑进上一个会话", async () => {
1930
1930
  const ilink = await fakeIlink();
1931
1931
  try {
1932
1932
  const sub = makeFakeSubscriber();
@@ -1958,7 +1958,7 @@ test("★ 事故锁:静音(/quiet)也要记下「最近完成的任务」
1958
1958
  }
1959
1959
  });
1960
1960
 
1961
- test("★ 事故锁:用户主动 /use 切走的会话不能被「最近完成」抢回去", async () => {
1961
+ test("★ 回归锁:用户主动 /use 切走的会话不能被「最近完成」抢回去", async () => {
1962
1962
  const ilink = await fakeIlink();
1963
1963
  try {
1964
1964
  const sub = makeFakeSubscriber();
@@ -1972,7 +1972,7 @@ test("★ 事故锁:用户主动 /use 切走的会话不能被「最近完成
1972
1972
  assert.equal(rt.currentSessionId, "session-mine", "前置条件:/use 切到了 2 号");
1973
1973
  await rt.notify({ kind: NODE_KINDS.TURN_END, sessionId: "session-done", reason: "completed", at: 3 });
1974
1974
 
1975
- // 完成事件在处理时会把它自己设为回复目标(这是产品口径);但用户**之后**再切回去要算数:
1975
+ // 完成事件在处理时会把它自己设为回复目标(这是产品约定);但用户**之后**再切回去要算数:
1976
1976
  await rt.handleInbound({ from_user_id: "u", item_list: [{ type: 1, text_item: { text: "/use 2" } }] });
1977
1977
  await rt.handleInbound({ from_user_id: "u", item_list: [{ type: 1, text_item: { text: "继续聊我的" } }] });
1978
1978
  const prompt = sub.calls.filter((c) => c.m === "promptSession").pop();
@@ -1984,7 +1984,7 @@ test("★ 事故锁:用户主动 /use 切走的会话不能被「最近完成
1984
1984
  });
1985
1985
 
1986
1986
  test("★ 待补发队列:过期的**不发**(丢弃最早的),且只保留最近几条", async () => {
1987
- // 业主 2026-10-09 口径:「积压超过一定时限后就丢弃掉最早的数据,只保留最近几条」。
1987
+ // 用户 2026-10-09 约定:「积压超过一定时限后就丢弃掉最早的数据,只保留最近几条」。
1988
1988
  // 两条理由:① 迟到几小时的"任务跑完了"只会误导用户;② 攒到十几条会在平台恢复时一次连打,
1989
1989
  // 实测正是这种突发批量触发 `ret=-2 prepare failed` 被限流 → 补发又失败 → 队列再涨(死循环)。
1990
1990
  const ilink = await fakeIlink();
@@ -2054,7 +2054,7 @@ test("★ 待补发队列:超过上限只留最近几条(丢最早,且如
2054
2054
  });
2055
2055
 
2056
2056
 
2057
- test("★ 事故锁:订阅 fault 必须落到日志 + 状态(旧实现只 emit、没人接 → 全静默)", async () => {
2057
+ test("★ 回归锁:订阅 fault 必须落到日志 + 状态(旧实现只 emit、没人接 → 全静默)", async () => {
2058
2058
  // 那次「一条推送都没有、日志里也没有任何报错」的根因就在这类静默里:
2059
2059
  // `#emitFault` 只 emit('fault'),而运行时没接这个事件 —— follow 失败/发现失败/流上限
2060
2060
  // 全都不留任何痕迹。现在必须:① 一条 warn 进 bridge 日志;② 非预期的记进状态文件。
@@ -2079,7 +2079,7 @@ test("★ 事故锁:订阅 fault 必须落到日志 + 状态(旧实现只 em
2079
2079
  const st2 = JSON.parse(fs.readFileSync(path.join(relayDir, ".wechat-state.json"), "utf8"));
2080
2080
  assert.equal(st2.last_fault.code, "follow-limit", "expected 的异常不该覆盖真正的故障");
2081
2081
  // ★2026-10-09:预期内的拒绝**不能写成 warn** —— 每生成一个子代理就会来一条,
2082
- // 成片的 `订阅异常 session/agent-busy …` 会把真正的故障淹掉(业主现场看到的就是这个)。
2082
+ // 成片的 `订阅异常 session/agent-busy …` 会把真正的故障淹掉(用户现场看到的就是这个)。
2083
2083
  assert.equal(warns.length, warnCountBefore, `expected 的拒绝不得写 warn,实际:${JSON.stringify(warns)}`);
2084
2084
  assert.ok(infos.some((m) => m.includes("订阅跳过(预期") && m.includes("session=sub-1")),
2085
2085
  `expected 的拒绝要有一条 info 留痕(绝不静默),实际:${JSON.stringify(infos)}`);
@@ -45,7 +45,7 @@ function loadTierResolver({ localKey = "", token = "tok-1", status = 200, body =
45
45
  if (throws) throw new Error("network down");
46
46
  return { ok: status >= 200 && status < 300, status, json: async () => bodyFor() };
47
47
  };
48
- // 多出口两个"内部状态"的只读探针 —— 否则"token 缓存被清空""快照不因缺 user 被覆盖"
48
+ // 多出口两个"内部状态"的只读探测 —— 否则"token 缓存被清空""快照不因缺 user 被覆盖"
49
49
  // 这两条只能用间接行为猜,抓不住回归(它们都在闭包里)。
50
50
  const make = new Function("process", "resolveToken", "API_BASE", "fetch", "AbortSignal", "Date",
51
51
  `${BRIDGE_SRC.slice(from, to)}
@@ -145,7 +145,7 @@ test("★档位来源:启动时必须复用桥接刚拿到的 JWT(否则每
145
145
  assert.ok(seedAt < startAt, "★必须在 startWeChat() **之前**喂,否则档位校准仍会再登录一次");
146
146
  });
147
147
 
148
- test("App 链接:由账号 API 地址推导,与插件半的 DEFAULT_APP_URL 同口径", () => {
148
+ test("App 链接:由账号 API 地址推导,与插件半的 DEFAULT_APP_URL 同约定", () => {
149
149
  assert.equal(loadAppUrl(API_BASE)(), "https://api.example/app/");
150
150
  assert.equal(loadAppUrl("https://n.risegao.cn:13443/relay-api")(), "https://n.risegao.cn:13443/app/",
151
151
  "生产地址必须推导出真实可点的 App 入口");
@@ -83,7 +83,7 @@ export function resolveClientVersion(raw) {
83
83
 
84
84
  /**
85
85
  * 数值协议常量 —— **逐个抄自归档参考实现**,不是猜的:
86
- * /Users/mac/AIWorkSpace/myFreeWork/dsh-relay-internal/reference/openclaw-weixin/plugin-2.4.9/src/api/types.ts
86
+ * OpenClaw 微信插件(第三方实现,仅作字段命名参考)
87
87
  * · MessageType (types.ts:64-68) NONE:0 / USER:1 / BOT:2
88
88
  * · MessageItemType (types.ts:70-79) NONE:0 TEXT:1 IMAGE:2 VOICE:3 FILE:4 VIDEO:5
89
89
  * TOOL_CALL_START:11 TOOL_CALL_RESULT:12
@@ -702,7 +702,7 @@ export class IlinkClient {
702
702
  // **入站消息**里(官方实现见 messaging/send.js 的 buildTextMessageReq:`msg.context_token`;
703
703
  // 腾讯自家插件同样是「从入站取出 → 按 (账号, 用户) 缓存 → 发送时回带」)。
704
704
  // 🔴 我们以前**从不发它** → 真机症状就是日志里的 `ret=-2 errmsg=prepare failed`:
705
- // 消息发不出去(业主 2026-09-23:「没收到微信通道」)。
705
+ // 消息发不出去(用户 2026-09-23:「没收到微信通道」)。
706
706
  // ⚠️ 只在"有令牌且属于同一个对端"时带上,绝不拿 A 的上下文发给 B。
707
707
  const peer = String(to);
708
708
  // 本产品一个 bot 只绑定一个微信用户,所以只要有令牌就回带;`contextTokenPeer` 仅作诊断。
@@ -715,7 +715,7 @@ export class IlinkClient {
715
715
  message_type: MessageType.BOT,
716
716
  message_state: MessageState.FINISH,
717
717
  item_list: text ? [{ type: MessageItemType.TEXT, text_item: { text: String(text) } }] : [],
718
- // 没有就**不出现这个键**(不是空串)—— 与官方 `contextToken ?? undefined` 同口径
718
+ // 没有就**不出现这个键**(不是空串)—— 与官方 `contextToken ?? undefined` 同约定
719
719
  ...(ctxToken ? { context_token: ctxToken } : {})
720
720
  };
721
721
  const { json } = await this.request({
@@ -1774,7 +1774,7 @@ export function markUnbound(relayDir, reason = "", onWarn) {
1774
1774
  export const DEFAULT_REPLY_TTL_MINUTES = 5;
1775
1775
 
1776
1776
  /**
1777
- * 完成通知里「结论」的展示上限(字)。业主口径(2026-09-22):
1777
+ * 完成通知里「结论」的展示上限(字)。用户约定(2026-09-22):
1778
1778
  * 「任务完成通知里有些关键结论被大量截断了,加长一些,更完整地展示最终结论 ——
1779
1779
  * 这可能是用户关注的内容,所以都给他们展示出来」。
1780
1780
  * 原值 400 对真实结论太短(实测一条正常的修复总结就有 190+ 字,稍详细就超)。
@@ -1898,7 +1898,7 @@ export function formatNotification(node = {}, opts = {}) {
1898
1898
  for (const l of Array.isArray(node.lines) ? node.lines : []) lines.push(String(l));
1899
1899
  // 措辞对齐"回顾今天做了什么" —— 日报不是待办清单
1900
1900
  if (!Array.isArray(node.lines) || !node.lines.length) lines.push("今天这台电脑上没有跑任务。");
1901
- // 品牌轻露出(业主:让用户知道我们在给他提供服务)。**克制一行**、放在最底,不喧宾夺主。
1901
+ // 品牌轻露出(用户:让用户知道我们在给他提供服务)。**克制一行**、放在最底,不喧宾夺主。
1902
1902
  lines.push("", "—— DSH 远程控制 · 微信机器人通道");
1903
1903
  // ⚠️ 这句是**功能**不是客套:微信 24h 推送窗口靠用户回消息续期,
1904
1904
  // 简报的产品作用正是每天制造一次互动(§7)。必须出现「回复」字样,否则用户不会回。
@@ -1974,7 +1974,7 @@ function clipText(s, n) {
1974
1974
  * 为什么必须转:微信**不渲染 Markdown**(最强证据是腾讯自己的插件在出站路径跑
1975
1975
  * `StreamingMarkdownFilter` 主动剥离"不支持的 markdown 语法")。所以模型写的表格会以
1976
1976
  * `| a | b |` 的**字面竖线**出现在气泡里、`**加粗**` 会显示成星号。
1977
- * 业主原话:「现在的表格形式看起来很奇怪,整个格式在微信上没有做过任何兼容,体验很差很差」
1977
+ * 用户原话:「现在的表格形式看起来很奇怪,整个格式在微信上没有做过任何兼容,体验很差很差」
1978
1978
  * —— 根因就在这里,不是"排版没调好"。
1979
1979
  *
1980
1980
  * 规则(只改"怎么显示",不改语义):
@@ -2365,10 +2365,10 @@ export function isDestructiveTool(toolName, detail) {
2365
2365
  if (DESTRUCTIVE_TOOL_NAME_RE.test(name)) return true;
2366
2366
 
2367
2367
  const text = flattenToolDetail(detail);
2368
- // ⚠️ 2026-09-22 按业主决定**删掉**了原来的规则③:「能跑命令的工具 + 完全看不到内容 → 算破坏性」。
2368
+ // ⚠️ 2026-09-22 按用户决定**删掉**了原来的规则③:「能跑命令的工具 + 完全看不到内容 → 算破坏性」。
2369
2369
  // 原意是防"盲批",但 DSH 的审批节点本来就只下发 toolName、常常没有命令原文,
2370
2370
  // 那条规则会把**绝大多数正常 Bash 审批**都降级成"只能回电脑确认",过严且伤体验。
2371
- // 业主明确:安全边界松一点没问题,**DSH 自身有权限控制**。
2371
+ // 用户明确:安全边界松一点没问题,**DSH 自身有权限控制**。
2372
2372
  // 所以现在只看**内容**:看得出是 rm -rf / 强推 / DROP TABLE / 批量覆盖这类才拦;
2373
2373
  // 看不到内容时按普通审批处理(保留一步回执)。SHELL_TOOL_NAME_RE 仍保留,
2374
2374
  // 供将来若要恢复"盲批保护"时使用,并由测试固化其语义。
@@ -2834,7 +2834,7 @@ export class WeChatChannel {
2834
2834
  // 冷却原本只按"撞到 -14"计时,但**换绑会拿到全新的 bot_token** ——
2835
2835
  // 旧 token 的会话超时对新 token 毫无意义。不清的后果:用户解绑→重绑拿到新 token,
2836
2836
  // 却仍被旧 token 的 1 小时冷却挡着,表现成"刚绑上就一小时内不能聊天",
2837
- // 而且面板只说"通知会延迟",用户完全不知道为什么。(业主实测撞到)
2837
+ // 而且面板只说"通知会延迟",用户完全不知道为什么。(用户实测撞到)
2838
2838
  this.cooldown.clear();
2839
2839
  return this.uiAccount();
2840
2840
  }