@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 CHANGED
@@ -28,7 +28,12 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
28
28
  └→ bridge (电脑端) → 127.0.0.1:3080 (dsh web)
29
29
  ```
30
30
 
31
- ## 0.6.0 速览(用户最关心的新能力)
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.0",
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
- /** 创建(或刷新)一次性访问密钥:GET /dsh-remote/access-key(node 半转发企业端 /api/auth-key)。 */
1272
+ /**
1273
+ * 创建(或刷新)一次性访问密钥:GET /dsh-remote/access-key(node 半转发企业端 /api/auth-key)。
1274
+ * 失败自愈:中继未就绪类失败(node 半 retryable / 网络 / 5xx)按退避自动重试,
1275
+ * 见「登录后首次加载自愈」——首次安装立即登录时不再需要手动刷新页面。
1276
+ */
1188
1277
  function loadAccessKey() {
1189
- if (akeyBusy) return;
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
- setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + why });
1200
- return;
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
- setAkeyMsg({ kind: "err", text: "获取一次性访问地址失败:" + e.message });
1217
- }).finally(function () { setAkeyBusy(false); });
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
- setDevMsg({ kind: "err", text: "加载已授权设备失败:" + e.message });
1245
- }).finally(function () { setDevBusy(""); });
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 () { clearInterval(rotateIv); };
1355
- }, [view, mode, loggedIn]);
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 ? h("div", { className: "dru-msg dru-msg-" + akeyMsg.kind, style: { marginTop: 8 } }, akeyMsg.text) : null,
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 ? h("div", { className: "dru-msg dru-msg-" + devMsg.kind, style: { marginTop: 6 } }, devMsg.text) : null
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