dsh-remote-web 0.6.10-beta.1 → 0.6.10-beta.10

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/lib/client.js CHANGED
@@ -40,6 +40,28 @@ window.__ModuleLoader__.load({
40
40
  var exports = module.exports;
41
41
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
42
42
 
43
+ // ── 微信通道能力**镜像**(面板展示用;判定真身在 clients/dsh-remote/wechat-runtime.mjs)──
44
+ //
45
+ // ⚠️ 这两份必须一致,`wechat-bot-ui.test.mjs` 会逐项比对。
46
+ // 面板多承诺一项,用户绑完去微信一试就会拿到付费提示 —— 只会觉得产品骗人。
47
+ // 业主口径:「免费用户支持什么能力,就有什么能力,不要承诺过多的能力,保持前后一致」。
48
+ // 之所以放在顶层常量而不是内联在 JSX 里:测试要能把它们抠出来跟运行时表对账。
49
+ var WECHAT_FREE_CAPS = ["notify", "approve", "status", "stop", "assign.continue"];
50
+ // 付费侧也用**细粒度**列出:面板要能跟"文案挂在哪个能力上"一一对上,
51
+ // 而运行时那边写粗粒度 "assign" —— 前缀规则下 assign 天然覆盖 assign.new/assign.continue,
52
+ // 所以两边不冲突(有测试按前缀规则核对)。
53
+ var WECHAT_PAID_CAPS = ["notify", "approve", "status", "stop", "assign.new", "assign.continue", "sessions", "summary"];
54
+ var WECHAT_FREE_MSGS_PER_MONTH = 20;
55
+ /** 能力 → 给用户看的一句话(顺序即展示顺序)。 */
56
+ var WECHAT_CAP_LABELS = [
57
+ { cap: "notify", free: "任务完成 / 出错 / 停下 推送到微信", paid: "" },
58
+ { cap: "approve", free: "要你拍板时,回一个数字就完成决定", paid: "" },
59
+ { cap: "assign.continue", free: "继续已有任务(回复即续接)", paid: "" },
60
+ { cap: "assign.new", free: "", paid: "在微信里开新任务" },
61
+ { cap: "sessions", free: "", paid: "列出 / 切换会话" },
62
+ { cap: "summary", free: "", paid: "随时重发重要结论" }
63
+ ];
64
+
43
65
  var react = require("react");
44
66
  var h = react.createElement;
45
67
  var useState = react.useState;
@@ -4442,15 +4464,71 @@ window.__ModuleLoader__.load({
4442
4464
 
4443
4465
  // ── 首屏「这功能是干什么用的」导览(业主口径:先讲清用途,它就是绑定的理由) ──────────
4444
4466
  // 位置:内嵌内容体的**最上面**,在「连接微信机器人」按钮之前(DOM 顺序也一样)——
4445
- // 第一次进来的人先看懂用途,再决定要不要绑。三行 + 一行小标题,紧凑小字,
4467
+ // 第一次进来的人先看懂用途,再决定要不要绑。紧凑小字,
4446
4468
  // 不会把连接按钮挤出首屏(见 .dru-wx-intro 的样式注释)。
4447
- // 只讲**产品真的会做的事**(推送 / 回数字拍板 / 微信里交代任务),不写承诺以外的话。
4469
+ //
4470
+ // ★ 能力对照是**数据驱动**的,不再是写死的四行:
4471
+ // · 判定来源 = `wx.caps`(微信通道**实际在用**的那一份,见 status()),
4472
+ // 所以"面板上写的"与"微信里强制的"不可能不一致(业主:话术与权限必须一致);
4473
+ // · 拿不到才退回本地面板镜像(旧版 bridge),两份由 wechat-bot-ui.test.mjs 锁死;
4474
+ // · 免费用户会看到**锁定的会员项**(🔒)—— 这就是业主要的"对比展示付费多什么"。
4448
4475
  // 已绑定态同样保留:它是常驻的用途说明(不是一次性引导气泡),不占用任何按钮。
4476
+ var wxPlan = (wx && wx.plan) ? String(wx.plan) : "free";
4477
+ var wxIsPaid = wxPlan !== "free";
4478
+ // ★ 判据是「wx.caps 是不是数组」,**空数组也算权威**(与服务端/bridge 同一口径):
4479
+ // 通道下发 `caps: []` = 后台把这一档清空了(契约 §4.1)。若当成"没给"而回退本地面板镜像,
4480
+ // 面板就会展示出一个用户**实际没有**的能力清单 —— 展示与判定分叉,正是业主红线。
4481
+ var wxCaps = (wx && Array.isArray(wx.caps)) ? wx.caps : null;
4482
+ var wxHas = function (cap) {
4483
+ var list = wxCaps || (wxIsPaid ? WECHAT_PAID_CAPS : WECHAT_FREE_CAPS);
4484
+ return list.some(function (c) { return c === cap || cap.indexOf(c + ".") === 0; });
4485
+ };
4486
+ var capRows = [];
4487
+ // 状态还没读到时**不显示锁**:否则付费用户会先看到一排 🔒 再变成 ✅(闪一下很难看)
4488
+ if (wx) {
4489
+ WECHAT_CAP_LABELS.forEach(function (row, i) {
4490
+ var text = row.free || row.paid;
4491
+ if (!text) return;
4492
+ var ok = wxHas(row.cap);
4493
+ // ★ 锁定的说法要看**当前档位**:
4494
+ // · 免费用户看到一个用不了的能力 → 「(会员)」是准确的(这正是升级理由)
4495
+ // · 付费用户看到一个用不了的能力 → 那是**后台把他的这项关了**,
4496
+ // 再写「(会员)」就是对着会员说"这是会员功能",纯属胡话。
4497
+ // 改成「(当前套餐不含)」,与后台配置保持一致(业主:话术与权限必须一致)。
4498
+ var lockNote = wxIsPaid ? "(当前套餐不含)" : "(会员)";
4499
+ capRows.push(h("div", { className: "dru-wx-intro-row", key: "cap" + i },
4500
+ (ok ? "✅ " : "🔒 ") + text + (ok ? "" : lockNote)));
4501
+ });
4502
+ } else {
4503
+ WECHAT_CAP_LABELS.forEach(function (row, i) {
4504
+ var text = row.free || row.paid;
4505
+ if (text) capRows.push(h("div", { className: "dru-wx-intro-row", key: "cap" + i }, "· " + text));
4506
+ });
4507
+ }
4508
+ // ★ 额度也必须是**通道实际在用的那一份**(`wx.limits`,来自服务端权益包):
4509
+ // 写死镜像的话,后台把免费额度从 20 改成 5,面板仍会说"20 条" ——
4510
+ // 用户按面板的承诺去发,第 6 条就被拦,只会认为产品坏了。
4511
+ // 拿不到 `wx.limits`(旧版 bridge / 状态没读到)才退回本地面板镜像。
4512
+ var wxLimitMsgs = (wx && wx.limits && typeof wx.limits.messages_per_month === "number")
4513
+ ? Number(wx.limits.messages_per_month)
4514
+ : WECHAT_FREE_MSGS_PER_MONTH;
4515
+ // 不再按"是不是付费"决定显不显示:付费档也可能是有限额度(后台可配),
4516
+ // 一律「额度 > 0 就如实写出来,0 = 不限就不提」。
4517
+ // 用量同理:**没有这个字段就不说** —— 旧版 bridge 不上报用量,补一句
4518
+ // 「本月已用 0 条」等于替用户编了一个事实(他可能已经用掉 8 条)。
4519
+ var wxUsed = (wx && typeof wx.messages_used_this_month === "number")
4520
+ ? Number(wx.messages_used_this_month)
4521
+ : null;
4522
+ var quotaRow = (wxLimitMsgs > 0)
4523
+ ? h("div", { className: "dru-wx-intro-row" },
4524
+ "· 每月 " + wxLimitMsgs + " 条消息额度"
4525
+ + (wxUsed === null ? "" : "(本月已用 " + wxUsed + " 条)"))
4526
+ : null;
4449
4527
  var intro = h("div", { className: "dru-wx-intro", role: "note", "aria-label": "微信机器人通道能做什么" },
4450
- h("div", { className: "dru-wx-intro-title" }, "绑定后能做什么"),
4451
- h("div", { className: "dru-wx-intro-row" }, "· 任务完成 / 出错 / 停下时**推送**到微信 —— 不用守着电脑。"),
4452
- h("div", { className: "dru-wx-intro-row" }, "· 需要你拍板时,在微信里**回一个数字**就完成决定(放行 / 拒绝 / 选哪个)。"),
4453
- h("div", { className: "dru-wx-intro-row" }, "· 也可以**直接在微信里交代任务**,并对同一个任务继续追问。")
4528
+ h("div", { className: "dru-wx-intro-title" },
4529
+ wx ? (wxIsPaid ? "你现在能用的(会员)" : "你现在能用的(免费)") : "绑定后能做什么"),
4530
+ capRows,
4531
+ quotaRow
4454
4532
  );
4455
4533
 
4456
4534
  if (loading && !wx && !fail) {
package/lib/index.js CHANGED
@@ -90,6 +90,20 @@ const DEFAULT_API = String(process.env.DSH_RELAY_DEFAULT_API || "").trim()
90
90
  || "https://n.risegao.cn:13443/relay-api";
91
91
  const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
92
92
 
93
+ /**
94
+ * 自启动服务(launchd plist / systemd unit)与子进程使用的 PATH。
95
+ *
96
+ * ⚠️ **必须含 `/usr/sbin` 与 `/sbin`**:它们是系统管理命令所在目录(`ioreg`、`sysctl`…),
97
+ * 而 bridge 会用这些命令探测本机信息。
98
+ * 真机实证(2026-09-22):本文件此前**硬编码**了不含 `/usr/sbin` 的 PATH,而插件每次
99
+ * 自愈/启动都会重写 plist —— 于是它覆盖掉了 dsh-setup.mjs 里那份**正确**的 SERVICE_PATH,
100
+ * 结果 bridge 日志里刷 `/bin/sh: ioreg: command not found`,机器指纹静默退化成
101
+ * hostname 哈希(用户改一次主机名就被当成新设备)。
102
+ * ⚠️ 与 `dsh-setup.mjs` 的 `SERVICE_PATH` **必须一致** —— 两处都会写同一个 plist,
103
+ * 谁写谁说了算;有测试锁死这条一致性(跨文件不变量)。
104
+ */
105
+ const SERVICE_PATH = "/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin";
106
+
93
107
  /**
94
108
  * 把文件权限收紧到「只有本人可读写」。
95
109
  *
@@ -226,7 +240,9 @@ function spawnEnv(extra) {
226
240
  // PATH 分隔符必须按平台取(Windows 是 ";",POSIX 是 ":"):写死 ":" 会把整条 PATH
227
241
  // 在 Windows 上拼成一个不存在的路径,node/npx 全部找不到。
228
242
  // 追加的 POSIX 目录只在类 Unix 上有意义,Windows 上不追加。
229
- const posixDirs = isWindows() ? [] : ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin"];
243
+ // 含 /usr/sbin 与 /sbin:插件用 spawnEnv 拉起的是 watcher,它会再拉起 bridge,
244
+ // 而 bridge 要用 ioreg 之类系统命令(缺了它 → 机器指纹静默退化,见 SERVICE_PATH 注释)。
245
+ const posixDirs = isWindows() ? [] : ["/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
230
246
  const PATH = [nodeDir, base, ...posixDirs].filter(Boolean).join(delimiter);
231
247
  return { ...process.env, PATH, ...(extra || {}) };
232
248
  }
@@ -1007,7 +1023,18 @@ function ensureRuntime(relayDir) {
1007
1023
  }
1008
1024
  }
1009
1025
 
1010
- /** 生成 plist(与 dsh-setup.mjs writeAutostartFile 同构),返回路径。 */
1026
+ /**
1027
+ * 生成 plist(与 dsh-setup.mjs 的 writeAutostartFile **必须同构**),返回路径。
1028
+ *
1029
+ * ⚠️ 两份模板历史上漂移过、并因此各造成一个真机事故(插件每次自愈/启动都会重写 plist,
1030
+ * 所以**插件这份谁写谁说了算**,漂移等于把安装器那份正确的覆盖掉):
1031
+ * · 漏 `/usr/sbin`、`/sbin`(SERVICE_PATH)→ bridge 找不到 `ioreg` → 机器指纹静默退化成
1032
+ * hostname 哈希(改一次主机名就被当成新设备);
1033
+ * · 漏 `LimitLoadToSessionType` → `bootstrap user/<uid>` 失败(rc=5)→ 域阶梯回退到
1034
+ * gui/<uid>,而 macOS 26 上 gui 域是 on-demand-only、`RunAtLoad`/`KeepAlive` 失效 →
1035
+ * **服务被系统(BTM)回收后再也回不来**,只能手动点面板「启动」。
1036
+ * 两者都由 launchd-domain.test.mjs 的跨文件不变量锁死。
1037
+ */
1011
1038
  function writeAutostartFile(relayDir) {
1012
1039
  const plistPath = launchAgentPath();
1013
1040
  if (!plistPath) return null;
@@ -1021,9 +1048,10 @@ function writeAutostartFile(relayDir) {
1021
1048
  <key>RunAtLoad</key><true/>
1022
1049
  <key>KeepAlive</key><true/>
1023
1050
  <key>ThrottleInterval</key><integer>10</integer>
1051
+ <key>LimitLoadToSessionType</key><array><string>Aqua</string><string>Background</string></array>
1024
1052
  <key>StandardOutPath</key><string>${join(relayDir, ".dsh-bridge.log")}</string>
1025
1053
  <key>StandardErrorPath</key><string>${join(relayDir, ".dsh-bridge.log")}</string>
1026
- <key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string><key>DSH_BRIDGE_INSTALL_SOURCE</key><string>${installSourceOf(relayDir)}</string><key>DSH_BRIDGE_INSTALL_VERSION</key><string>${PLUGIN_VERSION}</string></dict>
1054
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${SERVICE_PATH}</string><key>DSH_BRIDGE_INSTALL_SOURCE</key><string>${installSourceOf(relayDir)}</string><key>DSH_BRIDGE_INSTALL_VERSION</key><string>${PLUGIN_VERSION}</string></dict>
1027
1055
  </dict></plist>`;
1028
1056
  mkdirSync(dirname(plistPath), { recursive: true });
1029
1057
  writeFileSync(plistPath, plist, { mode: 0o644 });
@@ -2452,8 +2480,27 @@ const PLUGIN_ID = "dsh-remote-web";
2452
2480
  /** 更名前 id(≤0.4.9):彻底卸载/清理时一并移除,防旧拷贝残留。 */
2453
2481
  const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
2454
2482
  const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
2455
- /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
2456
- const PLUGIN_VERSION = "0.6.10-beta.1";
2483
+ /**
2484
+ * 插件自身发布版本 —— **从 package.json 派生,不再手写**(与 dsh-remote 根包同步递增)。
2485
+ *
2486
+ * 为什么改成派生(2026-09-23):原先这里是硬编码字符串,每次发版要**手改两处**
2487
+ * (package.json + 这里),beta.8 就漏改了 —— 装到用户机器上的插件在**装机上报**里
2488
+ * 仍然自称 beta.7。而 `x-dsh-client` 头、更新通道判定(含 `-` 即 beta)、面板「插件版本」、
2489
+ * 更新检测拿的都是这个值:一处漏改会同时污染装机来源统计与更新判断,
2490
+ * 而且**发出去之后改不回来**(npm 同一版本不可覆盖)。
2491
+ *
2492
+ * 测试本来就会红(它拿 package.json 与上报值对账 —— 这次正是它抓住的),
2493
+ * 但让两份数据**只有一个来源**才是根治。
2494
+ * 读不到时退回 `0.0.0-unknown`:让异常**显式可见**,绝不悄悄冒充某个真实版本。
2495
+ */
2496
+ const PLUGIN_VERSION = (() => {
2497
+ try {
2498
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
2499
+ return String(pkg && pkg.version ? pkg.version : "") || "0.0.0-unknown";
2500
+ } catch {
2501
+ return "0.0.0-unknown";
2502
+ }
2503
+ })();
2457
2504
  const UPDATE_LOG = ".dsh-update.log";
2458
2505
  const UPDATE_MARKER = ".dsh-update-running";
2459
2506
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.10-beta.1",
3
+ "version": "0.6.10-beta.10",
4
4
  "description": "公网远程控制 DeepSeek Harness(dsh web):安装即得专属加密地址,人在外面也能用手机访问电脑上的 dsh——无需同一局域网/WiFi、无需公网 IP 与内网穿透,全程加密;手机端 100% 还原电脑体验(对话/工具/审批/设置)。技术用户可选自建服务,流量走自己的服务器。Remote control DeepSeek Harness (dsh web) from anywhere over the public internet — install, get an encrypted URL, use it from your phone on any network (dual-half cordis plugin: Settings panel + same-origin /dsh-remote routes). (2026-09 由 dsh-remote-ui 更名 / renamed from dsh-remote-ui; dsh-remote 的 dsh web 插件半,不是纯 UI/皮肤插件)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",