@mrrisega/dsh-remote 0.6.2 → 0.6.4-beta.1
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 +7 -1
- package/clients/dsh-remote/dsh-bridge.mjs +53 -1
- package/dsh-setup.mjs +15 -2
- package/package.json +4 -3
- package/packages/dsh-remote-web/lib/client.js +163 -4
- package/packages/dsh-remote-web/lib/index.js +577 -11
- package/packages/dsh-remote-web/package.json +5 -2
- package/packages/dsh-remote-web/test/connect-loop.test.mjs +516 -0
- package/packages/dsh-remote-web/test/connect-ui.test.mjs +415 -0
package/README.md
CHANGED
|
@@ -28,7 +28,13 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
|
|
|
28
28
|
└→ bridge (电脑端) → 127.0.0.1:3080 (dsh web)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
## 0.6.
|
|
31
|
+
## 0.6.4 速览(首次安装不再卡)
|
|
32
|
+
|
|
33
|
+
- **登录后 bridge 自动连上,全程零刷新**:面板自动补运行环境 → 拉起 bridge → 显示「正在连接中继…」,连上后自动变成「已连接 ✅」并刷新二维码与设备列表;失败有可读原因、自动重试倒计时和「复制诊断信息」。
|
|
34
|
+
- **手机端自动等待电脑上线**:空设备列表不再让人干等或手动刷新,电脑一上线设备自己出现。
|
|
35
|
+
- **安装引导改版**:有插件市场入口就搜索 `dsh-remote` 安装(不用敲命令);没有就复制一条命令;两条路都收敛到「登录后稍等,电脑会自己出现」。
|
|
36
|
+
|
|
37
|
+
**0.6.2 起已有**
|
|
32
38
|
|
|
33
39
|
- **插件市场安装即可用**:市场只装「面板插件」,桌面运行环境(bridge + 自启动)由插件在后台自动补齐;修掉了
|
|
34
40
|
「装完 bridge 起不来且永远不自愈」的三处根因(运行环境缺失仍写自启动 → launchd 崩溃循环;崩溃循环被误判成
|
|
@@ -155,6 +155,45 @@ function clearBindError() {
|
|
|
155
155
|
try { fs.rmSync(BIND_ERROR_FILE, { force: true }); } catch { /* 非关键 */ }
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* bridge 运行状态文件:供 dsh web 插件面板判定「设备是否已在中继注册成功」(连接阶段 online)。
|
|
160
|
+
* 面板用它把「bridge 进程在跑」与「设备已注册、手机端真的能用」区分开——后者才是用户关心的状态,
|
|
161
|
+
* 只按进程存活判断会谎报可用。
|
|
162
|
+
* { device_id, started_at, account_bound_at, tunnel_registered_at, phase, last_error? }
|
|
163
|
+
* 每次进程启动都 reset 一次:上一轮进程的成功记录不能代表当前这一轮。
|
|
164
|
+
*/
|
|
165
|
+
const BRIDGE_STATE_FILE = path.join(path.dirname(CONFIG_PATH), ".dsh-bridge-state.json");
|
|
166
|
+
function persistBridgeState(patch, opts = {}) {
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(path.dirname(BRIDGE_STATE_FILE), { recursive: true });
|
|
169
|
+
let base = {};
|
|
170
|
+
if (!opts.reset) {
|
|
171
|
+
try { base = JSON.parse(fs.readFileSync(BRIDGE_STATE_FILE, "utf8")) || {}; } catch { base = {}; }
|
|
172
|
+
}
|
|
173
|
+
fs.writeFileSync(BRIDGE_STATE_FILE, JSON.stringify({ ...base, ...patch, at: Date.now() }, null, 2), { mode: 0o600 });
|
|
174
|
+
} catch { /* 非关键:面板侧还有日志/账号兜底判据 */ }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------- 安装来源 / 版本(服务端设备行统计口径) ----------
|
|
178
|
+
// DSH_BRIDGE_INSTALL_SOURCE 由一键安装器(dsh-setup.mjs)注入 = npx;未设置表示这台电脑走的是
|
|
179
|
+
// 「插件市场装面板插件 + 插件自愈补装运行环境」那条路径 = plugin_market;白名单外一律 unknown。
|
|
180
|
+
const INSTALL_SOURCE_VALUES = new Set(["npx", "plugin_market"]);
|
|
181
|
+
function resolveInstallSource() {
|
|
182
|
+
const v = String(process.env.DSH_BRIDGE_INSTALL_SOURCE || "").trim();
|
|
183
|
+
if (INSTALL_SOURCE_VALUES.has(v)) return v;
|
|
184
|
+
return v ? "unknown" : "plugin_market";
|
|
185
|
+
}
|
|
186
|
+
/** 安装版本:安装器注入的包版本优先,其次本包 package.json 版本(读不到留空)。 */
|
|
187
|
+
function resolveInstallVersion() {
|
|
188
|
+
const v = String(process.env.DSH_BRIDGE_INSTALL_VERSION || "").trim();
|
|
189
|
+
if (v) return v;
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version || "";
|
|
192
|
+
} catch { return ""; }
|
|
193
|
+
}
|
|
194
|
+
const INSTALL_SOURCE = resolveInstallSource();
|
|
195
|
+
const INSTALL_VERSION = resolveInstallVersion();
|
|
196
|
+
|
|
158
197
|
// ---------- 稳定设备身份(.dsh-config.json) ----------
|
|
159
198
|
|
|
160
199
|
function loadLocalConfig() {
|
|
@@ -921,12 +960,19 @@ async function registerDeviceInAccount(token) {
|
|
|
921
960
|
device_id: DEVICE_ID,
|
|
922
961
|
device_name: os.hostname() || "dsh-bridge",
|
|
923
962
|
pub_key: pubKey,
|
|
924
|
-
machine_fp: MACHINE_FP // v6 同机识别:服务端据此自动顶替旧设备(重装不再被设备数卡死)
|
|
963
|
+
machine_fp: MACHINE_FP, // v6 同机识别:服务端据此自动顶替旧设备(重装不再被设备数卡死)
|
|
964
|
+
// 安装口径(服务端存到设备行,供运营统计「市场安装/自愈补装」vs「安装器 npx」):
|
|
965
|
+
// install_source: npx(安装器注入) | plugin_market(插件拉起/自愈,默认) | unknown(白名单外)
|
|
966
|
+
install_source: INSTALL_SOURCE,
|
|
967
|
+
install_version: INSTALL_VERSION,
|
|
968
|
+
host_os: process.platform,
|
|
969
|
+
host_arch: process.arch
|
|
925
970
|
})
|
|
926
971
|
});
|
|
927
972
|
const d = await r.json().catch(() => ({}));
|
|
928
973
|
if (r.status === 201 || r.status === 200) {
|
|
929
974
|
clearBindError();
|
|
975
|
+
persistBridgeState({ device_id: DEVICE_ID, account_bound_at: Date.now(), phase: "bound" });
|
|
930
976
|
console.log(`[bridge] ✅ 设备已登记到账号: ${DEVICE_ID}`);
|
|
931
977
|
return;
|
|
932
978
|
}
|
|
@@ -961,6 +1007,7 @@ function connectTunnel(token) {
|
|
|
961
1007
|
ws.on("open", () => {
|
|
962
1008
|
tunnelRetry = 0;
|
|
963
1009
|
console.log(`[bridge] 隧道已连 ${endpoint},注册 ${DEVICE_ID}...`);
|
|
1010
|
+
persistBridgeState({ device_id: DEVICE_ID, phase: "connecting" }); // 新一轮连接:上一轮的成功记录已过期
|
|
964
1011
|
try {
|
|
965
1012
|
// caps:bridge E2EE 能力上报(§6.2/§7.1;router 纯透传)。enabled=false → 空数组(明文回退)
|
|
966
1013
|
ws.send(JSON.stringify({ type: "tunnel-register", deviceId: DEVICE_ID, token, name: os.hostname() || "dsh-bridge", caps: e2ee.caps() }));
|
|
@@ -981,10 +1028,13 @@ function connectTunnel(token) {
|
|
|
981
1028
|
try {
|
|
982
1029
|
receive(raw, (frame) => {
|
|
983
1030
|
if (frame?.type === "tunnel-register-ok") {
|
|
1031
|
+
// 中继注册成功 = 手机端此刻真的能进这台电脑:面板据此把阶段推进到 online(已连接 ✅)
|
|
1032
|
+
persistBridgeState({ device_id: DEVICE_ID, tunnel_registered_at: Date.now(), phase: "online", last_error: null });
|
|
984
1033
|
console.log(`[bridge] ✅ router 注册成功: ${DEVICE_ID},等待手机访问 /remote/${DEVICE_ID}/`);
|
|
985
1034
|
return;
|
|
986
1035
|
}
|
|
987
1036
|
if (frame?.type === "tunnel-register-err") {
|
|
1037
|
+
persistBridgeState({ phase: "error", last_error: { code: frame.code || "register_err", message: frame.message || "router 拒绝注册" } });
|
|
988
1038
|
console.error(`[bridge] router 拒绝注册: ${frame.code} ${frame.message || ""}`);
|
|
989
1039
|
return;
|
|
990
1040
|
}
|
|
@@ -1037,6 +1087,8 @@ async function runTunnel() {
|
|
|
1037
1087
|
console.error("[bridge] 隧道模式需要账号认证:请设 DSH_BRIDGE_TOKEN,或 DSH_BRIDGE_PHONE+DSH_BRIDGE_PASSWORD");
|
|
1038
1088
|
process.exit(1);
|
|
1039
1089
|
}
|
|
1090
|
+
// 新进程开始:重置状态文件(上一轮的成功记录不能代表这一轮,面板据此判定 online)
|
|
1091
|
+
persistBridgeState({ device_id: DEVICE_ID, started_at: Date.now(), phase: "connecting", last_error: null }, { reset: true });
|
|
1040
1092
|
await initE2ee(token);
|
|
1041
1093
|
await registerDeviceInAccount(token);
|
|
1042
1094
|
connectTunnel(token);
|
package/dsh-setup.mjs
CHANGED
|
@@ -131,6 +131,16 @@ function preferredNode() {
|
|
|
131
131
|
}
|
|
132
132
|
const NODE_BIN = preferredNode();
|
|
133
133
|
|
|
134
|
+
// ---------- 安装口径(随 bridge 环境变量注入;服务端存到设备行,供运营统计) ----------
|
|
135
|
+
/**
|
|
136
|
+
* install_source:本文件即「一键安装器」→ 默认 npx(用户自己跑 `npx @mrrisega/dsh-remote` 的那条路径)。
|
|
137
|
+
* 插件市场那条路径由插件半自愈补装:插件 spawn 本安装器时显式传入
|
|
138
|
+
* DSH_BRIDGE_INSTALL_SOURCE=plugin_market,这里原样透传(外部注入 > 硬编码默认)。
|
|
139
|
+
* install_version:优先取外部注入(插件/在线更新器),否则取本包 package.json 版本。
|
|
140
|
+
*/
|
|
141
|
+
const INSTALL_SOURCE = String(process.env.DSH_BRIDGE_INSTALL_SOURCE || "").trim() || "npx";
|
|
142
|
+
const INSTALL_VERSION = String(process.env.DSH_BRIDGE_INSTALL_VERSION || "").trim() || pkgVersion();
|
|
143
|
+
|
|
134
144
|
// ---------- 配置读写 ----------
|
|
135
145
|
function loadConfig() {
|
|
136
146
|
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8")); }
|
|
@@ -206,7 +216,7 @@ function writeAutostartFile() {
|
|
|
206
216
|
<key>KeepAlive</key><true/>
|
|
207
217
|
<key>StandardOutPath</key><string>${path.join(CONFIG_DIR, ".dsh-bridge.log")}</string>
|
|
208
218
|
<key>StandardErrorPath</key><string>${path.join(CONFIG_DIR, ".dsh-bridge.log")}</string>
|
|
209
|
-
<key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string></dict>
|
|
219
|
+
<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>${INSTALL_SOURCE}</string><key>DSH_BRIDGE_INSTALL_VERSION</key><string>${INSTALL_VERSION}</string></dict>
|
|
210
220
|
</dict></plist>`;
|
|
211
221
|
fs.writeFileSync(plistPath, plist);
|
|
212
222
|
console.log(`✅ 已创建自启动服务: ${plistPath}`);
|
|
@@ -215,7 +225,7 @@ function writeAutostartFile() {
|
|
|
215
225
|
if (process.platform === "linux") {
|
|
216
226
|
const dir = path.join(os.homedir(), ".config/systemd/user");
|
|
217
227
|
fs.mkdirSync(dir, { recursive: true });
|
|
218
|
-
const unit = `[Unit]\nDescription=dsh-remote bridge (auto-starts with dsh web)\n\n[Service]\nExecStart=${runCmd}\nRestart=on-failure\nRestartSec=5\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\n\n[Install]\nWantedBy=default.target\n`;
|
|
228
|
+
const unit = `[Unit]\nDescription=dsh-remote bridge (auto-starts with dsh web)\n\n[Service]\nExecStart=${runCmd}\nRestart=on-failure\nRestartSec=5\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\nEnvironment=DSH_BRIDGE_INSTALL_SOURCE=${INSTALL_SOURCE}\nEnvironment=DSH_BRIDGE_INSTALL_VERSION=${INSTALL_VERSION}\n\n[Install]\nWantedBy=default.target\n`;
|
|
219
229
|
const unitPath = autostartFilePath();
|
|
220
230
|
fs.writeFileSync(unitPath, unit);
|
|
221
231
|
console.log(`✅ 已创建自启动服务: ${unitPath}`);
|
|
@@ -350,6 +360,9 @@ async function runBridge() {
|
|
|
350
360
|
...process.env,
|
|
351
361
|
DSH_BRIDGE_CONFIG: CONFIG_PATH,
|
|
352
362
|
DSH_BRIDGE_TUNNEL_URL: cfg.tunnel_url,
|
|
363
|
+
// 安装口径透传给 bridge(bridge 把它随设备登记请求一起上报给账号 API)
|
|
364
|
+
DSH_BRIDGE_INSTALL_SOURCE: INSTALL_SOURCE,
|
|
365
|
+
DSH_BRIDGE_INSTALL_VERSION: INSTALL_VERSION,
|
|
353
366
|
...(saas ? { DSH_BRIDGE_PHONE: (cfg.phone || cfg.email || ""), DSH_BRIDGE_PASSWORD: cfg.password, DSH_BRIDGE_API: apiUrl, DSH_BRIDGE_SECRET: cfg.bridge_secret || "" } : {}),
|
|
354
367
|
...(local ? { DSH_BRIDGE_LOCAL_KEY: cfg.local_key } : {})
|
|
355
368
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4-beta.1",
|
|
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",
|
|
@@ -49,13 +49,14 @@
|
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"test": "npm run test:router && npm run test:plugin && npm run test:bridge",
|
|
52
|
-
"test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs",
|
|
52
|
+
"test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs packages/relay-router/test/presence-report.test.mjs",
|
|
53
53
|
"_comment:test:plugin": "DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)",
|
|
54
54
|
"test:plugin": "DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
55
55
|
"test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
|
|
56
56
|
"check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && bash -n deploy/install-open.sh && npm run check:alias",
|
|
57
57
|
"sync:alias": "node scripts/sync-legacy-alias.mjs",
|
|
58
|
-
"check:alias": "node scripts/sync-legacy-alias.mjs --check"
|
|
58
|
+
"check:alias": "node scripts/sync-legacy-alias.mjs --check",
|
|
59
|
+
"release:tarballs": "bash scripts/release-tarballs.sh"
|
|
59
60
|
},
|
|
60
61
|
"dependencies": {
|
|
61
62
|
"ws": "^8.18.0"
|
|
@@ -408,6 +408,13 @@ window.__ModuleLoader__.load({
|
|
|
408
408
|
// 导致 busy 护栏失效(重复请求)或误拦(该重试却跳过)。
|
|
409
409
|
var akeyInFlight = { v: false };
|
|
410
410
|
var devInFlight = { v: false };
|
|
411
|
+
/** 连接阶段轮询的在途标记(同上:必须放组件外,否则闭包过期会重复请求)。 */
|
|
412
|
+
var connInFlight = { v: false };
|
|
413
|
+
/** 最近一次连接阶段(供自调度定时器决定快/慢轮询节奏,避免依赖渲染闭包)。 */
|
|
414
|
+
var connPhaseRef = { v: "" };
|
|
415
|
+
/** 连接阶段轮询节奏:未连通时 2.5s(自动推进),已连接后退避到 15s;页面隐藏时完全停下。 */
|
|
416
|
+
var CONN_POLL_FAST_MS = 2500;
|
|
417
|
+
var CONN_POLL_SLOW_MS = 15000;
|
|
411
418
|
function cancelAkeyRetry() { clearLater(akeyRetry.timer); akeyRetry.timer = null; }
|
|
412
419
|
function cancelDevRetry() { clearLater(devRetry.timer); devRetry.timer = null; }
|
|
413
420
|
|
|
@@ -1182,6 +1189,12 @@ window.__ModuleLoader__.load({
|
|
|
1182
1189
|
// ── 💬 交流群(「加入交流群」按钮 + 弹窗):同样放在全部既有字段之后 ──
|
|
1183
1190
|
var communityArr = useState(null); var community = communityArr[0]; var setCommunity = communityArr[1]; // {qrcode,wechat};null=未加载
|
|
1184
1191
|
var commOpenArr = useState(false); var commOpen = commOpenArr[0]; var setCommOpen = commOpenArr[1];
|
|
1192
|
+
// ── 🔗 登录后自动闭环:连接阶段(运行环境 → bridge 进程 → 设备已在中继注册=online) ──
|
|
1193
|
+
// conn 来自 GET /dsh-remote/bridge-status(node 半在返回前会自动补装/拉起/退避重试),
|
|
1194
|
+
// 面板据它显示「正在准备运行环境…/正在启动 Bridge…/正在连接中继…/已连接 ✅」,
|
|
1195
|
+
// 并用短轮询自动推进到 online —— 用户不需要点按钮、也不需要刷新页面。
|
|
1196
|
+
var connArr = useState(null); var conn = connArr[0]; var setConn = connArr[1];
|
|
1197
|
+
var copiedDiagArr = useState(false); var copiedDiag = copiedDiagArr[0]; var setCopiedDiag = copiedDiagArr[1];
|
|
1185
1198
|
|
|
1186
1199
|
var refresh = useCallback(function () {
|
|
1187
1200
|
setBusy("status");
|
|
@@ -1222,6 +1235,13 @@ window.__ModuleLoader__.load({
|
|
|
1222
1235
|
// 是否需要重启 DeepSeek harness(首次安装/在线更新后置顶提醒 + 底部常驻按钮)
|
|
1223
1236
|
var restartInfo = (st && st.restart) || {};
|
|
1224
1237
|
var restartPending = !!restartInfo.pending;
|
|
1238
|
+
// 连接阶段(node 半下发):轮询拿到的新鲜结果优先,其次用 /status 里带的那一份;
|
|
1239
|
+
// 旧版 host(响应里没有 connect 字段)→ 回退到原来的 service.running 文案(行为不变)。
|
|
1240
|
+
var connInfo = conn || (st && st.connect) || null;
|
|
1241
|
+
var connPhase = (connInfo && connInfo.phase) || "";
|
|
1242
|
+
var connOnline = connPhase === "online";
|
|
1243
|
+
var connText = (connInfo && connInfo.text) || "";
|
|
1244
|
+
var connError = connInfo && connInfo.error ? connInfo.error : null;
|
|
1225
1245
|
// 账号标识(掩码手机号):登录成功/换账号时变化 → 触发上方的重取 effect(比布尔 loggedIn 更敏感)
|
|
1226
1246
|
var phoneKey = (st && st.config && st.config.phone) || "";
|
|
1227
1247
|
var launchdPid = st && st.service && st.service.launchd && st.service.launchd.pid;
|
|
@@ -1531,6 +1551,87 @@ window.__ModuleLoader__.load({
|
|
|
1531
1551
|
};
|
|
1532
1552
|
}, [view, mode, loggedIn, phoneKey, serviceRunning]);
|
|
1533
1553
|
|
|
1554
|
+
// ── 登录后自动闭环(0.6.4):连接阶段短轮询 ──────────────────────────────
|
|
1555
|
+
// 登录成功(含从别处已登录、被面板读到)后立刻开始推进连接,之后自调度:
|
|
1556
|
+
// 非 online → 2.5s 一次(阶段自动前进,用户零操作);online → 15s 一次(退避,只做保活/新设备发现)。
|
|
1557
|
+
// 页面隐藏时 pollConnect 直接返回(不空转),回前台由 visibilitychange 立即补一次。
|
|
1558
|
+
// 依赖里只有「视图/模式/登录态」:阶段变化不重建定时器,避免每次推进都多发一次请求。
|
|
1559
|
+
useEffect(function () {
|
|
1560
|
+
if (view !== "home" || mode !== "saas" || !loggedIn) return undefined;
|
|
1561
|
+
var stopped = false;
|
|
1562
|
+
var timer = null;
|
|
1563
|
+
var tick = function () {
|
|
1564
|
+
if (stopped) return;
|
|
1565
|
+
clearLater(timer);
|
|
1566
|
+
timer = later(function () {
|
|
1567
|
+
timer = null;
|
|
1568
|
+
tick();
|
|
1569
|
+
}, connPhaseRef.v === "online" ? CONN_POLL_SLOW_MS : CONN_POLL_FAST_MS);
|
|
1570
|
+
pollConnect();
|
|
1571
|
+
};
|
|
1572
|
+
var onVisible = function () { if (!document.hidden && !stopped) pollConnect(); };
|
|
1573
|
+
try { document.addEventListener("visibilitychange", onVisible); } catch (e) { /* 环境无 document */ }
|
|
1574
|
+
if (!document.hidden) { pollConnect(); tick(); }
|
|
1575
|
+
return function () {
|
|
1576
|
+
stopped = true;
|
|
1577
|
+
clearLater(timer);
|
|
1578
|
+
try { document.removeEventListener("visibilitychange", onVisible); } catch (e) { /* 忽略 */ }
|
|
1579
|
+
};
|
|
1580
|
+
}, [view, mode, loggedIn]);
|
|
1581
|
+
|
|
1582
|
+
/**
|
|
1583
|
+
* 连接阶段轮询:GET /dsh-remote/bridge-status(node 半在返回前自动补装运行环境 / 拉起 bridge /
|
|
1584
|
+
* 清理卡死标记,并自带退避),面板据此自动推进「准备运行环境 → 启动 Bridge → 连接中继 → 已连接」。
|
|
1585
|
+
* 关键点:
|
|
1586
|
+
* - document.hidden 时完全不发请求(后台标签页不空转);回前台由 visibilitychange 立即补一次;
|
|
1587
|
+
* - 阶段推进到 online 时自动重取二维码与已授权设备列表 —— 用户不用刷新页面就能扫码。
|
|
1588
|
+
*/
|
|
1589
|
+
function pollConnect() {
|
|
1590
|
+
if (document.hidden) return;
|
|
1591
|
+
if (connInFlight.v) return;
|
|
1592
|
+
connInFlight.v = true;
|
|
1593
|
+
api("/dsh-remote/bridge-status").then(function (b) {
|
|
1594
|
+
if (!b || !b.ok || !b.connect) return;
|
|
1595
|
+
var prev = connPhaseRef.v;
|
|
1596
|
+
connPhaseRef.v = b.connect.phase || "";
|
|
1597
|
+
setConn(b.connect);
|
|
1598
|
+
if (b.connect.phase === "online") {
|
|
1599
|
+
// 已连接:立刻补一次二维码/设备列表(首次连上时 prev 不是 online → 无条件重取)
|
|
1600
|
+
if (prev !== "online") {
|
|
1601
|
+
akeyRetry.attempt = 0; devRetry.attempt = 0;
|
|
1602
|
+
loadAccessKey();
|
|
1603
|
+
loadDevices();
|
|
1604
|
+
} else if (devSessions !== null) {
|
|
1605
|
+
refreshDeviceList(); // 手机扫码后设备列表自动出现,无需刷新页面
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}).catch(function () { /* 轮询失败静默:下一轮自动重试 */ })
|
|
1609
|
+
.finally(function () { connInFlight.v = false; });
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
/** 「重试」按钮:POST /dsh-remote/connect/retry(清退避 + 立刻再走一遍闭环),随后刷新阶段。 */
|
|
1613
|
+
var retryConnect = function () {
|
|
1614
|
+
setBusy("connect-retry");
|
|
1615
|
+
post("/dsh-remote/connect/retry").then(function (b) {
|
|
1616
|
+
if (b && b.connect) { connPhaseRef.v = b.connect.phase || ""; setConn(b.connect); }
|
|
1617
|
+
setMsg("ok", "已重新开始连接,正在自动重试…");
|
|
1618
|
+
}).catch(function (e) {
|
|
1619
|
+
setMsg("err", "重试失败:" + e.message);
|
|
1620
|
+
}).finally(function () { setBusy(""); });
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
/** 「复制诊断信息」:版本 / 配置目录 / 阶段 / 最近错误 / 进程状态 / 日志路径(node 半已拼好)。 */
|
|
1624
|
+
var copyDiagnostics = function () {
|
|
1625
|
+
var text = (connInfo && connInfo.diagnostics) || "";
|
|
1626
|
+
if (!text) return;
|
|
1627
|
+
try {
|
|
1628
|
+
navigator.clipboard.writeText(text).then(function () {
|
|
1629
|
+
setCopiedDiag(true);
|
|
1630
|
+
later(function () { setCopiedDiag(false); }, 1500);
|
|
1631
|
+
});
|
|
1632
|
+
} catch (e) { /* 剪贴板不可用:文案里已给出日志路径,用户可自行查看 */ }
|
|
1633
|
+
};
|
|
1634
|
+
|
|
1534
1635
|
function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
|
|
1535
1636
|
|
|
1536
1637
|
// ---------- 登录(密码/短信) ----------
|
|
@@ -1588,7 +1689,10 @@ window.__ModuleLoader__.load({
|
|
|
1588
1689
|
if (rpass.length < 8) { setMsg("err", "密码至少 8 位"); return; }
|
|
1589
1690
|
if (rpass !== rpass2) { setMsg("err", "两次输入的密码不一致"); return; }
|
|
1590
1691
|
setBusy("register");
|
|
1591
|
-
|
|
1692
|
+
// reg_source:注册来源(增长口径)——面板内注册 = panel_register;
|
|
1693
|
+
// 手机端网页注册走 clients/dsh-web/native.html(= remoteweb_register)。
|
|
1694
|
+
// 节点半 /dsh-remote/register 全量透传 body,企业端白名单外的值会归一成 api_unknown。
|
|
1695
|
+
var regPayload = { phone: rphone.trim(), sms_code: rsms.trim(), password: rpass, reg_source: "panel_register" };
|
|
1592
1696
|
var invite = rInvite.trim().toUpperCase();
|
|
1593
1697
|
if (invite) regPayload.invite_code = invite;
|
|
1594
1698
|
if (rcap) { regPayload.captcha_id = rcap.id; regPayload.captcha_answer = rcapTxt.trim(); }
|
|
@@ -1947,12 +2051,59 @@ window.__ModuleLoader__.load({
|
|
|
1947
2051
|
var expMs = akey ? toMs(akey.expires_at) : 0;
|
|
1948
2052
|
var remainMs = expMs ? expMs - nowTick : 0;
|
|
1949
2053
|
var loggedInSaaS = !!(st && st.config && st.config.phone);
|
|
2054
|
+
// 状态行优先用连接阶段文案(node 半下发的 user-facing 文案):
|
|
2055
|
+
// 「正在准备运行环境(首次约 1~2 分钟)… / 正在启动 Bridge… / 正在连接中继… / 已连接 ✅ …」;
|
|
2056
|
+
// 旧版 host 没有 connect 字段 → 完全回退到原来的 running 文案(既有行为不变)。
|
|
1950
2057
|
var statusTxt = st === null
|
|
1951
2058
|
? "查询中…"
|
|
1952
2059
|
: !loggedInSaaS
|
|
1953
2060
|
? "请先登录(下方账号卡片)后启用远程访问"
|
|
1954
|
-
: serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
|
|
1955
|
-
var
|
|
2061
|
+
: connInfo ? connText : serviceRunning ? "已连接(可远程访问)" : "等待设备连接";
|
|
2062
|
+
var dotOn = loggedInSaaS ? (connInfo ? connOnline : serviceRunning) : false;
|
|
2063
|
+
var dotCls = "dru-dot " + (dotOn ? "dru-dot-on" : "dru-dot-off");
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* 连接阶段区块:把「环境 → bridge 进程 → 中继注册」的自动推进过程如实展示出来,
|
|
2067
|
+
* 面向非技术用户——非 online 阶段一律说明「正在自动进行,无需操作」;
|
|
2068
|
+
* 只有失败(error)才给可操作项:重试 / 复制诊断信息 / 日志路径,绝不出现死胡同。
|
|
2069
|
+
*/
|
|
2070
|
+
function renderConnectBlock() {
|
|
2071
|
+
if (!loggedInSaaS || !connInfo) return null;
|
|
2072
|
+
var phase = connInfo.phase || "";
|
|
2073
|
+
if (phase === "online") {
|
|
2074
|
+
return h("div", { className: "dru-meta", style: { marginTop: 6 } },
|
|
2075
|
+
connInfo.registerSource === "account_api" || connInfo.registerSource === "account" || connInfo.registerSource === "state_account"
|
|
2076
|
+
? "设备已登记到你的账号:手机端「设备列表」可以看到这台电脑。"
|
|
2077
|
+
: "设备已注册到中继:手机端扫码或打开链接即可进入这台电脑。");
|
|
2078
|
+
}
|
|
2079
|
+
var rows = [];
|
|
2080
|
+
if (phase === "error") {
|
|
2081
|
+
rows.push(h("div", { className: "dru-msg dru-msg-err", style: { marginTop: 6 } },
|
|
2082
|
+
"⚠️ " + ((connError && connError.message) || connText)));
|
|
2083
|
+
if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } }, connInfo.detail));
|
|
2084
|
+
if (connInfo.nextRetryInMs > 0) {
|
|
2085
|
+
rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
2086
|
+
"已自动重试 " + (connInfo.attempts || 0) + " 次,约 " + Math.ceil(connInfo.nextRetryInMs / 1000) + " 秒后自动再试(也可以点下面按钮立刻重试)"));
|
|
2087
|
+
}
|
|
2088
|
+
rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
2089
|
+
h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "重试中…" : "重试"),
|
|
2090
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
|
|
2091
|
+
rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } },
|
|
2092
|
+
"查看日志:bridge " + (connInfo.logPath || "") + " ,安装 " + (connInfo.installLogPath || "") + "(把「复制诊断信息」的内容发给客服可加速定位)"));
|
|
2093
|
+
return h("div", null, rows);
|
|
2094
|
+
}
|
|
2095
|
+
// 非错误阶段:只解释「正在自动做什么」,并说明不需要任何操作
|
|
2096
|
+
if (connInfo.detail) rows.push(h("div", { className: "dru-hint", style: { marginTop: 6 } }, connInfo.detail));
|
|
2097
|
+
if (connInfo.installing || phase === "no_runtime") {
|
|
2098
|
+
rows.push(h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
2099
|
+
"首次安装会自动下载并配置,期间请不要关闭 DeepSeek;装完会自动启动 bridge,无需任何操作。"));
|
|
2100
|
+
}
|
|
2101
|
+
if (connInfo.deviceId) rows.push(h("div", { className: "dru-meta" }, "设备 ID:" + connInfo.deviceId));
|
|
2102
|
+
rows.push(h("div", { className: "dru-actions", style: { marginTop: 8 } },
|
|
2103
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", disabled: busy !== "", onClick: retryConnect }, busy === "connect-retry" ? "立即重试中…" : "立即重试"),
|
|
2104
|
+
h("button", { type: "button", className: "dru-btn dru-btn-ghost", onClick: copyDiagnostics }, copiedDiag ? "已复制" : "复制诊断信息")));
|
|
2105
|
+
return h("div", null, rows);
|
|
2106
|
+
}
|
|
1956
2107
|
// Phase-5:端到端加密(E2EE)状态行 —— 未登录/旧 host 未下发 e2ee 一律不渲染
|
|
1957
2108
|
// (桌面宽屏与手机镜像共用同一面板:纯文字状态行、不弹层不打扰);启用=绿点绿字(🔒已启用),
|
|
1958
2109
|
// 未启用=灰字 + 中性原因文案(回退普通 HTTPS 连接,不宣称“灰度等待”)。
|
|
@@ -1969,6 +2120,7 @@ window.__ModuleLoader__.load({
|
|
|
1969
2120
|
h("span", { className: dotCls }),
|
|
1970
2121
|
h("span", null, statusTxt)
|
|
1971
2122
|
),
|
|
2123
|
+
renderConnectBlock(),
|
|
1972
2124
|
renderE2eeBadge(),
|
|
1973
2125
|
akeyMsg
|
|
1974
2126
|
? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } },
|
|
@@ -1999,7 +2151,9 @@ window.__ModuleLoader__.load({
|
|
|
1999
2151
|
),
|
|
2000
2152
|
h("div", { className: "dru-hint", style: { marginTop: 4 } },
|
|
2001
2153
|
!serviceRunning
|
|
2002
|
-
?
|
|
2154
|
+
? (connInfo && connPhase !== "online"
|
|
2155
|
+
? "本机 bridge 正在自动准备中(" + connText + "):连上后这个链接/二维码即可使用,无需其他操作。"
|
|
2156
|
+
: "本机 bridge 未运行:先在下方「🖥 Bridge 服务」卡启动。")
|
|
2003
2157
|
: "打开链接/扫码进入即登录态;同设备重复扫码只更新授权,不新增设备。")
|
|
2004
2158
|
)
|
|
2005
2159
|
)
|
|
@@ -2195,6 +2349,11 @@ window.__ModuleLoader__.load({
|
|
|
2195
2349
|
h("span", null, st ? serviceStateText : "查询中…"),
|
|
2196
2350
|
launchdPid ? h("span", { className: "dru-meta", style: { marginTop: 0 } }, "(pid=" + launchdPid + ")") : null
|
|
2197
2351
|
),
|
|
2352
|
+
// 自动连接进度(登录后由面板 2.5s 短轮询自动推进):让用户在 bridge 卡也能一眼看到
|
|
2353
|
+
// 「到底走到哪一步了」——进程在跑≠能用,注册到中继才算。
|
|
2354
|
+
connInfo && connPhase && connPhase !== "no_account"
|
|
2355
|
+
? h("div", { className: "dru-hint", style: { marginTop: 6 } }, "自动连接进度:" + connText)
|
|
2356
|
+
: null,
|
|
2198
2357
|
h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
2199
2358
|
!serviceRunning
|
|
2200
2359
|
? h("button", { type: "button", className: "dru-btn dru-btn-primary", disabled: busy !== "", onClick: function () { toggleBridge(true); } }, busy === "start" ? "启动中…" : "启动 bridge")
|