feat: 添加前端控制台 console.html 及使用指南
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.api.routes import router
|
||||
from app.services.scheduler import scheduler_service
|
||||
from app.services.opencode_client import opencode_client
|
||||
@@ -35,6 +36,14 @@ app = FastAPI(
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
|
||||
565
console.html
Normal file
565
console.html
Normal file
@@ -0,0 +1,565 @@
|
||||
<!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>
|
||||
128
docs/console-guide.md
Normal file
128
docs/console-guide.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# XCClaw 控制台使用指南
|
||||
|
||||
## 访问地址
|
||||
|
||||
```
|
||||
http://localhost:8888/console.html
|
||||
```
|
||||
|
||||
## 界面概览
|
||||
|
||||
控制台采用 Tab 切换设计,包含以下模块:
|
||||
|
||||
| Tab | 功能 |
|
||||
|-----|------|
|
||||
| 任务管理 | 创建、执行、管理即时/持久/定时任务 |
|
||||
| 定时任务 | 创建和管理 Cron 定时任务 |
|
||||
| 历史记录 | 查看任务执行历史 |
|
||||
| 持久会话 | 创建和管理多轮对话会话 |
|
||||
| 实时日志 | WebSocket 实时推送的任务状态 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 任务管理
|
||||
|
||||
### 创建任务
|
||||
|
||||
1. 选择任务类型:
|
||||
- **Ephemeral (即时任务)**:一次性任务,执行后结束
|
||||
- **Persistent (持久任务)**:可多轮交互的任务
|
||||
- **Scheduled (定时任务)**:需要配合定时任务模块使用
|
||||
|
||||
2. 输入任务描述 (Prompt)
|
||||
|
||||
3. 点击「创建任务」
|
||||
|
||||
### 执行任务
|
||||
|
||||
创建任务后,在任务列表中可以:
|
||||
|
||||
| 按钮 | 功能 |
|
||||
|------|------|
|
||||
| 执行 | 同步执行任务,等待完成 |
|
||||
| 异步 | 异步执行任务,立即返回 |
|
||||
| 中止 | 终止正在执行的任务 |
|
||||
|
||||
### 任务状态
|
||||
|
||||
- `pending` - 待执行
|
||||
- `running` - 执行中
|
||||
- `completed` - 已完成
|
||||
- `failed` - 执行失败
|
||||
- `aborted` - 已中止
|
||||
|
||||
---
|
||||
|
||||
## 2. 定时任务
|
||||
|
||||
### 创建定时任务
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| 任务ID | 唯一标识符,如 `daily_task` |
|
||||
| 任务名称 | 显示名称,如「每日任务」 |
|
||||
| Cron 表达式 | 执行时间,如 `0 9 * * *` (每天9点) |
|
||||
| 执行内容 | 定时执行的 Prompt |
|
||||
| 启用 | 是否启用此定时任务 |
|
||||
|
||||
### 常用 Cron 示例
|
||||
|
||||
| 表达式 | 含义 |
|
||||
|--------|------|
|
||||
| `0 9 * * *` | 每天 9:00 |
|
||||
| `0 */2 * * *` | 每隔 2 小时 |
|
||||
| `0 9 * * 1-5` | 工作日 9:00 |
|
||||
| `*/30 * * * *` | 每隔 30 分钟 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 历史记录
|
||||
|
||||
- 查看所有已完成的任务记录
|
||||
- 支持限制显示条数
|
||||
- 可清空历史记录
|
||||
|
||||
---
|
||||
|
||||
## 4. 持久会话
|
||||
|
||||
### 创建会话
|
||||
|
||||
输入会话名称(可选),点击「创建」
|
||||
|
||||
### 发送消息
|
||||
|
||||
1. 在列表中找到目标会话
|
||||
2. 点击「发消息」按钮
|
||||
3. 输入消息内容
|
||||
4. 选择发送方式:
|
||||
- **同步发送**:等待响应返回
|
||||
- **异步发送**:立即返回,任务在后台执行
|
||||
|
||||
---
|
||||
|
||||
## 5. 实时日志
|
||||
|
||||
通过 WebSocket 实时显示:
|
||||
|
||||
- 任务状态变化
|
||||
- WebSocket 连接状态
|
||||
- 操作日志
|
||||
|
||||
---
|
||||
|
||||
## 快捷操作
|
||||
|
||||
- **自动刷新**:健康检查和任务列表每 5 秒自动刷新
|
||||
- **WebSocket**:页面左上角显示连接状态
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 问题 | 解决方法 |
|
||||
|------|----------|
|
||||
| 页面无法访问 | 确认 HTTP 服务运行在 8888 端口 |
|
||||
| API 请求失败 | 确认 XCClaw 服务运行在 3005 端口 |
|
||||
| WebSocket 未连接 | 检查网络连接,刷新页面 |
|
||||
| 任务执行失败 | 查看健康检查确认 OpenCode 服务正常 |
|
||||
Reference in New Issue
Block a user