890 lines
32 KiB
HTML
890 lines
32 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>AI Terminal Monitor - 实时终端查看器</title>
|
||
|
||
<!-- xterm.js 终端模拟器 -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
||
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
:root {
|
||
--claude-bg: #eeece2;
|
||
--claude-panel: rgba(255, 255, 255, 0.92);
|
||
--claude-border: rgba(118, 103, 84, 0.25);
|
||
--claude-text: #3d3929;
|
||
--claude-text-secondary: #7f7766;
|
||
--claude-accent: #da7756;
|
||
--claude-muted: rgba(121, 109, 94, 0.45);
|
||
}
|
||
body {
|
||
font-family: 'Iowan Old Style', ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||
background: var(--claude-bg);
|
||
color: var(--claude-text);
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.header {
|
||
background: var(--claude-bg);
|
||
padding: 15px 24px;
|
||
border-bottom: 1px solid var(--claude-border);
|
||
box-shadow: 0 10px 20px rgba(61, 57, 41, 0.08);
|
||
}
|
||
.header h1 {
|
||
font-size: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.status-indicator {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
background: #4ade80;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
@keyframes pulse {
|
||
0% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0.5); }
|
||
70% { box-shadow: 0 0 0 10px rgba(74, 222, 128, 0); }
|
||
100% { box-shadow: 0 0 0 0 rgba(74, 222, 128, 0); }
|
||
}
|
||
.status-indicator.disconnected {
|
||
background: #ef4444;
|
||
animation: none;
|
||
}
|
||
.session-tabs {
|
||
background: var(--claude-bg);
|
||
padding: 12px 24px;
|
||
display: flex;
|
||
gap: 10px;
|
||
overflow-x: auto;
|
||
border-bottom: 1px solid var(--claude-border);
|
||
box-shadow: inset 0 -1px rgba(61, 57, 41, 0.05);
|
||
}
|
||
.tab {
|
||
padding: 8px 16px;
|
||
background: rgba(255, 255, 255, 0.78);
|
||
border: 1px solid rgba(118, 103, 84, 0.25);
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: all 0.25s ease;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
white-space: nowrap;
|
||
color: var(--claude-text);
|
||
}
|
||
.tab:hover {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
box-shadow: 0 6px 16px rgba(61, 57, 41, 0.12);
|
||
transform: translateY(-2px);
|
||
}
|
||
.tab.active {
|
||
background: var(--claude-accent);
|
||
border-color: var(--claude-accent);
|
||
color: #fffaf0;
|
||
box-shadow: 0 10px 24px rgba(218, 119, 86, 0.26);
|
||
}
|
||
.tab-icon {
|
||
font-size: 14px;
|
||
}
|
||
.main-content {
|
||
flex: 1;
|
||
display: flex;
|
||
padding: 22px;
|
||
gap: 22px;
|
||
overflow: hidden;
|
||
}
|
||
.terminal-wrapper {
|
||
flex: 1;
|
||
background: #ffffff;
|
||
border-radius: 20px;
|
||
overflow: hidden;
|
||
box-shadow: 0 24px 48px rgba(61, 57, 41, 0.16);
|
||
display: flex;
|
||
flex-direction: column;
|
||
border: 1px solid var(--claude-border);
|
||
}
|
||
.terminal-header {
|
||
background: rgba(255, 255, 255, 0.86);
|
||
padding: 12px 18px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-bottom: 1px solid var(--claude-border);
|
||
}
|
||
.terminal-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 14px;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
.terminal-controls {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
.control-btn {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
transition: opacity 0.2s;
|
||
}
|
||
.control-btn:hover {
|
||
opacity: 0.8;
|
||
}
|
||
.control-btn.close { background: #ff5f56; }
|
||
.control-btn.minimize { background: #ffbd2e; }
|
||
.control-btn.maximize { background: #27c93f; }
|
||
.terminal-body {
|
||
flex: 1;
|
||
padding: 0 28px 0 32px;
|
||
background: #ffffff;
|
||
display: flex;
|
||
align-items: stretch;
|
||
justify-content: stretch;
|
||
}
|
||
#terminal {
|
||
flex: 1;
|
||
height: 100%;
|
||
margin: 0 28px 0 36px;
|
||
background: #ffffff;
|
||
color: var(--claude-text);
|
||
overflow: hidden;
|
||
}
|
||
#terminal .xterm-viewport {
|
||
overflow-x: hidden !important;
|
||
padding-bottom: 0;
|
||
}
|
||
#terminal .xterm-scroll-area {
|
||
margin-bottom: 32px;
|
||
}
|
||
#terminal .xterm-rows {
|
||
padding-bottom: 20px;
|
||
}
|
||
.sidebar {
|
||
width: 300px;
|
||
background: var(--claude-panel);
|
||
border-radius: 18px;
|
||
padding: 20px;
|
||
border: 1px solid var(--claude-border);
|
||
box-shadow: 0 18px 40px rgba(61, 57, 41, 0.1);
|
||
overflow-y: auto;
|
||
}
|
||
.info-section {
|
||
margin-bottom: 20px;
|
||
}
|
||
.info-section h3 {
|
||
font-size: 14px;
|
||
margin-bottom: 10px;
|
||
color: var(--claude-text-secondary);
|
||
letter-spacing: 0.05em;
|
||
}
|
||
.info-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid rgba(118, 103, 84, 0.15);
|
||
font-size: 14px;
|
||
}
|
||
.info-label {
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
.info-value {
|
||
color: var(--claude-text);
|
||
font-weight: 600;
|
||
}
|
||
.command-history {
|
||
max-height: 210px;
|
||
overflow-y: auto;
|
||
background: rgba(255, 255, 255, 0.78);
|
||
border-radius: 12px;
|
||
padding: 12px;
|
||
border: 1px solid rgba(118, 103, 84, 0.2);
|
||
}
|
||
.command-item {
|
||
padding: 6px;
|
||
margin: 3px 0;
|
||
font-family: inherit;
|
||
font-size: 12px;
|
||
color: var(--claude-accent);
|
||
border-left: 3px solid var(--claude-accent);
|
||
padding-left: 10px;
|
||
background: rgba(218, 119, 86, 0.08);
|
||
border-radius: 6px;
|
||
}
|
||
.status-bar {
|
||
background: var(--claude-panel);
|
||
padding: 12px 24px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 13px;
|
||
border-top: 1px solid var(--claude-border);
|
||
box-shadow: 0 -8px 18px rgba(61, 57, 41, 0.08);
|
||
}
|
||
.status-left {
|
||
display: flex;
|
||
gap: 20px;
|
||
}
|
||
.status-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: var(--claude-text-secondary);
|
||
}
|
||
@media (max-width: 768px) {
|
||
.main-content {
|
||
flex-direction: column;
|
||
}
|
||
.sidebar {
|
||
width: 100%;
|
||
}
|
||
}
|
||
.loader {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 3px solid rgba(61, 57, 41, 0.15);
|
||
border-top-color: var(--claude-accent);
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin: 20px auto;
|
||
}
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
.tooltip {
|
||
position: absolute;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
color: #fff;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
pointer-events: none;
|
||
z-index: 1000;
|
||
display: none;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- 头部 -->
|
||
<div class="header">
|
||
<h1>
|
||
🖥️ AI Terminal Monitor
|
||
<span class="status-indicator" id="connectionStatus"></span>
|
||
<span style="font-size: 14px; font-weight: normal; margin-left: 10px;" id="connectionText">连接中...</span>
|
||
</h1>
|
||
</div>
|
||
|
||
<!-- 会话标签 -->
|
||
<div class="session-tabs" id="sessionTabs">
|
||
<div class="tab" onclick="createNewSession()">
|
||
<span class="tab-icon">➕</span>
|
||
<span>等待终端会话...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 主内容区 -->
|
||
<div class="main-content">
|
||
<!-- 终端容器 -->
|
||
<div class="terminal-wrapper">
|
||
<div class="terminal-header">
|
||
<div class="terminal-title">
|
||
<span id="terminalSession">无活动会话</span>
|
||
<span id="terminalPath" style="color: #666;">-</span>
|
||
</div>
|
||
<div class="terminal-controls">
|
||
<div class="control-btn close" title="关闭"></div>
|
||
<div class="control-btn minimize" title="最小化"></div>
|
||
<div class="control-btn maximize" title="最大化"></div>
|
||
</div>
|
||
</div>
|
||
<div class="terminal-body">
|
||
<div id="terminal"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 侧边栏 -->
|
||
<div class="sidebar">
|
||
<!-- 会话信息 -->
|
||
<div class="info-section">
|
||
<h3>📊 会话信息</h3>
|
||
<div class="info-item">
|
||
<span class="info-label">会话名称</span>
|
||
<span class="info-value" id="sessionName">-</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">工作目录</span>
|
||
<span class="info-value" id="workingDir">-</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">Shell类型</span>
|
||
<span class="info-value" id="shellType">-</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">运行时间</span>
|
||
<span class="info-value" id="uptime">-</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">缓冲区</span>
|
||
<span class="info-value" id="bufferSize">0 KB</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 命令历史 -->
|
||
<div class="info-section">
|
||
<h3>📝 最近命令</h3>
|
||
<div class="command-history" id="commandHistory">
|
||
<div style="color: #666; text-align: center;">暂无命令</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 统计信息 -->
|
||
<div class="info-section">
|
||
<h3>📈 统计</h3>
|
||
<div class="info-item">
|
||
<span class="info-label">总命令数</span>
|
||
<span class="info-value" id="commandCount">0</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">输出行数</span>
|
||
<span class="info-value" id="outputLines">0</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">活动会话</span>
|
||
<span class="info-value" id="activeCount">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 状态栏 -->
|
||
<div class="status-bar">
|
||
<div class="status-left">
|
||
<div class="status-item">
|
||
<span>⚡</span>
|
||
<span id="latency">延迟: 0ms</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span>📡</span>
|
||
<span id="dataRate">0 KB/s</span>
|
||
</div>
|
||
<div class="status-item">
|
||
<span>🕐</span>
|
||
<span id="currentTime"></span>
|
||
</div>
|
||
</div>
|
||
<div class="status-right">
|
||
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 提示框 -->
|
||
<div class="tooltip" id="tooltip"></div>
|
||
|
||
<!-- 引入依赖 -->
|
||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-web-links@0.9.0/lib/xterm-addon-web-links.js"></script>
|
||
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let term = null;
|
||
let fitAddon = null;
|
||
let socket = null;
|
||
let currentSession = null;
|
||
let sessions = {};
|
||
let commandHistory = [];
|
||
const sessionLogs = {};
|
||
const sessionHydrated = {};
|
||
const pendingHistory = {};
|
||
const MAX_SESSION_LOG_LENGTH = 400000;
|
||
let stats = {
|
||
commandCount: 0,
|
||
outputLines: 0,
|
||
dataReceived: 0,
|
||
startTime: Date.now()
|
||
};
|
||
|
||
function normalizeHistoryOutput(payload) {
|
||
if (!payload) {
|
||
return '';
|
||
}
|
||
if (Array.isArray(payload)) {
|
||
return payload.join('');
|
||
}
|
||
if (typeof payload === 'string') {
|
||
return payload;
|
||
}
|
||
return String(payload);
|
||
}
|
||
|
||
function trimSessionLog(session) {
|
||
if (!session || !sessionLogs[session]) {
|
||
return;
|
||
}
|
||
if (sessionLogs[session].length > MAX_SESSION_LOG_LENGTH) {
|
||
sessionLogs[session] = sessionLogs[session].slice(-MAX_SESSION_LOG_LENGTH);
|
||
}
|
||
}
|
||
|
||
function appendSessionLog(session, chunk) {
|
||
if (!session || !chunk) {
|
||
return;
|
||
}
|
||
if (!sessionLogs[session]) {
|
||
sessionLogs[session] = '';
|
||
}
|
||
sessionLogs[session] += chunk;
|
||
trimSessionLog(session);
|
||
}
|
||
|
||
function renderCurrentSessionLog() {
|
||
if (!term) {
|
||
return;
|
||
}
|
||
term.clear();
|
||
if (!currentSession) {
|
||
term.writeln('\x1b[33m暂无活动终端\x1b[0m');
|
||
return;
|
||
}
|
||
term.writeln(`\x1b[36m[终端: ${currentSession}]\x1b[0m`);
|
||
term.writeln('');
|
||
const log = sessionLogs[currentSession];
|
||
if (log && log.length) {
|
||
term.write(log);
|
||
} else if (pendingHistory[currentSession]) {
|
||
term.writeln('\x1b[33m正在加载历史输出...\x1b[0m');
|
||
} else {
|
||
term.writeln('\x1b[90m暂无历史输出,等待新内容...\x1b[0m');
|
||
}
|
||
}
|
||
|
||
function handleHistoryPayload(data) {
|
||
if (!data || !data.session) {
|
||
return;
|
||
}
|
||
const session = data.session;
|
||
const historyText = normalizeHistoryOutput(data.output);
|
||
const existing = sessionLogs[session] || '';
|
||
const shouldCombine = !sessionHydrated[session];
|
||
sessionLogs[session] = historyText + (shouldCombine ? existing : '');
|
||
trimSessionLog(session);
|
||
sessionHydrated[session] = true;
|
||
pendingHistory[session] = false;
|
||
if (session === currentSession) {
|
||
renderCurrentSessionLog();
|
||
}
|
||
}
|
||
|
||
// 初始化终端
|
||
function initTerminal() {
|
||
term = new Terminal({
|
||
cursorBlink: true,
|
||
fontSize: 14,
|
||
fontFamily: '"JetBrains Mono", "SFMono-Regular", "Consolas", monospace',
|
||
theme: {
|
||
background: '#ffffff',
|
||
foreground: '#1f1f1f',
|
||
cursor: '#3d3929',
|
||
black: '#1f1f1f',
|
||
red: '#c04a2f',
|
||
green: '#4b8f60',
|
||
yellow: '#b48a2c',
|
||
blue: '#4a6ea9',
|
||
magenta: '#9b4d88',
|
||
cyan: '#2a8c8c',
|
||
white: '#f4f0ea'
|
||
},
|
||
scrollback: 10000,
|
||
cols: 120, // 设置固定列数
|
||
rows: 30, // 设置固定行数
|
||
convertEol: true, // 转换行尾
|
||
wordSeparator: ' ()[]{}\'"', // 单词分隔符
|
||
rendererType: 'canvas', // 使用canvas渲染器
|
||
allowTransparency: false
|
||
});
|
||
|
||
// 添加插件
|
||
fitAddon = new FitAddon.FitAddon();
|
||
term.loadAddon(fitAddon);
|
||
|
||
const webLinksAddon = new WebLinksAddon.WebLinksAddon();
|
||
term.loadAddon(webLinksAddon);
|
||
|
||
// 打开终端
|
||
term.open(document.getElementById('terminal'));
|
||
|
||
// 延迟执行fit以确保正确计算尺寸
|
||
setTimeout(() => {
|
||
fitAddon.fit();
|
||
console.log(`终端尺寸: ${term.cols}x${term.rows}`);
|
||
}, 100);
|
||
|
||
// 显示欢迎信息
|
||
term.writeln('\x1b[1;36m╔════════════════════════════════════════╗\x1b[0m');
|
||
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
||
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
||
term.writeln('');
|
||
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
|
||
term.writeln('');
|
||
}
|
||
|
||
// 初始化WebSocket连接
|
||
function initWebSocket() {
|
||
socket = io();
|
||
|
||
// 连接成功
|
||
socket.on('connect', () => {
|
||
console.log('WebSocket连接成功');
|
||
updateConnectionStatus(true);
|
||
term.writeln('\x1b[32m✓ 已连接到服务器\x1b[0m');
|
||
|
||
// 订阅所有终端事件
|
||
socket.emit('terminal_subscribe', { all: true });
|
||
console.log('已发送终端订阅请求');
|
||
});
|
||
|
||
// 连接断开
|
||
socket.on('disconnect', () => {
|
||
console.log('WebSocket连接断开');
|
||
updateConnectionStatus(false);
|
||
term.writeln('\x1b[31m✗ 与服务器断开连接\x1b[0m');
|
||
});
|
||
|
||
// 订阅成功确认
|
||
socket.on('terminal_subscribed', (data) => {
|
||
console.log('终端订阅成功:', data);
|
||
term.writeln('\x1b[32m✓ 已订阅终端事件\x1b[0m');
|
||
});
|
||
|
||
// 终端启动事件
|
||
socket.on('terminal_started', (data) => {
|
||
console.log('终端启动:', data);
|
||
addSession(data.session, data);
|
||
term.writeln(`\x1b[32m[终端启动]\x1b[0m ${data.session} - ${data.working_dir}`);
|
||
});
|
||
|
||
// 终端列表更新
|
||
socket.on('terminal_list_update', (data) => {
|
||
console.log('终端列表更新:', data);
|
||
if (data.terminals && data.terminals.length > 0) {
|
||
// 清除所有会话并重新添加
|
||
sessions = {};
|
||
for (const terminal of data.terminals) {
|
||
sessions[terminal.name] = {
|
||
working_dir: terminal.working_dir,
|
||
is_running: terminal.is_running,
|
||
shell: 'bash'
|
||
};
|
||
}
|
||
updateSessionTabs();
|
||
if (data.active && !currentSession) {
|
||
switchToSession(data.active);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 终端输出事件
|
||
socket.on('terminal_output', (data) => {
|
||
console.log('收到终端输出:', data.session, data.data.length + '字节');
|
||
appendSessionLog(data.session, data.data);
|
||
let switchedDuringEvent = false;
|
||
if (!currentSession && data.session) {
|
||
if (!sessions[data.session]) {
|
||
addSession(data.session, {
|
||
session: data.session,
|
||
working_dir: 'unknown'
|
||
});
|
||
}
|
||
switchToSession(data.session);
|
||
switchedDuringEvent = true;
|
||
}
|
||
if (data.session === currentSession && !switchedDuringEvent) {
|
||
term.write(data.data);
|
||
}
|
||
stats.outputLines++;
|
||
stats.dataReceived += data.data.length;
|
||
updateStats();
|
||
});
|
||
|
||
// 终端输入事件(AI发送的命令)
|
||
socket.on('terminal_input', (data) => {
|
||
console.log('收到终端输入:', data.session, data.data);
|
||
const formattedInput = `\x1b[1;32m➜ ${data.data}\x1b[0m`;
|
||
appendSessionLog(data.session, formattedInput);
|
||
let switchedDuringEvent = false;
|
||
if (!currentSession && data.session) {
|
||
if (!sessions[data.session]) {
|
||
addSession(data.session, {
|
||
session: data.session,
|
||
working_dir: 'unknown'
|
||
});
|
||
}
|
||
switchToSession(data.session);
|
||
switchedDuringEvent = true;
|
||
}
|
||
if (data.session === currentSession && !switchedDuringEvent) {
|
||
term.write(formattedInput);
|
||
}
|
||
|
||
addCommandToHistory(data.data.trim());
|
||
stats.commandCount++;
|
||
updateStats();
|
||
});
|
||
|
||
// 终端关闭事件
|
||
socket.on('terminal_closed', (data) => {
|
||
console.log('终端关闭:', data);
|
||
removeSession(data.session);
|
||
term.writeln(`\x1b[31m[终端关闭]\x1b[0m ${data.session}`);
|
||
});
|
||
|
||
// 终端重置事件
|
||
socket.on('terminal_reset', (data) => {
|
||
console.log('终端重置:', data);
|
||
if (data.session) {
|
||
if (!sessions[data.session]) {
|
||
addSession(data.session, {
|
||
session: data.session,
|
||
working_dir: data.working_dir || 'unknown'
|
||
});
|
||
} else {
|
||
sessions[data.session].working_dir = data.working_dir || sessions[data.session].working_dir;
|
||
}
|
||
switchToSession(data.session);
|
||
}
|
||
const targetSession = data.session || currentSession;
|
||
if (targetSession) {
|
||
sessionLogs[targetSession] = '';
|
||
sessionHydrated[targetSession] = true;
|
||
pendingHistory[targetSession] = false;
|
||
appendSessionLog(targetSession, `\x1b[33m[终端已重置]\x1b[0m ${targetSession}\n`);
|
||
if (targetSession === currentSession) {
|
||
renderCurrentSessionLog();
|
||
}
|
||
}
|
||
term.writeln(`\x1b[33m[终端已重置]\x1b[0m ${data.session || ''}`);
|
||
resetCommandHistory();
|
||
stats.commandCount = 0;
|
||
stats.outputLines = 0;
|
||
stats.dataReceived = 0;
|
||
stats.startTime = Date.now();
|
||
updateStats();
|
||
});
|
||
socket.on('terminal_output_history', (data) => {
|
||
console.log('收到终端历史输出:', data);
|
||
handleHistoryPayload(data);
|
||
});
|
||
|
||
socket.on('terminal_history', (data) => {
|
||
console.log('收到终端历史:', data);
|
||
handleHistoryPayload(data);
|
||
});
|
||
|
||
// 终端切换事件
|
||
socket.on('terminal_switched', (data) => {
|
||
console.log('终端切换:', data);
|
||
switchToSession(data.current);
|
||
});
|
||
|
||
// 调试:监听所有事件
|
||
const onevent = socket.onevent;
|
||
socket.onevent = function (packet) {
|
||
const args = packet.data || [];
|
||
console.log('Socket事件:', args[0], args[1]);
|
||
onevent.call(this, packet);
|
||
};
|
||
}
|
||
|
||
// 添加会话
|
||
function addSession(name, info) {
|
||
sessions[name] = info;
|
||
updateSessionTabs();
|
||
|
||
// 如果是第一个会话,自动切换
|
||
if (!currentSession) {
|
||
switchToSession(name);
|
||
}
|
||
}
|
||
|
||
// 移除会话
|
||
function removeSession(name) {
|
||
delete sessions[name];
|
||
delete sessionLogs[name];
|
||
delete sessionHydrated[name];
|
||
delete pendingHistory[name];
|
||
updateSessionTabs();
|
||
|
||
// 如果是当前会话,切换到其他会话
|
||
if (currentSession === name) {
|
||
const remaining = Object.keys(sessions);
|
||
if (remaining.length > 0) {
|
||
switchToSession(remaining[0]);
|
||
} else {
|
||
currentSession = null;
|
||
updateSessionInfo();
|
||
renderCurrentSessionLog();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 切换会话
|
||
function switchToSession(name) {
|
||
if (!name) {
|
||
return;
|
||
}
|
||
currentSession = name;
|
||
updateSessionTabs();
|
||
updateSessionInfo();
|
||
const needsHistory = !sessionHydrated[name];
|
||
pendingHistory[name] = needsHistory;
|
||
renderCurrentSessionLog();
|
||
if (needsHistory && socket) {
|
||
socket.emit('get_terminal_output', { session: name });
|
||
}
|
||
}
|
||
|
||
// 更新会话标签
|
||
function updateSessionTabs() {
|
||
const container = document.getElementById('sessionTabs');
|
||
container.innerHTML = '';
|
||
|
||
if (Object.keys(sessions).length === 0) {
|
||
container.innerHTML = `
|
||
<div class="tab">
|
||
<span class="tab-icon">⏳</span>
|
||
<span>等待终端会话...</span>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
for (const [name, info] of Object.entries(sessions)) {
|
||
const tab = document.createElement('div');
|
||
tab.className = `tab ${name === currentSession ? 'active' : ''}`;
|
||
tab.innerHTML = `
|
||
<span class="tab-icon">📟</span>
|
||
<span>${name}</span>
|
||
`;
|
||
tab.onclick = () => switchToSession(name);
|
||
container.appendChild(tab);
|
||
}
|
||
}
|
||
|
||
// 更新会话信息
|
||
function updateSessionInfo() {
|
||
if (!currentSession || !sessions[currentSession]) {
|
||
document.getElementById('sessionName').textContent = '-';
|
||
document.getElementById('workingDir').textContent = '-';
|
||
document.getElementById('shellType').textContent = '-';
|
||
document.getElementById('terminalSession').textContent = '无活动会话';
|
||
document.getElementById('terminalPath').textContent = '-';
|
||
return;
|
||
}
|
||
|
||
const info = sessions[currentSession];
|
||
document.getElementById('sessionName').textContent = currentSession;
|
||
document.getElementById('workingDir').textContent = info.working_dir || '-';
|
||
document.getElementById('shellType').textContent = info.shell || '-';
|
||
document.getElementById('terminalSession').textContent = currentSession;
|
||
document.getElementById('terminalPath').textContent = info.working_dir || '-';
|
||
}
|
||
|
||
// 添加命令到历史
|
||
function addCommandToHistory(command) {
|
||
commandHistory.unshift(command);
|
||
if (commandHistory.length > 20) {
|
||
commandHistory.pop();
|
||
}
|
||
updateCommandHistory();
|
||
}
|
||
|
||
function resetCommandHistory() {
|
||
commandHistory = [];
|
||
updateCommandHistory();
|
||
}
|
||
|
||
// 更新命令历史显示
|
||
function updateCommandHistory() {
|
||
const container = document.getElementById('commandHistory');
|
||
if (commandHistory.length === 0) {
|
||
container.innerHTML = '<div style="color: #666; text-align: center;">暂无命令</div>';
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = commandHistory
|
||
.map(cmd => `<div class="command-item">${cmd}</div>`)
|
||
.join('');
|
||
}
|
||
|
||
// 更新统计信息
|
||
function updateStats() {
|
||
document.getElementById('commandCount').textContent = stats.commandCount;
|
||
document.getElementById('outputLines').textContent = stats.outputLines;
|
||
document.getElementById('activeCount').textContent = Object.keys(sessions).length;
|
||
document.getElementById('bufferSize').textContent = (stats.dataReceived / 1024).toFixed(1) + ' KB';
|
||
|
||
// 更新运行时间
|
||
const uptime = Math.floor((Date.now() - stats.startTime) / 1000);
|
||
const hours = Math.floor(uptime / 3600);
|
||
const minutes = Math.floor((uptime % 3600) / 60);
|
||
const seconds = uptime % 60;
|
||
document.getElementById('uptime').textContent =
|
||
`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
// 更新连接状态
|
||
function updateConnectionStatus(connected) {
|
||
const indicator = document.getElementById('connectionStatus');
|
||
const text = document.getElementById('connectionText');
|
||
|
||
if (connected) {
|
||
indicator.classList.remove('disconnected');
|
||
text.textContent = '已连接';
|
||
text.style.color = '#4ade80';
|
||
} else {
|
||
indicator.classList.add('disconnected');
|
||
text.textContent = '未连接';
|
||
text.style.color = '#ef4444';
|
||
}
|
||
}
|
||
|
||
// 更新当前时间
|
||
function updateCurrentTime() {
|
||
const now = new Date();
|
||
document.getElementById('currentTime').textContent =
|
||
now.toLocaleTimeString('zh-CN');
|
||
}
|
||
|
||
// 窗口大小调整
|
||
window.addEventListener('resize', () => {
|
||
if (fitAddon) {
|
||
fitAddon.fit();
|
||
}
|
||
});
|
||
|
||
// 初始化
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initTerminal();
|
||
initWebSocket();
|
||
|
||
// 定时更新
|
||
setInterval(updateCurrentTime, 1000);
|
||
setInterval(updateStats, 1000);
|
||
updateCurrentTime();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|