@feiyang666/dsh-mobile-remote 1.1.0 → 1.4.2
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 +81 -0
- package/README.md +47 -3
- package/README.zh.md +47 -3
- package/RELEASE_NOTES.md +14 -25
- package/lib/client.js +699 -13
- package/lib/external.js +729 -0
- package/lib/index.js +929 -25
- package/lib/password.js +218 -0
- package/lib/qrcode.js +2300 -2300
- package/package.json +7 -5
package/lib/client.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// dsh-mobile-remote — DSH 移动端远程控制插件(client 侧)。
|
|
2
2
|
//
|
|
3
3
|
// 在 Web UI 的「设置」中注册一个「远程控制」section,提供:
|
|
4
|
-
// 1.
|
|
5
|
-
// 2.
|
|
6
|
-
//
|
|
4
|
+
// 1. 局域网开关(写入 / 移除 profile patch,经 HMR 热重载生效)+ 二维码 + 设备数;
|
|
5
|
+
// 2. 「外网访问」:通过中转服务器(dsh-update-server)启动 frpc 隧道,让外网设备
|
|
6
|
+
// 访问本机 DSH。含绑定码 / 中转服务地址输入、开启/关闭开关、外网链接 + 二维码 +
|
|
7
|
+
// 在线状态反馈(online / connecting / stopped / error)。
|
|
7
8
|
//
|
|
8
9
|
// 打包格式:浏览器 module loader(window.__ModuleLoader__.load)标准 bundle,
|
|
9
10
|
// 与官方 client 插件一致。纯 React.createElement,无 JSX 构建依赖。
|
|
@@ -20,6 +21,7 @@ window.__ModuleLoader__.load({
|
|
|
20
21
|
var useState = React.useState;
|
|
21
22
|
var useEffect = React.useEffect;
|
|
22
23
|
var useCallback = React.useCallback;
|
|
24
|
+
var useRef = React.useRef;
|
|
23
25
|
|
|
24
26
|
/* ------------------------------------------------------------------ */
|
|
25
27
|
/* 内联样式(跟随宿主主题 token,深色/浅色自动适配) */
|
|
@@ -38,7 +40,8 @@ window.__ModuleLoader__.load({
|
|
|
38
40
|
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
39
41
|
boxSizing: 'border-box',
|
|
40
42
|
},
|
|
41
|
-
|
|
43
|
+
// flexWrap: 窄屏下内容放不下时允许换行(如外网隧道详情行的长文本),桌面端无感知
|
|
44
|
+
row: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' },
|
|
42
45
|
title: { fontSize: 15, fontWeight: 600, margin: 0, lineHeight: 1.4 },
|
|
43
46
|
sub: { fontSize: 13, color: 'var(--dsw-alias-label-tertiary, #767676)', lineHeight: 1.6, margin: 0 },
|
|
44
47
|
hint: { fontSize: 13, color: 'var(--dsw-alias-label-tertiary, #767676)', lineHeight: 1.6, margin: 0 },
|
|
@@ -49,6 +52,8 @@ window.__ModuleLoader__.load({
|
|
|
49
52
|
},
|
|
50
53
|
badgeOn: { color: '#0d7a3f', background: 'rgba(18,150,78,.14)' },
|
|
51
54
|
badgeOff: { color: '#8a8a8a', background: 'rgba(127,127,127,.12)' },
|
|
55
|
+
badgeBusy: { color: '#9a6a00', background: 'rgba(200,140,0,.14)' },
|
|
56
|
+
badgeErr: { color: '#c0272b', background: 'rgba(200,40,40,.13)' },
|
|
52
57
|
switch: {
|
|
53
58
|
position: 'relative', width: 46, height: 26, flex: 'none',
|
|
54
59
|
borderRadius: 999, border: 'none', cursor: 'pointer',
|
|
@@ -95,14 +100,63 @@ window.__ModuleLoader__.load({
|
|
|
95
100
|
overflowX: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-all', lineHeight: 1.5,
|
|
96
101
|
color: 'var(--dsw-alias-label-primary, #1f1f1f)',
|
|
97
102
|
},
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
input: {
|
|
104
|
+
padding: '8px 10px', borderRadius: 8, fontSize: 13,
|
|
105
|
+
background: 'var(--dsw-alias-bg-base, #f0f0f1)',
|
|
100
106
|
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
101
|
-
|
|
102
|
-
|
|
107
|
+
color: 'var(--dsw-alias-label-primary, #1f1f1f)',
|
|
108
|
+
outline: 'none',
|
|
103
109
|
},
|
|
104
110
|
divider: { height: 1, background: 'var(--dsw-alias-border-l2, rgba(127,127,127,.18))', border: 'none' },
|
|
105
111
|
disabled: { opacity: 0.55 },
|
|
112
|
+
deviceList: { display: 'flex', flexDirection: 'column', gap: 8 },
|
|
113
|
+
deviceRow: {
|
|
114
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
115
|
+
padding: '8px 10px', borderRadius: 8,
|
|
116
|
+
background: 'var(--dsw-alias-bg-base, #f0f0f1)',
|
|
117
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
118
|
+
boxSizing: 'border-box', minWidth: 0,
|
|
119
|
+
},
|
|
120
|
+
dotOn: { width: 8, height: 8, borderRadius: 999, flex: 'none', background: '#18a058' },
|
|
121
|
+
deviceName: { fontSize: 13, fontWeight: 600, lineHeight: 1.4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
|
122
|
+
// minWidth + overflowWrap:窄屏下长文本(设备详情、外网状态行)不溢出卡片
|
|
123
|
+
meta: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary, #767676)', lineHeight: 1.5, minWidth: 0, overflowWrap: 'break-word' },
|
|
124
|
+
dotOff: { width: 8, height: 8, borderRadius: 999, flex: 'none', background: 'rgba(127,127,127,.38)' },
|
|
125
|
+
chevron: {
|
|
126
|
+
fontSize: 10, color: 'var(--dsw-alias-label-tertiary, #767676)', flex: 'none',
|
|
127
|
+
transition: 'transform .15s ease', lineHeight: 1,
|
|
128
|
+
},
|
|
129
|
+
chevronOpen: {
|
|
130
|
+
fontSize: 10, color: 'var(--dsw-alias-label-tertiary, #767676)', flex: 'none',
|
|
131
|
+
transform: 'rotate(180deg)', transition: 'transform .15s ease', lineHeight: 1,
|
|
132
|
+
},
|
|
133
|
+
deviceCard: {
|
|
134
|
+
display: 'flex', flexDirection: 'column', minWidth: 0,
|
|
135
|
+
borderRadius: 8, overflow: 'hidden',
|
|
136
|
+
background: 'var(--dsw-alias-bg-base, #f0f0f1)',
|
|
137
|
+
border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
138
|
+
boxSizing: 'border-box',
|
|
139
|
+
},
|
|
140
|
+
deviceHead: {
|
|
141
|
+
display: 'flex', alignItems: 'center', gap: 10,
|
|
142
|
+
padding: '8px 10px', cursor: 'pointer', width: '100%',
|
|
143
|
+
boxSizing: 'border-box', textAlign: 'left',
|
|
144
|
+
background: 'transparent', border: 'none', fontFamily: 'inherit', color: 'inherit',
|
|
145
|
+
},
|
|
146
|
+
deviceDetail: {
|
|
147
|
+
display: 'flex', flexDirection: 'column', gap: 4,
|
|
148
|
+
padding: '8px 12px 10px',
|
|
149
|
+
borderTop: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))',
|
|
150
|
+
},
|
|
151
|
+
detailRow: { display: 'flex', alignItems: 'baseline', gap: 8, fontSize: 12, lineHeight: 1.6 },
|
|
152
|
+
detailLabel: { flex: 'none', width: 92, color: 'var(--dsw-alias-label-tertiary, #767676)' },
|
|
153
|
+
detailValue: { flex: 1, minWidth: 0, wordBreak: 'break-all', overflowWrap: 'break-word' },
|
|
154
|
+
btn: {
|
|
155
|
+
padding: '6px 12px', borderRadius: 8, fontSize: 12, fontWeight: 600,
|
|
156
|
+
color: 'var(--dsw-alias-state-business-primary, #2a7de1)',
|
|
157
|
+
background: 'transparent', border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.25))',
|
|
158
|
+
cursor: 'pointer', whiteSpace: 'nowrap',
|
|
159
|
+
},
|
|
106
160
|
};
|
|
107
161
|
|
|
108
162
|
/* ------------------------------------------------------------------ */
|
|
@@ -111,11 +165,91 @@ window.__ModuleLoader__.load({
|
|
|
111
165
|
|
|
112
166
|
var STATUS_URL = '/__dsh_remote/status';
|
|
113
167
|
var TOGGLE_URL = '/__dsh_remote/toggle';
|
|
168
|
+
var EXT_START_URL = '/__dsh_remote/external/start';
|
|
169
|
+
var EXT_STOP_URL = '/__dsh_remote/external/stop';
|
|
170
|
+
var EXT_LOG_URL = '/__dsh_remote/external/log';
|
|
171
|
+
var SET_PASSWORD_URL = '/__dsh_remote/set-password';
|
|
172
|
+
var AUTH_STATUS_URL = '/__dsh_remote/auth-status';
|
|
173
|
+
|
|
174
|
+
// fetch 带超时:避免后端长时间无响应(如中转服务重启/网络慢)时,
|
|
175
|
+
// 开关按钮一直卡在"连接中"转圈。超时后走 catch 显示友好提示。
|
|
176
|
+
function fetchWithTimeout(url, opts, ms) {
|
|
177
|
+
var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
178
|
+
var t = null;
|
|
179
|
+
if (ctrl) {
|
|
180
|
+
t = setTimeout(function () { ctrl.abort(); }, ms || 25000);
|
|
181
|
+
opts = opts || {};
|
|
182
|
+
opts.signal = ctrl.signal;
|
|
183
|
+
}
|
|
184
|
+
var p = fetch(url, opts);
|
|
185
|
+
if (t) p = p.then(function (r) { clearTimeout(t); return r; }, function (e) { clearTimeout(t); throw e; });
|
|
186
|
+
return p;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 把 abort 超时错误转成用户可读文案
|
|
190
|
+
function fmtFetchError(e, fallback) {
|
|
191
|
+
var err = e && e.name === 'AbortError' ? null : (e && e.message) || e;
|
|
192
|
+
return err ? String(err) : fallback || '请求超时,请稍后重试';
|
|
193
|
+
}
|
|
114
194
|
|
|
115
195
|
function fmtUrl(url) {
|
|
116
196
|
return url || '未检测到局域网地址';
|
|
117
197
|
}
|
|
118
198
|
|
|
199
|
+
function fmtAgo(ts) {
|
|
200
|
+
if (!ts) return '—';
|
|
201
|
+
var s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
202
|
+
if (s < 10) return '刚刚';
|
|
203
|
+
if (s < 60) return s + ' 秒前';
|
|
204
|
+
var m = Math.floor(s / 60);
|
|
205
|
+
if (m < 60) return m + ' 分钟前';
|
|
206
|
+
var h = Math.floor(m / 60);
|
|
207
|
+
if (h < 24) return h + ' 小时前';
|
|
208
|
+
return Math.floor(h / 24) + ' 天前';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function fmtDuration(s) {
|
|
212
|
+
if (s === null || s === undefined) return '—';
|
|
213
|
+
var sec = Math.max(0, Math.floor(s));
|
|
214
|
+
var d = Math.floor(sec / 86400);
|
|
215
|
+
var h = Math.floor((sec % 86400) / 3600);
|
|
216
|
+
var m = Math.floor((sec % 3600) / 60);
|
|
217
|
+
var ss = sec % 60;
|
|
218
|
+
if (d > 0) return d + ' 天 ' + h + ' 小时';
|
|
219
|
+
if (h > 0) return h + ' 小时 ' + m + ' 分';
|
|
220
|
+
if (m > 0) return m + ' 分 ' + ss + ' 秒';
|
|
221
|
+
return ss + ' 秒';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function fmtBytes(b) {
|
|
225
|
+
if (b === null || b === undefined) return '—';
|
|
226
|
+
var n = Number(b);
|
|
227
|
+
if (!isFinite(n)) return '—';
|
|
228
|
+
if (n < 1024) return n + ' B';
|
|
229
|
+
var units = ['KB', 'MB', 'GB', 'TB'];
|
|
230
|
+
var i = -1;
|
|
231
|
+
do { n /= 1024; i += 1; } while (n >= 1024 && i < units.length - 1);
|
|
232
|
+
return n.toFixed(n >= 100 ? 0 : 1) + ' ' + units[i];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function fmtDateTime(ts) {
|
|
236
|
+
if (!ts) return '—';
|
|
237
|
+
try {
|
|
238
|
+
var d = new Date(ts);
|
|
239
|
+
if (isNaN(d.getTime())) return String(ts);
|
|
240
|
+
return d.toLocaleString('zh-CN', { hour12: false });
|
|
241
|
+
} catch (e) { return String(ts); }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 生成一组「标签: 值」行(key-value 行列表),供详情区渲染
|
|
245
|
+
function kvList(items) {
|
|
246
|
+
return items.map(function (it, idx) {
|
|
247
|
+
return el('div', { style: st.detailRow, key: idx },
|
|
248
|
+
el('span', { style: st.detailLabel }, it[0]),
|
|
249
|
+
el('span', { style: st.detailValue }, it[1] || '—'));
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
119
253
|
function RemoteControlPanel() {
|
|
120
254
|
var _s = useState(null);
|
|
121
255
|
var status = _s[0];
|
|
@@ -126,6 +260,19 @@ window.__ModuleLoader__.load({
|
|
|
126
260
|
var _e = useState(null);
|
|
127
261
|
var error = _e[0];
|
|
128
262
|
var setError = _e[1];
|
|
263
|
+
var _exp = useState({});
|
|
264
|
+
var expandedMap = _exp[0];
|
|
265
|
+
var setExpanded = _exp[1];
|
|
266
|
+
function toggleExpanded(id) {
|
|
267
|
+
setExpanded(function (m) {
|
|
268
|
+
var n = Object.assign({}, m);
|
|
269
|
+
n[id] = !n[id];
|
|
270
|
+
return n;
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
var _dshOpen = useState(false);
|
|
274
|
+
var dshOpen = _dshOpen[0];
|
|
275
|
+
var setDshOpen = _dshOpen[1];
|
|
129
276
|
|
|
130
277
|
var refresh = useCallback(function () {
|
|
131
278
|
fetch(STATUS_URL)
|
|
@@ -161,12 +308,12 @@ window.__ModuleLoader__.load({
|
|
|
161
308
|
});
|
|
162
309
|
}, [busy, status, refresh]);
|
|
163
310
|
|
|
164
|
-
//
|
|
311
|
+
// 局域网状态徽标
|
|
165
312
|
var on = !!(status && status.remoteEnabled);
|
|
166
313
|
var badge = el('span', { style: Object.assign({}, st.badge, on ? st.badgeOn : st.badgeOff) },
|
|
167
314
|
on ? '已开启' : '未开启');
|
|
168
315
|
|
|
169
|
-
//
|
|
316
|
+
// 局域网开关
|
|
170
317
|
var sw = el('button', {
|
|
171
318
|
type: 'button',
|
|
172
319
|
role: 'switch',
|
|
@@ -177,7 +324,7 @@ window.__ModuleLoader__.load({
|
|
|
177
324
|
style: Object.assign({}, st.switch, on ? st.switchOn : null),
|
|
178
325
|
}, el('span', { style: Object.assign({}, st.knob, on ? st.knobOn : null) }));
|
|
179
326
|
|
|
180
|
-
//
|
|
327
|
+
// 局域网二维码
|
|
181
328
|
var qrEl;
|
|
182
329
|
if (on && status && status.url) {
|
|
183
330
|
var qrSrc = '/__dsh_remote/qr?url=' + encodeURIComponent(status.url);
|
|
@@ -195,7 +342,133 @@ window.__ModuleLoader__.load({
|
|
|
195
342
|
// 设备数
|
|
196
343
|
var count = status ? status.deviceCount : 0;
|
|
197
344
|
|
|
198
|
-
//
|
|
345
|
+
// ---- 外网访问状态 + 操作 ----
|
|
346
|
+
var _bc = useState('');
|
|
347
|
+
var bindCode = _bc[0];
|
|
348
|
+
var setBindCode = _bc[1];
|
|
349
|
+
var _sb = useState('');
|
|
350
|
+
var serverBase = _sb[0];
|
|
351
|
+
var setServerBase = _sb[1];
|
|
352
|
+
var _xbusy = useState(false);
|
|
353
|
+
var extBusy = _xbusy[0];
|
|
354
|
+
var setExtBusy = _xbusy[1];
|
|
355
|
+
var _xerr = useState(null);
|
|
356
|
+
var extErr = _xerr[0];
|
|
357
|
+
var setExtErr = _xerr[1];
|
|
358
|
+
var _xlog = useState(null);
|
|
359
|
+
var extLog = _xlog[0];
|
|
360
|
+
var setExtLog = _xlog[1];
|
|
361
|
+
var _frpclog = useState(null);
|
|
362
|
+
var frpcLog = _frpclog[0];
|
|
363
|
+
var setFrpcLog = _frpclog[1];
|
|
364
|
+
function viewFrpcLog() {
|
|
365
|
+
if (frpcLog) { setFrpcLog(null); return; }
|
|
366
|
+
fetchWithTimeout(EXT_LOG_URL, null, 10000)
|
|
367
|
+
.then(function (r) {
|
|
368
|
+
var ct = String(r.headers.get('content-type') || '').toLowerCase();
|
|
369
|
+
return r.text().then(function (text) {
|
|
370
|
+
if (ct.indexOf('application/json') >= 0) {
|
|
371
|
+
try { return JSON.parse(text); } catch (e) { throw new Error('接口返回异常'); }
|
|
372
|
+
}
|
|
373
|
+
// 服务端返回 HTML(例如旧版 host 没有该路由,落到 SPA 回退页)
|
|
374
|
+
throw new Error('服务端未提供该接口(可能是 dsh 服务未重启)。请重启 dsh 服务后再试。');
|
|
375
|
+
});
|
|
376
|
+
})
|
|
377
|
+
.then(function (d) { setFrpcLog(d && d.log ? d.log : '(日志为空)'); })
|
|
378
|
+
.catch(function (err) { setFrpcLog('读取失败:' + String(err && err.message || err)); });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 用户是否已手动编辑过绑定码 / 中转地址。一旦编辑过就停止自动回填,
|
|
382
|
+
// 否则 status 每 5 秒轮询刷新时会用持久化旧值覆盖用户刚清空的输入框。
|
|
383
|
+
var editedRef = useRef(false);
|
|
384
|
+
function markEdited() { editedRef.current = true; }
|
|
385
|
+
|
|
386
|
+
// 回填已持久化的绑定信息(dsh 重启后免重新输入验证码 / 中转地址)
|
|
387
|
+
useEffect(function () {
|
|
388
|
+
if (!status || !status.external || !status.external.persisted) return;
|
|
389
|
+
if (editedRef.current) return;
|
|
390
|
+
var p = status.external.persisted;
|
|
391
|
+
if (p.bindCode && bindCode === '') setBindCode(p.bindCode);
|
|
392
|
+
if (p.serverBase && serverBase === '') setServerBase(p.serverBase);
|
|
393
|
+
}, [status, bindCode, serverBase]);
|
|
394
|
+
|
|
395
|
+
// 外网状态:online / connecting / stopped / idle / error
|
|
396
|
+
var extSt = status && status.external ? status.external.status : 'idle';
|
|
397
|
+
var extUrl = status && status.external ? status.external.url : null;
|
|
398
|
+
var extStarted = extSt === 'online';
|
|
399
|
+
var extConnecting = extSt === 'connecting';
|
|
400
|
+
var extError = extSt === 'error';
|
|
401
|
+
// 后端探测到的错误(如鉴权失败 / token 不匹配),优先于请求时的临时错误
|
|
402
|
+
var extErrText = extErr || (extError && status && status.external && status.external.error ? status.external.error : null);
|
|
403
|
+
|
|
404
|
+
function externalStart() {
|
|
405
|
+
if (extBusy || extConnecting) return;
|
|
406
|
+
if (!bindCode || !bindCode.trim()) {
|
|
407
|
+
setExtErr('请先填写绑定码(留空无法开启外网访问)');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
setExtBusy(true);
|
|
411
|
+
setExtErr(null);
|
|
412
|
+
setExtLog(null);
|
|
413
|
+
fetchWithTimeout(EXT_START_URL, {
|
|
414
|
+
method: 'POST',
|
|
415
|
+
headers: { 'Content-Type': 'application/json' },
|
|
416
|
+
body: JSON.stringify({ bindCode: bindCode, serverBase: serverBase || undefined }),
|
|
417
|
+
}, 30000)
|
|
418
|
+
.then(function (r) { return r.json(); })
|
|
419
|
+
.then(function (data) {
|
|
420
|
+
if (!data.ok) {
|
|
421
|
+
setExtLog(data.log || null); // 展示 frpc 真实日志,便于定位根因
|
|
422
|
+
throw new Error(data.message || data.error || 'start failed');
|
|
423
|
+
}
|
|
424
|
+
setExtLog(null);
|
|
425
|
+
setTimeout(refresh, 1400);
|
|
426
|
+
})
|
|
427
|
+
.catch(function (e) {
|
|
428
|
+
setExtErr(fmtFetchError(e, '开启外网访问超时(后端响应过慢),请稍后重试或查看下方 frpc 日志'));
|
|
429
|
+
setTimeout(refresh, 600); // 立即刷新,让开关从"连接中"恢复可操作
|
|
430
|
+
})
|
|
431
|
+
.finally(function () { setExtBusy(false); });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function externalStop() {
|
|
435
|
+
if (extBusy) return;
|
|
436
|
+
setExtBusy(true);
|
|
437
|
+
setExtErr(null);
|
|
438
|
+
setExtLog(null);
|
|
439
|
+
fetchWithTimeout(EXT_STOP_URL, { method: 'POST' }, 10000)
|
|
440
|
+
.then(function () { setTimeout(refresh, 800); })
|
|
441
|
+
.catch(function (e) { setExtErr(fmtFetchError(e, '关闭超时,请稍后重试')); })
|
|
442
|
+
.finally(function () { setExtBusy(false); });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 外网开关:offline / connecting / online / error 均可操作,连接中点击即取消
|
|
446
|
+
var extSwitch = el('button', {
|
|
447
|
+
type: 'button', role: 'switch',
|
|
448
|
+
'aria-checked': String(extStarted),
|
|
449
|
+
'aria-label': extStarted ? '关闭外网访问' : (extConnecting ? '取消连接' : '开启外网访问'),
|
|
450
|
+
onClick: (extStarted || extConnecting) ? externalStop : externalStart,
|
|
451
|
+
disabled: extBusy,
|
|
452
|
+
style: Object.assign({}, st.switch, (extStarted || extConnecting) ? st.switchOn : null),
|
|
453
|
+
}, el('span', { style: Object.assign({}, st.knob, (extStarted || extConnecting) ? st.knobOn : null) }));
|
|
454
|
+
|
|
455
|
+
var extBadge = el('span', {
|
|
456
|
+
style: Object.assign({}, st.badge,
|
|
457
|
+
extStarted ? st.badgeOn : (extConnecting ? st.badgeBusy : (extError ? st.badgeErr : st.badgeOff))),
|
|
458
|
+
}, extStarted ? '在线' : (extConnecting ? '连接中' : (extError ? '出错' : '未开启')));
|
|
459
|
+
|
|
460
|
+
// 外网二维码
|
|
461
|
+
var extQr;
|
|
462
|
+
if (extStarted && extUrl) {
|
|
463
|
+
extQr = el('div', { style: st.qrBox },
|
|
464
|
+
el('img', { src: '/__dsh_remote/qr?url=' + encodeURIComponent(extUrl), alt: '外网二维码', style: st.qrImg }));
|
|
465
|
+
} else {
|
|
466
|
+
extQr = el('div', { style: st.qrFallback },
|
|
467
|
+
extConnecting ? '正在连接中转服务器…'
|
|
468
|
+
: (extError ? '外网隧道异常,请查看上方错误信息' : '开启外网访问后,这里显示外网二维码'));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 命令行提示(未开启时展示 CLI 启动方式)
|
|
199
472
|
var cliHint = null;
|
|
200
473
|
if (status && !status.remoteEnabled) {
|
|
201
474
|
cliHint = el('div', { style: st.card },
|
|
@@ -213,7 +486,133 @@ window.__ModuleLoader__.load({
|
|
|
213
486
|
);
|
|
214
487
|
}
|
|
215
488
|
|
|
489
|
+
// ---- 远程访问密码管理 ----
|
|
490
|
+
var authOn = !!(status && status.auth && status.auth.enabled);
|
|
491
|
+
var authRequiredHere = !!(status && status.auth && status.auth.requiredHere);
|
|
492
|
+
var _p1 = useState('');
|
|
493
|
+
var pwd1 = _p1[0];
|
|
494
|
+
var setPwd1 = _p1[1];
|
|
495
|
+
var _p2 = useState('');
|
|
496
|
+
var pwd2 = _p2[0];
|
|
497
|
+
var setPwd2 = _p2[1];
|
|
498
|
+
var _msgbusy = useState(false);
|
|
499
|
+
var msgBusy = _msgbusy[0];
|
|
500
|
+
var setMsgBusy = _msgbusy[1];
|
|
501
|
+
var _msg = useState(null);
|
|
502
|
+
var msg = _msg[0];
|
|
503
|
+
var setMsg = _msg[1];
|
|
504
|
+
|
|
505
|
+
function savePassword() {
|
|
506
|
+
if (msgBusy) return;
|
|
507
|
+
if (pwd1 !== pwd2) {
|
|
508
|
+
setMsg({ type: 'err', text: '两次输入的密码不一致' });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (pwd1 && pwd1.length < 4) {
|
|
512
|
+
setMsg({ type: 'err', text: '密码至少 4 位' });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
setMsgBusy(true);
|
|
516
|
+
setMsg(null);
|
|
517
|
+
fetch(SET_PASSWORD_URL, {
|
|
518
|
+
method: 'POST',
|
|
519
|
+
headers: { 'Content-Type': 'application/json' },
|
|
520
|
+
body: JSON.stringify({ password: pwd1 }),
|
|
521
|
+
})
|
|
522
|
+
.then(function (r) { return r.json(); })
|
|
523
|
+
.then(function (data) {
|
|
524
|
+
if (!data.ok) throw new Error(data.message || data.error || '保存失败');
|
|
525
|
+
setMsg({ type: 'ok', text: data.enabled ? '已设置远程访问密码。' : '已清除远程访问密码。' });
|
|
526
|
+
setPwd1('');
|
|
527
|
+
setPwd2('');
|
|
528
|
+
setTimeout(refresh, 300);
|
|
529
|
+
})
|
|
530
|
+
.catch(function (e) { setMsg({ type: 'err', text: String(e && e.message || e) }); })
|
|
531
|
+
.finally(function () { setMsgBusy(false); });
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
var passwordCard = el('div', { style: st.card },
|
|
535
|
+
el('div', { style: st.row },
|
|
536
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
537
|
+
el('p', { style: st.title }, '远程访问密码'),
|
|
538
|
+
el('p', { style: st.sub },
|
|
539
|
+
authOn
|
|
540
|
+
? '已启用:远程访问需要输入密码'
|
|
541
|
+
: '未设置:远程访问无需密码')),
|
|
542
|
+
el('span', {
|
|
543
|
+
style: Object.assign({}, st.badge, authOn ? st.badgeOn : st.badgeOff),
|
|
544
|
+
}, authOn ? '已设置' : '未设置')),
|
|
545
|
+
|
|
546
|
+
el('p', { style: st.hint },
|
|
547
|
+
authRequiredHere
|
|
548
|
+
? '当前设备通过远程访问进入,需输入密码。你在本机设置/修改即可。'
|
|
549
|
+
: '设置后,通过外网隧道(远程)访问本机 DSH 时,需先输入该密码才能进入页面;本机/内网访问不受影响。'),
|
|
550
|
+
|
|
551
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
552
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
553
|
+
el('label', { style: st.hint }, authOn ? '新密码(留空并点击“清除”可关闭门禁)' : '设置远程访问密码'),
|
|
554
|
+
el('input', {
|
|
555
|
+
type: 'password', value: pwd1, placeholder: '至少 4 位',
|
|
556
|
+
onChange: function (e) { setPwd1(e.target.value); },
|
|
557
|
+
autoComplete: 'new-password', style: st.input,
|
|
558
|
+
})),
|
|
559
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
560
|
+
el('label', { style: st.hint }, '确认密码'),
|
|
561
|
+
el('input', {
|
|
562
|
+
type: 'password', value: pwd2,
|
|
563
|
+
onChange: function (e) { setPwd2(e.target.value); },
|
|
564
|
+
autoComplete: 'new-password', style: st.input,
|
|
565
|
+
}))),
|
|
566
|
+
|
|
567
|
+
el('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
568
|
+
el('button', {
|
|
569
|
+
type: 'button',
|
|
570
|
+
onClick: savePassword,
|
|
571
|
+
disabled: msgBusy,
|
|
572
|
+
style: {
|
|
573
|
+
padding: '8px 16px', borderRadius: 8, fontSize: 13, fontWeight: 600,
|
|
574
|
+
color: '#fff', background: 'var(--dsw-alias-state-business-primary, #2a7de1)',
|
|
575
|
+
border: 'none', cursor: 'pointer',
|
|
576
|
+
},
|
|
577
|
+
}, authOn ? '修改密码' : '设置密码'),
|
|
578
|
+
authOn
|
|
579
|
+
? el('button', {
|
|
580
|
+
type: 'button',
|
|
581
|
+
onClick: function () {
|
|
582
|
+
if (!window.confirm('确定清除远程访问密码吗?此后远程访问将不再要求输入密码。')) return;
|
|
583
|
+
setMsgBusy(true);
|
|
584
|
+
setMsg(null);
|
|
585
|
+
fetch(SET_PASSWORD_URL, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: { 'Content-Type': 'application/json' },
|
|
588
|
+
body: JSON.stringify({ password: '' }),
|
|
589
|
+
})
|
|
590
|
+
.then(function (r) { return r.json(); })
|
|
591
|
+
.then(function (data) {
|
|
592
|
+
if (!data.ok) throw new Error(data.message || data.error || '清除失败');
|
|
593
|
+
setMsg({ type: 'ok', text: '已清除远程访问密码。' });
|
|
594
|
+
setPwd1(''); setPwd2('');
|
|
595
|
+
setTimeout(refresh, 300);
|
|
596
|
+
})
|
|
597
|
+
.catch(function (e) { setMsg({ type: 'err', text: String(e && e.message || e) }); })
|
|
598
|
+
.finally(function () { setMsgBusy(false); });
|
|
599
|
+
},
|
|
600
|
+
disabled: msgBusy,
|
|
601
|
+
style: {
|
|
602
|
+
padding: '8px 16px', borderRadius: 8, fontSize: 13, fontWeight: 600,
|
|
603
|
+
color: '#c0272b', background: 'transparent',
|
|
604
|
+
border: '1px solid rgba(192,39,43,.4)', cursor: 'pointer',
|
|
605
|
+
},
|
|
606
|
+
}, '清除')
|
|
607
|
+
: null),
|
|
608
|
+
|
|
609
|
+
msg
|
|
610
|
+
? el('p', { style: Object.assign({}, st.hint, { color: msg.type === 'ok' ? '#0d7a3f' : '#c0272b' }) }, msg.text)
|
|
611
|
+
: null);
|
|
612
|
+
|
|
216
613
|
return el('div', { style: st.root },
|
|
614
|
+
// 远程访问密码门禁(第一个卡片,最醒目)
|
|
615
|
+
passwordCard,
|
|
217
616
|
// 状态行
|
|
218
617
|
el('div', { style: st.row },
|
|
219
618
|
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
@@ -222,7 +621,7 @@ window.__ModuleLoader__.load({
|
|
|
222
621
|
el('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
223
622
|
badge, sw)),
|
|
224
623
|
|
|
225
|
-
//
|
|
624
|
+
// 局域网二维码卡片
|
|
226
625
|
el('div', { style: st.card },
|
|
227
626
|
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
228
627
|
el('p', { style: st.title }, '手机连接'),
|
|
@@ -237,10 +636,75 @@ window.__ModuleLoader__.load({
|
|
|
237
636
|
el('div', { style: st.stat },
|
|
238
637
|
el('span', { style: st.statValue }, String(count)),
|
|
239
638
|
el('span', { style: st.statLabel }, '当前连接设备')),
|
|
639
|
+
el('div', { style: st.stat },
|
|
640
|
+
el('span', { style: st.statValue }, String(status ? (status.totalDevicesEver || 0) : 0)),
|
|
641
|
+
el('span', { style: st.statLabel }, '累计设备')),
|
|
642
|
+
el('div', { style: st.stat },
|
|
643
|
+
el('span', { style: st.statValue }, String(status ? (status.totalHeartbeats || 0) : 0)),
|
|
644
|
+
el('span', { style: st.statLabel }, '累计心跳')),
|
|
240
645
|
el('div', { style: st.stat },
|
|
241
646
|
el('span', { style: st.statValue }, on ? String(status ? status.lanAddresses.length : 0) : '—'),
|
|
242
647
|
el('span', { style: st.statLabel }, '局域网地址'))),
|
|
243
648
|
|
|
649
|
+
// 已连接设备列表(host 从心跳请求采集 IP / UA / 屏幕 / 网络等详情,可点击展开)
|
|
650
|
+
status && status.devices && status.devices.length
|
|
651
|
+
? el('div', { style: st.card },
|
|
652
|
+
el('div', { style: st.row },
|
|
653
|
+
el('p', { style: st.title }, '已连接设备'),
|
|
654
|
+
el('span', { style: Object.assign({}, st.badge, st.badgeOn) },
|
|
655
|
+
String(status.devices.length) + ' 台在线')),
|
|
656
|
+
el('p', { style: st.hint }, '点击设备行可查看详细上报信息(屏幕 / 网络 / 页面等)'),
|
|
657
|
+
el('div', { style: st.deviceList },
|
|
658
|
+
status.devices.map(function (d) {
|
|
659
|
+
var open = !!expandedMap[d.id];
|
|
660
|
+
var connTxt = d.connection
|
|
661
|
+
? ((d.connection.effectiveType || '') +
|
|
662
|
+
(d.connection.downlink ? ' · ' + d.connection.downlink + ' Mbps' : '') +
|
|
663
|
+
(d.connection.rtt !== null && d.connection.rtt !== undefined ? ' · ' + d.connection.rtt + 'ms' : ''))
|
|
664
|
+
: '';
|
|
665
|
+
return el('div', { style: st.deviceCard, key: d.id },
|
|
666
|
+
el('button', {
|
|
667
|
+
type: 'button',
|
|
668
|
+
onClick: function () { toggleExpanded(d.id); },
|
|
669
|
+
style: st.deviceHead,
|
|
670
|
+
},
|
|
671
|
+
el('span', { style: d.online ? st.dotOn : st.dotOff }),
|
|
672
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0, flex: 1 } },
|
|
673
|
+
el('span', { style: st.deviceName }, d.name || '未知设备'),
|
|
674
|
+
el('span', { style: st.meta },
|
|
675
|
+
(d.ip || '未知 IP') +
|
|
676
|
+
(d.os && d.os !== '未知系统' ? ' · ' + d.os : '') +
|
|
677
|
+
(d.browser ? ' · ' + d.browser : '') +
|
|
678
|
+
(d.mobile ? ' · 移动端' : '') +
|
|
679
|
+
(connTxt ? ' · ' + connTxt : '') +
|
|
680
|
+
' · 活跃于 ' + fmtAgo(d.lastSeen))),
|
|
681
|
+
el('span', { style: open ? st.chevronOpen : st.chevron }, '▼')),
|
|
682
|
+
open
|
|
683
|
+
? el('div', { style: st.deviceDetail }, kvList([
|
|
684
|
+
['IP 地址', d.ip || '未知'],
|
|
685
|
+
['系统 / 浏览器', (d.os || '—') + ' / ' + (d.browser || '—')],
|
|
686
|
+
['移动端标记', d.mobile ? '是' : '否'],
|
|
687
|
+
['屏幕', d.screen || '—'],
|
|
688
|
+
['视口', d.viewport || '—'],
|
|
689
|
+
['像素密度', d.dpr ? d.dpr + 'x' : '—'],
|
|
690
|
+
['触屏设备', d.touch ? '是' : '否'],
|
|
691
|
+
['网络连接', connTxt || '—'],
|
|
692
|
+
['设备内存', d.deviceMemory ? String(d.deviceMemory) + ' GB' : '—'],
|
|
693
|
+
['CPU 核数', d.hardwareConcurrency ? String(d.hardwareConcurrency) : '—'],
|
|
694
|
+
['电池', d.battery
|
|
695
|
+
? (String(d.battery.level || 0) + '%' + (d.battery.charging ? '(充电中)' : ''))
|
|
696
|
+
: '—'],
|
|
697
|
+
['在线状态', d.online ? '在线' : '离线'],
|
|
698
|
+
['语言 / 平台', [d.lang, d.platform].filter(Boolean).join(' / ') || '—'],
|
|
699
|
+
['当前页面', d.title ? (d.title + (d.path ? ' ' + d.path : '')) : (d.path || '—')],
|
|
700
|
+
['首次连接', fmtDateTime(d.firstSeen)],
|
|
701
|
+
['最后活跃', fmtDateTime(d.lastSeen)],
|
|
702
|
+
['心跳次数', String(d.beatCount || 0)],
|
|
703
|
+
]))
|
|
704
|
+
: null);
|
|
705
|
+
})))
|
|
706
|
+
: null,
|
|
707
|
+
|
|
244
708
|
status && status.lanAddresses.length
|
|
245
709
|
? el('div', { style: st.card },
|
|
246
710
|
el('p', { style: st.title }, '手机访问地址'),
|
|
@@ -254,6 +718,228 @@ window.__ModuleLoader__.load({
|
|
|
254
718
|
el('pre', { style: st.code }, status.patchFile))
|
|
255
719
|
: null,
|
|
256
720
|
|
|
721
|
+
// 运行时信息(进程 / 系统 / CPU / 内存,诊断用)
|
|
722
|
+
status && status.runtime
|
|
723
|
+
? el('div', { style: st.card },
|
|
724
|
+
el('div', { style: st.row },
|
|
725
|
+
el('p', { style: st.title }, '运行时信息'),
|
|
726
|
+
el('span', { style: Object.assign({}, st.badge, st.badgeOff) },
|
|
727
|
+
'PID ' + status.runtime.pid)),
|
|
728
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } }, kvList([
|
|
729
|
+
['运行时长', fmtDuration(status.runtime.uptime)],
|
|
730
|
+
['开机时长', fmtDuration(status.runtime.sysUptime)],
|
|
731
|
+
['Node.js', status.runtime.nodeVersion],
|
|
732
|
+
['主机名', status.runtime.hostname],
|
|
733
|
+
['系统', (status.runtime.platform || '—') + ' ' + (status.runtime.release || '') + ' (' + (status.runtime.arch || '—') + ')'],
|
|
734
|
+
['CPU 型号', status.runtime.cpuModel || '—'],
|
|
735
|
+
['CPU', String(status.runtime.cpus) + ' 核' + (status.runtime.cpuSpeed ? ' @ ' + status.runtime.cpuSpeed + ' MHz' : '')],
|
|
736
|
+
['系统负载', status.runtime.loadavg
|
|
737
|
+
? ('1m ' + status.runtime.loadavg[0] + ' · 5m ' + status.runtime.loadavg[1] + ' · 15m ' + status.runtime.loadavg[2])
|
|
738
|
+
: '—'],
|
|
739
|
+
['内存', fmtBytes(status.runtime.freemem) + ' 可用 / ' + fmtBytes(status.runtime.totalmem) + ' 总计' +
|
|
740
|
+
(status.runtime.memPct !== null && status.runtime.memPct !== undefined
|
|
741
|
+
? '(已用 ' + status.runtime.memPct + '%)'
|
|
742
|
+
: '')],
|
|
743
|
+
['RSS', fmtBytes(status.runtime.rss)],
|
|
744
|
+
['堆内存', fmtBytes(status.runtime.heapUsed) + ' / ' + fmtBytes(status.runtime.heapTotal)],
|
|
745
|
+
['Profile', status.runtime.profile],
|
|
746
|
+
['DSH 数据目录', status.runtime.dshHome],
|
|
747
|
+
])))
|
|
748
|
+
: null,
|
|
749
|
+
|
|
750
|
+
// DSH 应用层状态(版本 / 会话 / 工作区 / 插件 / 模型;后台缓存,服务不可用时不显示)
|
|
751
|
+
// 默认折叠,仅展示摘要行;点击标题展开 / 收起详情。
|
|
752
|
+
status && status.dsh
|
|
753
|
+
? el('div', { style: st.card },
|
|
754
|
+
el('button', {
|
|
755
|
+
type: 'button',
|
|
756
|
+
onClick: function () { setDshOpen(!dshOpen); },
|
|
757
|
+
style: Object.assign({}, st.deviceHead, { padding: '4px 0' }),
|
|
758
|
+
},
|
|
759
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0, flex: 1 } },
|
|
760
|
+
el('div', { style: st.row },
|
|
761
|
+
el('p', { style: st.title }, 'DSH 状态'),
|
|
762
|
+
el('span', { style: Object.assign({}, st.badge, st.badgeOff) },
|
|
763
|
+
status.dsh.version ? ('v' + status.dsh.version) : '版本未知')),
|
|
764
|
+
el('span', { style: st.meta },
|
|
765
|
+
'会话 ' + (status.dsh.sessions !== null && status.dsh.sessions !== undefined ? status.dsh.sessions : '—') +
|
|
766
|
+
' · 工作区 ' + (status.dsh.workspaces ? status.dsh.workspaces.length : '—') +
|
|
767
|
+
' · 插件 ' + (status.dsh.plugins ? status.dsh.plugins.length : '—') +
|
|
768
|
+
' · 模型 ' + (status.dsh.llm ? status.dsh.llm.length : '—'))),
|
|
769
|
+
el('span', { style: dshOpen ? st.chevronOpen : st.chevron }, '▼')),
|
|
770
|
+
dshOpen
|
|
771
|
+
? el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
|
|
772
|
+
kvList([
|
|
773
|
+
['版本号', status.dsh.version || '—'],
|
|
774
|
+
['会话总数', status.dsh.sessions !== null && status.dsh.sessions !== undefined ? String(status.dsh.sessions) : '—'],
|
|
775
|
+
['工作区数', status.dsh.workspaces ? String(status.dsh.workspaces.length) : '—'],
|
|
776
|
+
['已装插件', status.dsh.plugins ? String(status.dsh.plugins.length) : '—'],
|
|
777
|
+
['模型提供方', status.dsh.llm
|
|
778
|
+
? String(status.dsh.llm.length) + '(' + status.dsh.llm.map(function (p) { return p.name || p.id; }).join('、') + ')'
|
|
779
|
+
: '—'],
|
|
780
|
+
]),
|
|
781
|
+
status.dsh.workspaces && status.dsh.workspaces.length
|
|
782
|
+
? el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
|
|
783
|
+
el('span', { style: st.hint }, '工作区'),
|
|
784
|
+
status.dsh.workspaces.map(function (w, idx) {
|
|
785
|
+
return el('div', { style: st.deviceRow, key: 'ws' + idx },
|
|
786
|
+
el('span', { style: st.dotOn }),
|
|
787
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0, flex: 1 } },
|
|
788
|
+
el('span', { style: st.deviceName }, w.title || w.path),
|
|
789
|
+
el('span', { style: st.meta }, w.path + ' · ' + String(w.sessionCount || 0) + ' 会话')));
|
|
790
|
+
}))
|
|
791
|
+
: null,
|
|
792
|
+
status.dsh.plugins && status.dsh.plugins.length
|
|
793
|
+
? el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 } },
|
|
794
|
+
el('span', { style: st.hint }, '已安装插件'),
|
|
795
|
+
status.dsh.plugins.map(function (p, idx) {
|
|
796
|
+
return el('div', { style: st.deviceRow, key: 'pl' + idx },
|
|
797
|
+
el('span', { style: p.enabled ? st.dotOn : st.dotOff }),
|
|
798
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0, flex: 1 } },
|
|
799
|
+
el('span', { style: st.deviceName }, p.name),
|
|
800
|
+
el('span', { style: st.meta }, (p.enabled ? '已启用' : '已停用') + (p.phase ? ' · ' + p.phase : ''))));
|
|
801
|
+
}))
|
|
802
|
+
: null)
|
|
803
|
+
: null)
|
|
804
|
+
: null,
|
|
805
|
+
|
|
806
|
+
// 网络接口详情(网卡名 / 地址 / 掩码 / MAC)
|
|
807
|
+
status && status.lanInterfaces && status.lanInterfaces.length
|
|
808
|
+
? el('div', { style: st.card },
|
|
809
|
+
el('div', { style: st.row },
|
|
810
|
+
el('p', { style: st.title }, '网络接口'),
|
|
811
|
+
el('span', { style: Object.assign({}, st.badge, st.badgeOff) },
|
|
812
|
+
String(status.lanInterfaces.length) + ' 个 IPv4')),
|
|
813
|
+
el('div', { style: st.deviceList },
|
|
814
|
+
status.lanInterfaces.map(function (it) {
|
|
815
|
+
return el('div', { style: st.deviceRow, key: it.name + ':' + it.address },
|
|
816
|
+
el('span', { style: it.internal ? st.dotOff : st.dotOn }),
|
|
817
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0, flex: 1 } },
|
|
818
|
+
el('span', { style: st.deviceName }, it.name + (it.internal ? '(回环)' : '')),
|
|
819
|
+
el('span', { style: st.meta },
|
|
820
|
+
it.address + (it.cidr ? ' / ' + it.cidr : '') +
|
|
821
|
+
' · 掩码 ' + (it.netmask || '—') +
|
|
822
|
+
(it.mac ? ' · ' + it.mac : ''))));
|
|
823
|
+
})))
|
|
824
|
+
: null,
|
|
825
|
+
|
|
826
|
+
// 外网访问卡片
|
|
827
|
+
el('div', { style: st.card },
|
|
828
|
+
el('div', { style: st.row },
|
|
829
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
830
|
+
el('p', { style: st.title }, '外网访问'),
|
|
831
|
+
el('p', { style: st.sub }, '通过中转服务器,让外网设备访问本电脑的 DSH')),
|
|
832
|
+
el('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
833
|
+
extBadge, extSwitch)),
|
|
834
|
+
|
|
835
|
+
extStarted && extUrl
|
|
836
|
+
? el('a', { href: extUrl, style: st.link, target: '_blank', rel: 'noreferrer' }, extUrl)
|
|
837
|
+
: null,
|
|
838
|
+
|
|
839
|
+
// 外网隧道详细状态(域名 / 端口 / frpc 版本 / 中转服务器信息)
|
|
840
|
+
extStarted && status.external
|
|
841
|
+
? el('div', { style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '10px 12px', borderRadius: 10, background: 'var(--dsw-alias-bg-base, #f0f0f1)', border: '1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.18))' } },
|
|
842
|
+
el('div', { style: st.row },
|
|
843
|
+
el('span', { style: st.meta },
|
|
844
|
+
'域名:' + (status.external.domain || (status.external.server && status.external.server.externalDomain) || '—')),
|
|
845
|
+
el('span', { style: Object.assign({}, st.badge, st.badgeOn) }, '在线')),
|
|
846
|
+
el('div', { style: st.row },
|
|
847
|
+
el('span', { style: st.meta },
|
|
848
|
+
'隧道端口:' + (status.external.tunnelPort || (status.external.server && status.external.server.tunnelPort) || '—')),
|
|
849
|
+
status.external.frpcVersion
|
|
850
|
+
? el('span', { style: st.meta }, 'frpc v' + status.external.frpcVersion)
|
|
851
|
+
: null),
|
|
852
|
+
el('div', { style: st.row },
|
|
853
|
+
el('span', { style: st.meta },
|
|
854
|
+
'frpc 进程:' + (status.external.pid ? 'PID ' + status.external.pid : '—')),
|
|
855
|
+
status.external.startedAt
|
|
856
|
+
? el('span', { style: st.meta }, '已运行 ' + fmtDuration(Math.floor((Date.now() - status.external.startedAt) / 1000)))
|
|
857
|
+
: null),
|
|
858
|
+
el('div', { style: st.row },
|
|
859
|
+
el('span', { style: st.meta },
|
|
860
|
+
'绑定码:' + (status.external.bindCodeShort || '—')),
|
|
861
|
+
null),
|
|
862
|
+
status.external.server && status.external.server.proxy
|
|
863
|
+
? el('div', { style: st.row },
|
|
864
|
+
el('span', { style: st.meta },
|
|
865
|
+
'今日流量:↓' + fmtBytes(status.external.server.proxy.todayTrafficIn) +
|
|
866
|
+
' · ↑' + fmtBytes(status.external.server.proxy.todayTrafficOut)),
|
|
867
|
+
el('span', { style: st.meta },
|
|
868
|
+
'当前连接:' + String(status.external.server.proxy.curConns || 0)))
|
|
869
|
+
: null,
|
|
870
|
+
status.external.server && status.external.server.proxy && status.external.server.proxy.localPort
|
|
871
|
+
? el('div', { style: st.row },
|
|
872
|
+
el('span', { style: st.meta },
|
|
873
|
+
'本地转发:127.0.0.1:' + status.external.server.proxy.localPort +
|
|
874
|
+
(status.external.server.proxy.lastStartTime ? ' · 启动 ' + status.external.server.proxy.lastStartTime : '')),
|
|
875
|
+
null)
|
|
876
|
+
: null,
|
|
877
|
+
el('div', { style: st.row },
|
|
878
|
+
el('span', { style: st.meta },
|
|
879
|
+
'中转成员:' + (status.external.server ? (status.external.server.name || '—') : '—')),
|
|
880
|
+
status.external.server && status.external.server.last_seen
|
|
881
|
+
? el('span', { style: st.meta }, '最后心跳:' + status.external.server.last_seen)
|
|
882
|
+
: null),
|
|
883
|
+
status.external.server && status.external.server.status
|
|
884
|
+
? el('div', { style: st.row },
|
|
885
|
+
el('span', { style: st.meta },
|
|
886
|
+
'服务器状态:' + (status.external.server.status === 'online'
|
|
887
|
+
? '在线'
|
|
888
|
+
: (status.external.server.status === 'offline' ? '离线' : '未知'))),
|
|
889
|
+
status.external.server.serverTime
|
|
890
|
+
? el('span', { style: st.meta }, '服务器时间:' + status.external.server.serverTime)
|
|
891
|
+
: null)
|
|
892
|
+
: null,
|
|
893
|
+
el('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 2 } },
|
|
894
|
+
el('button', { type: 'button', onClick: viewFrpcLog, style: st.btn }, frpcLog ? '收起日志' : '查看 frpc 日志'),
|
|
895
|
+
el('button', { type: 'button', onClick: refresh, style: st.btn }, '刷新状态')))
|
|
896
|
+
: null,
|
|
897
|
+
|
|
898
|
+
// frpc 运行日志(可点击查看)
|
|
899
|
+
frpcLog
|
|
900
|
+
? el('pre', { style: Object.assign({}, st.code, { marginTop: 8, maxHeight: 220, overflowY: 'auto' }) }, frpcLog)
|
|
901
|
+
: null,
|
|
902
|
+
|
|
903
|
+
// 服务端 FRP_TOKEN 仍是占位符 → 隧道必然 502,醒目提示(后端 /status /stats 返回)
|
|
904
|
+
status && status.external && status.external.server && status.external.server.frpTokenWarning
|
|
905
|
+
? el('div', { style: Object.assign({}, st.card, { borderColor: 'rgba(200,150,0,.4)' }) },
|
|
906
|
+
el('p', { style: Object.assign({}, st.hint, { color: '#9a6a00' }) },
|
|
907
|
+
'⚠ ' + status.external.server.frpTokenWarning))
|
|
908
|
+
: null,
|
|
909
|
+
|
|
910
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
911
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
912
|
+
el('label', { style: st.hint }, '绑定码'),
|
|
913
|
+
el('input', {
|
|
914
|
+
type: 'text', value: bindCode,
|
|
915
|
+
placeholder: '从中转平台获取的绑定码',
|
|
916
|
+
onChange: function (e) { markEdited(); setBindCode(e.target.value); },
|
|
917
|
+
style: st.input,
|
|
918
|
+
})),
|
|
919
|
+
el('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
920
|
+
el('label', { style: st.hint }, '中转服务地址(可选,默认使用内置地址)'),
|
|
921
|
+
el('input', {
|
|
922
|
+
type: 'text', value: serverBase,
|
|
923
|
+
placeholder: (status && status.external && status.external.defaultServerBase)
|
|
924
|
+
? status.external.defaultServerBase
|
|
925
|
+
: '如 https://api.deepseekharness.desktop.cwj666.top',
|
|
926
|
+
onChange: function (e) { markEdited(); setServerBase(e.target.value); },
|
|
927
|
+
style: st.input,
|
|
928
|
+
}))),
|
|
929
|
+
|
|
930
|
+
extQr,
|
|
931
|
+
|
|
932
|
+
extErrText
|
|
933
|
+
? el('div', { style: Object.assign({}, st.card, { borderColor: 'rgba(200,40,40,.35)' }) },
|
|
934
|
+
el('p', { style: Object.assign({}, st.hint, { color: '#c0272b' }) }, '外网访问失败:' + extErrText),
|
|
935
|
+
extLog
|
|
936
|
+
? el('pre', { style: Object.assign({}, st.code, { marginTop: 8, maxHeight: 180, overflowY: 'auto' }) }, extLog)
|
|
937
|
+
: null,
|
|
938
|
+
el('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 6 } },
|
|
939
|
+
el('button', { type: 'button', onClick: viewFrpcLog, style: st.btn }, frpcLog ? '收起日志' : '查看 frpc 日志'),
|
|
940
|
+
el('button', { type: 'button', onClick: refresh, style: st.btn }, '刷新状态')))
|
|
941
|
+
: null),
|
|
942
|
+
|
|
257
943
|
cliHint,
|
|
258
944
|
|
|
259
945
|
error
|