chore: 工作手机 SDK v3.0 初始提交 | sdk/开发文档/机擎/资料

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
卡若
2026-02-24 14:08:48 +08:00
commit 1d29659185
230 changed files with 45135 additions and 0 deletions

476
sdk/app/static/index.html Normal file
View File

@@ -0,0 +1,476 @@
<!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); }
/* 移动端适配 */
@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>
</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" 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>
</div>
<script>
const API = location.origin;
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`);
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`);
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}`);
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'});
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`);
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);
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`);
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 = '不可用';
}
}
// 模式切换
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'},
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'},
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'); }
}
// 启动
(async () => {
await checkHealth();
await loadDevices();
await checkAI();
setInterval(async () => { await checkHealth(); await loadDevices(); }, 15000);
setInterval(checkAI, 30000);
})();
</script>
</body>
</html>