Files
workphone-sdk/sdk/app/static/hub.html

3118 lines
195 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AI数智员工 · 控制台</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;z-index:10;background:rgba(255,255,255,.92)}
.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(6,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:300px minmax(0,1fr);gap:20px}
@media (max-width:1080px){.device-layout{grid-template-columns:1fr}}
.device-list{display:flex;flex-direction:column;gap:8px;max-height:600px;overflow-y:auto;scrollbar-width:thin;padding-right:4px}
.device-list::-webkit-scrollbar{width:3px}
.device-list::-webkit-scrollbar-thumb{background:rgba(0,0,0,.12);border-radius:3px}
.device-item{display:flex;align-items:center;gap:12px;padding:14px 16px;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:14px;font-weight:700}
.device-meta{font-size:11.5px;color:var(--sub);margin-top:3px}
.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)}
.status-dot.unauthorized{background:var(--orange);box-shadow:0 0 6px rgba(245,158,11,.4)}
.status-dot.offline{background:var(--red);box-shadow:0 0 6px rgba(220,38,38,.3)}
.device-item.unauth{border-color:rgba(245,158,11,.25);background:rgba(245,158,11,.04)}
/* device detail right pane */
.device-main{display:flex;flex-direction:column;gap:16px}
.device-main .detail-row{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media (max-width:900px){.device-main .detail-row{grid-template-columns:1fr}}
.device-main .detail-wide{grid-column:1/-1}
/* health bar */
.health-bar{height:6px;border-radius:3px;background:rgba(15,23,42,.06);overflow:hidden;margin-top:6px}
.health-bar-fill{height:100%;border-radius:3px;transition:width .5s var(--ease)}
.health-score{font-size:28px;font-weight:800;letter-spacing:-.04em;line-height:1}
.health-label{font-size:11px;color:var(--sub);margin-top:2px}
/* 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:10px;margin-top:14px}
@media(max-width:1280px){.mode-detail-grid{grid-template-columns:repeat(3,1fr)}}
@media(max-width:720px){.mode-detail-grid{grid-template-columns:repeat(2,1fr)}}
.mode-detail-card{padding:16px 12px;border-radius:14px;border:1px solid var(--border);background:var(--panel-strong);text-align:center;transition:all .25s var(--ease)}
.mode-detail-card:hover{border-color:var(--border-active);transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.04)}
.mode-detail-card.active{border-color:var(--green);background:rgba(22,163,74,.03);box-shadow:0 0 0 2px rgba(22,163,74,.1)}
.mode-detail-card.best-active{border-color:var(--blue);background:rgba(37,99,235,.03);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:24px;margin-bottom:6px}
.mode-detail-card .mode-name{font-size:12.5px;font-weight:700}
.mode-detail-card .mode-status{font-size:11px;margin-top:6px;font-weight:500}
.mode-detail-card .mode-prio{font-size:10px;color:var(--muted);margin-top:4px;line-height:1.4}
/* 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}
/* ═══ Connection Status Bar ═══ */
.conn-bar{position:fixed;bottom:0;left:0;right:0;z-index:100;display:flex;align-items:center;justify-content:space-between;padding:8px 24px;background:rgba(15,23,42,.92);color:#e2e8f0;font-size:12px;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-top:1px solid rgba(255,255,255,.06);gap:12px}
.conn-bar .conn-group{display:flex;align-items:center;gap:16px}
.conn-bar .conn-item{display:flex;align-items:center;gap:6px;font-weight:500}
.conn-bar .conn-dot{width:8px;height:8px;border-radius:999px;flex-shrink:0}
.conn-bar .conn-dot.green{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.5)}
.conn-bar .conn-dot.yellow{background:#eab308;box-shadow:0 0 8px rgba(234,179,8,.4)}
.conn-bar .conn-dot.red{background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,.4)}
.conn-bar .conn-dot.pulse{animation:pulse-dot 2s infinite}
@keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.4}}
.conn-bar .conn-action{padding:4px 12px;border-radius:6px;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.08);color:#fff;font-size:11px;cursor:pointer;transition:.2s;font-weight:600}
.conn-bar .conn-action:hover{background:rgba(255,255,255,.18)}
/* ═══ Discovery Panel ═══ */
.discovery-card{padding:20px;border-radius:16px;border:1px solid rgba(37,99,235,.15);background:linear-gradient(135deg,rgba(37,99,235,.04),rgba(14,165,166,.03));transition:all .25s var(--ease)}
.discovery-card:hover{border-color:rgba(37,99,235,.25);box-shadow:0 8px 24px rgba(37,99,235,.08)}
.scan-progress{height:4px;border-radius:2px;background:rgba(37,99,235,.1);overflow:hidden;margin-top:12px}
.scan-progress-bar{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--blue),var(--teal));animation:scan-move 2s ease-in-out infinite;width:30%;transform-origin:left}
@keyframes scan-move{0%{transform:translateX(-100%)}50%{transform:translateX(200%)}100%{transform:translateX(400%)}}
/* ═══ KPI Hero ═══ */
.kpi-hero{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:16px}
@media(max-width:980px){.kpi-hero{grid-template-columns:1fr}}
.kpi-north-star{grid-column:1;grid-row:1/3;padding:28px 24px;border-radius:var(--radius);background:linear-gradient(135deg,var(--blue),var(--teal));color:#fff;display:flex;flex-direction:column;justify-content:center;box-shadow:0 12px 32px rgba(37,99,235,.2)}
.kpi-north-star .kpi-value{font-size:56px;font-weight:900;letter-spacing:-.06em;line-height:1}
.kpi-north-star .kpi-label{font-size:15px;font-weight:600;margin-top:8px;opacity:.9}
.kpi-north-star .kpi-sub{font-size:12px;margin-top:4px;opacity:.7}
.kpi-secondary{padding:20px;border-radius:var(--radius);background:var(--panel);border:1px solid var(--border);backdrop-filter:blur(var(--blur));-webkit-backdrop-filter:blur(var(--blur))}
.kpi-secondary .kpi-value{font-size:28px;font-weight:800;letter-spacing:-.04em;background:linear-gradient(135deg,var(--text),var(--blue));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.kpi-secondary .kpi-label{font-size:12px;color:var(--sub);font-weight:500;margin-top:4px}
/* ═══ Quick Action Grid ═══ */
.quick-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px}
.quick-item{display:flex;align-items:center;gap:10px;padding:14px 16px;border-radius:14px;border:1px solid var(--border);background:var(--panel-strong);cursor:pointer;transition:all .2s var(--ease)}
.quick-item:hover{border-color:var(--border-active);transform:translateY(-2px);box-shadow:0 6px 16px rgba(37,99,235,.08)}
.quick-item .qi-icon{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;font-size:18px;flex-shrink:0}
.quick-item .qi-text{font-size:12.5px;font-weight:600}
.quick-item .qi-desc{font-size:10.5px;color:var(--sub);margin-top:2px}
/* ═══ Responsive padding for conn-bar ═══ */
.app{padding-bottom:64px}
</style>
</head>
<body>
<div class="app">
<header class="topbar glass">
<div class="brand">
<div class="brand-icon">⚙️</div>
<div>
<h1>AI数智员工 · 控制台</h1>
<p>多手机统一管控 · 设备管理 · 智能引擎 · 对接网关 · 知识库</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">工作手机 SDK v3.0 · 机擎 v4.0 · 设备管理 · 智能引擎 · 对接网关 · 知识库</div>
</div>
<div class="conn-bar" id="conn-bar">
<div class="conn-group">
<div class="conn-item"><span class="conn-dot" id="cb-ws-dot"></span><span id="cb-ws-text">WS 检测中</span></div>
<div class="conn-item"><span class="conn-dot" id="cb-adb-dot"></span><span id="cb-adb-text">ADB 检测中</span></div>
<div class="conn-item"><span class="conn-dot" id="cb-beacon-dot"></span><span id="cb-beacon-text">Beacon 检测中</span></div>
<div class="conn-item" id="cb-discovery-status" style="display:none"><span class="conn-dot yellow pulse"></span><span>正在扫描局域网...</span></div>
</div>
<div class="conn-group">
<span id="cb-server-ip" style="color:rgba(255,255,255,.5);font-size:11px"></span>
<button class="conn-action" onclick="scanLanDevices()">🔍 扫描局域网</button>
<button class="conn-action" onclick="refreshAll()">🔄 刷新全部</button>
</div>
</div>
<div class="lightbox" id="lightbox" onclick="closeLightbox()">
<button class="lb-close" onclick="closeLightbox()">&times;</button>
<img id="lb-img" src="" alt="放大预览">
<div class="lb-title" id="lb-title"></div>
</div>
<script>
const API = (function() {
if (location.protocol === 'file:' && typeof NativeAgent !== 'undefined') {
try {
const cfg = JSON.parse(NativeAgent.getConfig());
const ws = cfg.server_url || '';
const m = ws.match(/^wss?:\/\/([^/]+)/);
if (m) return 'http://' + m[1];
} catch(e) {}
}
return location.origin !== 'null' ? location.origin : '';
})();
const NAVS = [
{id:'overview', icon:'🏠', name:'工作台', desc:'KPI 面板 · 快速操作 · 发现'},
{id:'devices', icon:'📱', name:'设备管理', desc:'控制 · 接口 · 系统架构'},
{id:'engine', icon:'🧠', name:'智能引擎', desc:'AI Brain · Hook · 心跳'},
{id:'gateway', icon:'🔗', name:'对接网关', desc:'REST · MCP · OpenAI'},
{id:'docs', icon:'📖', name:'知识库', desc:'文档 · 架构图 · 手册'},
];
const _validTabs = NAVS.map(n => n.id);
const _urlTab = new URLSearchParams(location.search).get('tab');
const _resolvedTab = _urlTab === 'wechat' ? 'devices' : (_urlTab === 'infra' ? 'engine' : _urlTab);
const state = {
current: _validTabs.includes(_resolvedTab) ? _resolvedTab : 'overview',
overview: null,
docs: [],
assets: [],
selectedDoc: null,
docFilter: '',
devices: [],
currentDevice: null,
deviceTab: 'control',
appFocus: 'wechat',
protocol: null,
connection: null,
hookModules: [],
hookEvents: [],
hookData: {},
hookDataLoading: false,
currentDeviceModules: null,
heartbeatDashboard: null,
heartbeatEvents: [],
gatewayTab: 'overview',
gatewayInfo: null,
fleetStatus: null,
journeys: null,
wxKeyword: '',
wxStatusFilter: 'all',
logs: [],
cmdMode: 'pattern',
engineTab: new URLSearchParams(location.search).get('engineTab') || 'brain',
skillRegistry: {},
brainDashboard: {},
brainSub: 'overview',
brainSkillFilter: 'all',
infraTab: 'connection',
docsTab: 'manual',
selectedAgentDoc: null,
};
/* ═══ Utilities ═══ */
function esc(text=''){
return String(text).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
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();
const box = document.querySelector('.log-box');
if (box) { box.innerHTML = state.logs.map(l=>`<div class="${l.type==='error'?'log-error':'log-info'}">[${l.ts}] ${esc(l.message)}</div>`).join(''); box.scrollTop=box.scrollHeight; }
}
/* ═══ API ═══ */
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();
}
/* ═══ Rendering Infrastructure ═══ */
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 ov = state.overview || {};
const unauth = ov.unauthorized_count ?? 0;
const adbAll = ov.adb_all ?? ov.adb_count ?? 0;
const totalDevices = ov.device_total ?? 0;
const wsCount = ov.ws_online ?? 0;
const adbCount = ov.adb_count ?? 0;
const controllable = wsCount + adbCount;
document.getElementById('stats').innerHTML = `
${statCard('📱 设备总数', `<span style="font-size:32px;color:${totalDevices?'var(--blue)':'var(--muted)'};">${totalDevices}</span>`, `${controllable} 可控 · ${unauth ? unauth + ' 待授权' : '全部就绪'}`)}
${statCard('🔌 WS 连接', wsCount, wsCount > 0 ? '设备在线' : '无连接')}
${statCard('📡 ADB 设备', `${adbCount}<span style="font-size:14px;color:var(--muted)"> / ${adbAll}</span>`, unauth ? `⚠️ ${unauth} 待授权` : 'USB/WiFi 直连')}
${statCard('🔗 API 端点', '150+', '3 种对接方式')}
${statCard('📄 知识文档', (ov.docs || state.docs || []).length || 0, '站内阅读')}
${statCard('⚡ 服务状态', ov.sdk_online ? '<span style="color:var(--green)">健康</span>' : '<span style="color:var(--red)">离线</span>', `v${ov.sdk_version || '-'}`)}
`;
document.getElementById('chip-health').innerHTML = `<span class="dot ${ov.sdk_online ? 'ok' : 'bad'}"></span>${ov.sdk_online ? '在线' : '离线'}`;
document.getElementById('chip-ws').textContent = `WS ${wsCount}`;
document.getElementById('chip-adb').textContent = `ADB ${adbCount}${unauth ? `${unauth}` : ''}`;
document.getElementById('chip-version').textContent = `v${ov.sdk_version || '-'}`;
updateConnBar();
}
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){
if (id === 'wechat') { state.current = 'devices'; state.deviceTab = 'control'; }
else if (id === 'infra') { state.current = 'engine'; state.engineTab = 'connection'; }
else { state.current = id; }
const url = new URL(location.href);
url.searchParams.set('tab', state.current);
history.replaceState({}, '', url.toString());
renderSidebar();
renderCurrentView();
}
function card(title, desc, action){
return `
<div class="card">
<h3>${title}</h3>
<p>${desc}</p>
${action || ''}
</div>
`;
}
function actionBtn(icon, label, handler){
return `<div class="action" onclick="${handler}"><div class="icon">${icon}</div><div>${label}</div></div>`;
}
function cmdPlaceholder(){
if (state.cmdMode === 'pattern') return '输入指令:打开微信;然后截图';
if (state.cmdMode === 'ai') return '自然语言任务:打开微信发消息给张三';
return '解析指令:打开淘宝搜索蓝牙耳机';
}
function deviceStatusLabel(device){
const st = device.status || device.adb_status || '';
if (st === 'online') return '<span style="color:var(--green);font-weight:600">● 在线</span>';
if (st === 'adb') return '<span style="color:var(--blue);font-weight:600">● ADB</span>';
if (st === 'unauthorized') return '<span style="color:var(--orange);font-weight:600">⚠ 待授权</span>';
if (st === 'offline') return '<span style="color:var(--red);font-weight:600">○ 离线</span>';
return `<span style="color:var(--muted)">${esc(st)}</span>`;
}
/* ═══ 1. Overview Panel ═══ */
function renderOverview(){
const ov = state.overview || {};
const modes = ov.control_modes || [];
const devices = ov.devices || [];
const totalDevices = ov.device_total || 0;
const wsCount = ov.ws_online || 0;
const adbCount = ov.adb_count || 0;
const controllable = wsCount + adbCount;
const unauth = ov.unauthorized_count || 0;
const discoveredCount = state.discoveredServers?.length || 0;
return `
<!-- KPI Hero Area (F-pattern: North Star at top-left) -->
<div class="kpi-hero">
<div class="kpi-north-star glass">
<div class="kpi-value">${totalDevices}</div>
<div class="kpi-label">已连接设备</div>
<div class="kpi-sub">${controllable} 可控 · ${wsCount} WS · ${adbCount} ADB${unauth ? ` · ${unauth} 待授权` : ''}</div>
</div>
<div class="kpi-secondary glass">
<div class="kpi-value">${ov.sdk_online ? '✓ 健康' : '✗ 离线'}</div>
<div class="kpi-label">服务状态 · v${ov.sdk_version || '-'}</div>
</div>
<div class="kpi-secondary glass">
<div class="kpi-value">150+</div>
<div class="kpi-label">API 端点 · 3 种对接</div>
</div>
<div class="kpi-secondary glass">
<div class="kpi-value">${(ov.docs || state.docs || []).length || 0}</div>
<div class="kpi-label">知识文档 · 站内阅读</div>
</div>
<div class="kpi-secondary glass">
<div class="kpi-value">4</div>
<div class="kpi-label">控制通道 · 智能切换</div>
</div>
</div>
<!-- Quick Actions -->
<div class="panel glass">
<div class="panel-head">
<div><h2>快速操作</h2><p class="sub">一键进入核心功能</p></div>
<button class="btn soft" onclick="refreshAll()">🔄 刷新全部</button>
</div>
<div class="quick-grid">
<div class="quick-item" onclick="navigate('devices')"><div class="qi-icon" style="background:rgba(37,99,235,.1);color:var(--blue)">📱</div><div><div class="qi-text">设备管理</div><div class="qi-desc">${totalDevices} 台设备</div></div></div>
<div class="quick-item" onclick="navigate('engine')"><div class="qi-icon" style="background:rgba(124,58,237,.1);color:var(--purple)">🧠</div><div><div class="qi-text">智能引擎</div><div class="qi-desc">AI Brain · Hook</div></div></div>
<div class="quick-item" onclick="navigate('gateway')"><div class="qi-icon" style="background:rgba(14,165,166,.1);color:var(--teal)">🔗</div><div><div class="qi-text">对接网关</div><div class="qi-desc">API · MCP · OpenAI</div></div></div>
<div class="quick-item" onclick="navigate('docs')"><div class="qi-icon" style="background:rgba(22,163,74,.1);color:var(--green)">📖</div><div><div class="qi-text">知识库</div><div class="qi-desc">${(ov.docs || state.docs || []).length} 篇文档</div></div></div>
<div class="quick-item" onclick="scanLanDevices()"><div class="qi-icon" style="background:rgba(245,158,11,.1);color:var(--orange)">🔍</div><div><div class="qi-text">扫描局域网</div><div class="qi-desc">${state.discoveryStatus === 'scanning' ? '扫描中...' : discoveredCount ? discoveredCount + ' 个服务' : '自动发现'}</div></div></div>
<div class="quick-item" onclick="window.open('/docs','_blank')"><div class="qi-icon" style="background:rgba(220,38,38,.1);color:var(--red)">📄</div><div><div class="qi-text">Swagger 文档</div><div class="qi-desc">完整 API 参考</div></div></div>
</div>
</div>
<!-- Discovery Status (if scanned) -->
${state.discoveredServers?.length ? `
<div class="panel glass discovery-card">
<div class="panel-head">
<div><h2>🔍 局域网发现</h2><p class="sub">已发现 ${state.discoveredServers.length} 个 SDK 服务${state.serverInfo ? ` · 本机 ${state.serverInfo.ips?.[0] || 'localhost'}:${state.serverInfo.port}` : ''}</p></div>
<button class="btn soft" onclick="scanLanDevices()">重新扫描</button>
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px">
${state.discoveredServers.map(s => `
<div class="card" style="${s.is_self ? 'border-color:var(--green);background:rgba(22,163,74,.04)' : ''}">
<h3>${s.is_self ? '🏠 本机服务器' : '🖥️ 发现的服务'}</h3>
<p><strong>${s.ip}:${s.port}</strong></p>
<p style="font-size:11px;color:var(--sub);margin-top:4px">
WS: ${esc(s.ws_url)}<br>
发现时间: ${s.discovered_at ? new Date(s.discovered_at).toLocaleTimeString() : '-'}
</p>
${!s.is_self ? `<button class="btn soft" style="margin-top:8px;font-size:11px" onclick="window.open('${s.url}','_blank')">打开控制台</button>` : ''}
</div>
`).join('')}
</div>
</div>` : ''}
<!-- Control Flow -->
<div class="panel glass">
<div class="panel-head">
<div><h2>控制链路</h2><p class="sub">从业务请求到设备执行的四步闭环</p></div>
</div>
<div class="flow">
<div class="flow-item"><div class="flow-step">STEP 1</div><h3>业务请求</h3><p>存客宝 / 管理端 / API 入口统一进入 SDK</p></div>
<div class="flow-item"><div class="flow-step">STEP 2</div><h3>智能决策</h3><p>Hook → Agent → ADB → AI 多通道自动择优</p></div>
<div class="flow-item"><div class="flow-step">STEP 3</div><h3>设备执行</h3><p>Frida / u2 / scrcpy 分别承接深度控制和自动化</p></div>
<div class="flow-item"><div class="flow-step">STEP 4</div><h3>结果沉淀</h3><p>事件、截图、经验回到工作台形成闭环</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 class="mode-detail-grid">
${modes.map(m => {
const icons = {hook:'🪝',agent:'🔌',adb:'📱',ai:'🤖',scrcpy:'🖥️'};
return `<div class="mode-detail-card"><div class="mode-icon">${icons[m.id]||'⚡'}</div><div class="mode-name">${m.name}</div><div class="mode-status" style="color:var(--sub)">${m.desc}</div><div class="mode-prio">P${m.priority||'?'}</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> APP</th></tr></thead><tbody>
${devices.length ? devices.slice(0,8).map(d => {
const isU = d.status==='unauthorized';
return `<tr onclick="${isU?'':`navigate('devices');selectDevice('${d.device_id}')`}" style="cursor:${isU?'default':'pointer'};${isU?'opacity:.6':''}">
<td><strong>${isU?'🔒 ':''}${esc(d.model||d.device_id||'-')}</strong></td>
<td>${deviceStatusLabel(d)}</td>
<td><div class="mode-badges">${isU?'<span class="mode-badge unavailable">⚠ 需授权</span>':((d.connection_modes||[]).filter(m=>m.available).slice(0,3).map(m=>`<span class="mode-badge available"><span class="badge-dot"></span>${esc(m.id)}</span>`).join('')||'ADB')}</div></td>
<td>${isU?'-':esc(d.current_app||'-')}</td></tr>`;
}).join('') : `<tr><td colspan="4" class="empty">暂无设备 · <a href="javascript:void(0)" onclick="navigate('devices')" style="color:var(--blue)">连接设备</a></td></tr>`}
</tbody></table></div>
</div>
`;
}
/* ═══ 1.5 WeChat Control Panel ═══ */
const WX_MODULES = {
message:{title:'消息管理',count:6,ch:'Hook优先(Frida RPC ~200ms) | u2降级(~5s)',actions:[
{n:'发送消息',a:'send_message',f:[{k:'to_id',l:'联系人'},{k:'content',l:'内容',t:'textarea'},{k:'msg_type',l:'类型',v:'text'}]},
{n:'获取消息',a:'get_messages',f:[{k:'conversation_id',l:'会话'},{k:'limit',l:'数量',v:'20'}]},
{n:'批量发送',a:'batch_send_message',f:[{k:'to_ids',l:'接收者(逗号分隔)'},{k:'content',l:'内容',t:'textarea'}]},
{n:'转发消息',a:'forward_message',f:[{k:'to_id',l:'转发给'}]},
{n:'撤回消息',a:'recall_message',f:[]},
{n:'发送名片',a:'send_card',f:[{k:'to_id',l:'发给谁'},{k:'card_wxid',l:'名片微信号'}]},
]},
friend:{title:'好友管理',count:8,ch:'Hook可读(SQLite ~100ms) | u2写(~5s)',actions:[
{n:'添加好友',a:'add_friend',f:[{k:'user_id',l:'微信号'},{k:'message',l:'验证消息'}]},
{n:'通过请求',a:'accept_friend',f:[{k:'user_id',l:'用户(可选)'}]},
{n:'设置备注',a:'set_remark',f:[{k:'user_id',l:'好友'},{k:'remark',l:'备注'}]},
{n:'删除好友',a:'delete_friend',f:[{k:'user_id',l:'好友'}]},
{n:'获取联系人',a:'get_contacts',f:[{k:'limit',l:'数量',v:'100'}]},
{n:'搜索联系人',a:'search_contact',f:[{k:'keyword',l:'关键词'}]},
{n:'好友详情',a:'get_friend_info',f:[{k:'user_id',l:'好友'}]},
{n:'批量添加',a:'batch_add_friend',f:[{k:'user_ids',l:'ID列表(逗号)'},{k:'message',l:'验证消息'}]},
]},
group:{title:'群聊管理',count:10,ch:'Hook可读(群/成员走SQLite) | u2写',actions:[
{n:'创建群聊',a:'create_group',f:[{k:'group_name',l:'群名'},{k:'member_ids',l:'成员(逗号)'}]},
{n:'邀请入群',a:'invite_to_group',f:[{k:'group_id',l:'群名'},{k:'member_ids',l:'邀请(逗号)'}]},
{n:'移出群聊',a:'remove_from_group',f:[{k:'group_id',l:'群名'},{k:'member_ids',l:'移除(逗号)'}]},
{n:'群公告',a:'set_group_notice',f:[{k:'group_id',l:'群名'},{k:'notice',l:'公告',t:'textarea'}]},
{n:'设置群名',a:'set_group_name',f:[{k:'group_id',l:'群名'},{k:'group_name',l:'新群名'}]},
{n:'群消息',a:'send_group_message',f:[{k:'group_id',l:'群名'},{k:'content',l:'内容',t:'textarea'}]},
{n:'群欢迎语',a:'set_group_welcome',f:[{k:'group_id',l:'群名'},{k:'welcome_text',l:'欢迎语'}]},
{n:'群列表',a:'get_groups',f:[{k:'limit',l:'数量',v:'100'}]},
{n:'群成员',a:'get_group_members',f:[{k:'group_id',l:'群名'}]},
{n:'退出群聊',a:'quit_group',f:[{k:'group_id',l:'群名'}]},
]},
moments:{title:'朋友圈',count:8,ch:'u2 UI自动化(~5-10s)',actions:[
{n:'发布朋友圈',a:'post_moments',f:[{k:'content',l:'文字',t:'textarea'}]},
{n:'点赞',a:'like_moments',f:[{k:'user_id',l:'好友'},{k:'post_index',l:'第几条',v:'0'}]},
{n:'评论',a:'comment_moments',f:[{k:'user_id',l:'好友'},{k:'comment',l:'评论'},{k:'post_index',l:'第几条',v:'0'}]},
{n:'获取朋友圈',a:'get_moments',f:[{k:'user_id',l:'好友(空=自己)'},{k:'limit',l:'数量',v:'10'}]},
{n:'删除',a:'delete_moments',f:[{k:'post_index',l:'第几条',v:'0'}]},
{n:'设置封面',a:'set_moments_cover',f:[]},
{n:'可见天数',a:'set_moments_privacy',f:[{k:'days',l:'天数',v:'180'}]},
{n:'分享链接',a:'forward_moments_link',f:[{k:'url',l:'URL'},{k:'title',l:'标题'}]},
]},
tag:{title:'标签管理',count:6,ch:'Hook可读(SQLite) | u2写',actions:[
{n:'创建标签',a:'create_tag',f:[{k:'tag_name',l:'标签名'}]},
{n:'删除标签',a:'delete_tag',f:[{k:'tag_name',l:'标签名'}]},
{n:'标签列表',a:'get_tags',f:[]},
{n:'打标签',a:'add_tag',f:[{k:'user_id',l:'好友'},{k:'tags',l:'标签(逗号)'}]},
{n:'移除标签',a:'remove_tag',f:[{k:'user_id',l:'好友'},{k:'tags',l:'标签(逗号)'}]},
{n:'按标签查人',a:'get_users_by_tag',f:[{k:'tag_name',l:'标签名'}]},
]},
profile:{title:'个人设置',count:6,ch:'Hook可读(SQLite ~50ms) | u2写(~6s)',actions:[
{n:'获取资料',a:'get_profile',f:[]},
{n:'修改昵称',a:'set_nickname',f:[{k:'nickname',l:'昵称'}]},
{n:'修改签名',a:'set_signature',f:[{k:'signature',l:'签名'}]},
{n:'修改头像',a:'set_avatar',f:[]},
{n:'设置性别',a:'set_gender',f:[{k:'gender',l:'性别(男/女)'}]},
{n:'设置地区',a:'set_region',f:[{k:'region',l:'地区(空格分隔)'}]},
]},
security:{title:'账号安全',count:9,ch:'u2 UI自动化(~5-10s)',actions:[
{n:'账号状态',a:'check_account_status',f:[]},
{n:'安全中心',a:'safety_center',f:[]},
{n:'微信解封',a:'unblock_account',f:[]},
{n:'自助解封',a:'unblock_self',f:[]},
{n:'申诉解封',a:'unblock_appeal',f:[]},
{n:'短信解封',a:'unblock_with_sms',f:[{k:'phone',l:'手机号'}]},
{n:'修改密码',a:'change_password',f:[]},
{n:'功能限制',a:'check_restrictions',f:[]},
{n:'申诉限制',a:'appeal_restriction',f:[]},
]},
payment:{title:'支付',count:7,ch:'u2(安全考虑不走Hook, ~5-10s)',actions:[
{n:'发红包',a:'send_red_packet',f:[{k:'to_id',l:'发给谁'},{k:'amount',l:'金额'}]},
{n:'转账',a:'transfer',f:[{k:'to_id',l:'转给谁'},{k:'amount',l:'金额'}]},
{n:'付款码',a:'show_payment_code',f:[]},
{n:'收款',a:'receive_payment',f:[]},
{n:'钱包',a:'view_wallet',f:[]},
{n:'账单',a:'view_transactions',f:[]},
{n:'领红包',a:'receive_red_packet',f:[]},
]},
more:{title:'更多功能',count:27,ch:'u2 UI自动化',actions:[
{n:'视频号',a:'open_video_channel',f:[]},
{n:'点赞视频',a:'like_video',f:[{k:'index',l:'第几个',v:'0'}]},
{n:'扫一扫',a:'scan_qr_code',f:[]},
{n:'我的二维码',a:'show_my_qr',f:[]},
{n:'置顶聊天',a:'set_chat_top',f:[{k:'user_id',l:'联系人'}]},
{n:'免打扰',a:'set_mute_chat',f:[{k:'user_id',l:'联系人'}]},
{n:'清空记录',a:'clear_chat_history',f:[{k:'user_id',l:'联系人'}]},
{n:'收藏列表',a:'get_favorites',f:[]},
{n:'打开小程序',a:'open_mini_program',f:[{k:'name',l:'名称'}]},
{n:'关注公众号',a:'follow_official_account',f:[{k:'account_name',l:'公众号名'}]},
{n:'语音通话',a:'voice_call',f:[{k:'user_id',l:'联系人'}]},
{n:'视频通话',a:'video_call',f:[{k:'user_id',l:'联系人'}]},
{n:'群发消息',a:'mass_send',f:[{k:'content',l:'内容',t:'textarea'}]},
{n:'搜一搜',a:'wechat_search',f:[{k:'keyword',l:'关键词'}]},
{n:'看一看',a:'top_stories',f:[]},
{n:'获取步数',a:'get_steps',f:[]},
{n:'点赞步数',a:'like_steps',f:[{k:'user_id',l:'好友'}]},
{n:'发送位置',a:'send_location',f:[{k:'user_id',l:'联系人'}]},
{n:'实时位置',a:'share_real_time_location',f:[{k:'user_id',l:'联系人'}]},
{n:'发送表情',a:'send_emoji',f:[{k:'user_id',l:'联系人'}]},
{n:'表情列表',a:'get_sticker_list',f:[]},
{n:'语音消息',a:'send_voice_message',f:[{k:'user_id',l:'联系人'},{k:'duration',l:'秒数',v:'3'}]},
{n:'发送文件',a:'send_file_from_chat',f:[{k:'user_id',l:'联系人'}]},
{n:'勿扰模式',a:'toggle_do_not_disturb',f:[]},
{n:'清理缓存',a:'clear_cache',f:[]},
{n:'退出登录',a:'logout',f:[]},
{n:'切换账号',a:'switch_account',f:[]},
]},
};
const _wxStats = {total:0,success:0,totalTime:0};
const _wxLogs = [];
let _wxModule = 'message';
function wxStatusTone(device){
const st = device?.status || device?.adb_status || 'offline';
if (st === 'online') return {label:'WS在线', color:'var(--green)'};
if (st === 'adb') return {label:'ADB可控', color:'var(--blue)'};
if (st === 'unauthorized') return {label:'待授权', color:'var(--orange)'};
return {label:'离线', color:'var(--red)'};
}
function wxHealthScore(device){
if (!device) return 0;
let score = 35;
const st = device.status || device.adb_status || '';
if (st === 'online') score += 35;
else if (st === 'adb') score += 28;
if (device.wechat_running || device.current_app === 'com.tencent.mm') score += 18;
if (device.last_heartbeat) score += 12;
return Math.min(score, 100);
}
function selectWechatWorkspaceDevice(deviceId){
state.currentDevice = deviceId;
renderCurrentView(false);
loadDeviceDetail(deviceId);
}
function wxReadInput(id){
const el = document.getElementById(id);
return el ? String(el.value || '').trim() : '';
}
function renderUnifiedControl(){
const allDevices = state.devices || [];
const did = state.currentDevice || (allDevices[0]?.device_id);
if (did && !state.currentDevice) state.currentDevice = did;
const current = allDevices.find(d=>d.device_id===did) || allDevices[0];
const keyword = (state.wxKeyword || '').toLowerCase();
const filter = state.wxStatusFilter || 'all';
const filtered = allDevices.filter(d => {
const src = [d.device_id,d.model,d.brand,d.nickname,d.profile_nickname].filter(Boolean).join(' ').toLowerCase();
if (keyword && !src.includes(keyword)) return false;
if (filter==='online' && d.status!=='online') return false;
if (filter==='adb' && d.status!=='adb' && d.adb_status!=='device') return false;
if (filter==='offline' && !['offline','unauthorized'].includes(d.status||d.adb_status||'offline')) return false;
return true;
});
const onlineCount = allDevices.filter(d=>['online','adb'].includes(d.status||d.adb_status)).length;
const wxRunning = allDevices.filter(d=>d.wechat_running||d.current_app==='com.tencent.mm').length;
const tone = wxStatusTone(current);
const hp = wxHealthScore(current);
const hpColor = hp>=80?'var(--green)':hp>=50?'var(--orange)':'var(--red)';
const hasDevice = !!current && current.controllable !== false;
const unauth = allDevices.filter(d=>d.status==='unauthorized'||d.adb_status==='unauthorized');
const appTab = state.appFocus || 'wechat';
const logs = state.logs.map(item => `<div class="${item.type==='error'?'log-error':'log-info'}">[${item.ts}] ${esc(item.message)}</div>`).join('');
const executionLogHtml = _wxLogs.slice(-15).reverse().map(l=>
`<div style="padding:3px 0;font-size:11px;border-bottom:1px solid var(--border)"><span style="color:var(--blue)">${l.ts}</span> <b>${esc(l.action)}</b> <span style="color:${l.ok?'var(--green)':'var(--red)'};">${l.ok?'\u2713':'\u2717'}</span> <span style="color:var(--sub)">${l.ch} ${l.ms}ms</span></div>`
).join('');
const deviceCards = filtered.length ? filtered.map(device => {
const st = wxStatusTone(device);
const active = device.device_id === did;
const name = device.profile_nickname || device.nickname || device.model || device.device_id;
const wxId = device.wechat_id || device.wx_id || '';
const wxRunningNow = device.wechat_running || device.current_app === 'com.tencent.mm';
const avatar = name ? name[0] : '\ud83d\udcf1';
return `<div onclick="selectDevice('${device.device_id}')" style="display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:12px;border:1.5px solid ${active?'rgba(37,99,235,.4)':'var(--border)'};background:${active?'rgba(37,99,235,.04)':'rgba(255,255,255,.85)'};cursor:pointer;margin-bottom:6px;transition:all .2s">
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,${st.color}22,${st.color}08);display:flex;align-items:center;justify-content:center;font-size:15px;font-weight:700;color:${st.color};flex-shrink:0">${esc(avatar)}</div>
<div style="flex:1;min-width:0">
<div style="display:flex;justify-content:space-between;align-items:center">
<span style="font-size:12px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(name)}</span>
<span style="font-size:9px;font-weight:700;color:#fff;padding:2px 7px;border-radius:20px;background:${st.color};flex-shrink:0">${st.label}</span>
</div>
<div style="font-size:10px;color:var(--sub);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(device.device_id)}</div>
<div style="display:flex;gap:8px;margin-top:2px;font-size:10px;color:var(--muted)">
<span>微信: ${esc(wxId || (wxRunningNow ? '运行中' : '-'))}</span>
${device.totalFriend ? '<span>好友: '+device.totalFriend+'</span>' : ''}
</div>
</div>
</div>`;
}).join('') : renderConnectGuide();
const filterBtns = ['all','online','adb','offline'].map(f =>
`<button class="btn ${state.wxStatusFilter===f?'primary':'soft'}" style="font-size:10px;padding:4px 10px" onclick="state.wxStatusFilter='${f}';state.wxKeyword='';renderCurrentView(false)">${{all:'全部 '+allDevices.length,online:'在线',adb:'ADB',offline:'离线'}[f]}</button>`
).join('');
const deviceInfoCard = current ? `
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
<div style="width:46px;height:46px;border-radius:13px;background:linear-gradient(135deg,${tone.color},${tone.color}88);display:flex;align-items:center;justify-content:center;font-size:21px;color:#fff">\ud83d\udcf1</div>
<div style="flex:1">
<div style="font-size:15px;font-weight:700">${esc(current.profile_nickname||current.nickname||current.model||current.device_id)}</div>
<div style="font-size:11px;color:var(--sub);margin-top:2px">${esc([current.brand,current.model].filter(Boolean).join(' ')||'')} · ${esc(current.device_id)}</div>
</div>
<div style="text-align:right">
<span style="font-size:10px;font-weight:700;color:#fff;padding:3px 10px;border-radius:20px;background:${tone.color}">${tone.label}</span>
<div style="font-size:11px;color:var(--sub);margin-top:4px">健康 <b style="color:${hpColor}">${hp}%</b></div>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;font-size:11px">
<div style="padding:6px 8px;background:rgba(37,99,235,.04);border-radius:8px;text-align:center"><span style="color:var(--sub);display:block;font-size:9px">Android</span><b>${esc(current.android_version||current.sdk_version||'-')}</b></div>
<div style="padding:6px 8px;background:rgba(14,165,166,.04);border-radius:8px;text-align:center"><span style="color:var(--sub);display:block;font-size:9px">当前APP</span><b>${esc(current.current_app||(current.wechat_running?'微信':'-'))}</b></div>
<div style="padding:6px 8px;background:rgba(22,163,74,.04);border-radius:8px;text-align:center"><span style="color:var(--sub);display:block;font-size:9px">电量</span><b>${current.battery_level>=0?current.battery_level+'%'+(current.battery_charging?' ⚡':''):'-'}</b></div>
<div style="padding:6px 8px;background:rgba(124,58,237,.04);border-radius:8px;text-align:center"><span style="color:var(--sub);display:block;font-size:9px">内存</span><b>${current.memory_usage_pct>0?current.memory_usage_pct.toFixed(1)+'%':'-'}</b></div>
</div>
${(current.connection_modes||[]).length ? '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:8px">'+(current.connection_modes||[]).map(m=>'<span class="mode-badge '+(m.available?(current.best_mode?.id===m.id?'best':'available'):'unavailable')+'"><span class="badge-dot"></span>'+esc(m.name||m.id)+'</span>').join('')+'</div>' : ''}
` : '<div style="text-align:center;padding:30px;color:var(--muted)">请连接设备或点击左侧选择</div>';
const deviceActions = `
<div style="margin-top:12px">
<div style="font-size:12px;font-weight:700;color:var(--sub);margin-bottom:8px">\u26a1 设备操作</div>
<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:6px;${!hasDevice?'opacity:.4;pointer-events:none':''}">
${[
['\ud83d\udcf8','截屏',"doAction('screenshot')"],
['\ud83c\udfe0','主页',"doAction('home')"],
['\u2b05\ufe0f','返回',"doAction('back')"],
['\ud83c\udf33','UI树',"doAction('ui_tree')"],
['\u2139\ufe0f','信息',"doAction('device_info')"],
].map(([icon,label,action])=>`<button onclick="${action}" style="display:flex;flex-direction:column;align-items:center;gap:3px;padding:8px 4px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.9);cursor:pointer;font-size:10px;color:var(--text);transition:all .15s" onmouseover="this.style.background='rgba(37,99,235,.06)'" onmouseout="this.style.background='rgba(255,255,255,.9)'"><span style="font-size:16px">${icon}</span>${label}</button>`).join('')}
</div>
</div>`;
const appSelector = `
<div style="margin-top:16px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<div style="font-size:12px;font-weight:700;color:var(--sub)">\ud83d\udcf2 应用控制</div>
</div>
<div style="display:flex;gap:4px;margin-bottom:10px">
${[['wechat','\ud83d\udcac','微信'],['soul','\ud83d\udc9c','Soul'],['xhs','\ud83d\udcd5','小红书'],['douyin','\ud83c\udfb5','抖音'],['xianyu','\ud83d\udc1f','闲鱼']].map(([id,icon,name])=>
`<button class="btn ${appTab===id?'primary':'soft'}" style="font-size:10px;padding:5px 10px" onclick="state.appFocus='${id}';renderCurrentView(false)">${icon} ${name}</button>`
).join('')}
</div>
<div style="${!hasDevice?'opacity:.4;pointer-events:none':''}">
${appTab==='wechat' ? `
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px">
${[
['\ud83d\udcf2','开微信',"doAction('open_wechat')"],
['\ud83d\udc64','资料',"wxQuickRun('get_profile')"],
['\ud83d\udc65','联系人',"wxQuickRun('get_contacts',{limit:100})"],
['\ud83c\udff7\ufe0f','标签',"wxQuickRun('get_tags')"],
['\u7fa4','群列表',"wxQuickRun('get_groups',{limit:100})"],
['\ud83d\udd14','未读',"wxQuickRun('get_unread_messages')"],
['\ud83d\udd0d','搜索',"wxQuickRun('wechat_search',{keyword:prompt('\u641c\u7d22\u5173\u952e\u8bcd')||''})"],
['\ud83d\udcf8','截屏',"doAction('screenshot')"],
].map(([icon,label,action])=>`<button onclick="${action}" style="display:flex;flex-direction:column;align-items:center;gap:3px;padding:8px 4px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.9);cursor:pointer;font-size:10px;color:var(--text);transition:all .15s" onmouseover="this.style.background='rgba(37,99,235,.06)'" onmouseout="this.style.background='rgba(255,255,255,.9)'"><span style="font-size:16px">${icon}</span>${label}</button>`).join('')}
</div>
` : `
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px">
${[
[appTab==='soul'?'\ud83d\udc9c':appTab==='xhs'?'\ud83d\udcd5':appTab==='douyin'?'\ud83c\udfb5':'\ud83d\udc1f','打开',"doAction('open_"+appTab+"')"],
['\ud83d\udcf8','截屏',"doAction('screenshot')"],
['\ud83c\udfe0','主页',"doAction('home')"],
['\u2b05\ufe0f','返回',"doAction('back')"],
].map(([icon,label,action])=>`<button onclick="${action}" style="display:flex;flex-direction:column;align-items:center;gap:3px;padding:8px 4px;border-radius:10px;border:1px solid var(--border);background:rgba(255,255,255,.9);cursor:pointer;font-size:10px;color:var(--text);transition:all .15s" onmouseover="this.style.background='rgba(37,99,235,.06)'" onmouseout="this.style.background='rgba(255,255,255,.9)'"><span style="font-size:16px">${icon}</span>${label}</button>`).join('')}
</div>
<div style="margin-top:10px;padding:10px;background:rgba(245,158,11,.04);border-radius:10px;border:1px solid rgba(245,158,11,.1);font-size:11px;color:var(--sub)">
\ud83d\udea7 ${{soul:'Soul',xhs:'\u5c0f\u7ea2\u4e66',douyin:'\u6296\u97f3',xianyu:'\u95f2\u9c7c'}[appTab]||appTab} \u81ea\u52a8\u5316\u6b63\u5728\u5f00\u53d1\u4e2d\uff0c\u5f53\u524d\u53ef\u901a\u8fc7\u8bbe\u5907\u64cd\u4f5c\u624b\u52a8\u63a7\u5236
</div>
`}
</div>
</div>`;
const msgSection = appTab==='wechat' ? `
<div style="margin-top:14px">
<div style="font-size:12px;font-weight:700;color:var(--sub);margin-bottom:8px">\ud83d\udcac \u5feb\u6377\u6d88\u606f</div>
<div style="display:flex;gap:6px;margin-bottom:6px">
<input id="wx_quick_to" placeholder="\u53d1\u9001\u7ed9(\u6635\u79f0/\u7fa4\u540d)" style="flex:1;padding:7px 10px;border-radius:10px;border:1px solid var(--border);font-size:11px;background:rgba(255,255,255,.92)">
</div>
<div style="display:flex;gap:6px">
<textarea id="wx_quick_msg" placeholder="\u6d88\u606f\u5185\u5bb9" style="flex:1;min-height:44px;padding:7px 10px;border-radius:10px;border:1px solid var(--border);font-size:11px;resize:vertical;background:rgba(255,255,255,.92)"></textarea>
</div>
<div style="display:flex;gap:6px;margin-top:6px">
<button class="btn primary" style="font-size:10px;padding:5px 12px" onclick="wxQuickRun('send_message',{to_id:'wx_quick_to',content:'wx_quick_msg',msg_type:'text'})">\u53d1\u9001</button>
<button class="btn soft" style="font-size:10px;padding:5px 12px" onclick="wxQuickRun('mass_send',{content:'wx_quick_msg'})">\u6279\u91cf\u53d1</button>
<button class="btn soft" style="font-size:10px;padding:5px 12px" onclick="wxQuickRun('add_friend',{user_id:wxReadInput('wx_quick_to'),message:wxReadInput('wx_quick_msg')})">\u52a0\u597d\u53cb</button>
</div>
</div>` : '';
const aiSection = `
<div style="margin-top:14px">
<div style="font-size:12px;font-weight:700;color:var(--sub);margin-bottom:8px">\ud83e\udd16 AI \u6307\u4ee4</div>
<div style="display:flex;gap:4px;margin-bottom:6px">
${['pattern','ai','ai-only'].map(m=>`<button class="btn ${state.cmdMode===m?'primary':'soft'}" style="font-size:10px;padding:4px 10px" onclick="state.cmdMode='${m}';renderCurrentView(false)">${{pattern:'\u6a21\u5f0f\u5339\u914d',ai:'AI \u6267\u884c','ai-only':'AI \u89e3\u6790'}[m]}</button>`).join('')}
</div>
<div class="command-area" style="display:flex;gap:6px">
<input id="cmd-input" placeholder="${esc(cmdPlaceholder())}" style="flex:1;padding:8px 10px;border-radius:10px;border:1px solid var(--border);font-size:12px;background:rgba(255,255,255,.92)" onkeydown="if(event.key==='Enter')sendCmd()">
<button class="btn primary" style="font-size:11px;padding:6px 14px" onclick="sendCmd()">\u6267\u884c</button>
</div>
</div>`;
return `
${unauth.length ? `
<div style="background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);border-radius:14px;padding:12px 16px;margin-top:14px;display:flex;align-items:center;gap:12px">
<span style="font-size:20px">\u26a0\ufe0f</span>
<div><div style="font-weight:600;font-size:12px">${unauth.length} \u53f0\u8bbe\u5907\u5f85 USB \u6388\u6743</div><div style="color:var(--sub);font-size:11px;margin-top:2px">\u5728\u624b\u673a\u5c4f\u5e55\u70b9\u51fb\u300c\u5141\u8bb8 USB \u8c03\u8bd5\u300d\u2192 \u52fe\u9009\u300c\u59cb\u7ec8\u5141\u8bb8\u300d\u2192\u300c\u786e\u5b9a\u300d</div></div>
</div>` : ''}
<div style="display:grid;grid-template-columns:260px minmax(0,1fr) 260px;gap:14px;margin-top:14px">
<div class="card" style="display:flex;flex-direction:column;padding:14px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span style="font-size:13px;font-weight:700">\u8bbe\u5907</span>
<span style="font-size:10px;color:var(--muted)">${filtered.length} / ${allDevices.length}</span>
</div>
<input placeholder="\u641c\u7d22\u8bbe\u5907\u2026" style="width:100%;padding:6px 10px;border-radius:10px;border:1px solid var(--border);font-size:11px;background:rgba(255,255,255,.92);margin-bottom:6px" oninput="state.wxKeyword=this.value.toLowerCase();renderCurrentView(false)">
<div style="display:flex;gap:3px;margin-bottom:8px">${filterBtns}</div>
<div style="flex:1;overflow-y:auto;max-height:720px">${deviceCards}</div>
</div>
<div style="display:flex;flex-direction:column;gap:12px">
<div class="card" style="padding:14px">${deviceInfoCard}${deviceActions}</div>
<div class="card" style="padding:14px">${appSelector}${msgSection}${aiSection}</div>
</div>
<div style="display:flex;flex-direction:column;gap:10px">
<div class="card" style="padding:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div style="font-size:12px;font-weight:700">\ud83d\udcf8 \u8bbe\u5907\u622a\u5c4f</div>
<div style="display:flex;gap:4px;align-items:center">
<button id="shot-auto-btn" onclick="toggleShotAutoRefresh()" style="padding:3px 8px;border-radius:6px;border:1px solid var(--border);background:${state.shotAutoRefresh?'rgba(22,163,74,.1)':'rgba(15,23,42,.04)'};color:${state.shotAutoRefresh?'var(--green)':'var(--sub)'};font-size:10px;cursor:pointer;font-weight:600">${state.shotAutoRefresh?'\u23f8 \u505c\u6b62\u5237\u65b0':'\u25b6 \u81ea\u52a8\u5237\u65b0'}</button>
<select onchange="setShotInterval(+this.value)" style="padding:2px 4px;border-radius:4px;border:1px solid var(--border);font-size:10px;background:#fff;color:var(--sub)">
<option value="3" ${_shotInterval===3?'selected':''}>3s</option>
<option value="5" ${_shotInterval===5?'selected':''}>5s</option>
<option value="10" ${_shotInterval===10?'selected':''}>10s</option>
<option value="30" ${_shotInterval===30?'selected':''}>30s</option>
</select>
<button onclick="doAction('screenshot')" style="padding:3px 8px;border-radius:6px;border:1px solid var(--border);background:rgba(37,99,235,.06);color:var(--blue);font-size:10px;cursor:pointer;font-weight:600">\ud83d\udcf7 \u7acb\u5373\u622a\u56fe</button>
</div>
</div>
<div class="shot" id="shot-box" style="max-height:320px"><div class="placeholder">\u70b9\u51fb\u201c\u622a\u5c4f\u201d\u6216\u5f00\u542f\u81ea\u52a8\u5237\u65b0</div></div>
<div id="shot-ts" style="text-align:center;font-size:10px;color:var(--muted);margin-top:4px"></div>
</div>
<div class="card" style="padding:12px">
<div style="font-size:12px;font-weight:700;margin-bottom:6px">\u6267\u884c\u7edf\u8ba1</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
<div style="text-align:center;padding:6px;background:rgba(37,99,235,.04);border-radius:8px"><div style="font-size:16px;font-weight:800;color:var(--blue)" id="wx-total">${_wxStats.total}</div><div style="font-size:9px;color:var(--sub)">\u603b\u64cd\u4f5c</div></div>
<div style="text-align:center;padding:6px;background:rgba(22,163,74,.04);border-radius:8px"><div style="font-size:16px;font-weight:800;color:var(--green)" id="wx-rate">${_wxStats.total?Math.round(_wxStats.success/_wxStats.total*100)+'%':'-'}</div><div style="font-size:9px;color:var(--sub)">\u6210\u529f\u7387</div></div>
</div>
</div>
<div class="card" style="padding:12px">
<div style="font-size:12px;font-weight:700;margin-bottom:6px">\u6267\u884c\u7ed3\u679c</div>
<div class="log-box" id="wx-result" style="min-height:50px;max-height:100px;font-size:11px">\u7b49\u5f85\u64cd\u4f5c...</div>
</div>
<div class="card" style="padding:12px">
<div style="font-size:12px;font-weight:700;margin-bottom:6px">\u64cd\u4f5c\u65e5\u5fd7</div>
<div id="wx-logs" style="max-height:160px;overflow-y:auto">${executionLogHtml || '<div style="color:var(--muted);font-size:11px;padding:4px">\u6682\u65e0\u8bb0\u5f55</div>'}</div>
</div>
<div class="card" style="padding:12px">
<div style="font-size:12px;font-weight:700;margin-bottom:6px">\u901a\u9053\u6027\u80fd</div>
<div style="font-size:11px;line-height:1.8">
<div style="display:flex;justify-content:space-between"><span style="color:var(--purple);font-weight:600">Hook</span><span style="color:var(--sub)">50-200ms</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:var(--blue);font-weight:600">u2 UI</span><span style="color:var(--sub)">2-15s</span></div>
<div style="display:flex;justify-content:space-between"><span style="color:var(--orange);font-weight:600">ADB</span><span style="color:var(--sub)">1-10s</span></div>
</div>
</div>
</div>
</div>
${renderBindSection()}
`;
}
const WX_ACTION_ROUTES = {
send_message:{m:'POST',p:'/message/send'},get_messages:{m:'POST',p:'/message/list'},
batch_send_message:{m:'POST',p:'/message/batch-send'},forward_message:{m:'POST',p:'/message/forward'},
recall_message:{m:'POST',p:'/message/recall'},send_card:{m:'POST',p:'/message/send-card'},
add_friend:{m:'POST',p:'/friend/add'},accept_friend:{m:'POST',p:'/friend/accept'},
set_remark:{m:'POST',p:'/friend/set-remark'},delete_friend:{m:'POST',p:'/friend/delete'},
get_contacts:{m:'GET',p:'/contacts'},search_contact:{m:'GET',p:'/contacts/search'},
get_friend_info:{m:'GET',p:'/contacts/search'},batch_add_friend:{m:'POST',p:'/friend/batch-add'},
create_group:{m:'POST',p:'/group/create'},invite_to_group:{m:'POST',p:'/group/invite'},
remove_from_group:{m:'POST',p:'/group/remove'},set_group_notice:{m:'POST',p:'/group/set-notice'},
set_group_name:{m:'POST',p:'/group/set-name'},send_group_message:{m:'POST',p:'/group/send-message'},
set_group_welcome:{m:'POST',p:'/group/set-welcome'},get_groups:{m:'GET',p:'/group/list'},
get_group_members:{m:'GET',p:'/group/members'},quit_group:{m:'POST',p:'/group/quit'},
create_tag:{m:'POST',p:'/tag/create'},delete_tag:{m:'POST',p:'/tag/delete'},
get_tags:{m:'GET',p:'/tag/list'},add_tag:{m:'POST',p:'/tag/add'},
remove_tag:{m:'POST',p:'/tag/remove'},get_users_by_tag:{m:'POST',p:'/tag/users'},
post_moments:{m:'POST',p:'/moments/post'},like_moments:{m:'POST',p:'/moments/like'},
comment_moments:{m:'POST',p:'/moments/comment'},get_moments:{m:'POST',p:'/moments/list'},
delete_moments:{m:'POST',p:'/moments/delete'},set_moments_cover:{m:'POST',p:'/moments/set-cover'},
set_moments_privacy:{m:'POST',p:'/moments/set-privacy'},forward_moments_link:{m:'POST',p:'/moments/share-link'},
get_profile:{m:'GET',p:'/profile/get'},set_nickname:{m:'POST',p:'/profile/set-nickname'},
set_signature:{m:'POST',p:'/profile/set-signature'},set_avatar:{m:'POST',p:'/profile/set-avatar'},
set_gender:{m:'POST',p:'/profile/set-gender'},set_region:{m:'POST',p:'/profile/set-region'},
check_account_status:{m:'GET',p:'/account/status'},safety_center:{m:'GET',p:'/account/safety-center'},
unblock_account:{m:'POST',p:'/account/unblock'},unblock_self:{m:'POST',p:'/account/unblock'},
unblock_appeal:{m:'POST',p:'/account/unblock'},unblock_with_sms:{m:'POST',p:'/account/unblock'},
change_password:{m:'POST',p:'/account/change-password'},check_restrictions:{m:'GET',p:'/account/status'},
appeal_restriction:{m:'POST',p:'/account/unblock'},
send_red_packet:{m:'POST',p:'/message/send'},transfer:{m:'POST',p:'/message/send'},
show_payment_code:{m:'GET',p:'/profile/get'},receive_payment:{m:'GET',p:'/profile/get'},
view_wallet:{m:'GET',p:'/profile/get'},view_transactions:{m:'GET',p:'/profile/get'},
receive_red_packet:{m:'POST',p:'/message/send'},
set_chat_top:{m:'POST',p:'/chat/set-top'},set_mute_chat:{m:'POST',p:'/chat/set-mute'},
clear_chat_history:{m:'POST',p:'/chat/clear-history'},
add_to_favorites:{m:'POST',p:'/favorites/add'},get_favorites:{m:'GET',p:'/favorites/list'},
open_mini_program:{m:'POST',p:'/message/send'},follow_official_account:{m:'POST',p:'/friend/add'},
open_video_channel:{m:'GET',p:'/profile/get'},like_video:{m:'POST',p:'/moments/like'},
comment_video:{m:'POST',p:'/moments/comment'},follow_video_creator:{m:'POST',p:'/friend/add'},
share_video:{m:'POST',p:'/message/forward'},get_video_list:{m:'GET',p:'/profile/get'},
scan_qr_code:{m:'POST',p:'/message/send'},scan_add_friend:{m:'POST',p:'/friend/add'},
show_my_qr:{m:'GET',p:'/profile/get'},extract_qr_from_image:{m:'POST',p:'/message/send'},
voice_call:{m:'POST',p:'/message/send'},video_call:{m:'POST',p:'/message/send'},
mass_send:{m:'POST',p:'/message/batch-send'},wechat_search:{m:'GET',p:'/search/wechat'},
top_stories:{m:'GET',p:'/profile/get'},get_steps:{m:'GET',p:'/profile/get'},
like_steps:{m:'POST',p:'/moments/like'},send_location:{m:'POST',p:'/message/send'},
share_real_time_location:{m:'POST',p:'/message/send'},send_emoji:{m:'POST',p:'/message/send'},
get_sticker_list:{m:'GET',p:'/profile/get'},send_voice_message:{m:'POST',p:'/message/send'},
send_file_from_chat:{m:'POST',p:'/message/send'},toggle_do_not_disturb:{m:'POST',p:'/chat/set-mute'},
clear_cache:{m:'GET',p:'/profile/get'},logout:{m:'POST',p:'/account/unblock'},
switch_account:{m:'POST',p:'/account/unblock'},
};
async function wxCall(action, params, actMeta){
const route = WX_ACTION_ROUTES[action] || {m:'POST',p:'/message/send'};
const act = actMeta || Object.values(WX_MODULES).flatMap(item => item.actions).find(item => item.a===action) || {n: action};
const finalParams = {platform:'wechat',device_id: state.currentDevice || '', ...(params || {})};
if(finalParams.to_ids && typeof finalParams.to_ids === 'string') finalParams.to_ids = finalParams.to_ids.split(',').map(s=>s.trim()).filter(Boolean);
if(finalParams.member_ids && typeof finalParams.member_ids === 'string') finalParams.member_ids = finalParams.member_ids.split(',').map(s=>s.trim()).filter(Boolean);
if(finalParams.user_ids && typeof finalParams.user_ids === 'string') finalParams.user_ids = finalParams.user_ids.split(',').map(s=>s.trim()).filter(Boolean);
if(finalParams.tags && typeof finalParams.tags === 'string') finalParams.tags = finalParams.tags.split(',').map(s=>s.trim()).filter(Boolean);
['limit','post_index','index','duration','days','interval'].forEach(k=>{if(finalParams[k]!==undefined && finalParams[k]!=='') finalParams[k]=Number(finalParams[k])});
finalParams.action = action;
const chainEls = ['wxc1','wxc2','wxc3','wxc4'];
chainEls.forEach(id=>{const e=document.getElementById(id);if(e)e.textContent='🟡 '+e.textContent.substring(2)});
const start = Date.now();
try{
let data;
if(route.m==='GET'){
const qs=Object.entries(finalParams).filter(([k,v])=>v!==undefined && v!==null && v!=='').map(([k,v])=>`${k}=${encodeURIComponent(Array.isArray(v) ? v.join(',') : v)}`).join('&');
data = await apiGet(`/api/v3${route.p}?${qs}`);
} else {
data = await apiPost(`/api/v3${route.p}`, finalParams);
}
const ms = Date.now() - start;
const ch = data.channel_used || data.data?.channel || 'sdk_control';
const ok = (data.code===200||data.code===undefined) && data.data?.success!==false;
_wxStats.total++; if(ok)_wxStats.success++; _wxStats.totalTime+=ms;
_wxLogs.push({ts:new Date().toLocaleTimeString(),action:act.n,ok,ch:String(ch),ms});
chainEls.forEach(id=>{const e=document.getElementById(id);if(e)e.textContent=(ok?'🟢 ':'🔴 ')+e.textContent.substring(2)});
const res=document.getElementById('wx-result');if(res)res.textContent=`// ${ch} | ${ms}ms\n`+JSON.stringify(data,null,2);
const total=document.getElementById('wx-total');if(total)total.textContent=_wxStats.total;
const rate=document.getElementById('wx-rate');if(rate)rate.textContent=Math.round(_wxStats.success/_wxStats.total*100)+'%';
const avg=document.getElementById('wx-avg');if(avg)avg.textContent=Math.round(_wxStats.totalTime/_wxStats.total)+'ms';
const chEl=document.getElementById('wx-ch');if(chEl)chEl.textContent=ch;
const logBox=document.getElementById('wx-logs');
if(logBox){
const entry=`<div style="padding:3px 0;font-size:11px;border-bottom:1px solid var(--border)"><span style="color:var(--blue)">${new Date().toLocaleTimeString()}</span> <span style="font-weight:600">${esc(act.n)}</span> <span style="color:${ok?'var(--green)':'var(--red)'};">${ok?'✓':'✗'}</span> <span style="color:var(--sub)">${ch} ${ms}ms</span></div>`;
logBox.innerHTML=entry+logBox.innerHTML;
}
log(`微信 ${act.n}: ${ok?'成功':'失败'} (${ch}, ${ms}ms)`, ok?'info':'error');
}catch(e){
const ms=Date.now()-start;
_wxStats.total++; _wxStats.totalTime+=ms;
_wxLogs.push({ts:new Date().toLocaleTimeString(),action:act.n,ok:false,ch:'error',ms});
chainEls.forEach(id=>{const e2=document.getElementById(id);if(e2)e2.textContent='🔴 '+e2.textContent.substring(2)});
const res=document.getElementById('wx-result');if(res)res.textContent='// 错误\n'+e.message;
log(`微信 ${act.n}: 失败 — ${e.message}`,'error');
}
}
async function wxExec(action){
const m = WX_MODULES[_wxModule];
const act = m?.actions.find(a=>a.a===action);
if(!act) return;
const params = {};
act.f.forEach(f=>{
const el = document.getElementById(`wx_${action}_${f.k}`);
if(el) params[f.k] = el.value || f.v || '';
});
return wxCall(action, params, act);
}
async function wxQuickRun(action, fieldMap){
const params = {};
Object.entries(fieldMap || {}).forEach(([key, source]) => {
if (typeof source === 'function') {
params[key] = source();
return;
}
if (typeof source === 'string' && document.getElementById(source)) {
params[key] = wxReadInput(source);
return;
}
params[key] = source;
});
return wxCall(action, params);
}
/* ═══ 2. Devices Panel (设备控制 / 接口文档 / 系统说明) ═══ */
function renderDevicePanel(){
const tab = state.deviceTab || 'control';
const devices = state.devices || [];
const wsCount = devices.filter(d=>d.status==='online').length;
const adbCount = devices.filter(d=>d.status==='adb'||d.adb_status==='device').length;
const onlineCount = wsCount + adbCount;
const unauth = devices.filter(d=>d.status==='unauthorized'||d.adb_status==='unauthorized');
const header = `
<div class="panel glass">
<div class="panel-head">
<div>
<h2>📱 设备管理</h2>
<p class="sub">${devices.length} 台设备 · ${onlineCount} 在线 · 统一管控中心</p>
</div>
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<span class="mode-badge ${wsCount>0?'available':'unavailable'}" title="WebSocket"><span class="badge-dot"></span>WS ${wsCount}</span>
<span class="mode-badge ${adbCount>0?'available':'unavailable'}" title="ADB"><span class="badge-dot"></span>ADB ${adbCount}</span>
${unauth.length?`<span class="mode-badge unavailable"><span class="badge-dot"></span>⚠ ${unauth.length} 待授权</span>`:''}
<button class="btn soft" style="font-size:11px" onclick="refreshDevices()">🔄 刷新</button>
<button class="btn primary" style="font-size:11px" onclick="adbRescan()">📡 扫描</button>
</div>
</div>
<div class="mode-tabs">
<div class="mode-btn ${tab==='control'?'active':''}" onclick="state.deviceTab='control';renderCurrentView()">🎮 设备控制</div>
<div class="mode-btn ${tab==='api-docs'?'active':''}" onclick="state.deviceTab='api-docs';renderCurrentView()">📡 接口文档 (150+)</div>
<div class="mode-btn ${tab==='system'?'active':''}" onclick="state.deviceTab='system';renderCurrentView()">📊 系统说明</div>
</div>
</div>`;
let content = '';
if (tab === 'control') content = renderUnifiedControl();
else if (tab === 'api-docs') content = renderApiDocsTab();
else if (tab === 'system') content = renderSystemFlowTab();
return header + content;
}
function renderApiDocsTab(){
const API_CATEGORIES = [
{id:'device',title:'设备管理',icon:'📱',color:'var(--blue)',endpoints:[
['GET','/api/v3/devices','获取所有设备列表'],['GET','/api/v3/devices/{id}','获取设备详情'],
['POST','/api/v3/devices/{id}/screenshot','截图'],['POST','/api/v3/devices/{id}/click','点击坐标'],
['POST','/api/v3/devices/{id}/click-text','点击文字'],['POST','/api/v3/devices/{id}/input','输入文字'],
['POST','/api/v3/devices/{id}/swipe','滑动'],['GET','/api/v3/devices/{id}/ui-tree','获取UI树'],
['POST','/api/v3/devices/{id}/execute','执行脚本'],['GET','/api/v3/devices/{id}/heartbeat','心跳状态'],
['POST','/api/v3/devices/{id}/heartbeat/config','心跳配置'],['GET','/api/v3/devices/{id}/health','健康评分'],
['GET','/api/v3/devices/{id}/health/events','健康事件'],['GET','/api/v3/heartbeat/dashboard','心跳仪表盘'],
['GET','/api/v3/heartbeat/events','全局健康事件'],
]},
{id:'adb',title:'ADB 操作',icon:'🔌',color:'var(--teal)',endpoints:[
['GET','/api/v3/adb/devices','ADB设备列表'],['POST','/api/v3/adb/scan','扫描设备'],
['GET','/api/v3/adb/devices/{serial}','设备详情'],['POST','/api/v3/adb/devices/{serial}/screenshot','截图'],
['POST','/api/v3/adb/devices/{serial}/click','点击'],['POST','/api/v3/adb/devices/{serial}/input','输入'],
['POST','/api/v3/adb/devices/{serial}/swipe','滑动'],['POST','/api/v3/adb/devices/{serial}/key','按键'],
['POST','/api/v3/adb/devices/{serial}/app/start','启动APP'],['POST','/api/v3/adb/devices/{serial}/app/stop','停止APP'],
['GET','/api/v3/adb/devices/{serial}/app/current','当前APP'],['GET','/api/v3/adb/devices/{serial}/app/list','已安装APP'],
['GET','/api/v3/adb/devices/{serial}/ui-tree','UI树'],['POST','/api/v3/adb/devices/{serial}/click-text','点击文字'],
]},
{id:'wx-msg',title:'微信 · 消息',icon:'💬',color:'var(--green)',endpoints:[
['POST','/api/v3/message/send','发送消息'],['POST','/api/v3/message/list','消息列表'],
['POST','/api/v3/message/batch-send','批量发送'],['POST','/api/v3/message/forward','转发'],
['POST','/api/v3/message/recall','撤回'],['POST','/api/v3/message/send-card','发名片'],
['POST','/api/v3/comment/reply','回复评论'],
]},
{id:'wx-friend',title:'微信 · 好友',icon:'👥',color:'var(--blue)',endpoints:[
['POST','/api/v3/friend/add','添加好友'],['POST','/api/v3/friend/accept','通过请求'],
['POST','/api/v3/friend/set-remark','设置备注'],['POST','/api/v3/friend/delete','删除好友'],
['POST','/api/v3/friend/batch-add','批量加友'],['GET','/api/v3/contacts','联系人列表'],
['GET','/api/v3/contacts/search','搜索联系人'],['GET','/api/v3/friend/info','好友资料'],
]},
{id:'wx-group',title:'微信 · 群聊',icon:'👪',color:'var(--purple)',endpoints:[
['POST','/api/v3/group/create','创建群'],['POST','/api/v3/group/invite','邀请入群'],
['POST','/api/v3/group/remove','移出群'],['POST','/api/v3/group/set-notice','群公告'],
['POST','/api/v3/group/set-name','群名'],['POST','/api/v3/group/send-message','群消息'],
['POST','/api/v3/group/set-welcome','欢迎语'],['GET','/api/v3/group/list','群列表'],
['GET','/api/v3/group/members','群成员'],['POST','/api/v3/group/quit','退群'],
]},
{id:'wx-tag',title:'微信 · 标签',icon:'🏷️',color:'var(--orange)',endpoints:[
['POST','/api/v3/tag/create','创建标签'],['POST','/api/v3/tag/delete','删除标签'],
['GET','/api/v3/tag/list','标签列表'],['POST','/api/v3/tag/add','添加标签'],
['POST','/api/v3/tag/remove','移除标签'],['POST','/api/v3/tag/users','按标签查好友'],
]},
{id:'wx-moments',title:'微信 · 朋友圈',icon:'📸',color:'var(--teal)',endpoints:[
['POST','/api/v3/moments/post','发朋友圈'],['POST','/api/v3/moments/like','点赞'],
['POST','/api/v3/moments/comment','评论'],['POST','/api/v3/moments/list','朋友圈列表'],
['POST','/api/v3/moments/delete','删除'],
]},
{id:'wx-profile',title:'微信 · 个人设置',icon:'👤',color:'var(--blue)',endpoints:[
['GET','/api/v3/profile/get','获取资料'],['POST','/api/v3/profile/set-nickname','改昵称'],
['POST','/api/v3/profile/set-signature','改签名'],['POST','/api/v3/profile/set-avatar','改头像'],
['POST','/api/v3/profile/set-gender','设性别'],['POST','/api/v3/profile/set-region','设地区'],
]},
{id:'wx-account',title:'微信 · 账号安全',icon:'🔒',color:'var(--red)',endpoints:[
['GET','/api/v3/account/status','账号状态'],['POST','/api/v3/account/unblock','解封'],
['GET','/api/v3/account/safety-center','安全中心'],['POST','/api/v3/account/change-password','改密码'],
['GET','/api/v3/account/restrictions','功能限制'],['POST','/api/v3/account/appeal-restriction','申诉'],
]},
{id:'ai',title:'AI 引擎',icon:'🧠',color:'var(--purple)',endpoints:[
['POST','/api/v3/agent/execute','执行任务'],['GET','/api/v3/agent/status/{id}','Agent状态'],
['POST','/api/v3/agent/stop/{id}','停止任务'],['GET','/api/v3/ai/status','AI状态'],
['POST','/api/v3/ai/chat','AI对话控制'],['POST','/api/v3/v1/chat/completions','OpenAI兼容'],
['GET','/api/v3/devices/{id}/ai/status','AI Brain状态'],['POST','/api/v3/devices/{id}/ai/task','推送AI任务'],
['POST','/api/v3/devices/{id}/ai/standing-order','常驻指令'],['POST','/api/v3/devices/{id}/ai/execute','AI执行'],
['POST','/api/v3/devices/{id}/ai/queue-task','任务队列'],
]},
{id:'hook',title:'Hook 模块',icon:'🪝',color:'var(--orange)',endpoints:[
['GET','/api/v3/hook/actions','Hook操作清单'],['GET','/api/v3/hook/probe/{id}','探测Hook状态'],
['GET','/api/v3/hook/data/{id}','拉取Hook数据'],['POST','/api/v3/hook/execute','执行Hook操作'],
['GET','/api/v3/modules','模块列表'],['POST','/api/v3/modules','创建模块'],
['GET','/api/v3/devices/{id}/modules','设备模块'],['POST','/api/v3/devices/{id}/modules/reload','重载模块'],
['GET','/api/v3/hook/events','事件列表'],['POST','/api/v3/hook/events','写入事件'],
]},
{id:'gateway',title:'对接网关',icon:'🔗',color:'var(--green)',endpoints:[
['POST','/api/v3/gateway/v1/chat/completions','OpenAI Chat'],['GET','/api/v3/gateway/v1/models','模型列表'],
['GET','/api/v3/gateway/mcp/tools','MCP工具列表'],['POST','/api/v3/gateway/mcp/call','调用MCP工具'],
['GET','/api/v3/gateway/info','网关信息'],['GET','/api/v3/gateway/fleet/status','多设备状态'],
['POST','/api/v3/gateway/fleet/broadcast','广播命令'],['GET','/api/v3/gateway/journey/{id}','AI旅程'],
]},
{id:'conn',title:'连接协议',icon:'🔌',color:'var(--teal)',endpoints:[
['GET','/api/v3/connection/protocol','协议结构'],['GET','/api/v3/connection/status','连接状态'],
['GET','/api/v3/connection/modes/{id}','设备控制模式'],['GET','/api/v3/connection/modes','所有设备模式'],
['POST','/api/v3/connection/simulate/register','模拟注册'],['POST','/api/v3/connection/simulate/heartbeat','模拟心跳'],
]},
{id:'misc',title:'其他服务',icon:'⚡',color:'var(--sub)',endpoints:[
['POST','/api/v3/qrcode/generate','生成绑定二维码'],['GET','/api/v3/qrcode/image/{id}','二维码图片'],
['POST','/api/v3/voice/command','语音命令'],['GET','/api/v3/experience/statistics','经验库统计'],
['GET','/api/v3/experience/operations','操作记录'],['POST','/api/v3/capture/start','开始抓包'],
['POST','/api/v3/registry/nodes/register','节点注册'],['GET','/api/v3/registry/nodes','节点列表'],
]},
];
const totalEndpoints = API_CATEGORIES.reduce((s,c)=>s+c.endpoints.length,0);
const expandedCat = state._apiDocExpanded || '';
const mkEndpoint = (m,p,d) => `<div style="display:flex;align-items:center;gap:6px;padding:4px 0"><code style="background:rgba(${m==='GET'?'37,99,235':m==='POST'?'22,163,74':m==='DELETE'?'220,38,38':'245,158,11'},.08);padding:1px 6px;border-radius:4px;font-size:10px;min-width:40px;text-align:center;font-weight:600">${m}</code><span style="font-family:monospace;font-size:10.5px;color:var(--text);cursor:pointer" onclick="navigator.clipboard.writeText('${p}');this.style.color='var(--green)';setTimeout(()=>this.style.color='var(--text)',1000)" title="点击复制路径">${p}</span><span style="color:var(--muted);font-size:11px;margin-left:auto;white-space:nowrap">${d}</span></div>`;
return `
<div class="panel glass" style="margin-top:14px">
<div class="panel-head">
<div><h2>\ud83d\udce1 \u5168\u90e8 API \u63a5\u53e3\u6587\u6863</h2><p class="sub">${totalEndpoints} \u4e2a\u7aef\u70b9 \u00b7 ${API_CATEGORIES.length} \u4e2a\u5206\u7c7b \u00b7 \u70b9\u51fb\u8def\u5f84\u53ef\u590d\u5236</p></div>
<div class="btns"><a class="btn primary" href="/docs" target="_blank" style="text-decoration:none">\ud83d\udcc4 Swagger</a><a class="btn soft" href="/redoc" target="_blank" style="text-decoration:none">ReDoc</a></div>
</div>
<div style="display:grid;gap:10px">
${API_CATEGORIES.map(cat => {
const isOpen = expandedCat === cat.id;
return `<div class="card" style="padding:0;overflow:hidden">
<div style="display:flex;align-items:center;gap:10px;padding:14px 16px;cursor:pointer;transition:background .15s" onclick="state._apiDocExpanded=state._apiDocExpanded==='${cat.id}'?'':'${cat.id}';renderCurrentView(false)" onmouseover="this.style.background='rgba(37,99,235,.03)'" onmouseout="this.style.background='transparent'">
<span style="font-size:18px">${cat.icon}</span>
<div style="flex:1"><span style="font-size:13px;font-weight:700">${cat.title}</span><span style="font-size:11px;color:var(--muted);margin-left:8px">${cat.endpoints.length} \u4e2a\u7aef\u70b9</span></div>
<span style="font-size:14px;color:var(--muted);transition:transform .2s;transform:rotate(${isOpen?'180':'0'}deg)">\u25bc</span>
</div>
${isOpen ? '<div style="padding:4px 16px 14px;border-top:1px solid var(--border);font-size:11px;line-height:2">'+cat.endpoints.map(([m,p,d])=>mkEndpoint(m,p,d)).join('')+'</div>' : ''}
</div>`;
}).join('')}
</div>
</div>`;
}
function renderSystemFlowTab(){
const devices = state.devices || [];
const onlineCount = devices.filter(d=>['online','adb'].includes(d.status||d.adb_status)).length;
const flowStep = (step,icon,title,desc,color) => `<div style="flex:1;text-align:center;padding:16px 12px;border-radius:14px;border:1px solid var(--border);background:var(--panel-strong);position:relative"><div style="font-size:9px;font-weight:800;color:${color};letter-spacing:.08em;text-transform:uppercase">STEP ${step}</div><div style="font-size:24px;margin:6px 0">${icon}</div><div style="font-size:13px;font-weight:700">${title}</div><div style="font-size:11px;color:var(--sub);margin-top:4px;line-height:1.5">${desc}</div></div>`;
return `
<div class="panel glass" style="margin-top:14px">
<div class="panel-head"><div><h2>\ud83d\udcca \u7cfb\u7edf\u67b6\u6784\u603b\u89c8</h2><p class="sub">${devices.length} \u53f0\u8bbe\u5907 \u00b7 ${onlineCount} \u5728\u7ebf \u00b7 \u591a\u901a\u9053\u667a\u80fd\u63a7\u5236</p></div></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:16px">
${flowStep('1','\ud83d\udcbb','\u5ba2\u6237\u7aef','\u5b58\u5ba2\u5b9d/API/MCP','var(--blue)')}
<div style="font-size:22px;color:var(--muted);flex-shrink:0">\u2192</div>
${flowStep('2','\u2699\ufe0f','\u670d\u52a1\u7aef','FastAPI + unified.py','var(--teal)')}
<div style="font-size:22px;color:var(--muted);flex-shrink:0">\u2192</div>
${flowStep('3','\ud83d\udd00','\u901a\u9053\u8def\u7531','Hook > u2 > ADB > AI','var(--purple)')}
<div style="font-size:22px;color:var(--muted);flex-shrink:0">\u2192</div>
${flowStep('4','\ud83d\udcf1','\u8bbe\u5907\u6267\u884c','Agent / ADB / Frida','var(--green)')}
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>\ud83d\udd00 \u56db\u901a\u9053\u63a7\u5236\u94fe\u8def</h2><p class="sub">\u667a\u80fd\u901a\u9053\u9009\u62e9\uff1a\u4f18\u5148 Hook \u2192 \u964d\u7ea7 u2 \u2192 ADB \u2192 AI</p></div></div>
<div class="grid col4">
<div class="card" style="border-top:3px solid var(--purple)">
<h3 style="color:var(--purple)">\ud83e\ude9d \u901a\u9053 1\uff1aFrida Hook</h3>
<div style="font-size:11px;color:var(--sub);line-height:1.7;margin-top:6px">\u76f4\u63a5\u8c03\u7528 APP \u5185\u90e8\u51fd\u6570<br>\u53d1\u6d88\u606f/\u8bfb\u8054\u7cfb\u4eba/\u7fa4\u7ba1\u7406<br><b>\u5ef6\u8fdf\uff1a50-200ms</b><br>\u9690\u853d\u6027\u9ad8\uff0c\u4e0d\u89e6\u53d1 UI</div>
<div style="margin-top:8px"><span class="mode-badge best"><span class="badge-dot"></span>\u4f18\u5148</span></div>
</div>
<div class="card" style="border-top:3px solid var(--blue)">
<h3 style="color:var(--blue)">\ud83d\uddb1\ufe0f \u901a\u9053 2\uff1auiautomator2</h3>
<div style="font-size:11px;color:var(--sub);line-height:1.7;margin-top:6px">UI \u81ea\u52a8\u5316\u64cd\u4f5c<br>\u70b9\u51fb/\u6ed1\u52a8/\u8f93\u5165/\u622a\u56fe<br><b>\u5ef6\u8fdf\uff1a2-15s</b><br>Frida \u4e0d\u53ef\u7528\u65f6\u81ea\u52a8\u964d\u7ea7</div>
<div style="margin-top:8px"><span class="mode-badge available"><span class="badge-dot"></span>\u5146\u5e95</span></div>
</div>
<div class="card" style="border-top:3px solid var(--orange)">
<h3 style="color:var(--orange)">\ud83d\udcf1 \u901a\u9053 3\uff1aADB \u76f4\u63a7</h3>
<div style="font-size:11px;color:var(--sub);line-height:1.7;margin-top:6px">\u8bbe\u5907\u7ea7\u522b\u63a7\u5236<br>\u5b89\u88c5/\u542f\u52a8/\u622a\u56fe/\u6309\u952e<br><b>\u5ef6\u8fdf\uff1a1-10s</b><br>USB \u6216 WiFi \u8fde\u63a5</div>
<div style="margin-top:8px"><span class="mode-badge available"><span class="badge-dot"></span>\u57fa\u7840</span></div>
</div>
<div class="card" style="border-top:3px solid var(--green)">
<h3 style="color:var(--green)">\ud83e\udd16 \u901a\u9053 4\uff1aAI Agent</h3>
<div style="font-size:11px;color:var(--sub);line-height:1.7;margin-top:6px">\u81ea\u7136\u8bed\u8a00 \u2192 \u6b65\u9aa4\u62c6\u89e3 \u2192 \u6267\u884c<br>\u4f9d\u8d56\u901a\u9053 1 \u6216 2<br><b>\u5ef6\u8fdf\uff1a3-30s</b><br>\u667a\u80fd\u51b3\u7b56 + \u5f02\u5e38\u6062\u590d</div>
<div style="margin-top:8px"><span class="mode-badge available"><span class="badge-dot"></span>\u589e\u5f3a</span></div>
</div>
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>\ud83c\udf10 \u4e09\u79cd\u5bf9\u63a5\u65b9\u5f0f</h2><p class="sub">\u5916\u90e8\u7a0b\u5e8f\u901a\u8fc7\u4efb\u4e00\u65b9\u5f0f\u63a5\u5165 SDK\uff0c\u5b9e\u73b0\u591a\u53f0\u624b\u673a\u7edf\u4e00\u7ba1\u63a7</p></div></div>
<div class="grid col3">
<div class="card" style="border-top:3px solid var(--blue)">
<h3 style="color:var(--blue)">\ud83c\udf10 REST API</h3>
<div style="font-size:11px;color:var(--sub);line-height:1.7;margin-top:6px">150+ \u7aef\u70b9\uff0c\u8986\u76d6\u5fae\u4fe1\u5168\u529f\u80fd<br>\u8bbe\u5907\u63a7\u5236\u3001AI \u4efb\u52a1\u3001Hook \u7ba1\u7406</div>
<div class="path" style="margin-top:8px">Base: /api/v3</div>
<div style="margin-top:8px"><a class="btn soft" href="/docs" target="_blank" style="text-decoration:none;font-size:11px">\ud83d\udcc4 Swagger</a></div>
</div>
<div class="card" style="border-top:3px solid var(--green)">
<h3 style="color:var(--green)">\ud83e\udd16 OpenAI Compatible</h3>
<div style="font-size:11px;color:var(--sub);line-height:1.7;margin-top:6px">\u50cf\u8c03\u7528 GPT \u4e00\u6837\u63a7\u5236\u624b\u673a<br>\u652f\u6301\u6240\u6709 OpenAI SDK</div>
<div class="path" style="margin-top:8px">Base: /api/v3/gateway/v1</div>
</div>
<div class="card" style="border-top:3px solid var(--purple)">
<h3 style="color:var(--purple)">\ud83e\udde9 MCP Protocol</h3>
<div style="font-size:11px;color:var(--sub);line-height:1.7;margin-top:6px">8 \u4e2a\u5de5\u5177\uff0c\u53ef\u88ab Claude/Cursor \u8c03\u7528<br>AI Agent \u539f\u751f\u96c6\u6210</div>
<div class="path" style="margin-top:8px">Base: /api/v3/gateway/mcp</div>
</div>
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>\ud83d\udce1 \u6570\u636e\u6d41\u5411</h2><p class="sub">\u4ece\u5ba2\u6237\u7aef\u8bf7\u6c42\u5230\u8bbe\u5907\u6267\u884c\u7684\u5b8c\u6574\u94fe\u8def</p></div></div>
<div style="background:#0f172a;color:#e2e8f0;padding:20px;border-radius:14px;font-family:ui-monospace,monospace;font-size:12px;line-height:2;overflow-x:auto">
<div style="color:#7dd3fc">\u2588 \u5ba2\u6237\u7aef\u8bf7\u6c42\u6d41\u7a0b</div>
<div>\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510</div>
<div>\u2502 <span style="color:#fbbf24">REST API</span> \u2502 \u2192 \u2502 <span style="color:#34d399">unified.py</span> \u2502 \u2192 \u2502 <span style="color:#a78bfa">\u901a\u9053\u9009\u62e9</span> \u2502 \u2192 \u2502 <span style="color:#f472b6">\u8bbe\u5907\u6267\u884c</span> \u2502</div>
<div>\u2502 OpenAI API \u2502 \u2502 \u53c2\u6570\u89e3\u6790 \u2502 \u2502 Hook/u2/ADB \u2502 \u2502 Agent/Frida\u2502</div>
<div>\u2502 MCP Tool \u2502 \u2502 \u901a\u9053\u8def\u7531 \u2502 \u2502 \u667a\u80fd\u964d\u7ea7 \u2502 \u2502 \u7ed3\u679c\u56de\u4f20 \u2502</div>
<div>\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518</div>
<div style="margin-top:10px;color:#7dd3fc">\u2588 \u8bbe\u5907\u5fc3\u8df3\u5faa\u73af (AI Brain)</div>
<div>\u6bcf 60s \u2192 \u68c0\u67e5\u8bbe\u5907\u72b6\u6001 \u2192 \u6709\u5f85\u529e? \u2192 AI \u51b3\u7b56\u6267\u884c \u2192 \u7ed3\u679c\u4e0a\u62a5</div>
<div>\u670d\u52a1\u5668\u65ad\u8fde \u2192 AI Brain \u81ea\u4e3b\u8fd0\u884c \u2192 \u91cd\u8fde\u540e\u4e0a\u4f20\u79bb\u7ebf\u7f13\u51b2</div>
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>\ud83d\udee0\ufe0f \u6280\u672f\u6808</h2><p class="sub">\u670d\u52a1\u7aef\u3001\u8bbe\u5907\u7aef\u3001\u5bf9\u63a5\u5c42\u6280\u672f\u9009\u578b</p></div></div>
<div class="grid col3">
${card('\u670d\u52a1\u7aef','Python 3.11 + FastAPI + uvicorn<br>MongoDB + Redis<br>WebSocket Hub \u957f\u8fde\u63a5<br>Docker \u4e00\u952e\u90e8\u7f72')}
${card('\u8bbe\u5907\u7aef','Android Agent (Python/Termux)<br>Frida Hook \u6846\u67b6<br>uiautomator2 UI \u81ea\u52a8\u5316<br>AI Brain \u81ea\u4e3b\u51b3\u7b56')}
${card('\u5bf9\u63a5\u5c42','REST API 150+ \u7aef\u70b9<br>OpenAI Compatible \u7f51\u5173<br>MCP Protocol 8 \u5de5\u5177<br>PHP/TypeScript SDK')}
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>\ud83d\udcf1 \u652f\u6301\u5e73\u53f0</h2><p class="sub">\u5f53\u524d\u5df2\u652f\u6301\u548c\u8ba1\u5212\u652f\u6301\u7684\u5e94\u7528\u5e73\u53f0</p></div></div>
<div class="grid col2">
<div class="card" style="border-left:3px solid var(--green)">
<h3>\u2705 \u5df2\u652f\u6301</h3>
<div style="font-size:12px;color:var(--sub);line-height:2;margin-top:8px">
<div>\ud83d\udcac <b>\u5fae\u4fe1</b> \u2014 107 \u4e2a Hook \u64cd\u4f5c / 24 \u4e2a\u6a21\u5757 / \u5168\u529f\u80fd\u8986\u76d6</div>
</div>
</div>
<div class="card" style="border-left:3px solid var(--orange)">
<h3>\ud83d\udea7 \u5f00\u53d1\u4e2d</h3>
<div style="font-size:12px;color:var(--sub);line-height:2;margin-top:8px">
<div>\ud83d\udc9c <b>Soul</b> \u2014 \u6d3e\u5bf9/\u6d88\u606f/\u597d\u53cb\u81ea\u52a8\u5316</div>
<div>\ud83d\udcd5 <b>\u5c0f\u7ea2\u4e66</b> \u2014 \u7b14\u8bb0\u53d1\u5e03/\u4e92\u52a8/\u6d41\u91cf</div>
<div>\ud83c\udfb5 <b>\u6296\u97f3</b> \u2014 \u89c6\u9891\u53d1\u5e03/\u4e92\u52a8/\u7c89\u4e1d</div>
<div>\ud83d\udc1f <b>\u95f2\u9c7c</b> \u2014 \u5546\u54c1\u7ba1\u7406/\u6d88\u606f\u81ea\u52a8\u56de\u590d</div>
</div>
</div>
</div>
</div>
`;
}
function renderBindSection(){
const defaultServer = `ws://${location.hostname}:${location.port || 8899}/ws/device`;
return `
<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 style="margin-bottom:12px">绑定配置</h3>
<div style="display:flex;flex-direction:column;gap:8px">
<input id="bind-project" value="cunkebao" placeholder="项目 ID" style="width:100%;padding:9px 12px;border-radius:10px;border:1px solid var(--border);font-size:12px">
<input id="bind-name" value="AI数智员工" placeholder="项目名称" style="width:100%;padding:9px 12px;border-radius:10px;border:1px solid var(--border);font-size:12px">
<input id="bind-server" value="${defaultServer}" placeholder="服务器地址" style="width:100%;padding:9px 12px;border-radius:10px;border:1px solid var(--border);font-size:11px;font-family:monospace">
<button class="btn primary" onclick="generateBindQR()">生成绑定二维码</button>
</div>
</div>
<div class="card" style="display:flex;align-items:center;justify-content:center;min-height:240px">
<div id="bind-qr-area" style="text-align:center"><div style="font-size:36px;opacity:.3;margin-bottom:10px">📱</div><div style="color:var(--muted);font-size:12px">点击「生成绑定二维码」</div></div>
</div>
</div>
</div>
`;
}
function renderConnectGuide(){
return `
<div style="padding:20px 0">
<div style="text-align:center;font-size:32px;margin-bottom:12px">📡</div>
<div style="text-align:center;font-weight:600;margin-bottom:16px">暂无设备连接</div>
<div style="color:var(--sub);font-size:12px;line-height:2">
<div style="background:rgba(37,99,235,.06);border-radius:12px;padding:14px;margin-bottom:10px">
<strong style="color:var(--blue)">方式 1USB 连接</strong><br>
① 手机开启「开发者选项」→ 打开「USB 调试」<br>
② USB 线连接电脑 → 手机弹窗点「允许」<br>
③ 点击上方「📡 扫描 ADB」
</div>
<div style="background:rgba(14,165,166,.06);border-radius:12px;padding:14px;margin-bottom:10px">
<strong style="color:var(--teal)">方式 2WiFi ADB 连接</strong><br>
① 先 USB 连接一次 → 执行 <code>adb tcpip 5555</code><br>
② 拔掉 USB → <code>adb connect 手机IP:5555</code><br>
③ 之后手机只需同一 WiFi 即可控制
</div>
<div style="background:rgba(124,58,237,.06);border-radius:12px;padding:14px">
<strong style="color:var(--purple)">方式 3Agent WebSocket</strong><br>
① 在手机 Termux 中安装 Python Agent<br>
② 启动 Agent → 自动通过 WiFi 连接本服务器<br>
③ 无需 USB支持远程控制
</div>
</div>
</div>
`;
}
/* ═══ 3. Engine Panel (AI Brain + Hook + Events) ═══ */
function renderEnginePanel(){
const tab = state.engineTab;
const devices = state.devices || [];
const modules = state.hookModules || [];
const currentModules = state.currentDeviceModules?.modules || [];
const events = state.hookEvents || [];
const deviceOptions = devices.map(d =>
`<option value="${d.device_id}" ${d.device_id===state.currentDevice?'selected':''}>${d.device_id} (${d.status||'offline'})</option>`
).join('');
const header = `
<div class="panel glass">
<div class="panel-head">
<div><h2>🧠 智能引擎</h2><p class="sub">AI Brain 自主决策 · Hook 模块控制 · 事件流监控</p></div>
<div class="btns"><button class="btn soft" onclick="refreshHook()">刷新数据</button></div>
</div>
<div class="mode-tabs">
<div class="mode-btn ${tab==='brain'?'active':''}" onclick="state.engineTab='brain';renderCurrentView()">🧠 AI Brain</div>
<div class="mode-btn ${tab==='hook'?'active':''}" onclick="state.engineTab='hook';renderCurrentView()">🪝 Hook</div>
<div class="mode-btn ${tab==='hookdata'?'active':''}" onclick="state.engineTab='hookdata';renderCurrentView()">📊 Hook 数据</div>
<div class="mode-btn ${tab==='events'?'active':''}" onclick="state.engineTab='events';renderCurrentView()">📡 Events (${events.length})</div>
<div class="mode-btn ${tab==='connection'?'active':''}" onclick="state.engineTab='connection';refreshConnection();renderCurrentView()">🔌 连接监控</div>
<div class="mode-btn ${tab==='heartbeat'?'active':''}" onclick="state.engineTab='heartbeat';refreshHeartbeat();renderCurrentView()">💓 心跳</div>
</div>
</div>`;
let content = '';
if (tab === 'brain') {
const sr = state.skillRegistry || {};
const srSkills = sr.skills || {};
const srSummary = sr.summary || {};
const brainDash = state.brainDashboard || {};
const sub = state.brainSub || 'overview';
const sf = state.brainSkillFilter || 'all';
const subTabs = `
<div class="mode-tabs" style="margin-top:12px">
<div class="mode-btn ${sub==='overview'?'active':''}" onclick="state.brainSub='overview';renderCurrentView()">📊 总览</div>
<div class="mode-btn ${sub==='skills'?'active':''}" onclick="state.brainSub='skills';renderCurrentView()">📦 技能库</div>
<div class="mode-btn ${sub==='console'?'active':''}" onclick="state.brainSub='console';renderCurrentView()">⚡ 脚本执行</div>
<div class="mode-btn ${sub==='tasks'?'active':''}" onclick="state.brainSub='tasks';renderCurrentView()">📋 任务管理</div>
<div class="mode-btn ${sub==='api'?'active':''}" onclick="state.brainSub='api';renderCurrentView()">📖 API</div>
</div>`;
let subContent = '';
if (sub === 'overview') {
subContent = `
<div class="stats" style="grid-template-columns:repeat(4,1fr);margin-top:16px">
<div class="stat glass"><div class="k">技能模块</div><div class="v">${srSummary.total_skills||0}</div><div class="d">${srSummary.total_modules||0} 个子模块</div></div>
<div class="stat glass"><div class="k">可用操作</div><div class="v">${srSummary.total_actions||0}</div><div class="d">全部 action</div></div>
<div class="stat glass"><div class="k">WS 在线</div><div class="v">${(brainDash.devices||{}).ws_online||0}</div><div class="d">ADB: ${(brainDash.devices||{}).adb_online||0}</div></div>
<div class="stat glass"><div class="k">AI Brain</div><div class="v">v${(brainDash.ai_brain||{}).version||'1.0'}</div><div class="d">${(brainDash.ai_brain||{}).architecture||'v3.1'}</div></div>
</div>
<div class="panel glass" style="margin-top:16px">
<div class="panel-head"><div><h2>执行通道</h2><p class="sub">4 层通道自动降级 · Hook → u2 → ADB → AI Agent</p></div></div>
<div class="grid col4">
${card('🔗 Frida Hook <span style="color:var(--green);font-size:11px">P1</span>','直接操作 APP 内部函数<br>50-200ms · 隐蔽性高<br>需 Root + Frida')}
${card('🖱️ u2 自动化 <span style="color:var(--blue);font-size:11px">P2</span>','UI 模拟操作<br>2-15s · 通用性强<br>无需 Root')}
${card('⌨️ ADB Shell <span style="color:var(--orange);font-size:11px">P3</span>','ADB 命令控制<br>1-10s · 基础操作<br>截图/点击/输入')}
${card('🧠 AI Agent <span style="color:var(--purple);font-size:11px">P4</span>','LLM 意图解析<br>自然语言 → 步骤执行<br>依赖通道 1/2/3')}
</div>
</div>
<div class="panel glass" style="margin-top:16px">
<div class="panel-head"><div><h2>AI Brain 能力</h2><p class="sub">心跳驱动自主决策 · 离线续航 · 任务队列</p></div></div>
<div class="grid col3">
${card('🧠 自主决策','每 60s 检查设备状态<br>LLM 分析待办事项<br>自动执行最优操作')}
${card('📡 离线模式','服务器断连后<br>AI Brain 独立运行<br>结果缓冲待上传')}
${card('📋 任务队列','服务器推送 + AI 自生<br>优先级排序执行<br>常驻指令定时触发')}
${card('🛡️ 防封引擎','拟人化操作行为<br>频率限制+内容差异化<br>设备环境自检')}
${card('🔄 自动重连','指数退避重连<br>2s→4s→8s→30s<br>断网自动恢复')}
${card('📊 状态上报','心跳+详细报告<br>电量/网络/APP 状态<br>异常自动告警')}
</div>
</div>`;
} else if (sub === 'skills') {
const skillKeys = Object.keys(srSkills);
const filterBtns = [['all','全部'],...skillKeys.map(k=>[k, (srSkills[k]||{}).icon+' '+(srSkills[k]||{}).name])].map(([k,label])=>
`<div class="mode-btn ${sf===k?'active':''}" onclick="state.brainSkillFilter='${k}';renderCurrentView()" style="font-size:12px">${label}</div>`
).join('');
const filtered = sf==='all' ? Object.entries(srSkills) : Object.entries(srSkills).filter(([k])=>k===sf);
const skillCards = filtered.map(([key, skill]) => {
const mods = Object.entries(skill.modules || {});
const actionCount = mods.reduce((s,[_,a])=>s+a.length,0);
const colors = {wechat:'var(--green)',douyin:'var(--purple)',xhs:'var(--red)',xianyu:'var(--teal)',soul:'var(--orange)',system:'var(--blue)',hook:'var(--orange)',ai_brain:'var(--purple)',anti_ban:'var(--green)'};
const modList = mods.map(([modName, actions]) =>
`<div style="margin-top:10px"><div style="font-size:11px;color:var(--sub);font-weight:700;margin-bottom:5px;text-transform:uppercase;letter-spacing:.03em">${modName} <span style="color:var(--muted);font-weight:500">(${actions.length})</span></div><div style="display:flex;flex-wrap:wrap;gap:5px">${actions.map(a=>`<span style="background:rgba(37,99,235,.07);color:var(--blue);padding:4px 10px;border-radius:8px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid rgba(37,99,235,.1);transition:.15s" onmouseover="this.style.background='rgba(37,99,235,.15)'" onmouseout="this.style.background='rgba(37,99,235,.07)'" onclick="state.brainSub='console';fillBrainScript('${key}','${a}');renderCurrentView()">${a}</span>`).join('')}</div></div>`
).join('');
return `<div class="card" style="border-left:3px solid ${colors[key]||'var(--sub)'}">
<div style="display:flex;justify-content:space-between;align-items:center">
<div><span style="font-size:22px;margin-right:8px">${skill.icon||'📦'}</span><b style="font-size:16px">${skill.name}</b></div>
<span style="background:rgba(37,99,235,.08);color:var(--blue);padding:3px 10px;border-radius:8px;font-size:11px;font-weight:700">${actionCount} actions</span>
</div>
${skill.package ? `<div style="font-size:11px;color:var(--muted);margin-top:3px;font-family:ui-monospace,monospace">${skill.package}</div>` : ''}
${modList}
</div>`;
}).join('');
subContent = `
<div style="margin-top:14px">
<div class="mode-tabs" style="flex-wrap:wrap">${filterBtns}</div>
</div>
<div class="grid ${sf==='all'?'col2':''}" style="margin-top:14px">${skillCards || '<div class="empty" style="text-align:center;padding:40px;color:var(--muted)">加载中... 点击上方刷新</div>'}</div>`;
} else if (sub === 'console') {
const presets = [
{cat:'微信 · 消息', items:[
{label:'发消息', s:'wechat', a:'send_message', p:'{"to_id":"联系人名","content":"你好,在吗?"}'},
{label:'群发消息', s:'wechat', a:'batch_send_message', p:'{"user_ids":["张三","李四"],"content":"通知明天下午3点开会"}'},
{label:'获取消息', s:'wechat', a:'get_messages', p:'{"limit":20}'},
{label:'转发消息', s:'wechat', a:'forward_message', p:'{"to_id":"李四","content":"转发内容"}'},
{label:'发名片', s:'wechat', a:'send_card', p:'{"to_id":"张三","card_wxid":"wxid_xxx"}'},
{label:'发表情', s:'wechat', a:'send_emoji', p:'{"user_id":"张三","emoji_name":"微笑"}'},
{label:'发语音', s:'wechat', a:'send_voice_message', p:'{"user_id":"张三","duration":3}'},
{label:'发位置', s:'wechat', a:'send_location', p:'{"user_id":"张三"}'},
{label:'发文件', s:'wechat', a:'send_file_from_chat', p:'{"user_id":"张三"}'},
]},
{cat:'微信 · 好友', items:[
{label:'添加好友', s:'wechat', a:'add_friend', p:'{"user_id":"微信号","message":"你好我是XXX"}'},
{label:'通过好友', s:'wechat', a:'accept_friend', p:'{}'},
{label:'批量加好友', s:'wechat', a:'batch_add_friend', p:'{"user_ids":["wxid1","wxid2"],"message":"你好"}'},
{label:'获取联系人', s:'wechat', a:'get_contacts', p:'{"limit":100}'},
{label:'搜索联系人', s:'wechat', a:'search_contact', p:'{"keyword":"张三"}'},
{label:'好友详情', s:'wechat', a:'get_friend_info', p:'{"user_id":"wxid_xxx"}'},
{label:'设置备注', s:'wechat', a:'set_remark', p:'{"user_id":"wxid_xxx","remark":"客户-张三"}'},
{label:'删除好友', s:'wechat', a:'delete_friend', p:'{"user_id":"wxid_xxx"}'},
]},
{cat:'微信 · 群聊', items:[
{label:'创建群', s:'wechat', a:'create_group', p:'{"group_name":"工作群","member_ids":["张三","李四"]}'},
{label:'邀请入群', s:'wechat', a:'invite_to_group', p:'{"group_id":"群名","member_ids":["王五"]}'},
{label:'群发消息', s:'wechat', a:'send_group_message', p:'{"group_id":"群名","content":"大家好"}'},
{label:'设置群公告', s:'wechat', a:'set_group_notice', p:'{"group_id":"群名","notice":"本周五聚餐"}'},
{label:'获取群列表', s:'wechat', a:'get_groups', p:'{"limit":100}'},
{label:'群成员', s:'wechat', a:'get_group_members', p:'{"group_id":"群名"}'},
{label:'群欢迎语', s:'wechat', a:'set_group_welcome', p:'{"group_id":"群名","welcome_text":"欢迎加入!"}'},
{label:'踢人', s:'wechat', a:'remove_from_group', p:'{"group_id":"群名","member_ids":["xxx"]}'},
]},
{cat:'微信 · 朋友圈', items:[
{label:'发朋友圈', s:'wechat', a:'post_moments', p:'{"content":"今天天气真好!"}'},
{label:'点赞', s:'wechat', a:'like_moments', p:'{"user_id":"张三","post_index":0}'},
{label:'查看朋友圈', s:'wechat', a:'get_moments', p:'{"limit":10}'},
{label:'删除朋友圈', s:'wechat', a:'delete_moments', p:'{"post_index":0}'},
{label:'转发链接', s:'wechat', a:'forward_moments_link', p:'{"url":"https://...","title":"好文分享"}'},
{label:'设置权限', s:'wechat', a:'set_moments_privacy', p:'{"days":180}'},
]},
{cat:'微信 · 标签', items:[
{label:'创建标签', s:'wechat', a:'create_tag', p:'{"tag_name":"VIP客户"}'},
{label:'添加标签', s:'wechat', a:'add_tag', p:'{"user_id":"wxid_xxx","tags":["VIP客户"]}'},
{label:'查看标签', s:'wechat', a:'get_tags', p:'{}'},
{label:'按标签查人', s:'wechat', a:'get_users_by_tag', p:'{"tag_name":"VIP客户"}'},
]},
{cat:'微信 · 个人', items:[
{label:'查看资料', s:'wechat', a:'get_profile', p:'{}'},
{label:'改昵称', s:'wechat', a:'set_nickname', p:'{"nickname":"新昵称"}'},
{label:'改签名', s:'wechat', a:'set_signature', p:'{"signature":"新签名"}'},
{label:'改头像', s:'wechat', a:'set_avatar', p:'{}'},
{label:'置顶聊天', s:'wechat', a:'set_chat_top', p:'{"user_id":"张三","enable":true}'},
{label:'免打扰', s:'wechat', a:'set_mute_chat', p:'{"user_id":"张三","enable":true}'},
{label:'发红包', s:'wechat', a:'send_red_packet', p:'{"to_id":"张三","amount":"1.00","message":"恭喜发财"}'},
]},
{cat:'抖音 / 小红书 / 闲鱼 / Soul', items:[
{label:'抖音发消息', s:'douyin', a:'send_message', p:'{"to_id":"用户","content":"你好"}'},
{label:'抖音粉丝', s:'douyin', a:'get_fans', p:'{}'},
{label:'抖音回复', s:'douyin', a:'reply_comment', p:'{}'},
{label:'小红书发消息', s:'xhs', a:'send_message', p:'{"to_id":"用户","content":"你好"}'},
{label:'小红书点赞', s:'xhs', a:'like_note', p:'{}'},
{label:'闲鱼发消息', s:'xianyu', a:'send_message', p:'{"to_id":"买家","content":"你好"}'},
{label:'Soul发消息', s:'soul', a:'send_message', p:'{"to_id":"用户","content":"你好"}'},
{label:'Soul发动态', s:'soul', a:'post_moments', p:'{"content":"今日心情"}'},
]},
{cat:'系统控制', items:[
{label:'截屏', s:'system', a:'screenshot', p:'{}'},
{label:'打开微信', s:'system', a:'app_start', p:'{"package":"com.tencent.mm"}'},
{label:'打开抖音', s:'system', a:'app_start', p:'{"package":"com.ss.android.ugc.aweme"}'},
{label:'关闭应用', s:'system', a:'app_stop', p:'{"package":"com.tencent.mm"}'},
{label:'设备信息', s:'system', a:'device_info', p:'{}'},
{label:'回主页', s:'system', a:'press_key', p:'{"key":"home"}'},
{label:'返回', s:'system', a:'press_key', p:'{"key":"back"}'},
{label:'点击坐标', s:'system', a:'click', p:'{"x":540,"y":1200}'},
{label:'UI树', s:'system', a:'ui_tree', p:'{}'},
{label:'恢复网络', s:'system', a:'reconnect_network', p:'{}'},
]},
];
const presetHtml = presets.map(g => `
<div style="margin-bottom:14px">
<div style="font-size:11.5px;font-weight:700;color:var(--sub);margin-bottom:6px;text-transform:uppercase;letter-spacing:.03em">${g.cat}</div>
<div style="display:flex;flex-wrap:wrap;gap:6px">${g.items.map(p =>
`<button class="btn soft" style="font-size:11px;padding:5px 12px;border-radius:8px" onclick="fillBrainPreset('${p.s}','${p.a}',${JSON.stringify(JSON.stringify(p.p))})">${p.label}</button>`
).join('')}</div>
</div>`).join('');
subContent = `
<div class="panel glass" style="margin-top:16px">
<div class="panel-head">
<div><h2>⚡ 脚本执行</h2><p class="sub">选择设备 · 点击预设指令或手动填写 · 发送到设备执行</p></div>
<select id="aibrain-device" onchange="state.currentDevice=this.value" style="padding:8px 12px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px">${deviceOptions}</select>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:12px">
<div><label style="font-size:11px;color:var(--sub);font-weight:600;display:block;margin-bottom:4px">Script</label><input id="brain-script" placeholder="wechat / douyin / system" style="width:100%;padding:10px 14px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px"></div>
<div><label style="font-size:11px;color:var(--sub);font-weight:600;display:block;margin-bottom:4px">Action</label><input id="brain-action" placeholder="send_message / get_contacts" style="width:100%;padding:10px 14px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px"></div>
<div><label style="font-size:11px;color:var(--sub);font-weight:600;display:block;margin-bottom:4px">Channel</label><select id="brain-channel" style="width:100%;padding:10px 14px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px"><option value="auto">auto (智能选择)</option><option value="hook">hook (Frida优先)</option><option value="u2">u2 (UI自动化)</option></select></div>
</div>
<div style="margin-bottom:12px"><label style="font-size:11px;color:var(--sub);font-weight:600;display:block;margin-bottom:4px">Params (JSON)</label><textarea id="brain-params" rows="3" placeholder='{"to_id": "张三", "content": "你好"}' style="width:100%;padding:10px 14px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px;resize:vertical;font-family:ui-monospace,monospace"></textarea></div>
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">
<button class="btn primary" onclick="brainExecuteScript()">⚡ 执行</button>
<button class="btn soft" onclick="brainBatchExecute()">📡 批量执行</button>
<button class="btn soft" onclick="aiBrainLoadStatus()">🔍 Brain 状态</button>
</div>
<div id="aibrain-result" style="background:#0f172a;border-radius:var(--radius-sm);padding:16px;min-height:80px;max-height:320px;overflow:auto;font-size:12px;line-height:1.75;white-space:pre-wrap;color:#cbd5e1;font-family:ui-monospace,SFMono-Regular,monospace"><span style="color:#7dd3fc">// 就绪 · 点击下方预设指令 或 手动填写后执行</span></div>
</div>
<div class="panel glass" style="margin-top:16px">
<div class="panel-head"><div><h2>📋 预设指令库</h2><p class="sub">点击任意指令自动填入控制台 · 覆盖微信全功能 + 多平台 + 系统控制</p></div></div>
${presetHtml}
</div>`;
} else if (sub === 'tasks') {
subContent = `
<div class="panel glass" style="margin-top:16px">
<div class="panel-head">
<div><h2>📋 AI 任务推送</h2><p class="sub">向设备 AI Brain 推送自然语言任务 · AI 自主拆解执行</p></div>
<select id="aibrain-device" onchange="state.currentDevice=this.value" style="padding:8px 12px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px">${deviceOptions}</select>
</div>
<div style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap">
<button class="btn primary" onclick="aiBrainPushTask()">📋 推送 AI 任务</button>
<button class="btn soft" onclick="aiBrainPushOrder()">📌 推送常驻指令</button>
<button class="btn soft" onclick="aiBrainExecute()">⚡ AI 自然语言执行</button>
<button class="btn soft" onclick="aiBrainLoadStatus()">🔍 查看 Brain 状态</button>
</div>
<div id="aibrain-result" style="background:#0f172a;border-radius:var(--radius-sm);padding:16px;min-height:80px;max-height:320px;overflow:auto;font-size:12px;line-height:1.75;white-space:pre-wrap;color:#cbd5e1;font-family:ui-monospace,SFMono-Regular,monospace"><span style="color:#7dd3fc">// 任务管理就绪</span></div>
</div>
<div class="panel glass" style="margin-top:16px">
<div class="panel-head"><div><h2>📌 常驻指令</h2><p class="sub">心跳周期自动触发 · 离线时 AI Brain 自主执行</p></div></div>
<div class="grid col3">
${card('📬 检查未读','自动检查微信未读消息<br>阅读并标记已读<br><code style="font-size:10px">wechat.get_messages</code>')}
${card('🤝 通过好友','自动通过新的好友请求<br>可附带欢迎语<br><code style="font-size:10px">wechat.accept_friend</code>')}
${card('💚 保活微信','检查微信运行状态<br>未运行则自动启动<br><code style="font-size:10px">system.app_start</code>')}
${card('🛡️ 弹窗守护','每5秒扫描系统弹窗<br>自动处理权限/USB授权<br><code style="font-size:10px">system.dismiss_popups</code>')}
${card('🌐 网络恢复','检测断网后自动重连<br>WiFi/4G 智能切换<br><code style="font-size:10px">system.reconnect</code>')}
${card('🌱 养号计划','新号/成长/成熟三阶段<br>自动执行养号动作<br><code style="font-size:10px">anti_ban.nurture</code>')}
${card('📊 朋友圈互动','定时浏览好友朋友圈<br>随机点赞+评论<br><code style="font-size:10px">wechat.like_moments</code>')}
${card('🏷️ 标签整理','新好友自动打标签<br>按来源/地区分类<br><code style="font-size:10px">wechat.add_tag</code>')}
${card('📱 设备状态上报','电量/网络/APP 状态<br>异常自动告警<br><code style="font-size:10px">system.status</code>')}
</div>
</div>
<div class="panel glass" style="margin-top:16px">
<div class="panel-head"><div><h2>🤖 AI 自然语言任务示例</h2><p class="sub">直接用中文描述任务 · AI Brain 自动拆解为技能执行链</p></div></div>
<div class="grid col2">
${card('💬 客户跟进','打开微信,找到张三,<br>发消息说"上次谈的方案已经确认,<br>方便的话约个时间聊聊"')}
${card('📢 群发通知','给所有VIP客户群发消息<br>"本周六下午2点线下沙龙<br>欢迎参加"')}
${card('🔍 数据采集','打开微信获取最近20条<br>未读消息,整理联系人信息,<br>按标签分类统计')}
${card('🌅 朋友圈营销','发一条朋友圈<br>"新品上市,限时优惠"<br>然后浏览好友动态点赞')}
${card('👋 新人欢迎','通过所有待处理的好友请求<br>给每个新好友发欢迎语<br>并打上"新客户"标签')}
${card('📊 日报汇总','截屏当前微信页面<br>获取今日消息统计<br>获取好友列表变化')}
</div>
</div>`;
} else if (sub === 'api') {
subContent = `
<div class="panel glass" style="margin-top:16px">
<div class="panel-head"><div><h2>🔌 API 端点</h2><p class="sub">AI Brain 全部 REST API</p></div></div>
<div style="font-size:13px;line-height:2.2;padding:8px 0">
<div style="font-weight:700;color:var(--text);margin-bottom:4px">AI Brain 核心</div>
<code style="background:rgba(37,99,235,.08);padding:3px 8px;border-radius:6px;margin-right:6px">GET</code> /api/v3/ai/brain/skill-registry — 技能注册表<br>
<code style="background:rgba(37,99,235,.08);padding:3px 8px;border-radius:6px;margin-right:6px">GET</code> /api/v3/ai/brain/dashboard — 仪表盘<br>
<code style="background:rgba(22,163,74,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/ai/brain/execute-script — 脚本执行<br>
<code style="background:rgba(22,163,74,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/ai/brain/batch-execute — 批量执行<br>
<div style="font-weight:700;color:var(--text);margin:12px 0 4px">设备 AI Brain</div>
<code style="background:rgba(37,99,235,.08);padding:3px 8px;border-radius:6px;margin-right:6px">GET</code> /api/v3/devices/{id}/ai/status — 设备 Brain 状态<br>
<code style="background:rgba(22,163,74,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/devices/{id}/ai/task — 推送任务<br>
<code style="background:rgba(22,163,74,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/devices/{id}/ai/standing-order — 常驻指令<br>
<code style="background:rgba(245,158,11,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/devices/{id}/ai/execute — 同步执行<br>
<div style="font-weight:700;color:var(--text);margin:12px 0 4px">AI 对话</div>
<code style="background:rgba(22,163,74,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/ai/chat — AI 对话控制<br>
<code style="background:rgba(22,163,74,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/v1/chat/completions — OpenAI 兼容<br>
<code style="background:rgba(22,163,74,.08);padding:3px 8px;border-radius:6px;margin-right:6px">POST</code> /api/v3/agent/execute — 自然语言执行<br>
</div>
</div>`;
}
content = `
<div class="panel glass">
<div class="panel-head">
<div><h2>🧠 AI Brain</h2><p class="sub">全设备统一控制中枢 · ${srSummary.total_skills||0} 技能 / ${srSummary.total_actions||0} 操作</p></div>
<div class="btns"><button class="btn primary" onclick="loadBrainDashboard()">🔄 刷新</button></div>
</div>
${subTabs}
</div>
${subContent}`;
} else if (tab === 'hook') {
content = `
<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(m => `
<tr>
<td>${esc(m.name || m.module_id)}</td>
<td>${esc(m.version || '-')}</td>
<td>${m.enabled ? '启用' : '停用'}</td>
<td>${esc((m.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>执行通道优先级</h2><p class="sub">Agent v3.1 的通道选择策略:优先 Hook → 降级 u2</p></div></div>
<div class="grid col2">
${card('🔗 通道 1Frida Hook优先','直接操作 APP 内部函数<br>发消息/读联系人/群管理<br>速度快、不触发 UI、隐蔽性高')}
${card('🖱️ 通道 2uiautomator2兜底','UI 自动化操作<br>点击/滑动/输入/截图<br>Frida 不可用时自动降级')}
</div>
<div class="grid col2" style="margin-top:10px">
${card('🧠 通道 3AI Agent增强','自然语言 → 步骤拆解 → 执行<br>依赖通道 1 或 2')}
${card('📺 通道 4scrcpy人工','投屏 + 人工操作<br>调试和特殊场景用')}
</div>
</div>`;
} else if (tab === 'hookdata') {
const hd = state.hookData || {};
const hdLoading = state.hookDataLoading;
const profile = hd.profile?.profile || hd.profile || {};
const contacts = hd.contacts?.contacts || [];
const groups = hd.groups?.groups || [];
const labels = hd.labels?.labels || [];
const messages = hd.messages?.messages || [];
const deviceInfo = hd.device_info?.device || {};
content = `
<div class="panel glass">
<div class="panel-head">
<div><h2>Hook 实时数据</h2><p class="sub">通过 Frida Hook 直连设备,实时读取微信数据</p></div>
<div class="btns">
<select id="hookdata-device" onchange="state.currentDevice=this.value" style="padding:8px 12px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px">${deviceOptions}</select>
<button class="btn primary" onclick="fetchHookData()" ${hdLoading?'disabled':''}>${hdLoading ? '加载中...' : '🔄 拉取数据'}</button>
</div>
</div>
${!hd.profile && !hdLoading ? '<div style="text-align:center;padding:40px 0;color:var(--muted);font-size:14px">选择设备后点击「拉取数据」获取 Hook 实时数据</div>' : ''}
${hd.profile ? `
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:16px">
<div class="card" style="border-left:3px solid var(--blue)">
<div style="font-size:11px;color:var(--sub);font-weight:600">微信资料</div>
<div style="font-size:18px;font-weight:800;margin-top:4px">${esc(profile.nickname || '-')}</div>
<div style="font-size:12px;color:var(--sub);margin-top:4px">wxid: ${esc(profile.wxid || '-')}</div>
<div style="font-size:11px;color:var(--muted);margin-top:2px">版本: ${esc(profile.wechat_version || '-')} · 签名: ${esc(profile.signature || '-')}</div>
</div>
<div class="card" style="border-left:3px solid var(--green)">
<div style="font-size:11px;color:var(--sub);font-weight:600">数据统计</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px">
<div><div style="font-size:22px;font-weight:800;color:var(--green)">${contacts.length || hd.contacts?.count || 0}</div><div style="font-size:10px;color:var(--muted)">联系人</div></div>
<div><div style="font-size:22px;font-weight:800;color:var(--blue)">${groups.length || hd.groups?.count || 0}</div><div style="font-size:10px;color:var(--muted)">群聊</div></div>
<div><div style="font-size:22px;font-weight:800;color:var(--purple)">${labels.length || hd.labels?.count || 0}</div><div style="font-size:10px;color:var(--muted)">标签</div></div>
<div><div style="font-size:22px;font-weight:800;color:var(--orange)">${messages.length || 0}</div><div style="font-size:10px;color:var(--muted)">近期消息</div></div>
</div>
</div>
<div class="card" style="border-left:3px solid var(--teal)">
<div style="font-size:11px;color:var(--sub);font-weight:600">设备信息</div>
<div style="font-size:13px;margin-top:6px;line-height:1.8">
${deviceInfo.model ? `机型: <b>${esc(deviceInfo.model)}</b><br>` : ''}
${deviceInfo.brand ? `品牌: ${esc(deviceInfo.brand)}<br>` : ''}
${deviceInfo.android_version ? `Android: ${esc(deviceInfo.android_version)} (SDK ${deviceInfo.sdk_int || ''})<br>` : ''}
${!deviceInfo.model ? '<span style="color:var(--muted)">点击拉取数据</span>' : ''}
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h3 style="font-size:14px;font-weight:700">👥 联系人 (${contacts.length})</h3>
<input type="text" id="hook-contact-search" placeholder="搜索..." oninput="filterHookTable('contacts')" style="padding:5px 10px;border-radius:8px;border:1px solid var(--border);font-size:12px;width:140px;background:var(--panel)">
</div>
<div style="max-height:360px;overflow-y:auto;scrollbar-width:thin" id="hook-contacts-list">
<table class="list-table" style="font-size:12px">
<thead><tr><th>wxid</th><th>昵称</th><th>备注</th></tr></thead>
<tbody>
${contacts.length ? contacts.slice(0,100).map(c => `
<tr class="hook-contact-row"><td style="font-family:monospace;font-size:11px">${esc((c.wxid||'').substring(0,20))}</td><td>${esc(c.nickname||'-')}</td><td>${esc(c.remark||'')}</td></tr>
`).join('') : '<tr><td colspan="3" class="empty">暂无联系人</td></tr>'}
</tbody>
</table>
</div>
</div>
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h3 style="font-size:14px;font-weight:700">👥 群聊 (${groups.length})</h3>
<input type="text" id="hook-group-search" placeholder="搜索..." oninput="filterHookTable('groups')" style="padding:5px 10px;border-radius:8px;border:1px solid var(--border);font-size:12px;width:140px;background:var(--panel)">
</div>
<div style="max-height:360px;overflow-y:auto;scrollbar-width:thin" id="hook-groups-list">
<table class="list-table" style="font-size:12px">
<thead><tr><th>群ID</th><th>群名</th><th>成员数</th></tr></thead>
<tbody>
${groups.length ? groups.slice(0,100).map(g => `
<tr class="hook-group-row"><td style="font-family:monospace;font-size:11px">${esc((g.chatroomId||g.wxid||'').substring(0,22))}</td><td>${esc(g.nickname||g.name||'-')}</td><td>${g.memberCount||g.member_count||'-'}</td></tr>
`).join('') : '<tr><td colspan="3" class="empty">暂无群聊</td></tr>'}
</tbody>
</table>
</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px">
<div class="card">
<h3 style="font-size:14px;font-weight:700;margin-bottom:10px">🏷️ 标签 (${labels.length})</h3>
<div style="display:flex;flex-wrap:wrap;gap:6px">
${labels.length ? labels.map(l => `<span class="pill" style="background:rgba(124,58,237,.08);color:var(--purple);border-color:rgba(124,58,237,.12)">${esc(l.name||l.labelName||'未命名')} <small style="opacity:.6">(${l.count||l.contactCount||0})</small></span>`).join('') : '<span style="color:var(--muted);font-size:12px">暂无标签</span>'}
</div>
</div>
<div class="card">
<h3 style="font-size:14px;font-weight:700;margin-bottom:10px">💬 最近消息</h3>
<div style="max-height:200px;overflow-y:auto;scrollbar-width:thin">
${messages.length ? messages.slice(0,15).map(m => `
<div style="padding:6px 0;border-bottom:1px solid rgba(0,0,0,.04);font-size:12px">
<span style="color:var(--blue);font-weight:600">${esc((m.talker||m.from||'').substring(0,16))}</span>
<span style="color:var(--muted);margin:0 6px">·</span>
<span>${esc((m.content||'').substring(0,60))}</span>
</div>
`).join('') : '<div style="color:var(--muted);font-size:12px">暂无消息</div>'}
</div>
</div>
</div>
` : ''}
</div>`;
} else if (tab === 'events') {
content = `
<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>`;
} else if (tab === 'connection') {
const protocol = state.protocol || {};
const connection = state.connection || {};
const handshake = protocol.handshake || {};
content = `
<div class="panel glass">
<div class="panel-head"><div><h2>连接入口</h2><p class="sub">WebSocket Hub、Hook 事件流和 REST API 的实时端点</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>`;
} else if (tab === 'heartbeat') {
const hb = state.heartbeatDashboard || {};
const hbDevices = hb.devices || {};
const hbEvents = state.heartbeatEvents || [];
const deviceIds = Object.keys(hbDevices);
const healthColor = s => s >= 80 ? 'var(--green)' : s >= 50 ? 'var(--orange)' : 'var(--red)';
const healthLabel = s => s >= 80 ? '健康' : s >= 50 ? '注意' : '异常';
content = `
<div class="panel glass">
<div class="panel-head"><div><h2>心跳监控</h2><p class="sub">AI 驱动的设备健康评分 · 异常检测 · 自动恢复</p></div></div>
<div class="stats" style="grid-template-columns:repeat(4,1fr);margin-bottom:8px">
${statCard('监控设备', deviceIds.length, '实时健康评分')}
${statCard('平均分', deviceIds.length ? Math.round(deviceIds.reduce((s,id)=>s+(hbDevices[id]?.score||0),0)/deviceIds.length) : '-', '满分 100')}
${statCard('告警事件', hbEvents.length, '最近记录')}
${statCard('监控周期', (hb.interval_sec || 10) + 's', '自动巡检')}
</div>
</div>
${deviceIds.length ? `<div class="panel glass"><div class="panel-head"><div><h2>设备健康</h2></div></div><div class="grid col2">${deviceIds.map(id => {
const d = hbDevices[id]; const score = d.score ?? 0;
return `<div class="card" style="position:relative;overflow:hidden"><div style="position:absolute;top:0;left:0;right:0;height:3px;background:rgba(15,23,42,.04)"><div style="height:100%;width:${score}%;background:${healthColor(score)};border-radius:0 2px 2px 0;transition:width .5s"></div></div><div style="display:flex;justify-content:space-between;align-items:flex-start;margin-top:4px"><div><h3 style="font-size:14px">${esc(id.substring(0,16))}${id.length>16?'…':''}</h3><div style="font-size:11px;color:var(--sub);margin-top:2px">上次心跳: ${d.last_heartbeat ? new Date(d.last_heartbeat).toLocaleTimeString('zh-CN',{hour12:false}) : '-'} · ${d.heartbeat_age_sec || 0}s 前</div></div><div style="text-align:right"><div class="health-score" style="color:${healthColor(score)}">${score}</div><div class="health-label">${healthLabel(score)}</div></div></div><div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px;font-size:11.5px"><div><span style="color:var(--sub)">电量</span><br><b>${d.battery_level >= 0 ? d.battery_level + '%' : '-'}</b>${d.battery_charging ? ' ⚡' : ''}</div><div><span style="color:var(--sub)">内存</span><br><b>${d.memory_usage_pct > 0 ? d.memory_usage_pct.toFixed(1) + '%' : '-'}</b></div><div><span style="color:var(--sub)">存储</span><br><b>${d.storage_free_mb > 0 ? d.storage_free_mb + 'MB' : '-'}</b></div><div><span style="color:var(--sub)">网络</span><br><b>${esc(d.network_type || '-')}</b></div></div><div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:10px"><span class="mode-badge ${d.ws_connected ? 'available' : 'unavailable'}"><span class="badge-dot"></span>WS</span><span class="mode-badge ${d.adb_connected ? 'available' : 'unavailable'}"><span class="badge-dot"></span>ADB</span><span class="mode-badge ${d.hook_available ? 'available' : 'unavailable'}"><span class="badge-dot"></span>Hook</span><span class="mode-badge ${d.ai_ready ? 'available' : 'unavailable'}"><span class="badge-dot"></span>AI</span><span class="mode-badge ${d.wechat_running ? 'available' : 'unavailable'}"><span class="badge-dot"></span>微信</span><span class="mode-badge ${d.screen_on ? 'available' : 'unavailable'}"><span class="badge-dot"></span>屏幕</span></div></div>`;
}).join('')}</div></div>` : ''}
<div class="panel glass"><div class="panel-head"><div><h2>健康事件</h2></div></div><div class="card"><table class="list-table"><thead><tr><th>时间</th><th>设备</th><th>级别</th><th>事件</th></tr></thead><tbody>${hbEvents.length ? hbEvents.slice(0, 30).map(ev => { const lvlColor = ev.level === 'CRITICAL' ? 'var(--red)' : ev.level === 'WARNING' ? 'var(--orange)' : 'var(--sub)'; return `<tr><td>${ev.time ? new Date(ev.time).toLocaleTimeString('zh-CN',{hour12:false}) : '-'}</td><td>${esc((ev.device_id || '').substring(0,12))}</td><td style="color:${lvlColor};font-weight:600">${esc(ev.level || '-')}</td><td>${esc(ev.message || '-')}</td></tr>`; }).join('') : '<tr><td colspan="4" class="empty">暂无事件 — 设备运行正常</td></tr>'}</tbody></table></div></div>
<div class="panel glass"><div class="panel-head"><div><h2>AI 任务队列</h2><p class="sub">向设备推送 AI 任务,通过心跳周期自动下发</p></div></div><div class="grid col2"><div class="card"><h3>推送任务</h3><div style="margin-top:10px"><select id="hb-device-select" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:var(--panel);font-size:13px;margin-bottom:8px">${(state.devices || []).map(d => `<option value="${d.device_id}">${esc(d.device_id)} (${d.status || 'offline'})</option>`).join('')}</select><input id="hb-task-input" placeholder="输入 AI 任务指令" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);font-size:13px;margin-bottom:8px"><button class="btn primary" onclick="pushHeartbeatTask()" style="width:100%">推送到任务队列</button></div><div id="hb-task-result" style="margin-top:10px;font-size:12px;color:var(--sub)"></div></div><div class="card"><h3>队列说明</h3><div style="font-size:12.5px;color:var(--sub);line-height:1.8;margin-top:8px"><div>1. 任务通过 API 推入设备队列</div><div>2. 每次心跳周期10s检查队列</div><div>3. 自动批量下发(每次最多 3 个)</div><div>4. 设备 Agent 收到后立即执行</div><div>5. 执行结果通过 WS 事件回传</div></div></div></div></div>`;
}
return header + content;
}
/* ═══ 4.5 Gateway Panel (外部对接 + Fleet + Journey) ═══ */
function renderGatewayPanel(){
const tab = state.gatewayTab;
const info = state.gatewayInfo || {};
const fleet = state.fleetStatus || {};
const journeys = state.journeys || {};
const header = `
<div class="panel glass">
<div class="panel-head">
<div><h2>🔗 对接网关</h2><p class="sub">REST API · OpenAI Compatible · MCP Protocol · 多设备管理 · AI 旅程</p></div>
<div class="btns"><button class="btn soft" onclick="refreshGateway()">刷新</button></div>
</div>
<div class="mode-tabs">
<div class="mode-btn ${tab==='overview'?'active':''}" onclick="state.gatewayTab='overview';renderCurrentView()">🌐 概览</div>
<div class="mode-btn ${tab==='fleet'?'active':''}" onclick="state.gatewayTab='fleet';renderCurrentView()">📱 多设备管理</div>
<div class="mode-btn ${tab==='journey'?'active':''}" onclick="state.gatewayTab='journey';renderCurrentView()">🛤️ AI 旅程</div>
<div class="mode-btn ${tab==='mcp'?'active':''}" onclick="state.gatewayTab='mcp';renderCurrentView()">🧩 MCP Tools</div>
<div class="mode-btn ${tab==='try'?'active':''}" onclick="state.gatewayTab='try';renderCurrentView()">⚡ 测试</div>
</div>
</div>`;
let content = '';
if (tab === 'overview') {
const protocols = info.protocols || {};
content = `
<div class="panel glass">
<div class="panel-head"><div><h2>三种对接方式</h2><p class="sub">外部程序通过以下任一方式接入工作手机 SDK实现多台手机统一管控</p></div></div>
<div class="grid col3">
<div class="card" style="border-top:3px solid var(--blue)">
<h3 style="color:var(--blue)">🌐 REST API</h3>
<p style="margin:8px 0">192+ 端点覆盖微信全功能、设备控制、AI 任务。</p>
<div class="path">Base: /api/v3</div>
<div style="margin-top:10px"><a class="btn soft" href="/docs" target="_blank" style="text-decoration:none">📄 Swagger 文档</a></div>
</div>
<div class="card" style="border-top:3px solid var(--green)">
<h3 style="color:var(--green)">🤖 OpenAI Compatible</h3>
<p style="margin:8px 0">像调用 GPT 一样控制手机,支持所有 OpenAI SDK。</p>
<div class="path">Base: /api/v3/gateway/v1</div>
<div style="margin-top:10px;font-size:11px;color:var(--sub);line-height:1.8">
<code style="background:rgba(22,163,74,.08);padding:2px 6px;border-radius:4px">POST</code> /chat/completions<br>
<code style="background:rgba(22,163,74,.08);padding:2px 6px;border-radius:4px">GET</code> /models
</div>
</div>
<div class="card" style="border-top:3px solid var(--purple)">
<h3 style="color:var(--purple)">🧩 MCP Protocol</h3>
<p style="margin:8px 0">8 个工具,可直接被 Claude/Cursor 等 AI Agent 调用。</p>
<div class="path">Base: /api/v3/gateway/mcp</div>
<div style="margin-top:10px;font-size:11px;color:var(--sub);line-height:1.8">
<code style="background:rgba(124,58,237,.08);padding:2px 6px;border-radius:4px">GET</code> /tools<br>
<code style="background:rgba(124,58,237,.08);padding:2px 6px;border-radius:4px">POST</code> /call
</div>
</div>
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>OpenAI SDK 接入示例</h2><p class="sub">Python / cURL 一行代码即可控制手机</p></div></div>
<div class="grid col2">
<div class="card">
<h3>Python 示例</h3>
<pre style="background:#0f172a;color:#e2e8f0;padding:14px;border-radius:12px;font-size:12px;line-height:1.7;overflow-x:auto;margin-top:8px">from openai import OpenAI
client = OpenAI(
base_url="http://服务器:8899/api/v3/gateway/v1",
api_key="any"
)
resp = client.chat.completions.create(
model="workphone-agent",
messages=[{
"role": "user",
"content": "打开微信,给张三发消息:你好"
}]
)
print(resp.choices[0].message.content)</pre>
</div>
<div class="card">
<h3>cURL 示例</h3>
<pre style="background:#0f172a;color:#e2e8f0;padding:14px;border-radius:12px;font-size:12px;line-height:1.7;overflow-x:auto;margin-top:8px"># 发送 AI 指令
curl -X POST \\
http://服务器:8899/api/v3/gateway/v1/chat/completions \\
-H "Content-Type: application/json" \\
-d '{
"model": "workphone-agent",
"messages": [{
"role": "user",
"content": "检查微信未读消息"
}]
}'
# MCP 工具调用
curl -X POST \\
http://服务器:8899/api/v3/gateway/mcp/call \\
-H "Content-Type: application/json" \\
-d '{
"name": "send_message",
"arguments": {
"to": "张三",
"content": "你好"
}
}'</pre>
</div>
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>管理入口</h2><p class="sub">多设备批量控制、状态监控和旅程追踪</p></div></div>
<div class="grid col3">
${card('📱 多设备管理', '设备分组 · 批量广播 · 全局状态<br><code>GET /gateway/fleet/status</code>', '<button class="btn soft" onclick="state.gatewayTab=\'fleet\';renderCurrentView()">进入</button>')}
${card('🛤️ AI 旅程', '每台手机的操作历史与 AI 决策记录<br><code>GET /gateway/journey</code>', '<button class="btn soft" onclick="state.gatewayTab=\'journey\';renderCurrentView()">进入</button>')}
${card('⚡ 在线测试', '直接测试三种对接方式<br>发送请求查看结果', '<button class="btn soft" onclick="state.gatewayTab=\'try\';renderCurrentView()">进入</button>')}
</div>
</div>`;
} else if (tab === 'fleet') {
const devices = fleet.devices || [];
const groups = fleet.groups || {};
content = `
<div class="panel glass">
<div class="panel-head"><div><h2>多设备管理</h2><p class="sub">${fleet.total || 0} 台设备 · ${fleet.healthy || 0} 健康 · ${fleet.offline || 0} 离线</p></div>
<div class="btns">
<button class="btn primary" onclick="fleetBroadcast()">📢 广播命令</button>
<button class="btn soft" onclick="refreshFleet()">刷新</button>
</div>
</div>
<div class="stats" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
${statCard('设备总数', fleet.total || 0, '已注册设备')}
${statCard('健康设备', fleet.healthy || 0, '可控制')}
${statCard('离线', fleet.offline || 0, '不可达')}
${statCard('分组', Object.keys(groups).length, '设备分组')}
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>设备列表</h2><p class="sub">所有已连接设备的实时状态</p></div></div>
<div class="card">
<table class="list-table">
<thead><tr><th>设备ID</th><th>状态</th><th>通道</th><th>型号</th><th>电量</th><th>网络</th><th>当前APP</th></tr></thead>
<tbody>
${devices.length ? devices.map(d => `
<tr>
<td style="font-family:monospace;font-size:11px">${esc(d.device_id?.substring(0,16))}${(d.device_id||'').length>16?'…':''}</td>
<td>${d.status==='online'?'<span style="color:var(--green);font-weight:600">● 在线</span>':d.status==='adb'?'<span style="color:var(--blue);font-weight:600">● ADB</span>':'<span style="color:var(--red)">○ 离线</span>'}</td>
<td>${esc(d.channel||'-')}</td>
<td>${esc(d.model||'-')}</td>
<td>${d.battery>=0?d.battery+'%':'-'}</td>
<td>${esc(d.network||'-')}</td>
<td>${esc(d.current_app||'-')}</td>
</tr>
`).join('') : '<tr><td colspan="7" class="empty">暂无设备</td></tr>'}
</tbody>
</table>
</div>
</div>
${Object.keys(groups).length ? `
<div class="panel glass">
<div class="panel-head"><div><h2>设备分组</h2></div></div>
<div class="grid col3">
${Object.entries(groups).map(([name, ids]) => card(name, `${ids.length} 台设备<br>${ids.slice(0,3).map(id => '<code>' + esc(id.substring(0,12)) + '</code>').join(', ')}${ids.length>3?'..':''}`)).join('')}
</div>
</div>` : ''}`;
} else if (tab === 'journey') {
const allJourneys = journeys.journeys || {};
const deviceIds = journeys.devices || [];
content = `
<div class="panel glass">
<div class="panel-head">
<div><h2>AI 旅程追踪</h2><p class="sub">每台手机的完整操作历史 · AI 决策链 · ${journeys.total_entries || 0} 条记录</p></div>
<div class="btns"><button class="btn soft" onclick="refreshJourney()">刷新</button></div>
</div>
<div class="stats" style="grid-template-columns:repeat(3,1fr);margin-bottom:12px">
${statCard('记录总数', journeys.total_entries || 0, '操作轨迹')}
${statCard('活跃设备', deviceIds.length || 0, '有旅程记录')}
${statCard('追踪状态', journeys.total_entries > 0 ? '运行中' : '等待', '实时记录')}
</div>
</div>
${deviceIds.length ? deviceIds.map(did => {
const entries = allJourneys[did] || [];
return `
<div class="panel glass">
<div class="panel-head"><div><h2>📱 ${esc(did.substring(0,16))}${did.length>16?'…':''}</h2><p class="sub">${entries.length} 条旅程记录</p></div></div>
<div class="card">
<table class="list-table">
<thead><tr><th>时间</th><th>操作</th><th>来源</th><th>结果</th></tr></thead>
<tbody>
${entries.slice(0,20).map(e => `
<tr>
<td style="font-size:11px;white-space:nowrap">${esc(e.time_str||'-')}</td>
<td style="font-weight:600">${esc(e.action||'-')}</td>
<td><span class="pill">${esc(e.source||'api')}</span></td>
<td style="font-size:11.5px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc((e.result||'').substring(0,80))}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>`;
}).join('') : `
<div class="panel glass">
<div class="card"><div class="empty" style="padding:40px">暂无旅程记录 · 设备开始操作后自动追踪</div></div>
</div>`}`;
} else if (tab === 'mcp') {
const tools = (info.protocols?.mcp?.tools || []);
content = `
<div class="panel glass">
<div class="panel-head"><div><h2>MCP Tools 清单</h2><p class="sub">MCP Protocol 兼容的工具列表,可被 AI Agent 直接调用</p></div></div>
</div>
${['send_message','take_screenshot','open_app','get_device_status','list_devices','execute_task','add_friend','batch_send'].map((name,i) => {
const descriptions = {
send_message: '通过微信发送消息给指定联系人',
take_screenshot: '对手机截屏,返回当前屏幕画面',
open_app: '打开手机上的APP微信/抖音/小红书/闲鱼)',
get_device_status: '获取设备实时状态电量、网络、APP等',
list_devices: '列出所有已连接设备及状态',
execute_task: '自然语言任务 → AI Agent 自动执行',
add_friend: '在微信中添加好友',
batch_send: '批量发送消息给多个联系人(含防封间隔)',
};
const icons = {
send_message:'💬', take_screenshot:'📸', open_app:'📱',
get_device_status:'📊', list_devices:'📋', execute_task:'🤖',
add_friend:'🤝', batch_send:'📨'
};
return `
<div class="panel glass" style="padding:18px 22px">
<div style="display:flex;align-items:center;gap:12px">
<div style="font-size:28px">${icons[name]||'⚡'}</div>
<div style="flex:1">
<div style="font-weight:700;font-size:14px">${name}</div>
<div style="font-size:12px;color:var(--sub);margin-top:2px">${descriptions[name]||''}</div>
</div>
<button class="btn soft" onclick="testMcpTool('${name}')">测试</button>
</div>
</div>`;
}).join('')}`;
} else if (tab === 'try') {
content = `
<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>OpenAI Chat 测试</h3>
<div style="margin-top:10px">
<input id="gw-chat-input" placeholder="输入自然语言指令…" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);font-size:13px;margin-bottom:8px">
<button class="btn primary" onclick="testOpenAIChat()" style="width:100%">发送到 OpenAI API</button>
</div>
</div>
<div class="card">
<h3>MCP Tool 测试</h3>
<div style="margin-top:10px">
<select id="gw-mcp-tool" style="width:100%;padding:10px;border-radius:10px;border:1px solid var(--border);font-size:13px;margin-bottom:8px">
<option value="list_devices">list_devices</option>
<option value="get_device_status">get_device_status</option>
<option value="take_screenshot">take_screenshot</option>
<option value="open_app">open_app</option>
<option value="send_message">send_message</option>
</select>
<button class="btn primary" onclick="testMcpToolDirect()" style="width:100%">调用 MCP Tool</button>
</div>
</div>
</div>
<div id="gw-test-result" style="margin-top:14px;background:rgba(15,23,42,.03);border-radius:var(--radius-sm);padding:16px;min-height:100px;font-size:12.5px;line-height:1.7;white-space:pre-wrap;color:var(--sub);font-family:ui-monospace,monospace">
点击上方按钮发送测试请求
</div>
</div>`;
}
return header + content;
}
async function refreshGateway(){
try {
const [infoRes, fleetRes, journeyRes] = await Promise.all([
apiGet('/api/v3/gateway/info'),
apiGet('/api/v3/gateway/fleet/status').catch(()=>({data:{}})),
apiGet('/api/v3/gateway/journey').catch(()=>({data:{}})),
]);
state.gatewayInfo = infoRes;
state.fleetStatus = fleetRes.data || {};
state.journeys = journeyRes.data || {};
if (state.current === 'gateway') renderCurrentView();
} catch(e) {
log('网关数据加载失败: ' + e.message, 'error');
}
}
async function refreshFleet(){ try { const r = await apiGet('/api/v3/gateway/fleet/status'); state.fleetStatus = r.data || {}; if(state.current==='gateway')renderCurrentView(); } catch(e){} }
async function refreshJourney(){ try { const r = await apiGet('/api/v3/gateway/journey'); state.journeys = r.data || {}; if(state.current==='gateway')renderCurrentView(); } catch(e){} }
async function testOpenAIChat(){
const input = document.getElementById('gw-chat-input');
const result = document.getElementById('gw-test-result');
const msg = input?.value?.trim();
if (!msg) { result.textContent = '请输入指令'; return; }
result.textContent = '请求中...';
try {
const res = await apiPost('/api/v3/gateway/v1/chat/completions', {
model: 'workphone-agent',
messages: [{role:'user', content: msg}]
});
result.textContent = JSON.stringify(res, null, 2);
} catch(e) { result.textContent = '错误: ' + e.message; }
}
async function testMcpTool(name){
state.gatewayTab = 'try';
renderCurrentView();
setTimeout(() => testMcpToolWithName(name), 100);
}
async function testMcpToolWithName(name){
const result = document.getElementById('gw-test-result');
if (result) result.textContent = `调用 MCP 工具: ${name} ...`;
try {
const res = await apiPost('/api/v3/gateway/mcp/call', {name, arguments:{}});
if (result) result.textContent = JSON.stringify(res, null, 2);
} catch(e) {
if (result) result.textContent = '错误: ' + e.message;
}
}
async function testMcpToolDirect(){
const tool = document.getElementById('gw-mcp-tool')?.value || 'list_devices';
await testMcpToolWithName(tool);
}
async function fleetBroadcast(){
const cmd = prompt('输入广播命令(发送到所有设备):', '检查微信状态');
if (!cmd) return;
try {
const res = await apiPost('/api/v3/gateway/fleet/broadcast', {command: cmd});
const data = res.data || {};
log(`广播完成: ${data.success||0}/${data.total||0} 成功`, 'info');
refreshFleet();
} catch(e) { log('广播失败: ' + e.message, 'error'); }
}
/* ═══ 5. Docs Panel (Documents + Architecture + Project) ═══ */
function renderDocsPanel(){
const allDocs = state.docs;
const assets = state.assets || [];
const projectDocs = allDocs.filter(d=>['project'].includes(d.category));
const archDocs = allDocs.filter(d=>['architecture','api'].includes(d.category));
const tab = state.docsTab;
const cats = [...new Set(allDocs.map(d=>d.section))];
const filter = state.docFilter || '';
const filteredDocs = filter ? allDocs.filter(d=>d.section===filter) : allDocs;
const selected = state.selectedDoc || filteredDocs[0]?.id;
state.selectedDoc = selected;
if (tab==='manual' && 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:16px">
<div class="mode-btn ${tab==='manual'?'active':''}" onclick="state.docsTab='manual';renderCurrentView()">📖 文档 (${allDocs.length})</div>
<div class="mode-btn ${tab==='arch'?'active':''}" onclick="state.docsTab='arch';renderCurrentView()">🧭 架构图 (${assets.length})</div>
<div class="mode-btn ${tab==='project'?'active':''}" onclick="state.docsTab='project';renderCurrentView()">📊 项目</div>
</div>
${tab==='manual' ? `
<div class="mode-tabs" style="margin-bottom:14px">
<div class="mode-btn ${!filter?'active':''}" onclick="filterDocs('')">全部</div>
${cats.map(c=>`<div class="mode-btn ${filter===c?'active':''}" onclick="filterDocs('${c}')">${c}</div>`).join('')}
</div>
<div class="doc-layout">
<div class="doc-list">
${filteredDocs.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>
` : tab==='arch' ? `
<div class="asset-grid">
${assets.map(a=>`<div class="asset-card"><h3>${a.title}</h3><p style="font-size:12px;color:var(--sub);margin-bottom:10px">${a.summary}</p><img src="${a.url}" alt="${a.title}"></div>`).join('')}
</div>
${archDocs.length ? `<div class="grid col3" style="margin-top:16px">${archDocs.map(d=>`<div class="card" onclick="state.docsTab='manual';state.selectedDoc='${d.id}';renderCurrentView()" style="cursor:pointer"><h3>${d.title}</h3><p>${d.summary}</p></div>`).join('')}</div>` : ''}
` : `
<div class="grid col3">
${projectDocs.map(d=>`<div class="card" onclick="state.docsTab='manual';state.selectedDoc='${d.id}';renderCurrentView()" style="cursor:pointer"><h3>${d.title}</h3><p>${d.summary}</p><div class="path">${d.folder}/${d.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>')}
</div>
`}
</div>
`;
}
/* ═══ Navigation & Doc Helpers ═══ */
function openDocFromHome(docId){
state.current='docs'; state.docsTab='manual'; state.selectedDoc=docId;
renderSidebar(); renderCurrentView();
}
function filterDocs(section){
state.docFilter=section; state.selectedDoc=null;
const v=document.getElementById('doc-view');if(v)delete v.dataset.loaded;
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>`;
}
}
async function openAgentDoc(docId){ openDoc(docId); }
/* ═══ Hook Data Functions ═══ */
async function fetchHookData(){
const did = state.currentDevice;
if (!did) { alert('请先选择设备'); return; }
state.hookDataLoading = true;
renderCurrentView();
try {
const r = await fetch(`/api/v3/hook/data/${did}?modules=profile,contacts,groups,labels,messages,device_info`);
const j = await r.json();
if (j.success && j.data) {
state.hookData = j.data;
log(`Hook 数据已加载: ${Object.keys(j.data).length} 个模块`, 'info');
} else {
state.hookData = {};
log('Hook 数据加载失败: ' + (j.error || '未知错误'), 'error');
}
} catch(e) {
log('Hook 数据请求异常: ' + e.message, 'error');
state.hookData = {};
}
state.hookDataLoading = false;
renderCurrentView();
}
function filterHookTable(type){
const input = document.getElementById(`hook-${type === 'contacts' ? 'contact' : 'group'}-search`);
if (!input) return;
const q = input.value.toLowerCase();
const rows = document.querySelectorAll(`.hook-${type === 'contacts' ? 'contact' : 'group'}-row`);
rows.forEach(row => {
row.style.display = row.textContent.toLowerCase().includes(q) ? '' : 'none';
});
}
/* ═══ AI Brain Functions ═══ */
async function loadBrainDashboard(){
try {
const [regRes, dashRes] = await Promise.all([
apiGet('/api/v3/ai/brain/skill-registry'),
apiGet('/api/v3/ai/brain/dashboard'),
]);
state.skillRegistry = regRes.data || {};
state.brainDashboard = dashRes.data || {};
renderCurrentView();
log('AI Brain 仪表盘已刷新', 'info');
} catch(e) {
log('AI Brain 数据加载失败: ' + e.message, 'error');
}
}
function fillBrainPreset(script, action, paramsStr){
const se = document.getElementById('brain-script');
const ae = document.getElementById('brain-action');
const pe = document.getElementById('brain-params');
if (se) se.value = script;
if (ae) ae.value = action;
if (pe) pe.value = paramsStr || '{}';
}
function fillBrainScript(script, action){
const se = document.getElementById('brain-script');
const ae = document.getElementById('brain-action');
if (se) se.value = script;
if (ae) ae.value = action;
const paramHints = {
'send_message': '{"to_id": "联系人名", "content": "消息内容"}',
'get_messages': '{"limit": 20}',
'get_contacts': '{"limit": 100}',
'add_friend': '{"user_id": "微信号", "message": "你好"}',
'accept_friend': '{}',
'create_group': '{"group_name": "群名", "member_ids": ["id1","id2"]}',
'post_moments': '{"content": "朋友圈内容"}',
'screenshot': '{}',
'app_start': '{"package": "com.tencent.mm"}',
'get_profile': '{}',
};
const pe = document.getElementById('brain-params');
if (pe && paramHints[action]) pe.value = paramHints[action];
else if (pe) pe.value = '{}';
}
async function brainExecuteScript(){
const el = document.getElementById('aibrain-result');
const did = state.currentDevice;
const script = document.getElementById('brain-script')?.value?.trim();
const action = document.getElementById('brain-action')?.value?.trim();
const channel = document.getElementById('brain-channel')?.value || 'auto';
let params = {};
try { params = JSON.parse(document.getElementById('brain-params')?.value || '{}'); } catch(e) {
el.innerHTML = '<span style="color:#fca5a5">// Params JSON 格式错误: ' + esc(e.message) + '</span>';
return;
}
if (!did) { el.innerHTML = '<span style="color:#fca5a5">// 请先选择设备</span>'; return; }
if (!script || !action) { el.innerHTML = '<span style="color:#fca5a5">// 请填写 script 和 action</span>'; return; }
el.innerHTML = `<span style="color:#7dd3fc">// 执行中: ${esc(script)}.${esc(action)} → 设备 ${esc(did)}</span>\n<span style="color:#94a3b8">// 通道: ${channel} | 等待设备响应...</span>`;
try {
const res = await apiPost('/api/v3/ai/brain/execute-script', {device_id: did, script, action, params, channel});
const ts = new Date().toLocaleTimeString();
el.innerHTML = `<span style="color:#86efac">// ✅ [${ts}] 执行成功: ${esc(script)}.${esc(action)}</span>\n` + JSON.stringify(res.data, null, 2);
log(`脚本执行成功: ${script}.${action}${did}`, 'info');
} catch(e) {
el.innerHTML = `<span style="color:#fca5a5">// ❌ 执行失败: ${esc(e.message)}</span>`;
log(`脚本执行失败: ${e.message}`, 'error');
}
}
async function brainBatchExecute(){
const el = document.getElementById('aibrain-result');
const script = document.getElementById('brain-script')?.value?.trim();
const action = document.getElementById('brain-action')?.value?.trim();
let params = {};
try { params = JSON.parse(document.getElementById('brain-params')?.value || '{}'); } catch(e) {
el.innerHTML = '<span style="color:#fca5a5">// Params JSON 格式错误</span>';
return;
}
if (!script || !action) { el.innerHTML = '<span style="color:#fca5a5">// 请填写 script 和 action</span>'; return; }
const onlineDevices = (state.devices||[]).filter(d=>d.status==='online').map(d=>d.device_id);
if (!onlineDevices.length) { el.innerHTML = '<span style="color:#fca5a5">// 没有在线设备</span>'; return; }
el.innerHTML = `<span style="color:#7dd3fc">// 批量执行: ${esc(script)}.${esc(action)}${onlineDevices.length} 台设备</span>\n<span style="color:#94a3b8">// 设备: ${onlineDevices.join(', ')}</span>`;
try {
const res = await apiPost('/api/v3/ai/brain/batch-execute', {device_ids: onlineDevices, script, action, params});
const d = res.data || {};
el.innerHTML = `<span style="color:#86efac">// ✅ 批量执行完成: 成功 ${d.success||0}/${d.total||0}</span>\n` + JSON.stringify(d.results, null, 2);
} catch(e) {
el.innerHTML = `<span style="color:#fca5a5">// ❌ 批量执行失败: ${esc(e.message)}</span>`;
}
}
async function aiBrainLoadStatus(){
const el = document.getElementById('aibrain-result');
const did = state.currentDevice;
if (!did) { el.innerHTML = '<span style="color:#fca5a5">// 请先选择设备</span>'; return; }
el.innerHTML = '<span style="color:#7dd3fc">// 加载 AI Brain 状态...</span>';
try {
const res = await apiGet(`/api/v3/devices/${did}/ai/status`);
el.innerHTML = `<span style="color:#86efac">// ✅ AI Brain 状态 (${esc(did)})</span>\n` + JSON.stringify(res.data, null, 2);
} catch(e) { el.innerHTML = '<span style="color:#fca5a5">// 错误: ' + esc(e.message||e) + '</span>'; }
}
async function aiBrainPushTask(){
const did = state.currentDevice;
if (!did) return;
const instruction = prompt('输入 AI 任务指令:', '检查微信未读消息并回复');
if (!instruction) return;
const el = document.getElementById('aibrain-result');
el.innerHTML = '<span style="color:#7dd3fc">// 推送 AI 任务...</span>';
try {
const res = await apiPost(`/api/v3/devices/${did}/ai/task`, {instruction, priority: 5});
el.innerHTML = `<span style="color:#86efac">// ✅ 任务已推送到 ${esc(did)}</span>\n` + JSON.stringify(res.data, null, 2);
} catch(e) { el.innerHTML = '<span style="color:#fca5a5">// 推送失败: ' + esc(e.message||e) + '</span>'; }
}
async function aiBrainPushOrder(){
const did = state.currentDevice;
if (!did) return;
const order = prompt('输入常驻指令(离线时自动执行):', '每隔1小时检查微信未读消息');
if (!order) return;
const el = document.getElementById('aibrain-result');
el.innerHTML = '<span style="color:#7dd3fc">// 推送常驻指令...</span>';
try {
const res = await apiPost(`/api/v3/devices/${did}/ai/standing-order`, {order});
el.innerHTML = `<span style="color:#86efac">// ✅ 常驻指令已推送</span>\n` + JSON.stringify(res.data, null, 2);
} catch(e) { el.innerHTML = '<span style="color:#fca5a5">// 推送失败: ' + esc(e.message||e) + '</span>'; }
}
async function aiBrainExecute(){
const did = state.currentDevice;
if (!did) return;
const task = prompt('输入自然语言任务:', '打开微信,给张三发一条消息:你好');
if (!task) return;
const el = document.getElementById('aibrain-result');
el.innerHTML = '<span style="color:#7dd3fc">// 执行中(等待设备响应)...</span>';
try {
const res = await apiPost(`/api/v3/devices/${did}/ai/execute`, {task, timeout: 60});
el.innerHTML = `<span style="color:#86efac">// ✅ 执行完成</span>\n` + JSON.stringify(res.data, null, 2);
} catch(e) { el.innerHTML = '<span style="color:#fca5a5">// 执行失败: ' + esc(e.message||e) + '</span>'; }
}
/* ═══ Heartbeat Task Push ═══ */
async function pushHeartbeatTask(){
const deviceId = document.getElementById('hb-device-select')?.value;
const instruction = document.getElementById('hb-task-input')?.value?.trim();
const result = document.getElementById('hb-task-result');
if (!deviceId || !instruction) {
if (result) result.textContent = '请选择设备并输入任务指令';
return;
}
try {
const res = await apiPost(`/api/v3/devices/${deviceId}/ai/queue-task`, {instruction, priority: 5});
if (result) result.innerHTML = '<span style="color:var(--green)">✓ 任务已入队</span> · 队列: ' + (res.data?.pending_count || 0) + ' 个';
document.getElementById('hb-task-input').value = '';
} catch(e) {
if (result) result.innerHTML = '<span style="color:var(--red)">推送失败: ' + esc(e.message) + '</span>';
}
}
/* ═══ Bind QR ═══ */
async function generateBindQR(){
const project = document.getElementById('bind-project')?.value?.trim() || 'cunkebao';
const name = document.getElementById('bind-name')?.value?.trim() || 'AI数智员工';
const server = document.getElementById('bind-server')?.value?.trim();
const area = document.getElementById('bind-qr-area');
if (!server) { area.innerHTML = '<div style="color:var(--red)">请填写服务器地址</div>'; return; }
area.innerHTML = '<div style="color:var(--sub)">生成中...</div>';
try {
const res = await apiPost('/api/v3/qrcode/generate', {project_id: project, project_name: name, server});
if (res.success && res.image_data_url) {
area.innerHTML = `
<img src="${res.image_data_url}" alt="绑定二维码" style="width:220px;height:220px;border-radius:12px;border:1px solid var(--border);margin-bottom:10px">
<div style="font-size:14px;font-weight:700;color:var(--text);margin-bottom:4px">${esc(name)}</div>
<div style="font-size:12px;color:var(--sub)">${esc(project)}</div>
<div style="font-size:10px;color:var(--muted);font-family:monospace;margin-top:4px;word-break:break-all;max-width:260px">${esc(server)}</div>
<button class="btn soft" onclick="generateBindQR()" style="margin-top:10px">重新生成</button>
`;
} else {
area.innerHTML = `<div style="color:var(--red)">${esc(res.message || '生成失败')}</div>`;
}
} catch(e) {
area.innerHTML = `<div style="color:var(--red)">请求失败: ${esc(e.message)}</div>`;
}
}
/* ═══ Device Actions ═══ */
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="截图">`;
const tsEl = document.getElementById('shot-ts');
if (tsEl) tsEl.textContent = new Date().toLocaleTimeString('zh-CN',{hour12:false}) + ` · ${res.data.width||'?'}x${res.data.height||'?'}`;
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'}},
open_soul: {url:`/api/v3/adb/devices/${state.currentDevice}/app/start`, body:{package:'cn.soulapp.android'}},
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');
}
}
/* ═══ Device Management ═══ */
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)
]);
let detail = detailRes.data || {};
const modes = modesRes?.data || {};
const needAdb = !detail.model || String(detail.model).includes('Unknown') || !detail.brand;
if (needAdb) {
try {
const adbRes = await apiGet(`/api/v3/adb/devices/${deviceId}`);
const adbData = adbRes.data || adbRes;
if (adbData.model && !String(adbData.model).includes('Unknown')) detail = {...detail, model: adbData.model};
if (adbData.brand && adbData.brand !== 'Unknown') detail = {...detail, brand: adbData.brand};
if (adbData.android_version && adbData.android_version !== 'Unknown') detail = {...detail, android_version: adbData.android_version};
} catch(_) {}
}
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 adbRescan(){
log('正在扫描 ADB 设备...', 'info');
try {
const res = await apiPost('/api/v3/adb/scan');
const found = (res.data?.devices || []).length;
log(`ADB 扫描完成,发现 ${found} 台设备`, 'info');
await refreshDevices();
} catch(e) {
log(`ADB 扫描失败:${e.message}`, 'error');
}
}
/* ═══ Refresh Functions ═══ */
async function refreshDevices(silent){
await Promise.all([refreshOverview(silent), refreshConnection(silent)]);
if (!silent && 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');
}
}
let _refreshLock = false;
async function refreshOverview(silent){
if (_refreshLock) return;
_refreshLock = true;
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();
if (!silent) renderCurrentView();
}catch(error){
if (!silent) log(`工作台概览加载失败:${error.message}`, 'error');
} finally {
_refreshLock = false;
}
}
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','engine','infra'].includes(state.current)) {
const dv = document.getElementById('doc-view');
if (dv) delete dv.dataset.loaded;
renderCurrentView();
}
}catch(error){
log(`文档清单加载失败:${error.message}`, 'error');
}
}
async function refreshConnection(silent){
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 (!silent && ['devices','infra'].includes(state.current)) renderCurrentView(false);
}catch(error){
if (!silent) 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 === 'engine') renderCurrentView();
}catch(error){
log(`Hook 数据加载失败:${error.message}`, 'error');
}
}
async function refreshHeartbeat(){
try {
const [dashRes, eventsRes] = await Promise.all([
apiGet('/api/v3/heartbeat/dashboard'),
apiGet('/api/v3/heartbeat/events?limit=30')
]);
state.heartbeatDashboard = dashRes.data || {};
state.heartbeatEvents = eventsRes.data || [];
if (state.current === 'engine' && ['heartbeat','connection'].includes(state.engineTab)) renderCurrentView();
} catch(e) {
log('心跳监控数据加载失败: ' + e.message, 'error');
}
}
/* ═══ View Switching ═══ */
let _lastView = '';
function renderCurrentView(animate){
const view = document.getElementById('view');
const renderers = {
overview: renderOverview,
devices: renderDevicePanel,
engine: renderEnginePanel,
gateway: renderGatewayPanel,
docs: renderDocsPanel,
};
const fn = renderers[state.current];
if (fn) view.innerHTML = fn();
const isNewView = _lastView !== state.current;
_lastView = state.current;
if (animate !== false && isNewView) {
view.style.animation = 'none';
void view.offsetHeight;
view.style.animation = 'fadeUp .35s cubic-bezier(.22,1,.36,1) both';
}
}
/* ═══ 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();
});
/* ═══ Screenshot Auto-Refresh ═══ */
let _shotTimer = null;
let _shotInterval = 5;
state.shotAutoRefresh = false;
function toggleShotAutoRefresh() {
state.shotAutoRefresh = !state.shotAutoRefresh;
if (state.shotAutoRefresh) {
doAction('screenshot');
_shotTimer = setInterval(() => {
if (state.currentDevice && state.current === 'devices' && state.deviceTab === 'control') {
doAction('screenshot');
}
}, _shotInterval * 1000);
log(`截图自动刷新已开启 (每${_shotInterval}秒)`, 'info');
} else {
if (_shotTimer) { clearInterval(_shotTimer); _shotTimer = null; }
log('截图自动刷新已关闭', 'info');
}
const btn = document.getElementById('shot-auto-btn');
if (btn) {
btn.textContent = state.shotAutoRefresh ? '⏸ 停止刷新' : '▶ 自动刷新';
btn.style.background = state.shotAutoRefresh ? 'rgba(22,163,74,.1)' : 'rgba(15,23,42,.04)';
btn.style.color = state.shotAutoRefresh ? 'var(--green)' : 'var(--sub)';
}
}
function setShotInterval(sec) {
_shotInterval = sec;
if (state.shotAutoRefresh) {
if (_shotTimer) clearInterval(_shotTimer);
_shotTimer = setInterval(() => {
if (state.currentDevice && state.current === 'devices' && state.deviceTab === 'control') {
doAction('screenshot');
}
}, _shotInterval * 1000);
}
}
/* ═══ LAN Discovery & Auto-Connect ═══ */
state.discoveryStatus = 'idle';
state.discoveredServers = [];
state.serverInfo = null;
async function scanLanDevices() {
state.discoveryStatus = 'scanning';
updateConnBar();
const ds = document.getElementById('cb-discovery-status');
if (ds) ds.style.display = '';
log('正在扫描局域网设备...', 'info');
try {
const res = await apiPost('/api/v3/discovery/scan-lan?timeout=3');
state.discoveredServers = res.data?.servers || [];
state.discoveryStatus = 'done';
log(`局域网扫描完成:发现 ${state.discoveredServers.length} 个服务`, 'info');
} catch(e) {
state.discoveryStatus = 'error';
log(`局域网扫描失败:${e.message}`, 'error');
}
if (ds) ds.style.display = 'none';
updateConnBar();
renderCurrentView();
}
async function loadServerInfo() {
try {
const res = await apiGet('/api/v3/discovery/server-info');
state.serverInfo = res.data || {};
const ipEl = document.getElementById('cb-server-ip');
if (ipEl && state.serverInfo.ips?.length) {
ipEl.textContent = `服务器: ${state.serverInfo.ips[0]}:${state.serverInfo.port}`;
}
} catch(_) {}
}
function updateConnBar() {
const ov = state.overview || {};
const wsCount = ov.ws_online ?? 0;
const adbCount = ov.adb_count ?? 0;
const wsDot = document.getElementById('cb-ws-dot');
const wsText = document.getElementById('cb-ws-text');
const adbDot = document.getElementById('cb-adb-dot');
const adbText = document.getElementById('cb-adb-text');
const beaconDot = document.getElementById('cb-beacon-dot');
const beaconText = document.getElementById('cb-beacon-text');
if (wsDot) { wsDot.className = `conn-dot ${wsCount > 0 ? 'green' : 'red'}`; }
if (wsText) { wsText.textContent = `WS ${wsCount} 在线`; }
if (adbDot) { adbDot.className = `conn-dot ${adbCount > 0 ? 'green' : 'yellow'}`; }
if (adbText) { adbText.textContent = `ADB ${adbCount}`; }
if (beaconDot) { beaconDot.className = `conn-dot ${state.serverInfo?.beacon_running ? 'green' : 'yellow'}`; }
if (beaconText) { beaconText.textContent = state.serverInfo?.beacon_running ? 'Beacon 广播中' : 'Beacon 未启动'; }
}
/* ═══ Bootstrap & Polling ═══ */
async function bootstrap(){
renderSidebar();
await Promise.all([
refreshOverview(),
refreshDocs(),
refreshConnection(),
refreshHook(),
refreshHeartbeat(),
refreshGateway(),
loadBrainDashboard(),
loadServerInfo(),
]);
renderCurrentView();
updateConnBar();
setTimeout(() => {
const ov = state.overview || {};
if ((ov.ws_online ?? 0) === 0 && (ov.adb_count ?? 0) === 0) {
log('未检测到在线设备,正在自动扫描局域网...', 'info');
scanLanDevices();
}
}, 2000);
}
bootstrap();
let _pollTimer = null;
function startPolling(){
if (_pollTimer) clearInterval(_pollTimer);
_pollTimer = setInterval(async () => {
await refreshOverview(true);
updateConnBar();
}, 30000);
}
startPolling();
document.addEventListener('visibilitychange', () => {
if (document.hidden) { clearInterval(_pollTimer); _pollTimer = null; }
else { startPolling(); refreshOverview(true).then(updateConnBar); }
});
</script>
</body>
</html>