@feiyang666/dsh-mobile-remote 1.7.0 → 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 +15 -0
- package/lib/client.js +82 -7
- package/lib/index.js +162 -30
- package/lib/ws-client.js +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# 更新日志 / Changelog
|
|
2
2
|
|
|
3
|
+
## v1.7.1 (2026-08-29)
|
|
4
|
+
|
|
5
|
+
### 新增 / New
|
|
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
|
+
- **运行时长本地每秒刷新**(`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()` 也已修复不再重复序列化,需部署后生效)。
|
|
14
|
+
|
|
15
|
+
### 变更 / Changed
|
|
16
|
+
- 无 API 破坏;旧版 host / 新版 client 混用时,缺少 `at` / `ws` / `service` 字段会自动降级为原行为。
|
|
17
|
+
|
|
3
18
|
## v1.7.0 (2026-08-28)
|
|
4
19
|
|
|
5
20
|
### 新增 / New
|
package/lib/client.js
CHANGED
|
@@ -225,6 +225,23 @@ window.__ModuleLoader__.load({
|
|
|
225
225
|
return ss + ' 秒';
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
// 实时时长组件:基于「一次采样的秒数 + 采集时刻」在浏览器本地每秒累加刷新,
|
|
229
|
+
// 让"运行时长 / 开机时长 / 隧道已运行"等秒级数字每秒跳动(秒表效果),
|
|
230
|
+
// 不再依赖 5s 的 /status HTTP 轮询(那会导致数字每 5s 才跳一次,且跳幅不连续)。
|
|
231
|
+
// 只要 host 在采集时带上 at 时间戳,两次 /status 之间也能精确走到下一秒。
|
|
232
|
+
function LiveDuration(props) {
|
|
233
|
+
var _n = useState(Date.now());
|
|
234
|
+
var now = _n[0];
|
|
235
|
+
var setNow = _n[1];
|
|
236
|
+
useEffect(function () {
|
|
237
|
+
var t = setInterval(function () { setNow(Date.now()); }, 1000);
|
|
238
|
+
return function () { clearInterval(t); };
|
|
239
|
+
}, []);
|
|
240
|
+
var at = props.at || now;
|
|
241
|
+
var sec = props.baseSec + Math.max(0, Math.floor((now - at) / 1000));
|
|
242
|
+
return fmtDuration(sec);
|
|
243
|
+
}
|
|
244
|
+
|
|
228
245
|
function fmtBytes(b) {
|
|
229
246
|
if (b === null || b === undefined) return '—';
|
|
230
247
|
var n = Number(b);
|
|
@@ -405,6 +422,32 @@ window.__ModuleLoader__.load({
|
|
|
405
422
|
// 后端探测到的错误(如鉴权失败 / token 不匹配),优先于请求时的临时错误
|
|
406
423
|
var extErrText = extErr || (extError && status && status.external && status.external.error ? status.external.error : null);
|
|
407
424
|
|
|
425
|
+
// ---- WS 状态推送通道(host 连接中转服务器的数据通道)----
|
|
426
|
+
// 让用户明确看到当前外网状态数据是经 WS 长连接实时推送,还是退让为 HTTP 低频轮询。
|
|
427
|
+
var wsInfo = status && status.external && status.external.ws ? status.external.ws : null;
|
|
428
|
+
var wsBadge = null;
|
|
429
|
+
var wsHint = null;
|
|
430
|
+
if (extStarted && wsInfo) {
|
|
431
|
+
if (wsInfo.connected && wsInfo.source === 'ws') {
|
|
432
|
+
wsBadge = el('span', { style: Object.assign({}, st.badge, st.badgeOn) }, 'WS 实时推送');
|
|
433
|
+
wsHint = '隧道状态经 WebSocket 长连接实时更新(零 HTTP 轮询)';
|
|
434
|
+
} else if (wsInfo.available === false) {
|
|
435
|
+
// 退让版本:ws 依赖缺失(如离线安装),WS 客户端 no-op,走 HTTP 低频兜底
|
|
436
|
+
wsBadge = el('span', { style: Object.assign({}, st.badge, st.badgeBusy) }, 'HTTP 兜底(退让模式)');
|
|
437
|
+
wsHint = '未安装 ws 依赖,已自动退让为 HTTP 低频查询(60s 节流),功能不受影响';
|
|
438
|
+
} else if (wsInfo.connected) {
|
|
439
|
+
wsBadge = el('span', { style: Object.assign({}, st.badge, st.badgeBusy) }, 'WS 已连接,等待推送');
|
|
440
|
+
wsHint = 'WS 长连接已建立,收到首条推送后自动切换为实时模式';
|
|
441
|
+
} else if (wsInfo.source === 'ws') {
|
|
442
|
+
wsBadge = el('span', { style: Object.assign({}, st.badge, st.badgeBusy) }, 'WS 已断开');
|
|
443
|
+
wsHint = 'WS 长连接已断开,正在自动重连;当前显示最近一次推送的缓存数据' +
|
|
444
|
+
(wsInfo.lastPushAt ? '(最后推送 ' + fmtAgo(wsInfo.lastPushAt) + ')' : '');
|
|
445
|
+
} else {
|
|
446
|
+
wsBadge = el('span', { style: Object.assign({}, st.badge, st.badgeBusy) }, 'WS 未连接');
|
|
447
|
+
wsHint = 'WS 长连接尚未建立(连接中 / 自动重连),期间数据走 HTTP 兜底';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
408
451
|
function externalStart() {
|
|
409
452
|
if (extBusy || extConnecting) return;
|
|
410
453
|
if (!bindCode || !bindCode.trim()) {
|
|
@@ -499,6 +542,10 @@ window.__ModuleLoader__.load({
|
|
|
499
542
|
var _p2 = useState('');
|
|
500
543
|
var pwd2 = _p2[0];
|
|
501
544
|
var setPwd2 = _p2[1];
|
|
545
|
+
// 当前密码:已设置密码后,修改 / 清除都必须先验证当前密码,防止误改或未授权篡改
|
|
546
|
+
var _cp = useState('');
|
|
547
|
+
var currentPwd = _cp[0];
|
|
548
|
+
var setCurrentPwd = _cp[1];
|
|
502
549
|
var _msgbusy = useState(false);
|
|
503
550
|
var msgBusy = _msgbusy[0];
|
|
504
551
|
var setMsgBusy = _msgbusy[1];
|
|
@@ -516,12 +563,17 @@ window.__ModuleLoader__.load({
|
|
|
516
563
|
setMsg({ type: 'err', text: '密码至少 4 位' });
|
|
517
564
|
return;
|
|
518
565
|
}
|
|
566
|
+
// 已设置过密码时,修改必须验证当前密码
|
|
567
|
+
if (authOn && !currentPwd) {
|
|
568
|
+
setMsg({ type: 'err', text: '请输入当前密码以确认修改' });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
519
571
|
setMsgBusy(true);
|
|
520
572
|
setMsg(null);
|
|
521
573
|
fetch(SET_PASSWORD_URL, {
|
|
522
574
|
method: 'POST',
|
|
523
575
|
headers: { 'Content-Type': 'application/json' },
|
|
524
|
-
body: JSON.stringify({ password: pwd1 }),
|
|
576
|
+
body: JSON.stringify({ password: pwd1, currentPassword: authOn ? currentPwd : undefined }),
|
|
525
577
|
})
|
|
526
578
|
.then(function (r) { return r.json(); })
|
|
527
579
|
.then(function (data) {
|
|
@@ -529,6 +581,7 @@ window.__ModuleLoader__.load({
|
|
|
529
581
|
setMsg({ type: 'ok', text: data.enabled ? '已设置远程访问密码。' : '已清除远程访问密码。' });
|
|
530
582
|
setPwd1('');
|
|
531
583
|
setPwd2('');
|
|
584
|
+
setCurrentPwd('');
|
|
532
585
|
setTimeout(refresh, 300);
|
|
533
586
|
})
|
|
534
587
|
.catch(function (e) { setMsg({ type: 'err', text: String(e && e.message || e) }); })
|
|
@@ -553,8 +606,17 @@ window.__ModuleLoader__.load({
|
|
|
553
606
|
: '设置后,通过外网隧道(远程)访问本机 DSH 时,需先输入该密码才能进入页面;本机/内网访问不受影响。'),
|
|
554
607
|
|
|
555
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,
|
|
556
618
|
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
557
|
-
el('label', { style: st.hint }, authOn ? '
|
|
619
|
+
el('label', { style: st.hint }, authOn ? '新密码' : '设置远程访问密码'),
|
|
558
620
|
el('input', {
|
|
559
621
|
type: 'password', value: pwd1, placeholder: '至少 4 位',
|
|
560
622
|
onChange: function (e) { setPwd1(e.target.value); },
|
|
@@ -583,19 +645,24 @@ window.__ModuleLoader__.load({
|
|
|
583
645
|
? el('button', {
|
|
584
646
|
type: 'button',
|
|
585
647
|
onClick: function () {
|
|
648
|
+
// 清除门禁必须先验证当前密码(host 侧同样校验,前端只是引导)
|
|
649
|
+
if (!currentPwd) {
|
|
650
|
+
setMsg({ type: 'err', text: '请输入当前密码以确认清除' });
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
586
653
|
if (!window.confirm('确定清除远程访问密码吗?此后远程访问将不再要求输入密码。')) return;
|
|
587
654
|
setMsgBusy(true);
|
|
588
655
|
setMsg(null);
|
|
589
656
|
fetch(SET_PASSWORD_URL, {
|
|
590
657
|
method: 'POST',
|
|
591
658
|
headers: { 'Content-Type': 'application/json' },
|
|
592
|
-
body: JSON.stringify({ password: '' }),
|
|
659
|
+
body: JSON.stringify({ password: '', currentPassword: currentPwd }),
|
|
593
660
|
})
|
|
594
661
|
.then(function (r) { return r.json(); })
|
|
595
662
|
.then(function (data) {
|
|
596
663
|
if (!data.ok) throw new Error(data.message || data.error || '清除失败');
|
|
597
664
|
setMsg({ type: 'ok', text: '已清除远程访问密码。' });
|
|
598
|
-
setPwd1(''); setPwd2('');
|
|
665
|
+
setPwd1(''); setPwd2(''); setCurrentPwd('');
|
|
599
666
|
setTimeout(refresh, 300);
|
|
600
667
|
})
|
|
601
668
|
.catch(function (e) { setMsg({ type: 'err', text: String(e && e.message || e) }); })
|
|
@@ -730,8 +797,8 @@ window.__ModuleLoader__.load({
|
|
|
730
797
|
el('span', { style: Object.assign({}, st.badge, st.badgeOff) },
|
|
731
798
|
'PID ' + status.runtime.pid)),
|
|
732
799
|
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, kvList([
|
|
733
|
-
['运行时长',
|
|
734
|
-
['开机时长',
|
|
800
|
+
['运行时长', el(LiveDuration, { baseSec: status.runtime.uptime || 0, at: status.runtime.at })],
|
|
801
|
+
['开机时长', el(LiveDuration, { baseSec: status.runtime.sysUptime || 0, at: status.runtime.at })],
|
|
735
802
|
['Node.js', status.runtime.nodeVersion],
|
|
736
803
|
['主机名', status.runtime.hostname],
|
|
737
804
|
['系统', (status.runtime.platform || '—') + ' ' + (status.runtime.release || '') + ' (' + (status.runtime.arch || '—') + ')'],
|
|
@@ -857,8 +924,16 @@ window.__ModuleLoader__.load({
|
|
|
857
924
|
el('span', { style: st.meta },
|
|
858
925
|
'frpc 进程:' + (status.external.pid ? 'PID ' + status.external.pid : '—')),
|
|
859
926
|
status.external.startedAt
|
|
860
|
-
? el('span', { style: st.meta }, '已运行 '
|
|
927
|
+
? el('span', { style: st.meta }, '已运行 ',
|
|
928
|
+
el(LiveDuration, { baseSec: 0, at: status.external.startedAt }))
|
|
861
929
|
: null),
|
|
930
|
+
wsBadge
|
|
931
|
+
? el('div', { style: st.row },
|
|
932
|
+
el('span', { style: st.meta }, '数据通道'),
|
|
933
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 2 } },
|
|
934
|
+
wsBadge,
|
|
935
|
+
wsHint ? el('span', { style: st.hint }, wsHint) : null))
|
|
936
|
+
: null,
|
|
862
937
|
el('div', { style: st.row },
|
|
863
938
|
el('span', { style: st.meta },
|
|
864
939
|
'绑定码:' + (status.external.bindCodeShort || '—')),
|
package/lib/index.js
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
hasPassword, verifyPassword, setPassword,
|
|
44
44
|
isEnabled, login, verifyToken, revokeToken, clearSessions,
|
|
45
45
|
} from './password.js'
|
|
46
|
+
import { wsAvailable } from './ws-client.js'
|
|
46
47
|
|
|
47
48
|
/** 插件名(loader 诊断用)。 */
|
|
48
49
|
export const name = 'dsh-mobile-remote'
|
|
@@ -214,11 +215,14 @@ function getLanInterfaces() {
|
|
|
214
215
|
|
|
215
216
|
/** 当前进程运行时信息(诊断用,设置页展示)。 */
|
|
216
217
|
function runtimeInfo() {
|
|
218
|
+
// 采集时刻:前端可据此在两次 /status 轮询之间本地累加,让"运行时长"每秒跳动。
|
|
219
|
+
const at = Date.now()
|
|
217
220
|
const mem = process.memoryUsage()
|
|
218
221
|
const cpu = cpus()
|
|
219
222
|
const load = loadavg()
|
|
220
223
|
const total = totalmem()
|
|
221
224
|
return {
|
|
225
|
+
at,
|
|
222
226
|
pid: process.pid,
|
|
223
227
|
hostname: hostname(),
|
|
224
228
|
platform: platform(),
|
|
@@ -445,7 +449,9 @@ function handleEvents() {
|
|
|
445
449
|
}
|
|
446
450
|
}
|
|
447
451
|
|
|
448
|
-
/** GET /__dsh_remote/auth-status:返回远程访问密码的启用状态与本次请求是否需鉴权。
|
|
452
|
+
/** GET /__dsh_remote/auth-status:返回远程访问密码的启用状态与本次请求是否需鉴权。
|
|
453
|
+
* 额外返回 service 摘要(服务名 / 本机主机名 / dsh 版本 / 当前访问域名),
|
|
454
|
+
* 供密码门禁界面展示"当前是谁在提供服务"等更多信息。 */
|
|
449
455
|
function handleAuthStatus() {
|
|
450
456
|
return async (req, res) => {
|
|
451
457
|
json(res, 200, {
|
|
@@ -455,6 +461,13 @@ function handleAuthStatus() {
|
|
|
455
461
|
required: isRemoteRequest(req) && isEnabled(),
|
|
456
462
|
authenticated: authorized(req),
|
|
457
463
|
remote: isRemoteRequest(req),
|
|
464
|
+
// 服务信息:密码界面上展示("服务者"以本机主机名标识)
|
|
465
|
+
service: {
|
|
466
|
+
name: 'DeepSeek Harness',
|
|
467
|
+
hostname: hostname(),
|
|
468
|
+
version: detectDshVersion() || '',
|
|
469
|
+
host: String(req.headers.host || ''),
|
|
470
|
+
},
|
|
458
471
|
})
|
|
459
472
|
}
|
|
460
473
|
}
|
|
@@ -493,8 +506,10 @@ function handleLogout() {
|
|
|
493
506
|
}
|
|
494
507
|
|
|
495
508
|
/**
|
|
496
|
-
* POST /__dsh_remote/set-password { password }(仅本机/内网来源可用):
|
|
497
|
-
*
|
|
509
|
+
* POST /__dsh_remote/set-password { password, currentPassword }(仅本机/内网来源可用):
|
|
510
|
+
* 设置或修改远程访问密码。远程来源被拒绝,防止外部任意篡改。
|
|
511
|
+
* 安全要求:已设置过密码时,修改或清除都必须携带 currentPassword 并验证通过,
|
|
512
|
+
* 防止"改密码/清密码"接口被无鉴权滥用。
|
|
498
513
|
*/
|
|
499
514
|
function handleSetPassword() {
|
|
500
515
|
return async (req, res) => {
|
|
@@ -504,6 +519,14 @@ function handleSetPassword() {
|
|
|
504
519
|
}
|
|
505
520
|
const body = await readBody(req)
|
|
506
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
|
+
}
|
|
507
530
|
// 密码强度检查
|
|
508
531
|
if (password && password.length < 4) {
|
|
509
532
|
json(res, 400, { ok: false, code: 'WEAK_PASSWORD', message: '密码至少 4 位' })
|
|
@@ -741,9 +764,18 @@ function handleStatus(ctx, devices, external, tracker, dshCache) {
|
|
|
741
764
|
// 外网隧道在线时,优先读 WS 推送缓存(经长连接实时更新,零中转服务器请求);
|
|
742
765
|
// 仅当从未收到推送(ws 依赖缺失 / WS 未建立)时才回退 HTTP 低频查询(60s 节流)。
|
|
743
766
|
let server = null
|
|
767
|
+
let serverSource = 'none'
|
|
744
768
|
if (extRunning && persisted) {
|
|
745
|
-
|
|
769
|
+
if (external.serverStatus) {
|
|
770
|
+
server = external.serverStatus
|
|
771
|
+
serverSource = 'ws'
|
|
772
|
+
} else {
|
|
773
|
+
server = await queryServerStatus(persisted.serverBase || undefined, persisted.bindCode)
|
|
774
|
+
serverSource = 'http'
|
|
775
|
+
}
|
|
746
776
|
}
|
|
777
|
+
// WS 长连接是否已建立(供前端判断当前处于"实时推送"还是"退让/重连")
|
|
778
|
+
const wsConnected = !!(external.wsClient && external.wsClient.isOpen())
|
|
747
779
|
json(res, 200, {
|
|
748
780
|
host: ctx.webServer.host,
|
|
749
781
|
port: ctx.webServer.port,
|
|
@@ -785,6 +817,19 @@ function handleStatus(ctx, devices, external, tracker, dshCache) {
|
|
|
785
817
|
: null,
|
|
786
818
|
// 中转服务器返回的成员状态(外网在线时才有)
|
|
787
819
|
server,
|
|
820
|
+
// WS 状态推送通道信息:让前端明确当前数据是 WS 实时推送还是 HTTP 兜底退让
|
|
821
|
+
ws: {
|
|
822
|
+
// ws 依赖是否可用(离线安装未带依赖时为 false,自动退让 HTTP)
|
|
823
|
+
available: wsAvailable(),
|
|
824
|
+
// WS 长连接是否已建立
|
|
825
|
+
connected: wsConnected,
|
|
826
|
+
// 当前 server 数据来源:'ws'(实时推送缓存)/ 'http'(兜底查询)/ 'none'
|
|
827
|
+
source: serverSource,
|
|
828
|
+
// 最近一次收到 WS 推送的时刻(未收到过为 null)
|
|
829
|
+
lastPushAt: external.serverStatus && external.serverStatus._at
|
|
830
|
+
? external.serverStatus._at
|
|
831
|
+
: null,
|
|
832
|
+
},
|
|
788
833
|
},
|
|
789
834
|
// 远程访问密码门禁状态(供设置面板展示)
|
|
790
835
|
auth: {
|
|
@@ -961,44 +1006,109 @@ function injectIntoIndex(html, remoteEnabled) {
|
|
|
961
1006
|
/* ------------------------------------------------------------------ */
|
|
962
1007
|
|
|
963
1008
|
const GATE_CSS = `
|
|
1009
|
+
/* DeepSeek 官网风格:深蓝黑底 + 极简白字 + 细边框 + 下划线输入 + 大留白 */
|
|
964
1010
|
#dsh-remote-gate {
|
|
965
1011
|
position: fixed; inset: 0; z-index: 2147483647;
|
|
966
1012
|
display: flex; align-items: center; justify-content: center;
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
970
|
-
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;
|
|
971
1026
|
}
|
|
972
1027
|
#dsh-remote-gate .gate-card {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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;
|
|
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;
|
|
977
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
|
+
/* 标题 + 副标题 */
|
|
978
1055
|
#dsh-remote-gate .gate-title {
|
|
979
|
-
font-size:
|
|
1056
|
+
font-size: 16px; font-weight: 600; margin: 0 0 8px; text-align: center; letter-spacing: .2px;
|
|
980
1057
|
}
|
|
981
1058
|
#dsh-remote-gate .gate-sub {
|
|
982
|
-
font-size: 13px; color: #
|
|
1059
|
+
font-size: 13px; color: #8a8a8a; margin: 0 0 28px; line-height: 1.6; text-align: center;
|
|
983
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
|
+
/* 圆角输入框:深色底 + 细白边 + 圆角,与整体圆角风格一致 */
|
|
984
1077
|
#dsh-remote-gate .gate-input {
|
|
985
1078
|
width: 100%; box-sizing: border-box;
|
|
986
|
-
padding:
|
|
987
|
-
background:
|
|
988
|
-
border
|
|
989
|
-
|
|
990
|
-
|
|
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) */
|
|
991
1088
|
#dsh-remote-gate .gate-btn {
|
|
992
|
-
width: 100%; margin-top:
|
|
993
|
-
font-size: 14px; font-weight: 600; color: #
|
|
994
|
-
background: #
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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); } }
|
|
998
1106
|
#dsh-remote-gate .gate-err {
|
|
999
|
-
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;
|
|
1000
1111
|
}
|
|
1001
|
-
#dsh-remote-gate .gate-loading { opacity: .6; }
|
|
1002
1112
|
`
|
|
1003
1113
|
|
|
1004
1114
|
const GATE_JS = `
|
|
@@ -1041,17 +1151,35 @@ const GATE_JS = `
|
|
|
1041
1151
|
function canCover() {
|
|
1042
1152
|
try { return document.getElementById('dsh-remote-gate') === null; } catch (e) { return true; }
|
|
1043
1153
|
}
|
|
1044
|
-
|
|
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) {
|
|
1045
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>';
|
|
1046
1169
|
var g = document.createElement('div');
|
|
1047
1170
|
g.id = 'dsh-remote-gate';
|
|
1048
1171
|
g.innerHTML =
|
|
1049
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>' +
|
|
1050
1176
|
'<p class="gate-title">远程访问已锁定</p>' +
|
|
1051
1177
|
'<p class="gate-sub">请输入管理员设置的远程访问密码以继续使用。</p>' +
|
|
1178
|
+
(rows ? '<div class="gate-service">' + rows + '</div>' : '') +
|
|
1052
1179
|
'<input class="gate-input" type="password" autocomplete="current-password" placeholder="远程访问密码" />' +
|
|
1053
|
-
'<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>' +
|
|
1054
1181
|
'<p class="gate-err"></p>' +
|
|
1182
|
+
'<p class="gate-foot">DeepSeek Harness · 移动端远程控制</p>' +
|
|
1055
1183
|
'</div>';
|
|
1056
1184
|
document.documentElement.appendChild(g);
|
|
1057
1185
|
return g;
|
|
@@ -1060,7 +1188,10 @@ const GATE_JS = `
|
|
|
1060
1188
|
function submit(g, input, btn) {
|
|
1061
1189
|
var pwd = input.value;
|
|
1062
1190
|
if (!pwd) { g.querySelector('.gate-err').textContent = '请输入密码'; return; }
|
|
1191
|
+
var btnText = btn.querySelector('.gate-btn-text');
|
|
1192
|
+
// 立即进入加载态:禁用按钮 + spinner 旋转 + 文案切换,让用户明确知道正在进入
|
|
1063
1193
|
btn.disabled = true; btn.classList.add('gate-loading');
|
|
1194
|
+
if (btnText) btnText.textContent = '正在进入…';
|
|
1064
1195
|
fetch('/__dsh_remote/auth', {
|
|
1065
1196
|
method: 'POST',
|
|
1066
1197
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1072,6 +1203,7 @@ const GATE_JS = `
|
|
|
1072
1203
|
window.location.reload();
|
|
1073
1204
|
}).catch(function (e) {
|
|
1074
1205
|
btn.disabled = false; btn.classList.remove('gate-loading');
|
|
1206
|
+
if (btnText) btnText.textContent = '进入';
|
|
1075
1207
|
g.querySelector('.gate-err').textContent = String(e && e.message || e);
|
|
1076
1208
|
input.select();
|
|
1077
1209
|
});
|
|
@@ -1082,7 +1214,7 @@ const GATE_JS = `
|
|
|
1082
1214
|
.then(function (r) { return r.json(); })
|
|
1083
1215
|
.then(function (st) {
|
|
1084
1216
|
if (!st || !st.required || st.authenticated) return;
|
|
1085
|
-
var g = createGate();
|
|
1217
|
+
var g = createGate(st.service);
|
|
1086
1218
|
if (!g) return;
|
|
1087
1219
|
var input = g.querySelector('.gate-input');
|
|
1088
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.
|
|
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",
|