@skillpie/dsh-remote 0.0.0-stage → 1.0.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/README.md +77 -2
- package/cordis.patch.yml +11 -0
- package/icon.svg +14 -0
- package/lib/bridge.js +377 -0
- package/lib/client.js +493 -0
- package/lib/console.js +434 -0
- package/lib/engine.js +367 -0
- package/lib/index.js +475 -0
- package/lib/pipe.js +83 -0
- package/lib/protocol.js +122 -0
- package/lib/relay-core.js +823 -0
- package/lib/remote.js +181 -0
- package/lib/ws-adapter.js +49 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +50 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-remote — Web client half (module-loader bundle).
|
|
3
|
+
*
|
|
4
|
+
* The sidebar foot gains a compact "远程控制" action button (icon when the
|
|
5
|
+
* rail is collapsed, icon + label when wide). Clicking it opens an
|
|
6
|
+
* application-level dialog (shell.overlay) that carries the owner panel:
|
|
7
|
+
* the 8-digit code, shareable links, relay/direct status, the activity log,
|
|
8
|
+
* and the owner controls (regenerate code, allow/deny visitor control,
|
|
9
|
+
* enable/disable remote access).
|
|
10
|
+
*
|
|
11
|
+
* No sidebar.panellist entry and no `main` view: the dialog is the surface.
|
|
12
|
+
*
|
|
13
|
+
* Host conventions: theme tokens only, locale dictionaries for visible
|
|
14
|
+
* text, React from the module table, no DOM writes outside the React tree,
|
|
15
|
+
* components with hooks rendered through h(Component, props).
|
|
16
|
+
*/
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: '@skillpie/dsh-remote',
|
|
19
|
+
factory(require) {
|
|
20
|
+
var React = require('react')
|
|
21
|
+
var h = React.createElement
|
|
22
|
+
var NS = '@skillpie/dsh-remote'
|
|
23
|
+
var API = '/dsh-remote/api'
|
|
24
|
+
|
|
25
|
+
var zh = {
|
|
26
|
+
title: '远程控制',
|
|
27
|
+
codeLabel: '远程码',
|
|
28
|
+
relayUrl: '中转链接',
|
|
29
|
+
directUrl: '本机直连',
|
|
30
|
+
copy: '复制',
|
|
31
|
+
copied: '已复制',
|
|
32
|
+
relay: '中转站',
|
|
33
|
+
direct: '直连',
|
|
34
|
+
online: '已连接',
|
|
35
|
+
offline: '未连接',
|
|
36
|
+
disabled: '远程控制已关闭',
|
|
37
|
+
visitors: '在线访客',
|
|
38
|
+
none: '暂无',
|
|
39
|
+
allowControl: '允许访客控制会话(关闭为只读)',
|
|
40
|
+
enabledToggle: '启用远程控制',
|
|
41
|
+
regenerate: '重新生成远程码',
|
|
42
|
+
log: '活动记录',
|
|
43
|
+
loading: '正在读取状态…',
|
|
44
|
+
actionFailed: '操作失败',
|
|
45
|
+
confirmRegenerate: '重新生成后旧链接立即失效,确认?',
|
|
46
|
+
relayFaceHosts: '中转面已注册宿主机',
|
|
47
|
+
offlineHost: '离线',
|
|
48
|
+
visitorAt: '接入于',
|
|
49
|
+
close: '关闭',
|
|
50
|
+
}
|
|
51
|
+
var en = {
|
|
52
|
+
title: 'Remote control',
|
|
53
|
+
codeLabel: 'Remote code',
|
|
54
|
+
relayUrl: 'Relay link',
|
|
55
|
+
directUrl: 'Direct link',
|
|
56
|
+
copy: 'Copy',
|
|
57
|
+
copied: 'Copied',
|
|
58
|
+
relay: 'Relay',
|
|
59
|
+
direct: 'Direct',
|
|
60
|
+
online: 'connected',
|
|
61
|
+
offline: 'not connected',
|
|
62
|
+
disabled: 'Remote control is disabled',
|
|
63
|
+
visitors: 'Visitors online',
|
|
64
|
+
none: 'none',
|
|
65
|
+
allowControl: 'Allow visitors to control sessions (off = read-only)',
|
|
66
|
+
enabledToggle: 'Enable remote control',
|
|
67
|
+
regenerate: 'Regenerate remote code',
|
|
68
|
+
log: 'Activity',
|
|
69
|
+
loading: 'Loading status…',
|
|
70
|
+
actionFailed: 'Action failed',
|
|
71
|
+
confirmRegenerate: 'The old link stops working immediately. Regenerate?',
|
|
72
|
+
relayFaceHosts: 'Relay face registered hosts',
|
|
73
|
+
offlineHost: 'offline',
|
|
74
|
+
visitorAt: 'connected at',
|
|
75
|
+
close: 'Close',
|
|
76
|
+
}
|
|
77
|
+
var pageT = function (key) { return key }
|
|
78
|
+
|
|
79
|
+
/* ── shared dialog open state (footer button ⇄ overlay) ───────────── */
|
|
80
|
+
|
|
81
|
+
var openStore = { open: false, listeners: new Set() }
|
|
82
|
+
function setPanelOpen(value) {
|
|
83
|
+
openStore.open = value
|
|
84
|
+
openStore.listeners.forEach(function (listener) { listener(value) })
|
|
85
|
+
}
|
|
86
|
+
function usePanelOpen() {
|
|
87
|
+
var pair = React.useState(openStore.open)
|
|
88
|
+
var open = pair[0]
|
|
89
|
+
var setLocal = pair[1]
|
|
90
|
+
React.useEffect(function () {
|
|
91
|
+
var listener = function (value) { setLocal(value) }
|
|
92
|
+
openStore.listeners.add(listener)
|
|
93
|
+
return function () { openStore.listeners.delete(listener) }
|
|
94
|
+
}, [])
|
|
95
|
+
return [open, setPanelOpen]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ── icon ─────────────────────────────────────────────────────────── */
|
|
99
|
+
|
|
100
|
+
// ZCode 同款远程控制图标(lucide MonitorSmartphone,24 网格 stroke 风格)。
|
|
101
|
+
function RemoteIcon(props) {
|
|
102
|
+
var size = props && props.size ? props.size : 16
|
|
103
|
+
return h('svg', {
|
|
104
|
+
viewBox: '0 0 24 24', width: size, height: size, 'aria-hidden': true,
|
|
105
|
+
fill: 'none', stroke: 'currentColor', 'stroke-width': 2,
|
|
106
|
+
'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
107
|
+
style: { display: 'block', flex: 'none' },
|
|
108
|
+
},
|
|
109
|
+
h('path', { d: 'M18 8V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h8' }),
|
|
110
|
+
h('path', { d: 'M10 19v-3.96 3.15' }),
|
|
111
|
+
h('path', { d: 'M7 19h5' }),
|
|
112
|
+
h('rect', { width: 6, height: 10, x: 16, y: 12, rx: 2 }),
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ── sidebar foot action button ───────────────────────────────────── */
|
|
117
|
+
|
|
118
|
+
function FooterAction(props) {
|
|
119
|
+
var t = pageT
|
|
120
|
+
var wide = !!(props && props.wide)
|
|
121
|
+
var pair = usePanelOpen()
|
|
122
|
+
var hoverState = React.useState(false)
|
|
123
|
+
var hovered = hoverState[0]
|
|
124
|
+
var setHovered = hoverState[1]
|
|
125
|
+
// ZCode 观感:图标与账号行同行、位于其右端。账号行本身是宿主占用的
|
|
126
|
+
// settingsArea(插件无法插入同排元素),这里用零高覆盖层把 28px 图标
|
|
127
|
+
// 按钮精确叠到账号行的右侧空白处(宽模式 trigger 高 42px + 上下 4px
|
|
128
|
+
// 外边距;收起模式 36x36、上 8px)。
|
|
129
|
+
var wrapperStyle = wide
|
|
130
|
+
? { position: 'relative', height: 0, width: '100%', zIndex: 2, pointerEvents: 'none' }
|
|
131
|
+
: { position: 'relative', height: 0, width: '36px', margin: '0 auto', zIndex: 2, pointerEvents: 'none' }
|
|
132
|
+
var buttonStyle = wide
|
|
133
|
+
? { position: 'absolute', top: 11, right: 12 }
|
|
134
|
+
: { position: 'absolute', top: 12, left: '50%', transform: 'translateX(-50%)' }
|
|
135
|
+
return h('div', { style: wrapperStyle },
|
|
136
|
+
h('button', {
|
|
137
|
+
type: 'button',
|
|
138
|
+
title: t('title'),
|
|
139
|
+
'aria-label': t('title'),
|
|
140
|
+
onClick: function () { setPanelOpen(true) },
|
|
141
|
+
onMouseEnter: function () { setHovered(true) },
|
|
142
|
+
onMouseLeave: function () { setHovered(false) },
|
|
143
|
+
onFocus: function () { setHovered(true) },
|
|
144
|
+
onBlur: function () { setHovered(false) },
|
|
145
|
+
style: Object.assign({
|
|
146
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
147
|
+
width: 28, height: 28,
|
|
148
|
+
borderRadius: 8,
|
|
149
|
+
border: '1px solid ' + (hovered ? 'var(--dsw-alias-border-l1)' : 'transparent'),
|
|
150
|
+
background: hovered ? 'var(--dsw-alias-bg-layer-1)' : 'transparent',
|
|
151
|
+
color: hovered ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
152
|
+
cursor: 'pointer', flex: 'none',
|
|
153
|
+
pointerEvents: 'auto',
|
|
154
|
+
}, buttonStyle),
|
|
155
|
+
},
|
|
156
|
+
h(RemoteIcon, { size: 16 }),
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ── status polling ───────────────────────────────────────────────── */
|
|
162
|
+
|
|
163
|
+
function useStatus() {
|
|
164
|
+
var state = React.useState(undefined)
|
|
165
|
+
var status = state[0]
|
|
166
|
+
var setStatus = state[1]
|
|
167
|
+
React.useEffect(function () {
|
|
168
|
+
var stopped = false
|
|
169
|
+
var tick = function () {
|
|
170
|
+
fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'status' }) })
|
|
171
|
+
.then(function (res) { return res.json() })
|
|
172
|
+
.then(function (data) { if (!stopped && data && data.ok) setStatus(data.status) })
|
|
173
|
+
.catch(function () { /* transient; next tick retries */ })
|
|
174
|
+
}
|
|
175
|
+
tick()
|
|
176
|
+
var timer = setInterval(tick, 3000)
|
|
177
|
+
return function () { stopped = true; clearInterval(timer) }
|
|
178
|
+
}, [])
|
|
179
|
+
return status
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function post(body) {
|
|
183
|
+
return fetch(API, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
184
|
+
.then(function (res) { return res.json() })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* ── owner panel pieces ───────────────────────────────────────────── */
|
|
188
|
+
|
|
189
|
+
var cardStyle = {
|
|
190
|
+
background: 'var(--dsw-alias-bg-layer-1)',
|
|
191
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
192
|
+
borderRadius: 12,
|
|
193
|
+
padding: '16px 18px',
|
|
194
|
+
}
|
|
195
|
+
var labelStyle = { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginBottom: 6 }
|
|
196
|
+
var buttonStyle = {
|
|
197
|
+
display: 'inline-flex', alignItems: 'center', gap: 6, height: 28, padding: '0 12px',
|
|
198
|
+
borderRadius: 8, border: '1px solid var(--dsw-alias-border-l1)', cursor: 'pointer',
|
|
199
|
+
background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', fontSize: 12,
|
|
200
|
+
}
|
|
201
|
+
var codeStyle = {
|
|
202
|
+
fontFamily: 'var(--dsw-font-mono, ui-monospace, Menlo, monospace)',
|
|
203
|
+
fontSize: 30, letterSpacing: 6, fontWeight: 700,
|
|
204
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function StatusChip(props) {
|
|
208
|
+
var on = props.on
|
|
209
|
+
var text = props.text
|
|
210
|
+
return h('span', {
|
|
211
|
+
style: {
|
|
212
|
+
display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12,
|
|
213
|
+
color: on ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
|
|
214
|
+
border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 999,
|
|
215
|
+
padding: '2px 10px',
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
h('span', { style: { width: 7, height: 7, borderRadius: 999, background: on ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-border-l1)' } }),
|
|
219
|
+
text,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function LinkRow(props) {
|
|
224
|
+
var t = props.t
|
|
225
|
+
var label = props.label
|
|
226
|
+
var url = props.url
|
|
227
|
+
var state = React.useState(false)
|
|
228
|
+
var copied = state[0]
|
|
229
|
+
var setCopied = state[1]
|
|
230
|
+
if (!url) return null
|
|
231
|
+
var copy = function () {
|
|
232
|
+
var done = function () { setCopied(true); setTimeout(function () { setCopied(false) }, 1500) }
|
|
233
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
234
|
+
navigator.clipboard.writeText(url).then(done, done)
|
|
235
|
+
} else {
|
|
236
|
+
var ta = document.createElement('textarea')
|
|
237
|
+
ta.value = url
|
|
238
|
+
document.body.appendChild(ta)
|
|
239
|
+
ta.select()
|
|
240
|
+
try { document.execCommand('copy') } catch (e) { /* noop */ }
|
|
241
|
+
document.body.removeChild(ta)
|
|
242
|
+
done()
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, marginTop: 8 } },
|
|
246
|
+
h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', flex: 'none', width: 62 } }, label),
|
|
247
|
+
h('code', {
|
|
248
|
+
style: {
|
|
249
|
+
flex: 1, fontSize: 12, fontFamily: 'var(--dsw-font-mono, ui-monospace, Menlo, monospace)',
|
|
250
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
251
|
+
color: 'var(--dsw-alias-label-primary)',
|
|
252
|
+
},
|
|
253
|
+
}, url),
|
|
254
|
+
h('button', { type: 'button', onClick: copy, style: buttonStyle }, copied ? t('copied') : t('copy')),
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function Toggle(props) {
|
|
259
|
+
var checked = props.checked
|
|
260
|
+
var onChange = props.onChange
|
|
261
|
+
return h('span', {
|
|
262
|
+
role: 'switch', 'aria-checked': checked ? 'true' : 'false', tabIndex: 0,
|
|
263
|
+
onClick: function () { onChange(!checked) },
|
|
264
|
+
onKeyDown: function (ev) { if (ev.key === 'Enter' || ev.key === ' ') onChange(!checked) },
|
|
265
|
+
style: {
|
|
266
|
+
width: 34, height: 20, borderRadius: 999, padding: 2, boxSizing: 'border-box',
|
|
267
|
+
background: checked ? 'var(--dsw-alias-brand-primary)' : 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-border-l1))',
|
|
268
|
+
display: 'inline-block', transition: 'background 0.15s', flex: 'none', cursor: 'pointer',
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
h('span', {
|
|
272
|
+
style: {
|
|
273
|
+
display: 'block', width: 16, height: 16, borderRadius: 999, background: '#ffffff',
|
|
274
|
+
transform: checked ? 'translateX(14px)' : 'translateX(0)', transition: 'transform 0.15s',
|
|
275
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function OwnerPanel() {
|
|
282
|
+
var t = pageT
|
|
283
|
+
var status = useStatus()
|
|
284
|
+
var errState = React.useState('')
|
|
285
|
+
var err = errState[0]
|
|
286
|
+
var setErr = errState[1]
|
|
287
|
+
|
|
288
|
+
var act = function (body) {
|
|
289
|
+
return function () {
|
|
290
|
+
post(body).then(function (data) {
|
|
291
|
+
if (!data.ok) setErr(data.error || t('actionFailed'))
|
|
292
|
+
else setErr('')
|
|
293
|
+
}).catch(function (error) { setErr(String(error)) })
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!status) {
|
|
298
|
+
return h('div', { style: { padding: 24, color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, t('loading'))
|
|
299
|
+
}
|
|
300
|
+
var relayText = status.enabled ? (status.relay && status.relay.connected ? t('online') : t('offline')) : t('disabled')
|
|
301
|
+
return h('div', {
|
|
302
|
+
style: {
|
|
303
|
+
minHeight: 0, boxSizing: 'border-box',
|
|
304
|
+
padding: 16, display: 'flex', flexDirection: 'column', gap: 14,
|
|
305
|
+
color: 'var(--dsw-alias-label-primary)', fontSize: 13,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
// Header chips
|
|
309
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
310
|
+
StatusChip({ on: status.enabled && status.relay && status.relay.connected, text: t('relay') + ': ' + relayText }),
|
|
311
|
+
status.direct ? StatusChip({ on: true, text: t('direct') }) : null,
|
|
312
|
+
),
|
|
313
|
+
// Code card
|
|
314
|
+
h('div', { style: cardStyle },
|
|
315
|
+
h('div', { style: labelStyle }, t('codeLabel') + '(访问 https://…/' + status.code + ')'),
|
|
316
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: 14 } },
|
|
317
|
+
h('span', { style: codeStyle }, status.code),
|
|
318
|
+
h('span', { style: { flex: 1 } }),
|
|
319
|
+
h('button', {
|
|
320
|
+
type: 'button', style: buttonStyle,
|
|
321
|
+
onClick: function () {
|
|
322
|
+
if (window.confirm(t('confirmRegenerate'))) act({ action: 'regenerate' })()
|
|
323
|
+
},
|
|
324
|
+
}, t('regenerate')),
|
|
325
|
+
),
|
|
326
|
+
h(LinkRow, { key: 'relay', t: t, label: t('relayUrl'), url: status.enabled ? status.relayUrlPublic : undefined }),
|
|
327
|
+
h(LinkRow, { key: 'direct', t: t, label: t('directUrl'), url: status.directUrl }),
|
|
328
|
+
status.enabled ? null : h('div', { style: { marginTop: 8, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('disabled')),
|
|
329
|
+
),
|
|
330
|
+
// Controls card
|
|
331
|
+
h('div', { style: cardStyle },
|
|
332
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } },
|
|
333
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
334
|
+
h('span', null, t('enabledToggle')),
|
|
335
|
+
Toggle({ checked: status.enabled, onChange: act({ action: 'set-enabled', value: !status.enabled }) }),
|
|
336
|
+
),
|
|
337
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
|
|
338
|
+
h('span', null, t('allowControl')),
|
|
339
|
+
Toggle({ checked: status.allowControl, onChange: act({ action: 'set-control', value: !status.allowControl }) }),
|
|
340
|
+
),
|
|
341
|
+
),
|
|
342
|
+
),
|
|
343
|
+
// Visitors card
|
|
344
|
+
h('div', { style: cardStyle },
|
|
345
|
+
h('div', { style: labelStyle }, t('visitors') + ':' + (status.visitors.length ? String(status.visitors.length) : t('none'))),
|
|
346
|
+
status.visitors.length
|
|
347
|
+
? h('div', { style: { display: 'flex', flexDirection: 'column', gap: 6 } }, status.visitors.map(function (v) {
|
|
348
|
+
return h('div', { key: v.id, style: { fontSize: 12, display: 'flex', gap: 8 } },
|
|
349
|
+
h('code', { style: { fontFamily: 'var(--dsw-font-mono, monospace)' } }, v.id),
|
|
350
|
+
h('span', { style: { color: 'var(--dsw-alias-label-secondary)' } },
|
|
351
|
+
t('visitorAt') + ' ' + new Date(v.connectedAt).toLocaleTimeString() +
|
|
352
|
+
(v.sessions && v.sessions.length ? ' · ' + v.sessions.length + ' 会话' : '')),
|
|
353
|
+
)
|
|
354
|
+
}))
|
|
355
|
+
: null,
|
|
356
|
+
status.relayFace && status.relayFace.hosts && status.relayFace.hosts.length
|
|
357
|
+
? h('div', { style: { marginTop: 12 } },
|
|
358
|
+
h('div', { style: labelStyle }, t('relayFaceHosts')),
|
|
359
|
+
status.relayFace.hosts.map(function (host) {
|
|
360
|
+
return h('div', { key: host.hostId, style: { fontSize: 12, display: 'flex', gap: 8, marginBottom: 4 } },
|
|
361
|
+
h('span', null, host.online ? '🟢' : '⚪'),
|
|
362
|
+
h('span', null, host.name + ' · ' + host.code + (host.online ? '' : '(' + t('offlineHost') + ')')),
|
|
363
|
+
)
|
|
364
|
+
}),
|
|
365
|
+
)
|
|
366
|
+
: null,
|
|
367
|
+
),
|
|
368
|
+
// Log card
|
|
369
|
+
h('div', { style: cardStyle },
|
|
370
|
+
h('div', { style: labelStyle }, t('log')),
|
|
371
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 3, maxHeight: 220, overflowY: 'auto' } },
|
|
372
|
+
(status.log || []).slice().reverse().map(function (entry, index) {
|
|
373
|
+
return h('div', { key: index, style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', display: 'flex', gap: 8 } },
|
|
374
|
+
h('span', { style: { flex: 'none', fontFamily: 'var(--dsw-font-mono, monospace)' } }, new Date(entry.time).toLocaleTimeString()),
|
|
375
|
+
h('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, entry.text),
|
|
376
|
+
)
|
|
377
|
+
}),
|
|
378
|
+
),
|
|
379
|
+
),
|
|
380
|
+
err ? h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, t('actionFailed') + ':' + err) : null,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* ── dialog (shell.overlay) ───────────────────────────────────────── */
|
|
385
|
+
|
|
386
|
+
function RemotePanelDialog() {
|
|
387
|
+
var t = pageT
|
|
388
|
+
var pair = usePanelOpen()
|
|
389
|
+
var open = pair[0]
|
|
390
|
+
var setOpen = pair[1]
|
|
391
|
+
var closeRef = React.useRef(null)
|
|
392
|
+
React.useEffect(function () {
|
|
393
|
+
if (!open) return undefined
|
|
394
|
+
var onKey = function (ev) {
|
|
395
|
+
if (ev.key === 'Escape') { ev.stopPropagation(); setOpen(false) }
|
|
396
|
+
}
|
|
397
|
+
document.addEventListener('keydown', onKey)
|
|
398
|
+
var timer = setTimeout(function () { if (closeRef.current) closeRef.current.focus() }, 0)
|
|
399
|
+
return function () {
|
|
400
|
+
document.removeEventListener('keydown', onKey)
|
|
401
|
+
clearTimeout(timer)
|
|
402
|
+
}
|
|
403
|
+
}, [open])
|
|
404
|
+
if (!open) return null
|
|
405
|
+
return h('div', {
|
|
406
|
+
style: {
|
|
407
|
+
position: 'absolute', inset: 0, zIndex: 10,
|
|
408
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
409
|
+
background: 'rgba(0,0,0,0.45)',
|
|
410
|
+
},
|
|
411
|
+
onClick: function () { setOpen(false) },
|
|
412
|
+
},
|
|
413
|
+
h('div', {
|
|
414
|
+
role: 'dialog', 'aria-modal': 'true', 'aria-label': t('title'),
|
|
415
|
+
style: {
|
|
416
|
+
width: 'min(680px, calc(100vw - 64px))', maxHeight: 'calc(100vh - 96px)',
|
|
417
|
+
display: 'flex', flexDirection: 'column',
|
|
418
|
+
background: 'var(--dsw-alias-bg-overlay)',
|
|
419
|
+
border: '1px solid var(--dsw-alias-border-l1)',
|
|
420
|
+
borderRadius: 14, boxShadow: '0 18px 48px rgba(0,0,0,0.35)',
|
|
421
|
+
overflow: 'hidden',
|
|
422
|
+
},
|
|
423
|
+
onClick: function (ev) { ev.stopPropagation() },
|
|
424
|
+
},
|
|
425
|
+
// Header
|
|
426
|
+
h('div', {
|
|
427
|
+
style: {
|
|
428
|
+
display: 'flex', alignItems: 'center', gap: 10, padding: '14px 16px',
|
|
429
|
+
borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: 'none',
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
h(RemoteIcon, { size: 18 }),
|
|
433
|
+
h('span', { style: { fontSize: 15, fontWeight: 700, color: 'var(--dsw-alias-label-primary)' } }, t('title')),
|
|
434
|
+
h('span', { style: { flex: 1 } }),
|
|
435
|
+
h('button', {
|
|
436
|
+
type: 'button', ref: closeRef, 'aria-label': t('close'), title: t('close'),
|
|
437
|
+
onClick: function () { setOpen(false) },
|
|
438
|
+
style: {
|
|
439
|
+
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
440
|
+
width: 28, height: 28, borderRadius: 8, border: '1px solid var(--dsw-alias-border-l1)',
|
|
441
|
+
background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
|
|
442
|
+
cursor: 'pointer', fontSize: 14,
|
|
443
|
+
},
|
|
444
|
+
}, '✕'),
|
|
445
|
+
),
|
|
446
|
+
// Body
|
|
447
|
+
h('div', { style: { overflowY: 'auto', minHeight: 0 } },
|
|
448
|
+
h(OwnerPanel),
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
name: '@skillpie/dsh-remote',
|
|
456
|
+
inject: ['slots', 'layout', 'locale'],
|
|
457
|
+
apply: function (ctx) {
|
|
458
|
+
ctx.effect(function () {
|
|
459
|
+
return ctx.locale.register(NS, { zh: zh, en: en })
|
|
460
|
+
}, 'dsh-remote: dictionaries')
|
|
461
|
+
|
|
462
|
+
pageT = (ctx.locale && typeof ctx.locale.bind === 'function')
|
|
463
|
+
? ctx.locale.bind(NS)
|
|
464
|
+
: function (key) { return (zh[key] != null ? zh[key] : (en[key] != null ? en[key] : key)) }
|
|
465
|
+
|
|
466
|
+
// Sidebar foot: compact action button beside Settings.
|
|
467
|
+
// 经中转透传访问的访客页(relay 注入 __DSH_REMOTE_VISITOR__)不挂远程入口:
|
|
468
|
+
// 属主 API 对访客是跨源拒绝的,面板对他们没有用处,只有桌面端展示。
|
|
469
|
+
if (window.__DSH_REMOTE_VISITOR__ === true) return
|
|
470
|
+
|
|
471
|
+
ctx.slots.inject('sidebar.footer.action', function () {
|
|
472
|
+
return ctx.slots.register({
|
|
473
|
+
name: 'sidebar.footer.action',
|
|
474
|
+
id: 'dsh-remote-footer',
|
|
475
|
+
order: 10,
|
|
476
|
+
locale: NS,
|
|
477
|
+
label: function () { return pageT('title') },
|
|
478
|
+
}, FooterAction)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
// Application-level floating layer: the dialog itself.
|
|
482
|
+
ctx.slots.inject('shell.overlay', function () {
|
|
483
|
+
return ctx.slots.register({
|
|
484
|
+
name: 'shell.overlay',
|
|
485
|
+
id: 'dsh-remote-overlay',
|
|
486
|
+
order: 20,
|
|
487
|
+
locale: NS,
|
|
488
|
+
}, RemotePanelDialog)
|
|
489
|
+
})
|
|
490
|
+
},
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
})
|