dsh-remote-web 0.6.10-beta.4 → 0.6.10-beta.7

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;
@@ -4444,16 +4466,46 @@ window.__ModuleLoader__.load({
4444
4466
  // 位置:内嵌内容体的**最上面**,在「连接微信机器人」按钮之前(DOM 顺序也一样)——
4445
4467
  // 第一次进来的人先看懂用途,再决定要不要绑。紧凑小字,
4446
4468
  // 不会把连接按钮挤出首屏(见 .dru-wx-intro 的样式注释)。
4447
- // 只讲**产品真的会做的事**,且必须**按档位如实分层**:
4448
- // 免费档只能收通知 + 回数字拍板,「在微信里交代任务」是会员能力 ——
4449
- // 在面板里含糊其辞,用户绑完去微信发句话拿到付费提示,只会觉得产品骗人。
4469
+ //
4470
+ // ★ 能力对照是**数据驱动**的,不再是写死的四行:
4471
+ // · 判定来源 = `wx.caps`(微信通道**实际在用**的那一份,见 status()),
4472
+ // 所以"面板上写的"与"微信里强制的"不可能不一致(业主:话术与权限必须一致);
4473
+ // · 拿不到才退回本地面板镜像(旧版 bridge),两份由 wechat-bot-ui.test.mjs 锁死;
4474
+ // · 免费用户会看到**锁定的会员项**(🔒)—— 这就是业主要的"对比展示付费多什么"。
4450
4475
  // 已绑定态同样保留:它是常驻的用途说明(不是一次性引导气泡),不占用任何按钮。
4476
+ var wxPlan = (wx && wx.plan) ? String(wx.plan) : "free";
4477
+ var wxIsPaid = wxPlan !== "free";
4478
+ var wxCaps = (wx && Array.isArray(wx.caps) && wx.caps.length) ? wx.caps : null;
4479
+ var wxHas = function (cap) {
4480
+ var list = wxCaps || (wxIsPaid ? WECHAT_PAID_CAPS : WECHAT_FREE_CAPS);
4481
+ return list.some(function (c) { return c === cap || cap.indexOf(c + ".") === 0; });
4482
+ };
4483
+ var capRows = [];
4484
+ // 状态还没读到时**不显示锁**:否则付费用户会先看到一排 🔒 再变成 ✅(闪一下很难看)
4485
+ if (wx) {
4486
+ WECHAT_CAP_LABELS.forEach(function (row, i) {
4487
+ var text = row.free || row.paid;
4488
+ if (!text) return;
4489
+ var ok = wxHas(row.cap);
4490
+ capRows.push(h("div", { className: "dru-wx-intro-row", key: "cap" + i },
4491
+ (ok ? "✅ " : "🔒 ") + text + (ok ? "" : "(会员)")));
4492
+ });
4493
+ } else {
4494
+ WECHAT_CAP_LABELS.forEach(function (row, i) {
4495
+ var text = row.free || row.paid;
4496
+ if (text) capRows.push(h("div", { className: "dru-wx-intro-row", key: "cap" + i }, "· " + text));
4497
+ });
4498
+ }
4499
+ var quotaRow = (!wxIsPaid && WECHAT_FREE_MSGS_PER_MONTH > 0)
4500
+ ? h("div", { className: "dru-wx-intro-row" },
4501
+ "· 免费版每月 " + WECHAT_FREE_MSGS_PER_MONTH + " 条消息额度"
4502
+ + (wx ? "(本月已用 " + Number(wx.messages_used_this_month || 0) + " 条)" : ""))
4503
+ : null;
4451
4504
  var intro = h("div", { className: "dru-wx-intro", role: "note", "aria-label": "微信机器人通道能做什么" },
4452
- h("div", { className: "dru-wx-intro-title" }, "绑定后能做什么"),
4453
- h("div", { className: "dru-wx-intro-row" }, "· 任务完成 / 出错 / 停下时**推送**到微信 —— 不用守着电脑。"),
4454
- h("div", { className: "dru-wx-intro-row" }, "· 需要你拍板时,在微信里**回一个数字**就完成决定(放行 / 拒绝 / 选哪个)。"),
4455
- h("div", { className: "dru-wx-intro-row" }, "· 以上两项**免费用**。"),
4456
- h("div", { className: "dru-wx-intro-row" }, "· **会员**:还能直接在微信里交代任务、切换会话,并对同一个任务继续追问。")
4505
+ h("div", { className: "dru-wx-intro-title" },
4506
+ wx ? (wxIsPaid ? "你现在能用的(会员)" : "你现在能用的(免费)") : "绑定后能做什么"),
4507
+ capRows,
4508
+ quotaRow
4457
4509
  );
4458
4510
 
4459
4511
  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 });
@@ -2453,7 +2481,7 @@ const PLUGIN_ID = "dsh-remote-web";
2453
2481
  const PLUGIN_LEGACY_IDS = ["dsh-remote-ui"];
2454
2482
  const PLUGIN_ALL_IDS = [PLUGIN_ID, ...PLUGIN_LEGACY_IDS];
2455
2483
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
2456
- const PLUGIN_VERSION = "0.6.10-beta.4";
2484
+ const PLUGIN_VERSION = "0.6.10-beta.7";
2457
2485
  const UPDATE_LOG = ".dsh-update.log";
2458
2486
  const UPDATE_MARKER = ".dsh-update-running";
2459
2487
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-web",
3
- "version": "0.6.10-beta.4",
3
+ "version": "0.6.10-beta.7",
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",