auto-save 2026-04-01 09:03 (+8, ~2)

This commit is contained in:
2026-04-01 09:04:04 +08:00
parent 0ddaa889de
commit 9709573870
70 changed files with 2331 additions and 9 deletions

192
web/templates/index.html Normal file
View File

@@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phone GUI Agent</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0a0a0a; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
header { padding: 12px 20px; background: #111; border-bottom: 1px solid #222; display: flex; align-items: center; gap: 12px; }
header h1 { font-size: 16px; font-weight: 600; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
.status-dot.connected { background: #22c55e; }
.status-dot.running { background: #f59e0b; animation: pulse 1s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
#device-info { font-size: 12px; color: #888; margin-left: auto; }
.main { flex: 1; display: flex; overflow: hidden; }
.panel-left { width: 320px; border-right: 1px solid #222; display: flex; flex-direction: column; }
.panel-center { flex: 1; display: flex; align-items: center; justify-content: center; background: #050505; }
.panel-right { width: 380px; border-left: 1px solid #222; display: flex; flex-direction: column; }
.phone-frame { width: 270px; height: 585px; border: 2px solid #333; border-radius: 24px; overflow: hidden; background: #111; position: relative; }
.phone-frame img { width: 100%; height: 100%; object-fit: contain; }
.phone-frame .placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: #444; font-size: 14px; }
.task-input { padding: 16px; border-bottom: 1px solid #222; }
.task-input textarea { width: 100%; height: 80px; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; color: #e0e0e0; padding: 10px; font-size: 14px; resize: none; }
.task-input textarea:focus { outline: none; border-color: #4a9eff; }
.btn-row { display: flex; gap: 8px; margin-top: 8px; }
.btn { padding: 8px 16px; border-radius: 6px; border: none; cursor: pointer; font-size: 13px; font-weight: 500; }
.btn-primary { background: #4a9eff; color: #fff; }
.btn-primary:hover { background: #3a8eef; }
.btn-danger { background: #ef4444; color: #fff; }
.btn-secondary { background: #333; color: #ccc; }
.steps-list { flex: 1; overflow-y: auto; padding: 12px; }
.step-card { background: #1a1a1a; border: 1px solid #222; border-radius: 8px; padding: 12px; margin-bottom: 8px; font-size: 13px; }
.step-card .step-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
.step-num { color: #4a9eff; font-weight: 600; }
.step-action { color: #22c55e; font-family: monospace; }
.step-action.error { color: #ef4444; }
.step-obs { color: #999; margin-top: 4px; }
.step-think { color: #f59e0b; margin-top: 4px; font-style: italic; }
.log-panel { flex: 1; overflow-y: auto; padding: 12px; }
.log-panel h3 { font-size: 13px; color: #888; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
</style>
</head>
<body>
<header>
<div class="status-dot" id="statusDot"></div>
<h1>Phone GUI Agent</h1>
<span id="device-info">检测设备中...</span>
</header>
<div class="main">
<div class="panel-left">
<div class="task-input">
<textarea id="taskInput" placeholder="输入任务指令,例如:&#10;打开设置连接WiFi&#10;打开微信,搜索张三发消息"></textarea>
<div class="btn-row">
<button class="btn btn-primary" id="btnRun" onclick="runTask()">执行任务</button>
<button class="btn btn-danger" id="btnStop" onclick="stopTask()" style="display:none">停止</button>
<button class="btn btn-secondary" onclick="refreshScreenshot()">截屏</button>
</div>
</div>
<div class="steps-list" id="stepsList"></div>
</div>
<div class="panel-center">
<div class="phone-frame">
<img id="phoneScreen" style="display:none" />
<div class="placeholder" id="phonePlaceholder">连接设备后显示截图</div>
</div>
</div>
<div class="panel-right">
<div class="log-panel">
<h3>Agent 思考过程</h3>
<div id="thinkingLog"></div>
</div>
</div>
</div>
<script>
let ws = null;
async function checkDevice() {
try {
const resp = await fetch('/api/device');
const data = await resp.json();
const dot = document.getElementById('statusDot');
const info = document.getElementById('device-info');
if (data.connected) {
dot.className = 'status-dot connected';
info.textContent = `${data.model} (${data.resolution}) - ${data.serial}`;
refreshScreenshot();
} else {
dot.className = 'status-dot';
info.textContent = data.error || '未连接设备';
}
} catch (e) {
document.getElementById('device-info').textContent = '服务未启动';
}
}
async function refreshScreenshot() {
try {
const resp = await fetch('/api/screenshot');
const data = await resp.json();
if (data.ok) {
const img = document.getElementById('phoneScreen');
img.src = 'data:image/png;base64,' + data.image;
img.style.display = 'block';
document.getElementById('phonePlaceholder').style.display = 'none';
}
} catch (e) {}
}
function runTask() {
const task = document.getElementById('taskInput').value.trim();
if (!task) return;
document.getElementById('stepsList').innerHTML = '';
document.getElementById('thinkingLog').innerHTML = '';
document.getElementById('btnRun').style.display = 'none';
document.getElementById('btnStop').style.display = 'inline-block';
document.getElementById('statusDot').className = 'status-dot running';
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${location.host}/ws/task`);
ws.onopen = () => {
ws.send(JSON.stringify({ task }));
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.status === 'step') {
addStep(data);
} else if (data.status === 'completed' || data.status === 'failed' || data.status === 'stopped') {
taskDone(data);
}
};
ws.onclose = () => taskDone({ status: 'disconnected' });
}
function addStep(data) {
const list = document.getElementById('stepsList');
const card = document.createElement('div');
card.className = 'step-card';
card.innerHTML = `
<div class="step-header">
<span class="step-num">Step ${data.step}</span>
<span class="step-action ${data.error ? 'error' : ''}">${data.error || data.action_desc || data.action_type}</span>
</div>
${data.observation ? `<div class="step-obs">${data.observation}</div>` : ''}
${data.thinking ? `<div class="step-think">${data.thinking}</div>` : ''}
`;
list.appendChild(card);
list.scrollTop = list.scrollHeight;
if (data.thinking) {
const log = document.getElementById('thinkingLog');
const p = document.createElement('div');
p.className = 'step-card';
p.innerHTML = `<span class="step-num">Step ${data.step}</span>: ${data.thinking}`;
log.appendChild(p);
log.scrollTop = log.scrollHeight;
}
refreshScreenshot();
}
function taskDone(data) {
document.getElementById('btnRun').style.display = 'inline-block';
document.getElementById('btnStop').style.display = 'none';
document.getElementById('statusDot').className = 'status-dot connected';
if (ws) { ws.close(); ws = null; }
}
async function stopTask() {
await fetch('/api/stop', { method: 'POST' });
}
checkDevice();
setInterval(checkDevice, 10000);
</script>
</body>
</html>