@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 +8 -2
- package/lib/client.js +28 -4
- package/lib/index.js +135 -29
- package/lib/ws-client.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
# 更新日志 / Changelog
|
|
2
2
|
|
|
3
|
-
## v1.7.1
|
|
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
|
-
|
|
994
|
-
|
|
995
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
996
|
-
color: #
|
|
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
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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:
|
|
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: #
|
|
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:
|
|
1013
|
-
background:
|
|
1014
|
-
border
|
|
1015
|
-
|
|
1016
|
-
|
|
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:
|
|
1019
|
-
font-size: 14px; font-weight: 600; color: #
|
|
1020
|
-
background: #
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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: #
|
|
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
|
-
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
1160
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
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
|
-
|
|
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
|
|
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",
|