@mrrisega/dsh-remote 0.6.0 → 0.6.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 +14 -1
- package/package.json +4 -2
- package/packages/dsh-remote-web/lib/client.js +220 -17
- package/packages/dsh-remote-web/lib/index.js +285 -30
- package/packages/dsh-remote-web/package.json +2 -2
- package/packages/dsh-remote-web/test/community-qr-ui.test.mjs +287 -0
- package/packages/dsh-remote-web/test/first-login-selfheal.test.mjs +247 -0
- package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +307 -0
package/README.md
CHANGED
|
@@ -28,7 +28,12 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
|
|
|
28
28
|
└→ bridge (电脑端) → 127.0.0.1:3080 (dsh web)
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
## 0.6.
|
|
31
|
+
## 0.6.1 速览(用户最关心的新能力)
|
|
32
|
+
|
|
33
|
+
- **首次安装体验修复**:装完插件立即登录,二维码与已授权设备列表不再报「尚未登录」红字;中继未就绪时面板自动退避重试,不用再手动刷新页面。
|
|
34
|
+
- **企微交流群**:README 底部扫码入群;服务端可在管理后台「推广」页上传交流群二维码,官网、付费页底部、设置面板「加入交流群」按钮与用户反馈页会同步展示。
|
|
35
|
+
|
|
36
|
+
**0.6.0 起已有**
|
|
32
37
|
|
|
33
38
|
- **扫码即加密,不再要求再输一次密码**:用电脑生成的二维码/一次性链接进入后,由电脑自动授权开启端到端加密;密码登录点设备也自动解锁。全程不输 E2EE 密码、不存服务端。
|
|
34
39
|
- **记住本机:刷新/重开不掉回明文**:解锁一次后,手机 App 刷新、镜像页刷新都自动恢复加密(退出登录即清除;浏览器无痕可不用此功能)。
|
|
@@ -151,6 +156,14 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
|
|
|
151
156
|
|
|
152
157
|
测试:`npm test`(router 契约 + 插件 + bridge 全部单测与回归)。
|
|
153
158
|
|
|
159
|
+
## 企微交流群
|
|
160
|
+
|
|
161
|
+
扫码加入 **企微交流群**:安装/使用答疑、问题反馈、版本更新都会在群里同步,欢迎来聊。
|
|
162
|
+
|
|
163
|
+
<img src="image/企微交流群.jpg" alt="企微交流群" width="240">
|
|
164
|
+
|
|
165
|
+
> 也可以在仓库提 [Issue](https://github.com/mrRisega/dsh-remote/issues);安全相关问题请按 [SECURITY.md](SECURITY.md) 私下反馈。
|
|
166
|
+
|
|
154
167
|
## 文档
|
|
155
168
|
|
|
156
169
|
- [docs/self-hosting.md](docs/self-hosting.md) — 开源自建完整指南(含安全提示)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.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",
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
"test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs",
|
|
53
53
|
"test:plugin": "node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
|
|
54
54
|
"test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
|
|
55
|
-
"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"
|
|
55
|
+
"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",
|
|
56
|
+
"sync:alias": "node scripts/sync-legacy-alias.mjs",
|
|
57
|
+
"check:alias": "node scripts/sync-legacy-alias.mjs --check"
|
|
56
58
|
},
|
|
57
59
|
"dependencies": {
|
|
58
60
|
"ws": "^8.18.0"
|
|
@@ -134,6 +134,9 @@ window.__ModuleLoader__.load({
|
|
|
134
134
|
".dru-popup-foot button{border:none;background:none;color:#57606a;cursor:pointer;font-size:12px;font-family:inherit;padding:4px 6px}",
|
|
135
135
|
".dru-popup-foot button:hover{color:#0969da}",
|
|
136
136
|
".dru-popup .dru-msg{text-align:left}",
|
|
137
|
+
".dru-community-qr{display:block;width:220px;max-width:62vw;margin:0 auto;background:#ffffff;padding:10px;border-radius:10px;border:1px solid #d0d7de;box-sizing:content-box}",
|
|
138
|
+
".dru-fb-community{margin-top:16px;padding-top:14px;border-top:1px dashed #d0d7de;text-align:center}",
|
|
139
|
+
".dru-fb-community .dru-community-qr{width:180px;max-width:56vw}",
|
|
137
140
|
// ── 自管理:版本与更新(插件面板内提供在线更新/彻底卸载,市场无更新按钮) ──
|
|
138
141
|
".dru-ver-badge{display:inline-block;font-size:11px;border-radius:999px;padding:1px 8px;margin-left:6px;vertical-align:1px}",
|
|
139
142
|
".dru-ver-badge-new{color:#9a6700;background:#fff8c5;border:1px solid #eed888}",
|
|
@@ -347,6 +350,60 @@ window.__ModuleLoader__.load({
|
|
|
347
350
|
return api(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(data || {}) });
|
|
348
351
|
};
|
|
349
352
|
|
|
353
|
+
// ── 交流群(运营二维码;管理后台统一上传,node 半经公开配置下发) ─────────
|
|
354
|
+
// 设置面板「加入交流群」按钮与用户反馈页都展示同一张码;未配置(qrcode 为空)→ 不展示任何入口。
|
|
355
|
+
var COMMUNITY_TTL_MS = 5 * 60 * 1000; // 面板/反馈卡共用缓存:5 分钟内不重复请求
|
|
356
|
+
var communityCache = { at: 0, data: null };
|
|
357
|
+
/** 取交流群信息(模块级缓存;失败视为未配置,不打扰用户)。 */
|
|
358
|
+
function loadCommunity(force) {
|
|
359
|
+
if (!force && communityCache.data && Date.now() - communityCache.at < COMMUNITY_TTL_MS) {
|
|
360
|
+
return Promise.resolve(communityCache.data);
|
|
361
|
+
}
|
|
362
|
+
return api("/dsh-remote/community").then(function (b) {
|
|
363
|
+
communityCache = { at: Date.now(), data: { qrcode: (b && b.qrcode) || "", wechat: (b && b.wechat) || "" } };
|
|
364
|
+
return communityCache.data;
|
|
365
|
+
}).catch(function () {
|
|
366
|
+
communityCache = { at: Date.now(), data: { qrcode: "", wechat: "" } };
|
|
367
|
+
return communityCache.data;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── 登录后首次加载的自愈(0.6.1) ───────────────────────────────
|
|
372
|
+
// 现象(首次安装后立即登录):面板在 loggedIn 翻真的瞬间就请求 /dsh-remote/access-key 与
|
|
373
|
+
// /dsh-remote/mobile-sessions;此时中继握手可能尚未就绪(bridge 刚被重启、device-login 共享密钥
|
|
374
|
+
// 刚补齐或正在轮换、中继冷启动/网络抖动),旧版只报一次红字且不重试——必须手动刷新页面才行。
|
|
375
|
+
// 现在:可重试类失败按退避自动重试(成功即自动清除红字),并给红字配「重试」按钮。
|
|
376
|
+
var LOGIN_LOAD_RETRY_MS = [1200, 3000, 6000];
|
|
377
|
+
|
|
378
|
+
/** node 半标记 retryable(中继未就绪)或网络/5xx → 值得自动重试;未登录/密码失效不重试。 */
|
|
379
|
+
function retryableFail(e) {
|
|
380
|
+
var body = e && e.body;
|
|
381
|
+
if (body && body.retryable === true) return true;
|
|
382
|
+
if (body && typeof body.hint === "string" && /^relay_/.test(body.hint)) return true;
|
|
383
|
+
var st = e && e.status;
|
|
384
|
+
if (typeof st !== "number") return true; // fetch 本身失败(网络中断/被中止)
|
|
385
|
+
return st === 0 || st === 408 || st === 429 || st >= 500;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 定时器节流:tests/SSR 环境没有 setTimeout 时静默降级(不抛错、不阻塞面板)。 */
|
|
389
|
+
function later(fn, ms) {
|
|
390
|
+
try { return setTimeout(fn, ms); } catch (e) { return null; }
|
|
391
|
+
}
|
|
392
|
+
function clearLater(t) {
|
|
393
|
+
if (t == null) return;
|
|
394
|
+
try { clearTimeout(t); } catch (e) { /* 忽略 */ }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** 一次性访问密钥 / 已授权设备列表的自动重试状态(放在组件外:不随渲染重建)。 */
|
|
398
|
+
var akeyRetry = { timer: null, attempt: 0 };
|
|
399
|
+
var devRetry = { timer: null, attempt: 0 };
|
|
400
|
+
// 请求在途标记同样放组件外:组件内 state 闭包会在定时器/轮询回调里过期,
|
|
401
|
+
// 导致 busy 护栏失效(重复请求)或误拦(该重试却跳过)。
|
|
402
|
+
var akeyInFlight = { v: false };
|
|
403
|
+
var devInFlight = { v: false };
|
|
404
|
+
function cancelAkeyRetry() { clearLater(akeyRetry.timer); akeyRetry.timer = null; }
|
|
405
|
+
function cancelDevRetry() { clearLater(devRetry.timer); devRetry.timer = null; }
|
|
406
|
+
|
|
350
407
|
/** 一次性访问 url → 升级/续费页 url(带同一 auth,进入后即登录态到 /app/promo)。 */
|
|
351
408
|
function promoUrlOf(u) {
|
|
352
409
|
var s = String(u || "");
|
|
@@ -442,6 +499,8 @@ window.__ModuleLoader__.load({
|
|
|
442
499
|
var mineListArr = useState(null); var mineList = mineListArr[0]; var setMineList = mineListArr[1];
|
|
443
500
|
var mineErrArr = useState(false); var mineErr = mineErrArr[0]; var setMineErr = mineErrArr[1];
|
|
444
501
|
var mineBusyArr = useState(false); var mineBusy = mineBusyArr[0]; var setMineBusy = mineBusyArr[1];
|
|
502
|
+
// 💬 交流群:反馈页底部展示「加入交流群」二维码(与设置面板共用同一份缓存)
|
|
503
|
+
var commArr = useState(communityCache.data); var comm = commArr[0]; var setComm = commArr[1];
|
|
445
504
|
|
|
446
505
|
function setMsg(kind, text) { setFbMsg({ kind: kind, text: text }); }
|
|
447
506
|
|
|
@@ -449,6 +508,12 @@ window.__ModuleLoader__.load({
|
|
|
449
508
|
// 自建/未登录(cfg.phone 为空)→ 仍走本地 thread_token,不发 /mine。
|
|
450
509
|
var fbAccount = !!(cfg && cfg.phone && fbAuth === "account");
|
|
451
510
|
|
|
511
|
+
useEffect(function () {
|
|
512
|
+
var alive = true;
|
|
513
|
+
loadCommunity().then(function (d) { if (alive) setComm(d); });
|
|
514
|
+
return function () { alive = false; };
|
|
515
|
+
}, []);
|
|
516
|
+
|
|
452
517
|
var loadCfg = useCallback(function () {
|
|
453
518
|
api("/dsh-remote/feedback-config").then(function (b) {
|
|
454
519
|
setCfg(b);
|
|
@@ -688,7 +753,15 @@ window.__ModuleLoader__.load({
|
|
|
688
753
|
h("button", { type: "button", className: "dru-btn dru-btn-primary", style: { width: "100%" }, disabled: busy !== "", onClick: doSubmit }, busy === "submit" ? "提交中…" : "提交反馈"),
|
|
689
754
|
fbMsg && h("div", { className: "dru-msg dru-msg-" + fbMsg.kind }, fbMsg.text)
|
|
690
755
|
)
|
|
691
|
-
: renderMine()
|
|
756
|
+
: renderMine(),
|
|
757
|
+
// 反馈页底部:企微交流群二维码(后台上传后出现;未配置不展示)
|
|
758
|
+
comm && comm.qrcode
|
|
759
|
+
? h("div", { className: "dru-fb-community" },
|
|
760
|
+
h("div", { className: "dru-fb-community-title" }, "💬 加入企微交流群"),
|
|
761
|
+
h("div", { className: "dru-hint", style: { marginBottom: 8 } }, "扫码入群:安装答疑 / 使用技巧 / 版本更新 / 问题反馈"),
|
|
762
|
+
h("img", { className: "dru-community-qr", src: comm.qrcode, alt: "企微交流群二维码" }),
|
|
763
|
+
comm.wechat ? h("div", { className: "dru-hint", style: { marginTop: 8 } }, "二维码失效或群满,可加客服微信:" + comm.wechat) : null)
|
|
764
|
+
: null
|
|
692
765
|
);
|
|
693
766
|
}
|
|
694
767
|
|
|
@@ -1099,6 +1172,9 @@ window.__ModuleLoader__.load({
|
|
|
1099
1172
|
var pwdSmsArr = useState(""); var pwdSms = pwdSmsArr[0]; var setPwdSms = pwdSmsArr[1];
|
|
1100
1173
|
var pwdSmsBtnArr = useState("获取验证码"); var pwdSmsBtn = pwdSmsBtnArr[0]; var setPwdSmsBtn = pwdSmsBtnArr[1];
|
|
1101
1174
|
var pwdNewArr = useState(""); var pwdNew = pwdNewArr[0]; var setPwdNew = pwdNewArr[1]; // 新密码(≥8)
|
|
1175
|
+
// ── 💬 交流群(「加入交流群」按钮 + 弹窗):同样放在全部既有字段之后 ──
|
|
1176
|
+
var communityArr = useState(null); var community = communityArr[0]; var setCommunity = communityArr[1]; // {qrcode,wechat};null=未加载
|
|
1177
|
+
var commOpenArr = useState(false); var commOpen = commOpenArr[0]; var setCommOpen = commOpenArr[1];
|
|
1102
1178
|
|
|
1103
1179
|
var refresh = useCallback(function () {
|
|
1104
1180
|
setBusy("status");
|
|
@@ -1126,6 +1202,8 @@ window.__ModuleLoader__.load({
|
|
|
1126
1202
|
|
|
1127
1203
|
var loggedIn = !!(st && st.config && (st.config.phone || st.config.hasLocalKey));
|
|
1128
1204
|
var serviceRunning = !!(st && st.service && st.service.running);
|
|
1205
|
+
// 账号标识(掩码手机号):登录成功/换账号时变化 → 触发上方的重取 effect(比布尔 loggedIn 更敏感)
|
|
1206
|
+
var phoneKey = (st && st.config && st.config.phone) || "";
|
|
1129
1207
|
var launchdPid = st && st.service && st.service.launchd && st.service.launchd.pid;
|
|
1130
1208
|
|
|
1131
1209
|
// 已登录(SaaS)→ 拉我的信息/配额/公共配置
|
|
@@ -1137,6 +1215,13 @@ window.__ModuleLoader__.load({
|
|
|
1137
1215
|
}, [st && st.config && st.config.phone]);
|
|
1138
1216
|
|
|
1139
1217
|
useEffect(function () { refresh(); }, [refresh]);
|
|
1218
|
+
|
|
1219
|
+
// 交流群二维码(后台上传即可展示;未配置 → community.qrcode 为空,入口不出现)
|
|
1220
|
+
useEffect(function () {
|
|
1221
|
+
var alive = true;
|
|
1222
|
+
loadCommunity().then(function (d) { if (alive) setCommunity(d); });
|
|
1223
|
+
return function () { alive = false; };
|
|
1224
|
+
}, []);
|
|
1140
1225
|
useEffect(function () {
|
|
1141
1226
|
// 展开「忘记密码」重置表单时不用预载登录表单验证码(收起的 reset 表单用独立 pwdCap)
|
|
1142
1227
|
if (st !== null && !loggedIn && mode === "saas" && authTab === "login" && !pwdOpen && !lcap) loadCaptcha("login");
|
|
@@ -1184,20 +1269,26 @@ window.__ModuleLoader__.load({
|
|
|
1184
1269
|
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " " + pad2(d.getHours()) + ":" + pad2(d.getMinutes());
|
|
1185
1270
|
}
|
|
1186
1271
|
|
|
1187
|
-
/**
|
|
1272
|
+
/**
|
|
1273
|
+
* 创建(或刷新)一次性访问密钥:GET /dsh-remote/access-key(node 半转发企业端 /api/auth-key)。
|
|
1274
|
+
* 失败自愈:中继未就绪类失败(node 半 retryable / 网络 / 5xx)按退避自动重试,
|
|
1275
|
+
* 见「登录后首次加载自愈」——首次安装立即登录时不再需要手动刷新页面。
|
|
1276
|
+
*/
|
|
1188
1277
|
function loadAccessKey() {
|
|
1189
|
-
if (
|
|
1278
|
+
if (akeyInFlight.v) return;
|
|
1190
1279
|
// 登录前不请求企业端(避免 401/404 噪音);由账号登录成功后触发
|
|
1191
1280
|
if (!(st && st.config && st.config.phone)) {
|
|
1192
1281
|
setAkeyMsg(null);
|
|
1282
|
+
cancelAkeyRetry();
|
|
1193
1283
|
return;
|
|
1194
1284
|
}
|
|
1285
|
+
akeyInFlight.v = true;
|
|
1195
1286
|
setAkeyBusy(true);
|
|
1196
1287
|
api("/dsh-remote/access-key").then(function (b) {
|
|
1197
1288
|
if (!b || !b.ok) {
|
|
1198
1289
|
var why = (b && (b.error || (b.body && b.body.error))) || "未知错误";
|
|
1199
|
-
|
|
1200
|
-
|
|
1290
|
+
var e = new Error(why); e.body = b || null;
|
|
1291
|
+
throw e;
|
|
1201
1292
|
}
|
|
1202
1293
|
// 契约容错:url 必须有;qr_data_url 取不到时仍展示链接/复制/直接打开
|
|
1203
1294
|
if (!b.url) {
|
|
@@ -1212,9 +1303,16 @@ window.__ModuleLoader__.load({
|
|
|
1212
1303
|
qr_data_url: b.qr_data_url != null ? b.qr_data_url : null
|
|
1213
1304
|
});
|
|
1214
1305
|
setAkeyMsg(null);
|
|
1306
|
+
akeyRetry.attempt = 0;
|
|
1215
1307
|
}).catch(function (e) {
|
|
1216
|
-
|
|
1217
|
-
|
|
1308
|
+
var text = "获取一次性访问地址失败:" + e.message;
|
|
1309
|
+
if (retryableFail(e) && akeyRetry.attempt < LOGIN_LOAD_RETRY_MS.length) {
|
|
1310
|
+
scheduleAkeyRetry(text);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
// 自动重试额度用尽(或不该重试)→ 红字 + 「立即重试」入口,不让用户只能刷新页面
|
|
1314
|
+
setAkeyMsg({ kind: "err", text: text, retry: retryAkeyManually });
|
|
1315
|
+
}).finally(function () { akeyInFlight.v = false; setAkeyBusy(false); });
|
|
1218
1316
|
}
|
|
1219
1317
|
|
|
1220
1318
|
var copyKeyUrl = function () {
|
|
@@ -1232,17 +1330,67 @@ window.__ModuleLoader__.load({
|
|
|
1232
1330
|
try { window.open(akey.url, "_blank", "noopener"); } catch (e) {}
|
|
1233
1331
|
};
|
|
1234
1332
|
|
|
1235
|
-
/** 加载已授权设备列表:GET /dsh-remote/mobile-sessions
|
|
1333
|
+
/** 加载已授权设备列表:GET /dsh-remote/mobile-sessions(失败同上:可重试类自动重试)。 */
|
|
1236
1334
|
function loadDevices() {
|
|
1237
|
-
if (devBusy !== "") return;
|
|
1335
|
+
if (devInFlight.v || devBusy !== "") return;
|
|
1336
|
+
devInFlight.v = true;
|
|
1238
1337
|
setDevBusy("list");
|
|
1239
1338
|
api("/dsh-remote/mobile-sessions").then(function (b) {
|
|
1240
1339
|
if (!b || !b.ok) throw new Error((b && b.error) || "加载已授权设备失败");
|
|
1241
1340
|
setDevSessions(Array.isArray(b.sessions) ? b.sessions : []);
|
|
1242
1341
|
setDevMsg(null);
|
|
1342
|
+
devRetry.attempt = 0;
|
|
1243
1343
|
}).catch(function (e) {
|
|
1244
|
-
|
|
1245
|
-
|
|
1344
|
+
var text = "加载已授权设备失败:" + e.message;
|
|
1345
|
+
if (retryableFail(e) && devRetry.attempt < LOGIN_LOAD_RETRY_MS.length) {
|
|
1346
|
+
scheduleDevRetry(text);
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
setDevMsg({ kind: "err", text: text, retry: retryDevicesManually });
|
|
1350
|
+
}).finally(function () { devInFlight.v = false; setDevBusy(""); });
|
|
1351
|
+
}
|
|
1352
|
+
/** 手动重试(红字旁的「重试」按钮):重置退避计数后立即重新拉取。 */
|
|
1353
|
+
var retryAkeyManually = function () {
|
|
1354
|
+
cancelAkeyRetry();
|
|
1355
|
+
akeyRetry.attempt = 0;
|
|
1356
|
+
setAkeyMsg(null);
|
|
1357
|
+
loadAccessKey();
|
|
1358
|
+
};
|
|
1359
|
+
var retryDevicesManually = function () {
|
|
1360
|
+
cancelDevRetry();
|
|
1361
|
+
devRetry.attempt = 0;
|
|
1362
|
+
setDevMsg(null);
|
|
1363
|
+
loadDevices();
|
|
1364
|
+
};
|
|
1365
|
+
/** 定时重试一次性访问密钥(黄字提示 + 退避;成功时由 loadAccessKey 清掉提示)。 */
|
|
1366
|
+
function scheduleAkeyRetry(why) {
|
|
1367
|
+
cancelAkeyRetry();
|
|
1368
|
+
var wait = LOGIN_LOAD_RETRY_MS[akeyRetry.attempt] || LOGIN_LOAD_RETRY_MS[LOGIN_LOAD_RETRY_MS.length - 1];
|
|
1369
|
+
akeyRetry.attempt += 1;
|
|
1370
|
+
setAkeyMsg({
|
|
1371
|
+
kind: "warn",
|
|
1372
|
+
text: "中继连接尚未就绪," + Math.round(wait / 1000) + " 秒后自动重试(第 " + akeyRetry.attempt + "/" + LOGIN_LOAD_RETRY_MS.length + " 次):" + why,
|
|
1373
|
+
retry: retryAkeyManually
|
|
1374
|
+
});
|
|
1375
|
+
akeyRetry.timer = later(function () {
|
|
1376
|
+
akeyRetry.timer = null;
|
|
1377
|
+
loadAccessKey(); // 在途标记由上一个请求的 finally 释放,退避窗口内用户也可手动刷新
|
|
1378
|
+
}, wait);
|
|
1379
|
+
}
|
|
1380
|
+
/** 定时重试已授权设备列表(同上)。 */
|
|
1381
|
+
function scheduleDevRetry(why) {
|
|
1382
|
+
cancelDevRetry();
|
|
1383
|
+
var wait = LOGIN_LOAD_RETRY_MS[devRetry.attempt] || LOGIN_LOAD_RETRY_MS[LOGIN_LOAD_RETRY_MS.length - 1];
|
|
1384
|
+
devRetry.attempt += 1;
|
|
1385
|
+
setDevMsg({
|
|
1386
|
+
kind: "warn",
|
|
1387
|
+
text: "中继连接尚未就绪," + Math.round(wait / 1000) + " 秒后自动重试(第 " + devRetry.attempt + "/" + LOGIN_LOAD_RETRY_MS.length + " 次):" + why,
|
|
1388
|
+
retry: retryDevicesManually
|
|
1389
|
+
});
|
|
1390
|
+
devRetry.timer = later(function () {
|
|
1391
|
+
devRetry.timer = null;
|
|
1392
|
+
loadDevices();
|
|
1393
|
+
}, wait);
|
|
1246
1394
|
}
|
|
1247
1395
|
/** 操作(取消配对/删除记录/清理已解绑)成功后静默重拉列表,覆盖行内状态。 */
|
|
1248
1396
|
function refreshDeviceList() {
|
|
@@ -1346,13 +1494,22 @@ window.__ModuleLoader__.load({
|
|
|
1346
1494
|
|
|
1347
1495
|
// 已登录云端主视图停留期间:打开即取一把新 key + 已授权设备;之后每 ~25s 自动轮换
|
|
1348
1496
|
// (未登录不轮询,避免 401 空转;离开栏目/切视图/退出登录即清理定时器)
|
|
1497
|
+
//
|
|
1498
|
+
// 依赖里带上「账号」与「bridge 运行态」:登录成功(含换账号)与 bridge 拉起/重启后立即重取,
|
|
1499
|
+
// 首次安装后立即登录不再需要手动刷新页面;失败重试由 loadAccessKey/loadDevices 内部退避处理。
|
|
1349
1500
|
useEffect(function () {
|
|
1350
1501
|
if (view !== "home" || mode !== "saas" || !loggedIn) return undefined;
|
|
1502
|
+
cancelAkeyRetry(); cancelDevRetry();
|
|
1503
|
+
akeyRetry.attempt = 0; devRetry.attempt = 0;
|
|
1351
1504
|
loadAccessKey();
|
|
1352
1505
|
loadDevices();
|
|
1353
1506
|
var rotateIv = setInterval(function () { loadAccessKey(); }, KEY_AUTO_REFRESH_MS);
|
|
1354
|
-
return function () {
|
|
1355
|
-
|
|
1507
|
+
return function () {
|
|
1508
|
+
clearInterval(rotateIv);
|
|
1509
|
+
cancelAkeyRetry();
|
|
1510
|
+
cancelDevRetry();
|
|
1511
|
+
};
|
|
1512
|
+
}, [view, mode, loggedIn, phoneKey, serviceRunning]);
|
|
1356
1513
|
|
|
1357
1514
|
function setMsg(kind, text) { setMessage({ kind: kind, text: text }); }
|
|
1358
1515
|
|
|
@@ -1760,7 +1917,13 @@ window.__ModuleLoader__.load({
|
|
|
1760
1917
|
h("span", null, statusTxt)
|
|
1761
1918
|
),
|
|
1762
1919
|
renderE2eeBadge(),
|
|
1763
|
-
akeyMsg
|
|
1920
|
+
akeyMsg
|
|
1921
|
+
? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } },
|
|
1922
|
+
akeyMsg.text,
|
|
1923
|
+
akeyMsg.retry
|
|
1924
|
+
? h("button", { type: "button", className: "dru-linkbtn", style: { marginLeft: 8 }, disabled: akeyBusy, onClick: akeyMsg.retry }, akeyBusy ? "重试中…" : "立即重试")
|
|
1925
|
+
: null)
|
|
1926
|
+
: null,
|
|
1764
1927
|
hasKey ? h("div", null, [
|
|
1765
1928
|
h("div", { className: "dru-url big", style: { marginTop: 8 } },
|
|
1766
1929
|
h("span", null, akey.url),
|
|
@@ -1883,7 +2046,13 @@ window.__ModuleLoader__.load({
|
|
|
1883
2046
|
}, devBusy === "list" ? "加载中…" : btnLabel)
|
|
1884
2047
|
),
|
|
1885
2048
|
devOpen ? renderDeviceList() : null,
|
|
1886
|
-
devMsg
|
|
2049
|
+
devMsg
|
|
2050
|
+
? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 6 } },
|
|
2051
|
+
devMsg.text,
|
|
2052
|
+
devMsg.retry
|
|
2053
|
+
? h("button", { type: "button", className: "dru-linkbtn", style: { marginLeft: 8 }, disabled: devBusy !== "", onClick: devMsg.retry }, devBusy === "list" ? "重试中…" : "立即重试")
|
|
2054
|
+
: null)
|
|
2055
|
+
: null
|
|
1887
2056
|
]);
|
|
1888
2057
|
}
|
|
1889
2058
|
|
|
@@ -1995,7 +2164,17 @@ window.__ModuleLoader__.load({
|
|
|
1995
2164
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "📱 远程访问:用手机或另一台电脑的浏览器,随时随地使用同一份 dsh web——人在哪都能用(免公网 IP、免内网穿透);官方托管中继,4G/5G 即用,也可自建服务。"),
|
|
1996
2165
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
|
|
1997
2166
|
h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
|
|
1998
|
-
h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。")
|
|
2167
|
+
h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。"),
|
|
2168
|
+
// 加入交流群(后台上传二维码后出现;点击弹出二维码大图便于扫码)
|
|
2169
|
+
community && community.qrcode
|
|
2170
|
+
? h("div", { className: "dru-actions", style: { marginTop: 10 } },
|
|
2171
|
+
h("button", {
|
|
2172
|
+
type: "button",
|
|
2173
|
+
className: "dru-btn dru-btn-ghost",
|
|
2174
|
+
style: { width: "100%" },
|
|
2175
|
+
onClick: function () { setCommOpen(true); }
|
|
2176
|
+
}, "💬 加入交流群"))
|
|
2177
|
+
: null
|
|
1999
2178
|
]),
|
|
2000
2179
|
// 版本与更新(自管理:检测新版 / 一键在线更新 / 彻底卸载)
|
|
2001
2180
|
h(SelfManageCard, null),
|
|
@@ -2003,6 +2182,29 @@ window.__ModuleLoader__.load({
|
|
|
2003
2182
|
);
|
|
2004
2183
|
}
|
|
2005
2184
|
|
|
2185
|
+
/** 交流群弹窗(复用满意度弹窗的 dru-popup 视觉;点遮罩/关闭即收起)。 */
|
|
2186
|
+
function renderCommunityModal() {
|
|
2187
|
+
if (!commOpen || !(community && community.qrcode)) return null;
|
|
2188
|
+
var close = function () { setCommOpen(false); };
|
|
2189
|
+
return h("div", { className: "dru-popup", onMouseDown: function (e) { if (e.target === e.currentTarget) close(); } },
|
|
2190
|
+
h("div", { className: "dru-popup-card", role: "dialog", "aria-label": "加入企微交流群" },
|
|
2191
|
+
h("div", { className: "dru-popup-body" },
|
|
2192
|
+
h("div", { className: "dru-popup-icon" }, "💬"),
|
|
2193
|
+
h("div", { className: "dru-popup-title" }, "加入企微交流群"),
|
|
2194
|
+
h("div", { className: "dru-popup-sub" }, "扫码入群:安装答疑 / 使用技巧 / 版本更新 / 问题反馈"),
|
|
2195
|
+
h("img", { className: "dru-community-qr", src: community.qrcode, alt: "企微交流群二维码" }),
|
|
2196
|
+
community.wechat
|
|
2197
|
+
? h("div", { className: "dru-hint", style: { marginTop: 10 } }, "二维码失效或群满,可加客服微信:" + community.wechat)
|
|
2198
|
+
: h("div", { className: "dru-hint", style: { marginTop: 10 } }, "二维码失效或群满,可到 GitHub 提 Issue 联系作者")
|
|
2199
|
+
),
|
|
2200
|
+
h("div", { className: "dru-popup-foot" },
|
|
2201
|
+
h("span", null, "手机微信/企业微信扫码即可入群"),
|
|
2202
|
+
h("button", { type: "button", onClick: close }, "关闭")
|
|
2203
|
+
)
|
|
2204
|
+
)
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2006
2208
|
return h("div", { className: "dru-settings-section", role: "region", "aria-label": "远程访问" },
|
|
2007
2209
|
h("div", { className: "dru-settings-head" },
|
|
2008
2210
|
h("span", { className: "dru-settings-icon" }, "📱"),
|
|
@@ -2013,7 +2215,8 @@ window.__ModuleLoader__.load({
|
|
|
2013
2215
|
),
|
|
2014
2216
|
h("div", { className: "dru-settings-body" },
|
|
2015
2217
|
view === "feedback" ? renderFeedback() : view === "invite" ? renderInvite() : renderHome()
|
|
2016
|
-
)
|
|
2218
|
+
),
|
|
2219
|
+
renderCommunityModal()
|
|
2017
2220
|
);
|
|
2018
2221
|
}
|
|
2019
2222
|
|