@mrrisega/dsh-remote 0.7.9-beta.1 → 0.7.10-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/README.md CHANGED
@@ -9,6 +9,9 @@
9
9
  <img alt="License" src="https://img.shields.io/badge/License-PolyForm%20Noncommercial-2ea44f?style=flat-square">
10
10
  </p>
11
11
 
12
+ > **⚠️ 同名澄清(先看这里)**:本产品**不是** [flymysql/dsh-remote](https://github.com/flymysql/dsh-remote)——那个项目是把任意 SSH 机器变成 DSH 工作区(远程 SSH 工作区),与本产品无关。npm 上的 `dsh-remote` 包也不是本产品。
13
+ > 认准 **`mrRisega/dsh-remote`** 仓库:CLI 根包 **`@mrrisega/dsh-remote`**(安装器)、dsh web 插件子包 **`dsh-remote-web`**。安装请用下面这条命令,不要用 `npx dsh-remote`。
14
+
12
15
  > **离开电脑也能用手机 100% 全功能接管电脑上的 DeepSeek Harness(`dsh web`)**——发消息、看工具执行、审批权限、改设置、管凭据,体验与坐在电脑前一致。免内网穿透,局域网即连。
13
16
  >
14
17
  > **一条命令安装**:`npx @mrrisega/dsh-remote`
@@ -22,7 +25,7 @@
22
25
  - **两端体验一致**:手机 PWA 还原电脑端界面;电脑端 `dsh web` 侧栏新增「📱 远程访问」入口。
23
26
  - **一条命令 + 开机自启**:自动装好 bridge 与 dsh web 插件,并按平台配置自启动与崩溃自愈(macOS launchd / Linux systemd --user / Windows 任务计划程序)。
24
27
  - **两种模式随时切**:官方云服务(手机号登录、含超管后台)或自建 relay(访问密钥认证),面板一键切换,互不影响。
25
- - **插件市场可装**:在 dsh 插件市场搜索 **`dsh-remote-web`** 即可;桌面运行环境由插件在后台自动补齐。
28
+ - **插件市场可装**:在 dsh 插件市场搜索 **`dsh-remote-web`**(npm 子包名)即可;部分市场条目显示名是 `dsh-remote`,认准作者 **mrRisega** 即可;桌面运行环境由插件在后台自动补齐。
26
29
 
27
30
  ```
28
31
  手机浏览器
@@ -55,7 +58,11 @@
55
58
  | 许可证 | 本仓库(见下文 License) | 商业授权,闭源 |
56
59
 
57
60
  > 👉 **不想自建服务器 / 没有公网 IP?** 直接用**官方云服务版**(无公网、4G、异地都稳定,多设备 + 超管后台):
58
- > **https://n.risegao.cn:13443/app/** —— 免费版 + PRO ¥19/月 + Pro Max ¥49/月,新用户送 7 天 PRO。
61
+ > **https://n.risegao.cn:13443/app/**
62
+ >
63
+ > **价格与套餐以官网 / 面板实时显示为准**(本节不再硬编码具体价格,避免改价后文案过期):
64
+ > 免费档可用;付费为 **PRO(¥20/月起)** 与 **Pro Max**(更多设备 + 优先线路),支持**季付 9 折、年付 8 折**。
65
+ > 托管免费档有**月度额度 + 限速**,具体额度以官网 / 面板实时显示为准;**自建 relay 与局域网直连不受托管额度限制**。
59
66
  > 客户端仍是同一条安装命令,登录后选「云服务模式」即可。
60
67
 
61
68
  ## 安装
@@ -114,6 +114,7 @@ import {
114
114
  // ⚠️ 微信流量只走「腾讯 ilink ↔ 本机」,**不经我们的 relay**;DSH 事件走本机回环。
115
115
  // 总开关 DSH_WECHAT=0(默认开启);未绑定时只起控制面,不起长轮询。
116
116
  import { createWeChatRuntime } from "./wechat-runtime.mjs";
117
+ import { createPushReporter } from "./push-reporter.mjs";
117
118
 
118
119
  // ---------- 强制直连:清除代理环境变量 ----------
119
120
  // 家庭网络常配 Clash 等代理(127.0.0.1:7890),node 的 ws/fetch 会继承
@@ -288,7 +289,44 @@ function stampBridgeHeartbeat() {
288
289
  const now = Date.now();
289
290
  if (now - lastStateHeartbeatAt < STATE_HEARTBEAT_MIN_MS) return;
290
291
  lastStateHeartbeatAt = now;
291
- persistBridgeState({ pid: process.pid, heartbeat_at: now });
292
+ persistBridgeState({
293
+ pid: process.pid,
294
+ heartbeat_at: now,
295
+ // 【0.7.10 / W1-T3】E2EE 死会话拒绝计数(见 noteE2eeWsReject 的说明)。
296
+ // 跟着心跳一起落盘:不额外写盘、不加定时器;旧面板/旧插件读到多余字段一律忽略。
297
+ e2ee_ws_reject_locked: e2eeWsRejectStats.locked,
298
+ e2ee_ws_reject_disabled: e2eeWsRejectStats.disabled,
299
+ e2ee_ws_reject_at: e2eeWsRejectStats.lastAt
300
+ });
301
+ }
302
+
303
+ /**
304
+ * 【0.7.10 / W1-T3】E2EE 会话拒绝计数 —— **只做可观测,不改任何协议行为**。
305
+ *
306
+ * 生产复盘(2026-10):WS 升级被拒里 71.8% 是 `e2ee_session_locked`(死会话),涉 163 用户;
307
+ * 0.7.3+ 已闭环(手机侧清内存会话 + 连续 3 次失败作废交接单 + 每页一次受控 reload;
308
+ * bridge 侧回 `retryable:true, action:"rehandshake"` 让手机自动重握手)。但「死会话占比
309
+ * 降到多少」此前只在 nginx/router 侧可见,客户端自己答不出来。
310
+ *
311
+ * 这里把本进程的拒绝次数记下来、随心跳写进状态文件(面板「复制诊断信息」会带上一行),
312
+ * 于是支持包/诊断信息本身就能回答「这台机器还在被死会话打吗」。
313
+ * 落盘节流 ≥10s:死循环期一台设备一天能拒近万次,绝不能每拒一次就写盘。
314
+ */
315
+ const e2eeWsRejectStats = { locked: 0, disabled: 0, lastAt: 0 };
316
+ const E2EE_REJECT_FLUSH_MIN_MS = 10_000;
317
+ let lastE2eeRejectFlushAt = 0;
318
+ function noteE2eeWsReject(kind) {
319
+ if (kind === "disabled") e2eeWsRejectStats.disabled += 1;
320
+ else e2eeWsRejectStats.locked += 1;
321
+ const now = Date.now();
322
+ e2eeWsRejectStats.lastAt = now;
323
+ if (now - lastE2eeRejectFlushAt < E2EE_REJECT_FLUSH_MIN_MS) return;
324
+ lastE2eeRejectFlushAt = now;
325
+ persistBridgeState({
326
+ e2ee_ws_reject_locked: e2eeWsRejectStats.locked,
327
+ e2ee_ws_reject_disabled: e2eeWsRejectStats.disabled,
328
+ e2ee_ws_reject_at: e2eeWsRejectStats.lastAt
329
+ });
292
330
  }
293
331
 
294
332
  // ---------- 安装来源 / 版本(服务端设备行统计约定) ----------
@@ -1714,6 +1752,7 @@ export async function handleWsOpen(dchOrSend, frame) {
1714
1752
  const tagged = parseWsE2eeParams(path);
1715
1753
  if (tagged) {
1716
1754
  if (!e2ee.enabled) {
1755
+ noteE2eeWsReject("disabled"); // 可观测计数(见 noteE2eeWsReject;不改行为)
1717
1756
  sendWsOpenReject(send, id, 1008, "e2ee_disabled:bridge 未启用端到端加密(请用明文连接)");
1718
1757
  return;
1719
1758
  }
@@ -1721,6 +1760,7 @@ export async function handleWsOpen(dchOrSend, frame) {
1721
1760
  if (!esess || !esess.verified) {
1722
1761
  // 会话可能刚被重启/被 LRU 挤掉:把「可自动重握手」写成机器可读信号,
1723
1762
  // 手机侧据此自动重建会话,而不是弹「请重新解锁」让用户输密码(生产 3964 次/设备)。
1763
+ noteE2eeWsReject("locked");
1724
1764
  console.error(`[bridge] e2ee ws 拒绝(会话未解锁/未验证): ${path}`);
1725
1765
  sendWsOpenReject(send, id, 1008, "e2ee_session_locked:加密会话已失效,需要重新握手", {
1726
1766
  retryable: true,
@@ -2801,6 +2841,45 @@ function currentUpstreamForChannel() {
2801
2841
  return UPSTREAM;
2802
2842
  }
2803
2843
 
2844
+ /**
2845
+ * 推送上报(2026-11-05):把"任务完成/需要拍板/出错"上报给 SaaS,由服务端规则引擎决定是否推送。
2846
+ *
2847
+ * 为什么放在这里有独立开关:它是**可选增强**,任何失败都不得影响远程访问主链路。
2848
+ * 真正的启动条件由 push-reporter 自己问服务端(`/api/push/public-key` 的 enabled+subscribed),
2849
+ * 本地只有 `DSH_PUSH_REPORT=0` 能强制关闭。
2850
+ */
2851
+ let pushReporter = null;
2852
+ let PUSH_TOKEN = "";
2853
+
2854
+ function startPushReporter() {
2855
+ if (String(process.env.DSH_PUSH_REPORT || "") === "0") {
2856
+ console.log("[push] 上报已禁用(DSH_PUSH_REPORT=0)");
2857
+ return;
2858
+ }
2859
+ try {
2860
+ if (pushReporter) return;
2861
+ pushReporter = createPushReporter({
2862
+ apiBase: API_BASE,
2863
+ deviceId: DEVICE_ID,
2864
+ tokenProvider: () => PUSH_TOKEN,
2865
+ upstream: () => currentUpstreamForChannel(),
2866
+ cookie: harnessCookieOf,
2867
+ detail: String(process.env.DSH_PUSH_DETAIL || "") === "1",
2868
+ log: (level, message) => {
2869
+ try {
2870
+ if (level === "warn") console.warn(message);
2871
+ else console.log(message);
2872
+ } catch { /* 忽略 */ }
2873
+ }
2874
+ });
2875
+ // 不 await:启动探测失败绝不影响隧道
2876
+ void pushReporter.start().catch(() => {});
2877
+ } catch (e) {
2878
+ // 可选增强失败不得冒泡
2879
+ console.warn(`[push] 上报器初始化失败(已忽略):${e && e.message ? e.message : e}`);
2880
+ }
2881
+ }
2882
+
2804
2883
  function startWeChat() {
2805
2884
  if (WECHAT_DISABLED) {
2806
2885
  console.log("[bridge] 微信通道已禁用(DSH_WECHAT=0)");
@@ -2983,6 +3062,7 @@ async function runTunnel() {
2983
3062
  // (真机日志实测:同一个桥接启动里出现两条并发的 device-login)。
2984
3063
  // 而 /api/device-login 是按出口 IP 限流的 —— 每次启动都多花一份登录额度没有道理。
2985
3064
  WECHAT_TIER_TOKEN = { value: token, at: Date.now() };
3065
+ PUSH_TOKEN = token;
2986
3066
  // 新进程开始:重置状态文件(上一轮的成功记录不能代表这一轮,面板据此判定 online)。
2987
3067
  // `pid` 一并写入 = **bridge 自证身份**:插件可以用「state.pid 活着 + heartbeat_at 新鲜」
2988
3068
  // 判断桥是否真的在跑,不必依赖按镜像名(node.exe)扫描 —— 桌面端宿主拉起的进程名不是 node.exe。
@@ -2998,6 +3078,8 @@ async function runTunnel() {
2998
3078
  }
2999
3079
  // 隧道已发起(或已进入锁定等待)后再起微信:主链路优先,微信永不阻塞远程访问
3000
3080
  startWeChat();
3081
+ // 同理:推送上报也是可选增强,最后起、失败静默
3082
+ startPushReporter();
3001
3083
  setTimeout(() => {
3002
3084
  console.log(`[bridge] 隧道模式运行中(上游 ${UPSTREAM},Ctrl-C 退出)`);
3003
3085
  }, 1000);
@@ -3043,6 +3125,8 @@ async function main() {
3043
3125
  console.log(`[bridge] 收到 ${sig},正在优雅退出...`);
3044
3126
  // 退出前把 E2EE 会话库刷一次(滑动 TTL 的 usedAt 要落盘;崩溃也能恢复到最多 60s 前)
3045
3127
  try { e2ee.flushStore(); } catch { /* 已内部兜底 */ }
3128
+ // 退出前尽力把待上报的事件发出去(最多等 1s,绝不阻塞退出)
3129
+ try { if (pushReporter) await pushReporter.stop(); } catch { /* 已内部兜底 */ }
3046
3130
  await stopWeChat();
3047
3131
  process.exit(0);
3048
3132
  };
@@ -3188,6 +3272,9 @@ export function __wsSessionState(id) {
3188
3272
  return { opened: s.opened === true, pending: s.pending ? s.pending.length : 0, cancelled: s.cancelled === true, failed: s.failed === true };
3189
3273
  }
3190
3274
 
3275
+ // test hook:E2EE 拒绝计数快照(W1-T3 可观测字段;状态文件里同名 e2ee_ws_reject_* 字段的来源)
3276
+ export function __e2eeRejectStatsForTest() { return { ...e2eeWsRejectStats }; }
3277
+
3191
3278
  // 直接运行(node dsh-bridge.mjs)时启动服务;被测试 import 时只导出协议函数。
3192
3279
  const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
3193
3280
  if (isMain) {