@sparkelf/dsh-plugin-mobile-gateway 0.8.1 → 0.8.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/lib/index.mjs +8 -0
- package/package.json +1 -1
- package/lib/client.js.pre-idfix-20260926-054024 +0 -520
package/lib/index.mjs
CHANGED
|
@@ -3065,6 +3065,14 @@ const plugin = {
|
|
|
3065
3065
|
}
|
|
3066
3066
|
|
|
3067
3067
|
const installSessionQueueBaseline = (value) => {
|
|
3068
|
+
// DSH's SessionControlBaseline carries projections only; the queue snapshot is a gateway
|
|
3069
|
+
// extension some builds omit entirely. Treating its absence as an error aborted the whole
|
|
3070
|
+
// control stream on every generation, so a build without the field could never open a Session.
|
|
3071
|
+
if (value === void 0) {
|
|
3072
|
+
sessionQueues = new Map()
|
|
3073
|
+
broadcastSessionMetadataFrame({ kind: 'session-queues', queues: {} })
|
|
3074
|
+
return
|
|
3075
|
+
}
|
|
3068
3076
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
3069
3077
|
throw new Error('session/control baseline returned invalid queues')
|
|
3070
3078
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sparkelf/dsh-plugin-mobile-gateway",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Mobile gateway for DSH 0.1.7 (Session format 4), carrying the fork's format adaptation: v3 rows are lifted on read so a Session written by an earlier release still opens. Per-session Agent preset selection, live preset updates, and the upstream LAN/pairing protocol are unchanged.",
|
|
5
5
|
"main": "lib/index.mjs",
|
|
6
6
|
"files": [
|
|
@@ -1,520 +0,0 @@
|
|
|
1
|
-
// Browser half of dsh-plugin-mobile-gateway. The package manifest makes this
|
|
2
|
-
// bundle part of DSH's client-module graph; it contributes one sidebar footer
|
|
3
|
-
// action and one shell overlay without replacing shipped UI seats.
|
|
4
|
-
window.__ModuleLoader__.load({
|
|
5
|
-
id: 'dsh-plugin-mobile-gateway',
|
|
6
|
-
factory: (require) => {
|
|
7
|
-
const React = require('react')
|
|
8
|
-
const module = { exports: {} }
|
|
9
|
-
const exports = module.exports
|
|
10
|
-
|
|
11
|
-
let open = false
|
|
12
|
-
const listeners = new Set()
|
|
13
|
-
const setOpen = (value) => {
|
|
14
|
-
open = value
|
|
15
|
-
for (const listener of listeners) listener()
|
|
16
|
-
}
|
|
17
|
-
const subscribe = (listener) => {
|
|
18
|
-
listeners.add(listener)
|
|
19
|
-
return () => listeners.delete(listener)
|
|
20
|
-
}
|
|
21
|
-
const getOpen = () => open
|
|
22
|
-
|
|
23
|
-
const colors = {
|
|
24
|
-
panel: 'var(--color-bg, #111318)',
|
|
25
|
-
card: 'var(--color-bg-elevated, rgba(127,127,127,.08))',
|
|
26
|
-
text: 'var(--color-text, #f4f4f5)',
|
|
27
|
-
muted: 'var(--color-text-muted, #9297a2)',
|
|
28
|
-
border: 'var(--color-border, rgba(127,127,127,.24))',
|
|
29
|
-
accent: 'var(--color-primary, #4f7cff)',
|
|
30
|
-
danger: '#dc4c64',
|
|
31
|
-
success: '#2fb171',
|
|
32
|
-
}
|
|
33
|
-
const styles = {
|
|
34
|
-
backdrop: {
|
|
35
|
-
position: 'fixed', inset: 0, zIndex: 1200, pointerEvents: 'auto',
|
|
36
|
-
background: 'rgba(0,0,0,.32)', display: 'flex', justifyContent: 'flex-end',
|
|
37
|
-
},
|
|
38
|
-
panel: {
|
|
39
|
-
width: 380, maxWidth: 'calc(100vw - 24px)', height: '100%', overflowY: 'auto',
|
|
40
|
-
display: 'flex', flexDirection: 'column',
|
|
41
|
-
background: colors.panel, color: colors.text, borderLeft: `1px solid ${colors.border}`,
|
|
42
|
-
boxShadow: '-12px 0 40px rgba(0,0,0,.22)', padding: 20,
|
|
43
|
-
fontFamily: 'system-ui, sans-serif', fontSize: 13, boxSizing: 'border-box',
|
|
44
|
-
},
|
|
45
|
-
header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 18, flexShrink: 0 },
|
|
46
|
-
title: { margin: 0, fontSize: 18, fontWeight: 650 },
|
|
47
|
-
card: { background: colors.card, border: `1px solid ${colors.border}`, borderRadius: 12, padding: 14, marginBottom: 12, flexShrink: 0 },
|
|
48
|
-
label: { display: 'block', color: colors.muted, fontSize: 12, marginBottom: 6 },
|
|
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
|
-
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
|
-
button: { cursor: 'pointer', border: `1px solid ${colors.border}`, background: 'transparent', borderRadius: 8, padding: '7px 11px', color: 'inherit', font: 'inherit' },
|
|
52
|
-
primary: { background: colors.accent, color: '#fff', borderColor: colors.accent, width: '100%' },
|
|
53
|
-
muted: { color: colors.muted, fontSize: 12, lineHeight: 1.55 },
|
|
54
|
-
row: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) auto', columnGap: 12, alignItems: 'center', padding: '12px 0', borderBottom: `1px solid ${colors.border}` },
|
|
55
|
-
badge: { display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11, marginLeft: 8, color: colors.muted },
|
|
56
|
-
dot: { width: 7, height: 7, borderRadius: 99, display: 'inline-block' },
|
|
57
|
-
error: { color: '#ff8092', background: 'rgba(220,76,100,.12)', borderRadius: 8, padding: 10, marginBottom: 12 },
|
|
58
|
-
qr: { display: 'block', width: 220, height: 220, margin: '12px auto', background: '#fff', borderRadius: 10, padding: 8 },
|
|
59
|
-
switch: { position: 'relative', display: 'inline-flex', width: 42, height: 24, flexShrink: 0 },
|
|
60
|
-
switchInput: { position: 'absolute', opacity: 0, width: 1, height: 1 },
|
|
61
|
-
switchTrack: { position: 'absolute', inset: 0, borderRadius: 99, transition: 'background .18s ease', cursor: 'pointer' },
|
|
62
|
-
switchKnob: { position: 'absolute', top: 3, width: 18, height: 18, borderRadius: 99, background: '#fff', boxShadow: '0 1px 3px rgba(0,0,0,.35)', transition: 'left .18s ease', pointerEvents: 'none' },
|
|
63
|
-
version: { flexShrink: 0, marginTop: 'auto', padding: '18px 2px 0', color: colors.muted, fontSize: 11, lineHeight: 1.4, textAlign: 'right', letterSpacing: '.02em' },
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function request(path, options) {
|
|
67
|
-
const response = await fetch(path, { credentials: 'same-origin', ...options })
|
|
68
|
-
const body = await response.json().catch(() => ({}))
|
|
69
|
-
if (!response.ok) throw new Error(body.message || body.error || `HTTP ${response.status}`)
|
|
70
|
-
return body
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function inferredUrl(wsPath) {
|
|
74
|
-
const scheme = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
75
|
-
return `${scheme}//${window.location.host}${wsPath || '/ws/mobile'}`
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function DeviceRow({ device, onRevoke, revoking }) {
|
|
79
|
-
const triggerRevoke = (event) => {
|
|
80
|
-
event.preventDefault()
|
|
81
|
-
event.stopPropagation()
|
|
82
|
-
if (!revoking) onRevoke(device)
|
|
83
|
-
}
|
|
84
|
-
return React.createElement('div', { style: styles.row },
|
|
85
|
-
React.createElement('div', { style: { minWidth: 0, overflow: 'hidden' } },
|
|
86
|
-
React.createElement('div', { style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
|
|
87
|
-
device.name,
|
|
88
|
-
React.createElement('span', { style: styles.badge },
|
|
89
|
-
React.createElement('span', { style: { ...styles.dot, background: device.online ? colors.success : colors.muted } }),
|
|
90
|
-
device.online ? `在线${device.connections > 1 ? ` · ${device.connections} 个连接` : ''}` : '离线')),
|
|
91
|
-
React.createElement('div', { style: { ...styles.muted, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginTop: 3 } },
|
|
92
|
-
device.lastSeenAt ? `最近连接:${new Date(device.lastSeenAt).toLocaleString()}` : '尚未连接')),
|
|
93
|
-
React.createElement('button', {
|
|
94
|
-
type: 'button',
|
|
95
|
-
disabled: revoking,
|
|
96
|
-
style: {
|
|
97
|
-
...styles.button,
|
|
98
|
-
color: colors.danger,
|
|
99
|
-
background: 'transparent',
|
|
100
|
-
borderColor: 'rgba(220,76,100,.55)',
|
|
101
|
-
flexShrink: 0,
|
|
102
|
-
minWidth: 54,
|
|
103
|
-
opacity: revoking ? .6 : 1,
|
|
104
|
-
},
|
|
105
|
-
title: `吊销 ${device.name}`,
|
|
106
|
-
// Pointer-up is more reliable than click inside the shell overlay on
|
|
107
|
-
// touch devices. The click handler remains for keyboard activation.
|
|
108
|
-
onPointerUp: triggerRevoke,
|
|
109
|
-
onClick: (event) => {
|
|
110
|
-
if (event.detail === 0) triggerRevoke(event)
|
|
111
|
-
},
|
|
112
|
-
}, revoking ? '吊销中…' : '吊销'))
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function DevicePanel() {
|
|
116
|
-
const [devices, setDevices] = React.useState(null)
|
|
117
|
-
const [status, setStatus] = React.useState(null)
|
|
118
|
-
const [publicUrl, setPublicUrl] = React.useState('')
|
|
119
|
-
const [publicIp, setPublicIp] = React.useState('')
|
|
120
|
-
const [publicSetup, setPublicSetup] = React.useState(null)
|
|
121
|
-
const [publicSetupBusy, setPublicSetupBusy] = React.useState(false)
|
|
122
|
-
const [deviceName, setDeviceName] = React.useState('iPhone')
|
|
123
|
-
const [qr, setQr] = React.useState(null)
|
|
124
|
-
const [error, setError] = React.useState(null)
|
|
125
|
-
const [busy, setBusy] = React.useState(false)
|
|
126
|
-
const [refreshing, setRefreshing] = React.useState(false)
|
|
127
|
-
const [refreshNotice, setRefreshNotice] = React.useState(null)
|
|
128
|
-
const [pairingTokenCopied, setPairingTokenCopied] = React.useState(false)
|
|
129
|
-
const [deviceNotice, setDeviceNotice] = React.useState(null)
|
|
130
|
-
const [deviceNoticeError, setDeviceNoticeError] = React.useState(false)
|
|
131
|
-
const [revokingIds, setRevokingIds] = React.useState(() => new Set())
|
|
132
|
-
const revokingIdsRef = React.useRef(new Set())
|
|
133
|
-
const automaticUrlRef = React.useRef('')
|
|
134
|
-
|
|
135
|
-
const refresh = React.useCallback(async () => {
|
|
136
|
-
try {
|
|
137
|
-
const [deviceData, statusData] = await Promise.all([
|
|
138
|
-
request('/mgw/devices'),
|
|
139
|
-
request('/mgw/status'),
|
|
140
|
-
])
|
|
141
|
-
const nextDevices = deviceData.devices || []
|
|
142
|
-
// Keep existing DOM rows alive when polling returns identical data.
|
|
143
|
-
// Replacing them every three seconds can swallow a pointer/click that
|
|
144
|
-
// started on the old button and ended after React replaced the row.
|
|
145
|
-
if (revokingIdsRef.current.size === 0) {
|
|
146
|
-
setDevices((current) => JSON.stringify(current) === JSON.stringify(nextDevices) ? current : nextDevices)
|
|
147
|
-
}
|
|
148
|
-
setStatus((current) => JSON.stringify(current) === JSON.stringify(statusData) ? current : statusData)
|
|
149
|
-
const lanUrl = statusData.lan && Array.isArray(statusData.lan.urls) ? statusData.lan.urls[0] : ''
|
|
150
|
-
const preferredUrl = statusData.publicUrl || lanUrl || inferredUrl(statusData.wsPath)
|
|
151
|
-
setPublicUrl((current) => {
|
|
152
|
-
if (!current || current === automaticUrlRef.current) {
|
|
153
|
-
automaticUrlRef.current = preferredUrl
|
|
154
|
-
return preferredUrl
|
|
155
|
-
}
|
|
156
|
-
return current
|
|
157
|
-
})
|
|
158
|
-
return true
|
|
159
|
-
} catch (cause) {
|
|
160
|
-
setError(cause.message)
|
|
161
|
-
return false
|
|
162
|
-
}
|
|
163
|
-
}, [])
|
|
164
|
-
|
|
165
|
-
const manualRefresh = async () => {
|
|
166
|
-
if (refreshing) return
|
|
167
|
-
setRefreshing(true)
|
|
168
|
-
setRefreshNotice(null)
|
|
169
|
-
const succeeded = await refresh()
|
|
170
|
-
setRefreshing(false)
|
|
171
|
-
if (succeeded) {
|
|
172
|
-
setError(null)
|
|
173
|
-
setRefreshNotice(`已刷新 · ${new Date().toLocaleTimeString()}`)
|
|
174
|
-
window.setTimeout(() => setRefreshNotice(null), 2200)
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
React.useEffect(() => {
|
|
179
|
-
let active = true
|
|
180
|
-
refresh()
|
|
181
|
-
request('/mgw/public-setup').then((data) => {
|
|
182
|
-
if (!active) return
|
|
183
|
-
setPublicSetup(data)
|
|
184
|
-
if (data.publicUrl) {
|
|
185
|
-
const match = /^wss:\/\/([^/]+)\/ws\/mobile$/.exec(data.publicUrl)
|
|
186
|
-
if (match) setPublicIp(match[1])
|
|
187
|
-
}
|
|
188
|
-
}).catch((cause) => active && setPublicSetup({ installed: false, configured: false, error: cause.message }))
|
|
189
|
-
const timer = window.setInterval(refresh, 3000)
|
|
190
|
-
return () => { active = false; window.clearInterval(timer) }
|
|
191
|
-
}, [refresh])
|
|
192
|
-
|
|
193
|
-
const configurePublicAccess = async () => {
|
|
194
|
-
if (publicSetupBusy) return
|
|
195
|
-
setPublicSetupBusy(true)
|
|
196
|
-
setError(null)
|
|
197
|
-
try {
|
|
198
|
-
const data = await request('/mgw/public-setup', {
|
|
199
|
-
method: 'POST',
|
|
200
|
-
headers: { 'Content-Type': 'application/json' },
|
|
201
|
-
body: JSON.stringify({ publicIp }),
|
|
202
|
-
})
|
|
203
|
-
setPublicSetup(data)
|
|
204
|
-
if (data.publicUrl) {
|
|
205
|
-
automaticUrlRef.current = data.publicUrl
|
|
206
|
-
setPublicUrl(data.publicUrl)
|
|
207
|
-
}
|
|
208
|
-
} catch (cause) {
|
|
209
|
-
setError(cause.message)
|
|
210
|
-
} finally {
|
|
211
|
-
setPublicSetupBusy(false)
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const createPairing = async (endpoint) => request('/mgw/pair', {
|
|
216
|
-
method: 'POST',
|
|
217
|
-
headers: { 'Content-Type': 'application/json' },
|
|
218
|
-
body: JSON.stringify({ name: deviceName, publicUrl: endpoint }),
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
const pair = async () => {
|
|
222
|
-
setBusy(true)
|
|
223
|
-
setError(null)
|
|
224
|
-
setPairingTokenCopied(false)
|
|
225
|
-
try {
|
|
226
|
-
const data = await createPairing(publicUrl)
|
|
227
|
-
setQr(data)
|
|
228
|
-
} catch (cause) {
|
|
229
|
-
setError(cause.message)
|
|
230
|
-
} finally {
|
|
231
|
-
setBusy(false)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const copyPairingText = async () => {
|
|
236
|
-
if (!qr || !qr.qrPayload) return
|
|
237
|
-
try {
|
|
238
|
-
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
|
239
|
-
throw new Error('当前浏览器不允许直接写入剪贴板,请选中文本后手动复制')
|
|
240
|
-
}
|
|
241
|
-
await navigator.clipboard.writeText(qr.qrPayload)
|
|
242
|
-
setError(null)
|
|
243
|
-
setPairingTokenCopied(true)
|
|
244
|
-
window.setTimeout(() => setPairingTokenCopied(false), 2200)
|
|
245
|
-
} catch (cause) {
|
|
246
|
-
setPairingTokenCopied(false)
|
|
247
|
-
setError(cause.message)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const changeGatewayMode = async (mode) => {
|
|
252
|
-
setBusy(true)
|
|
253
|
-
setError(null)
|
|
254
|
-
try {
|
|
255
|
-
const data = await request('/mgw/gateway', {
|
|
256
|
-
method: 'POST',
|
|
257
|
-
headers: { 'Content-Type': 'application/json' },
|
|
258
|
-
body: JSON.stringify({ mode }),
|
|
259
|
-
})
|
|
260
|
-
setStatus((current) => ({ ...(current || {}), ...data }))
|
|
261
|
-
if (mode === 'disabled') setQr(null)
|
|
262
|
-
} catch (cause) {
|
|
263
|
-
setError(cause.message)
|
|
264
|
-
} finally {
|
|
265
|
-
setBusy(false)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const toggleAuth = async () => {
|
|
270
|
-
const enabled = !(status && status.requireAuth)
|
|
271
|
-
if (!enabled && !window.confirm('关闭设备鉴权后,任何能访问移动网关的人都可以控制 DSH。此选项仅限 Debug 阶段使用,确定继续吗?')) return
|
|
272
|
-
setBusy(true)
|
|
273
|
-
setError(null)
|
|
274
|
-
try {
|
|
275
|
-
const data = await request('/mgw/auth', {
|
|
276
|
-
method: 'POST',
|
|
277
|
-
headers: { 'Content-Type': 'application/json' },
|
|
278
|
-
body: JSON.stringify({ enabled }),
|
|
279
|
-
})
|
|
280
|
-
setStatus((current) => ({ ...(current || {}), requireAuth: data.requireAuth }))
|
|
281
|
-
} catch (cause) {
|
|
282
|
-
setError(cause.message)
|
|
283
|
-
} finally {
|
|
284
|
-
setBusy(false)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const revoke = async (device) => {
|
|
289
|
-
if (revokingIdsRef.current.has(device.id)) return
|
|
290
|
-
setDeviceNotice(null)
|
|
291
|
-
setDeviceNoticeError(false)
|
|
292
|
-
setError(null)
|
|
293
|
-
revokingIdsRef.current.add(device.id)
|
|
294
|
-
setRevokingIds((current) => new Set(current).add(device.id))
|
|
295
|
-
try {
|
|
296
|
-
await request(`/mgw/devices/${encodeURIComponent(device.id)}/revoke`, { method: 'POST' })
|
|
297
|
-
setDevices((current) => Array.isArray(current) ? current.filter((item) => item.id !== device.id) : current)
|
|
298
|
-
setDeviceNotice(`已吊销 ${device.name}`)
|
|
299
|
-
window.setTimeout(() => setDeviceNotice(null), 2200)
|
|
300
|
-
} catch (cause) {
|
|
301
|
-
setDeviceNoticeError(true)
|
|
302
|
-
setDeviceNotice(`吊销失败:${cause.message}`)
|
|
303
|
-
setError(cause.message)
|
|
304
|
-
} finally {
|
|
305
|
-
revokingIdsRef.current.delete(device.id)
|
|
306
|
-
setRevokingIds((current) => {
|
|
307
|
-
const next = new Set(current)
|
|
308
|
-
next.delete(device.id)
|
|
309
|
-
return next
|
|
310
|
-
})
|
|
311
|
-
await refresh()
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
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': '移动设备管理' },
|
|
317
|
-
React.createElement('div', { style: styles.header },
|
|
318
|
-
React.createElement('div', null,
|
|
319
|
-
React.createElement('h2', { style: styles.title }, '移动设备'),
|
|
320
|
-
React.createElement('div', { style: styles.muted }, status && status.gatewayEnabled ? '移动网关已开启 · 设备鉴权生效中' : '移动网关已关闭 · 普通 WebUI 模式')),
|
|
321
|
-
React.createElement('button', { style: styles.button, onClick: () => setOpen(false), 'aria-label': '关闭' }, '✕')),
|
|
322
|
-
error ? React.createElement('div', { style: styles.error }, error) : null,
|
|
323
|
-
React.createElement('section', { style: styles.card },
|
|
324
|
-
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 14, alignItems: 'center' } },
|
|
325
|
-
React.createElement('div', null,
|
|
326
|
-
React.createElement('strong', null, '网关运行模式'),
|
|
327
|
-
React.createElement('div', { style: { ...styles.muted, marginTop: 4 } }, status && status.gatewayEnabled
|
|
328
|
-
? status.gatewayMode === 'persistent'
|
|
329
|
-
? '常驻开启,重启后保持;无需设备在线'
|
|
330
|
-
: status.waitExpiresAt
|
|
331
|
-
? `等待可信设备连接,约 ${Math.max(1, Math.ceil((status.waitExpiresAt - Date.now()) / 60000))} 分钟后自动关闭`
|
|
332
|
-
: '设备已连接过,本次运行保持开启;重启后重新计时'
|
|
333
|
-
: '关闭时不会接受移动设备连接,重启后保持关闭')),
|
|
334
|
-
React.createElement('span', { style: { position: 'relative', display: 'inline-flex', flexShrink: 0 } },
|
|
335
|
-
React.createElement('select', {
|
|
336
|
-
style: { ...styles.input, width: 'auto', marginBottom: 0, padding: '8px 40px 8px 12px', appearance: 'none', WebkitAppearance: 'none' },
|
|
337
|
-
'aria-label': '网关运行模式',
|
|
338
|
-
value: status ? status.gatewayMode || (status.gatewayEnabled ? 'temporary' : 'disabled') : 'disabled',
|
|
339
|
-
disabled: busy || !status,
|
|
340
|
-
onChange: (event) => changeGatewayMode(event.target.value),
|
|
341
|
-
},
|
|
342
|
-
React.createElement('option', { value: 'disabled' }, '关闭'),
|
|
343
|
-
React.createElement('option', { value: 'temporary' }, '临时开启'),
|
|
344
|
-
React.createElement('option', { value: 'persistent' }, '常驻开启')),
|
|
345
|
-
React.createElement('span', {
|
|
346
|
-
'aria-hidden': true,
|
|
347
|
-
style: {
|
|
348
|
-
position: 'absolute', right: 15, top: '50%', width: 7, height: 7,
|
|
349
|
-
borderRight: `1.5px solid ${colors.text}`, borderBottom: `1.5px solid ${colors.text}`,
|
|
350
|
-
transform: 'translateY(-65%) rotate(45deg)', pointerEvents: 'none',
|
|
351
|
-
},
|
|
352
|
-
}))),
|
|
353
|
-
status && status.gatewayId ? React.createElement('div', { style: { ...styles.muted, marginTop: 12, overflowWrap: 'anywhere' } },
|
|
354
|
-
`${status.gatewayName} · ${status.gatewayId}`) : null),
|
|
355
|
-
React.createElement('section', { style: styles.card },
|
|
356
|
-
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 14, alignItems: 'center' } },
|
|
357
|
-
React.createElement('div', null,
|
|
358
|
-
React.createElement('strong', null, '设备鉴权'),
|
|
359
|
-
React.createElement('div', { style: { ...styles.muted, marginTop: 4 } }, status && status.requireAuth ? '已开启,仅允许可信设备连接' : '已关闭,仅影响本机 Debug 入口')),
|
|
360
|
-
React.createElement('label', { style: { ...styles.switch, opacity: busy || !status ? .55 : 1 }, title: status && status.requireAuth ? '关闭设备鉴权' : '开启设备鉴权' },
|
|
361
|
-
React.createElement('input', {
|
|
362
|
-
type: 'checkbox',
|
|
363
|
-
role: 'switch',
|
|
364
|
-
'aria-label': '设备鉴权',
|
|
365
|
-
'aria-checked': !!(status && status.requireAuth),
|
|
366
|
-
checked: !!(status && status.requireAuth),
|
|
367
|
-
disabled: busy || !status,
|
|
368
|
-
onChange: toggleAuth,
|
|
369
|
-
style: styles.switchInput,
|
|
370
|
-
}),
|
|
371
|
-
React.createElement('span', { style: { ...styles.switchTrack, background: status && status.requireAuth ? colors.success : colors.danger } }),
|
|
372
|
-
React.createElement('span', { style: { ...styles.switchKnob, left: status && status.requireAuth ? 21 : 3 } }))),
|
|
373
|
-
status && !status.requireAuth
|
|
374
|
-
? React.createElement('p', { style: { ...styles.error, margin: '12px 0 0' } }, 'Debug 模式:本机 DSH 入口将跳过设备凭证校验;独立局域网入口仍强制鉴权。')
|
|
375
|
-
: null),
|
|
376
|
-
React.createElement('section', { style: styles.card },
|
|
377
|
-
React.createElement('strong', null, '公网接入'),
|
|
378
|
-
publicSetup === null
|
|
379
|
-
? React.createElement('p', { style: styles.muted }, '正在检查系统组件…')
|
|
380
|
-
: publicSetup.installed
|
|
381
|
-
? React.createElement(React.Fragment, null,
|
|
382
|
-
React.createElement('div', { style: { ...styles.muted, margin: '5px 0 12px' } }, publicSetup.configured
|
|
383
|
-
? publicSetup.backendPort && status && status.webPort && publicSetup.backendPort !== status.webPort
|
|
384
|
-
? `端口需要更新:Nginx 当前为 ${publicSetup.backendPort},DSH 当前为 ${status.webPort}`
|
|
385
|
-
: `已配置${publicSetup.backendPort ? `,Nginx 转发至 DSH 端口 ${publicSetup.backendPort}` : ''}`
|
|
386
|
-
: `Helper 已就绪,将自动使用当前 DSH 端口${status && status.webPort ? ` ${status.webPort}` : ''}`),
|
|
387
|
-
React.createElement('label', { style: styles.label }, '服务器公网 IPv4'),
|
|
388
|
-
React.createElement('input', {
|
|
389
|
-
style: styles.input,
|
|
390
|
-
value: publicIp,
|
|
391
|
-
onChange: (event) => setPublicIp(event.target.value.trim()),
|
|
392
|
-
placeholder: '203.0.113.10',
|
|
393
|
-
inputMode: 'decimal',
|
|
394
|
-
spellCheck: false,
|
|
395
|
-
}),
|
|
396
|
-
React.createElement('button', {
|
|
397
|
-
style: { ...styles.button, ...styles.primary, opacity: publicSetupBusy || !publicIp ? .65 : 1 },
|
|
398
|
-
disabled: publicSetupBusy || !publicIp,
|
|
399
|
-
onClick: configurePublicAccess,
|
|
400
|
-
}, publicSetupBusy ? '正在配置 Nginx 与证书…' : publicSetup.configured ? '更新公网配置' : '配置公网接入'),
|
|
401
|
-
publicSetup.publicUrl
|
|
402
|
-
? React.createElement('div', { style: { ...styles.muted, marginTop: 9, overflowWrap: 'anywhere' } }, publicSetup.publicUrl)
|
|
403
|
-
: null)
|
|
404
|
-
: React.createElement(React.Fragment, null,
|
|
405
|
-
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'))),
|
|
407
|
-
React.createElement('section', { style: styles.card },
|
|
408
|
-
React.createElement('label', { style: styles.label }, 'WebSocket 地址'),
|
|
409
|
-
React.createElement('input', { style: styles.input, value: publicUrl, onChange: (event) => { automaticUrlRef.current = ''; setPublicUrl(event.target.value) }, placeholder: 'ws://192.168.1.10:3081/ws/mobile', spellCheck: false }),
|
|
410
|
-
status && status.lan && status.lan.enabled
|
|
411
|
-
? React.createElement('div', { style: { ...styles.muted, margin: '-7px 0 10px', color: status.lan.error ? colors.danger : colors.muted } },
|
|
412
|
-
status.lan.error
|
|
413
|
-
? `局域网监听失败:${status.lan.error}`
|
|
414
|
-
: status.lan.listening
|
|
415
|
-
? `局域网入口已开启${status.lan.urls && status.lan.urls.length ? `:${status.lan.urls.join('、')}` : `,端口 ${status.lan.port}`}`
|
|
416
|
-
: '局域网入口启动中…')
|
|
417
|
-
: null,
|
|
418
|
-
React.createElement('label', { style: styles.label }, '设备名称'),
|
|
419
|
-
React.createElement('input', { style: styles.input, value: deviceName, onChange: (event) => setDeviceName(event.target.value), maxLength: 80, placeholder: 'iPhone' }),
|
|
420
|
-
React.createElement('button', { style: { ...styles.button, ...styles.primary, opacity: busy || !(status && status.gatewayEnabled) ? .65 : 1 }, disabled: busy || !(status && status.gatewayEnabled), onClick: pair }, busy ? '正在生成…' : status && status.gatewayEnabled ? '生成配对二维码' : '请先开启移动网关'),
|
|
421
|
-
React.createElement('p', { style: { ...styles.muted, margin: '10px 0 0' } }, '同一局域网可直接使用私有地址的 ws://;公网地址仍必须使用 wss://。配对码只能使用一次,并在 5 分钟内过期。')),
|
|
422
|
-
qr ? React.createElement('section', { style: { ...styles.card, textAlign: 'center' } },
|
|
423
|
-
React.createElement('strong', null, '使用 iOS 客户端扫码'),
|
|
424
|
-
React.createElement('img', { style: styles.qr, src: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(qr.svg)}`, alt: '一次性设备配对二维码' }),
|
|
425
|
-
React.createElement('label', { style: { ...styles.label, textAlign: 'left' } }, 'Base64URL 配对字符串'),
|
|
426
|
-
React.createElement('textarea', { style: styles.pairingText, value: qr.qrPayload, readOnly: true, spellCheck: false, onFocus: (event) => event.target.select() }),
|
|
427
|
-
React.createElement('button', { style: { ...styles.button, width: '100%', marginBottom: 8, borderColor: pairingTokenCopied ? colors.success : colors.border, color: pairingTokenCopied ? colors.success : colors.text }, onClick: copyPairingText }, pairingTokenCopied ? '✓ 已复制' : '复制配对 Token'),
|
|
428
|
-
pairingTokenCopied ? React.createElement('div', { style: { ...styles.muted, color: colors.success, marginBottom: 8 } }, '已复制到剪贴板') : null,
|
|
429
|
-
React.createElement('div', { style: styles.muted }, `有效期至 ${new Date(qr.pairing.expiresAt).toLocaleTimeString()};扫码成功后此二维码立即失效。`)) : null,
|
|
430
|
-
React.createElement('section', { style: styles.card },
|
|
431
|
-
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
432
|
-
React.createElement('strong', null, '可信设备'),
|
|
433
|
-
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 10 } },
|
|
434
|
-
deviceNotice || refreshNotice ? React.createElement('span', {
|
|
435
|
-
style: { ...styles.muted, color: deviceNoticeError ? colors.danger : colors.success },
|
|
436
|
-
}, deviceNotice || refreshNotice) : null,
|
|
437
|
-
React.createElement('button', {
|
|
438
|
-
style: { ...styles.button, opacity: refreshing ? .6 : 1 },
|
|
439
|
-
disabled: refreshing,
|
|
440
|
-
onClick: manualRefresh,
|
|
441
|
-
}, refreshing ? '刷新中…' : '刷新'))),
|
|
442
|
-
devices === null
|
|
443
|
-
? React.createElement('p', { style: styles.muted }, '加载中…')
|
|
444
|
-
: devices.length === 0
|
|
445
|
-
? React.createElement('p', { style: styles.muted }, '暂无已配对设备。')
|
|
446
|
-
: devices.map((device) => React.createElement(DeviceRow, {
|
|
447
|
-
key: device.id,
|
|
448
|
-
device,
|
|
449
|
-
onRevoke: revoke,
|
|
450
|
-
revoking: revokingIds.has(device.id),
|
|
451
|
-
}))),
|
|
452
|
-
status && status.version
|
|
453
|
-
? React.createElement('div', { style: styles.version, 'aria-label': `插件版本 ${status.version}` }, `v${status.version}`)
|
|
454
|
-
: null))
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function FooterButton(props) {
|
|
458
|
-
const isOpen = React.useSyncExternalStore(subscribe, getOpen)
|
|
459
|
-
const wide = !!props.wide
|
|
460
|
-
const iconSize = wide ? 16 : 18
|
|
461
|
-
const icon = React.createElement('svg', {
|
|
462
|
-
width: iconSize,
|
|
463
|
-
height: iconSize,
|
|
464
|
-
viewBox: '0 0 24 24',
|
|
465
|
-
fill: 'none',
|
|
466
|
-
stroke: 'currentColor',
|
|
467
|
-
strokeWidth: 1.8,
|
|
468
|
-
strokeLinecap: 'round',
|
|
469
|
-
strokeLinejoin: 'round',
|
|
470
|
-
'aria-hidden': true,
|
|
471
|
-
},
|
|
472
|
-
React.createElement('rect', { x: 6, y: 2, width: 12, height: 20, rx: 2.5 }),
|
|
473
|
-
React.createElement('path', { d: 'M10 18h4' }))
|
|
474
|
-
return React.createElement('button', {
|
|
475
|
-
title: '移动设备管理',
|
|
476
|
-
'aria-label': '移动设备管理',
|
|
477
|
-
'aria-pressed': isOpen,
|
|
478
|
-
onClick: () => setOpen(!isOpen),
|
|
479
|
-
style: {
|
|
480
|
-
cursor: 'pointer',
|
|
481
|
-
background: 'transparent',
|
|
482
|
-
border: 'none',
|
|
483
|
-
color: 'inherit',
|
|
484
|
-
font: 'inherit',
|
|
485
|
-
display: 'flex',
|
|
486
|
-
alignItems: 'center',
|
|
487
|
-
justifyContent: wide ? 'flex-start' : 'center',
|
|
488
|
-
gap: wide ? 8 : 0,
|
|
489
|
-
boxSizing: 'border-box',
|
|
490
|
-
width: wide ? 'calc(100% + 4px)' : 36,
|
|
491
|
-
height: wide ? 42 : 36,
|
|
492
|
-
margin: wide ? '4px -2px' : '3px 0',
|
|
493
|
-
padding: wide ? '0 10px 0 8px' : 0,
|
|
494
|
-
lineHeight: wide ? '22px' : 'normal',
|
|
495
|
-
},
|
|
496
|
-
}, icon, wide ? React.createElement('span', null, '移动设备') : null)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function OverlayEntry() {
|
|
500
|
-
const isOpen = React.useSyncExternalStore(subscribe, getOpen)
|
|
501
|
-
return isOpen ? React.createElement(DevicePanel) : null
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const inject = ['slots']
|
|
505
|
-
function apply(ctx) {
|
|
506
|
-
ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register(
|
|
507
|
-
{ name: 'sidebar.footer.action', id: 'mobile-gateway-devices', order: 80, label: '移动设备' },
|
|
508
|
-
(props) => React.createElement(FooterButton, props),
|
|
509
|
-
))
|
|
510
|
-
ctx.slots.inject('shell.overlay', () => ctx.slots.register(
|
|
511
|
-
{ name: 'shell.overlay', id: 'mobile-gateway-devices-panel', order: 100, label: '移动设备' },
|
|
512
|
-
() => React.createElement(OverlayEntry),
|
|
513
|
-
))
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
exports.apply = apply
|
|
517
|
-
exports.inject = inject
|
|
518
|
-
return module.exports
|
|
519
|
-
},
|
|
520
|
-
})
|