nav_cache 模块单例;index 增加 Hub 链接与 hook 探测;SetupActivity 注册 Launcher。 Co-authored-by: Cursor <cursoragent@cursor.com>
703 lines
29 KiB
HTML
703 lines
29 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>工作手机 · 控制中心</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>
|