@mrrisega/dsh-remote 0.4.7 → 0.4.8

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.
@@ -47,7 +47,8 @@ import WebSocket from "ws";
47
47
  import fs from "node:fs";
48
48
  import os from "node:os";
49
49
  import path from "node:path";
50
- import { randomBytes, generateKeyPairSync } from "node:crypto";
50
+ import { execSync } from "node:child_process";
51
+ import { randomBytes, generateKeyPairSync, createHash } from "node:crypto";
51
52
  import { pathToFileURL, fileURLToPath } from "node:url";
52
53
  import { promisify } from "node:util";
53
54
  import { gzip as gzipCb } from "node:zlib";
@@ -80,6 +81,46 @@ const PHONE = process.env.DSH_BRIDGE_PHONE || EMAIL;
80
81
  const PASSWORD = process.env.DSH_BRIDGE_PASSWORD || "";
81
82
  const TOKEN = process.env.DSH_BRIDGE_TOKEN || "";
82
83
 
84
+ // ---------- 本机稳定指纹(同机重装识别;供服务端自动顶替旧设备) ----------
85
+
86
+ /** 读取与安装无关的机器级唯一值:macOS IOPlatformUUID / Linux machine-id。 */
87
+ function machineUniqueId() {
88
+ if (process.env.DSH_BRIDGE_MACHINE_FP) return String(process.env.DSH_BRIDGE_MACHINE_FP).slice(0, 64);
89
+ if (process.platform === "darwin") {
90
+ try {
91
+ const out = execSync("ioreg -rd1 -c IOPlatformExpertDevice", { encoding: "utf8", timeout: 5000 });
92
+ const m = /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out);
93
+ if (m && m[1]) return m[1];
94
+ } catch { /* 兜底 */ }
95
+ } else if (process.platform === "linux") {
96
+ for (const f of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
97
+ try {
98
+ const s = fs.readFileSync(f, "utf8").trim();
99
+ if (s) return s;
100
+ } catch { /* 继续 */ }
101
+ }
102
+ }
103
+ return ""; // 读不到(如容器) → 回退宿主名指纹
104
+ }
105
+ /** 稳定指纹:机器唯一值哈希;同机卸载重装后不变。 */
106
+ function machineFingerprint() {
107
+ const base = machineUniqueId() || `${os.hostname()}|${os.platform()}`;
108
+ return createHash("sha256").update(`dsh-remote/machine/v1:${base}`).digest("hex").slice(0, 32);
109
+ }
110
+ const MACHINE_FP = machineFingerprint();
111
+
112
+ /** 绑定/登录失败提示文件:供 dsh web 插件面板读取并展示(如“注册失败,已达到上限”)。 */
113
+ const BIND_ERROR_FILE = path.join(path.dirname(CONFIG_PATH), ".bind-error.json");
114
+ function persistBindError(payload) {
115
+ try {
116
+ fs.mkdirSync(path.dirname(BIND_ERROR_FILE), { recursive: true });
117
+ fs.writeFileSync(BIND_ERROR_FILE, JSON.stringify({ ...payload, at: Date.now() }, null, 2), { mode: 0o600 });
118
+ } catch { /* 非关键 */ }
119
+ }
120
+ function clearBindError() {
121
+ try { fs.rmSync(BIND_ERROR_FILE, { force: true }); } catch { /* 非关键 */ }
122
+ }
123
+
83
124
  // ---------- 稳定设备身份(.dsh-config.json) ----------
84
125
 
85
126
  function loadLocalConfig() {
@@ -575,25 +616,36 @@ async function registerDeviceInAccount(token) {
575
616
  const r = await fetch(API_BASE + "/api/devices", {
576
617
  method: "POST",
577
618
  headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
578
- body: JSON.stringify({ device_id: DEVICE_ID, device_name: os.hostname() || "dsh-bridge", pub_key: pubKey })
619
+ body: JSON.stringify({
620
+ device_id: DEVICE_ID,
621
+ device_name: os.hostname() || "dsh-bridge",
622
+ pub_key: pubKey,
623
+ machine_fp: MACHINE_FP // v6 同机识别:服务端据此自动顶替旧设备(重装不再被设备数卡死)
624
+ })
579
625
  });
580
626
  const d = await r.json().catch(() => ({}));
581
627
  if (r.status === 201 || r.status === 200) {
628
+ clearBindError();
582
629
  console.log(`[bridge] ✅ 设备已登记到账号: ${DEVICE_ID}`);
583
630
  return;
584
631
  }
585
632
  if (r.status === 409) {
586
633
  const code = d.error?.code || "";
587
634
  if (code === "device_limit_exceeded") {
588
- console.error(`[bridge] 设备数已达上限: ${d.error?.message || "当前套餐最多绑定 1 台设备"}`);
589
- console.error(" 请在手机端设备管理或后台移除旧设备后重启。");
635
+ const msg = d.error?.message || "当前套餐最多绑定 1 台设备";
636
+ persistBindError({ code, message: msg });
637
+ console.error(`[bridge] 设备数已达上限: ${msg}`);
638
+ console.error(" 同机重装会自动顶替旧设备;仍失败请到手机端设备管理解绑旧设备后重启(免费每月可解绑 3 次)。");
590
639
  } else {
640
+ persistBindError({ code, status: 409, message: d.error?.message || "绑定冲突" });
591
641
  console.error(`[bridge] 设备 ${DEVICE_ID} 绑定失败(${code || 409}): ${d.error?.message || "未知错误"}`);
592
642
  }
593
643
  process.exit(1);
594
644
  }
645
+ persistBindError({ code: d.error?.code || `http_${r.status}`, status: r.status, message: d.error?.message || "设备登记失败" });
595
646
  console.warn(`[bridge] 设备登记失败(${r.status}): ${d.error?.message || "未知错误"}(手机端设备列表可能看不到本设备)`);
596
647
  } catch (e) {
648
+ persistBindError({ code: "api_unreachable", message: `无法连接账号 API: ${e.message}` });
597
649
  console.warn(`[bridge] 无法连接账号 API ${API_BASE}: ${e.message}(手机端设备列表可能看不到本设备)`);
598
650
  }
599
651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -1177,7 +1177,16 @@ window.__ModuleLoader__.load({
1177
1177
  : h("button", { type: "button", className: "dru-btn dru-btn-danger", disabled: busy !== "", onClick: function () { toggleBridge(false); } }, busy === "stop" ? "停止中…" : "停止 bridge")
1178
1178
  ),
1179
1179
  h("div", { className: "dru-meta" }, st && st.config && st.config.deviceId ? "设备 ID:" + st.config.deviceId : "设备 ID:生成中"),
1180
- h("div", { className: "dru-meta" }, st ? (st.service && st.service.plistExists ? "自启动服务已安装" : "自启动服务未安装(启动时自动创建)") : "")
1180
+ h("div", { className: "dru-meta" }, st ? (st.service && st.service.plistExists ? "自启动服务已安装" : "自启动服务未安装(启动时自动创建)") : ""),
1181
+ st && st.service && st.service.bindError
1182
+ ? h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 8 } },
1183
+ "⚠️ 设备注册失败:" + (st.service.bindError.message || "未说明原因"),
1184
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
1185
+ st.service.bindError.code === "device_limit_exceeded"
1186
+ ? "已达本套餐设备数上限。若是同一台电脑重装,稍等片刻会自动顶替旧设备;仍未恢复请在手机端「设备管理」解绑旧设备(免费用户每月可解绑 3 次)后,回到这里点「启动 bridge」。"
1187
+ : "请确认网络与账号状态后重试;仍未解决可点下方「彻底卸载」后重新安装。")
1188
+ )
1189
+ : null
1181
1190
  ),
1182
1191
  // 关于 dsh-remote(开源项目说明卡片)
1183
1192
  card("📖 关于 dsh-remote", [
@@ -704,6 +704,12 @@ async function composeStatus(relayDir) {
704
704
  const cfg = loadConfig(relayDir);
705
705
  const launchd = launchdStatus();
706
706
  const manual = manualStatus();
707
+ // 注册/绑定失败提示(bridge 写 .bind-error.json;面板据此展示“已达上限/需解绑”引导)
708
+ let bindError = null;
709
+ try {
710
+ const f = join(relayDir, ".bind-error.json");
711
+ if (existsSync(f)) bindError = JSON.parse(readFileSync(f, "utf8"));
712
+ } catch { /* 无/损坏忽略 */ }
707
713
  // 远程地址(public-config 的 app_url,取不到用默认)
708
714
  const pub = await relayFetch(relayDir, "/api/public-config");
709
715
  const pubBody = pub.ok && pub.body && typeof pub.body === "object" ? pub.body : {};
@@ -727,6 +733,7 @@ async function composeStatus(relayDir) {
727
733
  launchd,
728
734
  manual,
729
735
  running: launchd.running || manual.bridge.length > 0,
736
+ bindError,
730
737
  },
731
738
  host: hostname(),
732
739
  };
@@ -830,7 +837,7 @@ async function proxyFeedback(relayDir, req, res, pathname) {
830
837
  // ---------- 自管理:版本 / 在线更新 / 彻底卸载(面板内“版本与更新”卡片) ----------
831
838
 
832
839
  /** 插件自身发布版本(与 dsh-remote 根包同步递增)。 */
833
- const PLUGIN_VERSION = "0.4.7";
840
+ const PLUGIN_VERSION = "0.4.8";
834
841
  const UPDATE_LOG = ".dsh-update.log";
835
842
  const UPDATE_MARKER = ".dsh-update-running";
836
843