1391 lines
65 KiB
HTML
1391 lines
65 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>机擎 · 工作手机统一工作台</title>
|
||
<style>
|
||
*{box-sizing:border-box;margin:0;padding:0}
|
||
:root{
|
||
--bg:#f2f4f8;
|
||
--panel:rgba(255,255,255,.72);
|
||
--panel-strong:rgba(255,255,255,.88);
|
||
--border:rgba(15,23,42,.06);
|
||
--border-active:rgba(37,99,235,.22);
|
||
--shadow:0 8px 32px rgba(15,23,42,.06),0 1.5px 4px rgba(15,23,42,.04);
|
||
--shadow-lg:0 16px 48px rgba(15,23,42,.1),0 2px 6px rgba(15,23,42,.05);
|
||
--text:#0f172a;
|
||
--sub:#64748b;
|
||
--muted:#94a3b8;
|
||
--blue:#2563eb;
|
||
--teal:#0ea5a6;
|
||
--green:#16a34a;
|
||
--orange:#f59e0b;
|
||
--red:#dc2626;
|
||
--purple:#7c3aed;
|
||
--radius:20px;
|
||
--radius-sm:14px;
|
||
--blur:20px;
|
||
--ease:cubic-bezier(.22,1,.36,1);
|
||
}
|
||
html{-webkit-font-smoothing:antialiased;scroll-behavior:smooth}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","PingFang SC","Helvetica Neue",sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
|
||
body::before{content:"";position:fixed;inset:0;z-index:-1;background:
|
||
radial-gradient(ellipse 60% 50% at 20% -5%,rgba(37,99,235,.1),transparent),
|
||
radial-gradient(ellipse 50% 45% at 80% 5%,rgba(14,165,166,.08),transparent),
|
||
radial-gradient(ellipse 40% 40% at 10% 90%,rgba(124,58,237,.06),transparent)}
|
||
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
||
.app{max-width:1600px;margin:0 auto;padding:16px 20px 48px}
|
||
|
||
/* glass mixin */
|
||
.glass{background:var(--panel);border:1px solid var(--border);border-radius:var(--radius);backdrop-filter:blur(var(--blur)) saturate(180%);-webkit-backdrop-filter:blur(var(--blur)) saturate(180%);box-shadow:var(--shadow);transition:box-shadow .3s var(--ease),border-color .3s var(--ease),transform .3s var(--ease)}
|
||
|
||
/* topbar */
|
||
.topbar{position:sticky;top:10px;z-index:20;display:flex;justify-content:space-between;align-items:center;gap:12px;padding:16px 24px;box-shadow:var(--shadow-lg)}
|
||
.brand{display:flex;align-items:center;gap:14px}
|
||
.brand-icon{width:44px;height:44px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,var(--blue),var(--teal));color:#fff;font-size:22px;box-shadow:0 8px 20px rgba(37,99,235,.22)}
|
||
.brand h1{font-size:20px;font-weight:700;letter-spacing:-.03em}
|
||
.brand p{font-size:11.5px;color:var(--sub);margin-top:2px}
|
||
.chips{display:flex;gap:8px;flex-wrap:wrap}
|
||
.chip{display:inline-flex;align-items:center;gap:6px;padding:6px 13px;border-radius:999px;background:rgba(15,23,42,.04);border:1px solid var(--border);font-size:11.5px;font-weight:600;color:var(--sub);transition:.2s}
|
||
.dot{width:7px;height:7px;border-radius:999px;background:var(--orange);flex-shrink:0}
|
||
.dot.ok{background:var(--green);box-shadow:0 0 8px rgba(22,163,74,.45)}
|
||
.dot.bad{background:var(--red);box-shadow:0 0 8px rgba(220,38,38,.35)}
|
||
|
||
/* layout */
|
||
.layout{display:grid;grid-template-columns:256px minmax(0,1fr);gap:16px;margin-top:16px}
|
||
@media (max-width:980px){.layout{grid-template-columns:1fr}.sidebar{position:static}}
|
||
|
||
/* sidebar */
|
||
.sidebar{position:sticky;top:96px;align-self:start;padding:12px 10px;max-height:calc(100vh - 110px);overflow-y:auto;scrollbar-width:none}
|
||
.sidebar::-webkit-scrollbar{width:0}
|
||
.nav-title{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.1em;font-weight:700;padding:14px 10px 6px}
|
||
.nav-item{display:flex;align-items:center;gap:10px;padding:11px 12px;border-radius:var(--radius-sm);cursor:pointer;border:1px solid transparent;transition:all .2s var(--ease);margin-bottom:2px}
|
||
.nav-item:hover{background:rgba(37,99,235,.05);border-color:rgba(37,99,235,.08)}
|
||
.nav-item.active{background:linear-gradient(135deg,var(--blue),var(--teal));color:#fff;box-shadow:0 6px 16px rgba(37,99,235,.2);transform:scale(1.01)}
|
||
.nav-item.active .nav-icon{background:rgba(255,255,255,.2)}
|
||
.nav-icon{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;background:rgba(15,23,42,.04);font-size:15px;flex-shrink:0;transition:.2s}
|
||
|
||
/* content */
|
||
.content{display:flex;flex-direction:column;gap:16px;animation:fadeUp .4s var(--ease) both}
|
||
|
||
/* stat cards */
|
||
.stats{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
|
||
@media (max-width:1180px){.stats{grid-template-columns:repeat(3,1fr)}}
|
||
@media (max-width:720px){.stats{grid-template-columns:repeat(2,1fr)}}
|
||
.stat{padding:16px 18px;transition:transform .25s var(--ease),box-shadow .25s var(--ease)}
|
||
.stat:hover{transform:translateY(-2px);box-shadow:var(--shadow-lg)}
|
||
.stat .k{font-size:11.5px;color:var(--sub);font-weight:500}
|
||
.stat .v{font-size:26px;font-weight:800;letter-spacing:-.04em;margin-top:3px;background:linear-gradient(135deg,var(--text),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
||
.stat .d{font-size:10.5px;color:var(--muted);margin-top:4px}
|
||
|
||
/* panel */
|
||
.panel{padding:24px;transition:box-shadow .3s var(--ease)}
|
||
.panel:hover{box-shadow:var(--shadow-lg)}
|
||
.panel-head{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;flex-wrap:wrap;margin-bottom:18px}
|
||
.panel h2{font-size:19px;font-weight:700;letter-spacing:-.03em}
|
||
.panel p.sub{font-size:12.5px;color:var(--sub);margin-top:4px;line-height:1.6}
|
||
.btns{display:flex;gap:8px;flex-wrap:wrap}
|
||
.btn{border:0;border-radius:11px;padding:9px 16px;font-size:12px;font-weight:600;cursor:pointer;transition:all .2s var(--ease)}
|
||
.btn.primary{background:linear-gradient(135deg,var(--blue),var(--teal));color:#fff;box-shadow:0 4px 12px rgba(37,99,235,.2)}
|
||
.btn.primary:hover{transform:translateY(-1px);box-shadow:0 6px 18px rgba(37,99,235,.3)}
|
||
.btn.soft{background:rgba(15,23,42,.05);color:var(--text)}
|
||
.btn.soft:hover{background:rgba(15,23,42,.09);transform:translateY(-1px)}
|
||
|
||
/* grid */
|
||
.grid{display:grid;gap:12px}
|
||
.grid.col2{grid-template-columns:repeat(2,minmax(0,1fr))}
|
||
.grid.col3{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||
.grid.col4{grid-template-columns:repeat(4,minmax(0,1fr))}
|
||
@media (max-width:980px){.grid.col2,.grid.col3,.grid.col4{grid-template-columns:1fr}}
|
||
|
||
/* card */
|
||
.card{padding:18px;border-radius:16px;background:var(--panel-strong);border:1px solid var(--border);transition:all .25s var(--ease)}
|
||
.card:hover{border-color:var(--border-active);box-shadow:0 8px 24px rgba(37,99,235,.06)}
|
||
.card h3{font-size:14.5px;font-weight:700;margin-bottom:6px}
|
||
.card p{font-size:12px;color:var(--sub);line-height:1.65}
|
||
.path{display:inline-flex;align-items:center;gap:6px;background:rgba(15,23,42,.04);padding:4px 10px;border-radius:999px;font-size:10.5px;color:var(--sub);margin-top:8px;font-weight:500}
|
||
|
||
/* flow steps */
|
||
.flow{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
|
||
@media (max-width:980px){.flow{grid-template-columns:1fr}}
|
||
.flow-item{position:relative;padding:18px;border-radius:16px;background:var(--panel-strong);border:1px solid var(--border);transition:all .25s var(--ease)}
|
||
.flow-item:hover{border-color:var(--border-active);transform:translateY(-2px)}
|
||
.flow-item:not(:last-child)::after{content:"\2192";position:absolute;right:-10px;top:50%;transform:translateY(-50%);font-size:18px;color:var(--muted);font-weight:300}
|
||
@media (max-width:980px){.flow-item:not(:last-child)::after{content:"\2193";right:auto;left:50%;bottom:-20px;top:auto;transform:translateX(-50%)}}
|
||
.flow-step{font-size:10px;color:var(--blue);font-weight:800;letter-spacing:.08em;text-transform:uppercase}
|
||
.flow-item h3{margin-top:8px;font-size:15px;font-weight:700}
|
||
.flow-item p{margin-top:6px;font-size:12px;color:var(--sub);line-height:1.7}
|
||
|
||
/* doc layout */
|
||
.doc-layout{display:grid;grid-template-columns:340px minmax(0,1fr);gap:16px}
|
||
@media (max-width:1180px){.doc-layout{grid-template-columns:1fr}}
|
||
.doc-list{display:flex;flex-direction:column;gap:8px;max-height:calc(100vh - 200px);overflow-y:auto;scrollbar-width:thin;padding-right:4px}
|
||
.doc-list::-webkit-scrollbar{width:3px}
|
||
.doc-list::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:3px}
|
||
.doc-item{padding:14px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--panel-strong);cursor:pointer;transition:all .2s var(--ease)}
|
||
.doc-item:hover{border-color:var(--border-active);background:rgba(255,255,255,.95)}
|
||
.doc-item.active{border-color:var(--blue);box-shadow:0 0 0 2px rgba(37,99,235,.12),0 6px 16px rgba(37,99,235,.08);background:#fff}
|
||
.doc-item h3{font-size:13.5px;font-weight:700}
|
||
.doc-item p{font-size:11.5px;color:var(--sub);margin-top:4px;line-height:1.55}
|
||
.doc-meta{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
|
||
.pill{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:999px;background:rgba(15,23,42,.04);font-size:10.5px;color:var(--sub);font-weight:500;border:1px solid rgba(15,23,42,.04)}
|
||
|
||
/* doc view */
|
||
.doc-view{padding:24px;border-radius:var(--radius);background:rgba(255,255,255,.95);border:1px solid var(--border);min-height:640px;box-shadow:inset 0 1px 0 rgba(255,255,255,1)}
|
||
.doc-view .doc-header{padding-bottom:16px;border-bottom:1px solid rgba(15,23,42,.06);margin-bottom:18px}
|
||
.doc-view .doc-header h3{font-size:22px;font-weight:800;letter-spacing:-.04em}
|
||
.doc-view .doc-header p{font-size:13px;color:var(--sub);margin-top:8px;line-height:1.6}
|
||
.doc-body{font-size:14px;line-height:1.85;color:#1e293b}
|
||
.doc-body h1{font-size:22px;font-weight:800;margin:1.4em 0 .6em;letter-spacing:-.03em}
|
||
.doc-body h2{font-size:18px;font-weight:700;margin:1.2em 0 .5em;padding-bottom:6px;border-bottom:1px solid rgba(15,23,42,.06)}
|
||
.doc-body h3{font-size:15px;font-weight:700;margin:1em 0 .4em}
|
||
.doc-body p,.doc-body ul,.doc-body ol,.doc-body table,.doc-body pre,.doc-body blockquote{margin:.8em 0}
|
||
.doc-body ul,.doc-body ol{padding-left:1.4em}
|
||
.doc-body li{margin:.3em 0}
|
||
.doc-body pre{overflow:auto;padding:16px;border-radius:var(--radius-sm);background:#0f172a;color:#e2e8f0;font-size:13px;line-height:1.7}
|
||
.doc-body code{font-family:ui-monospace,SFMono-Regular,"JetBrains Mono",Menlo,monospace;font-size:.9em}
|
||
.doc-body :not(pre)>code{background:rgba(37,99,235,.07);color:var(--blue);padding:2px 6px;border-radius:5px}
|
||
.doc-body table{width:100%;border-collapse:collapse}
|
||
.doc-body th,.doc-body td{border:1px solid rgba(15,23,42,.08);padding:8px 12px;text-align:left;font-size:13px}
|
||
.doc-body th{background:rgba(15,23,42,.03);font-weight:600}
|
||
.doc-body blockquote{border-left:3px solid var(--blue);padding:8px 16px;background:rgba(37,99,235,.03);border-radius:0 10px 10px 0;color:var(--sub)}
|
||
|
||
/* device layout */
|
||
.device-layout{display:grid;grid-template-columns:280px minmax(0,1.1fr) 320px;gap:16px}
|
||
@media (max-width:1280px){.device-layout{grid-template-columns:1fr}}
|
||
.device-list{display:flex;flex-direction:column;gap:8px}
|
||
.device-item{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--panel-strong);cursor:pointer;transition:all .2s var(--ease)}
|
||
.device-item:hover{border-color:var(--border-active);transform:translateX(2px)}
|
||
.device-item.active{border-color:var(--blue);box-shadow:0 0 0 2px rgba(37,99,235,.1),0 4px 12px rgba(37,99,235,.08)}
|
||
.device-badge{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;font-size:19px;background:linear-gradient(135deg,var(--blue),var(--teal));color:#fff;box-shadow:0 4px 10px rgba(37,99,235,.15)}
|
||
.device-name{font-size:13.5px;font-weight:700}
|
||
.device-meta{font-size:11px;color:var(--sub);margin-top:2px}
|
||
.status-dot{width:9px;height:9px;border-radius:999px;background:var(--orange);flex-shrink:0}
|
||
.status-dot.online,.status-dot.adb{background:var(--green);box-shadow:0 0 6px rgba(22,163,74,.4)}
|
||
|
||
/* connection mode badges */
|
||
.mode-badges{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
|
||
.mode-badge{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border-radius:999px;font-size:9.5px;font-weight:600;border:1px solid transparent}
|
||
.mode-badge.available{background:rgba(22,163,74,.08);color:var(--green);border-color:rgba(22,163,74,.15)}
|
||
.mode-badge.unavailable{background:rgba(148,163,184,.06);color:var(--muted);border-color:rgba(148,163,184,.1)}
|
||
.mode-badge.best{background:rgba(37,99,235,.1);color:var(--blue);border-color:rgba(37,99,235,.2);font-weight:700}
|
||
.mode-badge .badge-dot{width:5px;height:5px;border-radius:999px}
|
||
.mode-badge.available .badge-dot{background:var(--green)}
|
||
.mode-badge.unavailable .badge-dot{background:var(--muted)}
|
||
.mode-badge.best .badge-dot{background:var(--blue);box-shadow:0 0 4px rgba(37,99,235,.5)}
|
||
.priority-tag{font-size:9px;font-weight:800;color:var(--blue);letter-spacing:.05em;text-transform:uppercase}
|
||
.mode-detail-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin-top:12px}
|
||
@media(max-width:1280px){.mode-detail-grid{grid-template-columns:repeat(3,1fr)}}
|
||
@media(max-width:720px){.mode-detail-grid{grid-template-columns:1fr}}
|
||
.mode-detail-card{padding:12px;border-radius:12px;border:1px solid var(--border);background:var(--panel-strong);text-align:center;transition:all .2s var(--ease)}
|
||
.mode-detail-card:hover{border-color:var(--border-active);transform:translateY(-1px)}
|
||
.mode-detail-card.active{border-color:var(--green);box-shadow:0 0 0 2px rgba(22,163,74,.08)}
|
||
.mode-detail-card.best-active{border-color:var(--blue);box-shadow:0 0 0 2px rgba(37,99,235,.12),0 4px 12px rgba(37,99,235,.06)}
|
||
.mode-detail-card .mode-icon{font-size:20px;margin-bottom:4px}
|
||
.mode-detail-card .mode-name{font-size:11.5px;font-weight:700}
|
||
.mode-detail-card .mode-status{font-size:10px;margin-top:4px}
|
||
.mode-detail-card .mode-prio{font-size:9px;color:var(--muted);margin-top:2px}
|
||
|
||
/* actions */
|
||
.actions{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
||
.action{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:14px 10px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--panel-strong);cursor:pointer;transition:all .2s var(--ease);font-size:11.5px;font-weight:600}
|
||
.action:hover{border-color:var(--border-active);transform:translateY(-2px);box-shadow:0 6px 16px rgba(37,99,235,.08)}
|
||
.action .icon{font-size:22px}
|
||
|
||
/* command */
|
||
.command-area{display:flex;gap:10px;flex-wrap:wrap}
|
||
.command-area input{flex:1;min-width:240px;padding:11px 16px;border-radius:12px;border:1px solid var(--border);background:rgba(255,255,255,.9);font-size:13.5px;outline:0;transition:.2s}
|
||
.command-area input:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(37,99,235,.08)}
|
||
.hint{font-size:11.5px;color:var(--sub);line-height:1.7;margin-top:10px}
|
||
.mode-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}
|
||
.mode-btn{padding:7px 14px;border-radius:10px;background:rgba(15,23,42,.04);font-size:11.5px;color:var(--sub);cursor:pointer;transition:.2s;border:1px solid transparent;font-weight:500}
|
||
.mode-btn:hover{background:rgba(37,99,235,.06)}
|
||
.mode-btn.active{background:rgba(37,99,235,.1);color:var(--blue);font-weight:700;border-color:rgba(37,99,235,.15)}
|
||
|
||
/* log */
|
||
.log-box{min-height:200px;max-height:320px;overflow:auto;padding:14px 16px;border-radius:var(--radius-sm);background:#0f172a;color:#cbd5e1;font-family:ui-monospace,SFMono-Regular,"JetBrains Mono",Menlo,monospace;font-size:12px;line-height:1.75}
|
||
.log-info{color:#7dd3fc}.log-error{color:#fca5a5}
|
||
|
||
/* screenshot */
|
||
.shot{aspect-ratio:9/19.5;border-radius:24px;overflow:hidden;border:2px solid rgba(15,23,42,.08);background:linear-gradient(180deg,#0f172a,#1e293b);display:flex;align-items:center;justify-content:center}
|
||
.shot img{width:100%;height:100%;object-fit:cover}
|
||
.placeholder{font-size:12px;color:var(--muted);text-align:center;padding:14px}
|
||
|
||
/* detail table */
|
||
.detail-table{width:100%;border-collapse:collapse}
|
||
.detail-table td{padding:7px 0;border-bottom:1px solid rgba(15,23,42,.06);font-size:12.5px;vertical-align:top}
|
||
.detail-table td:first-child{width:88px;color:var(--sub);font-weight:500}
|
||
|
||
/* protocol */
|
||
.proto{background:#0f172a;color:#dbeafe;padding:16px;border-radius:var(--radius-sm);font-size:12px;line-height:1.8}
|
||
.proto strong{color:#fff}
|
||
.mini-table{width:100%;border-collapse:collapse;margin-top:8px}
|
||
.mini-table th,.mini-table td{border-bottom:1px solid rgba(255,255,255,.06);padding:6px 4px;text-align:left;font-size:11px}
|
||
|
||
/* asset */
|
||
.asset-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
||
@media (max-width:980px){.asset-grid{grid-template-columns:1fr}}
|
||
.asset-card{padding:16px;border-radius:16px;background:var(--panel-strong);border:1px solid var(--border);transition:all .25s var(--ease)}
|
||
.asset-card:hover{transform:translateY(-2px);box-shadow:var(--shadow-lg)}
|
||
.asset-card img{width:100%;height:auto;border-radius:12px;border:1px solid rgba(15,23,42,.06);background:#fff}
|
||
.asset-card h3{margin-bottom:6px;font-size:14px;font-weight:700}
|
||
|
||
/* table */
|
||
.list-table{width:100%;border-collapse:collapse}
|
||
.list-table th,.list-table td{padding:10px 12px;border-bottom:1px solid rgba(15,23,42,.06);text-align:left;font-size:12px}
|
||
.list-table th{font-size:11px;color:var(--sub);font-weight:600}
|
||
.list-table tr:hover td{background:rgba(37,99,235,.02)}
|
||
.empty{padding:32px 14px;text-align:center;color:var(--muted);font-size:13px}
|
||
|
||
/* image zoom */
|
||
.asset-card img,.doc-body img{cursor:zoom-in;transition:transform .25s var(--ease),box-shadow .25s var(--ease)}
|
||
.asset-card img:hover,.doc-body img:hover{transform:scale(1.015);box-shadow:0 12px 32px rgba(0,0,0,.12)}
|
||
|
||
/* lightbox */
|
||
.lightbox{position:fixed;inset:0;z-index:200;background:rgba(0,0,0,.88);display:none;align-items:center;justify-content:center;cursor:zoom-out;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);opacity:0;transition:opacity .25s ease}
|
||
.lightbox.show{display:flex;opacity:1}
|
||
.lightbox img{max-width:94vw;max-height:94vh;border-radius:16px;box-shadow:0 24px 64px rgba(0,0,0,.5);object-fit:contain;transform:scale(.92);transition:transform .3s var(--ease)}
|
||
.lightbox.show img{transform:scale(1)}
|
||
.lb-close{position:absolute;top:20px;right:24px;width:40px;height:40px;border-radius:12px;background:rgba(255,255,255,.15);border:none;color:#fff;font-size:20px;cursor:pointer;display:grid;place-items:center;transition:.2s;backdrop-filter:blur(8px)}
|
||
.lb-close:hover{background:rgba(255,255,255,.25);transform:scale(1.05)}
|
||
.lb-title{position:absolute;bottom:24px;left:50%;transform:translateX(-50%);color:rgba(255,255,255,.8);font-size:13px;font-weight:600;background:rgba(0,0,0,.4);padding:6px 16px;border-radius:999px;backdrop-filter:blur(8px);white-space:nowrap}
|
||
|
||
.foot{text-align:center;font-size:11px;color:var(--muted);padding:16px;margin-top:12px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<header class="topbar glass">
|
||
<div class="brand">
|
||
<div class="brand-icon">⚙️</div>
|
||
<div>
|
||
<h1>机擎 · 工作手机统一工作台</h1>
|
||
<p>说明文档、设备控制、Hook、AI Agent、连接协议、架构图和项目管理全部融合到一个站内页面</p>
|
||
</div>
|
||
</div>
|
||
<div class="chips">
|
||
<span class="chip" id="chip-health"><span class="dot"></span>检测中</span>
|
||
<span class="chip" id="chip-ws">WS 0</span>
|
||
<span class="chip" id="chip-adb">ADB 0</span>
|
||
<span class="chip" id="chip-version">v-</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="layout">
|
||
<aside class="sidebar glass" id="sidebar"></aside>
|
||
<main class="content">
|
||
<section class="stats" id="stats"></section>
|
||
<section id="view"></section>
|
||
</main>
|
||
</div>
|
||
|
||
<div class="foot">机擎 v3.0 · 工作手机SDK · 统一工作台 · 站内文档渲染 · Agent 集群</div>
|
||
</div>
|
||
|
||
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
|
||
<button class="lb-close" onclick="closeLightbox()">×</button>
|
||
<img id="lb-img" src="" alt="放大预览">
|
||
<div class="lb-title" id="lb-title"></div>
|
||
</div>
|
||
|
||
<script>
|
||
const API = location.origin;
|
||
const NAVS = [
|
||
{id:'overview', icon:'🏠', name:'首页总览', desc:'总览卡片、系统链路、控制模式'},
|
||
{id:'docs', icon:'📖', name:'说明文档', desc:'文档总览卡片 + 站内正文页'},
|
||
{id:'devices', icon:'📱', name:'设备控制', desc:'设备列表、截图、快捷操作、AI 指令'},
|
||
{id:'hook', icon:'🪝', name:'Hook 控制', desc:'模块、事件、脚本、设备 Hook 状态'},
|
||
{id:'agent', icon:'🤖', name:'AI Agent', desc:'自然语言控制、能力、执行模式'},
|
||
{id:'agentCluster', icon:'🧩', name:'Agent 集群', desc:'架构部署、技能引擎、Hawk、Hook 脚本'},
|
||
{id:'connection', icon:'🔌', name:'连接与协议', desc:'WebSocket、心跳、device_request、event'},
|
||
{id:'architecture', icon:'🧭', name:'架构与经验', desc:'架构图、控制模式、经验沉淀'},
|
||
{id:'project', icon:'📊', name:'项目管理', desc:'进度、文档清单、项目入口'},
|
||
];
|
||
|
||
const state = {
|
||
current: new URLSearchParams(location.search).get('tab') || 'overview',
|
||
overview: null,
|
||
docs: [],
|
||
assets: [],
|
||
selectedDoc: null,
|
||
selectedAgentDoc: null,
|
||
docFilter: '',
|
||
devices: [],
|
||
currentDevice: null,
|
||
protocol: null,
|
||
connection: null,
|
||
hookModules: [],
|
||
hookEvents: [],
|
||
currentDeviceModules: null,
|
||
logs: [],
|
||
cmdMode: 'pattern',
|
||
};
|
||
|
||
function esc(text=''){
|
||
return String(text).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||
}
|
||
|
||
function log(message, type='info'){
|
||
const ts = new Date().toLocaleTimeString('zh-CN', {hour12:false});
|
||
state.logs.push({message, type, ts});
|
||
if (state.logs.length > 80) state.logs.shift();
|
||
if (state.current === 'devices') renderCurrentView();
|
||
}
|
||
|
||
async function apiGet(url){
|
||
const res = await fetch(`${API}${url}`);
|
||
if(!res.ok) throw new Error(`请求失败: ${res.status}`);
|
||
return res.json();
|
||
}
|
||
|
||
async function apiPost(url, body){
|
||
const res = await fetch(`${API}${url}`, {
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify(body || {})
|
||
});
|
||
if(!res.ok) throw new Error(`请求失败: ${res.status}`);
|
||
return res.json();
|
||
}
|
||
|
||
function renderSidebar(){
|
||
const box = document.getElementById('sidebar');
|
||
box.innerHTML = `
|
||
<div class="nav-title">统一工作台</div>
|
||
${NAVS.map(item => `
|
||
<div class="nav-item ${state.current===item.id?'active':''}" onclick="navigate('${item.id}')">
|
||
<div class="nav-icon">${item.icon}</div>
|
||
<div>
|
||
<div style="font-size:13px;font-weight:700">${item.name}</div>
|
||
<div style="font-size:11px;opacity:.8;margin-top:2px">${item.desc}</div>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
`;
|
||
}
|
||
|
||
function renderStats(){
|
||
const summary = state.overview?.summary || {};
|
||
document.getElementById('stats').innerHTML = `
|
||
${statCard('服务状态', summary.sdk_online ? '健康' : '离线', summary.sdk_online ? `版本 ${summary.sdk_version || '-'}` : '等待连接')}
|
||
${statCard('WS 在线', summary.ws_online ?? 0, '设备长连接')}
|
||
${statCard('ADB 设备', summary.adb_count ?? 0, '本地/真机直连')}
|
||
${statCard('说明文档', summary.manual_docs ?? 0, '站内阅读')}
|
||
${statCard('Agent 文档', state.docs.filter(d => d.category === 'agent').length || 0, 'Agent 集群')}
|
||
`;
|
||
document.getElementById('chip-health').innerHTML = `<span class="dot ${summary.sdk_online ? 'ok' : 'bad'}"></span>${summary.sdk_online ? '在线' : '离线'}`;
|
||
document.getElementById('chip-ws').textContent = `WS ${summary.ws_online ?? 0}`;
|
||
document.getElementById('chip-adb').textContent = `ADB ${summary.adb_count ?? 0}`;
|
||
document.getElementById('chip-version').textContent = `v${summary.sdk_version || '-'}`;
|
||
}
|
||
|
||
function statCard(title, value, desc){
|
||
return `
|
||
<div class="stat glass">
|
||
<div class="k">${title}</div>
|
||
<div class="v">${value}</div>
|
||
<div class="d">${desc}</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function navigate(id){
|
||
state.current = id;
|
||
const url = new URL(location.href);
|
||
url.searchParams.set('tab', id);
|
||
history.replaceState({}, '', url.toString());
|
||
renderSidebar();
|
||
renderCurrentView();
|
||
}
|
||
|
||
function card(title, desc, action){
|
||
return `
|
||
<div class="card">
|
||
<h3>${title}</h3>
|
||
<p>${desc}</p>
|
||
${action || ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderOverview(){
|
||
const docs = state.docs.filter(d => d.category === 'manual').slice(0, 4);
|
||
const modes = state.overview?.control_modes || [];
|
||
const devices = state.overview?.devices || [];
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>首页总览</h2>
|
||
<p class="sub">把说明文档、控制台、Hook、AI Agent、连接状态和架构图收进一个站内工作台。</p>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="btn primary" onclick="navigate('devices')">进入设备控制</button>
|
||
<button class="btn soft" onclick="navigate('agentCluster')">Agent 集群</button>
|
||
<button class="btn soft" onclick="navigate('docs')">说明文档</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flow">
|
||
<div class="flow-item">
|
||
<div class="flow-step">STEP 1</div>
|
||
<h3>业务请求进入</h3>
|
||
<p>存客宝、管理端或本地控制入口统一进入工作手机 SDK,先经过统一工作台和接口收口。</p>
|
||
</div>
|
||
<div class="flow-item">
|
||
<div class="flow-step">STEP 2</div>
|
||
<h3>服务器决策</h3>
|
||
<p>FastAPI、unified、WebSocket Hub、Hook、ADB、AI Agent 共同决定最合适的执行通道。</p>
|
||
</div>
|
||
<div class="flow-item">
|
||
<div class="flow-step">STEP 3</div>
|
||
<h3>设备执行</h3>
|
||
<p>设备端 Agent、SkillExecutor、uiautomator2、Frida、scrcpy 分别承担自动化、深度 Hook 和人工接管。</p>
|
||
</div>
|
||
<div class="flow-item">
|
||
<div class="flow-step">STEP 4</div>
|
||
<h3>结果沉淀</h3>
|
||
<p>response、event、状态、截图、经验和说明文档统一回到工作台,形成持续复用的操作体系。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>控制模式</h2>
|
||
<p class="sub">5 种控制模式按优先级自动选择。绿色 = 可用,蓝色 = 当前最优。</p>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="btn soft" onclick="refreshConnectionModes()">检测模式</button>
|
||
</div>
|
||
</div>
|
||
<div id="overview-modes-grid" class="grid col3">
|
||
${modes.map(m => {
|
||
const icons = {hook:'🪝', agent:'🔌', adb:'📱', ai:'🤖', scrcpy:'🖥️'};
|
||
return card(
|
||
m.name,
|
||
m.desc,
|
||
'<div class="path" style="gap:8px">' +
|
||
'<span class="priority-tag">P' + (m.priority || '?') + '</span>' +
|
||
(icons[m.id] || '⚡') + ' ' + m.id +
|
||
'</div>'
|
||
);
|
||
}).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>说明文档首页</h2>
|
||
<p class="sub">只保留一个文档首页,卡片展示后在站内直接展开正文,不再使用 iframe。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col2">
|
||
${docs.map(d => `
|
||
<div class="card" onclick="openDocFromHome('${d.id}')" style="cursor:pointer">
|
||
<h3>${d.title}</h3>
|
||
<p>${d.summary}</p>
|
||
<div class="doc-meta">
|
||
<span class="pill">${d.section}</span>
|
||
<span class="pill">${d.folder}/${d.file}</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>设备快照</h2>
|
||
<p class="sub">实时展示设备状态、连接模式和心跳。点击设备行可查看详情。</p>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="btn soft" onclick="refreshAll()">刷新全部</button>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<table class="list-table">
|
||
<thead>
|
||
<tr><th>设备</th><th>状态</th><th>连接模式</th><th>项目</th><th>当前 APP</th><th>心跳</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
${devices.length ? devices.slice(0, 8).map(d => {
|
||
const best = d.best_mode;
|
||
const modes = (d.connection_modes || []).filter(m => m.available);
|
||
const statusClass = d.status === 'online' ? 'online' : (d.status === 'adb' ? 'adb' : '');
|
||
return `
|
||
<tr onclick="navigate('devices');selectDevice('${d.device_id}')" style="cursor:pointer">
|
||
<td><strong>${esc(d.model || d.device_id || '-')}</strong></td>
|
||
<td><span class="status-dot ${statusClass}" style="display:inline-block;vertical-align:middle;margin-right:4px"></span>${esc(d.status || '-')}</td>
|
||
<td>
|
||
<div class="mode-badges">
|
||
${best ? `<span class="mode-badge best"><span class="badge-dot"></span>${esc(best.name)}</span>` : ''}
|
||
${modes.filter(m => !best || m.id !== best.id).slice(0,2).map(m => `<span class="mode-badge available"><span class="badge-dot"></span>${esc(m.id)}</span>`).join('')}
|
||
</div>
|
||
</td>
|
||
<td>${esc(d.project_id || '-')}</td>
|
||
<td>${esc(d.current_app || '-')}</td>
|
||
<td>${typeof d.heartbeat_age_seconds === 'number' ? d.heartbeat_age_seconds + 's' : (d.last_heartbeat ? '活跃' : '-')}</td>
|
||
</tr>`;
|
||
}).join('') : `<tr><td colspan="6" class="empty">暂无设备数据</td></tr>`}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function openDocFromHome(docId){
|
||
state.current = 'docs';
|
||
state.selectedDoc = docId;
|
||
renderSidebar();
|
||
renderCurrentView();
|
||
}
|
||
|
||
async function openDoc(docId){
|
||
state.selectedDoc = docId;
|
||
document.querySelectorAll('#view .doc-item').forEach(el => {
|
||
el.classList.toggle('active', el.dataset.docid === docId);
|
||
});
|
||
const target = document.getElementById('doc-view');
|
||
if (target) target.innerHTML = `<div class="empty">正文加载中...</div>`;
|
||
try {
|
||
const res = await apiGet(`/api/v3/workbench/docs/${docId}`);
|
||
const doc = res.data;
|
||
const view = document.getElementById('doc-view');
|
||
if (!view) return;
|
||
view.innerHTML = `
|
||
<div class="doc-header">
|
||
<h3>${doc.title}</h3>
|
||
<p>${doc.summary}</p>
|
||
<div class="doc-meta">
|
||
<span class="pill">${doc.section}</span>
|
||
<span class="pill">${doc.folder}/${doc.file}</span>
|
||
<span class="pill">站内正文渲染</span>
|
||
</div>
|
||
</div>
|
||
<div class="doc-body">${doc.content_html}</div>
|
||
`;
|
||
} catch (error) {
|
||
const view = document.getElementById('doc-view');
|
||
if (view) view.innerHTML = `<div class="empty">正文加载失败:${esc(error.message)}</div>`;
|
||
}
|
||
}
|
||
|
||
function renderDocs(){
|
||
const allDocs = state.docs;
|
||
const cats = [...new Set(allDocs.map(d => d.section))];
|
||
const filter = state.docFilter || '';
|
||
const docs = filter ? allDocs.filter(d => d.section === filter) : allDocs;
|
||
const selected = state.selectedDoc || docs[0]?.id;
|
||
state.selectedDoc = selected;
|
||
if (selected) requestAnimationFrame(() => {
|
||
const v = document.getElementById('doc-view');
|
||
if (v && !v.dataset.loaded) { v.dataset.loaded = '1'; openDoc(selected); }
|
||
});
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>说明文档</h2>
|
||
<p class="sub">全部文档站内阅读,点击左侧卡片直接展开正文。支持按分类筛选。</p>
|
||
</div>
|
||
</div>
|
||
<div class="mode-tabs" style="margin-bottom:14px">
|
||
<div class="mode-btn ${!filter?'active':''}" onclick="filterDocs('')">全部 (${allDocs.length})</div>
|
||
${cats.map(c => `<div class="mode-btn ${filter===c?'active':''}" onclick="filterDocs('${c}')">${c} (${allDocs.filter(d=>d.section===c).length})</div>`).join('')}
|
||
</div>
|
||
<div class="doc-layout">
|
||
<div class="doc-list">
|
||
${docs.map(doc => `
|
||
<div class="doc-item ${doc.id===selected ? 'active' : ''}" data-docid="${doc.id}" onclick="openDoc('${doc.id}')">
|
||
<h3>${doc.title}</h3>
|
||
<p>${doc.summary}</p>
|
||
<div class="doc-meta">
|
||
<span class="pill">${doc.section}</span>
|
||
<span class="pill">${doc.folder}</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
<div class="doc-view" id="doc-view"><div class="empty">选择左侧文档查看正文</div></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
function filterDocs(section){
|
||
state.docFilter = section;
|
||
state.selectedDoc = null;
|
||
const v = document.getElementById('doc-view');
|
||
if (v) delete v.dataset.loaded;
|
||
renderCurrentView();
|
||
}
|
||
|
||
function renderDevicePanel(){
|
||
const devices = state.devices;
|
||
const current = devices.find(item => item.device_id === state.currentDevice) || devices[0];
|
||
if (current && !state.currentDevice) state.currentDevice = current.device_id;
|
||
const protocol = state.protocol || {};
|
||
const connection = state.connection || {};
|
||
const logs = state.logs.map(item => `<div class="${item.type==='error'?'log-error':'log-info'}">[${item.ts}] ${esc(item.message)}</div>`).join('');
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>设备控制</h2>
|
||
<p class="sub">把原来的控制中心融合进工作台,统一展示设备列表、详情、截图、快捷操作、AI 对话和连接状态。</p>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="btn soft" onclick="refreshDevices()">刷新设备</button>
|
||
<button class="btn primary" onclick="navigate('connection')">查看协议</button>
|
||
</div>
|
||
</div>
|
||
<div class="device-layout">
|
||
<div class="card">
|
||
<h3 style="margin-bottom:12px">设备列表</h3>
|
||
<div class="device-list">
|
||
${devices.length ? devices.map(device => {
|
||
const best = device.best_mode;
|
||
const avail = (device.connection_modes || []).filter(m => m.available);
|
||
return `
|
||
<div class="device-item ${device.device_id===state.currentDevice?'active':''}" onclick="selectDevice('${device.device_id}')">
|
||
<div class="device-badge">📱</div>
|
||
<div style="flex:1">
|
||
<div class="device-name">${esc(device.model || device.device_id)}</div>
|
||
<div class="device-meta">${esc(device.device_id?.substring(0,12))}... · ${esc(device.status || '-')} · ${esc(device.project_id || '未绑定')}</div>
|
||
<div class="mode-badges">
|
||
${best ? `<span class="mode-badge best"><span class="badge-dot"></span>${esc(best.name)}</span>` : ''}
|
||
${avail.filter(m => !best || m.id !== best.id).slice(0,3).map(m => `<span class="mode-badge available"><span class="badge-dot"></span>${esc(m.id)}</span>`).join('')}
|
||
${avail.length === 0 ? '<span class="mode-badge unavailable">离线</span>' : ''}
|
||
</div>
|
||
</div>
|
||
<div class="status-dot ${esc(device.status || '')}"></div>
|
||
</div>`;
|
||
}).join('') : `<div class="empty">暂无设备,请先接入 Agent 或连接 ADB 设备</div>`}
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;flex-direction:column;gap:16px">
|
||
<div class="card">
|
||
<h3 style="margin-bottom:12px">快捷操作</h3>
|
||
<div class="actions">
|
||
${actionBtn('📸','截屏',"doAction('screenshot')")}
|
||
${actionBtn('💬','打开微信',"doAction('open_wechat')")}
|
||
${actionBtn('🎵','打开抖音',"doAction('open_douyin')")}
|
||
${actionBtn('📕','打开小红书',"doAction('open_xhs')")}
|
||
${actionBtn('🐟','打开闲鱼',"doAction('open_xianyu')")}
|
||
${actionBtn('🏠','回主页',"doAction('home')")}
|
||
${actionBtn('⬅️','返回',"doAction('back')")}
|
||
${actionBtn('🌳','UI 树',"doAction('ui_tree')")}
|
||
${actionBtn('ℹ️','设备信息',"doAction('device_info')")}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>AI 对话控制</h3>
|
||
<div class="mode-tabs">
|
||
<div class="mode-btn ${state.cmdMode==='pattern'?'active':''}" onclick="setMode('pattern')">模式匹配</div>
|
||
<div class="mode-btn ${state.cmdMode==='ai'?'active':''}" onclick="setMode('ai')">AI 智能</div>
|
||
<div class="mode-btn ${state.cmdMode==='ai-only'?'active':''}" onclick="setMode('ai-only')">AI 纯解析</div>
|
||
</div>
|
||
<div class="command-area">
|
||
<input id="cmd-input" placeholder="${cmdPlaceholder()}" onkeydown="if(event.key==='Enter')sendCmd()">
|
||
<button class="btn primary" onclick="sendCmd()">发送</button>
|
||
</div>
|
||
<div class="hint">${cmdHint()}</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>连接方式与协议</h3>
|
||
<div class="proto">
|
||
<div><strong>WS 入口</strong> ${esc(connection.ws_path || '-')}</div>
|
||
<div><strong>Hook 事件流</strong> ${esc(connection.hook_events_stream || '-')}</div>
|
||
<div><strong>在线设备</strong> ${connection.online_ws_count ?? '-'} / <strong>ADB</strong> ${connection.adb_count ?? '-'}</div>
|
||
<div><strong>推荐心跳</strong> ${(protocol.heartbeat?.recommended_interval_seconds || []).join(' / ') || '-'}</div>
|
||
<table class="mini-table">
|
||
<thead><tr><th>设备</th><th>项目</th><th>状态</th><th>心跳</th></tr></thead>
|
||
<tbody>
|
||
${(connection.devices || []).length ? (connection.devices || []).slice(0,6).map(item => `
|
||
<tr>
|
||
<td>${esc(item.device_id)}</td>
|
||
<td>${esc(item.project_id || '-')}</td>
|
||
<td>${esc(item.status || '-')}</td>
|
||
<td>${typeof item.heartbeat_age_seconds === 'number' ? item.heartbeat_age_seconds : '-'}</td>
|
||
</tr>
|
||
`).join('') : `<tr><td colspan="4">暂无连接状态</td></tr>`}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>实时日志</h3>
|
||
<div class="log-box">${logs || '<div>暂无日志</div>'}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;flex-direction:column;gap:16px">
|
||
<div class="card">
|
||
<h3 style="margin-bottom:12px">设备截屏</h3>
|
||
<div class="shot" id="shot-box"><div class="placeholder">点击“截屏”查看当前设备画面</div></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="margin-bottom:12px">设备详情</h3>
|
||
${current ? `
|
||
<table class="detail-table">
|
||
<tr><td>设备 ID</td><td style="font-family:monospace;font-size:11px;word-break:break-all">${esc(current.device_id)}</td></tr>
|
||
<tr><td>型号</td><td>${esc(current.model || '-')}</td></tr>
|
||
<tr><td>品牌</td><td>${esc(current.brand || '-')}</td></tr>
|
||
<tr><td>Android</td><td>${esc(current.android_version || current.sdk_version || '-')}</td></tr>
|
||
<tr><td>状态</td><td><span class="status-dot ${current.status || ''}" style="display:inline-block;vertical-align:middle;margin-right:4px"></span>${esc(current.status || '-')}</td></tr>
|
||
<tr><td>当前模式</td><td>${current.best_mode ? `<span class="mode-badge best" style="font-size:11px"><span class="badge-dot"></span>${esc(current.best_mode.name)}</span>` : '-'}</td></tr>
|
||
<tr><td>当前 APP</td><td>${esc(current.current_app || current.quick_status?.current_app || '-')}</td></tr>
|
||
<tr><td>网络</td><td>${esc(current.network_type || current.quick_status?.network_type || '-')}</td></tr>
|
||
<tr><td>能力</td><td>${esc((current.capabilities || []).join(', ') || '-')}</td></tr>
|
||
</table>
|
||
${(current.connection_modes || []).length ? `
|
||
<h3 style="margin:14px 0 8px">连接模式 (${(current.connection_modes||[]).filter(m=>m.available).length}/5 可用)</h3>
|
||
<div class="mode-detail-grid">
|
||
${(current.connection_modes || []).map(m => {
|
||
const icons = {hook:'🪝', agent:'🔌', adb:'📱', ai:'🤖', scrcpy:'🖥️'};
|
||
const cls = m.available ? (current.best_mode?.id === m.id ? 'best-active' : 'active') : '';
|
||
return `<div class="mode-detail-card ${cls}">
|
||
<div class="mode-icon">${icons[m.id] || '⚡'}</div>
|
||
<div class="mode-name">${esc(m.name)}</div>
|
||
<div class="mode-status" style="color:${m.available ? 'var(--green)' : 'var(--muted)'}">${m.available ? '✓ 可用' : '✗ 不可用'}</div>
|
||
<div class="mode-prio">P${m.priority} · ${esc(m.detail?.substring(0,20) || '')}</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>` : ''}
|
||
` : `<div class="empty">未选中设备</div>`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function actionBtn(icon, label, handler){
|
||
return `<div class="action" onclick="${handler}"><div class="icon">${icon}</div><div>${label}</div></div>`;
|
||
}
|
||
|
||
function cmdHint(){
|
||
if (state.cmdMode === 'pattern') return '模式匹配:打开[APP]、点击[文字]、输入[内容]、返回/主页、截图。复杂命令自动升级为 AI 解析。';
|
||
if (state.cmdMode === 'ai') return 'AI 智能:用自然语言描述操作,适合多步任务和跨应用动作。';
|
||
return 'AI 纯解析:只解析不执行,适合看 AI 理解步骤和调试任务编排。';
|
||
}
|
||
|
||
function cmdPlaceholder(){
|
||
if (state.cmdMode === 'pattern') return '输入指令:打开微信;然后截图';
|
||
if (state.cmdMode === 'ai') return '输入自然语言任务:打开微信给文件传输助手发消息说下午开会';
|
||
return '输入要解析的指令:打开淘宝搜索蓝牙耳机';
|
||
}
|
||
|
||
function renderHookPanel(){
|
||
const modules = state.hookModules || [];
|
||
const currentModules = state.currentDeviceModules?.modules || [];
|
||
const events = state.hookEvents || [];
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>Hook 控制</h2>
|
||
<p class="sub">参考奥创后台的设备控制习惯,把 Hook 模块、事件流、脚本部署和设备 Hook 状态直接放进工作台。</p>
|
||
</div>
|
||
<div class="btns">
|
||
<button class="btn soft" onclick="refreshHook()">刷新 Hook 数据</button>
|
||
<button class="btn primary" onclick="navigate('devices')">切到设备控制</button>
|
||
</div>
|
||
</div>
|
||
<div class="grid col3">
|
||
${card('统一入口', '业务请求先进入 /api/v3/hook/execute,再自动选择 WebSocket Agent 或 ADB + 本地 Frida 通道。')}
|
||
${card('设备侧 Hook 模块', 'FridaManager、HookExecutor、wechat_hook_v2.js、EventReporter 共同组成设备侧 Hook 能力。')}
|
||
${card('详细功能', '消息拦截、撤回拦截、联系人读取、好友管理、群管理、朋友圈、进程信息、Hook 状态。')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>Hook 模块清单</h2>
|
||
<p class="sub">服务器端模块管理、脚本部署和设备侧加载状态。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col2">
|
||
<div class="card">
|
||
<h3>服务器模块</h3>
|
||
<table class="list-table">
|
||
<thead><tr><th>模块</th><th>版本</th><th>状态</th><th>能力</th></tr></thead>
|
||
<tbody>
|
||
${modules.length ? modules.slice(0,12).map(module => `
|
||
<tr>
|
||
<td>${esc(module.name || module.module_id)}</td>
|
||
<td>${esc(module.version || '-')}</td>
|
||
<td>${module.enabled ? '启用' : '停用'}</td>
|
||
<td>${esc((module.capabilities || []).slice(0,3).join(', ') || '-')}</td>
|
||
</tr>
|
||
`).join('') : `<tr><td colspan="4" class="empty">暂无 Hook 模块</td></tr>`}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="card">
|
||
<h3>当前设备 Hook 状态</h3>
|
||
<p>当前设备:${esc(state.currentDevice || '未选中')}</p>
|
||
<div class="doc-meta" style="margin:10px 0 14px">
|
||
<span class="pill">支持 Hook:${state.currentDeviceModules?.supports_hook ? '是' : '否'}</span>
|
||
<span class="pill">Frida:${esc(state.currentDeviceModules?.frida_version || '-')}</span>
|
||
<span class="pill">Root:${state.currentDeviceModules?.root_status ? '已就绪' : '未就绪'}</span>
|
||
</div>
|
||
<table class="list-table">
|
||
<thead><tr><th>模块</th><th>状态</th><th>进程</th><th>事件</th></tr></thead>
|
||
<tbody>
|
||
${currentModules.length ? currentModules.map(item => `
|
||
<tr>
|
||
<td>${esc(item.module_id || '-')}</td>
|
||
<td>${esc(item.status || '-')}</td>
|
||
<td>${esc(item.target_process || '-')}</td>
|
||
<td>${item.events_today ?? 0}</td>
|
||
</tr>
|
||
`).join('') : `<tr><td colspan="4" class="empty">暂无设备模块状态</td></tr>`}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>Hook 事件流</h2>
|
||
<p class="sub">事件接收与广播面板,适合看消息监听、撤回、设备回传和脚本结果。</p>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<table class="list-table">
|
||
<thead><tr><th>时间</th><th>类型</th><th>设备</th><th>平台</th><th>内容</th></tr></thead>
|
||
<tbody>
|
||
${events.length ? events.slice(0,20).map(event => `
|
||
<tr>
|
||
<td>${esc(event.created_at || event.timestamp || '-')}</td>
|
||
<td>${esc(event.event_type || '-')}</td>
|
||
<td>${esc(event.device_id || '-')}</td>
|
||
<td>${esc(event.platform || '-')}</td>
|
||
<td>${esc(JSON.stringify(event.payload || {})).slice(0, 90)}</td>
|
||
</tr>
|
||
`).join('') : `<tr><td colspan="5" class="empty">暂无 Hook 事件</td></tr>`}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderAgentPanel(){
|
||
const agent = state.overview?.agent || {};
|
||
const docCards = state.docs.filter(doc => ['agent-install','usage-solution','wechat-e2e'].includes(doc.id));
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>AI Agent</h2>
|
||
<p class="sub">自然语言任务、模式匹配、设备执行和结果回传统一收口在工作台里。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col3">
|
||
${card('当前状态', agent.available ? `${agent.backend || 'AI'} · ${agent.model || '已连接'}` : 'AI 引擎当前不可用')}
|
||
${card('能力路径', '任务理解 → 步骤拆解 → 平台识别 → 动作编排 → 设备执行 → 结果回传')}
|
||
${card('建议分工', '简单动作走模式匹配;多步任务走 AI 智能;调试时先走 AI 纯解析')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>AI Agent 说明入口</h2>
|
||
<p class="sub">把安装、使用和验证类文档挂到 Agent 板块里,形成工作流式说明入口。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col3">
|
||
${docCards.map(doc => `
|
||
<div class="card" onclick="openDocFromAgent('${doc.id}')" style="cursor:pointer">
|
||
<h3>${doc.title}</h3>
|
||
<p>${doc.summary}</p>
|
||
<div class="path">${doc.folder}/${doc.file}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>控制台入口</h2>
|
||
<p class="sub">AI Agent 不再是单独页面,而是直接融进工作台控制区。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col2">
|
||
${card('进入设备控制', '在“设备控制”板块直接用 AI 对话控制设备、发起截图、查看日志。','<button class="btn primary" onclick="navigate(\'devices\')">去设备控制</button>')}
|
||
${card('连接协议', '在“连接与协议”板块查看 register、heartbeat、execute、response、event、device_request。','<button class="btn soft" onclick="navigate(\'connection\')">看协议链路</button>')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function openDocFromAgent(docId){
|
||
navigate('docs');
|
||
setTimeout(() => openDoc(docId), 50);
|
||
}
|
||
|
||
function renderConnectionPanel(){
|
||
const protocol = state.protocol || {};
|
||
const connection = state.connection || {};
|
||
const handshake = protocol.handshake || {};
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>连接与协议</h2>
|
||
<p class="sub">设备端 Agent、服务端 WebSocket Hub 和 Hook 事件流的完整消息类型与实时状态。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col3">
|
||
${card('WebSocket 入口', connection.ws_path || protocol.transport?.agent_ws || '-')}
|
||
${card('Hook 事件流', connection.hook_events_stream || protocol.transport?.hook_events_ws || '-')}
|
||
${card('REST 入口', protocol.transport?.rest_base || `${location.origin}/api/v3`)}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>消息类型</h2>
|
||
<p class="sub">把注册、心跳、执行、响应、事件和设备请求统一展示出来。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col2">
|
||
<div class="card">
|
||
<h3>设备 -> 服务端</h3>
|
||
<div class="doc-meta">
|
||
${(protocol.message_types?.device_to_server || []).map(item => `<span class="pill">${item}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>服务端 -> 设备</h3>
|
||
<div class="doc-meta">
|
||
${(protocol.message_types?.server_to_device || []).map(item => `<span class="pill">${item}</span>`).join('')}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>握手与在线状态</h2>
|
||
<p class="sub">协议示例与在线设备数据同屏查看。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col2">
|
||
<div class="card">
|
||
<h3>注册握手</h3>
|
||
<div class="proto">
|
||
<div><strong>register</strong> ${esc(JSON.stringify(handshake.client_register || {}))}</div>
|
||
<div style="margin-top:10px"><strong>registered</strong> ${esc(JSON.stringify(handshake.server_ack || {}))}</div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>在线设备</h3>
|
||
<table class="list-table">
|
||
<thead><tr><th>设备</th><th>项目</th><th>状态</th><th>心跳</th></tr></thead>
|
||
<tbody>
|
||
${(connection.devices || []).length ? (connection.devices || []).map(item => `
|
||
<tr>
|
||
<td>${esc(item.device_id)}</td>
|
||
<td>${esc(item.project_id || '-')}</td>
|
||
<td>${esc(item.status || '-')}</td>
|
||
<td>${typeof item.heartbeat_age_seconds === 'number' ? item.heartbeat_age_seconds + 's' : '-'}</td>
|
||
</tr>
|
||
`).join('') : `<tr><td colspan="4" class="empty">暂无在线设备</td></tr>`}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderArchitecturePanel(){
|
||
const assets = state.assets || [];
|
||
const archDocs = state.docs.filter(doc => ['architecture','api'].includes(doc.category));
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>架构与经验</h2>
|
||
<p class="sub">把核心架构图、控制模式和关键文档做成站内展区,方便工作台直接查看和复盘。</p>
|
||
</div>
|
||
</div>
|
||
<div class="asset-grid">
|
||
${assets.map(asset => `
|
||
<div class="asset-card">
|
||
<h3>${asset.title}</h3>
|
||
<p style="font-size:12px;color:var(--sub);margin-bottom:10px">${asset.summary}</p>
|
||
<img src="${asset.url}" alt="${asset.title}">
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>架构文档入口</h2>
|
||
<p class="sub">架构类文档同样走站内正文页,不再跳出单独预览页。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col3">
|
||
${archDocs.map(doc => `
|
||
<div class="card" onclick="openDocFromHome('${doc.id}')" style="cursor:pointer">
|
||
<h3>${doc.title}</h3>
|
||
<p>${doc.summary}</p>
|
||
<div class="path">${doc.folder}/${doc.file}</div>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderProjectPanel(){
|
||
const projectDocs = state.docs.filter(doc => ['project'].includes(doc.category));
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>项目管理</h2>
|
||
<p class="sub">开发进度、项目入口和说明文档统一在工作台收口,方便每次对话直接续上工作流。</p>
|
||
</div>
|
||
</div>
|
||
<div class="grid col3">
|
||
${projectDocs.map(doc => `
|
||
<div class="card" onclick="openDocFromHome('${doc.id}')" style="cursor:pointer">
|
||
<h3>${doc.title}</h3>
|
||
<p>${doc.summary}</p>
|
||
<div class="path">${doc.folder}/${doc.file}</div>
|
||
</div>
|
||
`).join('')}
|
||
${card('Swagger 文档', '接口调试仍然保留,但不再内嵌在工作台里。','<a class="btn soft" href="/docs" target="_blank">新窗口打开</a>')}
|
||
${card('ReDoc', '大纲式 API 文档继续保留为外部阅读入口。','<a class="btn soft" href="/redoc" target="_blank">新窗口打开</a>')}
|
||
${card('语音控制', '语音控制能力保留原入口,工作台负责统一导航。','<a class="btn soft" href="/voice" target="_blank">新窗口打开</a>')}
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderAgentClusterPanel(){
|
||
const agentDocs = state.docs.filter(d => d.category === 'agent');
|
||
const selected = state.selectedAgentDoc || agentDocs[0]?.id;
|
||
state.selectedAgentDoc = selected;
|
||
if (selected) requestAnimationFrame(() => {
|
||
const v = document.getElementById('agent-doc-view');
|
||
if (v && !v.dataset.loaded) { v.dataset.loaded = '1'; openAgentDoc(selected); }
|
||
});
|
||
return `
|
||
<div class="panel glass">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h2>Agent 集群文档</h2>
|
||
<p class="sub">设备端 Agent 的架构部署、技能引擎、网络恢复和 Hook 脚本系统完整文档。</p>
|
||
</div>
|
||
</div>
|
||
<div class="doc-layout">
|
||
<div class="doc-list">
|
||
${agentDocs.map(doc => `
|
||
<div class="doc-item ${doc.id===selected ? 'active' : ''}" data-docid="${doc.id}" onclick="openAgentDoc('${doc.id}')">
|
||
<h3>${doc.title}</h3>
|
||
<p>${doc.summary}</p>
|
||
<div class="doc-meta">
|
||
<span class="pill">${doc.section}</span>
|
||
<span class="pill">${doc.folder}/${doc.file}</span>
|
||
</div>
|
||
</div>
|
||
`).join('')}
|
||
${!agentDocs.length ? '<div class="empty">Agent 文档加载中...</div>' : ''}
|
||
</div>
|
||
<div class="doc-view" id="agent-doc-view"><div class="empty">选择左侧文档查看正文</div></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
async function openAgentDoc(docId){
|
||
state.selectedAgentDoc = docId;
|
||
document.querySelectorAll('#view .doc-item').forEach(el => {
|
||
el.classList.toggle('active', el.dataset.docid === docId);
|
||
});
|
||
const view = document.getElementById('agent-doc-view');
|
||
if (view) view.innerHTML = '<div class="empty">正文加载中...</div>';
|
||
try {
|
||
const res = await apiGet('/api/v3/workbench/docs/' + docId);
|
||
const doc = res.data;
|
||
if (!view) return;
|
||
view.innerHTML = '<div class="doc-header"><h3>' + doc.title + '</h3><p>' + doc.summary + '</p><div class="doc-meta"><span class="pill">' + doc.section + '</span><span class="pill">' + doc.folder + '/' + doc.file + '</span></div></div><div class="doc-body">' + doc.content_html + '</div>';
|
||
} catch(e) {
|
||
if (view) view.innerHTML = '<div class="empty">正文加载失败:' + esc(e.message) + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderCurrentView(){
|
||
const view = document.getElementById('view');
|
||
if (state.current === 'overview') view.innerHTML = renderOverview();
|
||
else if (state.current === 'docs') view.innerHTML = renderDocs();
|
||
else if (state.current === 'devices') view.innerHTML = renderDevicePanel();
|
||
else if (state.current === 'hook') view.innerHTML = renderHookPanel();
|
||
else if (state.current === 'agent') view.innerHTML = renderAgentPanel();
|
||
else if (state.current === 'agentCluster') view.innerHTML = renderAgentClusterPanel();
|
||
else if (state.current === 'connection') view.innerHTML = renderConnectionPanel();
|
||
else if (state.current === 'architecture') view.innerHTML = renderArchitecturePanel();
|
||
else if (state.current === 'project') view.innerHTML = renderProjectPanel();
|
||
view.style.animation = 'none';
|
||
void view.offsetHeight;
|
||
view.style.animation = 'fadeUp .35s cubic-bezier(.22,1,.36,1) both';
|
||
}
|
||
|
||
async function refreshOverview(){
|
||
try{
|
||
const res = await apiGet('/api/v3/workbench/overview');
|
||
state.overview = res.data;
|
||
state.devices = res.data.devices || [];
|
||
if (!state.currentDevice && state.devices.length) state.currentDevice = state.devices[0].device_id;
|
||
renderStats();
|
||
renderCurrentView();
|
||
}catch(error){
|
||
log(`工作台概览加载失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
async function refreshDocs(){
|
||
try{
|
||
const res = await apiGet('/api/v3/workbench/docs');
|
||
state.docs = res.data.docs || [];
|
||
state.assets = res.data.architecture_assets || [];
|
||
if (!state.selectedDoc && state.docs.length) state.selectedDoc = state.docs[0].id;
|
||
if (['docs','overview','architecture','project','agent','agentCluster'].includes(state.current)) {
|
||
const dv = document.getElementById('doc-view');
|
||
if (dv) delete dv.dataset.loaded;
|
||
const av = document.getElementById('agent-doc-view');
|
||
if (av) delete av.dataset.loaded;
|
||
renderCurrentView();
|
||
}
|
||
}catch(error){
|
||
log(`文档清单加载失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
async function refreshConnection(){
|
||
try{
|
||
const [protocolRes, statusRes] = await Promise.all([
|
||
apiGet('/api/v3/connection/protocol'),
|
||
apiGet('/api/v3/connection/status')
|
||
]);
|
||
state.protocol = protocolRes.data || {};
|
||
state.connection = statusRes.data || {};
|
||
if (['devices','connection'].includes(state.current)) renderCurrentView();
|
||
}catch(error){
|
||
log(`连接状态加载失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
async function refreshHook(){
|
||
try{
|
||
const [modulesRes, eventsRes] = await Promise.all([
|
||
apiGet('/api/v3/modules'),
|
||
apiGet('/api/v3/hook/events?limit=20')
|
||
]);
|
||
state.hookModules = modulesRes.data?.modules || [];
|
||
state.hookEvents = eventsRes.data?.events || [];
|
||
if (state.currentDevice){
|
||
try{
|
||
const deviceModulesRes = await apiGet(`/api/v3/devices/${state.currentDevice}/modules`);
|
||
state.currentDeviceModules = deviceModulesRes.data || null;
|
||
}catch(_){}
|
||
}
|
||
if (state.current === 'hook') renderCurrentView();
|
||
}catch(error){
|
||
log(`Hook 数据加载失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
async function refreshDevices(){
|
||
await Promise.all([refreshOverview(), refreshConnection()]);
|
||
if (state.currentDevice) await loadDeviceDetail(state.currentDevice);
|
||
}
|
||
|
||
async function refreshAll(){
|
||
await Promise.all([refreshOverview(), refreshConnection(), refreshDocs()]);
|
||
log('全部数据已刷新', 'info');
|
||
}
|
||
|
||
async function refreshConnectionModes(){
|
||
try {
|
||
const res = await apiGet('/api/v3/connection/modes');
|
||
const modeData = res.data || [];
|
||
for (const item of modeData) {
|
||
state.devices = (state.devices || []).map(d => {
|
||
if (d.device_id === item.device_id) {
|
||
return {...d, connection_modes: item.modes, best_mode: item.best_mode};
|
||
}
|
||
return d;
|
||
});
|
||
}
|
||
log(`连接模式检测完成:${modeData.length} 台设备`, 'info');
|
||
renderCurrentView();
|
||
} catch(error) {
|
||
log(`连接模式检测失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function selectDevice(deviceId){
|
||
state.currentDevice = deviceId;
|
||
renderCurrentView();
|
||
loadDeviceDetail(deviceId);
|
||
refreshHook();
|
||
log(`已切换设备:${deviceId}`, 'info');
|
||
}
|
||
|
||
async function loadDeviceDetail(deviceId){
|
||
try{
|
||
const [detailRes, modesRes] = await Promise.all([
|
||
apiGet(`/api/v3/devices/${deviceId}`),
|
||
apiGet(`/api/v3/connection/modes/${deviceId}`).catch(() => null)
|
||
]);
|
||
const detail = detailRes.data || {};
|
||
const modes = modesRes?.data || {};
|
||
const merged = {...detail, connection_modes: modes.modes || [], best_mode: modes.best_mode || null};
|
||
state.devices = (state.devices || []).map(item => item.device_id === deviceId ? {...item, ...merged} : item);
|
||
if (state.current === 'devices') renderCurrentView();
|
||
}catch(error){
|
||
log(`设备详情加载失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
async function doAction(action){
|
||
if (!state.currentDevice) return log('请先选择设备', 'error');
|
||
log(`执行操作:${action}`, 'info');
|
||
try{
|
||
if (action === 'screenshot'){
|
||
const res = await apiPost(`/api/v3/adb/devices/${state.currentDevice}/screenshot`);
|
||
const box = document.getElementById('shot-box');
|
||
if (box && res.data?.base64){
|
||
box.innerHTML = `<img src="data:image/png;base64,${res.data.base64}" alt="截图">`;
|
||
log(`截屏成功 ${res.data.width || ''}x${res.data.height || ''}`.trim(), 'info');
|
||
}
|
||
return;
|
||
}
|
||
if (action === 'ui_tree'){
|
||
const res = await apiGet(`/api/v3/adb/devices/${state.currentDevice}/ui-tree`);
|
||
log(`UI 树获取成功:${(res.data?.xml || '').length} 字符`, 'info');
|
||
return;
|
||
}
|
||
if (action === 'device_info'){
|
||
await loadDeviceDetail(state.currentDevice);
|
||
log('设备详情已刷新', 'info');
|
||
return;
|
||
}
|
||
const actionMap = {
|
||
open_wechat: {url:`/api/v3/adb/devices/${state.currentDevice}/app/start`, body:{package:'com.tencent.mm'}},
|
||
open_douyin: {url:`/api/v3/adb/devices/${state.currentDevice}/app/start`, body:{package:'com.ss.android.ugc.aweme'}},
|
||
open_xhs: {url:`/api/v3/adb/devices/${state.currentDevice}/app/start`, body:{package:'com.xingin.xhs'}},
|
||
open_xianyu: {url:`/api/v3/adb/devices/${state.currentDevice}/app/start`, body:{package:'com.taobao.idlefish'}},
|
||
home: {url:`/api/v3/adb/devices/${state.currentDevice}/key`, body:{key:'home'}},
|
||
back: {url:`/api/v3/adb/devices/${state.currentDevice}/key`, body:{key:'back'}},
|
||
};
|
||
const config = actionMap[action];
|
||
if (!config) return log(`未知操作:${action}`, 'error');
|
||
const res = await apiPost(config.url, config.body);
|
||
log(`操作完成:${action} ${JSON.stringify(res.data || {}).slice(0, 80)}`, 'info');
|
||
setTimeout(() => doAction('screenshot'), 900);
|
||
}catch(error){
|
||
log(`操作失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function setMode(mode){
|
||
state.cmdMode = mode;
|
||
if (state.current === 'devices') renderCurrentView();
|
||
}
|
||
|
||
async function sendCmd(){
|
||
const input = document.getElementById('cmd-input');
|
||
const task = input?.value?.trim();
|
||
if (!task) return;
|
||
if (input) input.value = '';
|
||
if (!state.currentDevice && state.cmdMode !== 'ai-only') return log('请先选择设备', 'error');
|
||
const start = Date.now();
|
||
log(`命令开始:${task}`, 'info');
|
||
try{
|
||
if (state.cmdMode === 'pattern'){
|
||
const res = await apiPost('/api/v3/agent/execute', {device_id: state.currentDevice, task});
|
||
const data = res.data || {};
|
||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||
if (data.success) log(`执行成功:${(data.steps || []).join(' → ') || '完成'} (${elapsed}s)`, 'info');
|
||
else log(`执行失败:${data.error || '未知错误'} (${elapsed}s)`, 'error');
|
||
} else {
|
||
const body = {message: task};
|
||
if (state.cmdMode === 'ai' && state.currentDevice) body.device_id = state.currentDevice;
|
||
const res = await apiPost('/api/v3/ai/chat', body);
|
||
const data = res.data || {};
|
||
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
|
||
log(`AI 返回:${data.message || '已解析'} (${elapsed}s)`, 'info');
|
||
(data.executed || []).forEach(item => log(`${item.success ? '完成' : '失败'}:${item.step}`, item.success ? 'info' : 'error'));
|
||
}
|
||
if (state.cmdMode !== 'ai-only') setTimeout(() => doAction('screenshot'), 1200);
|
||
}catch(error){
|
||
log(`命令执行失败:${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
/* ═══ Lightbox ═══ */
|
||
function openLightbox(src, title){
|
||
const lb = document.getElementById('lightbox');
|
||
const img = document.getElementById('lb-img');
|
||
const tt = document.getElementById('lb-title');
|
||
img.src = src;
|
||
tt.textContent = title || '';
|
||
lb.classList.add('show');
|
||
}
|
||
function closeLightbox(){
|
||
document.getElementById('lightbox').classList.remove('show');
|
||
}
|
||
document.addEventListener('click', function(e){
|
||
const img = e.target;
|
||
if (img.tagName !== 'IMG') return;
|
||
if (img.closest('.asset-card') || img.closest('.doc-body') || img.closest('.shot')) {
|
||
e.stopPropagation();
|
||
openLightbox(img.src, img.alt || '');
|
||
}
|
||
});
|
||
document.addEventListener('keydown', function(e){
|
||
if (e.key === 'Escape') closeLightbox();
|
||
});
|
||
|
||
async function bootstrap(){
|
||
renderSidebar();
|
||
await Promise.all([refreshOverview(), refreshDocs(), refreshConnection(), refreshHook()]);
|
||
renderCurrentView();
|
||
}
|
||
|
||
bootstrap();
|
||
setInterval(() => {
|
||
refreshOverview();
|
||
refreshConnection();
|
||
if (state.current === 'hook') refreshHook();
|
||
}, 15000);
|
||
</script>
|
||
</body>
|
||
</html>
|