dsh-plugin-mobile-gateway 0.7.7 → 0.9.0
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/PROTOCOL.md +68 -12
- package/README.md +32 -22
- package/cordis.patch.yml +4 -0
- package/docs/dsh-0.1.7-rc.1-compatibility-audit.md +52 -0
- package/docs/dsh-0.2.0-rc.2-plugin-management-mobile-integration.md +109 -0
- package/docs/multi-gateway-app-integration.md +2 -1
- package/lib/client.js +273 -18
- package/lib/cloudflare-tunnel.mjs +282 -0
- package/lib/cloudflared-binary.mjs +114 -0
- package/lib/dsh-host-adapter.mjs +47 -6
- package/lib/gateway-tools.mjs +36 -0
- package/lib/index.mjs +267 -47
- package/lib/plugin-management.mjs +194 -0
- package/lib/web-restart.mjs +47 -0
- package/package.json +3 -3
package/lib/client.js
CHANGED
|
@@ -49,6 +49,7 @@ window.__ModuleLoader__.load({
|
|
|
49
49
|
input: { width: '100%', boxSizing: 'border-box', border: `1px solid ${colors.border}`, borderRadius: 8, padding: '8px 10px', marginBottom: 10, background: 'transparent', color: 'inherit', outline: 'none' },
|
|
50
50
|
pairingText: { width: '100%', minHeight: 86, boxSizing: 'border-box', resize: 'vertical', border: `1px solid ${colors.border}`, borderRadius: 8, padding: '9px 10px', margin: '10px 0', background: 'transparent', color: 'inherit', outline: 'none', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 11, lineHeight: 1.45, overflowWrap: 'anywhere' },
|
|
51
51
|
button: { cursor: 'pointer', border: `1px solid ${colors.border}`, background: 'transparent', borderRadius: 8, padding: '7px 11px', color: 'inherit', font: 'inherit' },
|
|
52
|
+
headerButton: { width: 34, height: 34, boxSizing: 'border-box', padding: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 },
|
|
52
53
|
primary: { background: colors.accent, color: '#fff', borderColor: colors.accent, width: '100%' },
|
|
53
54
|
muted: { color: colors.muted, fontSize: 12, lineHeight: 1.55 },
|
|
54
55
|
row: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', columnGap: 12, alignItems: 'center', padding: '12px 0', borderBottom: `1px solid ${colors.border}` },
|
|
@@ -56,6 +57,10 @@ window.__ModuleLoader__.load({
|
|
|
56
57
|
dot: { width: 7, height: 7, borderRadius: 99, display: 'inline-block' },
|
|
57
58
|
error: { color: '#ff8092', background: 'rgba(220,76,100,.12)', borderRadius: 8, padding: 10, marginBottom: 12 },
|
|
58
59
|
qr: { display: 'block', width: 220, height: 220, margin: '12px auto', background: '#fff', borderRadius: 10, padding: 8 },
|
|
60
|
+
helpButton: { width: 20, height: 20, boxSizing: 'border-box', padding: 0, borderRadius: '50%', border: `1px solid ${colors.muted}`, background: 'transparent', color: colors.muted, cursor: 'help', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, fontSize: 12, fontWeight: 600, lineHeight: 1 },
|
|
61
|
+
helpBubble: { position: 'absolute', top: '100%', left: 0, zIndex: 20, width: 'min(300px, calc(100vw - 88px))', maxHeight: 'min(440px, calc(100vh - 160px))', overflowY: 'auto', boxSizing: 'border-box', padding: 12, borderRadius: 10, border: '1px solid rgba(255,255,255,.18)', background: '#20242c', color: '#f4f4f5', boxShadow: '0 12px 28px rgba(0,0,0,.35)', fontSize: 12, lineHeight: 1.5 },
|
|
62
|
+
toolsMenu: { position: 'absolute', top: 'calc(100% + 6px)', right: 0, zIndex: 30, width: 250, maxWidth: 'calc(100vw - 48px)', border: `1px solid ${colors.border}`, borderRadius: 10, padding: 5, background: '#20242c', boxShadow: '0 12px 28px rgba(0,0,0,.35)' },
|
|
63
|
+
toolsItem: { display: 'block', width: '100%', padding: '9px 10px', textAlign: 'left', border: 0, borderRadius: 7, background: 'transparent', color: colors.text, font: 'inherit', cursor: 'pointer' },
|
|
59
64
|
switch: { position: 'relative', display: 'inline-flex', width: 42, height: 24, flexShrink: 0 },
|
|
60
65
|
switchInput: { position: 'absolute', opacity: 0, width: 1, height: 1 },
|
|
61
66
|
switchTrack: { position: 'absolute', inset: 0, borderRadius: 99, transition: 'background .18s ease', cursor: 'pointer' },
|
|
@@ -75,6 +80,42 @@ window.__ModuleLoader__.load({
|
|
|
75
80
|
return `${scheme}//${window.location.host}${wsPath || '/ws/mobile'}`
|
|
76
81
|
}
|
|
77
82
|
|
|
83
|
+
function lanPairingUrl(status) {
|
|
84
|
+
const urls = status?.lan?.listening && Array.isArray(status.lan.urls) ? status.lan.urls : []
|
|
85
|
+
return urls.find((url) => !/^ws:\/\/169\.254\./.test(url)) || urls[0] || ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pairingUrlFor(route, status) {
|
|
89
|
+
const lanUrl = lanPairingUrl(status)
|
|
90
|
+
const tunnelUrl = status.cloudflare?.enabled ? status.cloudflare.publicUrl || '' : ''
|
|
91
|
+
if (route === 'lan') return lanUrl
|
|
92
|
+
if (route === 'cloudflare') return tunnelUrl
|
|
93
|
+
if (status.cloudflare?.enabled && status.cloudflare.mode === 'quick') return tunnelUrl
|
|
94
|
+
return tunnelUrl || status.publicUrl || lanUrl || inferredUrl(status.wsPath)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function connectionAdvice(status, publicSetup) {
|
|
98
|
+
if (!status) return { detected: '正在读取网关环境…', local: '', remote: '' }
|
|
99
|
+
const system = status.platform === 'darwin' ? 'macOS 电脑' : status.platform === 'win32' ? 'Windows 电脑' : status.platform === 'linux' ? 'Linux 服务器' : '当前设备'
|
|
100
|
+
const lanReady = !!lanPairingUrl(status)
|
|
101
|
+
const tunnel = status.cloudflare
|
|
102
|
+
const tunnelReady = !!(tunnel?.enabled && tunnel.state === 'online' && tunnel.publicUrl)
|
|
103
|
+
const serverReady = status.platform === 'linux' && !!(
|
|
104
|
+
(publicSetup?.configured && publicSetup.publicUrl) || /^wss:\/\//.test(status.publicUrl || ''))
|
|
105
|
+
const detected = `当前检测:${system};局域网入口${lanReady ? '已就绪' : '未就绪'};${serverReady ? '公网服务器入口已配置' : tunnelReady ? `${tunnel.mode === 'quick' ? 'Quick' : '命名'} Tunnel 已连接` : tunnel?.enabled ? 'Cloudflare Tunnel 连接中' : '外网入口未连接'}。`
|
|
106
|
+
const local = lanReady
|
|
107
|
+
? '手机与 DSH 处于可互访的同一局域网时,建议选“局域网直连”。'
|
|
108
|
+
: '如果手机和 DSH 在同一局域网,先确认局域网入口已开启并监听成功。'
|
|
109
|
+
let remote
|
|
110
|
+
if (serverReady) remote = '手机在外网时,建议使用已配置的公网服务器地址(“自动选择”)。'
|
|
111
|
+
else if (tunnelReady && tunnel.mode === 'named') remote = '手机在外网时,建议选“Cloudflare 外网”;当前命名 Tunnel 有固定域名。'
|
|
112
|
+
else if (tunnelReady) remote = '手机在外网临时使用时,建议选“Cloudflare 外网”;Quick 地址变化后需要重新确认。'
|
|
113
|
+
else if (status.platform === 'darwin' || status.platform === 'win32') remote = '手机在外网时,无自有域名可开启 Quick Tunnel;有域名且需要固定地址可配置命名 Tunnel。'
|
|
114
|
+
else if (status.platform === 'linux') remote = '手机在外网时,若使用受支持的 Ubuntu/Debian 服务器且有固定公网 IPv4,可先配置“公网接入”,再选“自动选择”。'
|
|
115
|
+
else remote = '手机在外网时,需要一个可访问的 wss:// 地址;可在“手动输入地址”中填写。'
|
|
116
|
+
return { detected, local, remote }
|
|
117
|
+
}
|
|
118
|
+
|
|
78
119
|
function DeviceRow({ device, onRevoke, revoking }) {
|
|
79
120
|
const triggerRevoke = (event) => {
|
|
80
121
|
event.preventDefault()
|
|
@@ -116,9 +157,19 @@ window.__ModuleLoader__.load({
|
|
|
116
157
|
const [devices, setDevices] = React.useState(null)
|
|
117
158
|
const [status, setStatus] = React.useState(null)
|
|
118
159
|
const [publicUrl, setPublicUrl] = React.useState('')
|
|
160
|
+
const [pairingRoute, setPairingRoute] = React.useState('auto')
|
|
161
|
+
const [connectionHelpOpen, setConnectionHelpOpen] = React.useState(false)
|
|
162
|
+
const [toolsOpen, setToolsOpen] = React.useState(false)
|
|
163
|
+
const [toolBusy, setToolBusy] = React.useState(false)
|
|
164
|
+
const [toolNotice, setToolNotice] = React.useState('')
|
|
165
|
+
const stoppedRef = React.useRef(false)
|
|
119
166
|
const [publicIp, setPublicIp] = React.useState('')
|
|
120
167
|
const [publicSetup, setPublicSetup] = React.useState(null)
|
|
121
168
|
const [publicSetupBusy, setPublicSetupBusy] = React.useState(false)
|
|
169
|
+
const [cloudflareHost, setCloudflareHost] = React.useState('')
|
|
170
|
+
const [cloudflareToken, setCloudflareToken] = React.useState('')
|
|
171
|
+
const [cloudflareMode, setCloudflareMode] = React.useState('quick')
|
|
172
|
+
const [cloudflareBusy, setCloudflareBusy] = React.useState(false)
|
|
122
173
|
const [deviceName, setDeviceName] = React.useState('iPhone')
|
|
123
174
|
const [qr, setQr] = React.useState(null)
|
|
124
175
|
const [error, setError] = React.useState(null)
|
|
@@ -131,6 +182,8 @@ window.__ModuleLoader__.load({
|
|
|
131
182
|
const [revokingIds, setRevokingIds] = React.useState(() => new Set())
|
|
132
183
|
const revokingIdsRef = React.useRef(new Set())
|
|
133
184
|
const automaticUrlRef = React.useRef('')
|
|
185
|
+
const pairingRouteRef = React.useRef('auto')
|
|
186
|
+
const cloudflareModeLoadedRef = React.useRef(false)
|
|
134
187
|
|
|
135
188
|
const refresh = React.useCallback(async () => {
|
|
136
189
|
try {
|
|
@@ -146,15 +199,23 @@ window.__ModuleLoader__.load({
|
|
|
146
199
|
setDevices((current) => JSON.stringify(current) === JSON.stringify(nextDevices) ? current : nextDevices)
|
|
147
200
|
}
|
|
148
201
|
setStatus((current) => JSON.stringify(current) === JSON.stringify(statusData) ? current : statusData)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
202
|
+
if (statusData.cloudflare && statusData.cloudflare.hostname) {
|
|
203
|
+
setCloudflareHost((current) => current || statusData.cloudflare.hostname)
|
|
204
|
+
}
|
|
205
|
+
if (statusData.cloudflare && !cloudflareModeLoadedRef.current) {
|
|
206
|
+
cloudflareModeLoadedRef.current = true
|
|
207
|
+
setCloudflareMode(statusData.cloudflare.mode || 'quick')
|
|
208
|
+
}
|
|
209
|
+
if (pairingRouteRef.current === 'cloudflare' && !statusData.cloudflare?.enabled) {
|
|
210
|
+
pairingRouteRef.current = 'auto'
|
|
211
|
+
setPairingRoute('auto')
|
|
212
|
+
}
|
|
213
|
+
if (pairingRouteRef.current !== 'custom') {
|
|
214
|
+
const preferredUrl = pairingUrlFor(pairingRouteRef.current, statusData)
|
|
215
|
+
if (automaticUrlRef.current && preferredUrl !== automaticUrlRef.current) setQr(null)
|
|
216
|
+
automaticUrlRef.current = preferredUrl
|
|
217
|
+
setPublicUrl(preferredUrl)
|
|
218
|
+
}
|
|
158
219
|
return true
|
|
159
220
|
} catch (cause) {
|
|
160
221
|
setError(cause.message)
|
|
@@ -175,6 +236,43 @@ window.__ModuleLoader__.load({
|
|
|
175
236
|
}
|
|
176
237
|
}
|
|
177
238
|
|
|
239
|
+
const runTool = async (tool) => {
|
|
240
|
+
if (toolBusy) return
|
|
241
|
+
setToolsOpen(false)
|
|
242
|
+
setToolBusy(true)
|
|
243
|
+
setToolNotice('')
|
|
244
|
+
setError(null)
|
|
245
|
+
try {
|
|
246
|
+
const oldPid = status?.webPid
|
|
247
|
+
await request(`/mgw/tools/${tool}`, { method: 'POST' })
|
|
248
|
+
if (tool === 'stop-web') {
|
|
249
|
+
stoppedRef.current = true
|
|
250
|
+
setToolNotice('DSH Web 已收到停止指令。需要时可在 Terminal 运行 dsh web 重新启动。')
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
if (tool === 'restart-web') {
|
|
254
|
+
setToolNotice('正在重启 DSH Web,当前连接会短暂中断…')
|
|
255
|
+
const deadline = Date.now() + 60_000
|
|
256
|
+
while (Date.now() < deadline) {
|
|
257
|
+
await new Promise((resolve) => window.setTimeout(resolve, 700))
|
|
258
|
+
try {
|
|
259
|
+
const next = await request('/mgw/status')
|
|
260
|
+
if (next.webPid && next.webPid !== oldPid) {
|
|
261
|
+
window.location.reload()
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
} catch { /* old process is shutting down */ }
|
|
265
|
+
}
|
|
266
|
+
throw new Error('DSH Web 重启超时,请检查 /tmp/mobile-gateway.log')
|
|
267
|
+
}
|
|
268
|
+
} catch (cause) {
|
|
269
|
+
setError(cause.message)
|
|
270
|
+
setToolNotice('')
|
|
271
|
+
} finally {
|
|
272
|
+
setToolBusy(false)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
178
276
|
React.useEffect(() => {
|
|
179
277
|
let active = true
|
|
180
278
|
refresh()
|
|
@@ -186,10 +284,25 @@ window.__ModuleLoader__.load({
|
|
|
186
284
|
if (match) setPublicIp(match[1])
|
|
187
285
|
}
|
|
188
286
|
}).catch((cause) => active && setPublicSetup({ installed: false, configured: false, error: cause.message }))
|
|
189
|
-
const timer = window.setInterval(refresh, 3000)
|
|
287
|
+
const timer = window.setInterval(() => { if (!stoppedRef.current) refresh() }, 3000)
|
|
190
288
|
return () => { active = false; window.clearInterval(timer) }
|
|
191
289
|
}, [refresh])
|
|
192
290
|
|
|
291
|
+
const choosePairingRoute = (route) => {
|
|
292
|
+
pairingRouteRef.current = route
|
|
293
|
+
setPairingRoute(route)
|
|
294
|
+
setQr(null)
|
|
295
|
+
if (route === 'custom') {
|
|
296
|
+
automaticUrlRef.current = ''
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
if (status) {
|
|
300
|
+
const nextUrl = pairingUrlFor(route, status)
|
|
301
|
+
automaticUrlRef.current = nextUrl
|
|
302
|
+
setPublicUrl(nextUrl)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
193
306
|
const configurePublicAccess = async () => {
|
|
194
307
|
if (publicSetupBusy) return
|
|
195
308
|
setPublicSetupBusy(true)
|
|
@@ -201,7 +314,7 @@ window.__ModuleLoader__.load({
|
|
|
201
314
|
body: JSON.stringify({ publicIp }),
|
|
202
315
|
})
|
|
203
316
|
setPublicSetup(data)
|
|
204
|
-
if (data.publicUrl) {
|
|
317
|
+
if (data.publicUrl && pairingRouteRef.current === 'auto') {
|
|
205
318
|
automaticUrlRef.current = data.publicUrl
|
|
206
319
|
setPublicUrl(data.publicUrl)
|
|
207
320
|
}
|
|
@@ -212,6 +325,49 @@ window.__ModuleLoader__.load({
|
|
|
212
325
|
}
|
|
213
326
|
}
|
|
214
327
|
|
|
328
|
+
const configureCloudflare = async (enabled) => {
|
|
329
|
+
if (cloudflareBusy) return
|
|
330
|
+
setCloudflareBusy(true)
|
|
331
|
+
setError(null)
|
|
332
|
+
try {
|
|
333
|
+
const data = await request('/mgw/cloudflare', {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
headers: { 'Content-Type': 'application/json' },
|
|
336
|
+
body: JSON.stringify({ enabled, ...(enabled ? {
|
|
337
|
+
mode: cloudflareMode,
|
|
338
|
+
...(cloudflareMode === 'named' ? { hostname: cloudflareHost, token: cloudflareToken } : {}),
|
|
339
|
+
} : {}) }),
|
|
340
|
+
})
|
|
341
|
+
setCloudflareToken('')
|
|
342
|
+
setStatus((current) => ({ ...(current || {}), cloudflare: data, gatewayEnabled: data.gatewayEnabled, gatewayMode: data.gatewayMode, publicUrl: data.publicUrl || null }))
|
|
343
|
+
if (!enabled && pairingRouteRef.current === 'cloudflare') {
|
|
344
|
+
pairingRouteRef.current = 'auto'
|
|
345
|
+
setPairingRoute('auto')
|
|
346
|
+
}
|
|
347
|
+
await refresh()
|
|
348
|
+
} catch (cause) {
|
|
349
|
+
setError(cause.message)
|
|
350
|
+
} finally {
|
|
351
|
+
setCloudflareBusy(false)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const restartCloudflare = async () => {
|
|
356
|
+
if (cloudflareBusy) return
|
|
357
|
+
setCloudflareBusy(true)
|
|
358
|
+
setError(null)
|
|
359
|
+
try {
|
|
360
|
+
const data = await request('/mgw/cloudflare/restart', { method: 'POST' })
|
|
361
|
+
if (data.mode === 'quick') setQr(null)
|
|
362
|
+
setStatus((current) => ({ ...(current || {}), cloudflare: data }))
|
|
363
|
+
await refresh()
|
|
364
|
+
} catch (cause) {
|
|
365
|
+
setError(cause.message)
|
|
366
|
+
} finally {
|
|
367
|
+
setCloudflareBusy(false)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
215
371
|
const createPairing = async (endpoint) => request('/mgw/pair', {
|
|
216
372
|
method: 'POST',
|
|
217
373
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -312,13 +468,41 @@ window.__ModuleLoader__.load({
|
|
|
312
468
|
}
|
|
313
469
|
}
|
|
314
470
|
|
|
471
|
+
const advice = connectionAdvice(status, publicSetup)
|
|
472
|
+
const helpLine = (name, description) => React.createElement('div', { style: { marginTop: 7 } },
|
|
473
|
+
React.createElement('strong', null, name), ':', description)
|
|
474
|
+
|
|
315
475
|
return React.createElement('div', { style: styles.backdrop, onMouseDown: () => setOpen(false) },
|
|
316
|
-
React.createElement('aside', { style: styles.panel, onMouseDown: (event) => event.stopPropagation(), role: 'dialog', 'aria-modal': true, 'aria-label': '移动设备管理' },
|
|
476
|
+
React.createElement('aside', { style: styles.panel, onMouseDown: (event) => { event.stopPropagation(); setToolsOpen(false) }, role: 'dialog', 'aria-modal': true, 'aria-label': '移动设备管理' },
|
|
317
477
|
React.createElement('div', { style: styles.header },
|
|
318
478
|
React.createElement('div', null,
|
|
319
479
|
React.createElement('h2', { style: styles.title }, '移动设备'),
|
|
320
480
|
React.createElement('div', { style: styles.muted }, status && status.gatewayEnabled ? '移动网关已开启 · 设备鉴权生效中' : '移动网关已关闭 · 普通 WebUI 模式')),
|
|
321
|
-
React.createElement('
|
|
481
|
+
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
482
|
+
React.createElement('div', { style: { position: 'relative' }, onMouseDown: (event) => event.stopPropagation() },
|
|
483
|
+
React.createElement('button', {
|
|
484
|
+
type: 'button', style: { ...styles.button, ...styles.headerButton, borderRadius: '50%' }, title: '工具', 'aria-label': '工具',
|
|
485
|
+
'aria-haspopup': 'menu', 'aria-expanded': toolsOpen,
|
|
486
|
+
onClick: () => setToolsOpen((value) => !value),
|
|
487
|
+
}, React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
488
|
+
React.createElement('path', { d: 'M10.3 2.8h3.4l.5 2.2a7.6 7.6 0 0 1 1.7.7l1.9-1.2 2.4 2.4L19 8.8c.3.5.5 1.1.7 1.7l2.2.5v3.4l-2.2.5a7.6 7.6 0 0 1-.7 1.7l1.2 1.9-2.4 2.4-1.9-1.2a7.6 7.6 0 0 1-1.7.7l-.5 2.2h-3.4l-.5-2.2a7.6 7.6 0 0 1-1.7-.7l-1.9 1.2-2.4-2.4L5 16.6a7.6 7.6 0 0 1-.7-1.7l-2.2-.5V11l2.2-.5A7.6 7.6 0 0 1 5 8.8L3.8 6.9l2.4-2.4 1.9 1.2a7.6 7.6 0 0 1 1.7-.7l.5-2.2Z' }),
|
|
489
|
+
React.createElement('circle', { cx: 12, cy: 12.7, r: 3.1 }))),
|
|
490
|
+
toolsOpen ? React.createElement('div', { style: styles.toolsMenu, role: 'menu', 'aria-label': '网关工具' },
|
|
491
|
+
React.createElement('button', {
|
|
492
|
+
type: 'button', role: 'menuitem', disabled: toolBusy || stoppedRef.current || !status?.tools?.restartWeb,
|
|
493
|
+
style: { ...styles.toolsItem, opacity: status?.tools?.restartWeb ? 1 : .5 },
|
|
494
|
+
onClick: () => runTool('restart-web'),
|
|
495
|
+
}, '重启当前网关',
|
|
496
|
+
React.createElement('span', { style: { ...styles.muted, display: 'block', marginTop: 2 } }, '结束当前进程并重新启动 DSH Web')),
|
|
497
|
+
React.createElement('button', {
|
|
498
|
+
type: 'button', role: 'menuitem', disabled: toolBusy || stoppedRef.current || !status?.tools?.stopWeb,
|
|
499
|
+
style: { ...styles.toolsItem, color: colors.danger, opacity: status?.tools?.stopWeb ? 1 : .5 },
|
|
500
|
+
onClick: () => runTool('stop-web'),
|
|
501
|
+
}, '停止 DSH Web',
|
|
502
|
+
React.createElement('span', { style: { ...styles.muted, display: 'block', marginTop: 2 } }, '结束后台进程,所有连接将断开')))
|
|
503
|
+
: null),
|
|
504
|
+
React.createElement('button', { style: { ...styles.button, ...styles.headerButton }, onClick: () => setOpen(false), 'aria-label': '关闭' }, '✕'))),
|
|
505
|
+
toolNotice ? React.createElement('div', { style: { ...styles.muted, marginBottom: 10 } }, toolNotice) : null,
|
|
322
506
|
error ? React.createElement('div', { style: styles.error }, error) : null,
|
|
323
507
|
React.createElement('section', { style: styles.card },
|
|
324
508
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 14, alignItems: 'center' } },
|
|
@@ -373,7 +557,38 @@ window.__ModuleLoader__.load({
|
|
|
373
557
|
status && !status.requireAuth
|
|
374
558
|
? React.createElement('p', { style: { ...styles.error, margin: '12px 0 0' } }, 'Debug 模式:本机 DSH 入口将跳过设备凭证校验;独立局域网入口仍强制鉴权。')
|
|
375
559
|
: null),
|
|
376
|
-
|
|
560
|
+
status && status.cloudflare && status.cloudflare.supported
|
|
561
|
+
? React.createElement('section', { style: styles.card },
|
|
562
|
+
React.createElement('strong', null, 'Cloudflare Tunnel · PC 外网接入'),
|
|
563
|
+
React.createElement('p', { style: styles.muted }, '首次开启时自动下载并校验 cloudflared;公网入口只接受已鉴权的移动 WebSocket。'),
|
|
564
|
+
React.createElement('label', { style: styles.label }, '接入方式'),
|
|
565
|
+
React.createElement('select', { style: styles.input, value: cloudflareMode, onChange: (event) => setCloudflareMode(event.target.value), disabled: cloudflareBusy },
|
|
566
|
+
React.createElement('option', { value: 'quick' }, 'Quick Tunnel · 无需域名'),
|
|
567
|
+
React.createElement('option', { value: 'named' }, '命名 Tunnel · 固定域名')),
|
|
568
|
+
cloudflareMode === 'quick'
|
|
569
|
+
? React.createElement('p', { style: styles.muted }, 'Cloudflare 会分配随机地址;DSH 重启或隧道重连后地址可能改变,手机需重新扫码确认新地址。适合临时使用。')
|
|
570
|
+
: React.createElement(React.Fragment, null,
|
|
571
|
+
React.createElement('p', { style: styles.muted }, '先在 Cloudflare 创建命名 Tunnel,把公开域名的 Service URL 设为 ', React.createElement('code', null, `http://127.0.0.1:${status.cloudflare.port}`), '。'),
|
|
572
|
+
React.createElement('label', { style: styles.label }, '公开域名'),
|
|
573
|
+
React.createElement('input', { style: styles.input, value: cloudflareHost, onChange: (event) => setCloudflareHost(event.target.value.trim()), placeholder: 'gateway.example.com', spellCheck: false }),
|
|
574
|
+
React.createElement('label', { style: styles.label }, 'Tunnel Token'),
|
|
575
|
+
React.createElement('input', { style: styles.input, type: 'password', value: cloudflareToken, onChange: (event) => setCloudflareToken(event.target.value.trim()), placeholder: status.cloudflare.configured ? '已保存;留空则保持原 Token' : '粘贴 eyJ… Token', autoComplete: 'off', spellCheck: false })),
|
|
576
|
+
React.createElement('button', {
|
|
577
|
+
style: { ...styles.button, ...styles.primary, opacity: cloudflareBusy || (cloudflareMode === 'named' && (!cloudflareHost || (!cloudflareToken && !status.cloudflare.configured))) ? .65 : 1 },
|
|
578
|
+
disabled: cloudflareBusy || (cloudflareMode === 'named' && (!cloudflareHost || (!cloudflareToken && !status.cloudflare.configured))),
|
|
579
|
+
onClick: () => configureCloudflare(true),
|
|
580
|
+
}, cloudflareBusy ? '正在处理…' : status.cloudflare.enabled && cloudflareMode === status.cloudflare.mode ? '更新 Tunnel 配置' : cloudflareMode === 'quick' ? '一键开启 Quick Tunnel' : '开启命名 Tunnel'),
|
|
581
|
+
status.cloudflare.enabled
|
|
582
|
+
? React.createElement(React.Fragment, null,
|
|
583
|
+
React.createElement('button', { style: { ...styles.button, width: '100%', marginTop: 8 }, disabled: cloudflareBusy || !status.gatewayEnabled, onClick: restartCloudflare }, '重启 Tunnel'),
|
|
584
|
+
React.createElement('button', { style: { ...styles.button, width: '100%', marginTop: 8 }, disabled: cloudflareBusy, onClick: () => configureCloudflare(false) }, '关闭 Cloudflare Tunnel'),
|
|
585
|
+
React.createElement('p', { style: styles.muted }, '重启会短暂断开手机连接;Quick Tunnel 可能获得新地址,需要重新扫码。'))
|
|
586
|
+
: null,
|
|
587
|
+
React.createElement('div', { style: { ...styles.muted, marginTop: 9, overflowWrap: 'anywhere', color: status.cloudflare.error ? colors.danger : colors.muted } },
|
|
588
|
+
status.cloudflare.error || ({ disabled: '未开启', starting: '正在启动本机入口…', preparing: '正在准备 cloudflared(首次需下载)…', connecting: '正在连接 Cloudflare…', online: '已连接 Cloudflare', 'waiting-for-gateway': '等待移动网关开启' }[status.cloudflare.state] || status.cloudflare.state),
|
|
589
|
+
status.cloudflare.publicUrl ? ` · ${status.cloudflare.publicUrl}` : ''))
|
|
590
|
+
: null,
|
|
591
|
+
status && status.platform === 'linux' ? React.createElement('section', { style: styles.card },
|
|
377
592
|
React.createElement('strong', null, '公网接入'),
|
|
378
593
|
publicSetup === null
|
|
379
594
|
? React.createElement('p', { style: styles.muted }, '正在检查系统组件…')
|
|
@@ -403,10 +618,46 @@ window.__ModuleLoader__.load({
|
|
|
403
618
|
: null)
|
|
404
619
|
: React.createElement(React.Fragment, null,
|
|
405
620
|
React.createElement('p', { style: { ...styles.muted, marginBottom: 8 } }, '尚未安装系统 Helper。请在服务器执行一次初始化:'),
|
|
406
|
-
React.createElement('code', { style: { display: 'block', fontSize: 10, lineHeight: 1.5, overflowWrap: 'anywhere', userSelect: 'all' } }, 'npx --yes dsh-plugin-mobile-gateway@latest init'))),
|
|
621
|
+
React.createElement('code', { style: { display: 'block', fontSize: 10, lineHeight: 1.5, overflowWrap: 'anywhere', userSelect: 'all' } }, 'npx --yes dsh-plugin-mobile-gateway@latest init'))) : null,
|
|
407
622
|
React.createElement('section', { style: styles.card },
|
|
623
|
+
React.createElement('div', { style: { position: 'relative', display: 'flex', alignItems: 'center', gap: 6, marginBottom: 6 }, onMouseLeave: () => setConnectionHelpOpen(false) },
|
|
624
|
+
React.createElement('span', { style: { ...styles.label, marginBottom: 0 } }, '配对连接方式'),
|
|
625
|
+
React.createElement('button', {
|
|
626
|
+
type: 'button',
|
|
627
|
+
style: styles.helpButton,
|
|
628
|
+
'aria-label': '连接方式说明与场景建议',
|
|
629
|
+
'aria-expanded': connectionHelpOpen,
|
|
630
|
+
'aria-describedby': connectionHelpOpen ? 'mgw-route-help' : undefined,
|
|
631
|
+
onMouseEnter: () => setConnectionHelpOpen(true),
|
|
632
|
+
onFocus: () => setConnectionHelpOpen(true),
|
|
633
|
+
onBlur: () => setConnectionHelpOpen(false),
|
|
634
|
+
onClick: () => setConnectionHelpOpen(true),
|
|
635
|
+
onKeyDown: (event) => { if (event.key === 'Escape') setConnectionHelpOpen(false) },
|
|
636
|
+
}, '?'),
|
|
637
|
+
connectionHelpOpen ? React.createElement('div', { id: 'mgw-route-help', role: 'tooltip', style: styles.helpBubble },
|
|
638
|
+
React.createElement('strong', null, '怎么选连接方式'),
|
|
639
|
+
helpLine('局域网直连', '手机与 DSH 在同一个可互访的局域网,使用 ws:// 私有地址;离开该网络就无法直连。'),
|
|
640
|
+
helpLine('Cloudflare Channel(命名 Tunnel)', 'PC 主动连接 Cloudflare,外网用固定域名的 wss://;需要 Cloudflare 账户和域名。'),
|
|
641
|
+
helpLine('Quick Channel(Quick Tunnel)', '无需账户或域名,Cloudflare 分配临时 wss:// 地址;隧道重建后地址可能变化。'),
|
|
642
|
+
helpLine('公网服务器连接', 'DSH 运行在有固定公网 IPv4 的 Linux 服务器上,配置公网入口后用 wss:// 公网 IP 连接。'),
|
|
643
|
+
React.createElement('div', { style: { borderTop: '1px solid rgba(255,255,255,.18)', marginTop: 10, paddingTop: 8 } },
|
|
644
|
+
React.createElement('strong', null, '结合当前环境'),
|
|
645
|
+
React.createElement('div', { style: { color: '#c6cbd4', marginTop: 4 } }, advice.detected),
|
|
646
|
+
React.createElement('div', { style: { marginTop: 6 } }, advice.local),
|
|
647
|
+
React.createElement('div', { style: { marginTop: 6 } }, advice.remote),
|
|
648
|
+
React.createElement('div', { style: { color: '#aeb6c3', marginTop: 6 } }, '面板无法判断手机当前网络;请按手机所在位置选择,提示不会替你修改选择。')))
|
|
649
|
+
: null),
|
|
650
|
+
React.createElement('select', { style: styles.input, value: pairingRoute, onChange: (event) => choosePairingRoute(event.target.value), disabled: !status, 'aria-label': '配对连接方式' },
|
|
651
|
+
React.createElement('option', { value: 'auto' }, '自动选择 · 优先外网'),
|
|
652
|
+
lanPairingUrl(status) || pairingRoute === 'lan'
|
|
653
|
+
? React.createElement('option', { value: 'lan', disabled: !lanPairingUrl(status) }, lanPairingUrl(status)
|
|
654
|
+
? '局域网直连 · ' + lanPairingUrl(status).replace(/^wss?:\/\//, '').split('/')[0]
|
|
655
|
+
: '局域网直连 · 当前不可用')
|
|
656
|
+
: null,
|
|
657
|
+
status?.cloudflare?.enabled ? React.createElement('option', { value: 'cloudflare' }, 'Cloudflare 外网') : null,
|
|
658
|
+
React.createElement('option', { value: 'custom' }, '手动输入地址')),
|
|
408
659
|
React.createElement('label', { style: styles.label }, 'WebSocket 地址'),
|
|
409
|
-
React.createElement('input', { style: styles.input, value: publicUrl, onChange: (event) => {
|
|
660
|
+
React.createElement('input', { style: styles.input, value: publicUrl, onChange: (event) => { choosePairingRoute('custom'); setPublicUrl(event.target.value) }, placeholder: 'ws://192.168.1.10:3081/ws/mobile', spellCheck: false }),
|
|
410
661
|
status && status.lan && status.lan.enabled
|
|
411
662
|
? React.createElement('div', { style: { ...styles.muted, margin: '-7px 0 10px', color: status.lan.error ? colors.danger : colors.muted } },
|
|
412
663
|
status.lan.error
|
|
@@ -417,8 +668,12 @@ window.__ModuleLoader__.load({
|
|
|
417
668
|
: null,
|
|
418
669
|
React.createElement('label', { style: styles.label }, '设备名称'),
|
|
419
670
|
React.createElement('input', { style: styles.input, value: deviceName, onChange: (event) => setDeviceName(event.target.value), maxLength: 80, placeholder: 'iPhone' }),
|
|
420
|
-
React.createElement('button', {
|
|
421
|
-
|
|
671
|
+
React.createElement('button', {
|
|
672
|
+
style: { ...styles.button, ...styles.primary, opacity: busy || !(status && status.gatewayEnabled) || !publicUrl.trim() ? .65 : 1 },
|
|
673
|
+
disabled: busy || !(status && status.gatewayEnabled) || !publicUrl.trim(),
|
|
674
|
+
onClick: pair,
|
|
675
|
+
}, busy ? '正在生成…' : !(status && status.gatewayEnabled) ? '请先开启移动网关' : !publicUrl.trim() ? '等待连接地址' : '生成配对二维码'),
|
|
676
|
+
React.createElement('p', { style: { ...styles.muted, margin: '10px 0 0' } }, '同一局域网选择“局域网直连”,无需关闭 Cloudflare Tunnel。局域网使用私有地址的 ws://;公网地址必须使用 wss://。配对码只能使用一次,并在 5 分钟内过期。')),
|
|
422
677
|
qr ? React.createElement('section', { style: { ...styles.card, textAlign: 'center' } },
|
|
423
678
|
React.createElement('strong', null, '使用 iOS 客户端扫码'),
|
|
424
679
|
React.createElement('img', { style: styles.qr, src: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(qr.svg)}`, alt: '一次性设备配对二维码' }),
|