@skillpie/dsh-remote 0.0.0-stage → 1.1.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 +95 -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/file-route.js +159 -0
- package/lib/index.js +496 -0
- package/lib/pipe.js +83 -0
- package/lib/protocol.js +122 -0
- package/lib/relay-core.js +1025 -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/console.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-remote — visitor console page (served by the relay face and by local
|
|
3
|
+
* direct mode). Single self-contained HTML document: no external assets, no
|
|
4
|
+
* build step. The 8-digit code comes from the URL path; all traffic rides
|
|
5
|
+
* `/<code>/ws`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CSS = `
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--bg: #f6f7f9; --bg2: #ffffff; --fg: #1a1c1f; --fg2: #6b7280;
|
|
12
|
+
--line: #e2e5ea; --accent: #4d6bfe; --accentfg: #ffffff;
|
|
13
|
+
--user-bg: #4d6bfe; --user-fg: #ffffff; --ok: #16a34a; --warn: #d97706; --bad: #dc2626;
|
|
14
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-color-scheme: dark) {
|
|
17
|
+
:root {
|
|
18
|
+
--bg: #101114; --bg2: #17191d; --fg: #e8eaed; --fg2: #9aa0a6;
|
|
19
|
+
--line: #2a2d33; --accent: #6b84ff; --accentfg: #0e1116;
|
|
20
|
+
--user-bg: #4d6bfe; --user-fg: #ffffff; --ok: #4ade80; --warn: #fbbf24; --bad: #f87171;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
* { box-sizing: border-box; }
|
|
24
|
+
html, body { height: 100%; }
|
|
25
|
+
body {
|
|
26
|
+
margin: 0; background: var(--bg); color: var(--fg);
|
|
27
|
+
font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
28
|
+
display: flex; flex-direction: column;
|
|
29
|
+
}
|
|
30
|
+
header {
|
|
31
|
+
display: flex; align-items: center; gap: 10px; padding: 10px 16px;
|
|
32
|
+
border-bottom: 1px solid var(--line); background: var(--bg2); flex: none;
|
|
33
|
+
}
|
|
34
|
+
header .brand { font-weight: 700; font-size: 13px; display: flex; align-items: center; gap: 8px; }
|
|
35
|
+
header .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--bad); flex: none; }
|
|
36
|
+
header .dot.on { background: var(--ok); }
|
|
37
|
+
header .host { color: var(--fg2); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
38
|
+
header .code { font-family: var(--mono); font-size: 12px; color: var(--fg2); margin-left: auto; flex: none; }
|
|
39
|
+
main { flex: 1; display: flex; min-height: 0; }
|
|
40
|
+
aside {
|
|
41
|
+
width: 232px; flex: none; border-right: 1px solid var(--line); background: var(--bg2);
|
|
42
|
+
display: flex; flex-direction: column; min-height: 0;
|
|
43
|
+
}
|
|
44
|
+
@media (max-width: 720px) { aside { display: none; } }
|
|
45
|
+
aside .head {
|
|
46
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
47
|
+
padding: 10px 12px 6px; color: var(--fg2); font-size: 12px; flex: none;
|
|
48
|
+
}
|
|
49
|
+
aside .new {
|
|
50
|
+
border: 1px solid var(--line); background: transparent; color: var(--fg);
|
|
51
|
+
border-radius: 6px; padding: 2px 8px; font-size: 12px; cursor: pointer;
|
|
52
|
+
}
|
|
53
|
+
aside .new:hover { border-color: var(--accent); color: var(--accent); }
|
|
54
|
+
aside .list { overflow-y: auto; flex: 1; padding: 4px 6px 10px; }
|
|
55
|
+
.session {
|
|
56
|
+
padding: 7px 9px; border-radius: 8px; cursor: pointer; margin-bottom: 2px;
|
|
57
|
+
display: flex; align-items: center; gap: 8px; min-width: 0;
|
|
58
|
+
}
|
|
59
|
+
.session:hover { background: var(--bg); }
|
|
60
|
+
.session.active { background: var(--bg); outline: 1px solid var(--line); }
|
|
61
|
+
.session .rdot { width: 7px; height: 7px; border-radius: 50%; background: var(--line); flex: none; }
|
|
62
|
+
.session .rdot.run { background: var(--ok); animation: pulse 1.4s ease-in-out infinite; }
|
|
63
|
+
@keyframes pulse { 50% { opacity: 0.35; } }
|
|
64
|
+
.session .title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
|
65
|
+
.session .time { color: var(--fg2); font-size: 11px; flex: none; }
|
|
66
|
+
section.chat { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
67
|
+
#log { flex: 1; overflow-y: auto; padding: 18px 18px 8px; display: flex; flex-direction: column; gap: 10px; }
|
|
68
|
+
.row { display: flex; }
|
|
69
|
+
.row.user { justify-content: flex-end; }
|
|
70
|
+
.bubble {
|
|
71
|
+
max-width: min(76%, 720px); padding: 8px 12px; border-radius: 12px;
|
|
72
|
+
white-space: pre-wrap; word-break: break-word; font-size: 14px;
|
|
73
|
+
}
|
|
74
|
+
.row.user .bubble { background: var(--user-bg); color: var(--user-fg); border-bottom-right-radius: 4px; }
|
|
75
|
+
.row.assistant .bubble { background: var(--bg2); border: 1px solid var(--line); border-bottom-left-radius: 4px; }
|
|
76
|
+
.row.assistant .bubble.thinking { color: var(--fg2); font-style: italic; }
|
|
77
|
+
.row.tool { justify-content: flex-start; }
|
|
78
|
+
.toolchip {
|
|
79
|
+
font-family: var(--mono); font-size: 11.5px; color: var(--fg2);
|
|
80
|
+
background: var(--bg2); border: 1px solid var(--line); border-radius: 7px; padding: 2px 9px;
|
|
81
|
+
max-width: min(76%, 720px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
82
|
+
}
|
|
83
|
+
.meta { font-size: 11px; color: var(--fg2); margin: 0 6px; }
|
|
84
|
+
.system {
|
|
85
|
+
text-align: center; color: var(--fg2); font-size: 12px; padding: 6px 0;
|
|
86
|
+
}
|
|
87
|
+
.statusbar {
|
|
88
|
+
display: flex; align-items: center; gap: 10px; padding: 4px 18px 0; font-size: 12px; color: var(--fg2); flex: none; min-height: 22px;
|
|
89
|
+
}
|
|
90
|
+
.statusbar .pending { color: var(--warn); }
|
|
91
|
+
.statusbar .spacer { flex: 1; }
|
|
92
|
+
footer { padding: 10px 18px 14px; flex: none; }
|
|
93
|
+
.composer {
|
|
94
|
+
display: flex; gap: 8px; align-items: flex-end;
|
|
95
|
+
background: var(--bg2); border: 1px solid var(--line); border-radius: 12px; padding: 8px 10px;
|
|
96
|
+
}
|
|
97
|
+
.composer:focus-within { border-color: var(--accent); }
|
|
98
|
+
textarea {
|
|
99
|
+
flex: 1; border: none; outline: none; resize: none; background: transparent; color: var(--fg);
|
|
100
|
+
font: inherit; max-height: 160px; min-height: 22px; line-height: 1.5;
|
|
101
|
+
}
|
|
102
|
+
button.go {
|
|
103
|
+
border: none; background: var(--accent); color: var(--accentfg); border-radius: 9px;
|
|
104
|
+
padding: 6px 16px; font-size: 13px; cursor: pointer; flex: none;
|
|
105
|
+
}
|
|
106
|
+
button.go:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
107
|
+
button.stop {
|
|
108
|
+
border: 1px solid var(--bad); background: transparent; color: var(--bad); border-radius: 9px;
|
|
109
|
+
padding: 5px 14px; font-size: 13px; cursor: pointer; flex: none;
|
|
110
|
+
}
|
|
111
|
+
.notice { color: var(--fg2); font-size: 12px; margin-top: 6px; text-align: center; }
|
|
112
|
+
.offline-wrap { flex: 1; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 10px; color: var(--fg2); }
|
|
113
|
+
.offline-wrap .big { font-size: 16px; color: var(--fg); }
|
|
114
|
+
.offline-wrap .hint { font-size: 12.5px; }
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
function pageBody(code, offline) {
|
|
118
|
+
if (offline) {
|
|
119
|
+
return `<div class="offline-wrap">
|
|
120
|
+
<div class="big">宿主机暂时离线</div>
|
|
121
|
+
<div class="hint">远程码 <b style="font-family:var(--mono)">${code}</b> 有效,但宿主机尚未连上中转站。页面会自动重试。</div>
|
|
122
|
+
</div>
|
|
123
|
+
<script>setTimeout(function(){ location.reload() }, 10000)</script>`
|
|
124
|
+
}
|
|
125
|
+
return `
|
|
126
|
+
<header>
|
|
127
|
+
<span class="brand"><span class="dot" id="conn-dot"></span>远程控制</span>
|
|
128
|
+
<span class="host" id="host-name">连接中…</span>
|
|
129
|
+
<span class="code">远程码 ${code}</span>
|
|
130
|
+
</header>
|
|
131
|
+
<main>
|
|
132
|
+
<aside>
|
|
133
|
+
<div class="head"><span>会话</span><button class="new" id="btn-new" title="新建会话">+ 新会话</button></div>
|
|
134
|
+
<div class="list" id="sessions"></div>
|
|
135
|
+
</aside>
|
|
136
|
+
<section class="chat">
|
|
137
|
+
<div id="log"></div>
|
|
138
|
+
<div class="statusbar">
|
|
139
|
+
<span id="run-state"></span>
|
|
140
|
+
<span class="pending" id="pending"></span>
|
|
141
|
+
<span class="spacer"></span>
|
|
142
|
+
<span id="observing"></span>
|
|
143
|
+
</div>
|
|
144
|
+
<footer>
|
|
145
|
+
<div class="composer">
|
|
146
|
+
<textarea id="input" rows="1" placeholder="输入指令,Enter 发送,Shift+Enter 换行"></textarea>
|
|
147
|
+
<button class="stop" id="btn-stop" style="display:none">中断</button>
|
|
148
|
+
<button class="go" id="btn-send">发送</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="notice" id="notice"></div>
|
|
151
|
+
</footer>
|
|
152
|
+
</section>
|
|
153
|
+
</main>`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const SCRIPT = `
|
|
157
|
+
'use strict'
|
|
158
|
+
var CODE = location.pathname.replace(/^\\/+/, '').split('/')[0]
|
|
159
|
+
if (!/^\\d{8}$/.test(CODE)) CODE = null
|
|
160
|
+
var ws = null
|
|
161
|
+
var nextId = 1
|
|
162
|
+
var pendingReqs = new Map()
|
|
163
|
+
var sessions = []
|
|
164
|
+
var currentSession = null
|
|
165
|
+
var allowControl = false
|
|
166
|
+
var hostName = ''
|
|
167
|
+
var running = false
|
|
168
|
+
var queued = 0
|
|
169
|
+
var pendingInput = []
|
|
170
|
+
|
|
171
|
+
function $(id) { return document.getElementById(id) }
|
|
172
|
+
function send(cmd, p) {
|
|
173
|
+
return new Promise(function (resolve, reject) {
|
|
174
|
+
if (!ws || ws.readyState !== 1) return reject(new Error('未连接'))
|
|
175
|
+
var id = 'r' + nextId++
|
|
176
|
+
pendingReqs.set(id, { resolve: resolve, reject: reject })
|
|
177
|
+
ws.send(JSON.stringify({ t: 'cmd', id: id, cmd: cmd, p: p || {} }))
|
|
178
|
+
setTimeout(function () {
|
|
179
|
+
if (pendingReqs.has(id)) { pendingReqs.delete(id); reject(new Error('响应超时')) }
|
|
180
|
+
}, 25000)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
function connect() {
|
|
184
|
+
var proto = location.protocol === 'https:' ? 'wss://' : 'ws://'
|
|
185
|
+
ws = new WebSocket(proto + location.host + '/' + CODE + '/ws')
|
|
186
|
+
ws.onopen = function () { setConn(true) }
|
|
187
|
+
ws.onclose = function () {
|
|
188
|
+
setConn(false)
|
|
189
|
+
setTimeout(connect, 3000)
|
|
190
|
+
}
|
|
191
|
+
ws.onmessage = function (ev) {
|
|
192
|
+
var frame
|
|
193
|
+
try { frame = JSON.parse(ev.data) } catch (e) { return }
|
|
194
|
+
if (frame.t === 'welcome') onWelcome(frame)
|
|
195
|
+
else if (frame.t === 'res') {
|
|
196
|
+
var entry = pendingReqs.get(frame.id)
|
|
197
|
+
if (!entry) return
|
|
198
|
+
pendingReqs.delete(frame.id)
|
|
199
|
+
if (frame.ok) entry.resolve(frame.data)
|
|
200
|
+
else entry.reject(new Error(frame.error || '命令失败'))
|
|
201
|
+
} else if (frame.t === 'push') onPush(frame.f)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function setConn(on) {
|
|
205
|
+
$('conn-dot').className = 'dot' + (on ? ' on' : '')
|
|
206
|
+
if (!on) $('host-name').textContent = '连接断开,重试中…'
|
|
207
|
+
}
|
|
208
|
+
function onWelcome(frame) {
|
|
209
|
+
if (frame.error) { $('notice').textContent = frame.error; return }
|
|
210
|
+
allowControl = !!(frame.host && frame.host.allowControl)
|
|
211
|
+
hostName = (frame.host && frame.host.name) || ''
|
|
212
|
+
$('host-name').textContent = hostName + '(可控)'
|
|
213
|
+
if (!allowControl) $('host-name').textContent = hostName + '(只读)'
|
|
214
|
+
$('btn-new').style.display = allowControl ? '' : 'none'
|
|
215
|
+
applySessions(frame.sessions || [])
|
|
216
|
+
if (!currentSession && sessions.length) observeSession(sessions[0].id)
|
|
217
|
+
if (!sessions.length) $('log').innerHTML = '<div class="system">宿主机还没有会话,点「+ 新会话」开始</div>'
|
|
218
|
+
}
|
|
219
|
+
function applySessions(list) {
|
|
220
|
+
sessions = list || []
|
|
221
|
+
renderSessions()
|
|
222
|
+
}
|
|
223
|
+
function renderSessions() {
|
|
224
|
+
var wrap = $('sessions')
|
|
225
|
+
wrap.innerHTML = ''
|
|
226
|
+
sessions.forEach(function (s) {
|
|
227
|
+
var div = document.createElement('div')
|
|
228
|
+
div.className = 'session' + (s.id === currentSession ? ' active' : '')
|
|
229
|
+
var dot = document.createElement('span')
|
|
230
|
+
dot.className = 'rdot' + (s.running ? ' run' : '')
|
|
231
|
+
var title = document.createElement('span')
|
|
232
|
+
title.className = 'title'
|
|
233
|
+
title.textContent = s.title || '(未命名会话)'
|
|
234
|
+
var time = document.createElement('span')
|
|
235
|
+
time.className = 'time'
|
|
236
|
+
time.textContent = fmtTime(s.updatedAt)
|
|
237
|
+
div.appendChild(dot); div.appendChild(title); div.appendChild(time)
|
|
238
|
+
div.onclick = function () { observeSession(s.id) }
|
|
239
|
+
wrap.appendChild(div)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
function fmtTime(ms) {
|
|
243
|
+
if (!ms) return ''
|
|
244
|
+
var d = new Date(ms)
|
|
245
|
+
var today = new Date(); today.setHours(0, 0, 0, 0)
|
|
246
|
+
var hm = d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0')
|
|
247
|
+
return d.getTime() < today.getTime() ? (d.getMonth() + 1) + '/' + d.getDate() + ' ' + hm : hm
|
|
248
|
+
}
|
|
249
|
+
function observeSession(id) {
|
|
250
|
+
currentSession = id
|
|
251
|
+
$('log').innerHTML = '<div class="system">加载会话…</div>'
|
|
252
|
+
send('sessions.observe', { sessionId: id }).then(function () { renderSessions() })
|
|
253
|
+
.catch(function (err) { $('log').innerHTML = '<div class="system">' + esc(err.message) + '</div>' })
|
|
254
|
+
renderSessions()
|
|
255
|
+
}
|
|
256
|
+
function onPush(f) {
|
|
257
|
+
if (!f) return
|
|
258
|
+
if (f.t === 'sessions-updated') { applySessions(f.sessions); return }
|
|
259
|
+
if (currentSession && f.sessionId !== currentSession) return
|
|
260
|
+
if (f.t === 'session-history') {
|
|
261
|
+
$('log').innerHTML = ''
|
|
262
|
+
foldRecords(f.records || [])
|
|
263
|
+
applyState(f.state || {})
|
|
264
|
+
autoscroll(true)
|
|
265
|
+
} else if (f.t === 'session-event') {
|
|
266
|
+
foldRecords([f.event])
|
|
267
|
+
autoscroll(false)
|
|
268
|
+
} else if (f.t === 'session-state') {
|
|
269
|
+
applyState(f.state || {})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function applyState(state) {
|
|
273
|
+
running = !!state.running
|
|
274
|
+
queued = state.queued || 0
|
|
275
|
+
pendingInput = state.pendingInput || []
|
|
276
|
+
$('btn-stop').style.display = running && allowControl ? '' : 'none'
|
|
277
|
+
$('btn-send').disabled = !allowControl
|
|
278
|
+
$('input').disabled = !allowControl
|
|
279
|
+
var bits = []
|
|
280
|
+
if (running) bits.push('运行中' + (queued ? '(排队 ' + queued + ')' : ''))
|
|
281
|
+
$('run-state').textContent = bits.join(' ')
|
|
282
|
+
var pend = pendingInput.length ? '待处理:' + pendingInput.map(function (p) { return p === 'approval' ? '权限确认' : p === 'question' ? '提问' : '输入' }).join('、') + '(请在本机 GUI 处理)' : ''
|
|
283
|
+
$('pending').textContent = pend
|
|
284
|
+
$('observing').textContent = currentSession ? '正在查看 ' + short(currentSession) : ''
|
|
285
|
+
}
|
|
286
|
+
function short(id) { return id ? id.slice(0, 8) : '' }
|
|
287
|
+
function foldRecords(records) {
|
|
288
|
+
var log = $('log')
|
|
289
|
+
if (log.querySelector('.system') && records.length) log.innerHTML = ''
|
|
290
|
+
records.forEach(function (entry) {
|
|
291
|
+
var ev = entry && entry.event ? entry.event : entry
|
|
292
|
+
if (!ev || typeof ev.type !== 'string') return
|
|
293
|
+
var data = ev.data || {}
|
|
294
|
+
if (ev.type === 'user/message') {
|
|
295
|
+
var src = data.source || {}
|
|
296
|
+
if (src.kind && src.kind !== 'user') return
|
|
297
|
+
var text = partsText(data.content)
|
|
298
|
+
if (!text) return
|
|
299
|
+
addRow('user', text, ev.time)
|
|
300
|
+
} else if (ev.type === 'assistant/message') {
|
|
301
|
+
var atext = streamText(data.stream)
|
|
302
|
+
if (!atext) atext = partsText(data.content)
|
|
303
|
+
if (!atext) return
|
|
304
|
+
addRow('assistant', atext, ev.time)
|
|
305
|
+
} else if (ev.type === 'tool/call') {
|
|
306
|
+
addTool(data.name || 'tool', data.arguments)
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
function partsText(content) {
|
|
311
|
+
if (!Array.isArray(content)) return ''
|
|
312
|
+
return content.filter(function (p) { return p && p.type === 'text' && p.text })
|
|
313
|
+
.map(function (p) { return p.text }).join('\\n').trim()
|
|
314
|
+
}
|
|
315
|
+
function streamText(stream) {
|
|
316
|
+
if (!Array.isArray(stream)) return ''
|
|
317
|
+
var out = ''
|
|
318
|
+
stream.forEach(function (chunk) {
|
|
319
|
+
if (chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string') out += chunk.text
|
|
320
|
+
})
|
|
321
|
+
return out.trim()
|
|
322
|
+
}
|
|
323
|
+
function addRow(kind, text, time) {
|
|
324
|
+
var log = $('log')
|
|
325
|
+
var row = document.createElement('div')
|
|
326
|
+
row.className = 'row ' + kind
|
|
327
|
+
var bubble = document.createElement('div')
|
|
328
|
+
bubble.className = 'bubble' + (kind === 'assistant' && !text ? ' thinking' : '')
|
|
329
|
+
bubble.textContent = text || '…'
|
|
330
|
+
row.appendChild(bubble)
|
|
331
|
+
if (time) {
|
|
332
|
+
var meta = document.createElement('span')
|
|
333
|
+
meta.className = 'meta'
|
|
334
|
+
meta.textContent = fmtTime(time)
|
|
335
|
+
row.appendChild(meta)
|
|
336
|
+
}
|
|
337
|
+
log.appendChild(row)
|
|
338
|
+
autoscroll(false)
|
|
339
|
+
}
|
|
340
|
+
function addTool(name, args) {
|
|
341
|
+
var log = $('log')
|
|
342
|
+
var row = document.createElement('div')
|
|
343
|
+
row.className = 'row tool'
|
|
344
|
+
var chip = document.createElement('div')
|
|
345
|
+
chip.className = 'toolchip'
|
|
346
|
+
var detail = ''
|
|
347
|
+
try {
|
|
348
|
+
if (args && typeof args === 'object') {
|
|
349
|
+
var keys = Object.keys(args)
|
|
350
|
+
if (keys.length) detail = ' ' + JSON.stringify(args[keys[0]]).slice(0, 60)
|
|
351
|
+
} else if (typeof args === 'string') detail = ' ' + args.slice(0, 60)
|
|
352
|
+
} catch (e) { /* display only */ }
|
|
353
|
+
chip.textContent = '⚙ ' + name + detail
|
|
354
|
+
row.appendChild(chip)
|
|
355
|
+
log.appendChild(row)
|
|
356
|
+
autoscroll(false)
|
|
357
|
+
}
|
|
358
|
+
var stick = true
|
|
359
|
+
$('log').addEventListener('scroll', function () {
|
|
360
|
+
var el = $('log')
|
|
361
|
+
stick = el.scrollTop + el.clientHeight >= el.scrollHeight - 40
|
|
362
|
+
})
|
|
363
|
+
function autoscroll(force) {
|
|
364
|
+
if (!force && !stick) return
|
|
365
|
+
var el = $('log')
|
|
366
|
+
el.scrollTop = el.scrollHeight
|
|
367
|
+
}
|
|
368
|
+
function doSend() {
|
|
369
|
+
var input = $('input')
|
|
370
|
+
var text = input.value.trim()
|
|
371
|
+
if (!text || !currentSession || !allowControl) return
|
|
372
|
+
input.value = ''
|
|
373
|
+
autoGrow()
|
|
374
|
+
send('sessions.send', { sessionId: currentSession, text: text })
|
|
375
|
+
.then(function () { addRow('user', text, Date.now()); autoscroll(true) })
|
|
376
|
+
.catch(function (err) { $('notice').textContent = '发送失败:' + err.message })
|
|
377
|
+
}
|
|
378
|
+
function doStop() {
|
|
379
|
+
if (!currentSession) return
|
|
380
|
+
send('sessions.cancel', { sessionId: currentSession })
|
|
381
|
+
.catch(function (err) { $('notice').textContent = '中断失败:' + err.message })
|
|
382
|
+
}
|
|
383
|
+
function doNew() {
|
|
384
|
+
if (!allowControl) return
|
|
385
|
+
send('sessions.create', {}).then(function (data) {
|
|
386
|
+
applySessionsRefresh(function () { observeSession(data.sessionId) })
|
|
387
|
+
}).catch(function (err) { $('notice').textContent = '新建失败:' + err.message })
|
|
388
|
+
}
|
|
389
|
+
function applySessionsRefresh(next) {
|
|
390
|
+
send('sessions.list', {}).then(function (data) {
|
|
391
|
+
applySessions(data.sessions)
|
|
392
|
+
next()
|
|
393
|
+
}).catch(next)
|
|
394
|
+
}
|
|
395
|
+
function autoGrow() {
|
|
396
|
+
var el = $('input')
|
|
397
|
+
el.style.height = 'auto'
|
|
398
|
+
el.style.height = Math.min(el.scrollHeight, 160) + 'px'
|
|
399
|
+
}
|
|
400
|
+
function esc(s) {
|
|
401
|
+
return String(s == null ? '' : s).replace(/[&<>\"]/, function (c) {
|
|
402
|
+
return { '&': '&', '<': '<', '>': '>', '\"': '"' }[c]
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
if (!CODE) {
|
|
406
|
+
document.body.innerHTML = '<div class="offline-wrap"><div class="big">无效的远程码</div></div>'
|
|
407
|
+
} else {
|
|
408
|
+
$('btn-send').onclick = doSend
|
|
409
|
+
$('btn-stop').onclick = doStop
|
|
410
|
+
$('btn-new').onclick = doNew
|
|
411
|
+
$('input').addEventListener('keydown', function (ev) {
|
|
412
|
+
if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); doSend() }
|
|
413
|
+
})
|
|
414
|
+
$('input').addEventListener('input', autoGrow)
|
|
415
|
+
connect()
|
|
416
|
+
}
|
|
417
|
+
`
|
|
418
|
+
|
|
419
|
+
export function consoleHtml({ code, offline }) {
|
|
420
|
+
return `<!DOCTYPE html>
|
|
421
|
+
<html lang="zh-CN">
|
|
422
|
+
<head>
|
|
423
|
+
<meta charset="utf-8">
|
|
424
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
425
|
+
<meta name="robots" content="noindex, nofollow">
|
|
426
|
+
<title>远程控制 ${code}</title>
|
|
427
|
+
<style>${CSS}</style>
|
|
428
|
+
</head>
|
|
429
|
+
<body>
|
|
430
|
+
${pageBody(code, offline === true)}
|
|
431
|
+
<script>${SCRIPT}</script>
|
|
432
|
+
</body>
|
|
433
|
+
</html>`
|
|
434
|
+
}
|