@wendongfly/myhi 1.0.30 → 1.0.31
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/dist/terminal.html +319 -143
- package/package.json +1 -1
package/dist/terminal.html
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
overflow: hidden;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
body {
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
#top-bar {
|
|
29
34
|
display: flex;
|
|
30
35
|
align-items: center;
|
|
@@ -65,16 +70,87 @@
|
|
|
65
70
|
}
|
|
66
71
|
.top-btn:hover { background: #30363d; }
|
|
67
72
|
|
|
73
|
+
/* ── Tab bar ── */
|
|
74
|
+
#tab-bar {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
background: #0d1117;
|
|
78
|
+
border-bottom: 1px solid #30363d;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
overflow-x: auto;
|
|
81
|
+
scrollbar-width: none;
|
|
82
|
+
-webkit-overflow-scrolling: touch;
|
|
83
|
+
}
|
|
84
|
+
#tab-bar::-webkit-scrollbar { display: none; }
|
|
85
|
+
|
|
86
|
+
.tab {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 0.4rem;
|
|
90
|
+
padding: 0.4rem 0.75rem;
|
|
91
|
+
font-size: 0.78rem;
|
|
92
|
+
color: #8b949e;
|
|
93
|
+
background: transparent;
|
|
94
|
+
border: none;
|
|
95
|
+
border-right: 1px solid #21262d;
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
flex-shrink: 0;
|
|
99
|
+
max-width: 160px;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
text-overflow: ellipsis;
|
|
102
|
+
position: relative;
|
|
103
|
+
}
|
|
104
|
+
.tab:hover { color: #e6edf3; background: #161b22; }
|
|
105
|
+
.tab.active {
|
|
106
|
+
color: #e6edf3;
|
|
107
|
+
background: #161b22;
|
|
108
|
+
border-bottom: 2px solid #7c3aed;
|
|
109
|
+
}
|
|
110
|
+
.tab .tab-close {
|
|
111
|
+
font-size: 0.7rem;
|
|
112
|
+
color: #6e7681;
|
|
113
|
+
border: none;
|
|
114
|
+
background: none;
|
|
115
|
+
cursor: pointer;
|
|
116
|
+
padding: 0 0.15rem;
|
|
117
|
+
border-radius: 3px;
|
|
118
|
+
line-height: 1;
|
|
119
|
+
}
|
|
120
|
+
.tab .tab-close:hover { color: #f85149; background: #21262d; }
|
|
121
|
+
.tab .tab-dot {
|
|
122
|
+
width: 6px; height: 6px;
|
|
123
|
+
border-radius: 50%;
|
|
124
|
+
background: #3fb950;
|
|
125
|
+
flex-shrink: 0;
|
|
126
|
+
}
|
|
127
|
+
.tab .tab-dot.dead { background: #f85149; }
|
|
128
|
+
|
|
129
|
+
#tab-add {
|
|
130
|
+
background: none;
|
|
131
|
+
border: none;
|
|
132
|
+
color: #6e7681;
|
|
133
|
+
font-size: 1.1rem;
|
|
134
|
+
padding: 0.3rem 0.6rem;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
flex-shrink: 0;
|
|
137
|
+
}
|
|
138
|
+
#tab-add:hover { color: #e6edf3; }
|
|
139
|
+
|
|
140
|
+
/* ── Terminal area ── */
|
|
68
141
|
#terminal-wrapper {
|
|
69
142
|
flex: 1;
|
|
70
143
|
overflow: hidden;
|
|
71
|
-
|
|
144
|
+
position: relative;
|
|
72
145
|
}
|
|
73
146
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
147
|
+
.term-pane {
|
|
148
|
+
position: absolute;
|
|
149
|
+
inset: 0;
|
|
150
|
+
padding: 4px;
|
|
151
|
+
display: none;
|
|
77
152
|
}
|
|
153
|
+
.term-pane.active { display: block; }
|
|
78
154
|
|
|
79
155
|
/* Mobile shortcut bar */
|
|
80
156
|
#shortcut-bar {
|
|
@@ -109,12 +185,6 @@
|
|
|
109
185
|
}
|
|
110
186
|
.sk:active { background: #30363d; transform: scale(0.95); }
|
|
111
187
|
|
|
112
|
-
/* Layout */
|
|
113
|
-
body {
|
|
114
|
-
display: flex;
|
|
115
|
-
flex-direction: column;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
188
|
/* QR modal */
|
|
119
189
|
#qr-modal {
|
|
120
190
|
display: none;
|
|
@@ -173,11 +243,13 @@
|
|
|
173
243
|
<button class="top-btn" id="logout-btn" onclick="doLogout()" style="display:none;color:#f0883e">退出</button>
|
|
174
244
|
</div>
|
|
175
245
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<
|
|
246
|
+
<!-- Tab bar -->
|
|
247
|
+
<div id="tab-bar">
|
|
248
|
+
<button id="tab-add" onclick="addTab()" title="新建标签页">+</button>
|
|
179
249
|
</div>
|
|
180
250
|
|
|
251
|
+
<div id="terminal-wrapper"></div>
|
|
252
|
+
|
|
181
253
|
<!-- Mobile shortcut bar -->
|
|
182
254
|
<div id="shortcut-bar">
|
|
183
255
|
<button class="sk" id="btn-photo" onclick="openCamera()" style="color:#7c3aed;border-color:#7c3aed33;background:#7c3aed11">📷</button>
|
|
@@ -217,127 +289,240 @@
|
|
|
217
289
|
fetch('/logout', { method: 'POST' }).then(() => { location.href = '/login'; });
|
|
218
290
|
}
|
|
219
291
|
|
|
220
|
-
//
|
|
292
|
+
// ── Tab 管理 ─────────────────────────────────────────────
|
|
293
|
+
const TERM_THEME = {
|
|
294
|
+
background: '#0d1117', foreground: '#e6edf3', cursor: '#58a6ff',
|
|
295
|
+
selectionBackground: '#264f78',
|
|
296
|
+
black: '#0d1117', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
|
|
297
|
+
blue: '#58a6ff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#e6edf3',
|
|
298
|
+
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364',
|
|
299
|
+
brightYellow: '#e3b341', brightBlue: '#79c0ff', brightMagenta: '#d2a8ff',
|
|
300
|
+
brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const tabs = []; // { id, sessionId, title, term, fitAddon, socket, pane, tabEl, alive }
|
|
304
|
+
let activeTabId = null;
|
|
305
|
+
|
|
306
|
+
const tabBar = document.getElementById('tab-bar');
|
|
307
|
+
const tabAddBtn = document.getElementById('tab-add');
|
|
308
|
+
const wrapper = document.getElementById('terminal-wrapper');
|
|
309
|
+
|
|
310
|
+
// 从 URL 获取初始会话 ID
|
|
221
311
|
const pathParts = location.pathname.split('/');
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
// ──
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
312
|
+
const INIT_SESSION_ID = pathParts[pathParts.length - 1];
|
|
313
|
+
|
|
314
|
+
// ── 创建 Tab ──
|
|
315
|
+
function createTab(sessionId) {
|
|
316
|
+
const id = Date.now() + '-' + Math.random().toString(36).slice(2, 6);
|
|
317
|
+
|
|
318
|
+
// 创建终端容器
|
|
319
|
+
const pane = document.createElement('div');
|
|
320
|
+
pane.className = 'term-pane';
|
|
321
|
+
wrapper.appendChild(pane);
|
|
322
|
+
|
|
323
|
+
// 创建 xterm
|
|
324
|
+
const term = new Terminal({
|
|
325
|
+
theme: TERM_THEME,
|
|
326
|
+
fontFamily: "'SF Mono', 'Cascadia Code', 'Consolas', 'Menlo', monospace",
|
|
327
|
+
fontSize: 14, lineHeight: 1.2, cursorBlink: true,
|
|
328
|
+
allowTransparency: false, scrollback: 5000,
|
|
329
|
+
});
|
|
330
|
+
const fitAddon = new FitAddon.FitAddon();
|
|
331
|
+
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
|
332
|
+
term.loadAddon(fitAddon);
|
|
333
|
+
term.loadAddon(webLinksAddon);
|
|
334
|
+
term.open(pane);
|
|
335
|
+
|
|
336
|
+
// 创建 Socket 连接
|
|
337
|
+
const socket = io({ transports: ['websocket'] });
|
|
338
|
+
|
|
339
|
+
const tab = { id, sessionId, title: '连接中...', term, fitAddon, socket, pane, tabEl: null, alive: true };
|
|
340
|
+
tabs.push(tab);
|
|
341
|
+
|
|
342
|
+
// Tab 按钮
|
|
343
|
+
renderTab(tab);
|
|
344
|
+
|
|
345
|
+
// Socket 事件
|
|
346
|
+
socket.on('connect', () => {
|
|
347
|
+
socket.emit('join', sessionId);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
socket.on('joined', (session) => {
|
|
351
|
+
tab.title = session.title || 'shell';
|
|
352
|
+
tab.alive = session.alive !== false;
|
|
353
|
+
renderTab(tab);
|
|
354
|
+
if (activeTabId === id) {
|
|
355
|
+
updateTopBar(tab);
|
|
356
|
+
fitTab(tab);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
socket.on('output', (data) => {
|
|
361
|
+
term.write(data);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
socket.on('session-exit', ({ code }) => {
|
|
365
|
+
tab.alive = false;
|
|
366
|
+
tab.title = (tab.title || 'shell') + ' (已退出)';
|
|
367
|
+
renderTab(tab);
|
|
368
|
+
if (activeTabId === id) updateTopBar(tab);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
socket.on('sessions', (sessions) => {
|
|
372
|
+
const s = sessions.find(x => x.id === sessionId);
|
|
373
|
+
if (s && activeTabId === id) {
|
|
374
|
+
document.getElementById('viewer-count').textContent = s.viewers > 1 ? `👁 ${s.viewers}` : '';
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
socket.on('disconnect', () => {
|
|
379
|
+
// 自动重连由 socket.io 处理
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// 终端输入
|
|
383
|
+
term.onData((data) => {
|
|
384
|
+
socket.emit('input', data);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// 点击终端获取焦点
|
|
388
|
+
pane.addEventListener('click', () => term.focus());
|
|
255
389
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
390
|
+
return tab;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// ── 渲染 Tab 按钮 ──
|
|
394
|
+
function renderTab(tab) {
|
|
395
|
+
if (tab.tabEl) tab.tabEl.remove();
|
|
396
|
+
|
|
397
|
+
const el = document.createElement('div');
|
|
398
|
+
el.className = 'tab' + (activeTabId === tab.id ? ' active' : '');
|
|
399
|
+
el.innerHTML = `<span class="tab-dot${tab.alive ? '' : ' dead'}"></span>` +
|
|
400
|
+
`<span style="overflow:hidden;text-overflow:ellipsis">${esc(tab.title)}</span>` +
|
|
401
|
+
`<button class="tab-close" title="关闭">×</button>`;
|
|
402
|
+
|
|
403
|
+
el.addEventListener('click', (e) => {
|
|
404
|
+
if (e.target.classList.contains('tab-close')) {
|
|
405
|
+
closeTab(tab.id);
|
|
406
|
+
} else {
|
|
407
|
+
switchTab(tab.id);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
261
410
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
411
|
+
tab.tabEl = el;
|
|
412
|
+
tabBar.insertBefore(el, tabAddBtn);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── 切换 Tab ──
|
|
416
|
+
function switchTab(id) {
|
|
417
|
+
activeTabId = id;
|
|
418
|
+
tabs.forEach(t => {
|
|
419
|
+
const isActive = t.id === id;
|
|
420
|
+
t.pane.classList.toggle('active', isActive);
|
|
421
|
+
if (t.tabEl) t.tabEl.classList.toggle('active', isActive);
|
|
422
|
+
});
|
|
423
|
+
const tab = tabs.find(t => t.id === id);
|
|
424
|
+
if (tab) {
|
|
425
|
+
updateTopBar(tab);
|
|
426
|
+
fitTab(tab);
|
|
427
|
+
tab.term.focus();
|
|
266
428
|
}
|
|
267
429
|
}
|
|
268
430
|
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
431
|
+
// ── 关闭 Tab ──
|
|
432
|
+
function closeTab(id) {
|
|
433
|
+
const idx = tabs.findIndex(t => t.id === id);
|
|
434
|
+
if (idx === -1) return;
|
|
435
|
+
const tab = tabs[idx];
|
|
436
|
+
|
|
437
|
+
// 断开连接
|
|
438
|
+
tab.socket.disconnect();
|
|
439
|
+
tab.term.dispose();
|
|
440
|
+
tab.pane.remove();
|
|
441
|
+
if (tab.tabEl) tab.tabEl.remove();
|
|
442
|
+
tabs.splice(idx, 1);
|
|
443
|
+
|
|
444
|
+
if (tabs.length === 0) {
|
|
445
|
+
window.location.href = '/';
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// 切换到相邻 tab
|
|
450
|
+
if (activeTabId === id) {
|
|
451
|
+
const next = tabs[Math.min(idx, tabs.length - 1)];
|
|
452
|
+
switchTab(next.id);
|
|
289
453
|
}
|
|
290
454
|
}
|
|
291
455
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
456
|
+
// ── 新建 Tab(弹出会话选择)──
|
|
457
|
+
async function addTab() {
|
|
458
|
+
try {
|
|
459
|
+
const res = await fetch('/api/sessions');
|
|
460
|
+
const sessions = await res.json();
|
|
461
|
+
const alive = sessions.filter(s => s.alive && s.mode !== 'agent');
|
|
296
462
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
463
|
+
if (alive.length === 0) {
|
|
464
|
+
alert('没有可用的终端会话,请先在首页创建');
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
300
467
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
468
|
+
// 过滤掉已打开的会话
|
|
469
|
+
const opened = new Set(tabs.map(t => t.sessionId));
|
|
470
|
+
const available = alive.filter(s => !opened.has(s.id));
|
|
304
471
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
document.getElementById('session-title').textContent = session.title;
|
|
310
|
-
updateViewers(session.viewers);
|
|
311
|
-
fitTerm();
|
|
312
|
-
});
|
|
472
|
+
if (available.length === 0) {
|
|
473
|
+
alert('所有会话已在标签页中打开');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
313
476
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
477
|
+
if (available.length === 1) {
|
|
478
|
+
// 只有一个,直接打开
|
|
479
|
+
const tab = createTab(available[0].id);
|
|
480
|
+
switchTab(tab.id);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
317
483
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
484
|
+
// 多个可选:简单弹窗选择
|
|
485
|
+
const names = available.map((s, i) => `${i + 1}. ${s.title}`).join('\n');
|
|
486
|
+
const choice = prompt(`选择要打开的会话:\n\n${names}\n\n输入编号:`);
|
|
487
|
+
if (!choice) return;
|
|
488
|
+
const n = parseInt(choice, 10) - 1;
|
|
489
|
+
if (n >= 0 && n < available.length) {
|
|
490
|
+
const tab = createTab(available[n].id);
|
|
491
|
+
switchTab(tab.id);
|
|
492
|
+
}
|
|
493
|
+
} catch (e) {
|
|
494
|
+
alert('获取会话列表失败: ' + e.message);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
324
497
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
498
|
+
// ── 辅助函数 ──
|
|
499
|
+
function fitTab(tab) {
|
|
500
|
+
requestAnimationFrame(() => {
|
|
501
|
+
try { tab.fitAddon.fit(); } catch {}
|
|
502
|
+
if (tab.socket.connected) {
|
|
503
|
+
tab.socket.emit('resize', { cols: tab.term.cols, rows: tab.term.rows });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
329
507
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
508
|
+
function updateTopBar(tab) {
|
|
509
|
+
document.getElementById('session-title').textContent = tab.title;
|
|
510
|
+
document.title = tab.title + ' — myhi';
|
|
511
|
+
}
|
|
333
512
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
513
|
+
function esc(s) {
|
|
514
|
+
const d = document.createElement('div');
|
|
515
|
+
d.textContent = s;
|
|
516
|
+
return d.innerHTML;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// 窗口 resize 时 fit 当前 tab
|
|
520
|
+
window.addEventListener('resize', () => {
|
|
521
|
+
const tab = tabs.find(t => t.id === activeTabId);
|
|
522
|
+
if (tab) fitTab(tab);
|
|
337
523
|
});
|
|
338
524
|
|
|
339
|
-
// ──
|
|
340
|
-
// Map data-send keys to actual byte sequences
|
|
525
|
+
// ── 快捷键发送到当前 tab ──
|
|
341
526
|
const SEQ = {
|
|
342
527
|
'ctrl-c': '\x03', 'ctrl-d': '\x04', 'tab': '\t',
|
|
343
528
|
'up': '\x1b[A', 'down': '\x1b[B', 'right': '\x1b[C', 'left': '\x1b[D',
|
|
@@ -348,21 +533,21 @@
|
|
|
348
533
|
|
|
349
534
|
document.querySelectorAll('.sk').forEach(btn => {
|
|
350
535
|
const send = () => {
|
|
536
|
+
const tab = tabs.find(t => t.id === activeTabId);
|
|
537
|
+
if (!tab) return;
|
|
351
538
|
const seq = SEQ[btn.dataset.send] || btn.dataset.send;
|
|
352
|
-
socket.emit('input', seq);
|
|
353
|
-
term.focus();
|
|
539
|
+
tab.socket.emit('input', seq);
|
|
540
|
+
tab.term.focus();
|
|
354
541
|
};
|
|
355
|
-
|
|
356
|
-
// Mobile: fire on touchend, prevent synthetic click + double-tap zoom
|
|
357
542
|
btn.addEventListener('touchend', (e) => { e.preventDefault(); send(); });
|
|
358
|
-
// Desktop: fire on click
|
|
359
543
|
btn.addEventListener('click', send);
|
|
360
544
|
});
|
|
361
545
|
|
|
362
|
-
// ── QR modal
|
|
546
|
+
// ── QR modal ──
|
|
363
547
|
function showQR() {
|
|
364
|
-
const
|
|
365
|
-
|
|
548
|
+
const tab = tabs.find(t => t.id === activeTabId);
|
|
549
|
+
if (!tab) return;
|
|
550
|
+
document.getElementById('qr-img').src = `/qr/${tab.sessionId}`;
|
|
366
551
|
document.getElementById('qr-modal').classList.add('open');
|
|
367
552
|
}
|
|
368
553
|
function closeQR() {
|
|
@@ -372,19 +557,10 @@
|
|
|
372
557
|
if (e.target === e.currentTarget) closeQR();
|
|
373
558
|
});
|
|
374
559
|
|
|
375
|
-
|
|
376
|
-
function updateViewers(count) {
|
|
377
|
-
document.getElementById('viewer-count').textContent =
|
|
378
|
-
count > 1 ? `👁 ${count}` : '';
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function goBack() {
|
|
382
|
-
window.location.href = '/';
|
|
383
|
-
}
|
|
560
|
+
function goBack() { window.location.href = '/'; }
|
|
384
561
|
|
|
385
|
-
// ── Photo upload
|
|
386
|
-
const MAX_PX
|
|
387
|
-
const QUALITY = 0.85; // JPEG quality
|
|
562
|
+
// ── Photo upload ──
|
|
563
|
+
const MAX_PX = 1920, QUALITY = 0.85;
|
|
388
564
|
|
|
389
565
|
function compressImage(file) {
|
|
390
566
|
return new Promise((resolve, reject) => {
|
|
@@ -418,6 +594,8 @@
|
|
|
418
594
|
input.onchange = async () => {
|
|
419
595
|
const file = input.files?.[0];
|
|
420
596
|
if (!file) return;
|
|
597
|
+
const tab = tabs.find(t => t.id === activeTabId);
|
|
598
|
+
if (!tab) return;
|
|
421
599
|
|
|
422
600
|
const btn = document.getElementById('btn-photo');
|
|
423
601
|
btn.textContent = '⏳';
|
|
@@ -425,16 +603,13 @@
|
|
|
425
603
|
|
|
426
604
|
try {
|
|
427
605
|
const compressed = await compressImage(file);
|
|
428
|
-
const kb = (compressed.size / 1024).toFixed(0);
|
|
429
|
-
console.log(`[photo] ${(file.size/1024).toFixed(0)}KB → ${kb}KB`);
|
|
430
|
-
|
|
431
606
|
const form = new FormData();
|
|
432
607
|
form.append('image', compressed, 'photo.jpg');
|
|
433
|
-
const res = await fetch('/upload', { method: 'POST', body: form });
|
|
608
|
+
const res = await fetch('/upload?sessionId=' + tab.sessionId, { method: 'POST', body: form });
|
|
434
609
|
const data = await res.json();
|
|
435
610
|
if (data.path) {
|
|
436
|
-
socket.emit('input', data.path);
|
|
437
|
-
term.focus();
|
|
611
|
+
tab.socket.emit('input', data.path);
|
|
612
|
+
tab.term.focus();
|
|
438
613
|
} else {
|
|
439
614
|
alert('上传失败: ' + (data.error || '未知'));
|
|
440
615
|
}
|
|
@@ -448,8 +623,9 @@
|
|
|
448
623
|
input.click();
|
|
449
624
|
}
|
|
450
625
|
|
|
451
|
-
//
|
|
452
|
-
|
|
626
|
+
// ── 初始化:打开 URL 中指定的会话 ──
|
|
627
|
+
const initTab = createTab(INIT_SESSION_ID);
|
|
628
|
+
switchTab(initTab.id);
|
|
453
629
|
</script>
|
|
454
630
|
</body>
|
|
455
631
|
</html>
|