@mrrisega/dsh-remote 0.4.7 → 0.4.9

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
@@ -63,12 +63,9 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
63
63
  npx @mrrisega/dsh-remote
64
64
  ```
65
65
 
66
- 安装完成后运行下面命令打开设置页,用手机号+密码登录即可(注册在手机端完成,
67
- 登录后 bridge 自动启动):
68
-
69
- ```bash
70
- npx @mrrisega/dsh-remote settings
71
- ```
66
+ 安装完成后**无需任何命令**:打开 dsh web → 设置 → 「远程控制」,注册/登录手机号即可
67
+ (注册在手机端完成,登录后 bridge 自动启动;自建用户在同一面板切「自建服务」标签)。
68
+ 原来的独立设置页(`dsh-remote settings`)已移除,避免与插件面板重复造成困惑。
72
69
 
73
70
  自建模式(自己部署了 relay-router,无需账号体系):
74
71
 
@@ -76,7 +73,7 @@ npx @mrrisega/dsh-remote settings
76
73
  npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访问密钥>
77
74
  ```
78
75
 
79
- 其他命令:`settings`(设置页)、`status`(查看状态)、`run`(前台调试)、
76
+ 其他命令:`status`(查看状态)、`run`(前台调试)、`settings`(仅显示登录指引)、
80
77
  `plugin`(重装/卸载 dsh web 插件)。运行 `npx @mrrisega/dsh-remote --help` 查看完整说明。
81
78
 
82
79
  > **版本与更新 / 卸载**:dsh 官方插件市场目前不提供更新按钮,也不会改写用户补丁(因此市场
@@ -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/dsh-setup.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * 用法:
6
6
  * dsh-remote [setup] [选项] 一键安装(默认命令,无需任何参数)
7
- * dsh-remote settings 打开本地设置页(登录账号 / 自建配置)
7
+ * dsh-remote settings 提示如何登录(独立设置页已移除,见 dsh web 面板)
8
8
  * dsh-remote run 前台运行 bridge(调试/守护)
9
9
  * dsh-remote status 查看配置与服务状态
10
10
  * dsh-remote plugin [--uninstall] 手动安装/卸载 dsh web 远程控制插件
@@ -19,10 +19,10 @@
19
19
  * 1. 写入配置 <CONFIG_DIR>/.dsh-config.json(0600;npm 安装时为 ~/.dsh-remote/)
20
20
  * 2. 生成自启动服务(macOS launchd / Linux systemd),随 dsh web(3080) 存活自动保活
21
21
  * 3. 自动把远程控制插件装进 dsh web 设置页(若检测到 profile)
22
- * 4. 登录在设置页完成: dsh-remote settings(手机号+密码,或自建密钥)
22
+ * 4. 登录/连接配置在 dsh web → 设置 → 「远程控制」面板完成(SaaS 注册登录 / 自建切换)
23
+ * 自建 CLI 用户也可用 `dsh-remote setup --server … --key …`(无需再打开设置页)
23
24
  */
24
25
 
25
- import http from "node:http";
26
26
  import fs from "node:fs";
27
27
  import os from "node:os";
28
28
  import path from "node:path";
@@ -35,7 +35,6 @@ const IS_NPM_INSTALL = THIS_DIR.includes(`${path.sep}node_modules${path.sep}`);
35
35
  // 配置目录:npm 安装时放在用户目录(node_modules 内不可写);仓库开发时放在仓库根。
36
36
  const CONFIG_DIR = process.env.DSH_RELAY_DIR || (IS_NPM_INSTALL ? path.join(os.homedir(), ".dsh-remote") : THIS_DIR);
37
37
  const CONFIG_PATH = path.join(CONFIG_DIR, ".dsh-config.json");
38
- const SETTINGS_PORT = 3499;
39
38
  // 默认云端服务地址(服务商 SaaS 入口;自建用户用 --server/--key 指向自己的 router)
40
39
  const DEFAULT_API = "https://n.risegao.cn:13443/relay-api";
41
40
  const DEFAULT_APP_URL = "https://n.risegao.cn:13443/app/";
@@ -281,97 +280,20 @@ function installAutostart() {
281
280
  return { path: svcPath, status: r };
282
281
  }
283
282
 
284
- // ---------- 设置页(本地 HTTP:远程地址 + 账号/自建配置) ----------
285
- function serveSettings() {
286
- const cfg = loadConfig();
287
- const server = http.createServer(async (req, res) => {
288
- const url = new URL(req.url, `http://127.0.0.1:${SETTINGS_PORT}`);
289
- const send = (code, body, type = "text/html") => {
290
- res.writeHead(code, { "Content-Type": type + "; charset=utf-8" });
291
- res.end(body);
292
- };
293
-
294
- if (req.method === "POST" && url.pathname === "/api/config") {
295
- let raw = "";
296
- for await (const c of req) raw += c;
297
- try {
298
- const body = JSON.parse(raw);
299
- const local = Boolean(body.local_key || body.server);
300
- if (local) {
301
- if (!body.server || !body.local_key) {
302
- return send(400, JSON.stringify({ ok: false, message: "自建模式需要服务器地址与访问密钥" }), "application/json");
303
- }
304
- cfg.tunnel_url = normalizeTunnelUrl(body.server);
305
- cfg.local_key = String(body.local_key).trim();
306
- delete cfg.phone; delete cfg.password;
307
- } else {
308
- if (!body.phone || !body.password) {
309
- return send(400, JSON.stringify({ ok: false, message: "SaaS 模式需要手机号与密码" }), "application/json");
310
- }
311
- cfg.phone = body.phone; cfg.password = body.password;
312
- applySaaSMode(cfg); // 清自建残留,api/tunnel 权威重算
313
- }
314
- // 自动获取服务端下发的 bridge_secret(device-login 共享密钥,一键安装开箱即用)
315
- if (!cfg.bridge_secret && !cfg.local_key) {
316
- const pub = await fetchPublicConfig(cfg.api_url || DEFAULT_API);
317
- if (pub.bridge_secret) cfg.bridge_secret = String(pub.bridge_secret);
318
- }
319
- saveConfig(cfg);
320
- const r = restartBridgeService();
321
- return send(200, JSON.stringify({ ok: true, service: r.ok ? "running" : (r.status || "failed") }), "application/json");
322
- } catch { return send(400, JSON.stringify({ ok: false, message: "JSON 解析失败" }), "application/json"); }
323
- }
283
+ // ---------- settings:旧独立设置页已移除(引导到 dsh web 插件面板) ----------
284
+ // 云服务用户在 dsh web → 设置 → 「远程控制」面板即可注册/登录(自建切换也在此面板);
285
+ // 独立设置页(127.0.0.1:3499)与面板功能完全重复、且暴露自建表单给普通用户造成困惑,已移除。
286
+ // 保留 `dsh-remote settings` 命令为“引导提示”,避免旧脚本/旧文档直接调它时报错。
287
+ function settingsHint() {
288
+ console.log(`
289
+ dsh-remote:独立的本地设置页已移除。
324
290
 
325
- const mode = cfg.local_key ? "自建服务" : "SaaS 云端服务";
326
- const pub = await fetchPublicConfig(cfg.api_url || DEFAULT_API);
327
- const remoteUrl = pub.app_url || (cfg.local_key ? (cfg.tunnel_url || "") : DEFAULT_APP_URL);
328
-
329
- const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"/>
330
- <meta name="viewport" content="width=device-width,initial-scale=1"/>
331
- <title>dsh-remote 设置</title>
332
- <style>
333
- body{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;background:#0d1117;color:#e6edf3;max-width:520px;margin:0 auto;padding:24px}
334
- h1{font-size:18px}label{display:block;font-size:13px;color:#8b949e;margin:14px 0 6px}
335
- input{width:100%;padding:11px 13px;border-radius:8px;border:1px solid #30363d;background:#161b22;color:#e6edf3;font-size:15px;box-sizing:border-box}
336
- button{width:100%;padding:13px;border-radius:8px;border:none;background:#2f81f7;color:#fff;font-size:15px;font-weight:600;cursor:pointer;margin-top:16px}
337
- .card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:18px;margin-bottom:16px}
338
- .url{background:#010409;border:1px solid #30363d;border-radius:8px;padding:12px;font-family:monospace;font-size:14px;word-break:break-all}
339
- .msg{font-size:13px;margin-top:10px;min-height:18px}.ok{color:#3fb950}.err{color:#f85149}
340
- </style></head><body>
341
- <h1>dsh-remote 设置</h1>
342
- <div class="card"><h3 style="margin:0 0 8px">📱 远程控制地址(当前模式:${mode})</h3>
343
- <div class="url">${remoteUrl || "(未配置)"}</div>
344
- <div style="font-size:12px;color:#8b949e;margin-top:8px">手机浏览器打开此地址,即可远程控制本机 dsh web。</div></div>
345
- <div class="card"><h3 style="margin:0 0 4px">🔑 连接配置</h3>
346
- <div style="font-size:12px;color:#8b949e;margin-bottom:8px">二选一:填手机号密码(SaaS),或填服务器地址+访问密钥(自建)。</div>
347
- <label>SaaS 手机号</label><input id="phone" value="${cfg.phone || ""}" autocomplete="tel"/>
348
- <label>SaaS 密码</label><input id="pass" type="password" placeholder="••••••••" autocomplete="current-password"/>
349
- <div style="height:1px;background:#30363d;margin:16px 0"></div>
350
- <label>自建服务器地址(wss://host:port)</label><input id="server" value="${cfg.tunnel_url || ""}" placeholder="wss://relay.example.com"/>
351
- <label>自建访问密钥</label><input id="lkey" type="password" value="${cfg.local_key || ""}" placeholder="访问密钥"/>
352
- <button onclick="save()">保存并生效</button>
353
- <div class="msg" id="msg"></div></div>
354
- <script>
355
- async function save(){
356
- const m=document.getElementById("msg");m.className="msg";m.textContent="保存中...";
357
- const local = document.getElementById("lkey").value.trim() !== "" || document.getElementById("server").value.trim() !== "";
358
- try{
359
- const r=await fetch("/api/config",{method:"POST",headers:{"content-type":"application/json"},
360
- body:JSON.stringify(local
361
- ? {server:document.getElementById("server").value.trim(),local_key:document.getElementById("lkey").value.trim()}
362
- : {phone:document.getElementById("phone").value.trim(),password:document.getElementById("pass").value})});
363
- const d=await r.json();
364
- if(d.ok){m.className="msg ok";m.textContent = d.service==="running" ? "✅ 已保存并生效,无需手动重启" : "✅ 已保存(服务状态: "+(d.service||"未知")+",可运行 dsh-remote setup 修复)";}
365
- else{m.className="msg err";m.textContent=d.message||"保存失败";}
366
- }catch(e){m.className="msg err";m.textContent="保存失败: "+e.message;}
367
- }
368
- </script></body></html>`;
369
- return send(200, html);
370
- });
371
- server.listen(SETTINGS_PORT, "127.0.0.1", () => {
372
- console.log(`\n✅ 设置页已打开: http://127.0.0.1:${SETTINGS_PORT}`);
373
- console.log(" 在浏览器中配置账号或自建连接,查看远程控制地址。Ctrl-C 关闭。\n");
374
- });
291
+ 请直接在 dsh web 里完成登录/连接配置(无需任何命令):
292
+ 打开 dsh web 设置 「远程控制」→ 注册或登录手机号即可(自建模式切换也在该面板)。
293
+
294
+ 自建用户若偏好命令行,可用:
295
+ dsh-remote setup --server wss://你的域名:端口 --key 访问密钥
296
+ `);
375
297
  }
376
298
 
377
299
  // ---------- run:前台跑 bridge(带配置 + 自启动 watcher) ----------
@@ -400,7 +322,7 @@ async function runBridge() {
400
322
  if (!saas && !local) {
401
323
  if (!warnedNoLogin) {
402
324
  warnedNoLogin = true;
403
- console.log("⚠️ 尚未登录:打开设置页 `dsh-remote settings` 登录账号(或配置自建密钥)后自动启动。");
325
+ console.log("⚠️ 尚未登录:请打开 dsh web → 设置 → 「远程控制」,注册/登录手机号(或切到自建模式)后自动启动。");
404
326
  }
405
327
  return;
406
328
  }
@@ -802,7 +724,7 @@ const raw = process.argv[2];
802
724
  const cmd = raw && !raw.startsWith("-") ? raw : "setup";
803
725
  const args = raw && !raw.startsWith("-") ? process.argv.slice(3) : process.argv.slice(2);
804
726
  if (cmd === "setup" || cmd === "install") await setup(args);
805
- else if (cmd === "settings") serveSettings();
727
+ else if (cmd === "settings") settingsHint();
806
728
  else if (cmd === "run") await runBridge();
807
729
  else if (cmd === "plugin") await pluginCmd(process.argv.slice(3));
808
730
  else if (cmd === "status") {
@@ -811,13 +733,13 @@ else if (cmd === "status") {
811
733
  console.log("配置文件:", CONFIG_PATH);
812
734
  console.log("连接模式:", local ? `自建服务(${cfg.tunnel_url || "未设置服务器地址"})` : `SaaS 云端服务(${cfg.phone || "未配置账号"})`);
813
735
  console.log("API:", cfg.api_url || (local ? "(自建模式无需账号 API)" : DEFAULT_API));
814
- console.log("远程地址: 运行 settings 查看最新");
736
+ console.log("远程地址/登录: 打开 dsh web → 设置 → 「远程控制」查看与操作");
815
737
  } else {
816
738
  console.log(`dsh-remote — 手机远程控制 dsh web(隧道模式)
817
739
 
818
740
  用法:
819
741
  dsh-remote 一键安装(默认命令,无需任何参数;含插件与自启动)
820
- dsh-remote settings 打开本地设置页(登录账号 / 自建配置)
742
+ dsh-remote settings 显示登录/连接配置指引(独立设置页已移除)
821
743
  dsh-remote run 前台运行 bridge(调试)
822
744
  dsh-remote status 查看配置与服务状态
823
745
  dsh-remote plugin 手动安装 dsh web 远程控制插件(--uninstall 卸载)
@@ -825,7 +747,8 @@ else if (cmd === "status") {
825
747
  自建模式(可选):
826
748
  dsh-remote setup --server wss://你的域名:端口 --key 访问密钥
827
749
 
828
- 登录: 安装后运行 \`dsh-remote settings\`,用手机号+密码登录(或配置自建密钥)。
750
+ 登录/连接配置: 打开 dsh web → 设置 → 「远程控制」→ 注册或登录手机号即可
751
+ (自建用户切「自建服务」标签或直接用上方 setup 命令,无需另开页面)。
829
752
  文档: ${REPO_URL}
830
753
  `);
831
754
  process.exit(cmd === "help" ? 0 : 1);
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.9",
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.9";
834
841
  const UPDATE_LOG = ".dsh-update.log";
835
842
  const UPDATE_MARKER = ".dsh-update-running";
836
843