agent-Specialization/static/terminal.html

890 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>