Files
XCClaw/console.html

566 lines
25 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>XCClaw 控制台</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1 { font-size: 24px; margin-bottom: 20px; color: #00d9ff; }
.tabs { display: flex; gap: 5px; margin-bottom: 20px; border-bottom: 1px solid #333; }
.tab { padding: 10px 20px; background: transparent; border: none; color: #888; cursor: pointer; font-size: 14px; transition: all 0.2s; }
.tab:hover { color: #fff; }
.tab.active { color: #00d9ff; border-bottom: 2px solid #00d9ff; }
.panel { display: none; }
.panel.active { display: block; }
.card { background: #16213e; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.card h2 { font-size: 16px; margin-bottom: 15px; color: #00d9ff; }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; font-size: 12px; color: #888; }
input, select, textarea { width: 100%; padding: 10px; background: #0f0f23; border: 1px solid #333; border-radius: 4px; color: #eee; font-size: 14px; }
textarea { resize: vertical; min-height: 80px; }
input:focus, textarea:focus { outline: none; border-color: #00d9ff; }
.btn { padding: 10px 20px; background: #00d9ff; color: #1a1a2e; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn:hover { background: #00b8d4; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-danger { background: #ff4757; }
.btn-danger:hover { background: #ff6b7a; }
.btn-success { background: #2ed573; }
.btn-success:hover { background: #7bed9f; }
.status-badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 11px; font-weight: 500; }
.status-pending { background: #f39c12; color: #1a1a2e; }
.status-running { background: #3498db; color: #fff; }
.status-completed { background: #2ecc71; color: #1a1a2e; }
.status-failed { background: #e74c3c; color: #fff; }
.status-aborted { background: #95a5a6; color: #1a1a2e; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #333; }
th { color: #888; font-weight: 500; }
tr:hover { background: #1f2b47; }
.log-box { background: #0f0f23; border-radius: 4px; padding: 15px; max-height: 300px; overflow-y: auto; font-family: monospace; font-size: 12px; white-space: pre-wrap; }
.log-entry { margin-bottom: 8px; padding: 8px; background: #16213e; border-radius: 4px; border-left: 3px solid #00d9ff; }
.log-entry.error { border-left-color: #ff4757; }
.log-time { color: #666; font-size: 11px; }
.ws-status { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; }
.ws-dot { width: 8px; height: 8px; border-radius: 50%; background: #ff4757; }
.ws-dot.connected { background: #2ed573; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media (max-width: 768px) { .grid { grid-template-columns: 1fr; } }
.flex { display: flex; gap: 10px; align-items: center; }
.flex-between { display: flex; justify-content: space-between; align-items: center; }
</style>
</head>
<body>
<div class="container">
<div class="flex-between">
<h1>XCClaw 控制台</h1>
<div class="ws-status">
<span class="ws-dot" id="wsDot"></span>
<span id="wsStatus">未连接</span>
</div>
</div>
<div class="tabs">
<button class="tab active" data-tab="tasks">任务管理</button>
<button class="tab" data-tab="schedules">定时任务</button>
<button class="tab" data-tab="history">历史记录</button>
<button class="tab" data-tab="persistent">持久会话</button>
<button class="tab" data-tab="logs">实时日志</button>
</div>
<!-- 任务管理 -->
<div class="panel active" id="panel-tasks">
<div class="grid">
<div class="card">
<h2>创建任务</h2>
<div class="form-group">
<label>任务类型</label>
<select id="taskType">
<option value="ephemeral">即时任务 (Ephemeral)</option>
<option value="persistent">持久任务 (Persistent)</option>
<option value="scheduled">定时任务 (Scheduled)</option>
</select>
</div>
<div class="form-group">
<label>任务描述 (Prompt)</label>
<textarea id="taskPrompt" placeholder="输入任务描述..."></textarea>
</div>
<button class="btn" onclick="createTask()">创建任务</button>
</div>
<div class="card">
<h2>健康检查</h2>
<div id="healthStatus">加载中...</div>
</div>
</div>
<div class="card">
<h2>任务列表</h2>
<button class="btn" onclick="loadTasks()" style="margin-bottom: 15px;">刷新</button>
<table>
<thead>
<tr>
<th>ID</th>
<th>类型</th>
<th>描述</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="tasksTable"></tbody>
</table>
</div>
</div>
<!-- 定时任务 -->
<div class="panel" id="panel-schedules">
<div class="card">
<h2>创建定时任务</h2>
<div class="grid">
<div class="form-group">
<label>任务ID</label>
<input type="text" id="scheduleId" placeholder="唯一标识符">
</div>
<div class="form-group">
<label>任务名称</label>
<input type="text" id="scheduleName" placeholder="任务名称">
</div>
</div>
<div class="form-group">
<label>Cron 表达式</label>
<input type="text" id="scheduleCron" placeholder="0 9 * * *" value="0 9 * * *">
</div>
<div class="form-group">
<label>执行内容</label>
<textarea id="schedulePrompt" placeholder="定时执行的提示词..."></textarea>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="scheduleEnabled" checked> 启用
</label>
</div>
<button class="btn" onclick="createSchedule()">创建定时任务</button>
</div>
<div class="card">
<h2>定时任务列表</h2>
<button class="btn" onclick="loadSchedules()" style="margin-bottom: 15px;">刷新</button>
<table>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
<th>Cron</th>
<th>提示词</th>
<th>操作</th>
</tr>
</thead>
<tbody id="schedulesTable"></tbody>
</table>
</div>
</div>
<!-- 历史记录 -->
<div class="panel" id="panel-history">
<div class="card">
<div class="flex-between">
<h2>任务历史</h2>
<button class="btn btn-danger" onclick="clearHistory()">清空历史</button>
</div>
<div class="form-group" style="margin-top: 15px;">
<label>显示条数</label>
<input type="number" id="historyLimit" value="20" style="width: 100px;" onchange="loadHistory()">
</div>
<button class="btn" onclick="loadHistory()" style="margin-bottom: 15px;">刷新</button>
<table>
<thead>
<tr>
<th>ID</th>
<th>类型</th>
<th>描述</th>
<th>状态</th>
<th>创建时间</th>
<th>完成时间</th>
</tr>
</thead>
<tbody id="historyTable"></tbody>
</table>
</div>
</div>
<!-- 持久会话 -->
<div class="panel" id="panel-persistent">
<div class="card">
<h2>创建持久会话</h2>
<div class="form-group">
<label>会话名称</label>
<input type="text" id="persistentName" placeholder="会话名称(可选)">
</div>
<button class="btn" onclick="createPersistentSession()">创建</button>
</div>
<div class="card">
<h2>持久会话列表</h2>
<button class="btn" onclick="loadPersistentSessions()" style="margin-bottom: 15px;">刷新</button>
<table>
<thead>
<tr>
<th>ID</th>
<th>会话ID</th>
<th>名称</th>
<th>创建时间</th>
<th>最后使用</th>
<th>操作</th>
</tr>
</thead>
<tbody id="persistentTable"></tbody>
</table>
</div>
<div class="card" id="sendMessagePanel" style="display: none;">
<h2>发送消息</h2>
<div class="form-group">
<label>会话 ID</label>
<input type="text" id="messageSessionId" readonly>
</div>
<div class="form-group">
<label>消息内容</label>
<textarea id="messageText" placeholder="输入消息..."></textarea>
</div>
<div class="flex">
<button class="btn btn-success" onclick="sendMessage(false)">同步发送</button>
<button class="btn" onclick="sendMessage(true)">异步发送</button>
</div>
</div>
</div>
<!-- 实时日志 -->
<div class="panel" id="panel-logs">
<div class="card">
<div class="flex-between">
<h2>实时日志 (WebSocket)</h2>
<button class="btn" onclick="clearLogs()">清空</button>
</div>
<div class="log-box" id="logBox"></div>
</div>
</div>
</div>
<script>
const API_BASE = 'http://127.0.0.1:3005/api/xcclaw';
let ws = null;
let selectedSessionId = null;
// Tab 切换
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
});
});
// WebSocket 连接
function connectWS() {
ws = new WebSocket('ws://127.0.0.1:3005/api/xcclaw/ws');
ws.onopen = () => {
document.getElementById('wsDot').classList.add('connected');
document.getElementById('wsStatus').textContent = '已连接';
addLog('WebSocket 连接已建立', 'info');
};
ws.onclose = () => {
document.getElementById('wsDot').classList.remove('connected');
document.getElementById('wsStatus').textContent = '未连接';
addLog('WebSocket 连接已断开', 'error');
setTimeout(connectWS, 3000);
};
ws.onmessage = (e) => {
addLog('收到消息: ' + e.data, 'info');
};
ws.onerror = (e) => {
addLog('WebSocket 错误', 'error');
};
}
function addLog(msg, type = 'info') {
const box = document.getElementById('logBox');
const time = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'log-entry' + (type === 'error' ? ' error' : '');
entry.innerHTML = `<span class="log-time">[${time}]</span> ${msg}`;
box.insertBefore(entry, box.firstChild);
if (box.children.length > 100) box.lastChild.remove();
}
function clearLogs() {
document.getElementById('logBox').innerHTML = '';
}
// 健康检查
async function checkHealth() {
try {
const res = await fetch(API_BASE + '/health');
const data = await res.json();
document.getElementById('healthStatus').innerHTML = `
<pre style="background: #0f0f23; padding: 10px; border-radius: 4px; overflow-x: auto;">${JSON.stringify(data, null, 2)}</pre>
`;
} catch (e) {
document.getElementById('healthStatus').innerHTML = '<span style="color: #ff4757;">连接失败: ' + e.message + '</span>';
}
}
// 任务管理
async function createTask() {
const type = document.getElementById('taskType').value;
const prompt = document.getElementById('taskPrompt').value;
if (!prompt) return alert('请输入任务描述');
try {
const res = await fetch(API_BASE + '/task', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({type, prompt})
});
const task = await res.json();
addLog('创建任务: ' + task.id, 'info');
document.getElementById('taskPrompt').value = '';
loadTasks();
} catch (e) {
alert('创建失败: ' + e.message);
}
}
async function loadTasks() {
try {
const res = await fetch(API_BASE + '/task');
const tasks = await res.json();
const tbody = document.getElementById('tasksTable');
tbody.innerHTML = tasks.map(t => `
<tr>
<td>${t.id.substring(0, 8)}...</td>
<td>${t.type}</td>
<td>${t.prompt.substring(0, 30)}${t.prompt.length > 30 ? '...' : ''}</td>
<td><span class="status-badge status-${t.status}">${t.status}</span></td>
<td class="flex">
<button class="btn" style="padding: 5px 10px; font-size: 12px;" onclick="executeTask('${t.id}', false)">执行</button>
<button class="btn btn-success" style="padding: 5px 10px; font-size: 12px;" onclick="executeTask('${t.id}', true)">异步</button>
<button class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;" onclick="abortTask('${t.id}')">中止</button>
</td>
</tr>
`).join('');
} catch (e) {
addLog('加载任务失败: ' + e.message, 'error');
}
}
async function executeTask(taskId, async_) {
try {
const endpoint = async_ ? '/execute_async' : '/execute';
const res = await fetch(API_BASE + '/task/' + taskId + endpoint, {method: 'POST'});
const data = await res.json();
addLog(async_ ? '异步任务已启动: ' + taskId : '执行完成: ' + JSON.stringify(data).substring(0, 100), 'info');
loadTasks();
} catch (e) {
alert('执行失败: ' + e.message);
}
}
async function abortTask(taskId) {
try {
await fetch(API_BASE + '/task/' + taskId + '/abort', {method: 'POST'});
addLog('已中止任务: ' + taskId, 'info');
loadTasks();
} catch (e) {
alert('中止失败: ' + e.message);
}
}
// 定时任务
async function createSchedule() {
const id = document.getElementById('scheduleId').value;
const name = document.getElementById('scheduleName').value;
const cron = document.getElementById('scheduleCron').value;
const prompt = document.getElementById('schedulePrompt').value;
const enabled = document.getElementById('scheduleEnabled').checked;
if (!id || !prompt) return alert('请填写必要字段');
try {
const res = await fetch(API_BASE + '/schedule', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id, name, cron, prompt, enabled})
});
const data = await res.json();
addLog('创建定时任务: ' + id, 'info');
loadSchedules();
} catch (e) {
alert('创建失败: ' + e.message);
}
}
async function loadSchedules() {
try {
const res = await fetch(API_BASE + '/schedule');
const schedules = await res.json();
const tbody = document.getElementById('schedulesTable');
tbody.innerHTML = schedules.map(s => `
<tr>
<td>${s.id}</td>
<td>${s.name || '-'}</td>
<td>${s.cron}</td>
<td>${(s.prompt || '').substring(0, 30)}...</td>
<td>
<button class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;" onclick="deleteSchedule('${s.id}')">删除</button>
</td>
</tr>
`).join('');
} catch (e) {
addLog('加载定时任务失败: ' + e.message, 'error');
}
}
async function deleteSchedule(id) {
if (!confirm('确认删除?')) return;
try {
await fetch(API_BASE + '/schedule/' + id, {method: 'DELETE'});
addLog('删除定时任务: ' + id, 'info');
loadSchedules();
} catch (e) {
alert('删除失败: ' + e.message);
}
}
// 历史记录
async function loadHistory() {
const limit = document.getElementById('historyLimit').value;
try {
const res = await fetch(API_BASE + '/history?limit=' + limit);
const history = await res.json();
const tbody = document.getElementById('historyTable');
tbody.innerHTML = history.map(h => `
<tr>
<td>${h.id.substring(0, 8)}...</td>
<td>${h.type}</td>
<td>${h.prompt.substring(0, 30)}${h.prompt.length > 30 ? '...' : ''}</td>
<td><span class="status-badge status-${h.status}">${h.status}</span></td>
<td>${h.created_at || '-'}</td>
<td>${h.finished_at || '-'}</td>
</tr>
`).join('');
} catch (e) {
addLog('加载历史失败: ' + e.message, 'error');
}
}
async function clearHistory() {
if (!confirm('确认清空所有历史记录?')) return;
try {
await fetch(API_BASE + '/history', {method: 'DELETE'});
addLog('已清空历史记录', 'info');
loadHistory();
} catch (e) {
alert('清空失败: ' + e.message);
}
}
// 持久会话
async function createPersistentSession() {
const name = document.getElementById('persistentName').value;
try {
const res = await fetch(API_BASE + '/persistent' + (name ? '?name=' + encodeURIComponent(name) : ''), {
method: 'POST'
});
const session = await res.json();
addLog('创建持久会话: ' + session.id, 'info');
document.getElementById('persistentName').value = '';
loadPersistentSessions();
} catch (e) {
alert('创建失败: ' + e.message);
}
}
async function loadPersistentSessions() {
try {
const res = await fetch(API_BASE + '/persistent');
const sessions = await res.json();
const tbody = document.getElementById('persistentTable');
tbody.innerHTML = sessions.map(s => `
<tr>
<td>${s.id.substring(0, 8)}...</td>
<td>${s.session_id.substring(0, 8)}...</td>
<td>${s.name || '-'}</td>
<td>${s.created_at || '-'}</td>
<td>${s.last_used_at || '-'}</td>
<td class="flex">
<button class="btn" style="padding: 5px 10px; font-size: 12px;" onclick="selectSession('${s.id}')">发消息</button>
<button class="btn btn-danger" style="padding: 5px 10px; font-size: 12px;" onclick="deleteSession('${s.id}')">删除</button>
</td>
</tr>
`).join('');
} catch (e) {
addLog('加载持久会话失败: ' + e.message, 'error');
}
}
function selectSession(id) {
selectedSessionId = id;
document.getElementById('messageSessionId').value = id;
document.getElementById('sendMessagePanel').style.display = 'block';
}
async function deleteSession(id) {
if (!confirm('确认删除?')) return;
try {
await fetch(API_BASE + '/persistent/' + id, {method: 'DELETE'});
addLog('删除持久会话: ' + id, 'info');
loadPersistentSessions();
} catch (e) {
alert('删除失败: ' + e.message);
}
}
async function sendMessage(async_) {
if (!selectedSessionId) return alert('请选择会话');
const text = document.getElementById('messageText').value;
if (!text) return alert('请输入消息');
try {
const endpoint = async_ ? '/message_async' : '/message';
const res = await fetch(API_BASE + '/persistent/' + selectedSessionId + endpoint + '?text=' + encodeURIComponent(text), {
method: 'POST'
});
const data = await res.json();
addLog(async_ ? '消息已发送(异步)' : '消息已发送: ' + JSON.stringify(data).substring(0, 100), 'info');
document.getElementById('messageText').value = '';
loadPersistentSessions();
} catch (e) {
alert('发送失败: ' + e.message);
}
}
// 初始化
checkHealth();
loadTasks();
loadSchedules();
loadHistory();
loadPersistentSessions();
connectWS();
setInterval(() => {
checkHealth();
loadTasks();
}, 5000);
</script>
</body>
</html>