@feiyang666/dsh-mobile-remote 1.7.1-1 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,13 +1,19 @@
1
1
  # 更新日志 / Changelog
2
2
 
3
- ## v1.7.1-1 (2026-08-29) — 预发布(beta,供前端测试)
3
+ ## v1.7.1 (2026-08-29)
4
4
 
5
5
  ### 新增 / New
6
6
  - **外网访问面板显示数据通道状态**(`lib/index.js` + `lib/client.js`):`/status` 新增 `external.ws` 字段(`available` = ws 依赖是否可用 / `connected` = WS 长连接是否建立 / `source` = 当前数据来源 `ws` | `http` | `none` / `lastPushAt` = 最后推送时刻),设置页「外网访问」卡片新增「数据通道」行徽标,让用户一眼看清当前是 **WS 实时推送**(绿色)、**HTTP 兜底(退让模式)**(未安装 ws 依赖)、**WS 已连接等待推送**、**WS 已断开 / 重连中**(含最后推送时间)中的哪一种。
7
7
  - **运行时长本地每秒刷新**(`lib/client.js`):`runtimeInfo()` 增加采集时刻 `at`,前端新增 `LiveDuration` 组件——基于「采样秒数 + 采集时刻」在浏览器本地每秒累加,让「运行时长 / 开机时长 / 隧道已运行」等秒级数字每秒跳动(秒表效果),不再依赖 5s 的 `/status` 轮询刷新(此前数字每 5s 才跳一次且跳幅不连续)。
8
+ - **远程访问密码界面美化**(`lib/index.js` GATE_CSS / GATE_JS):按 DeepSeek 官网风格重做门禁界面——深蓝黑背景 + 细微光斑纹理、全套圆角元素(卡片 / 胶囊 logo 条 / 输入框 / 按钮 / 服务信息框)、DSH FishLogo + 「deepseek | Harness」文字 logo 组合(复刻官网);`/__dsh_remote/auth-status` 新增 `service` 摘要(服务名 / 本机主机名 / DSH 版本 / 当前访问域名),门禁界面据此展示"访问地址 / 提供服务者 / 版本"信息。
9
+ - **输入密码后的加载动画**(`lib/index.js` GATE_JS):点击「进入」立即显示按钮内旋转 spinner + 文案切换为「正在进入…」,并禁用防重复提交,用户明确知道系统正在处理;验证失败自动恢复。
10
+ - **清除 / 修改密码需验证当前密码**(`lib/index.js` + `lib/client.js`):`/__dsh_remote/set-password` 在已设置密码后,修改或清除必须携带 `currentPassword` 并通过校验,防止门禁被未授权一键关闭;设置页新增「当前密码」输入框引导。
11
+
12
+ ### 修复 / Fixed
13
+ - **WS 推送一直显示"等待推送"**(`lib/ws-client.js`):兼容中转服务器 `send()` 对已序列化 payload 二次 `JSON.stringify` 造成的双重序列化消息(`"{\"type\":...}"`),首层解析得到 JSON 字符串时二次解析,插件端恢复正常接收 `tunnel:status` 推送(中转服务器 `dsh-update-server` 的 `send()` 也已修复不再重复序列化,需部署后生效)。
8
14
 
9
15
  ### 变更 / Changed
10
- - 无 API 破坏;旧版 host / 新版 client 混用时,缺少 `at` / `ws` 字段会自动降级为原行为(数字按轮询刷新、不显示数据通道行)。
16
+ - 无 API 破坏;旧版 host / 新版 client 混用时,缺少 `at` / `ws` / `service` 字段会自动降级为原行为。
11
17
 
12
18
  ## v1.7.0 (2026-08-28)
13
19
 
package/lib/client.js CHANGED
@@ -542,6 +542,10 @@ window.__ModuleLoader__.load({
542
542
  var _p2 = useState('');
543
543
  var pwd2 = _p2[0];
544
544
  var setPwd2 = _p2[1];
545
+ // 当前密码:已设置密码后,修改 / 清除都必须先验证当前密码,防止误改或未授权篡改
546
+ var _cp = useState('');
547
+ var currentPwd = _cp[0];
548
+ var setCurrentPwd = _cp[1];
545
549
  var _msgbusy = useState(false);
546
550
  var msgBusy = _msgbusy[0];
547
551
  var setMsgBusy = _msgbusy[1];
@@ -559,12 +563,17 @@ window.__ModuleLoader__.load({
559
563
  setMsg({ type: 'err', text: '密码至少 4 位' });
560
564
  return;
561
565
  }
566
+ // 已设置过密码时,修改必须验证当前密码
567
+ if (authOn && !currentPwd) {
568
+ setMsg({ type: 'err', text: '请输入当前密码以确认修改' });
569
+ return;
570
+ }
562
571
  setMsgBusy(true);
563
572
  setMsg(null);
564
573
  fetch(SET_PASSWORD_URL, {
565
574
  method: 'POST',
566
575
  headers: { 'Content-Type': 'application/json' },
567
- body: JSON.stringify({ password: pwd1 }),
576
+ body: JSON.stringify({ password: pwd1, currentPassword: authOn ? currentPwd : undefined }),
568
577
  })
569
578
  .then(function (r) { return r.json(); })
570
579
  .then(function (data) {
@@ -572,6 +581,7 @@ window.__ModuleLoader__.load({
572
581
  setMsg({ type: 'ok', text: data.enabled ? '已设置远程访问密码。' : '已清除远程访问密码。' });
573
582
  setPwd1('');
574
583
  setPwd2('');
584
+ setCurrentPwd('');
575
585
  setTimeout(refresh, 300);
576
586
  })
577
587
  .catch(function (e) { setMsg({ type: 'err', text: String(e && e.message || e) }); })
@@ -596,8 +606,17 @@ window.__ModuleLoader__.load({
596
606
  : '设置后,通过外网隧道(远程)访问本机 DSH 时,需先输入该密码才能进入页面;本机/内网访问不受影响。'),
597
607
 
598
608
  el('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
609
+ authOn
610
+ ? el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
611
+ el('label', { style: st.hint }, '当前密码(修改 / 清除需验证)'),
612
+ el('input', {
613
+ type: 'password', value: currentPwd, placeholder: '输入当前密码',
614
+ onChange: function (e) { setCurrentPwd(e.target.value); },
615
+ autoComplete: 'current-password', style: st.input,
616
+ }))
617
+ : null,
599
618
  el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
600
- el('label', { style: st.hint }, authOn ? '新密码(留空并点击“清除”可关闭门禁)' : '设置远程访问密码'),
619
+ el('label', { style: st.hint }, authOn ? '新密码' : '设置远程访问密码'),
601
620
  el('input', {
602
621
  type: 'password', value: pwd1, placeholder: '至少 4 位',
603
622
  onChange: function (e) { setPwd1(e.target.value); },
@@ -626,19 +645,24 @@ window.__ModuleLoader__.load({
626
645
  ? el('button', {
627
646
  type: 'button',
628
647
  onClick: function () {
648
+ // 清除门禁必须先验证当前密码(host 侧同样校验,前端只是引导)
649
+ if (!currentPwd) {
650
+ setMsg({ type: 'err', text: '请输入当前密码以确认清除' });
651
+ return;
652
+ }
629
653
  if (!window.confirm('确定清除远程访问密码吗?此后远程访问将不再要求输入密码。')) return;
630
654
  setMsgBusy(true);
631
655
  setMsg(null);
632
656
  fetch(SET_PASSWORD_URL, {
633
657
  method: 'POST',
634
658
  headers: { 'Content-Type': 'application/json' },
635
- body: JSON.stringify({ password: '' }),
659
+ body: JSON.stringify({ password: '', currentPassword: currentPwd }),
636
660
  })
637
661
  .then(function (r) { return r.json(); })
638
662
  .then(function (data) {
639
663
  if (!data.ok) throw new Error(data.message || data.error || '清除失败');
640
664
  setMsg({ type: 'ok', text: '已清除远程访问密码。' });
641
- setPwd1(''); setPwd2('');
665
+ setPwd1(''); setPwd2(''); setCurrentPwd('');
642
666
  setTimeout(refresh, 300);
643
667
  })
644
668
  .catch(function (e) { setMsg({ type: 'err', text: String(e && e.message || e) }); })
package/lib/index.js CHANGED
@@ -449,7 +449,9 @@ function handleEvents() {
449
449
  }
450
450
  }
451
451
 
452
- /** GET /__dsh_remote/auth-status:返回远程访问密码的启用状态与本次请求是否需鉴权。 */
452
+ /** GET /__dsh_remote/auth-status:返回远程访问密码的启用状态与本次请求是否需鉴权。
453
+ * 额外返回 service 摘要(服务名 / 本机主机名 / dsh 版本 / 当前访问域名),
454
+ * 供密码门禁界面展示"当前是谁在提供服务"等更多信息。 */
453
455
  function handleAuthStatus() {
454
456
  return async (req, res) => {
455
457
  json(res, 200, {
@@ -459,6 +461,13 @@ function handleAuthStatus() {
459
461
  required: isRemoteRequest(req) && isEnabled(),
460
462
  authenticated: authorized(req),
461
463
  remote: isRemoteRequest(req),
464
+ // 服务信息:密码界面上展示("服务者"以本机主机名标识)
465
+ service: {
466
+ name: 'DeepSeek Harness',
467
+ hostname: hostname(),
468
+ version: detectDshVersion() || '',
469
+ host: String(req.headers.host || ''),
470
+ },
462
471
  })
463
472
  }
464
473
  }
@@ -497,8 +506,10 @@ function handleLogout() {
497
506
  }
498
507
 
499
508
  /**
500
- * POST /__dsh_remote/set-password { password }(仅本机/内网来源可用):
501
- * 设置或修改远程访问密码。远程来源被拒绝,防止外部任意篡改。空密码=清除。
509
+ * POST /__dsh_remote/set-password { password, currentPassword }(仅本机/内网来源可用):
510
+ * 设置或修改远程访问密码。远程来源被拒绝,防止外部任意篡改。
511
+ * 安全要求:已设置过密码时,修改或清除都必须携带 currentPassword 并验证通过,
512
+ * 防止"改密码/清密码"接口被无鉴权滥用。
502
513
  */
503
514
  function handleSetPassword() {
504
515
  return async (req, res) => {
@@ -508,6 +519,14 @@ function handleSetPassword() {
508
519
  }
509
520
  const body = await readBody(req)
510
521
  const password = typeof body.password === 'string' ? body.password : ''
522
+ const current = typeof body.currentPassword === 'string' ? body.currentPassword : ''
523
+ // 已设置密码时:修改 / 清除都必须先验证当前密码(防未授权篡改门禁)
524
+ if (isEnabled()) {
525
+ if (!verifyPassword(current)) {
526
+ json(res, 401, { ok: false, code: 'BAD_CURRENT_PASSWORD', message: '当前密码不正确,无法修改或清除' })
527
+ return
528
+ }
529
+ }
511
530
  // 密码强度检查
512
531
  if (password && password.length < 4) {
513
532
  json(res, 400, { ok: false, code: 'WEAK_PASSWORD', message: '密码至少 4 位' })
@@ -987,44 +1006,109 @@ function injectIntoIndex(html, remoteEnabled) {
987
1006
  /* ------------------------------------------------------------------ */
988
1007
 
989
1008
  const GATE_CSS = `
1009
+ /* DeepSeek 官网风格:深蓝黑底 + 极简白字 + 细边框 + 下划线输入 + 大留白 */
990
1010
  #dsh-remote-gate {
991
1011
  position: fixed; inset: 0; z-index: 2147483647;
992
1012
  display: flex; align-items: center; justify-content: center;
993
- background: rgba(15, 17, 26, 0.92);
994
- backdrop-filter: blur(6px);
995
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
996
- color: #e8e8ea;
1013
+ padding: 24px; box-sizing: border-box;
1014
+ background: #06101e;
1015
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;
1016
+ color: #fff;
1017
+ }
1018
+ /* 背景光斑纹理(参考官网深蓝氛围) */
1019
+ #dsh-remote-gate::before {
1020
+ content: '';
1021
+ position: absolute; inset: 0;
1022
+ background:
1023
+ radial-gradient(800px 500px at 18% 8%, rgba(70, 100, 170, 0.12), transparent 60%),
1024
+ radial-gradient(700px 420px at 88% 92%, rgba(50, 70, 140, 0.10), transparent 60%);
1025
+ pointer-events: none;
997
1026
  }
998
1027
  #dsh-remote-gate .gate-card {
999
- width: min(360px, 88vw); box-sizing: border-box;
1000
- background: #1d2030; border: 1px solid rgba(255,255,255,.12);
1001
- border-radius: 14px; padding: 24px 22px;
1002
- box-shadow: 0 18px 50px rgba(0,0,0,.45);
1028
+ position: relative;
1029
+ width: min(360px, 92vw); box-sizing: border-box;
1030
+ background: rgba(10, 18, 32, 0.85);
1031
+ border: 1px solid rgba(255,255,255,.14);
1032
+ border-radius: 14px; padding: 44px 36px 30px;
1033
+ backdrop-filter: blur(10px);
1034
+ }
1035
+ /* Logo 组合:FishLogo + 文字 "deepseek | Harness",胶囊圆角条(复刻官网 logo 与药丸按钮) */
1036
+ #dsh-remote-gate .gate-logo-wrap {
1037
+ display: flex; align-items: center; justify-content: center; gap: 10px;
1038
+ width: max-content; margin: 0 auto 28px;
1039
+ padding: 10px 18px;
1040
+ border: 1px solid rgba(255,255,255,.16);
1041
+ border-radius: 999px;
1042
+ background: rgba(255,255,255,.04);
1043
+ }
1044
+ #dsh-remote-gate .gate-logo {
1045
+ width: 26px; height: 19px; color: #fff; display: block; flex: none;
1003
1046
  }
1047
+ #dsh-remote-gate .gate-wordmark {
1048
+ font-size: 18px; line-height: 1; letter-spacing: .3px;
1049
+ display: flex; align-items: center; gap: 8px;
1050
+ }
1051
+ #dsh-remote-gate .gate-wordmark .w-name { color: #fff; font-weight: 500; }
1052
+ #dsh-remote-gate .gate-wordmark .w-sep { color: #555; font-weight: 300; }
1053
+ #dsh-remote-gate .gate-wordmark .w-prod { color: #d0d0d0; font-weight: 400; }
1054
+ /* 标题 + 副标题 */
1004
1055
  #dsh-remote-gate .gate-title {
1005
- font-size: 17px; font-weight: 700; margin: 0 0 6px;
1056
+ font-size: 16px; font-weight: 600; margin: 0 0 8px; text-align: center; letter-spacing: .2px;
1006
1057
  }
1007
1058
  #dsh-remote-gate .gate-sub {
1008
- font-size: 13px; color: #9aa0b5; margin: 0 0 16px; line-height: 1.5;
1059
+ font-size: 13px; color: #8a8a8a; margin: 0 0 28px; line-height: 1.6; text-align: center;
1009
1060
  }
1061
+ /* 服务信息(访问地址 / 提供服务者 / 版本):细边框、黑白灰 */
1062
+ #dsh-remote-gate .gate-service {
1063
+ margin: 0 0 24px; padding: 12px 14px;
1064
+ border: 1px solid rgba(255,255,255,.12);
1065
+ border-radius: 10px;
1066
+ font-size: 12px; color: #8a8a8a; line-height: 1.9;
1067
+ }
1068
+ #dsh-remote-gate .gate-service .sv-row {
1069
+ display: flex; justify-content: space-between; align-items: baseline; gap: 12px; min-width: 0;
1070
+ }
1071
+ #dsh-remote-gate .gate-service .sv-label { flex: none; color: #6a6a6a; }
1072
+ #dsh-remote-gate .gate-service .sv-value {
1073
+ flex: 1; min-width: 0; text-align: right; color: #e8e8e8;
1074
+ word-break: break-all; overflow-wrap: anywhere; font-weight: 500;
1075
+ }
1076
+ /* 圆角输入框:深色底 + 细白边 + 圆角,与整体圆角风格一致 */
1010
1077
  #dsh-remote-gate .gate-input {
1011
1078
  width: 100%; box-sizing: border-box;
1012
- padding: 10px 12px; font-size: 14px; color: #e8e8ea;
1013
- background: #14171f; border: 1px solid rgba(255,255,255,.18);
1014
- border-radius: 9px; outline: none;
1015
- }
1016
- #dsh-remote-gate .gate-input:focus { border-color: #4c8dff; }
1079
+ padding: 11px 13px; font-size: 14px; color: #fff;
1080
+ background: rgba(255,255,255,.05);
1081
+ border: 1px solid rgba(255,255,255,.18);
1082
+ border-radius: 10px; outline: none;
1083
+ transition: border-color .15s ease, background .15s ease;
1084
+ }
1085
+ #dsh-remote-gate .gate-input:focus { border-color: #fff; background: rgba(255,255,255,.08); }
1086
+ #dsh-remote-gate .gate-input::placeholder { color: #555; }
1087
+ /* 主按钮:白底黑字(强对比 CTA) */
1017
1088
  #dsh-remote-gate .gate-btn {
1018
- width: 100%; margin-top: 12px; padding: 10px 12px;
1019
- font-size: 14px; font-weight: 600; color: #fff;
1020
- background: #2a7de1; border: none; border-radius: 9px; cursor: pointer;
1021
- }
1022
- #dsh-remote-gate .gate-btn:hover { background: #3a8df0; }
1023
- #dsh-remote-gate .gate-btn:disabled { opacity: .6; cursor: default; }
1089
+ width: 100%; margin-top: 24px; padding: 12px;
1090
+ font-size: 14px; font-weight: 600; color: #000;
1091
+ background: #fff; border: none; border-radius: 10px; cursor: pointer;
1092
+ display: flex; align-items: center; justify-content: center; gap: 6px;
1093
+ transition: background .15s ease, opacity .15s ease;
1094
+ }
1095
+ #dsh-remote-gate .gate-btn:hover { background: #e8e8e8; }
1096
+ #dsh-remote-gate .gate-btn:disabled { opacity: .5; cursor: default; }
1097
+ /* 加载动画:提交后按钮内旋转 spinner + 文案切换 */
1098
+ #dsh-remote-gate .gate-spinner {
1099
+ display: none; width: 15px; height: 15px; flex: none;
1100
+ border: 2px solid rgba(0,0,0,.25); border-top-color: #000;
1101
+ border-radius: 50%; box-sizing: border-box;
1102
+ animation: dsh-gate-spin .7s linear infinite;
1103
+ }
1104
+ #dsh-remote-gate .gate-btn.gate-loading .gate-spinner { display: inline-block; }
1105
+ @keyframes dsh-gate-spin { to { transform: rotate(360deg); } }
1024
1106
  #dsh-remote-gate .gate-err {
1025
- font-size: 12px; color: #ff6b6b; margin-top: 10px; min-height: 16px;
1107
+ font-size: 12px; color: #ff5f56; margin-top: 10px; min-height: 16px; line-height: 1.4;
1108
+ }
1109
+ #dsh-remote-gate .gate-foot {
1110
+ margin-top: 20px; font-size: 11px; color: #555; text-align: center; letter-spacing: .2px;
1026
1111
  }
1027
- #dsh-remote-gate .gate-loading { opacity: .6; }
1028
1112
  `
1029
1113
 
1030
1114
  const GATE_JS = `
@@ -1067,17 +1151,35 @@ const GATE_JS = `
1067
1151
  function canCover() {
1068
1152
  try { return document.getElementById('dsh-remote-gate') === null; } catch (e) { return true; }
1069
1153
  }
1070
- function createGate() {
1154
+ /* DSH FishLogo(来自 @deepseek-ai/dsh-client-ui-primitives FishLogo,SVG path 直出) */
1155
+ var GATE_FISH = '<svg class="gate-logo" viewBox="0 0 23.16 17.04" fill="currentColor" aria-hidden="true"><path d="M22.9168 1.43018C22.6713 1.31018 22.5658 1.53918 22.4223 1.65519C22.3733 1.69269 22.3318 1.74169 22.2903 1.78669C21.9317 2.1697 21.5127 2.42121 20.9657 2.39121C20.1657 2.34621 19.4827 2.59771 18.8787 3.20973C18.7502 2.45521 18.3236 2.0047 17.6746 1.71569C17.3351 1.56568 16.9916 1.41518 16.7536 1.08867C16.5876 0.856163 16.5421 0.597155 16.4591 0.341647C16.4061 0.187643 16.3536 0.0301382 16.1761 0.00363739C15.9836 -0.0263635 15.9081 0.135141 15.8326 0.270145C15.5306 0.822162 15.4136 1.43018 15.4251 2.0462C15.4516 3.43174 16.0366 4.53527 17.1991 5.3203C17.3311 5.4103 17.3651 5.5003 17.3236 5.63181C17.2441 5.90231 17.1501 6.16482 17.0671 6.43533C17.0141 6.60784 16.9351 6.64584 16.7501 6.57033C16.1121 6.30383 15.5611 5.90931 15.074 5.4328C14.2475 4.63328 13.5 3.75075 12.568 3.05973C12.349 2.89822 12.13 2.74822 11.9034 2.60522C10.9524 1.68169 12.028 0.923165 12.277 0.833162C12.5375 0.739159 12.3675 0.41615 11.5259 0.42015C10.6844 0.42365 9.91439 0.705658 8.93286 1.08117C8.78935 1.13767 8.63835 1.17867 8.48384 1.21267C7.59332 1.04367 6.66829 1.00617 5.70226 1.11517C3.88321 1.31768 2.43016 2.1777 1.36213 3.64575C0.0790928 5.4103 -0.222916 7.41536 0.146595 9.50642C0.535106 11.7105 1.66014 13.535 3.38869 14.9616C5.18125 16.4406 7.24581 17.1657 9.60138 17.0266C11.0319 16.9441 12.6245 16.7526 14.421 15.2321C14.874 15.4576 15.3496 15.5476 16.1381 15.6151C16.7456 15.6716 17.3306 15.5851 17.7836 15.4911C18.4931 15.3411 18.4441 14.6841 18.1876 14.5636C16.1081 13.595 16.5646 13.9891 16.1496 13.67C17.2061 12.42 18.8202 10.1979 19.3182 7.17235C19.3672 6.83834 19.4297 6.36783 19.4222 6.09732C19.4182 5.93231 19.4562 5.86831 19.6447 5.84931C20.1657 5.78931 20.6712 5.64681 21.1357 5.3913C22.4833 4.65528 23.0268 3.44624 23.1548 1.9972C23.1738 1.77569 23.1508 1.54668 22.9168 1.43018ZM11.1749 14.4736C9.15936 12.889 8.18184 12.3675 7.77832 12.39C7.40081 12.4125 7.46881 12.8445 7.55182 13.126C7.63882 13.404 7.75182 13.5955 7.91033 13.8396C8.01983 14.0011 8.09533 14.2411 7.80083 14.4216C7.15181 14.8231 6.02327 14.2866 5.97027 14.2601C4.65673 13.4865 3.5587 12.4655 2.78467 11.069C2.03715 9.72493 1.60314 8.28289 1.53164 6.74384C1.51264 6.37233 1.62214 6.24082 1.99215 6.17332C2.47916 6.08332 2.98118 6.06432 3.46769 6.13582C5.52476 6.43633 7.27581 7.35586 8.74385 8.8129C9.58188 9.64243 10.2159 10.634 10.8689 11.6025C11.5634 12.631 12.3105 13.611 13.262 14.4146C13.598 14.6961 13.866 14.9101 14.1225 15.0681C13.349 15.1546 12.058 15.1731 11.1749 14.4746L11.1749 14.4736ZM12.141 8.25988C12.141 8.09488 12.273 7.96338 12.439 7.96338C12.4765 7.96338 12.5105 7.97088 12.541 7.98188C12.5825 7.99688 12.6205 8.01938 12.6505 8.05338C12.7035 8.10588 12.7335 8.18088 12.7335 8.25988C12.7335 8.42489 12.6015 8.55639 12.4355 8.55639C12.2695 8.55639 12.141 8.42489 12.141 8.25988ZM15.1415 9.79893C14.949 9.87793 14.7565 9.94544 14.5715 9.95294C14.2845 9.96794 13.9715 9.85143 13.8015 9.70893C13.5375 9.48742 13.3485 9.36342 13.2695 8.97691C13.2355 8.8119 13.2545 8.55639 13.2845 8.40989C13.3525 8.09438 13.277 7.89187 13.0545 7.70787C12.8735 7.55786 12.643 7.51636 12.39 7.51636C12.2955 7.51636 12.209 7.47486 12.1445 7.44136C12.039 7.38886 11.9519 7.25735 12.035 7.09585C12.0615 7.04335 12.19 6.91584 12.22 6.89334C12.5635 6.69784 12.9595 6.76184 13.326 6.90834C13.6655 7.04735 13.9225 7.30236 14.292 7.66287C14.6695 8.09838 14.7375 8.21838 14.9525 8.54539C15.1225 8.8009 15.277 9.06341 15.3831 9.36392C15.4471 9.55142 15.3641 9.70493 15.1415 9.79893Z"/></svg>';
1156
+ /* HTML 转义,防止服务信息中的特殊字符破坏结构 */
1157
+ function esc(s) {
1158
+ return String(s == null ? '' : s)
1159
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1160
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
1161
+ }
1162
+ function createGate(service) {
1071
1163
  if (!canCover()) return;
1164
+ var svc = service || {};
1165
+ var rows = '';
1166
+ if (svc.host) rows += '<div class="sv-row"><span class="sv-label">访问地址</span><span class="sv-value">' + esc(svc.host) + '</span></div>';
1167
+ if (svc.hostname) rows += '<div class="sv-row"><span class="sv-label">提供服务</span><span class="sv-value">' + esc(svc.hostname) + '</span></div>';
1168
+ if (svc.version) rows += '<div class="sv-row"><span class="sv-label">版本</span><span class="sv-value">' + esc(svc.version) + '</span></div>';
1072
1169
  var g = document.createElement('div');
1073
1170
  g.id = 'dsh-remote-gate';
1074
1171
  g.innerHTML =
1075
1172
  '<div class="gate-card">' +
1173
+ '<div class="gate-logo-wrap">' + GATE_FISH +
1174
+ '<div class="gate-wordmark"><span class="w-name">deepseek</span><span class="w-sep">|</span><span class="w-prod">Harness</span></div>' +
1175
+ '</div>' +
1076
1176
  '<p class="gate-title">远程访问已锁定</p>' +
1077
1177
  '<p class="gate-sub">请输入管理员设置的远程访问密码以继续使用。</p>' +
1178
+ (rows ? '<div class="gate-service">' + rows + '</div>' : '') +
1078
1179
  '<input class="gate-input" type="password" autocomplete="current-password" placeholder="远程访问密码" />' +
1079
- '<button class="gate-btn" type="button">进入</button>' +
1180
+ '<button class="gate-btn" type="button"><span class="gate-spinner"></span><span class="gate-btn-text">进入</span></button>' +
1080
1181
  '<p class="gate-err"></p>' +
1182
+ '<p class="gate-foot">DeepSeek Harness · 移动端远程控制</p>' +
1081
1183
  '</div>';
1082
1184
  document.documentElement.appendChild(g);
1083
1185
  return g;
@@ -1086,7 +1188,10 @@ const GATE_JS = `
1086
1188
  function submit(g, input, btn) {
1087
1189
  var pwd = input.value;
1088
1190
  if (!pwd) { g.querySelector('.gate-err').textContent = '请输入密码'; return; }
1191
+ var btnText = btn.querySelector('.gate-btn-text');
1192
+ // 立即进入加载态:禁用按钮 + spinner 旋转 + 文案切换,让用户明确知道正在进入
1089
1193
  btn.disabled = true; btn.classList.add('gate-loading');
1194
+ if (btnText) btnText.textContent = '正在进入…';
1090
1195
  fetch('/__dsh_remote/auth', {
1091
1196
  method: 'POST',
1092
1197
  headers: { 'Content-Type': 'application/json' },
@@ -1098,6 +1203,7 @@ const GATE_JS = `
1098
1203
  window.location.reload();
1099
1204
  }).catch(function (e) {
1100
1205
  btn.disabled = false; btn.classList.remove('gate-loading');
1206
+ if (btnText) btnText.textContent = '进入';
1101
1207
  g.querySelector('.gate-err').textContent = String(e && e.message || e);
1102
1208
  input.select();
1103
1209
  });
@@ -1108,7 +1214,7 @@ const GATE_JS = `
1108
1214
  .then(function (r) { return r.json(); })
1109
1215
  .then(function (st) {
1110
1216
  if (!st || !st.required || st.authenticated) return;
1111
- var g = createGate();
1217
+ var g = createGate(st.service);
1112
1218
  if (!g) return;
1113
1219
  var input = g.querySelector('.gate-input');
1114
1220
  var btn = g.querySelector('.gate-btn');
package/lib/ws-client.js CHANGED
@@ -85,7 +85,10 @@ export function createWsStatusClient({ serverBase, bindCode, onStatus, logger })
85
85
 
86
86
  client.on('message', (buf) => {
87
87
  try {
88
- const msg = JSON.parse(buf.toString())
88
+ let msg = JSON.parse(buf.toString())
89
+ // 兼容旧服务器对 payload 二次 JSON.stringify 的双重序列化消息:
90
+ // 首层解析得到的是 JSON 字符串(如 "{\"type\":...}"),再解析一次拿到真实对象。
91
+ if (typeof msg === 'string') msg = JSON.parse(msg)
89
92
  if (msg && msg.type === 'tunnel:status' && msg.data) {
90
93
  msg.data._at = Date.now() // 缓存新鲜度标记
91
94
  lastStatus = msg.data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feiyang666/dsh-mobile-remote",
3
- "version": "1.7.1-1",
3
+ "version": "1.7.1",
4
4
  "description": "DeepSeek Harness mobile remote-control plugin: LAN/external remote control (QR / on-off toggle / remote-access password gate), rich telemetry (expandable device details, system CPU/memory/load, DSH app state: version/sessions/workspaces/plugins/models), real external-tunnel status detection (frpc token / reconnect diagnostics), mobile-first UI polish (composer image upload entry via system picker, settings single-column layout) and faster remote loading.",
5
5
  "keywords": [
6
6
  "deepseek",