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

4446 lines
281 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>工作手机控制台</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(3,minmax(0,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{width:100%;padding:16px 12px;border-radius:14px;border:1px solid var(--border);background:var(--panel-strong);text-align:center;transition:all .25s var(--ease);cursor:pointer;font:inherit;color:inherit}
.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}
.capability-hub{position:relative;overflow:hidden;background:linear-gradient(135deg,#f8fbff 0%,#eef7ff 52%,#f0fbfa 100%);border:1px solid rgba(37,99,235,.14)}
.capability-hub:before{content:'';position:absolute;width:260px;height:260px;right:-110px;top:-150px;border-radius:50%;background:rgba(14,165,166,.11);pointer-events:none}
.capability-hub-head{position:relative;display:flex;align-items:center;justify-content:space-between;gap:12px}
.capability-hub-kicker{display:flex;align-items:center;gap:7px;color:var(--blue);font-size:11px;font-weight:750;letter-spacing:.04em}
.capability-hub-kicker i{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(22,163,74,.12)}
.capability-hub-title{font-size:20px;font-weight:800;letter-spacing:-.02em;margin-top:5px}
.capability-hub-sub{font-size:11px;color:var(--sub);margin-top:4px}
.capability-hub-status{padding:7px 10px;border-radius:20px;background:rgba(255,255,255,.82);border:1px solid rgba(37,99,235,.12);font-size:10px;color:var(--sub);white-space:nowrap}
.capability-grid{position:relative;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-top:18px}
.capability-card{position:relative;display:flex;flex-direction:column;min-height:174px;padding:16px;border:1px solid rgba(148,163,184,.23);border-radius:18px;background:rgba(255,255,255,.86);box-shadow:0 8px 22px rgba(37,99,235,.055);text-align:left;cursor:pointer;transition:transform .2s var(--ease),box-shadow .2s var(--ease),border-color .2s var(--ease)}
.capability-card:hover{transform:translateY(-3px);border-color:rgba(37,99,235,.35);box-shadow:0 14px 28px rgba(37,99,235,.12)}
.capability-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
.capability-icon{width:42px;height:42px;display:grid;place-items:center;border-radius:13px;font-size:22px;background:linear-gradient(135deg,rgba(37,99,235,.12),rgba(14,165,166,.12))}
.capability-index{font-size:10px;color:var(--muted);font-weight:750;letter-spacing:.08em}
.capability-name{margin-top:14px;font-size:15px;font-weight:800;color:var(--text)}
.capability-desc{margin-top:5px;min-height:30px;font-size:10.5px;line-height:1.5;color:var(--sub)}
.capability-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:auto;padding-top:12px}
.capability-state{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--sub)}
.capability-state i{width:6px;height:6px;border-radius:50%;background:var(--muted)}
.capability-state.ready{color:var(--green)} .capability-state.ready i{background:var(--green);box-shadow:0 0 0 3px rgba(22,163,74,.1)}
.capability-enter{font-size:10px;color:var(--blue);font-weight:750}
.capability-note{position:relative;margin-top:12px;padding:9px 11px;border-radius:11px;background:rgba(255,255,255,.62);font-size:10px;color:var(--sub)}
@media(max-width:720px){.capability-grid{grid-template-columns:1fr}.capability-card{min-height:140px}.capability-hub-title{font-size:18px}}
.phone-console-card{padding:16px;border:1px solid rgba(37,99,235,.15);border-radius:18px;background:linear-gradient(145deg,#fff 0%,#f6faff 100%);box-shadow:0 8px 24px rgba(37,99,235,.05)}
.phone-console-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px}
.phone-console-title{font-size:14px;font-weight:800}.phone-console-sub{font-size:10px;color:var(--sub);margin-top:3px}
.phone-console-state{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--green);font-weight:700}.phone-console-state i{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px rgba(22,163,74,.1)}
.phone-primary-actions{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
.phone-primary-action{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;min-height:72px;border:1px solid var(--border);border-radius:13px;background:#fff;color:var(--text);font-size:10.5px;font-weight:700;cursor:pointer;transition:all .18s}
.phone-primary-action:hover{border-color:rgba(37,99,235,.38);background:rgba(37,99,235,.05);transform:translateY(-2px)}
.phone-primary-action b{font-size:21px;font-weight:500}.phone-more{margin-top:10px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.72);overflow:hidden}.phone-more summary{list-style:none;padding:10px 12px;cursor:pointer;font-size:11px;font-weight:750;color:var(--text)}.phone-more summary::-webkit-details-marker{display:none}.phone-more summary:after{content:'';float:right;color:var(--muted);font-size:14px}.phone-more[open] summary{color:var(--blue);border-bottom:1px solid var(--border)}.phone-more[open] summary:after{content:''}.phone-more-body{padding:12px}
@media(max-width:720px){.phone-primary-actions{grid-template-columns:repeat(2,1fr)}}
.phone-live-body{display:grid;grid-template-columns:minmax(0,1fr) 92px;gap:10px;align-items:start}
.phone-live-actions{display:flex;flex-direction:column;gap:6px}
.phone-live-action{display:flex;flex-direction:column;align-items:center;gap:3px;padding:8px 3px;border:1px solid var(--border);border-radius:10px;background:#fff;color:var(--text);font-size:9px;font-weight:700;cursor:pointer;transition:all .15s}
.phone-live-action b{font-size:16px;font-weight:500}.phone-live-action:hover{border-color:rgba(37,99,235,.35);background:rgba(37,99,235,.05)}
@media(max-width:720px){.phone-live-body{grid-template-columns:1fr}.phone-live-actions{display:grid;grid-template-columns:repeat(4,1fr)}}
.device-admin-actions{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
.device-admin-btn{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 6px;border:1px solid var(--border);border-radius:11px;background:#fff;color:var(--text);font-size:10.5px;font-weight:700;cursor:pointer;transition:all .15s}.device-admin-btn b{font-size:16px;font-weight:500}.device-admin-btn:hover{border-color:rgba(37,99,235,.35);background:rgba(37,99,235,.05)}
.service-summary-row{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}
.service-chip{display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border-radius:20px;background:rgba(148,163,184,.1);color:var(--sub);font-size:9.5px}.service-chip i{width:6px;height:6px;border-radius:50%;background:var(--muted)}.service-chip.ready{color:var(--green);background:rgba(22,163,74,.08)}.service-chip.ready i{background:var(--green)}
@media(max-width:720px){.device-admin-actions{grid-template-columns:repeat(2,1fr)}}
/* 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}
/* application control: one app per entry, details revealed on demand */
.app-control-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}
@media(max-width:900px){.app-control-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(max-width:560px){.app-control-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
.app-control-card{display:flex;align-items:center;gap:10px;padding:13px 12px;border:1px solid var(--border);border-radius:15px;background:rgba(255,255,255,.82);cursor:pointer;text-align:left;transition:all .2s var(--ease)}
.app-control-card:hover{border-color:var(--border-active);transform:translateY(-2px);box-shadow:0 8px 18px rgba(37,99,235,.08)}
.app-control-card.active{border-color:rgba(37,99,235,.35);background:linear-gradient(135deg,rgba(37,99,235,.1),rgba(14,165,166,.08));box-shadow:0 0 0 2px rgba(37,99,235,.08)}
.app-control-icon{width:36px;height:36px;border-radius:11px;display:grid;place-items:center;font-size:19px;background:rgba(15,23,42,.05);flex-shrink:0}
.app-control-name{font-size:12px;font-weight:750;color:var(--text)}
.app-control-desc{font-size:10px;color:var(--sub);margin-top:2px;line-height:1.35}
.app-control-panel{margin-top:12px;padding:14px;border:1px solid rgba(37,99,235,.14);border-radius:16px;background:rgba(248,250,252,.72)}
.app-control-panel-head{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:12px}
.app-control-panel-title{font-size:14px;font-weight:750}
.app-control-panel-sub{font-size:10.5px;color:var(--sub);margin-top:3px}
.app-entry-focus{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid rgba(37,99,235,.24);border-radius:16px;background:linear-gradient(135deg,rgba(37,99,235,.1),rgba(14,165,166,.08));margin-bottom:10px}
.app-entry-focus .app-control-icon{width:44px;height:44px;font-size:23px;background:#fff;box-shadow:0 4px 12px rgba(37,99,235,.12)}
.app-entry-focus .app-control-name{font-size:14px}
.app-entry-focus .app-control-desc{font-size:10.5px}
.app-entry-focus .btn{margin-left:auto;white-space:nowrap;font-size:11px;padding:7px 12px}
.app-entry-nav{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}
.app-entry-nav .app-control-card{padding:8px 9px;gap:7px;border-radius:11px}
.app-entry-nav .app-control-icon{width:27px;height:27px;border-radius:8px;font-size:15px}
.app-entry-nav .app-control-name{font-size:10.5px}
.app-entry-nav .app-control-desc{display:none}
.wx-result-card{font-size:11px;color:var(--text);line-height:1.55}
.wx-result-card .wx-result-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;font-weight:750}
.wx-result-card .wx-result-badge{font-size:9px;color:var(--green);background:rgba(22,163,74,.1);padding:2px 7px;border-radius:20px}
.wx-result-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
.wx-result-item{padding:7px 8px;background:rgba(37,99,235,.045);border-radius:8px;min-width:0}
.wx-result-item small{display:block;color:var(--sub);font-size:9px;margin-bottom:2px}
.wx-result-item b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wx-result-list{display:flex;flex-direction:column;gap:5px;max-height:180px;overflow:auto}
.wx-result-row{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 8px;border:1px solid var(--border);border-radius:8px;background:#fff}
.wx-result-row small{color:var(--sub);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wx-result-empty{padding:10px;border-radius:8px;background:rgba(245,158,11,.08);color:#92400e;font-size:10.5px}
.wx-result-error{padding:9px 10px;border-radius:8px;background:rgba(220,38,38,.07);color:var(--red);font-size:10.5px;line-height:1.55}
.app-fold{border:1px solid var(--border);border-radius:13px;background:rgba(255,255,255,.8);margin-top:9px;overflow:hidden}
.app-fold summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:11px 13px;cursor:pointer;font-size:11.5px;font-weight:700;color:var(--text)}
.app-fold summary::-webkit-details-marker{display:none}
.app-fold summary::after{content:'';color:var(--muted);font-size:15px;font-weight:400}
.app-fold[open] summary{border-bottom:1px solid var(--border);color:var(--blue)}
.app-fold[open] summary::after{content:''}
.app-fold-body{padding:12px}
.app-action-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}
@media(max-width:720px){.app-action-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
.app-action-btn{display:flex;align-items:center;gap:8px;padding:10px 9px;border:1px solid var(--border);border-radius:11px;background:#fff;color:var(--text);cursor:pointer;font-size:11px;text-align:left;transition:all .15s}
.app-action-btn:hover{border-color:var(--border-active);background:rgba(37,99,235,.05);transform:translateY(-1px)}
.app-action-btn .icon{font-size:17px;width:22px;text-align:center}
.app-disabled{opacity:.5;pointer-events:none}
/* 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}
/* realtime device control */
.shot{aspect-ratio:9/19.5;border-radius:30px;overflow:hidden;border:7px solid #111827;background:#0b1220;display:flex;align-items:center;justify-content:center;box-shadow:0 18px 42px rgba(15,23,42,.22),inset 0 0 0 1px rgba(255,255,255,.08);position:relative}
.shot img{display:block;width:auto;max-width:100%;height:100%;object-fit:contain;user-select:none;-webkit-user-drag:none}
.shot canvas{display:block;width:auto;max-width:100%;height:100%;object-fit:contain;user-select:none}
.shot .live-screen-fallback{display:none}
.live-device-card{position:sticky;top:12px;background:linear-gradient(180deg,rgba(255,255,255,.98),rgba(248,250,252,.96))}
.live-device-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}
.live-device-title{display:flex;align-items:center;gap:7px;font-size:13px;font-weight:800}
.live-device-status{display:inline-flex;align-items:center;gap:5px;padding:4px 9px;border-radius:999px;background:rgba(22,163,74,.1);color:var(--green);font-size:10px;font-weight:700}
.live-device-status::before{content:'';width:6px;height:6px;border-radius:50%;background:currentColor;box-shadow:0 0 0 4px rgba(22,163,74,.1)}
.live-device-status.waiting{background:rgba(245,158,11,.1);color:#b45309}
.live-device-status.error{background:rgba(220,38,38,.08);color:var(--red)}
.live-device-hint{text-align:center;font-size:10px;color:var(--muted);margin:8px 0 2px}
.live-device-tools{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin-top:10px}
.live-device-tools .phone-live-action{min-height:50px}
.device-control-layout{display:grid;grid-template-columns:minmax(390px,460px) minmax(0,1fr);gap:12px;margin-top:10px;align-items:start}
.device-live-column{order:1;display:flex;flex-direction:column;gap:8px;min-width:0}
.device-tools-column{order:2;display:flex;flex-direction:column;gap:8px;min-width:0}
.device-tools-column>.card,.device-tools-column>.phone-console-card{padding:10px}
.device-tools-column .phone-console-card{border-radius:14px}
.device-tools-column .phone-console-head{margin-bottom:8px}
.device-tools-column .device-admin-actions{gap:6px}
.device-tools-column .device-admin-btn{padding:8px 5px;font-size:10px}
.device-tools-column .service-summary-row{margin-top:7px;padding-top:7px;gap:4px}
.device-tools-column .service-chip{font-size:9px;padding:3px 7px}
.control-section{border:1px solid var(--border);border-radius:14px;background:rgba(255,255,255,.9);overflow:hidden;box-shadow:0 4px 14px rgba(15,23,42,.035)}
.control-section>summary{list-style:none;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 13px;cursor:pointer;font-size:12px;font-weight:750;color:var(--text);transition:background .16s}
.control-section>summary::-webkit-details-marker{display:none}
.control-section>summary:hover{background:rgba(37,99,235,.045)}
.control-section>summary::after{content:'';font-size:17px;font-weight:400;color:var(--muted)}
.control-section[open]>summary{color:var(--blue);border-bottom:1px solid var(--border)}
.control-section[open]>summary::after{content:''}
.control-section-body{padding:11px 13px}
.control-section-kicker{font-size:9.5px;font-weight:600;color:var(--sub);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.control-section .phone-console-head{margin:0 0 9px}.control-section .phone-console-title{font-size:12px}.control-section .phone-console-sub{font-size:9.5px}
.control-section .app-entry-nav{margin-bottom:10px!important}.control-section .app-action-grid{gap:6px}
.control-section .app-action-btn{padding:8px 7px;font-size:10px}.control-section .app-action-btn .icon{font-size:15px}
.device-summary-main{display:flex;align-items:center;gap:9px}
.device-summary-icon{width:38px;height:38px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:18px;color:#fff;flex:0 0 auto}
.device-summary-copy{flex:1;min-width:0}
.device-summary-name{font-size:14px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.device-summary-id{font-size:9.5px;color:var(--sub);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.device-summary-state{text-align:right;flex:0 0 auto}
.device-metric-strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:5px;margin-top:8px}
.device-mini-metric{padding:6px;border-radius:9px;background:rgba(37,99,235,.045);text-align:center;min-width:0}
.device-mini-metric small{display:block;font-size:8.5px;color:var(--sub);margin-bottom:1px}
.device-mini-metric b{display:block;font-size:10.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.compact-device-more{margin-top:7px;border-top:1px solid var(--border)}
.compact-device-more summary{list-style:none;cursor:pointer;padding:7px 1px 0;font-size:9.5px;color:var(--sub);font-weight:700}
.compact-device-more summary::-webkit-details-marker{display:none}
.compact-device-more summary::after{content:'';float:right;color:var(--muted)}
.compact-device-more[open] summary::after{content:''}
.compact-device-body{padding-top:7px}
.compact-meta-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px;margin-top:6px}
.compact-meta-item{padding:6px 7px;border-radius:8px;background:rgba(15,23,42,.035);min-width:0}
.compact-meta-item small{display:block;font-size:8px;color:var(--muted)}
.compact-meta-item b{display:block;font-size:9px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.device-detail-header{padding:10px 14px!important}
.device-detail-header h2{font-size:14px}
.device-detail-header .sub{font-size:9.5px;margin-top:1px}
.control-feedback{box-shadow:0 0 0 4px rgba(37,99,235,.14)!important}
.phone-live-action:active,.device-admin-btn:active,.app-control-card:active,.app-action-btn:active{transform:scale(.96)}
@media(max-width:1100px){
.live-device-card{position:static}
.device-control-layout{grid-template-columns:minmax(340px,410px) minmax(0,1fr)}
}
@media(max-width:860px){
.device-control-layout{grid-template-columns:1fr}
.device-live-column,.device-tools-column{order:initial}
.device-live-column{order:1}.device-tools-column{order:2}
}
.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}
.device-fleet-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:8px}
.device-fleet-strip>div{margin-bottom:0!important;min-height:86px}
@media(max-width:980px){.device-fleet-strip{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){.device-fleet-strip{grid-template-columns:1fr}}
.phone-device-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:14px;margin-top:16px}
.phone-device-card{position:relative;display:flex;min-height:320px;flex-direction:column;align-items:center;padding:22px 16px 16px;border:1px solid var(--border);border-radius:22px;background:rgba(255,255,255,.82);box-shadow:var(--shadow);cursor:pointer;color:var(--text);transition:all .28s var(--ease)}
.phone-device-card:hover{transform:translateY(-4px);border-color:var(--border-active);box-shadow:var(--shadow-lg)}
.phone-online-dot{position:absolute;right:16px;top:16px;width:9px;height:9px;border-radius:50%;box-shadow:0 0 0 5px rgba(22,163,74,.09)}
.phone-shell{position:relative;width:76px;height:138px;padding:9px 6px 7px;border:4px solid #172033;border-radius:20px;background:#172033;box-shadow:0 16px 30px rgba(15,23,42,.16)}
.phone-speaker{position:absolute;z-index:2;left:50%;top:5px;width:22px;height:3px;transform:translateX(-50%);border-radius:4px;background:#64748b}
.phone-screen{display:flex;width:100%;height:100%;flex-direction:column;align-items:center;justify-content:center;gap:6px;border-radius:12px;background:linear-gradient(145deg,#eff6ff,#ecfeff);font-size:10px;font-weight:700;color:var(--blue)}
.phone-screen-icon{font-size:27px}.phone-device-card strong{margin-top:16px;font-size:14px}.phone-device-card small{max-width:100%;margin-top:5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--muted);font-size:10px}
.phone-tags{display:flex;flex-wrap:wrap;justify-content:center;gap:5px;margin-top:11px}.phone-tags i{padding:4px 7px;border-radius:999px;background:rgba(15,23,42,.04);font-size:9px;font-style:normal;color:var(--sub)}
.phone-enter{margin-top:auto;padding-top:14px;font-size:10px;font-weight:700;color:var(--blue)}
.overview-hero{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:26px 28px;background:linear-gradient(125deg,rgba(255,255,255,.92),rgba(239,246,255,.88))}.overview-hero h2{font-size:26px;margin:5px 0 4px;letter-spacing:-.04em}.overview-hero p{font-size:12px;color:var(--sub)}
.section-eyebrow{font-size:10px;font-weight:800;letter-spacing:.12em;color:var(--blue)}.section-title{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.section-title h3{font-size:16px;margin-top:3px}
.overview-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:14px}.overview-metric{display:flex;align-items:center;gap:14px;padding:18px}.overview-metric>span{display:grid;width:42px;height:42px;place-items:center;border-radius:13px;background:rgba(37,99,235,.08);color:var(--blue)}.overview-metric small,.overview-metric strong,.overview-metric p{display:block}.overview-metric small{font-size:10px;color:var(--sub)}.overview-metric strong{font-size:25px;line-height:1.2;margin-top:2px}.overview-metric p{font-size:9px;color:var(--muted);margin-top:2px}
.overview-quick{margin-top:14px;padding:20px}.overview-quick-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.overview-quick-grid button{display:flex;min-height:86px;flex-direction:column;align-items:flex-start;justify-content:center;padding:14px;border:1px solid var(--border);border-radius:15px;background:rgba(255,255,255,.84);cursor:pointer;text-align:left;transition:.2s}.overview-quick-grid button:hover{transform:translateY(-2px);border-color:var(--border-active)}.overview-quick-grid i{font-size:20px;font-style:normal}.overview-quick-grid b{font-size:12px;margin-top:7px}.overview-quick-grid small{font-size:9px;color:var(--muted);margin-top:3px}
.overview-columns{display:grid;grid-template-columns:1.15fr .85fr;gap:14px;margin-top:14px}.overview-columns>.panel{margin:0}.overview-columns .grid.col2{grid-template-columns:1fr}.overview-device-list{display:grid;gap:7px}.overview-device-row{display:grid;grid-template-columns:38px minmax(0,1fr) auto auto;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--border);border-radius:12px;background:rgba(255,255,255,.78);cursor:pointer;text-align:left}.overview-device-icon{display:grid;width:36px;height:36px;place-items:center;border-radius:11px;background:rgba(37,99,235,.07)}.overview-device-row b,.overview-device-row small{display:block}.overview-device-row b{font-size:11px}.overview-device-row small{font-size:9px;color:var(--muted);margin-top:2px}.overview-device-row i{font-size:10px;font-style:normal}.overview-device-row em{font-size:9px;font-style:normal;color:var(--blue)}
.engine-nav-groups{display:grid;grid-template-columns:1fr 1.2fr 1.6fr;gap:10px}.engine-nav-group{padding:10px;border-radius:14px;background:rgba(15,23,42,.025)}.engine-nav-group>small{display:block;padding:0 3px 7px;font-size:9px;font-weight:800;color:var(--muted);letter-spacing:.1em}.engine-nav-actions{display:flex;flex-wrap:wrap;gap:5px}.engine-nav-actions .mode-btn{margin:0;flex:1;min-width:max-content;text-align:center}
@media(max-width:1050px){.overview-metrics,.overview-quick-grid{grid-template-columns:repeat(2,1fr)}.overview-columns,.engine-nav-groups{grid-template-columns:1fr}}
@media(max-width:640px){.overview-hero{align-items:flex-start;flex-direction:column}.overview-metrics,.overview-quick-grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="app">
<header class="topbar glass">
<div class="brand">
<div class="brand-icon">⚙️</div>
<div>
<h1>工作手机控制台</h1>
<p>多手机统一管控 · 设备管理 · 智能引擎 · 对接网关 · 知识库</p>
</div>
</div>
<div class="chips">
<button class="chip" id="chip-auth" onclick="toggleConsoleAuth()" style="cursor:pointer;border:0">🔐 控制台登录</button>
<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:'数据 · 快捷入口 · 扫码'},
{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,
deviceView: 'list',
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',
bindingServer: '',
selectedAgentDoc: null,
integrationManifest: null,
integrationManifestLoading: false,
integrationManifestError: '',
realtime: {devices: [], recent_events: []},
realtimeError: '',
antibanStatus: null,
aiConfig: null,
aiConfigLoading: false,
};
/* ═══ 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.getElementById('device-action-logs') || document.querySelector('.log-box:not(#wx-result)');
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 ═══ */
let consoleAuthPromise = null;
let consoleAuthenticated = false;
function updateConsoleAuthChip(ok){
const chip = document.getElementById('chip-auth');
if (!chip) return;
consoleAuthenticated = ok;
chip.textContent = ok ? '🔐 已登录 · 退出' : '🔓 登录';
chip.style.color = ok ? 'var(--green)' : 'var(--red)';
}
async function ensureConsoleAuthenticated(){
if (consoleAuthPromise) return consoleAuthPromise;
consoleAuthPromise = (async () => {
const res = await fetch(`${API}/api/v3/console/session`, {credentials:'same-origin'});
if (res.ok) {
updateConsoleAuthChip(true);
return true;
}
updateConsoleAuthChip(false);
return false;
})().finally(() => { consoleAuthPromise = null; });
return consoleAuthPromise;
}
async function loginConsole(){
const username = document.getElementById('console-username')?.value?.trim() || '';
const password = document.getElementById('console-password')?.value || '';
const error = document.getElementById('console-login-error');
if (!username || !password) { if (error) error.textContent = '请输入账号和密码'; return; }
const res = await fetch(`${API}/api/v3/console/login`, {
method:'POST', credentials:'same-origin', headers:{'Content-Type':'application/json'},
body:JSON.stringify({username,password}),
});
if (!res.ok) { if (error) error.textContent = '账号或密码错误'; return; }
updateConsoleAuthChip(true);
await bootstrap();
startPolling();
}
async function logoutConsole(){
await fetch(`${API}/api/v3/console/logout`, {method:'POST', credentials:'same-origin'});
clearInterval(_pollTimer); _pollTimer = null;
updateConsoleAuthChip(false);
await bootstrap();
}
async function toggleConsoleAuth(){
if (consoleAuthenticated) await logoutConsole();
else {
await bootstrap();
document.getElementById('console-username')?.focus();
}
}
async function authenticatedFetch(url, options={}){
if (!consoleAuthenticated && !await ensureConsoleAuthenticated()) {
throw new Error('控制台未认证');
}
const headers = new Headers(options.headers || {});
const res = await fetch(url, {...options, headers, credentials:'same-origin'});
if (res.status === 401) {
updateConsoleAuthChip(false);
}
return res;
}
async function apiErrorFromResponse(res){
const text = await res.text().catch(() => '');
let message = `请求失败: ${res.status}`;
try {
const obj = JSON.parse(text);
if (obj && typeof obj === 'object') {
message = obj.message || obj.detail || obj.error || message;
}
} catch (_e) {}
const err = new Error(message);
err.status = res.status;
err.body = text;
return err;
}
async function apiGet(url){
const res = await authenticatedFetch(`${API}${url}`);
if(!res.ok) throw await apiErrorFromResponse(res);
return res.json();
}
async function apiPost(url, body){
const res = await authenticatedFetch(`${API}${url}`, {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(body || {})
});
if(!res.ok) throw await apiErrorFromResponse(res);
return res.json();
}
async function apiDelete(url){
const res = await authenticatedFetch(`${API}${url}`, {method:'DELETE'});
if(!res.ok) throw await apiErrorFromResponse(res);
return res.json();
}
async function apiUpload(url, file){
const form = new FormData();
form.append('file', file);
const res = await authenticatedFetch(`${API}${url}`, {method:'POST', body:form});
if(!res.ok) throw await apiErrorFromResponse(res);
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 = ov.device_online ?? (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('📷 扫码设备', wsCount, wsCount > 0 ? '无线控制已连接' : '等待手机扫码连接')}
${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 = `扫码 ${wsCount}`;
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.deviceView = 'list'; }
else if (id === 'devices') { state.current = 'devices'; state.deviceView = 'list'; }
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>`;
}
function deviceControlCards(modes){
const list = Array.isArray(modes) ? modes : [];
const find = (...ids) => list.find(mode => ids.includes(mode.id));
const hook = find('hook');
const agent = find('agent','u2');
const adb = find('adb');
return [
{
id:'data', icon:'🪝', name:'数据能力',
desc:'微信资料、好友、群聊、标签',
source:hook, status:hook?.available?'已连接':'等待 Hook',
detail:hook?.available ? (hook.detail || 'Hook / Frida 已就绪') : '需要 Root + Frida Server'
},
{
id:'ui', icon:'🖱️', name:'交互操作',
desc:'打开应用、点击、输入、朋友圈',
source:agent, status:agent?.available?'已连接':'等待 Agent',
detail:agent?.available ? (agent.detail || 'Agent WebSocket 已就绪') : '需要手机端工作 App'
},
{
id:'device', icon:'🔗', name:'高级连接',
desc:'Agent、无障碍、ADB、注入引擎',
source:agent?.available ? agent : adb, status:agent?.available?'已连接':(adb?.available?'ADB兜底':'待配置'),
detail:agent?.available ? '无线主控与系统服务统一管理' : (adb?.detail || '等待设备服务上线')
}
];
}
function openControlCapability(type){
if(type === 'data'){
state.current = 'engine';
state.engineTab = 'hook';
} else if(type === 'ui'){
state.current = 'devices';
state.deviceView = 'detail';
} else {
state.current = 'devices';
state.deviceView = 'detail';
}
renderSidebar();
renderCurrentView();
}
/* ═══ 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 = ov.device_online ?? (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>` : ''}
<!-- Capability Hub -->
<div class="panel glass capability-hub">
<div class="capability-hub-head">
<div>
<div class="capability-hub-kicker"><i></i> DEVICE CONTROL</div>
<div class="capability-hub-title">设备能力中心</div>
<div class="capability-hub-sub">按使用目的选择,不需要理解底层技术。</div>
</div>
<div style="display:flex;align-items:center;gap:7px">
<span class="capability-hub-status">AI 自动编排</span>
<button class="btn soft" style="font-size:10px;padding:7px 10px" onclick="refreshConnectionModes()">刷新状态</button>
</div>
</div>
<div class="capability-grid">
${deviceControlCards(modes).map((m,index) => `<button class="capability-card" onclick="openControlCapability('${m.id}')">
<div class="capability-card-top"><span class="capability-icon">${m.icon}</span><span class="capability-index">0${index+1}</span></div>
<div class="capability-name">${m.name}</div>
<div class="capability-desc">${m.desc}</div>
<div class="capability-foot"><span class="capability-state ${m.source?.available?'ready':''}"><i></i>${m.status}</span><span class="capability-enter">进入 →</span></div>
</button>`).join('')}
</div>
<div class="capability-note">系统会自动选择最快可用通道:数据优先走 Hook界面操作走 Agent设备级操作走 ADB。</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?'':`openDeviceDetail('${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>
${renderBindSection()}
`;
}
function renderOverviewClean(){
const ov = state.overview || {};
const devices = state.devices || ov.devices || [];
const total = ov.device_total ?? devices.length;
const online = ov.device_online ?? devices.filter(d=>['online','adb'].includes(d.status||d.adb_status)).length;
const ws = ov.ws_online ?? devices.filter(d=>d.status==='online').length;
const adb = ov.adb_count ?? devices.filter(deviceHasAdb).length;
const offline = Math.max(0, total-online);
const deviceRows = devices.slice(0,5).map(d=>`<button class="overview-device-row" onclick="openDeviceDetail('${esc(d.device_id)}')">
<span class="overview-device-icon">📱</span><span><b>${esc(d.model||d.nickname||d.device_id)}</b><small>${esc(d.device_id)}</small></span>
<i>${deviceStatusLabel(d)}</i><em> </em>
</button>`).join('') || '<div class="empty"></div>';
return `
<section class="overview-hero glass">
<div><span class="section-eyebrow">工作手机运营中心</span><h2>数据总览</h2><p>设备、连接和服务状态集中查看,核心入口一步到达。</p></div>
<button class="btn primary" onclick="refreshAll()">🔄 刷新实时数据</button>
</section>
<section class="overview-metrics">
${[['设备总数',total,'📱','全部登记设备'],['在线设备',online,'●','当前可控制'],['WS / ADB',`${ws} / ${adb}`,'🔗','无线与直连'],['离线设备',offline,'○','等待设备上线']].map(([label,value,icon,desc])=>`<article class="overview-metric glass"><span>${icon}</span><div><small>${label}</small><strong>${value}</strong><p>${desc}</p></div></article>`).join('')}
</section>
<section class="overview-quick glass">
<div class="section-title"><div><span class="section-eyebrow">快捷入口</span><h3>常用操作</h3></div></div>
<div class="overview-quick-grid">
<button onclick="navigate('devices')"><i>📱</i><b>手机设备</b><small>选择手机进入控制</small></button>
<button onclick="scanLanDevices()"><i>🔍</i><b>扫描局域网</b><small>发现SDK与手机节点</small></button>
<button onclick="navigate('engine')"><i>🧠</i><b>智能引擎</b><small>AI、Hook与运行监控</small></button>
<button onclick="navigate('gateway')"><i>🔗</i><b>对接网关</b><small>REST、MCP与存客宝</small></button>
</div>
</section>
<section class="overview-columns">
<div class="panel glass"><div class="section-title"><div><span class="section-eyebrow">设备快照</span><h3>最近设备</h3></div><button class="btn soft" onclick="navigate('devices')">全部手机</button></div><div class="overview-device-list">${deviceRows}</div></div>
${renderBindSection()}
</section>`;
}
/* ═══ 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:'500'}]},
{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:'500'}]},
{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:'user_ids',l:'接收者(逗号)'},{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 wxMapValue(device, ...keys){
const profile = device?.device_profile || device?.profile || {};
for (const source of [device || {}, profile || {}]) {
for (const key of keys) {
if (source[key] !== undefined && source[key] !== null && source[key] !== '') return source[key];
}
}
return '';
}
function wxDisplayTime(value){
if (!value) return '待现场核对';
const date = new Date(value);
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString('zh-CN', {hour12:false});
}
function wxDeviceMap(device){
return {
wxid: wxMapValue(device, 'wxid', 'wx_id', 'wechat_wxid'),
wechatId: wxMapValue(device, 'wechat_id', 'wechatId', 'account_wechat_id'),
friendCount: wxMapValue(device, 'friend_count', 'friends_count', 'contact_count', 'totalFriend', 'total_friend'),
lastActive: wxMapValue(device, 'last_active', 'last_seen', 'last_activity_at', 'last_heartbeat', 'updated_at'),
};
}
function deviceHasAdb(device){
return !!device && (
device.status==='adb' ||
device.adb_status==='device' ||
device.connection_type==='adb' ||
device.best_mode?.id==='adb' ||
(device.connection_modes||[]).some(mode=>mode.id==='adb' && mode.available)
);
}
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 wxCompactResult(data){
const payload = data?.data && typeof data.data === 'object' ? data.data : data;
const summary = {};
['count','returned_count','total_count','raw_total_count','has_more','offset','channel','action_resolved'].forEach(key => {
if (payload?.[key] !== undefined) summary[key] = payload[key];
});
['contacts','messages','groups','labels','tags','moments'].forEach(key => {
if (Array.isArray(payload?.[key])) summary[`${key}_returned_count`] = payload[key].length;
});
const display = Object.keys(summary).length ? {summary, response:data} : data;
return JSON.stringify(display, (_key, value) => {
if (Array.isArray(value) && value.length > 20) {
return {returned_count:value.length, preview:value.slice(0, 10)};
}
return value;
}, 2);
}
function wxResultHtml(data){
const payload = data?.data && typeof data.data === 'object' ? data.data : data || {};
const source = data?.channel_used || payload.channel || 'sdk_control';
const profileKeys = [
['wxid','微信 ID'],['nickname','昵称'],['remark','备注'],['wechat_version','微信版本'],
['wechat_running','运行状态'],['installed','已安装']
];
const profileItems = profileKeys.filter(([key]) => payload[key] !== undefined && payload[key] !== null && payload[key] !== '')
.map(([key,label]) => `<div class="wx-result-item"><small>${label}</small><b>${esc(payload[key] === true ? '正常' : payload[key] === false ? '否' : String(payload[key]))}</b></div>`).join('');
if(profileItems){
return `<div class="wx-result-card"><div class="wx-result-head"><span>资料已获取</span><span class="wx-result-badge">成功</span></div><div class="wx-result-grid">${profileItems}</div></div>`;
}
const listKey = ['contacts','groups','labels','tags','moments'].find(key => Array.isArray(payload[key]));
if(listKey){
const list = payload[listKey] || [];
if(!list.length) return `<div class="wx-result-card"><div class="wx-result-head"><span>${listKey==='contacts'?'联系人':listKey==='groups'?'群聊':'数据'}查询</span><span class="wx-result-badge">已完成</span></div><div class="wx-result-empty">当前没有返回可展示的${listKey==='contacts'?'联系人':listKey==='groups'?'群聊':'记录'}。</div></div>`;
return `<div class="wx-result-card"><div class="wx-result-head"><span>${listKey==='contacts'?'联系人':listKey==='groups'?'群聊':'查询结果'}${list.length}</span><span class="wx-result-badge">成功</span></div><div class="wx-result-list">${list.slice(0,20).map(item=>{const name=item.nickname||item.remark||item.name||item.user_id||item.wxid||'未命名';const sub=item.wxid||item.user_id||item.username||'';return `<div class="wx-result-row"><b>${esc(String(name))}</b><small>${esc(String(sub))}</small></div>`}).join('')}</div></div>`;
}
if(payload.ui_tree){
const tree = String(payload.ui_tree).trim();
return `<div class="wx-result-card"><div class="wx-result-head"><span>微信界面已读取</span><span class="wx-result-badge">成功</span></div>${tree && tree!=='null'?`<div class="wx-result-empty" style="white-space:pre-wrap;max-height:150px;overflow:auto">${esc(tree.slice(0,2000))}</div>`:'<div class="wx-result-empty">已连接,当前页面没有可读的微信节点。</div>'}</div>`;
}
const simple = Object.entries(payload).filter(([key,value])=>!['raw','response','data'].includes(key) && ['string','number','boolean'].includes(typeof value)).slice(0,8);
if(simple.length) return `<div class="wx-result-card"><div class="wx-result-head"><span>操作已完成</span><span class="wx-result-badge">成功</span></div><div class="wx-result-grid">${simple.map(([key,value])=>`<div class="wx-result-item"><small>${esc(key)}</small><b>${esc(String(value))}</b></div>`).join('')}</div></div>`;
return `<div class="wx-result-card"><div class="wx-result-head"><span>操作已完成</span><span class="wx-result-badge">成功</span></div></div>`;
}
function renderUnifiedControl(showFleet=false){
const allDevices = state.devices || [];
if (!state.aiConfig && !state.aiConfigLoading) setTimeout(loadAIConfig, 0);
const did = state.currentDevice || (allDevices[0]?.device_id);
if (did && !state.currentDevice) {
state.currentDevice = did;
setTimeout(()=>loadDeviceDetail(did), 0);
}
const current = allDevices.find(d=>d.device_id===did) || allDevices[0];
const currentMap = wxDeviceMap(current);
const currentRealtime = (state.realtime?.devices || []).find(d => d.device_id === did) || {};
const lastRealtimeEvent = (state.realtime?.recent_events || []).find(e => e.device_id === did || e.payload?.device_id === did);
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' && !deviceHasAdb(d)) 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 map = wxDeviceMap(device);
const wxId = map.wxid || map.wechatId;
const wxRunningNow = device.wechat_running || device.current_app === 'com.tencent.mm';
const wxDisplay = wxId || device.wechat_version || (wxRunningNow ? '运行中' : '-');
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="font-size:9px;color:var(--muted);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">IMEI: ${esc(device.imei || '待登记')}</div>
<div style="display:flex;gap:8px;margin-top:2px;font-size:10px;color:var(--muted)">
<span>微信: ${esc(wxDisplay)}</span>
<span>微信ID: ${esc(wxId || '待现场核对')}</span>
<span>好友: ${esc(map.friendCount === '' ? '待现场核对' : String(map.friendCount))}</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 class="device-summary-main">
<div class="device-summary-icon" style="background:linear-gradient(135deg,${tone.color},${tone.color}88)">📱</div>
<div class="device-summary-copy">
<div class="device-summary-name" id="device-summary-name">${esc(current.profile_nickname||current.nickname||current.model||current.device_id)}</div>
<div class="device-summary-id" id="device-summary-id">${esc([current.brand,current.model].filter(Boolean).join(' ')||'Android')} · ${esc(current.device_id)}</div>
</div>
<div class="device-summary-state">
<span style="font-size:9px;font-weight:750;color:#fff;padding:3px 8px;border-radius:20px;background:${tone.color}">${tone.label}</span>
<div style="font-size:9px;color:var(--sub);margin-top:3px">健康 <b id="device-summary-health" style="color:${hpColor}">${hp}%</b></div>
</div>
</div>
<div class="device-metric-strip">
<div class="device-mini-metric"><small>系统</small><b id="device-metric-android">${esc(current.android_version||current.sdk_version||'-')}</b></div>
<div class="device-mini-metric"><small>当前应用</small><b id="device-metric-app">${esc(current.current_app||(current.wechat_running?'微信':'-'))}</b></div>
<div class="device-mini-metric"><small>电量</small><b id="device-metric-battery">${current.battery_level>=0?current.battery_level+'%'+(current.battery_charging?' ⚡':''):'-'}</b></div>
<div class="device-mini-metric"><small>连接</small><b id="device-metric-online">${currentRealtime.online||current.status==='online'?'在线':'可控'}</b></div>
</div>
<details class="compact-device-more">
<summary>连接、账号与设备详情</summary>
<div class="compact-device-body">
${(current.connection_modes||[]).length ? '<div style="display:flex;gap:4px;flex-wrap:wrap">'+(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 class="compact-meta-grid">
<div class="compact-meta-item"><small>微信 wxid</small><b>${esc(currentMap.wxid || '待核对')}</b></div>
<div class="compact-meta-item"><small>好友数</small><b>${esc(currentMap.friendCount === '' ? '待核对' : String(currentMap.friendCount))}</b></div>
<div class="compact-meta-item"><small>最后活跃</small><b>${esc(wxDisplayTime(currentMap.lastActive))}</b></div>
<div class="compact-meta-item"><small>IMEI</small><b>${esc(current.imei || '待登记')}</b></div>
<div class="compact-meta-item"><small>内存</small><b>${current.memory_usage_pct>0?current.memory_usage_pct.toFixed(1)+'%':'-'}</b></div>
<div class="compact-meta-item"><small>最近事件</small><b>${esc(lastRealtimeEvent?.event_type || '暂无')}</b></div>
</div>
</div>
</details>
` : '<div style="text-align:center;padding:20px;color:var(--muted)">请选择设备</div>';
const deviceActions = `
<div class="phone-console-card" style="${!hasDevice?'opacity:.5;pointer-events:none':''}">
<div class="phone-console-head"><div><div class="phone-console-title">📱 手机控制</div><div class="phone-console-sub">常用操作放在这里,点击即可执行</div></div><span class="phone-console-state"><i></i>${current?.wechat_running?'微信运行中':'设备在线'}</span></div>
<div class="phone-primary-actions">
<button class="phone-primary-action" onclick="doAction('open_wechat')"><b>💬</b>打开微信</button>
<button class="phone-primary-action" onclick="doAction('recent')"><b>▣</b>最近任务</button>
<button class="phone-primary-action" onclick="doAction('home')"><b>🏠</b>主页</button>
<button class="phone-primary-action" onclick="doAction('back')"><b>↩️</b>返回</button>
</div>
</div>`;
const serviceSummary = Array.isArray(current?.service_summary) ? current.service_summary : [];
const serviceSummaryHtml = serviceSummary.length ? serviceSummary.map(service => `<span class="service-chip ${service.running?'ready':''}"><i></i>${esc(service.name)} · ${service.running?'运行中':'待配置'}</span>`).join('') : '<span style="font-size:10px;color:var(--muted)">服务状态读取中</span>';
const deviceManagementCard = `
<details class="control-section">
<summary><span>🔗 设备操作与服务</span><span class="control-section-kicker">${esc(current?.status==='online'?'设备在线':'可控设备')} · 点击展开</span></summary>
<div class="control-section-body">
<div class="phone-console-head"><div><div class="phone-console-title">常用设备操作</div><div class="phone-console-sub">安装、重启、服务检测统一放在这里。</div></div></div>
<div class="device-admin-actions">
<button class="device-admin-btn" onclick="deviceAdminAction('reboot')"><b>🔄</b><span>重启手机</span></button>
<button class="device-admin-btn" onclick="chooseDeviceApk()"><b>📦</b><span>安装软件</span></button>
<button class="device-admin-btn" onclick="chooseDeviceApk('wechat')"><b>💬</b><span>安装微信</span></button>
<button class="device-admin-btn" onclick="loadDeviceServices('${esc(did)}')"><b>⚡</b><span>检测服务</span></button>
</div>
<input id="device-apk-file" type="file" accept=".apk" style="display:none" onchange="installSelectedApk(this.files[0])">
<div class="service-summary-row">${serviceSummaryHtml}</div>
</div>
</details>`;
const runtimeConfigCard = current ? `
<div class="general-settings-body">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<div><div style="font-size:12px;font-weight:700">⚙ APK 运行参数</div><div style="font-size:10px;color:var(--sub);margin-top:2px">通过 SDK 热更新,手机无需重装</div></div>
<span style="font-size:10px;color:${current.wechat_running?'var(--green)':'var(--orange)'};font-weight:700">${current.wechat_running?'● 微信正在运行':'○ 微信未运行'}</span>
</div>
<div style="display:grid;grid-template-columns:1.4fr .8fr;gap:8px;margin-bottom:8px">
<div><label style="display:block;font-size:10px;color:var(--sub);margin-bottom:4px">设备 IMEI</label><input id="runtime-imei" value="${esc(current.imei||'')}" placeholder="输入1417位IMEI" maxlength="17" style="width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--border);font-size:11px;background:#fff"></div>
<div><label style="display:block;font-size:10px;color:var(--sub);margin-bottom:4px">检测周期</label><select id="runtime-wx-interval" style="width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--border);font-size:11px;background:#fff"><option value="30" ${Number(current.wechat_keepalive_interval_sec)===30?'selected':''}>30秒</option><option value="60" ${Number(current.wechat_keepalive_interval_sec)===60?'selected':''}>1分钟</option><option value="300" ${![30,60,600,1800].includes(Number(current.wechat_keepalive_interval_sec))?'selected':''}>5分钟</option><option value="600" ${Number(current.wechat_keepalive_interval_sec)===600?'selected':''}>10分钟</option><option value="1800" ${Number(current.wechat_keepalive_interval_sec)===1800?'selected':''}>30分钟</option></select></div>
</div>
<label style="display:flex;align-items:center;gap:7px;font-size:11px;color:var(--text);margin-bottom:10px"><input id="runtime-wx-enabled" type="checkbox" ${current.wechat_keepalive_enabled!==false?'checked':''}> 微信未运行时自动启动</label>
<div style="display:flex;gap:6px"><button class="btn primary" style="font-size:10px" onclick="saveDeviceRuntimeConfig()">保存到手机</button><button class="btn soft" style="font-size:10px" onclick="doAction('open_wechat')">立即启动微信</button></div>
</div>` : '';
const appMeta = {
wechat:{icon:'\ud83d\udcac',name:'微信',desc:'基础操作 · 数据 · 账号'},
soul:{icon:'\ud83d\udc9c',name:'Soul',desc:'打开 · 截图 · 设备操作'},
xhs:{icon:'\ud83d\udcd5',name:'小红书',desc:'打开 · 截图 · 设备操作'},
douyin:{icon:'\ud83c\udfb5',name:'抖音',desc:'打开 · 截图 · 设备操作'},
xianyu:{icon:'\ud83d\udc1f',name:'闲鱼',desc:'打开 · 截图 · 设备操作'},
};
const selectedApp = appMeta[appTab] || appMeta.wechat;
const controlButton = (icon,label,action) => `<button class="app-action-btn" onclick="${action}"><span class="icon">${icon}</span><span>${label}</span></button>`;
const appSelector = `
<div style="margin-top:16px">
<div class="panel-head" style="margin-bottom:12px">
<div><div style="font-size:13px;font-weight:750;color:var(--text)">📱 应用控制</div><div class="app-control-panel-sub">先进入一个应用,功能按需展开。</div></div>
<span class="pill">${hasDevice ? '已绑定设备' : '请选择可控设备'}</span>
</div>
<div class="${!hasDevice?'app-disabled':''}">
<div class="app-entry-focus">
<span class="app-control-icon">${selectedApp.icon}</span>
<span><span class="app-control-name">进入${selectedApp.name}控制</span><span class="app-control-desc">${appTab==='wechat'?'常用操作、资料查询、账号状态都在这里。':'设备级操作独立管理。'}</span></span>
<button class="btn primary" onclick="document.getElementById('app-control-panel')?.scrollIntoView({behavior:'smooth',block:'start'})">进入控制</button>
</div>
<div class="app-entry-nav">
${Object.entries(appMeta).filter(([id])=>id!==appTab).map(([id,meta])=>`
<button class="app-control-card" onclick="state.appFocus='${id}';renderCurrentView(false)">
<span class="app-control-icon">${meta.icon}</span><span class="app-control-name">${meta.name}</span>
</button>`).join('')}
</div>
</div>
<div id="app-control-panel" class="app-control-panel ${!hasDevice?'app-disabled':''}">
<div class="app-control-panel-head">
<div><div class="app-control-panel-title">${selectedApp.icon} ${selectedApp.name}</div><div class="app-control-panel-sub">${appTab==='wechat'?'常用功能优先,更多功能点开查看。':'设备操作单独管理。'}</div></div>
<span class="mode-badge ${current?.wechat_running || current?.current_app==='com.tencent.mm' ? 'available':'unavailable'}"><span class="badge-dot"></span>${appTab==='wechat'?(current?.wechat_running?'微信运行中':'微信待启动'):'设备操作'}</span>
</div>
${appTab==='wechat' ? `
<details class="app-fold" open><summary>⚡ 基础操作</summary><div class="app-fold-body"><div class="app-action-grid">
${controlButton('📱','打开微信',"doAction('open_wechat')")}
${controlButton('📸','截屏',"doAction('screenshot')")}
${controlButton('🏠','回到主页',"doAction('home')")}
${controlButton('↩️','返回上级',"doAction('back')")}
</div></div></details>
<details class="app-fold"><summary>👤 微信数据</summary><div class="app-fold-body"><div class="app-action-grid">
${controlButton('👤','个人资料',"wxQuickRun('get_profile')")}
${controlButton('👥','联系人',"wxQuickRun('get_contacts',{limit:500})")}
${controlButton('🏷️','标签',"wxQuickRun('get_tags')")}
${controlButton('群','群列表',"wxQuickRun('get_groups',{limit:500})")}
${controlButton('🔔','未读消息',"wxQuickRun('get_unread_messages')")}
${controlButton('🔎','搜索微信',"wxQuickRun('wechat_search',{keyword:prompt('搜索关键词')||''})")}
</div></div></details>
<details class="app-fold"><summary>✨ 微信常用功能</summary><div class="app-fold-body"><div class="app-action-grid">
${controlButton('📸','发朋友圈',"wxCatalogRun('moments','post_moments')")}
${controlButton('🧧','发红包',"wxCatalogRun('payment','send_red_packet')")}
${controlButton('🧧','收红包',"wxCatalogRun('payment','receive_red_packet')")}
${controlButton('🔎','查询好友',"wxCatalogRun('friend','get_friend_info')")}
${controlButton('🏷️','按标签查人',"wxCatalogRun('tag','get_users_by_tag')")}
</div></div></details>
<details class="app-fold"><summary>🧰 微信功能目录</summary><div class="app-fold-body"><div class="hint" style="margin:0 0 8px">按模块查看当前已登记动作,带参数的功能点击后填写参数。</div>${renderWxActionCatalog()}</div></details>
<details class="app-fold"><summary>🧪 测试与诊断</summary><div class="app-fold-body"><div class="app-action-grid">
${controlButton('🧩','Hook 状态',"wxQuickRun('get_hook_status')")}
${controlButton('🏷️','微信版本',"wxQuickRun('get_wechat_version')")}
${controlButton('🔐','账号状态',"wxQuickRun('check_account_status')")}
${controlButton('🖼️','读取 UI 树',"doAction('ui_tree')")}
</div><div class="hint">测试结果统一显示在右侧“执行结果”和“操作日志”,不与日常功能混排。</div></div></details>
` : `
<details class="app-fold" open><summary>⚡ 基础操作</summary><div class="app-fold-body"><div class="app-action-grid">
${controlButton(selectedApp.icon,'打开'+selectedApp.name,`doAction('open_${appTab}')`)}
${controlButton('📸','截屏',"doAction('screenshot')")}
${controlButton('🏠','回到主页',"doAction('home')")}
${controlButton('↩️','返回上级',"doAction('back')")}
</div></div></details>
<div class="hint">${selectedApp.name} 的业务动作会继续放在本应用面板内,避免和微信、设备、测试功能混在一起。</div>
`}
</div>
</div>`;
const msgSection = appTab==='wechat' ? `
<details class="app-fold" style="margin-top:12px"><summary>💬 微信消息操作</summary>
<div class="app-fold-body">
<div style="display:flex;gap:6px;margin-bottom:6px"><input id="wx_quick_to" placeholder="发送给(昵称 / 群名 / wxid" style="flex:1;padding:8px 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="消息内容" style="flex:1;min-height:54px;padding:8px 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;flex-wrap:wrap;margin-top:8px">
<button class="btn primary" style="font-size:10px;padding:6px 12px" onclick="wxQuickRun('send_message',{to_id:'wx_quick_to',content:'wx_quick_msg',msg_type:'text'})">发送消息</button>
<button class="btn soft" style="font-size:10px;padding:6px 12px" onclick="wxQuickRun('mass_send',{content:'wx_quick_msg'})">批量发送</button>
<button class="btn soft" style="font-size:10px;padding:6px 12px" onclick="wxQuickRun('add_friend',{user_id:wxReadInput('wx_quick_to'),message:wxReadInput('wx_quick_msg')})">添加好友</button>
</div>
</div>
</details>` : '';
const aiSection = `
<details class="app-fold" style="margin-top:12px"><summary>🤖 AI 指令</summary>
<div class="app-fold-body">
<details class="app-fold" style="margin:0 0 10px"><summary>🔧 AI 接口 <span class="pill">${state.aiConfig?.provider || '龙虾U盘'}</span></summary>
<div class="app-fold-body">
<div style="display:grid;grid-template-columns:1.5fr .8fr;gap:7px;margin-bottom:7px">
<input id="ai-config-base" value="${esc(state.aiConfig?.base_url || 'https://simpleapi.quwanzhi.com/v1')}" placeholder="接口地址,例如 https://simpleapi.quwanzhi.com/v1" style="width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--border);font-size:11px;background:#fff">
<input id="ai-config-model" value="${esc(state.aiConfig?.model || 'deepseek-chat')}" placeholder="模型名称" style="width:100%;padding:8px 10px;border-radius:9px;border:1px solid var(--border);font-size:11px;background:#fff">
</div>
<div style="display:flex;gap:7px;align-items:center"><input id="ai-config-key" type="password" placeholder="API Key${state.aiConfig?.api_key || '待配置'}" style="flex:1;padding:8px 10px;border-radius:9px;border:1px solid var(--border);font-size:11px;background:#fff"><button class="btn primary" style="font-size:10px;padding:7px 12px" onclick="saveAIConfig()">保存接口</button></div>
<div id="ai-config-tip" style="font-size:10px;color:var(--sub);margin-top:6px">Agent、AI指令和设备自动化统一使用这里的接口。</div>
</div>
</details>
<div style="display:flex;gap:4px;margin-bottom:8px">${['pattern','ai','ai-only'].map(m=>`<button class="btn ${state.cmdMode===m?'primary':'soft'}" style="font-size:10px;padding:5px 10px" onclick="state.cmdMode='${m}';renderCurrentView(false)">${{pattern:'模式匹配',ai:'AI 执行','ai-only':'AI 解析'}[m]}</button>`).join('')}</div>
<div class="command-area" style="display:flex;gap:6px"><input id="cmd-input" placeholder="${esc(cmdPlaceholder())}" style="flex:1;padding:9px 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:7px 14px" onclick="sendCmd()">执行</button></div>
</div>
</details>`;
const compactAppSelector = `
<details class="control-section" open>
<summary><span>📱 应用控制</span><span class="control-section-kicker">${selectedApp.name} · 点击切换</span></summary>
<div class="control-section-body">
<div class="app-entry-nav" style="grid-template-columns:repeat(5,minmax(0,1fr));margin-bottom:12px">
${Object.entries(appMeta).map(([id,meta])=>`<button class="app-control-card ${appTab===id?'active':''}" onclick="state.appFocus='${id}';renderCurrentView(false)"><span class="app-control-icon">${meta.icon}</span><span class="app-control-name">${meta.name}</span></button>`).join('')}
</div>
${appTab==='wechat' ? `
<div class="app-action-grid" style="grid-template-columns:repeat(4,minmax(0,1fr))">
${controlButton('👤','个人资料',"wxQuickRun('get_profile')")}
${controlButton('👥','联系人',"wxQuickRun('get_contacts',{limit:500})")}
${controlButton('🔎','搜索好友',"wxCatalogRun('friend','search_contact')")}
${controlButton('📸','发朋友圈',"wxCatalogRun('moments','post_moments')")}
</div>
<details class="phone-more" style="margin-top:10px"><summary>更多微信功能</summary><div class="phone-more-body">
<div class="app-action-grid">
${controlButton('🏷️','标签',"wxQuickRun('get_tags')")}
${controlButton('群','群列表',"wxQuickRun('get_groups',{limit:500})")}
${controlButton('🧧','发红包',"wxCatalogRun('payment','send_red_packet')")}
${controlButton('🔐','账号状态',"wxQuickRun('check_account_status')")}
</div>
${msgSection}${aiSection}
</div></details>
` : `
<div class="app-action-grid" style="grid-template-columns:repeat(4,minmax(0,1fr))">
${controlButton(selectedApp.icon,'打开'+selectedApp.name,`doAction('open_${appTab}')`)}
${controlButton('↻','重连画面',"restartLiveScreen()")}
${controlButton('🏠','主页',"doAction('home')")}
${controlButton('↩️','返回',"doAction('back')")}
</div>
`}
</div>
</details>`;
if (!showFleet && current) {
window.setTimeout(() => {
if (state.current === 'devices' && state.deviceView === 'detail' && state.currentDevice === current.device_id) {
startRemoteScreen(current.device_id);
}
}, 60);
}
return `
${showFleet ? `<div class="card" style="margin-top:14px;padding:14px">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px">
<div><span style="font-size:13px;font-weight:700">我的工作手机</span><span style="font-size:10px;color:var(--muted);margin-left:8px">点击手机后,下方所有操作切换到该设备</span></div>
<div style="display:flex;align-items:center;gap:6px">
<input placeholder="搜索设备…" style="width:180px;padding:6px 10px;border-radius:10px;border:1px solid var(--border);font-size:11px;background:rgba(255,255,255,.92)" oninput="state.wxKeyword=this.value.toLowerCase();renderCurrentView(false)">
${filterBtns}
</div>
</div>
<div class="device-fleet-strip">${deviceCards}</div>
</div>` : ''}
<div class="device-control-layout">
<div class="device-tools-column">
<div class="card">${deviceInfoCard}</div>
${deviceManagementCard}
<details class="control-section"><summary><span>⚙️ 通用设置</span><span class="control-section-kicker">保活、检测周期、IMEI</span></summary><div class="control-section-body">${runtimeConfigCard || '<div style="color:var(--muted);font-size:11px">请选择设备</div>'}</div></details>
${compactAppSelector}
</div>
<div class="device-live-column">
<div class="card live-device-card" style="padding:12px">
<div class="live-device-head">
<div class="live-device-title">📱 实时设备控制</div>
<span class="live-device-status waiting" id="live-screen-status">正在连接</span>
</div>
<div class="phone-live-body">
<div class="shot" id="shot-box" style="height:min(66vh,680px);min-height:500px;touch-action:none;cursor:crosshair" onpointerdown="phonePointerStart(event)" onpointermove="phonePointerMove(event)" onpointerup="phonePointerEnd(event)" onpointercancel="phonePointerCancel(event)">
<canvas id="live-screen" width="272" height="600" aria-label="scrcpy 远程实时设备画面"></canvas>
<img id="live-screen-fallback" class="live-screen-fallback" alt="兼容设备画面" draggable="false" onload="fallbackScreenReady()" onerror="liveScreenFailed(this)">
</div>
<div class="live-device-hint">直接点击、滑动上方手机画面进行操作</div>
<div class="phone-live-actions live-device-tools">
<button class="phone-live-action" onclick="doAction('home')"><b>🏠</b>主页</button>
<button class="phone-live-action" onclick="doAction('back')"><b>↩️</b>返回</button>
<button class="phone-live-action" onclick="restartLiveScreen()"><b>↻</b>重连画面</button>
</div>
</div>
</div>
<details class="phone-more"><summary>📊 执行记录 <span style="float:right;color:var(--sub);font-weight:500">统计、日志、通道</span></summary><div class="phone-more-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:10px">
<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)">总操作</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)">成功率</div></div>
</div>
<div style="font-size:11px;font-weight:700;margin-bottom:6px">操作日志</div><div id="device-action-logs" style="max-height:160px;overflow-y:auto">${logs || executionLogHtml || '<div style="color:var(--muted);font-size:11px;padding:4px">暂无记录</div>'}</div>
<div style="margin-top:10px;font-size:10px;color:var(--sub)">通道Hook 50-200ms · u2 UI 2-15s · ADB 1-10s</div>
</div></details>
</div>
</div>
`;
}
async function loadAIConfig(){
if (state.aiConfigLoading) return;
state.aiConfigLoading = true;
try {
const res = await apiGet('/api/v3/ai/config');
state.aiConfig = res.data || {};
} catch (error) {
log(`AI接口配置读取失败${error.message}`, 'error');
} finally {
state.aiConfigLoading = false;
if (state.current === 'devices') renderCurrentView(false);
}
}
async function saveAIConfig(){
const baseUrl = wxReadInput('ai-config-base');
const model = wxReadInput('ai-config-model');
const apiKey = wxReadInput('ai-config-key');
const tip = document.getElementById('ai-config-tip');
if (!baseUrl || !model) {
if (tip) tip.textContent = '请填写接口地址和模型名称';
return;
}
try {
const payload = {provider:'longxia', base_url:baseUrl, model};
if (apiKey) payload.api_key = apiKey;
const res = await apiPost('/api/v3/ai/config', payload);
state.aiConfig = res.data || {...state.aiConfig, base_url:baseUrl, model};
if (tip) tip.textContent = '已保存Agent下次执行立即使用';
log('统一 AI 接口配置已保存', 'info');
} catch (error) {
if (tip) tip.textContent = `保存失败:${error.message}`;
log(`AI接口保存失败${error.message}`, 'error');
}
}
const WX_ACTION_ROUTES = {
send_message:{m:'POST',p:'/message/send'},get_messages:{m:'POST',p:'/message/list'},
get_unread_messages:{m:'POST',p:'/message/list'},get_recent_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:'/payment/red-packet'},transfer:{m:'POST',p:'/payment/transfer'},
show_payment_code:{m:'GET',p:'/payment/code'},view_wallet:{m:'GET',p:'/payment/wallet'},
view_transactions:{m:'GET',p:'/payment/transactions'},receive_red_packet:{m:'POST',p:'/hook/execute',hook:true},
receive_payment:{m:'POST',p:'/hook/execute',hook:true},
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:'/miniprogram/open'},follow_official_account:{m:'POST',p:'/hook/execute',hook:true},
open_video_channel:{m:'GET',p:'/video-channel/list'},get_video_list:{m:'GET',p:'/video-channel/list'},
like_video:{m:'POST',p:'/hook/execute',hook:true},comment_video:{m:'POST',p:'/hook/execute',hook:true},
follow_video_creator:{m:'POST',p:'/hook/execute',hook:true},share_video:{m:'POST',p:'/hook/execute',hook:true},
scan_qr_code:{m:'POST',p:'/hook/execute',hook:true},scan_add_friend:{m:'POST',p:'/hook/execute',hook:true},
show_my_qr:{m:'GET',p:'/scan/my-qr'},extract_qr_from_image:{m:'POST',p:'/hook/execute',hook:true},
voice_call:{m:'POST',p:'/hook/execute',hook:true},video_call:{m:'POST',p:'/hook/execute',hook:true},
mass_send:{m:'POST',p:'/mass-send'},wechat_search:{m:'GET',p:'/search/wechat'},
top_stories:{m:'GET',p:'/discover/top-stories'},get_steps:{m:'GET',p:'/wechat-sport/steps'},
like_steps:{m:'POST',p:'/hook/execute',hook:true},send_location:{m:'POST',p:'/location/send'},
share_real_time_location:{m:'POST',p:'/hook/execute',hook:true},send_emoji:{m:'POST',p:'/emoji/send'},
get_sticker_list:{m:'GET',p:'/emoji/stickers'},send_voice_message:{m:'POST',p:'/message/voice'},
send_file_from_chat:{m:'POST',p:'/file/send'},toggle_do_not_disturb:{m:'POST',p:'/settings/do-not-disturb'},
clear_cache:{m:'POST',p:'/settings/clear-cache'},logout:{m:'POST',p:'/hook/execute',hook:true},
switch_account:{m:'POST',p:'/hook/execute',hook:true},
};
async function wxCall(action, params, actMeta){
const route = WX_ACTION_ROUTES[action] || {m:'POST',p:'/hook/execute',hook:true};
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.hook){
const hookParams = {...finalParams};
delete hookParams.device_id;
delete hookParams.platform;
delete hookParams.action;
data = await apiPost(`/api/v3${route.p}`, {
device_id: finalParams.device_id,
platform: finalParams.platform,
action,
params: hookParams,
hook_only: false
});
} else 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 nestedCode = Number(data.data?.code);
const ok = (data.code===200||data.code===undefined) && data.success!==false && data.data?.success!==false && !(nestedCode>=400);
_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.innerHTML=ok?wxResultHtml(data):`<div class="wx-result-error">${esc(wxResponseMessage(data))}</div>`;
const resultChannel=document.getElementById('wx-result-channel');if(resultChannel)resultChannel.textContent=`${ch} · ${ms}ms`;
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('device-action-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?'':' — '+wxResponseMessage(data)}`, 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.innerHTML=`<div class="wx-result-error">${esc(e.message || '请求失败')}</div>`;
const resultChannel=document.getElementById('wx-result-channel');if(resultChannel)resultChannel.textContent='请求失败';
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);
}
function wxCatalogRun(moduleId, action){
const module = WX_MODULES[moduleId];
const act = module?.actions.find(item=>item.a===action);
if(!act) return;
const params = {};
for(const field of (act.f || [])){
const value = prompt(`${act.n} · ${field.l}`, field.v || '');
if(value === null) return;
params[field.k] = value;
}
return wxCall(action, params, act);
}
function renderWxActionCatalog(){
return Object.entries(WX_MODULES).map(([moduleId,module])=>`
<details class="app-fold">
<summary>📂 ${module.title}<span class="pill">${module.actions.length} 项</span></summary>
<div class="app-fold-body">
<div class="app-action-grid">
${module.actions.map(act=>`<button class="app-action-btn" onclick="wxCatalogRun('${moduleId}','${act.a}')"><span class="icon">⚙️</span><span>${act.n}</span></button>`).join('')}
</div>
</div>
</details>`).join('');
}
function wxResponseMessage(data){
const payload = data?.data;
return data?.message || data?.detail || payload?.message || payload?.error || payload?.detail || '请求失败';
}
function wxUnavailableGuide(data){
const detail = wxResponseMessage(data);
const is503 = Number(data?.code)===503 || Number(data?.status)===503 || /503/.test(String(detail));
if(!is503) return `// 错误\n${detail}`;
const wsOnline = state.devices?.find(d=>d.device_id===state.currentDevice)?.status === 'online';
return `// 微信通道暂不可用\n${detail}\n\n处理建议:${wsOnline?'检查手机端 Frida/Hook 状态':'启动手机端 Agent保持 WS 连接后再执行微信数据操作'}\n当前设备:${state.currentDevice || '-'}\n说明ADB 连接只负责设备级操作,好友、资料、朋友圈等数据需要 WS + 微信通道。`;
}
/* ═══ 2. Devices Panel (设备控制 / 接口文档 / 系统说明) ═══ */
function renderDevicePanel(){
const devices = state.devices || [];
const wsCount = devices.filter(d=>d.status==='online').length;
const adbCount = devices.filter(deviceHasAdb).length;
const onlineCount = devices.filter(d=>
d.status==='online' ||
d.status==='adb' ||
d.adb_status==='device' ||
d.controllable===true
).length;
const unauth = devices.filter(d=>d.status==='unauthorized'||d.adb_status==='unauthorized');
if (state.deviceView === 'detail' && state.currentDevice) {
const current = devices.find(d => d.device_id === state.currentDevice);
const title = current?.profile_nickname || current?.nickname || current?.model || state.currentDevice;
return `
<div class="panel glass device-detail-header">
<div class="panel-head" style="margin-bottom:0">
<div style="display:flex;align-items:center;gap:12px">
<button class="btn soft" onclick="state.deviceView='list';renderCurrentView()">← 返回手机设备</button>
<div><h2>📱 ${esc(title)}</h2><p class="sub">设备控制详情 · 所有操作仅作用于当前手机</p></div>
</div>
<div style="display:flex;gap:8px;align-items:center">
<button class="btn soft" onclick="loadDeviceDetail('${esc(state.currentDevice)}')">🔄 刷新设备</button>
${current?.status === 'offline' ? `<button class="btn danger" onclick="deleteOfflineDevice('${esc(state.currentDevice)}')">删除失效设备</button>` : ''}
</div>
</div>
</div>${renderUnifiedControl(false)}`;
}
const cards = devices.length ? devices.map(device => {
const tone = wxStatusTone(device);
const name = device.profile_nickname || device.nickname || device.model || device.device_id;
const wxRunning = device.wechat_running || device.current_app === 'com.tencent.mm';
const online = device.status === 'online' || device.status === 'adb' || device.adb_status === 'device' || device.controllable === true;
return `<button class="phone-device-card" onclick="openDeviceDetail('${esc(device.device_id)}')">
<span class="phone-online-dot" style="background:${online?'var(--green)':'var(--muted)'}"></span>
<span class="phone-shell"><span class="phone-speaker"></span><span class="phone-screen"><span class="phone-screen-icon">📱</span><span>${online?'已连接':'离线'}</span></span></span>
<strong>${esc(name)}</strong>
<small>${esc(device.brand||'Android')} · ${esc(device.device_id)}</small>
<span class="phone-tags"><i style="color:${tone.color}">● ${tone.label}</i><i>${wxRunning?'微信运行':'微信待启动'}</i></span>
<span class="phone-enter">进入控制台 →</span>
</button>`;
}).join('') : `<div class="card" style="grid-column:1/-1;text-align:center;padding:54px 20px">${renderConnectGuide()}</div>`;
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="openBindQRCode()">📷 扫码绑定</button>
<button class="btn soft" style="font-size:11px" onclick="adbRescan()">📡 扫描 ADB</button>
</div>
</div>
</div>`;
return header + `<div class="phone-device-grid">${cards}</div>`;
}
function openDeviceDetail(deviceId){
state.current = 'devices';
state.deviceView = 'detail';
state.currentDevice = deviceId;
renderSidebar();
renderCurrentView();
window.setTimeout(() => {
if (state.current === 'devices' && state.deviceView === 'detail' && state.currentDevice === deviceId) {
loadDeviceDetail(deviceId);
}
}, 3000);
log(`进入手机控制:${deviceId}`, 'info');
}
function loadIntegrationManifest(){
if (state.integrationManifest || state.integrationManifestLoading) return;
state.integrationManifestLoading = true;
state.integrationManifestError = '';
apiGet('/api/v3/integration/manifest')
.then(res => {
state.integrationManifest = res.data || res;
state.integrationManifestLoading = false;
renderCurrentView(false);
})
.catch(err => {
state.integrationManifestLoading = false;
state.integrationManifestError = err.message || String(err);
renderCurrentView(false);
});
}
function renderApiDocsDynamicTab(){
loadIntegrationManifest();
const manifest = state.integrationManifest;
if (!manifest) {
return `
<div class="panel glass" style="margin-top:14px">
<div class="panel-head">
<div><h2>📡 全部 API 接口文档</h2><p class="sub">正在从 /api/v3/integration/manifest 实时加载接口清单...</p></div>
<div class="btns"><a class="btn primary" href="/docs" target="_blank" style="text-decoration:none">📄 Swagger</a><a class="btn soft" href="/redoc" target="_blank" style="text-decoration:none">ReDoc</a></div>
</div>
${state.integrationManifestError ? `<div class="card" style="border-color:rgba(220,38,38,.25);color:var(--red)">加载失败:${esc(state.integrationManifestError)}</div>` : `<div class="card">接口中心加载中,稍等 1 秒自动刷新。</div>`}
</div>`;
}
const modules = manifest.modules || [];
const totalEndpoints = manifest.total_endpoints || modules.reduce((sum, mod) => sum + ((mod.endpoints || []).length), 0);
const consumers = manifest.consumers || {};
const expandedCat = state._apiDocExpanded || (modules[0] && (modules[0].module || modules[0].id)) || '';
const copyPath = p => String(p || '').replace(/'/g, "\\'");
const methodTone = m => m === 'GET' ? '37,99,235' : m === 'POST' ? '22,163,74' : m === 'DELETE' ? '220,38,38' : '245,158,11';
const mkEndpoint = ep => {
const m = ep.method || 'GET';
const p = ep.path || '';
const summary = ep.summary || ep.name || '接口';
return `<div style="display:grid;grid-template-columns:58px minmax(0,1fr) minmax(120px,240px);align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid rgba(15,23,42,.04)">
<code style="background:rgba(${methodTone(m)},.08);padding:2px 7px;border-radius:5px;font-size:10px;text-align:center;font-weight:700">${esc(m)}</code>
<span style="font-family:monospace;font-size:11px;color:var(--text);cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" onclick="navigator.clipboard.writeText('${copyPath(p)}');this.style.color='var(--green)';setTimeout(()=>this.style.color='var(--text)',1000)" title="点击复制路径">${esc(p)}</span>
<span style="color:var(--muted);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(summary)}</span>
</div>`;
};
return `
<div class="panel glass" style="margin-top:14px">
<div class="panel-head">
<div>
<h2>📡 全部 API 接口文档</h2>
<p class="sub">${totalEndpoints} 个实时端点 · ${modules.length} 个模块 · 来源 /api/v3/integration/manifest · 点击路径可复制</p>
</div>
<div class="btns">
<a class="btn primary" href="/docs" target="_blank" style="text-decoration:none">📄 Swagger</a>
<a class="btn soft" href="/redoc" target="_blank" style="text-decoration:none">ReDoc</a>
<button class="btn soft" onclick="state.integrationManifest=null;state._apiDocExpanded='';renderCurrentView(false)">🔄 刷新清单</button>
</div>
</div>
<div class="grid col4" style="margin-bottom:12px">
<div class="card"><h3>总端点</h3><p style="font-size:24px;font-weight:800;color:var(--blue)">${totalEndpoints}</p></div>
<div class="card"><h3>模块</h3><p style="font-size:24px;font-weight:800;color:var(--teal)">${modules.length}</p></div>
<div class="card"><h3>存客宝</h3><p style="font-size:18px;font-weight:800;color:var(--green)">${esc(consumers.cunkebao || '可筛选')}</p></div>
<div class="card"><h3>对接原则</h3><p>统一走 /api/v3/integration/* 发现能力,业务调用再进具体接口。</p></div>
</div>
<div style="display:grid;gap:10px">
${modules.map(mod => {
const id = mod.module || mod.id || mod.label || 'module';
const isOpen = expandedCat === id;
const endpoints = mod.endpoints || [];
const consumerText = (mod.consumers || []).join(' / ');
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==='${esc(id)}'?'':'${esc(id)}';renderCurrentView(false)" onmouseover="this.style.background='rgba(37,99,235,.03)'" onmouseout="this.style.background='transparent'">
<span style="font-size:18px">📦</span>
<div style="flex:1"><span style="font-size:13px;font-weight:700">${esc(mod.label || id)}</span><span style="font-size:11px;color:var(--muted);margin-left:8px">${endpoints.length} 个端点 · ${esc(consumerText || 'common')}</span></div>
<code style="font-size:10px;color:var(--muted)">${esc(id)}</code>
<span style="font-size:14px;color:var(--muted);transition:transform .2s;transform:rotate(${isOpen?'180':'0'}deg)">▼</span>
</div>
${isOpen ? '<div style="padding:4px 16px 14px;border-top:1px solid var(--border);font-size:11px;line-height:2">'+endpoints.map(mkEndpoint).join('')+'</div>' : ''}
</div>`;
}).join('')}
</div>
</div>`;
}
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 wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
const privateIp = (state.serverInfo?.ips || []).find(ip =>
/^10\./.test(ip) || /^192\.168\./.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip)
);
const bindHost = privateIp || location.hostname;
const wsPort = state.serverInfo?.port
? `:${state.serverInfo.port}`
: (location.port ? `:${location.port}` : (location.protocol === 'https:' ? '' : ':8899'));
const defaultServer = state.bindingServer || `${wsProtocol}://${bindHost}${wsPort}/ws/device`;
return `
<div class="panel glass" id="bind-panel">
<div class="panel-head"><div><h2>📲 扫码绑定手机</h2><p class="sub">先扫描服务器,再用手机扫码;二维码只绑定当前服务器</p></div><span class="pill">首页唯一入口</span></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="工作手机" 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" readonly>
<button class="btn soft" onclick="scanLanDevices();setTimeout(()=>renderCurrentView(),600)">🔍 扫描服务器并作为绑定基准</button>
<button class="btn primary" onclick="generateBindQR()">生成绑定二维码</button>
<div style="font-size:11px;line-height:1.7;color:var(--muted)">授权关系:网页用管理员会话,存客宝用 API Key手机用配对令牌。三者分开配置。</div>
</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(124,58,237,.06);border-radius:12px;padding:14px">
<strong style="color:var(--purple)">扫码无线连接</strong><br>
① 打开手机端工作手机 APK<br>
② 扫描中台生成的设备绑定二维码<br>
③ Agent WebSocket 自动连接并接受远程控制
</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="engine-nav-groups">
<div class="engine-nav-group"><small>AI 中枢</small><div class="engine-nav-actions">
<div class="mode-btn ${tab==='brain'?'active':''}" onclick="state.engineTab='brain';renderCurrentView()">🧠 AI Brain</div>
</div></div>
<div class="engine-nav-group"><small>HOOK 能力</small><div class="engine-nav-actions">
<div class="mode-btn ${tab==='hook'?'active':''}" onclick="state.engineTab='hook';renderCurrentView()">🪝 模块</div>
<div class="mode-btn ${tab==='hookdata'?'active':''}" onclick="state.engineTab='hookdata';renderCurrentView()">📊 数据</div>
</div></div>
<div class="engine-nav-group"><small>运行与安全</small><div class="engine-nav-actions">
<div class="mode-btn ${tab==='events'?'active':''}" onclick="state.engineTab='events';renderCurrentView()">📡 事件 ${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 class="mode-btn ${tab==='antiban'?'active':''}" onclick="state.engineTab='antiban';refreshAntiban();renderCurrentView()">🛡️ 防封</div>
</div></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('🧩 微信注入能力 <span style="color:var(--green);font-size:11px">P1</span>','微信深度数据与接口能力<br>服务状态在设备高级连接中统一查看<br>需 Root + 注入引擎')}
${card('🖱️ u2 自动化 <span style="color:var(--blue);font-size:11px">P2</span>','UI 模拟操作<br>2-15s · 通用性强<br>无需 Root')}
${card('⌨️ 设备底座 <span style="color:var(--orange);font-size:11px">P3</span>','ADB 作为安装与运维兜底<br>截图、点击、输入与系统操作<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":3000}'},
{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":10000}'},
{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":500}'},
{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 || {};
const contactTotal = hd.contacts?.total_count || hd.contacts?._diag?.filtered_total || hd.contacts?.count || contacts.length || 0;
const contactLoaded = contacts.length || 0;
const messageTotal = hd.messages?.total_count || hd.messages?.count || messages.length || 0;
const messageLoaded = messages.length || 0;
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)">${contactTotal}</div><div style="font-size:10px;color:var(--muted)">联系人${contactLoaded && contactLoaded !== contactTotal ? ` · 已载 ${contactLoaded}` : ''}</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)">${messageTotal}</div><div style="font-size:10px;color:var(--muted)">近期消息${messageLoaded && messageLoaded !== messageTotal ? ` · 已载 ${messageLoaded}` : ''}</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">👥 联系人 (${contactTotal}${contactLoaded && contactLoaded !== contactTotal ? ` / 已载 ${contactLoaded}` : ''})</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>`;
} else if (tab === 'antiban') {
const ab = state.antibanStatus || {};
const hours = ab.operation_hours || {};
const limits = (ab.platform_limits || {}).wechat || {};
const device = ab.device || {};
const red = device.redlight || {};
const sentinel = device.sentinel || {};
const warnings = device.guard_warnings || [];
const limitRows = Object.entries(limits).map(([action, cfg]) => `
<tr>
<td><code>${esc(action)}</code></td>
<td>${(cfg.interval || []).join(' - ')}s</td>
<td>${cfg.daily_max ?? '-'}</td>
<td>${cfg.new_daily_max ?? '-'}</td>
</tr>`).join('');
const redTone = red.level === 'red' ? 'var(--red)' : red.level === 'yellow' ? 'var(--orange)' : 'var(--green)';
const rt = device.runtime || {};
const st = rt.silent_throttle || {};
const lp = rt.long_pause || {};
const hr = rt.last_highrisk;
const stTone = st.active ? 'var(--red)' : 'var(--green)';
const lpTone = lp.active ? 'var(--orange)' : 'var(--green)';
const fmtMin = s => s >= 60 ? `${Math.round(s/60)}min` : `${s}s`;
content = `
<div class="panel glass">
<div class="panel-head">
<div><h2>🛡️ 防封风控</h2><p class="sub">账号暖机 · 限流节律 · Root/Frida 红灯 · AB-02 静默限流熔断</p></div>
<div class="btns"><button class="btn soft" onclick="refreshAntiban()">刷新防封状态</button></div>
</div>
<div class="stats" style="grid-template-columns:repeat(4,1fr);margin-bottom:8px">
${statCard('操作时段', `${(hours.range || [7,23]).join(':00-')}:00`, hours.in_window ? '当前允许操作' : '当前应暂停')}
${statCard('红灯等级', red.level || 'unknown', red.reason || '等待设备上报')}
${statCard('RiskSentinel', sentinel.level_name || sentinel.level || 'unknown', `失败 ${sentinel.consecutive_failures ?? '-'}`)}
${statCard('长暂停', ab.long_pause?.pause_seconds || '600-1800', `${ab.long_pause?.every_actions || '8-12'} 动作`)}
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>AB-02/03 实时熔断(本设备)</h2><p class="sub">rate_limiter 运行态 · /antiban/status?device_id 真源</p></div></div>
<div class="stats" style="grid-template-columns:repeat(3,1fr)">
${statCard('AB-02 静默限流熔断', st.active ? `熔断中 ${fmtMin(st.until_in_s||0)}` : '正常', `<span style="color:${stTone}">累计触发 ${st.hits ?? 0} 次</span>`)}
${statCard('AB-03 长暂停', lp.active ? `暂停中 ${fmtMin(lp.until_in_s||0)}` : '正常', `<span style="color:${lpTone}">动作节拍 ${lp.action_seq ?? 0}${lp.next_pause_at ? '/'+lp.next_pause_at : ''}</span>`)}
${statCard('最近高危动作', hr ? hr.action : '无', hr ? `${hr.ago_s}s 前` : '窗口内无高危')}
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>微信动作限流</h2><p class="sub">服务端 _anti_ban_guard 真源,所有写操作必须经过该守卫</p></div></div>
<div class="card">
<table class="list-table">
<thead><tr><th>动作</th><th>随机间隔</th><th>成熟号/日</th><th>新号/日</th></tr></thead>
<tbody>${limitRows || '<tr><td colspan="4" class="empty">暂无限流配置</td></tr>'}</tbody>
</table>
</div>
</div>
<div class="panel glass">
<div class="grid col2">
<div class="card">
<h3 style="color:${redTone}">设备红灯 / Guard</h3>
<p>设备:<code>${esc(device.device_id || state.currentDevice || '-')}</code> · 在线:${device.online ? '✅' : '❌'}</p>
<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
${(warnings || []).length ? warnings.map(w => `<span class="pill" style="color:var(--orange);background:rgba(245,158,11,.08)">${esc(w)}</span>`).join('') : '<span class="pill" style="color:var(--green)">暂无警告</span>'}
</div>
</div>
<div class="card">
<h3>高危动作互斥</h3>
<p>以下动作不得同窗口并发,触发风控后需静置 24-48h。</p>
<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
${(ab.high_risk_exclusive || []).map(x => `<span class="pill">${esc(x)}</span>`).join('')}
</div>
</div>
</div>
</div>
<div class="panel glass">
<div class="panel-head"><div><h2>下一步防封 WBS</h2><p class="sub">共享基线:开发文档/2、架构/05-规范/防封共享策略与执行基线.md</p></div></div>
<div class="grid col3">
${card('AB-02 静默限流 ✅','success 但无 message_id / svr_id → 设备级熔断,写类冷却 30-60min 逐次退避')}
${card('AB-03 长暂停 ✅','每 8-12 个动作插入 10-30 分钟长暂停 + 高危动作互斥')}
${card('AB-04 量产红灯','prod 下 Root/Frida/ADB 暴露标红dev 标黄')}
</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 authenticatedFetch(`/api/v3/hook/data/${did}?modules=profile,contacts,groups,labels,messages,device_info&contact_limit=200&message_limit=50&contact_offset=0&message_offset=0`);
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": 3000, "offset": 0}',
'get_contacts': '{"limit": 10000, "offset": 0}',
'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 openBindQRCode(){
state.current = 'overview';
state.deviceView = 'list';
renderSidebar();
renderCurrentView();
document.getElementById('bind-panel')?.scrollIntoView({behavior:'smooth', block:'center'});
await generateBindQR();
}
async function deleteOfflineDevice(deviceId){
const device = (state.devices || []).find(item => item.device_id === deviceId);
if (!device || device.status !== 'offline') {
log('仅离线的失效设备可删除', 'error');
return;
}
if (!confirm(`确认删除失效设备 ${device.model || deviceId}?历史命令记录会保留。`)) return;
try {
const res = await apiDelete(`/api/v3/devices/${encodeURIComponent(deviceId)}`);
log(res.message || '失效设备已删除', 'info');
state.currentDevice = null;
state.deviceView = 'list';
await refreshDevices();
renderCurrentView();
} catch (e) {
log(`删除失败:${e.message || e}`, 'error');
}
}
async function generateBindQR(){
const project = document.getElementById('bind-project')?.value?.trim() || 'cunkebao';
const name = document.getElementById('bind-name')?.value?.trim() || '工作手机';
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 payload = {project_id: project, project_name: name, server};
const shouldRetry = (e) => {
const msg = String(e?.message || '').toLowerCase();
const status = Number(e?.status || 0);
return status >= 500 || msg.includes('重连') || msg.includes('暂时') || msg.includes('超时');
};
let res;
try {
res = await apiPost('/api/v3/qrcode/generate', payload);
} catch (firstError) {
if (shouldRetry(firstError)) {
area.innerHTML = '<div style="color:var(--sub)">服务波动,重试中...</div>';
await new Promise(resolve => setTimeout(resolve, 800));
res = await apiPost('/api/v3/qrcode/generate', payload);
} else {
throw firstError;
}
}
const qr = res?.data || res || {};
const image = qr.image_data_url || (qr.image_base64 ? `data:image/png;base64,${qr.image_base64}` : '');
if (qr.success !== false && image) {
area.innerHTML = `
<img src="${image}" 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>
`;
return;
}
const msg = qr.message || '二维码生成失败,请重试';
area.innerHTML = `<div style="color:var(--red)">${esc(msg)}</div><button class="btn soft" onclick="generateBindQR()" style="margin-top:10px">重新生成</button>`;
} catch(e) {
let tip = '服务连接异常,请稍后重试';
if (e && e.status === 401) tip = '控制台授权已失效请重新登录若存客宝调用返回401请核对宝塔 .env 的 API_KEY';
else if (e && /qrcode|二维码/.test(e.message || '')) tip = `服务异常:${e.message}`;
else if (e && e.message && e.message.includes('服务正在重连')) tip = '服务正在重连,请稍后再试(建议先在控制台执行服务重启)';
else if (e && e.status && e.status >= 500) tip = '服务重启中,请稍后点击重新生成';
area.innerHTML = `<div style="color:var(--red)">${esc(tip)}</div><button class="btn soft" onclick="generateBindQR()" style="margin-top:10px">重新生成</button>`;
}
}
/* ═══ Device Actions ═══ */
async function saveDeviceRuntimeConfig(){
const did = state.currentDevice;
if (!did) return log('请先选择设备', 'error');
const imei = document.getElementById('runtime-imei')?.value?.trim() || '';
if (imei && !/^\d{14,17}$/.test(imei)) return log('IMEI 必须是1417位数字', 'error');
const enabled = document.getElementById('runtime-wx-enabled')?.checked !== false;
const interval = Number(document.getElementById('runtime-wx-interval')?.value || 300);
try {
await apiPost(`/api/v3/devices/${did}/hot-update`, {
update_type:'config',
config:{
device_imei:imei,
wechat_keepalive_enabled:enabled,
wechat_keepalive_interval_sec:interval
}
});
log(`APK参数已更新IMEI ${imei?'已登记':'使用系统读取'} · 微信保活 ${enabled?'开启':'关闭'} · ${interval}`, 'info');
setTimeout(()=>loadDeviceDetail(did), 1500);
} catch(error) {
log(`APK参数更新失败${error.message}`, 'error');
}
}
async function doAction(action){
if (!state.currentDevice) return log('请先选择设备', 'error');
log(`执行操作:${action}`, 'info');
setControlFeedback('指令已发送', true);
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'}},
recent: {url:`/api/v3/adb/devices/${state.currentDevice}/key`, body:{key:'recent'}},
};
if (['home','back','recent'].includes(action) && remoteSend({type:'key',key:action})) {
log(`scrcpy 远程按键:${action}`,'info');
return;
}
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');
}catch(error){
log(`操作失败:${error.message}`, 'error');
}finally{
window.setTimeout(() => setControlFeedback(_remoteFirstFrame?(_remoteTransport==='agent-jpeg'?'扫码设备实时':'scrcpy 实时'):'实时在线', false), 120);
}
}
async function deviceAdminAction(action){
if (!state.currentDevice) return log('请先选择设备', 'error');
if (action === 'reboot' && !window.confirm('确认重启当前手机?设备会短暂离线后自动回连。')) return;
try {
const res = await apiPost(`/api/v3/adb/devices/${state.currentDevice}/reboot`, {});
log(res.code === 200 ? '重启指令已发送,等待手机回连' : `重启失败:${res.message || '请求失败'}`, res.code === 200 ? 'info' : 'error');
setTimeout(() => refreshDevices(true), 3500);
} catch (error) { log(`重启失败:${error.message}`, 'error'); }
}
function chooseDeviceApk(kind='app'){
state.pendingInstallKind = kind;
document.getElementById('device-apk-file')?.click();
}
async function installSelectedApk(file){
const input = document.getElementById('device-apk-file');
if (!file || !state.currentDevice) return;
if (!file.name.toLowerCase().endsWith('.apk')) { log('请选择 APK 文件', 'error'); return; }
try {
log(`正在安装${state.pendingInstallKind === 'wechat' ? '微信' : '软件'}${file.name}`, 'info');
const res = await apiUpload(`/api/v3/adb/devices/${state.currentDevice}/app/install-ws`, file);
const ok = res.code === 200 && res.data?.success !== false;
log(ok ? `安装完成:${file.name}` : `安装失败:${res.message || res.data?.output || '设备返回失败'}`, ok ? 'info' : 'error');
if (input) input.value = '';
if (ok) setTimeout(() => loadDeviceDetail(state.currentDevice), 1200);
} catch (error) { log(`安装失败:${error.message}`, 'error'); if (input) input.value = ''; }
}
async function loadDeviceServices(deviceId=state.currentDevice){
if (!deviceId) return;
try {
await apiPost(`/api/v3/adb/devices/${deviceId}/services/ensure`, {}).catch(() => null);
const res = await apiGet(`/api/v3/adb/devices/${deviceId}/services`);
const services = res.data?.services || res.services || [];
state.devices = (state.devices || []).map(device => device.device_id === deviceId ? {...device, service_summary: services} : device);
const active = (state.devices || []).find(device => device.device_id === deviceId);
if (active) patchDeviceDetailSummary(active);
log('设备服务状态已刷新', 'info');
} catch (error) { log(`服务状态读取失败:${error.message}`, 'error'); }
}
let _phonePointerStart = null;
let _remoteSocket = null;
let _remoteDecoder = null;
let _remoteConfigPacket = null;
let _remoteVideoSize = {width:272,height:600};
let _remoteFirstFrame = false;
let _remoteMoveAt = 0;
let _remoteSerial = '';
let _remoteGeneration = 0;
let _remoteTransport = '';
function remoteSocketUrl(deviceId){
const source = API || location.origin;
const base = source.replace(/^http:/,'ws:').replace(/^https:/,'wss:');
return `${base}/api/v3/adb/devices/${encodeURIComponent(deviceId)}/screen/remote.ws`;
}
function remoteSend(message){
if (!_remoteSocket || _remoteSocket.readyState !== WebSocket.OPEN) return false;
_remoteSocket.send(JSON.stringify(message));
return true;
}
function remotePoint(event){
const screen = document.getElementById('live-screen');
if (!screen || screen.style.display === 'none') return null;
const rect = screen.getBoundingClientRect();
if (!rect.width || !rect.height) return null;
return {
x:Math.round(Math.min(1,Math.max(0,(event.clientX-rect.left)/rect.width))*_remoteVideoSize.width),
y:Math.round(Math.min(1,Math.max(0,(event.clientY-rect.top)/rect.height))*_remoteVideoSize.height)
};
}
function phonePointerStart(event){
if (event.pointerType === 'mouse' && event.button !== 0) return;
event.currentTarget?.setPointerCapture?.(event.pointerId);
const point = remotePoint(event);
const remote = !!point && remoteSend({type:'touch',action:'down',...point,pointer_id:-1});
_phonePointerStart = {x:event.clientX,y:event.clientY,time:Date.now(),remote,lastPoint:point};
setControlFeedback(remote?'远程触控':'触控中', true);
}
function phonePointerMove(event){
if (!_phonePointerStart?.remote || Date.now()-_remoteMoveAt<16) return;
const point = remotePoint(event);
if (!point) return;
_remoteMoveAt = Date.now();
_phonePointerStart.lastPoint = point;
remoteSend({type:'touch',action:'move',...point,pointer_id:-1});
}
function phonePointerCancel(event){
if (_phonePointerStart?.remote && _phonePointerStart.lastPoint) {
remoteSend({type:'touch',action:'up',..._phonePointerStart.lastPoint,pointer_id:-1});
}
_phonePointerStart = null;
setControlFeedback(_remoteFirstFrame?(_remoteTransport==='agent-jpeg'?'扫码设备实时':'scrcpy 实时'):'实时在线', false);
}
async function phonePointerEnd(event){
if (!_phonePointerStart || !state.currentDevice) return;
const start = _phonePointerStart; _phonePointerStart = null;
const dx = event.clientX-start.x, dy = event.clientY-start.y;
const distance = Math.hypot(dx,dy);
try {
if (start.remote) {
const point = remotePoint(event) || start.lastPoint;
if (point) remoteSend({type:'touch',action:'up',...point,pointer_id:-1});
log(distance>14?'scrcpy 远程滑动已发送':'scrcpy 远程点击已发送','info');
return;
}
if (distance > 24) {
const direction = Math.abs(dx)>Math.abs(dy) ? (dx>0?'right':'left') : (dy>0?'down':'up');
await apiPost(`/api/v3/adb/devices/${state.currentDevice}/swipe`, {direction,scale:Math.min(.8,Math.max(.25,distance/420))});
log(`手机画面滑动:${direction}`,'info');
} else if (distance < 14) {
const screen = document.getElementById('live-screen');
if (!screen) return;
const rect = screen.getBoundingClientRect();
const x = Math.round(((event.clientX-rect.left)/rect.width)*(currentDeviceDisplay().width||1080));
const y = Math.round(((event.clientY-rect.top)/rect.height)*(currentDeviceDisplay().height||2400));
await apiPost(`/api/v3/adb/devices/${state.currentDevice}/click`, {x,y});
log(`手机画面点击:${x}, ${y}`,'info');
}
} catch(error) {
log(`手机画面操作失败:${error.message}`,'error');
} finally {
window.setTimeout(()=>setControlFeedback(_remoteFirstFrame?(_remoteTransport==='agent-jpeg'?'扫码设备实时':'scrcpy 实时'):'实时在线',false),80);
}
}
function currentDeviceDisplay(){
const device = (state.devices || []).find(item => item.device_id === state.currentDevice) || {};
return device.display || {width:1080,height:2400};
}
function liveScreenUrl(){
if (!state.currentDevice) return '';
return `${API}/api/v3/adb/devices/${encodeURIComponent(state.currentDevice)}/screen/live.mjpeg?fps=3&quality=65&max_width=600&t=${Date.now()}`;
}
function liveScreenReady(){
const status = document.getElementById('live-screen-status');
if (status) { status.className = 'live-device-status'; status.textContent = _remoteTransport==='agent-jpeg' ? '扫码设备实时' : 'scrcpy 实时'; }
}
function setControlFeedback(text, active=false){
const status = document.getElementById('live-screen-status');
const frame = document.getElementById('shot-box');
if (status) {
status.className = active ? 'live-device-status waiting' : 'live-device-status';
status.textContent = text;
}
frame?.classList.toggle('control-feedback', active);
}
function startRemoteScreen(deviceId=state.currentDevice){
if (!deviceId || !document.getElementById('live-screen')) return;
const generation = ++_remoteGeneration;
if (_remoteSocket) { try{_remoteSocket.close()}catch(_){} }
if (_remoteDecoder) { try{_remoteDecoder.close()}catch(_){} }
_remoteSerial = deviceId;
_remoteTransport = '';
_remoteFirstFrame = false;
_remoteConfigPacket = null;
const canvas = document.getElementById('live-screen');
const fallback = document.getElementById('live-screen-fallback');
if (canvas) canvas.style.display = 'block';
if (fallback) fallback.style.display = 'none';
setControlFeedback('远程连接中',true);
if (!('VideoDecoder' in window) || !('EncodedVideoChunk' in window)) {
startFallbackScreen('浏览器解码兼容模式');
return;
}
const context = canvas.getContext('2d',{alpha:false,desynchronized:true});
_remoteDecoder = new VideoDecoder({
output(frame){
if (canvas.width!==frame.displayWidth || canvas.height!==frame.displayHeight) {
canvas.width=frame.displayWidth; canvas.height=frame.displayHeight;
}
context.drawImage(frame,0,0,canvas.width,canvas.height);
frame.close();
if (!_remoteFirstFrame) {
_remoteFirstFrame=true;
liveScreenReady();
}
},
error(error){
log(`scrcpy 解码异常:${error.message}`,'error');
startFallbackScreen('视频解码兼容模式');
}
});
const socket = new WebSocket(remoteSocketUrl(deviceId));
_remoteSocket = socket;
socket.binaryType = 'arraybuffer';
socket.onmessage = event => {
if (generation !== _remoteGeneration) return;
if (typeof event.data === 'string') {
const message = JSON.parse(event.data);
if (message.type==='init') {
_remoteTransport=message.transport || 'scrcpy-h264';
_remoteVideoSize={width:Number(message.width)||272,height:Number(message.height)||600};
canvas.width=_remoteVideoSize.width; canvas.height=_remoteVideoSize.height;
if (message.transport==='agent-jpeg') {
_remoteDecoder.close(); _remoteDecoder=null;
setControlFeedback('扫码设备连接中',true);
} else {
_remoteDecoder.configure({codec:message.codec||'avc1.64000A',optimizeForLatency:true,hardwareAcceleration:'prefer-hardware'});
setControlFeedback('视频接收中',true);
}
} else if (message.type==='config' && _remoteDecoder?.state!=='closed') {
try{_remoteDecoder.configure({codec:message.codec||'avc1.64000A',optimizeForLatency:true,hardwareAcceleration:'prefer-hardware'})}catch(_){}
} else if (message.type==='error') {
log(`scrcpy 连接失败:${message.message}`,'error');
}
return;
}
const bytes = new Uint8Array(event.data);
if (bytes.byteLength<10) return;
const view = new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);
const kind = view.getUint8(0);
const timestamp = Number(view.getBigUint64(1,false));
const payload = bytes.slice(9);
if (kind===3) {
createImageBitmap(new Blob([payload],{type:'image/jpeg'})).then(frame=>{
if (generation!==_remoteGeneration) { frame.close(); return; }
if (canvas.width!==frame.width || canvas.height!==frame.height) { canvas.width=frame.width; canvas.height=frame.height; }
context.drawImage(frame,0,0,canvas.width,canvas.height); frame.close();
if (!_remoteFirstFrame) { _remoteFirstFrame=true; liveScreenReady(); }
}).catch(()=>{ if (!_remoteFirstFrame) startFallbackScreen('远程画面重试'); });
return;
}
if (!_remoteDecoder || _remoteDecoder.state!=='configured') return;
if (kind===0) { _remoteConfigPacket=payload; return; }
let data=payload;
if (kind===1 && _remoteConfigPacket) {
data=new Uint8Array(_remoteConfigPacket.byteLength+payload.byteLength);
data.set(_remoteConfigPacket); data.set(payload,_remoteConfigPacket.byteLength);
}
try {
_remoteDecoder.decode(new EncodedVideoChunk({type:kind===1?'key':'delta',timestamp,duration:33333,data}));
} catch(error) {
if (!_remoteFirstFrame) startFallbackScreen('视频解码兼容模式');
}
};
socket.onerror = () => {
if (generation !== _remoteGeneration) return;
if (!_remoteFirstFrame) startFallbackScreen('远程视频兼容模式');
};
socket.onclose = () => {
if (generation !== _remoteGeneration) return;
if (!_remoteFirstFrame && _remoteSerial===deviceId) startFallbackScreen('远程视频兼容模式');
else if (_remoteSerial===deviceId) setControlFeedback('远程已断开',false);
};
}
function startFallbackScreen(reason='兼容模式'){
++_remoteGeneration;
if (_remoteSocket) { try{_remoteSocket.close()}catch(_){} }
const canvas=document.getElementById('live-screen');
const image=document.getElementById('live-screen-fallback');
if (canvas) canvas.style.display='none';
if (image) { image.style.display='block'; image.src=liveScreenUrl(); }
const status=document.getElementById('live-screen-status');
if (status) { status.className='live-device-status waiting'; status.textContent=reason; }
}
function fallbackScreenReady(){
const status=document.getElementById('live-screen-status');
if (status) { status.className='live-device-status waiting'; status.textContent='兼容画面'; }
}
function liveScreenFailed(image){
const status = document.getElementById('live-screen-status');
if (status) { status.className = 'live-device-status error'; status.textContent = '连接重试'; }
window.setTimeout(() => {
if (image?.isConnected && state.currentDevice) image.src = liveScreenUrl();
}, 1600);
}
function restartLiveScreen(){
const status = document.getElementById('live-screen-status');
if (status) { status.className = 'live-device-status waiting'; status.textContent = '正在重连'; }
startRemoteScreen(state.currentDevice);
log('scrcpy 远程视频正在重新连接','info');
}
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();
window.setTimeout(() => {
if (state.current === 'devices' && state.currentDevice === deviceId) loadDeviceDetail(deviceId);
}, 3000);
log(`已切换设备:${deviceId}`, 'info');
}
async function loadDeviceDetail(deviceId){
try{
const preserveLiveScreen = state.current === 'devices'
&& state.deviceView === 'detail'
&& state.currentDevice === deviceId
&& !!document.getElementById('live-screen');
const [detailRes, modesRes, servicesRes] = await Promise.all([
apiGet(`/api/v3/devices/${deviceId}`),
apiGet(`/api/v3/connection/modes/${deviceId}`).catch(() => null),
apiGet(`/api/v3/adb/devices/${deviceId}/services`).catch(() => null)
]);
let detail = detailRes.data || {};
const modes = modesRes?.data || {};
const services = servicesRes?.data || servicesRes || {};
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,
device_id: deviceId,
connection_modes: modes.modes || [],
best_mode: modes.best_mode || null,
service_summary: services.services || detail.service_summary || []
};
state.devices = (state.devices || []).map(item => item.device_id === deviceId ? {...item, ...merged} : item);
if (preserveLiveScreen) patchDeviceDetailSummary(merged);
else if (state.current === 'devices') renderCurrentView();
}catch(error){
log(`设备详情加载失败:${error.message}`, 'error');
}
}
function patchDeviceDetailSummary(device){
const setText = (id, value) => {
const element = document.getElementById(id);
if (element && value !== undefined && value !== null && value !== '') element.textContent = String(value);
};
setText('device-summary-name', device.profile_nickname || device.nickname || device.model || device.device_id);
setText('device-summary-id', `${[device.brand,device.model].filter(Boolean).join(' ') || 'Android'} · ${device.device_id}`);
setText('device-metric-android', device.android_version || device.sdk_version || '-');
setText('device-metric-app', device.current_app || (device.wechat_running ? '微信' : '-'));
setText('device-metric-battery', device.battery_level >= 0 ? `${device.battery_level}%${device.battery_charging ? ' ⚡' : ''}` : '-');
const services = Array.isArray(device.service_summary) ? device.service_summary : [];
const serviceRow = document.querySelector('.service-summary-row');
if (serviceRow && services.length) {
serviceRow.innerHTML = services.map(service => `<span class="service-chip ${service.running?'ready':''}"><i></i>${esc(service.name)} · ${service.running?'运行中':'待配置'}</span>`).join('');
}
}
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)]);
await refreshRealtimeStatus(silent);
if (!silent && state.currentDevice) await loadDeviceDetail(state.currentDevice);
}
async function refreshAll(){
await Promise.all([refreshOverview(), refreshConnection(), refreshDocs()]);
await refreshRealtimeStatus();
log('全部数据已刷新', 'info');
}
async function refreshRealtimeStatus(silent=true){
try {
const res = await apiGet('/api/v3/integration/realtime/status?consumer=cunkebao&event_limit=20');
const data = res.data || res || {};
state.realtime = {
devices: Array.isArray(data.devices) ? data.devices : [],
recent_events: Array.isArray(data.recent_events) ? data.recent_events : [],
};
state.realtimeError = '';
const liveById = new Map(state.realtime.devices.map(item => [item.device_id, item]));
state.devices = (state.devices || []).map(device => {
const live = liveById.get(device.device_id);
return live ? {...device, ...live, status: live.status || device.status} : device;
});
if (!silent && state.current === 'devices') renderCurrentView(false);
} catch (error) {
state.realtimeError = error.message || String(error);
if (!silent) log(`实时状态加载失败:${state.realtimeError}`, 'error');
}
}
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.devices.length && !state.devices.some(device => device.device_id === state.currentDevice)) {
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');
}
}
async function refreshAntiban(){
try {
const qs = state.currentDevice ? `?device_id=${encodeURIComponent(state.currentDevice)}` : '';
const res = await apiGet(`/api/v3/antiban/status${qs}`);
state.antibanStatus = res.data || {};
if (state.current === 'engine' && state.engineTab === 'antiban') renderCurrentView(false);
} catch(e) {
log('防封状态加载失败: ' + e.message, 'error');
}
}
/* ═══ View Switching ═══ */
let _lastView = '';
function renderCurrentView(animate){
const view = document.getElementById('view');
const stats = document.getElementById('stats');
if (stats) stats.style.display = 'none';
const renderers = {
overview: renderOverviewClean,
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.id !== 'live-screen' && (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.deviceView === 'detail') {
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.deviceView === 'detail') {
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 || [];
const preferred = state.discoveredServers.find(server => server.is_self) || state.discoveredServers[0];
if (preferred?.ws_url) state.bindingServer = preferred.ws_url.replace(/\/ws\/device\/?$/, '/ws/device');
state.discoveryStatus = 'done';
log(`局域网扫描完成:发现 ${state.discoveredServers.length} 个服务,绑定基准已切换到 ${state.bindingServer || '当前服务器'}`, '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();
if (!await ensureConsoleAuthenticated()) {
document.getElementById('view').innerHTML = `<section class="panel glass" style="text-align:center;padding:64px 24px">
<div style="font-size:44px;margin-bottom:12px">🔐</div>
<h2>登录工作手机控制台</h2>
<p class="sub">管理员账号用于网页控制台API Key 仅放在存客宝后端或宝塔环境变量中。</p>
<div style="max-width:360px;margin:20px auto 0;display:grid;gap:10px;text-align:left">
<input id="console-username" value="admin" autocomplete="username" placeholder="账号" style="padding:12px 14px;border-radius:12px;border:1px solid var(--border)">
<input id="console-password" type="password" autocomplete="current-password" placeholder="密码" onkeydown="if(event.key==='Enter')loginConsole()" style="padding:12px 14px;border-radius:12px;border:1px solid var(--border)">
<div id="console-login-error" style="min-height:20px;color:var(--red);font-size:12px"></div>
<button class="btn primary" onclick="loginConsole()">登录并加载设备</button>
</div>
<div style="max-width:620px;margin:22px auto 0;text-align:left;background:rgba(37,99,235,.06);border:1px solid rgba(37,99,235,.16);border-radius:14px;padding:14px 16px;font-size:12px;line-height:1.8;color:var(--sub)">
<b style="color:var(--text)">授权配置位置</b><br>
1. 宝塔:<code>/www/wwwroot/workphone-sdk/.env</code> 配置 <code>API_KEY</code>、<code>DEVICE_PAIRING_TOKEN</code>、<code>CONSOLE_PASSWORD</code>,然后重启 <code>workphone-sdk-baota</code>。<br>
2. 存客宝后端:配置 <code>WORKPHONE_SDK_URL=https://wpsdk.quwanzhi.com</code>、<code>WORKPHONE_SDK_KEY=同一API_KEY</code>。<br>
3. 手机扫码:登录后进入“手机设备”,点击“生成绑定二维码”;二维码只包含工作手机服务地址与配对令牌,不包含 API Key。
</div>
</section>`;
updateConsoleAuthChip(false);
return false;
}
await Promise.all([
refreshOverview(),
refreshDocs(),
refreshConnection(),
refreshHook(),
refreshHeartbeat(),
refreshAntiban(),
refreshGateway(),
loadBrainDashboard(),
loadServerInfo(),
]);
await refreshRealtimeStatus();
if (state.currentDevice) await loadDeviceDetail(state.currentDevice);
renderCurrentView();
updateConnBar();
setTimeout(() => {
const ov = state.overview || {};
if ((ov.ws_online ?? 0) === 0 && (ov.adb_count ?? 0) === 0) {
log('未检测到在线设备,正在自动扫描局域网...', 'info');
scanLanDevices();
}
}, 2000);
return true;
}
bootstrap().then(ok => { if (ok) startPolling(); });
let _pollTimer = null;
function startPolling(){
if (_pollTimer) clearInterval(_pollTimer);
_pollTimer = setInterval(async () => {
await refreshOverview(true);
await refreshRealtimeStatus(true);
updateConnBar();
}, 30000);
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) { clearInterval(_pollTimer); _pollTimer = null; }
else if (consoleAuthenticated) { startPolling(); refreshOverview(true).then(() => refreshRealtimeStatus(true)).then(updateConnBar); }
});
</script>
</body>
</html>