Files
workphone-sdk/sdk/app/static/index.html
Manus AI 5919659930 feat: 控制台 WS 全量检测 · APK 桌面「工作手机」入口
nav_cache 模块单例;index 增加 Hub 链接与 hook 探测;SetupActivity 注册 Launcher。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 09:19:12 +08:00

703 lines
29 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>工作手机 · 控制中心</title>
<style>
:root {
--glass-bg: rgba(255,255,255,0.12);
--glass-border: rgba(255,255,255,0.18);
--glass-shadow: 0 8px 32px rgba(0,0,0,0.25);
--accent: #007AFF;
--accent-hover: #0a84ff;
--text: #f5f5f7;
--text-secondary: rgba(245,245,247,0.6);
--green: #30d158;
--red: #ff453a;
--orange: #ff9f0a;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh; color: var(--text);
}
/* 顶栏 */
.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 32px;
background: rgba(0,0,0,0.3);
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
position: sticky; top: 0; z-index: 100;
}
.topbar h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.3px; }
.topbar .status { display: flex; gap: 16px; align-items: center; font-size: 13px; }
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; }
.dot-green { background: var(--green); box-shadow: 0 0 6px var(--green); }
.dot-red { background: var(--red); }
.dot-orange { background: var(--orange); }
/* 主布局 */
.main { display: grid; grid-template-columns: 280px 1fr 320px; gap: 20px; padding: 20px 32px; min-height: calc(100vh - 60px); }
/* 玻璃卡片 */
.card {
background: var(--glass-bg);
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
border-radius: 16px;
border: 1px solid var(--glass-border);
box-shadow: var(--glass-shadow);
padding: 20px;
transition: transform .2s, box-shadow .2s;
}
.card:hover { transform: translateY(-2px); box-shadow: 0 12px 40px rgba(0,0,0,0.35); }
.card h2 { font-size: 15px; font-weight: 600; margin-bottom: 16px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: .5px; }
/* 左侧:设备列表 */
.devices { display: flex; flex-direction: column; gap: 12px; }
.device-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 14px; border-radius: 12px; cursor: pointer;
background: rgba(255,255,255,0.06);
border: 1px solid transparent;
transition: all .15s;
}
.device-item:hover, .device-item.active { background: rgba(0,122,255,0.15); border-color: var(--accent); }
.device-icon { font-size: 28px; }
.device-info { flex: 1; }
.device-name { font-size: 14px; font-weight: 500; }
.device-meta { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
/* 中间:操作区 */
.center { display: flex; flex-direction: column; gap: 20px; }
/* 快捷操作网格 */
.actions-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.action-btn {
display: flex; flex-direction: column; align-items: center; gap: 8px;
padding: 18px 8px; border-radius: 14px; cursor: pointer;
background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
color: var(--text); font-size: 12px; font-weight: 500;
transition: all .15s;
}
.action-btn:hover { background: rgba(0,122,255,0.2); border-color: var(--accent); }
.action-btn .icon { font-size: 28px; }
/* 命令输入 */
.cmd-input-wrap {
display: flex; gap: 10px;
}
.cmd-input {
flex: 1; padding: 12px 16px; border-radius: 12px;
background: rgba(255,255,255,0.08); border: 1px solid var(--glass-border);
color: var(--text); font-size: 14px; outline: none;
}
.cmd-input::placeholder { color: var(--text-secondary); }
.cmd-input:focus { border-color: var(--accent); }
.cmd-send {
padding: 12px 24px; border-radius: 12px; border: none;
background: var(--accent); color: #fff; font-weight: 600; cursor: pointer;
transition: background .15s;
}
.cmd-send:hover { background: var(--accent-hover); }
/* 日志 */
.log-area {
flex: 1; min-height: 200px; max-height: 400px; overflow-y: auto;
padding: 14px; border-radius: 12px;
background: rgba(0,0,0,0.3); font-family: 'SF Mono', monospace;
font-size: 12px; line-height: 1.6; color: #a0d8a0;
}
.log-area .log-error { color: var(--red); }
.log-area .log-info { color: #7ec8e3; }
/* 右侧:设备详情 + 截屏 */
.detail-section { display: flex; flex-direction: column; gap: 16px; }
.screenshot-wrap {
position: relative; width: 100%; aspect-ratio: 9/19.5;
border-radius: 24px; overflow: hidden;
background: #000; border: 3px solid rgba(255,255,255,0.15);
}
.screenshot-wrap img { width: 100%; height: 100%; object-fit: cover; }
.screenshot-placeholder {
display: flex; align-items: center; justify-content: center;
width: 100%; height: 100%; color: var(--text-secondary); font-size: 13px;
}
.detail-table { width: 100%; font-size: 13px; }
.detail-table td { padding: 6px 0; }
.detail-table td:first-child { color: var(--text-secondary); width: 80px; }
/* 模式切换按钮 */
.mode-btn {
padding: 6px 14px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15);
background: rgba(255,255,255,0.05); color: var(--text-secondary);
font-size: 12px; cursor: pointer; transition: all .15s;
}
.mode-btn:hover { background: rgba(255,255,255,0.1); }
.mode-btn.active { background: rgba(0,122,255,0.2); border-color: var(--accent); color: var(--text); }
/* AI 步骤展示 */
.ai-steps { margin-top: 4px; padding-left: 16px; font-size: 11px; color: var(--text-secondary); }
.ai-step-ok { color: var(--green); }
.ai-step-fail { color: var(--red); }
/* 连接协议面板 */
.protocol-box {
background: rgba(0,0,0,0.28);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 12px;
padding: 12px;
font-size: 12px;
line-height: 1.6;
}
.protocol-line { color: var(--text-secondary); margin-bottom: 4px; }
.protocol-line b { color: var(--text); font-weight: 600; }
.mini-table {
width: 100%;
margin-top: 8px;
border-collapse: collapse;
font-size: 11px;
}
.mini-table th, .mini-table td {
border-bottom: 1px solid rgba(255,255,255,0.08);
padding: 6px 4px;
text-align: left;
}
.mini-table th { color: var(--text-secondary); font-weight: 500; }
/* 移动端适配 */
@media(max-width:900px){
.main { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="topbar">
<h1>📱 工作手机 · 控制中心</h1>
<div class="status">
<span id="sdk-status"><span class="dot dot-red"></span>SDK 检测中</span>
<span id="device-count">0 台设备</span>
<a href="/static/hub.html" style="color:#7ec8e3;font-size:13px;text-decoration:none;margin-left:12px;">工作台</a>
<a href="#" onclick="runWsFullCheck();return false;" style="color:#30d158;font-size:13px;text-decoration:none;margin-left:8px;">WS全量检测</a>
</div>
</div>
<div class="main">
<!-- 左侧:设备列表 -->
<div class="card devices-panel">
<h2>📋 设备列表</h2>
<div class="devices" id="device-list">
<div style="color:var(--text-secondary); font-size:13px;">加载中...</div>
</div>
</div>
<!-- 中间:操作 -->
<div class="center">
<div class="card">
<h2>⚡ 快捷操作</h2>
<div class="actions-grid">
<div class="action-btn" onclick="doAction('screenshot')"><span class="icon">📸</span>截屏</div>
<div class="action-btn" onclick="doAction('open_wechat')"><span class="icon">💬</span>打开微信</div>
<div class="action-btn" onclick="doAction('open_douyin')"><span class="icon">🎵</span>打开抖音</div>
<div class="action-btn" onclick="doAction('open_xhs')"><span class="icon">📕</span>打开小红书</div>
<div class="action-btn" onclick="doAction('open_xianyu')"><span class="icon">🐟</span>打开闲鱼</div>
<div class="action-btn" onclick="doAction('home')"><span class="icon">🏠</span>回主页</div>
<div class="action-btn" onclick="doAction('back')"><span class="icon">⬅️</span>返回</div>
<div class="action-btn" onclick="doAction('ui_tree')"><span class="icon">🌳</span>UI 树</div>
<div class="action-btn" onclick="doAction('device_info')"><span class="icon"></span>设备信息</div>
</div>
</div>
<div class="card">
<h2>🤖 AI 对话控制 <span id="ai-badge" style="font-size:11px; padding:2px 8px; border-radius:8px; background:rgba(48,209,88,0.2); color:var(--green); font-weight:400;">检测中</span></h2>
<div style="display:flex; gap:8px; margin-bottom:10px;">
<button id="mode-pattern" class="mode-btn active" onclick="setMode('pattern')">⚡ 模式匹配</button>
<button id="mode-ai" class="mode-btn" onclick="setMode('ai')">🧠 AI 智能</button>
<button id="mode-ai-only" class="mode-btn" onclick="setMode('ai-only')">💡 AI 纯解析</button>
</div>
<div class="cmd-input-wrap">
<input class="cmd-input" id="cmd-input" placeholder="输入指令:打开微信给张三发消息说下午开会" onkeydown="if(event.key==='Enter')sendCmd()">
<button class="cmd-send" onclick="sendCmd()">发送</button>
</div>
<div id="mode-hint" style="font-size:11px; color:var(--text-secondary); margin-top:8px; line-height:1.6;">
<b>模式匹配</b>: 打开[APP] · 点击[文字] · 输入[内容] · 返回/主页 · 截图<br>
复杂命令自动升级为 AI 解析 · 多步用 <b>;</b><b>然后</b> 分隔
</div>
</div>
<div class="card">
<h2>🔌 连接方式与协议</h2>
<div class="protocol-box">
<div class="protocol-line"><b>WS 入口</b> <span id="p-ws-path">加载中...</span></div>
<div class="protocol-line"><b>Hook 流</b> <span id="p-hook-path">加载中...</span></div>
<div class="protocol-line"><b>在线设备</b> <span id="p-online-count">-</span> | <b>ADB</b> <span id="p-adb-count">-</span></div>
<div class="protocol-line"><b>心跳建议</b> <span id="p-heartbeat">-</span></div>
<table class="mini-table">
<thead>
<tr><th>设备</th><th>项目</th><th>状态</th><th>心跳延迟(s)</th></tr>
</thead>
<tbody id="p-device-rows">
<tr><td colspan="4" style="color:var(--text-secondary);">暂无数据</td></tr>
</tbody>
</table>
</div>
</div>
<div class="card" style="flex:1;">
<h2>📝 实时日志</h2>
<div class="log-area" id="log-area"></div>
</div>
</div>
<!-- 右侧:设备详情 -->
<div class="detail-section">
<div class="card">
<h2>📱 设备截屏</h2>
<div class="screenshot-wrap" id="screenshot-wrap">
<div class="screenshot-placeholder">点击「截屏」查看</div>
</div>
</div>
<div class="card">
<h2>📊 设备详情</h2>
<table class="detail-table" id="detail-table">
<tr><td>设备 ID</td><td id="d-id">-</td></tr>
<tr><td>型号</td><td id="d-model">-</td></tr>
<tr><td>Android</td><td id="d-android">-</td></tr>
<tr><td>状态</td><td id="d-status">-</td></tr>
<tr><td>已装 APP</td><td id="d-apps">-</td></tr>
<tr><td>能力</td><td id="d-caps">-</td></tr>
</table>
</div>
<div class="card">
<h2>🛡️ 连接守护</h2>
<div id="guard-status" style="font-size:12px; margin-bottom:10px;">
<span id="guard-badge" style="padding:3px 10px; border-radius:8px; background:rgba(255,255,255,0.08); color:var(--text-secondary); font-size:11px;">等待数据</span>
</div>
<div id="guard-stats" style="display:grid; grid-template-columns:1fr 1fr; gap:6px; margin-bottom:12px;">
<div style="background:rgba(255,255,255,0.05); border-radius:8px; padding:8px 10px; text-align:center;">
<div style="font-size:20px; font-weight:600;" id="guard-popup-count">0</div>
<div style="font-size:10px; color:var(--text-secondary);">弹窗已处理</div>
</div>
<div style="background:rgba(255,255,255,0.05); border-radius:8px; padding:8px 10px; text-align:center;">
<div style="font-size:20px; font-weight:600;" id="guard-cycle-count">0</div>
<div style="font-size:10px; color:var(--text-secondary);">守护周期</div>
</div>
</div>
<div id="guard-events" style="max-height:180px; overflow-y:auto; font-size:11px; line-height:1.8; font-family:'SF Mono',monospace; background:rgba(0,0,0,0.2); border-radius:8px; padding:8px;">
<div style="color:var(--text-secondary);">暂无守护事件</div>
</div>
</div>
</div>
</div>
<script>
const API = location.origin;
const API_HEADERS = {'X-API-Key': 'workphone-secret-key'};
let currentDevice = null;
let cmdMode = 'pattern'; // pattern | ai | ai-only
// 日志
function log(msg, type='') {
const el = document.getElementById('log-area');
const cls = type === 'error' ? 'log-error' : type === 'info' ? 'log-info' : '';
const ts = new Date().toLocaleTimeString('zh-CN', {hour12:false});
el.innerHTML += `<div class="${cls}">[${ts}] ${msg}</div>`;
el.scrollTop = el.scrollHeight;
}
// 健康检查
async function checkHealth() {
try {
const r = await fetch(`${API}/health`, {headers: API_HEADERS});
const d = await r.json();
document.getElementById('sdk-status').innerHTML = `<span class="dot dot-green"></span>SDK 在线`;
document.getElementById('device-count').textContent = `${d.devices_online} 台在线 | ${d.adb_devices} ADB`;
log(`SDK 在线: ${d.devices_online} 台 WebSocket, ${d.adb_devices} 台 ADB`, 'info');
return d;
} catch(e) {
document.getElementById('sdk-status').innerHTML = `<span class="dot dot-red"></span>SDK 离线`;
log(`SDK 连接失败: ${e.message}`, 'error');
return null;
}
}
// 设备列表
async function loadDevices() {
try {
const r = await fetch(`${API}/api/v3/devices`, {headers: API_HEADERS});
const d = await r.json();
const devices = d.data || [];
const el = document.getElementById('device-list');
if (!devices.length) {
el.innerHTML = '<div style="color:var(--text-secondary);font-size:13px;">暂无设备在线</div>';
return;
}
el.innerHTML = devices.map(dev => {
const statusDot = dev.status==='online' ? 'dot-green' : dev.status==='adb' ? 'dot-green' : 'dot-orange';
const statusLabel = dev.status==='online' ? 'WS在线' : dev.status==='adb' ? 'ADB直连' : '离线';
const brand = dev.brand && dev.brand !== 'Unknown' ? `${dev.brand} ` : '';
return `
<div class="device-item ${currentDevice===dev.device_id?'active':''}" onclick="selectDevice('${dev.device_id}')">
<span class="device-icon">📱</span>
<div class="device-info">
<div class="device-name">${brand}${dev.model||dev.device_id}</div>
<div class="device-meta">${dev.device_id} · ${statusLabel}</div>
</div>
<span class="dot ${statusDot}"></span>
</div>`;
}).join('');
if (!currentDevice && devices.length) selectDevice(devices[0].device_id);
} catch(e) { log(`设备列表加载失败: ${e.message}`, 'error'); }
}
function selectDevice(id) {
currentDevice = id;
document.querySelectorAll('.device-item').forEach(el => el.classList.toggle('active', el.textContent.includes(id)));
loadDeviceDetail(id);
log(`已选设备: ${id}`, 'info');
}
async function loadDeviceDetail(id) {
try {
const r = await fetch(`${API}/api/v3/devices/${id}`, {headers: API_HEADERS});
const d = await r.json();
const dev = d.device || d.data || d;
document.getElementById('d-id').textContent = dev.device_id || id;
document.getElementById('d-model').textContent = dev.model || '-';
document.getElementById('d-android').textContent = dev.android_version || '-';
document.getElementById('d-status').textContent = dev.status || (d.online ? '在线' : '离线');
document.getElementById('d-apps').textContent = (dev.installed_apps||[]).join(', ') || '-';
document.getElementById('d-caps').textContent = (dev.capabilities||[]).slice(0,6).join(', ') || '-';
} catch(e) {}
}
// 操作
async function doAction(action) {
if (!currentDevice) { log('请先选择设备', 'error'); return; }
log(`执行: ${action}`, 'info');
// ADB 路由映射(直接操作设备,不需 WebSocket
if (action === 'screenshot') {
try {
const r = await fetch(`${API}/api/v3/adb/devices/${currentDevice}/screenshot`, {method:'POST', headers: API_HEADERS});
const d = await r.json();
if (d.data?.base64) {
document.getElementById('screenshot-wrap').innerHTML = `<img src="data:image/png;base64,${d.data.base64}">`;
log(`截屏成功 ${d.data.width}x${d.data.height}`, 'info');
} else { log(`截屏失败: ${JSON.stringify(d).slice(0,200)}`, 'error'); }
} catch(e) { log(`截屏失败: ${e.message}`, 'error'); }
return;
}
if (action === 'ui_tree') {
try {
const r = await fetch(`${API}/api/v3/adb/devices/${currentDevice}/ui-tree`, {headers: API_HEADERS});
const d = await r.json();
log(`UI 树: ${(d.data?.xml||'').length} 字符`, 'info');
} catch(e) { log(`UI树失败: ${e.message}`, 'error'); }
return;
}
// 通用 ADB 操作URL 与后端路由一致)
const appMap = {
open_wechat: {url:`/api/v3/adb/devices/${currentDevice}/app/start`, body:{package:'com.tencent.mm'}},
open_douyin: {url:`/api/v3/adb/devices/${currentDevice}/app/start`, body:{package:'com.ss.android.ugc.aweme'}},
open_xhs: {url:`/api/v3/adb/devices/${currentDevice}/app/start`, body:{package:'com.xingin.xhs'}},
open_xianyu: {url:`/api/v3/adb/devices/${currentDevice}/app/start`, body:{package:'com.taobao.idlefish'}},
home: {url:`/api/v3/adb/devices/${currentDevice}/key`, body:{key:'home'}},
back: {url:`/api/v3/adb/devices/${currentDevice}/key`, body:{key:'back'}},
device_info: {url:`/api/v3/adb/devices/${currentDevice}`, method:'GET'},
};
const op = appMap[action];
if (!op) { log(`未知操作: ${action}`, 'error'); return; }
try {
const opts = op.method === 'GET'
? {}
: {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(op.body||{})};
const r = await fetch(`${API}${op.url}`, {...opts, headers: {...API_HEADERS, ...opts.headers}});
const d = await r.json();
log(`结果: ${JSON.stringify(d).slice(0,200)}`, d.success===false?'error':'');
// 操作后自动截屏刷新
setTimeout(() => doAction('screenshot'), 1200);
} catch(e) { log(`执行失败: ${e.message}`, 'error'); }
}
// AI 状态
async function checkAI() {
try {
const r = await fetch(`${API}/api/v3/ai/status`, {headers: API_HEADERS});
const d = await r.json();
const info = d.data || {};
const badge = document.getElementById('ai-badge');
if (info.available) {
badge.textContent = `${info.backend} · ${info.model}`;
badge.style.background = 'rgba(48,209,88,0.2)';
badge.style.color = 'var(--green)';
} else {
badge.textContent = '离线';
badge.style.background = 'rgba(255,69,58,0.2)';
badge.style.color = 'var(--red)';
}
} catch(e) {
document.getElementById('ai-badge').textContent = '不可用';
}
}
// 连接协议
async function loadConnectionProtocol() {
try {
const r = await fetch(`${API}/api/v3/connection/protocol`, {headers: API_HEADERS});
const d = await r.json();
const data = d.data || {};
document.getElementById('p-heartbeat').textContent =
(data.heartbeat?.recommended_interval_seconds || []).join(' / ') + ' 秒';
} catch (e) {}
}
// 连接状态
async function loadConnectionStatus() {
try {
const r = await fetch(`${API}/api/v3/connection/status`, {headers: API_HEADERS});
const d = await r.json();
const data = d.data || {};
document.getElementById('p-ws-path').textContent = data.ws_path || '-';
document.getElementById('p-hook-path').textContent = data.hook_events_stream || '-';
document.getElementById('p-online-count').textContent = data.online_ws_count ?? '-';
document.getElementById('p-adb-count').textContent = data.adb_count ?? '-';
const rows = data.devices || [];
const tbody = document.getElementById('p-device-rows');
if (!rows.length) {
tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-secondary);">暂无在线设备</td></tr>';
return;
}
tbody.innerHTML = rows.slice(0, 8).map((x) => `
<tr>
<td>${x.device_id || '-'}</td>
<td>${x.project_id || '-'}</td>
<td>${x.status || '-'}</td>
<td>${typeof x.heartbeat_age_seconds === 'number' ? x.heartbeat_age_seconds : '-'}</td>
</tr>
`).join('');
} catch (e) {}
}
// 模式切换
function setMode(mode) {
cmdMode = mode;
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
document.getElementById(`mode-${mode}`).classList.add('active');
const hint = document.getElementById('mode-hint');
const input = document.getElementById('cmd-input');
if (mode === 'pattern') {
hint.innerHTML = '<b>模式匹配</b>: 打开[APP] · 点击[文字] · 输入[内容] · 返回/主页 · 截图<br>复杂命令自动升级为 AI 解析 · 多步用 <b>;</b> 或 <b>然后</b> 分隔';
input.placeholder = '输入指令:打开微信;然后截图';
} else if (mode === 'ai') {
hint.innerHTML = '<b>AI 智能</b>: 用自然语言描述任意操作AI 自动解析并执行<br>例: "打开Chrome搜索今天天气" · "打开微信给张三发消息"';
input.placeholder = '用自然语言控制手机:打开微信给文件传输助手发消息说你好';
} else {
hint.innerHTML = '<b>AI 纯解析</b>: 只解析不执行,查看 AI 生成的操作步骤<br>适合预览和调试 AI 命令理解能力';
input.placeholder = '输入要解析的指令:打开淘宝搜索蓝牙耳机';
}
}
// 发送命令
async function sendCmd() {
const input = document.getElementById('cmd-input');
const task = input.value.trim();
if (!task) return;
if (!currentDevice && cmdMode !== 'ai-only') { log('请先选择设备', 'error'); return; }
input.value = '';
const modeLabel = cmdMode === 'pattern' ? '模式匹配' : cmdMode === 'ai' ? 'AI 智能' : 'AI 解析';
log(`>>> [${modeLabel}] ${task}`, 'info');
const startTime = Date.now();
try {
let r, d;
if (cmdMode === 'pattern') {
// 模式匹配(自动 AI fallback
r = await fetch(`${API}/api/v3/agent/execute`, {
method: 'POST', headers: {'Content-Type':'application/json', ...API_HEADERS},
body: JSON.stringify({device_id: currentDevice, task})
});
d = await r.json();
const data = d.data || {};
const engine = data.engine === 'ai' ? ' [AI]' : '';
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
if (data.success) {
const steps = (data.steps || []).join(' → ');
log(`${engine} ${steps || '完成'} (${elapsed}s)`, 'info');
} else {
const steps = (data.steps || []).join(' → ');
log(`${engine} ${data.error || steps || '执行失败'} (${elapsed}s)`, 'error');
}
} else {
// AI 模式
const body = {message: task};
if (cmdMode === 'ai' && currentDevice) body.device_id = currentDevice;
r = await fetch(`${API}/api/v3/ai/chat`, {
method: 'POST', headers: {'Content-Type':'application/json', ...API_HEADERS},
body: JSON.stringify(body)
});
d = await r.json();
const data = d.data || {};
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
const actionsStr = (data.actions || []).map(a => {
const detail = a.app || a.text || a.key || a.direction || (a.seconds ? a.seconds+'s' : '');
return `${a.action}${detail ? ':'+detail : ''}`;
}).join(' → ');
log(`🧠 ${data.message} (${elapsed}s)`, 'info');
if (actionsStr) log(` 步骤: ${actionsStr}`, 'info');
// 显示执行结果
if (data.executed && data.executed.length > 0) {
data.executed.forEach(e => {
const icon = e.success ? '✅' : '❌';
log(` ${icon} ${e.step}`, e.success ? '' : 'error');
});
}
}
// 命令执行后自动截屏刷新(仅执行模式)
if (cmdMode !== 'ai-only' && !task.includes('截图')) {
setTimeout(() => doAction('screenshot'), 1500);
}
} catch(e) { log(`任务失败: ${e.message}`, 'error'); }
}
// ─── 连接守护面板 ───
let guardPopupTotal = 0;
let guardCycleTotal = 0;
const guardEventLog = [];
const MAX_GUARD_EVENTS = 50;
function addGuardEvent(text, type='info') {
const ts = new Date().toLocaleTimeString('zh-CN', {hour12:false});
const color = type === 'alert' ? 'var(--red)' : type === 'popup' ? 'var(--orange)' : '#a0d8a0';
guardEventLog.unshift({ts, text, color});
if (guardEventLog.length > MAX_GUARD_EVENTS) guardEventLog.pop();
renderGuardEvents();
}
function renderGuardEvents() {
const el = document.getElementById('guard-events');
if (!guardEventLog.length) {
el.innerHTML = '<div style="color:var(--text-secondary);">暂无守护事件</div>';
return;
}
el.innerHTML = guardEventLog.map(e =>
`<div style="color:${e.color}">[${e.ts}] ${e.text}</div>`
).join('');
}
function updateGuardBadge(guardData) {
const badge = document.getElementById('guard-badge');
if (!guardData) {
badge.textContent = '未激活';
badge.style.background = 'rgba(255,255,255,0.08)';
badge.style.color = 'var(--text-secondary)';
return;
}
const fc = guardData.full_cycle;
if (fc && fc.all_ok === false) {
badge.textContent = '⚠️ 异常';
badge.style.background = 'rgba(255,69,58,0.2)';
badge.style.color = 'var(--red)';
} else {
badge.textContent = '✅ 正常';
badge.style.background = 'rgba(48,209,88,0.2)';
badge.style.color = 'var(--green)';
}
}
function processGuardFromHeartbeat(guard) {
if (!guard) return;
if (guard.popups && guard.popups.count > 0) {
guardPopupTotal += guard.popups.count;
document.getElementById('guard-popup-count').textContent = guardPopupTotal;
(guard.popups.dismissed || []).forEach(d => {
addGuardEvent(`🛡️ 弹窗处理: 「${d.popup}」→ 点击「${d.clicked}`, 'popup');
});
}
if (guard.full_cycle) {
guardCycleTotal++;
document.getElementById('guard-cycle-count').textContent = guardCycleTotal;
const fc = guard.full_cycle;
if (fc.all_ok) {
addGuardEvent('✅ 守护周期: 全部正常');
} else {
const issues = [];
if (!fc.u2?.alive) issues.push('u2异常');
if (!fc.network?.network) issues.push('网络断开');
addGuardEvent(`⚠️ 守护周期: ${issues.join(', ')}`, 'alert');
}
}
updateGuardBadge(guard);
}
// 守护事件 API 轮询
async function pollGuardEvents() {
if (!currentDevice) return;
try {
const r = await fetch(`${API}/api/v3/devices/${currentDevice}/guard-events`, {headers: API_HEADERS});
if (r.ok) {
const d = await r.json();
const events = d.data?.events || [];
events.forEach(evt => {
if (evt.type === 'popup_dismissed') {
(evt.dismissed || []).forEach(d => {
guardPopupTotal++;
addGuardEvent(`🛡️ 弹窗: 「${d.popup}」→「${d.clicked}`, 'popup');
});
} else if (evt.type === 'guard_alert') {
addGuardEvent(`⚠️ ${JSON.stringify(evt.issues || {}).slice(0,80)}`, 'alert');
}
});
document.getElementById('guard-popup-count').textContent = guardPopupTotal;
}
} catch(e) {}
}
// WS + Hook 全量检测(控制台一键,对应 scripts/ws_full_test.py
async function runWsFullCheck() {
const EXPECT = 'xgfe65eimrrofyws';
log('开始 WS 全量检测…', 'info');
try {
const st = await fetch(`${API}/api/v3/connection/status`, {headers: API_HEADERS}).then(r => r.json());
const data = st.data || {};
const onWs = (data.online_device_ids || []).includes(EXPECT);
const onAdb = (data.adb_serials || []).includes(EXPECT);
log(`connection/status: WS=${data.online_ws_count} ADB=${data.adb_count}`, 'info');
log(`指定设备 ${EXPECT}: WS=${onWs ? '在线' : '离线'} ADB=${onAdb ? '在线' : '离线'}`, onWs || onAdb ? 'info' : 'error');
const probe = await fetch(`${API}/api/v3/hook/probe/${EXPECT}`, {headers: API_HEADERS}).then(r => r.json());
log(`hook/probe: supports_hook=${probe.supports_hook} connect=${probe.hook_tests?.connect || '-'}`, probe.supports_hook ? 'info' : 'error');
if (onWs || onAdb) {
const ex = await fetch(`${API}/api/v3/hook/execute`, {
method: 'POST', headers: {...API_HEADERS, 'Content-Type': 'application/json'},
body: JSON.stringify({device_id: EXPECT, platform: 'wechat', action: 'get_profile', params: {}, hook_only: true})
}).then(r => r.json());
log(`hook/execute get_profile: success=${ex.data?.success} channel=${ex.channel_used || ex.data?._channel_used}`, ex.data?.success ? 'info' : 'error');
}
await loadConnectionStatus();
log('WS 全量检测完成', 'info');
} catch (e) {
log('WS 全量检测失败: ' + e.message, 'error');
}
}
// 启动
(async () => {
await checkHealth();
await loadDevices();
await checkAI();
await loadConnectionProtocol();
await loadConnectionStatus();
setInterval(async () => { await checkHealth(); await loadDevices(); }, 15000);
setInterval(checkAI, 30000);
setInterval(loadConnectionStatus, 10000);
setInterval(pollGuardEvents, 10000);
})();
</script>
</body>
</html>