@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
|
-
|
|
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
|
-
其他命令:`
|
|
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 {
|
|
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({
|
|
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
|
-
|
|
589
|
-
|
|
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.
|
|
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
|
-
// ----------
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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("⚠️
|
|
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")
|
|
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("
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|