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

522 lines
33 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.0">
<title>微信全链路控制面板 — 工作手机SDK</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--bg:#0a0e17;--card:#131a2b;--border:#1e2940;--accent:#3b82f6;--accent2:#22c55e;--accent3:#f59e0b;--text:#e2e8f0;--text2:#94a3b8;--danger:#ef4444;--glass:rgba(30,41,64,0.7)}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
.app{display:flex;height:100vh}
.sidebar{width:220px;background:var(--card);border-right:1px solid var(--border);overflow-y:auto;flex-shrink:0}
.sidebar h2{padding:16px;font-size:14px;color:var(--accent);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
.nav-group{padding:4px 0}
.nav-group-title{padding:8px 16px;font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:1px}
.nav-item{padding:8px 16px 8px 24px;cursor:pointer;font-size:13px;color:var(--text2);transition:all .15s;display:flex;align-items:center;gap:6px}
.nav-item:hover{background:rgba(59,130,246,0.1);color:var(--text)}
.nav-item.active{background:rgba(59,130,246,0.15);color:var(--accent);border-right:2px solid var(--accent)}
.nav-item .count{margin-left:auto;font-size:10px;background:var(--border);padding:1px 6px;border-radius:8px}
.main{flex:1;display:flex;flex-direction:column;overflow:hidden}
.header{padding:12px 20px;background:var(--card);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:16px}
.header select{background:var(--bg);color:var(--text);border:1px solid var(--border);padding:6px 12px;border-radius:6px;font-size:13px}
.status-pill{padding:4px 12px;border-radius:12px;font-size:11px;font-weight:600}
.status-online{background:rgba(34,197,94,0.15);color:var(--accent2)}
.status-offline{background:rgba(239,68,68,0.15);color:var(--danger)}
.channel-badge{padding:3px 8px;border-radius:4px;font-size:10px;font-weight:600;letter-spacing:.5px}
.ch-hook{background:rgba(168,85,247,0.2);color:#a855f7}
.ch-u2{background:rgba(59,130,246,0.2);color:#3b82f6}
.ch-adb{background:rgba(245,158,11,0.2);color:#f59e0b}
.content{flex:1;display:flex;overflow:hidden}
.panel{flex:1;overflow-y:auto;padding:20px}
.right-panel{width:360px;border-left:1px solid var(--border);overflow-y:auto;display:flex;flex-direction:column}
.section{margin-bottom:20px}
.section-title{font-size:13px;font-weight:600;color:var(--accent);margin-bottom:10px;display:flex;align-items:center;gap:6px}
.card{background:var(--glass);border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:12px}
.form-group{margin-bottom:12px}
.form-group label{display:block;font-size:12px;color:var(--text2);margin-bottom:4px}
.form-group input,.form-group textarea,.form-group select{width:100%;background:var(--bg);color:var(--text);border:1px solid var(--border);padding:8px 12px;border-radius:6px;font-size:13px}
.form-group textarea{resize:vertical;min-height:60px}
.btn{padding:8px 16px;border:none;border-radius:6px;font-size:13px;cursor:pointer;font-weight:500;transition:all .15s}
.btn-primary{background:var(--accent);color:#fff}
.btn-primary:hover{background:#2563eb}
.btn-success{background:var(--accent2);color:#fff}
.btn-danger{background:var(--danger);color:#fff}
.btn-sm{padding:5px 10px;font-size:11px}
.btn-group{display:flex;gap:8px;flex-wrap:wrap}
.result-box{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:12px;font-family:monospace;font-size:12px;max-height:300px;overflow-y:auto;white-space:pre-wrap;word-break:break-all}
.chain-step{display:flex;align-items:center;gap:8px;padding:6px 0;font-size:12px}
.chain-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.chain-dot.active{background:var(--accent2);box-shadow:0 0 6px var(--accent2)}
.chain-dot.pending{background:var(--text2)}
.chain-line{width:1px;height:16px;background:var(--border);margin-left:3.5px}
.log-entry{padding:4px 0;font-size:11px;color:var(--text2);border-bottom:1px solid rgba(30,41,64,0.5)}
.log-entry .time{color:var(--accent);margin-right:6px}
.log-entry .action{color:var(--accent3)}
.log-entry .ok{color:var(--accent2)}
.log-entry .fail{color:var(--danger)}
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.stat-card{background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px;text-align:center}
.stat-card .val{font-size:20px;font-weight:700;color:var(--accent)}
.stat-card .lbl{font-size:10px;color:var(--text2);margin-top:2px}
.api-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px}
.api-btn{background:var(--bg);border:1px solid var(--border);border-radius:4px;padding:6px 8px;font-size:11px;cursor:pointer;text-align:left;color:var(--text2);transition:all .15s}
.api-btn:hover{border-color:var(--accent);color:var(--text)}
.api-btn .method{color:var(--accent2);font-size:9px;font-weight:700}
@media(max-width:900px){.sidebar{width:180px}.right-panel{width:280px}}
@media(max-width:700px){.sidebar{display:none}.right-panel{display:none}}
</style>
</head>
<body>
<div class="app">
<div class="sidebar">
<h2>微信控制</h2>
<div class="nav-group">
<div class="nav-group-title">核心功能</div>
<div class="nav-item active" onclick="showModule('message')">消息管理 <span class="count">6</span></div>
<div class="nav-item" onclick="showModule('friend')">好友管理 <span class="count">8</span></div>
<div class="nav-item" onclick="showModule('group')">群聊管理 <span class="count">10</span></div>
<div class="nav-item" onclick="showModule('moments')">朋友圈 <span class="count">8</span></div>
<div class="nav-item" onclick="showModule('tag')">标签管理 <span class="count">6</span></div>
</div>
<div class="nav-group">
<div class="nav-group-title">个人 & 安全</div>
<div class="nav-item" onclick="showModule('profile')">个人设置 <span class="count">6</span></div>
<div class="nav-item" onclick="showModule('security')">账号安全 <span class="count">9</span></div>
<div class="nav-item" onclick="showModule('payment')">支付 <span class="count">7</span></div>
</div>
<div class="nav-group">
<div class="nav-group-title">更多功能</div>
<div class="nav-item" onclick="showModule('video')">视频号 <span class="count">6</span></div>
<div class="nav-item" onclick="showModule('scan')">扫一扫 <span class="count">4</span></div>
<div class="nav-item" onclick="showModule('chat_settings')">聊天设置 <span class="count">3</span></div>
<div class="nav-item" onclick="showModule('favorites')">收藏 <span class="count">2</span></div>
<div class="nav-item" onclick="showModule('miniapp')">小程序/公众号 <span class="count">2</span></div>
<div class="nav-item" onclick="showModule('call')">通话 <span class="count">2</span></div>
<div class="nav-item" onclick="showModule('discover')">群发/搜索/发现 <span class="count">3</span></div>
<div class="nav-item" onclick="showModule('sport')">运动/位置/表情 <span class="count">6</span></div>
<div class="nav-item" onclick="showModule('media')">语音/文件 <span class="count">3</span></div>
<div class="nav-item" onclick="showModule('settings')">设置管理 <span class="count">5</span></div>
</div>
<div class="nav-group">
<div class="nav-group-title">系统</div>
<div class="nav-item" onclick="showModule('chain')">全链路监控</div>
<div class="nav-item" onclick="showModule('all_api')">全部API端点</div>
</div>
</div>
<div class="main">
<div class="header">
<span style="font-weight:600;font-size:14px">设备:</span>
<select id="deviceSelect" onchange="switchDevice()"><option value="">加载中...</option></select>
<span id="deviceStatus" class="status-pill status-offline">离线</span>
<span style="font-size:12px;color:var(--text2)">通道:</span>
<span id="channelBadge" class="channel-badge ch-u2">u2</span>
<button type="button" class="btn btn-sm btn-primary" onclick="loadDevices()" title="重新扫描本机 ADB 与在线 Agent">刷新设备</button>
<span style="margin-left:auto;font-size:11px;color:var(--text2)" id="apiCount">96 actions</span>
</div>
<div class="content">
<div class="panel" id="modulePanel"></div>
<div class="right-panel">
<div style="padding:12px">
<div class="section">
<div class="section-title">全链路状态</div>
<div id="chainStatus">
<div class="chain-step"><div class="chain-dot active"></div> 网页控制面板</div>
<div class="chain-line"></div>
<div class="chain-step"><div class="chain-dot" id="cd1"></div> <span id="cs1">unified.py (API)</span></div>
<div class="chain-line"></div>
<div class="chain-step"><div class="chain-dot" id="cd2"></div> <span id="cs2">通道路由</span></div>
<div class="chain-line"></div>
<div class="chain-step"><div class="chain-dot" id="cd3"></div> <span id="cs3">Agent/ADB</span></div>
<div class="chain-line"></div>
<div class="chain-step"><div class="chain-dot" id="cd4"></div> <span id="cs4">Hook/u2/Shell</span></div>
<div class="chain-line"></div>
<div class="chain-step"><div class="chain-dot" id="cd5"></div> <span id="cs5">微信APP</span></div>
</div>
</div>
<div class="section">
<div class="section-title">执行统计</div>
<div class="stats-grid">
<div class="stat-card"><div class="val" id="totalOps">0</div><div class="lbl">总操作</div></div>
<div class="stat-card"><div class="val" id="successRate">-</div><div class="lbl">成功率</div></div>
<div class="stat-card"><div class="val" id="avgTime">-</div><div class="lbl">平均耗时</div></div>
<div class="stat-card"><div class="val" id="lastChannel">-</div><div class="lbl">最近通道</div></div>
</div>
</div>
<div class="section">
<div class="section-title">执行结果</div>
<div class="result-box" id="resultBox">等待操作...</div>
</div>
<div class="section">
<div class="section-title">操作日志</div>
<div id="logBox" style="max-height:200px;overflow-y:auto"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const BASE='/api/v3';
/** API 返回 data 可能是设备数组,或 { devices: [] } */
function normalizeDeviceArray(json){
if(!json||typeof json!=='object')return[];
const d=json.data;
if(Array.isArray(d))return d;
if(d&&Array.isArray(d.devices))return d.devices;
if(Array.isArray(json.devices))return json.devices;
return [];
}
let loadedDeviceList=[];
let currentDevice='';
let stats={total:0,success:0,totalTime:0};
const MODULES={
message:{title:'消息管理',desc:'发送/获取/转发/撤回消息',channel:'Hook优先(发消息走Frida RPC ~200ms) | u2降级(~5s)',actions:[
{name:'发送消息',endpoint:'/message/send',method:'POST',fields:[{k:'to_id',l:'联系人',ph:'昵称或备注'},{k:'content',l:'内容',ph:'消息内容',type:'textarea'},{k:'msg_type',l:'类型',ph:'text',v:'text'}]},
{name:'获取消息',endpoint:'/message/list',method:'POST',fields:[{k:'conversation_id',l:'会话',ph:'联系人名称'},{k:'limit',l:'数量',ph:'20',v:'20'}]},
{name:'批量发送',endpoint:'/message/batch-send',method:'POST',fields:[{k:'to_ids',l:'接收者(逗号分隔)',ph:'张三,李四'},{k:'content',l:'内容',ph:'消息',type:'textarea'},{k:'interval',l:'间隔秒',v:'2'}]},
{name:'转发消息',endpoint:'/message/forward',method:'POST',fields:[{k:'to_id',l:'转发给',ph:'联系人'},{k:'content',l:'内容',ph:''}]},
{name:'撤回消息',endpoint:'/message/recall',method:'POST',fields:[]},
{name:'发送名片',endpoint:'/message/send-card',method:'POST',fields:[{k:'to_id',l:'发给谁',ph:'联系人'},{k:'card_wxid',l:'名片',ph:'微信号'}]}
]},
friend:{title:'好友管理',desc:'添加/通过/备注/删除/搜索好友',channel:'Hook可读(联系人列表走SQLite ~100ms) | u2写操作(~5s)',actions:[
{name:'添加好友',endpoint:'/friend/add',method:'POST',fields:[{k:'user_id',l:'微信号',ph:'微信号/手机号'},{k:'message',l:'验证消息',ph:'你好'}]},
{name:'通过请求',endpoint:'/friend/accept',method:'POST',fields:[{k:'user_id',l:'用户',ph:'可选'}]},
{name:'设置备注',endpoint:'/friend/set-remark',method:'POST',fields:[{k:'user_id',l:'好友',ph:'昵称'},{k:'remark',l:'备注',ph:'新备注'}]},
{name:'删除好友',endpoint:'/friend/delete',method:'POST',fields:[{k:'user_id',l:'好友',ph:'昵称'}]},
{name:'获取联系人',endpoint:'/contacts',method:'GET',fields:[{k:'limit',l:'数量',v:'100'}]},
{name:'搜索联系人',endpoint:'/contacts/search',method:'GET',fields:[{k:'keyword',l:'关键词',ph:'搜索'}]},
{name:'好友详情',endpoint:'/friend/info',method:'GET',fields:[{k:'user_id',l:'好友',ph:'昵称'}]},
{name:'批量添加',endpoint:'/friend/batch-add',method:'POST',fields:[{k:'user_ids',l:'ID列表(逗号)',ph:'id1,id2'},{k:'message',l:'验证消息',ph:''}]}
]},
group:{title:'群聊管理',desc:'创建/邀请/移除/公告/群名/群消息',channel:'Hook可读(群列表/成员走SQLite) | u2写操作',actions:[
{name:'创建群聊',endpoint:'/group/create',method:'POST',fields:[{k:'group_name',l:'群名',ph:'新群'},{k:'member_ids',l:'成员(逗号)',ph:'张三,李四'}]},
{name:'邀请入群',endpoint:'/group/invite',method:'POST',fields:[{k:'group_id',l:'群名',ph:''},{k:'member_ids',l:'邀请(逗号)',ph:''}]},
{name:'移出群聊',endpoint:'/group/remove',method:'POST',fields:[{k:'group_id',l:'群名',ph:''},{k:'member_ids',l:'移除(逗号)',ph:''}]},
{name:'群公告',endpoint:'/group/set-notice',method:'POST',fields:[{k:'group_id',l:'群名',ph:''},{k:'notice',l:'公告',ph:'',type:'textarea'}]},
{name:'群名',endpoint:'/group/set-name',method:'POST',fields:[{k:'group_id',l:'群名',ph:''},{k:'group_name',l:'新群名',ph:''}]},
{name:'群消息',endpoint:'/group/send-message',method:'POST',fields:[{k:'group_id',l:'群名',ph:''},{k:'content',l:'内容',ph:'',type:'textarea'}]},
{name:'群欢迎语',endpoint:'/group/set-welcome',method:'POST',fields:[{k:'group_id',l:'群名',ph:''},{k:'welcome_text',l:'欢迎语',ph:''}]},
{name:'群列表',endpoint:'/group/list',method:'GET',fields:[{k:'limit',l:'数量',v:'100'}]},
{name:'群成员',endpoint:'/group/members',method:'GET',fields:[{k:'group_id',l:'群名',ph:''}]},
{name:'退出群聊',endpoint:'/group/quit',method:'POST',fields:[{k:'group_id',l:'群名',ph:''}]}
]},
moments:{title:'朋友圈',desc:'发布/浏览/点赞/评论/删除/隐私设置',channel:'u2 UI自动化(~5-10s)',actions:[
{name:'发布朋友圈',endpoint:'/moments/post',method:'POST',fields:[{k:'content',l:'文字',ph:'',type:'textarea'}]},
{name:'点赞',endpoint:'/moments/like',method:'POST',fields:[{k:'user_id',l:'好友',ph:''},{k:'post_index',l:'第几条',v:'0'}]},
{name:'评论',endpoint:'/moments/comment',method:'POST',fields:[{k:'user_id',l:'好友',ph:''},{k:'comment',l:'评论',ph:''},{k:'post_index',l:'第几条',v:'0'}]},
{name:'获取朋友圈',endpoint:'/moments/list',method:'POST',fields:[{k:'user_id',l:'好友(空=自己)',ph:''},{k:'limit',l:'数量',v:'10'}]},
{name:'删除',endpoint:'/moments/delete',method:'POST',fields:[{k:'post_index',l:'第几条',v:'0'}]},
{name:'设置封面',endpoint:'/moments/set-cover',method:'POST',fields:[]},
{name:'可见天数',endpoint:'/moments/set-privacy',method:'POST',fields:[{k:'days',l:'天数',v:'180'}]},
{name:'分享链接',endpoint:'/moments/share-link',method:'POST',fields:[{k:'url',l:'URL',ph:''},{k:'title',l:'标题',ph:''}]}
]},
tag:{title:'标签管理',desc:'创建/删除/打标/查标',channel:'Hook可读(标签列表走SQLite) | u2写操作',actions:[
{name:'创建标签',endpoint:'/tag/create',method:'POST',fields:[{k:'tag_name',l:'标签名',ph:''}]},
{name:'删除标签',endpoint:'/tag/delete',method:'POST',fields:[{k:'tag_name',l:'标签名',ph:''}]},
{name:'标签列表',endpoint:'/tag/list',method:'GET',fields:[]},
{name:'打标签',endpoint:'/tag/add',method:'POST',fields:[{k:'user_id',l:'好友',ph:''},{k:'tags',l:'标签(逗号)',ph:'VIP,新客'}]},
{name:'移除标签',endpoint:'/tag/remove',method:'POST',fields:[{k:'user_id',l:'好友',ph:''},{k:'tags',l:'标签(逗号)',ph:''}]},
{name:'按标签查人',endpoint:'/tag/users',method:'POST',fields:[{k:'tag_name',l:'标签名',ph:''}]}
]},
profile:{title:'个人设置',desc:'昵称/签名/头像/性别/地区/资料',channel:'Hook可读(get_profile走SQLite userinfo ~50ms) | u2写操作(~6s)',actions:[
{name:'获取资料',endpoint:'/profile/get',method:'GET',fields:[]},
{name:'修改昵称',endpoint:'/profile/set-nickname',method:'POST',fields:[{k:'nickname',l:'昵称',ph:''}]},
{name:'修改签名',endpoint:'/profile/set-signature',method:'POST',fields:[{k:'signature',l:'签名',ph:''}]},
{name:'修改头像',endpoint:'/profile/set-avatar',method:'POST',fields:[]},
{name:'设置性别',endpoint:'/profile/set-gender',method:'POST',fields:[{k:'gender',l:'性别',ph:'男/女'}]},
{name:'设置地区',endpoint:'/profile/set-region',method:'POST',fields:[{k:'region',l:'地区',ph:'中国 福建 厦门'}]}
]},
security:{title:'账号安全',desc:'状态检查/解封/改密码/限制',channel:'u2 UI自动化(~5-10s)',actions:[
{name:'账号状态',endpoint:'/account/status',method:'GET',fields:[]},
{name:'安全中心',endpoint:'/account/safety-center',method:'GET',fields:[]},
{name:'微信解封',endpoint:'/account/unblock',method:'POST',fields:[]},
{name:'自助解封',endpoint:'/account/unblock-self',method:'POST',fields:[]},
{name:'申诉解封',endpoint:'/account/unblock-appeal',method:'POST',fields:[]},
{name:'短信解封',endpoint:'/account/unblock-sms',method:'POST',fields:[{k:'phone',l:'手机号',ph:''}]},
{name:'修改密码',endpoint:'/account/change-password',method:'POST',fields:[{k:'old_pwd',l:'旧密码',ph:''},{k:'new_pwd',l:'新密码',ph:''}]},
{name:'功能限制',endpoint:'/account/restrictions',method:'GET',fields:[]},
{name:'申诉限制',endpoint:'/account/appeal-restriction',method:'POST',fields:[]}
]},
payment:{title:'支付',desc:'红包/转账/收款/钱包/账单',channel:'u2 UI自动化(安全考虑不走Hook, ~5-10s)',actions:[
{name:'发红包',endpoint:'/payment/red-packet',method:'POST',fields:[{k:'to_id',l:'发给谁',ph:''},{k:'amount',l:'金额',ph:''},{k:'message',l:'祝福语',v:'恭喜发财'}]},
{name:'转账',endpoint:'/payment/transfer',method:'POST',fields:[{k:'to_id',l:'转给谁',ph:''},{k:'amount',l:'金额',ph:''}]},
{name:'付款码',endpoint:'/payment/code',method:'GET',fields:[]},
{name:'收款',endpoint:'/payment/receive',method:'POST',fields:[]},
{name:'钱包',endpoint:'/payment/wallet',method:'GET',fields:[]},
{name:'账单',endpoint:'/payment/transactions',method:'GET',fields:[]},
{name:'领红包',endpoint:'/payment/receive-red-packet',method:'POST',fields:[]}
]},
video:{title:'视频号',desc:'浏览/点赞/评论/关注/分享',channel:'u2 UI自动化(~4-6s)',actions:[
{name:'打开视频号',endpoint:'/video-channel/list',method:'GET',fields:[]},
{name:'点赞视频',endpoint:'/video-channel/like',method:'POST',fields:[{k:'index',l:'第几个',v:'0'}]},
{name:'评论视频',endpoint:'/video-channel/comment',method:'POST',fields:[{k:'index',l:'第几个',v:'0'},{k:'comment',l:'评论',ph:''}]},
{name:'关注创作者',endpoint:'/video-channel/follow',method:'POST',fields:[{k:'index',l:'第几个',v:'0'}]},
{name:'分享视频',endpoint:'/video-channel/share',method:'POST',fields:[{k:'index',l:'第几个',v:'0'},{k:'to_id',l:'分享给',ph:''}]},
{name:'视频列表',endpoint:'/video-channel/list',method:'GET',fields:[{k:'limit',l:'数量',v:'10'}]}
]},
scan:{title:'扫一扫',desc:'扫码/加好友/我的二维码',channel:'u2 UI自动化(~3-5s)',actions:[
{name:'扫一扫',endpoint:'/scan/qr-code',method:'POST',fields:[]},
{name:'扫码加好友',endpoint:'/scan/add-friend',method:'POST',fields:[]},
{name:'我的二维码',endpoint:'/scan/my-qr',method:'GET',fields:[]},
{name:'图片识别码',endpoint:'/scan/extract-qr',method:'POST',fields:[]}
]},
chat_settings:{title:'聊天设置',desc:'置顶/免打扰/清空',channel:'u2 UI自动化(~5s)',actions:[
{name:'置顶聊天',endpoint:'/chat/set-top',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''},{k:'enable',l:'开启',v:'true'}]},
{name:'免打扰',endpoint:'/chat/set-mute',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''},{k:'enable',l:'开启',v:'true'}]},
{name:'清空记录',endpoint:'/chat/clear-history',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''}]}
]},
favorites:{title:'收藏管理',desc:'收藏/列表',channel:'Hook可读(Favorite.db ~100ms) | u2写操作',actions:[
{name:'收藏',endpoint:'/favorites/add',method:'POST',fields:[]},
{name:'收藏列表',endpoint:'/favorites/list',method:'GET',fields:[{k:'limit',l:'数量',v:'20'}]}
]},
miniapp:{title:'小程序 & 公众号',desc:'',channel:'u2 UI自动化(~8s)',actions:[
{name:'打开小程序',endpoint:'/miniprogram/open',method:'POST',fields:[{k:'name',l:'名称',ph:''}]},
{name:'关注公众号',endpoint:'/official-account/follow',method:'POST',fields:[{k:'account_name',l:'公众号名',ph:''}]}
]},
call:{title:'通话',desc:'语音/视频通话',channel:'u2 UI自动化(~6s)',actions:[
{name:'语音通话',endpoint:'/call/voice',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''}]},
{name:'视频通话',endpoint:'/call/video',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''}]}
]},
discover:{title:'群发/搜索/发现',desc:'群发助手/搜一搜/看一看',channel:'u2(~3-10s) | Hook(搜一搜走SQLite)',actions:[
{name:'群发消息',endpoint:'/mass-send',method:'POST',fields:[{k:'content',l:'内容',ph:'',type:'textarea'},{k:'user_ids',l:'用户(逗号)',ph:''}]},
{name:'搜一搜',endpoint:'/search/wechat',method:'GET',fields:[{k:'keyword',l:'关键词',ph:''}]},
{name:'看一看',endpoint:'/discover/top-stories',method:'GET',fields:[]}
]},
sport:{title:'运动/位置/表情',desc:'',channel:'u2 UI自动化(~5s)',actions:[
{name:'获取步数',endpoint:'/wechat-sport/steps',method:'GET',fields:[]},
{name:'点赞步数',endpoint:'/wechat-sport/like-steps',method:'POST',fields:[{k:'user_id',l:'好友',ph:''}]},
{name:'发送位置',endpoint:'/location/send',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''}]},
{name:'实时位置',endpoint:'/location/share-realtime',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''}]},
{name:'发送表情',endpoint:'/emoji/send',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''},{k:'emoji_name',l:'表情名',ph:''}]},
{name:'表情列表',endpoint:'/emoji/stickers',method:'GET',fields:[]}
]},
media:{title:'语音/文件',desc:'',channel:'u2 UI自动化(~3-6s)',actions:[
{name:'语音消息',endpoint:'/message/voice',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''},{k:'duration',l:'秒数',v:'3'}]},
{name:'发送文件',endpoint:'/file/send',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''}]},
{name:'下载文件',endpoint:'/file/download',method:'POST',fields:[{k:'user_id',l:'联系人',ph:''}]}
]},
settings:{title:'设置管理',desc:'勿扰/缓存/更新/退出/切换',channel:'u2 UI自动化(~5s)',actions:[
{name:'勿扰模式',endpoint:'/settings/do-not-disturb',method:'POST',fields:[{k:'enable',l:'开启',v:'true'}]},
{name:'清理缓存',endpoint:'/settings/clear-cache',method:'POST',fields:[]},
{name:'检查更新',endpoint:'/settings/check-update',method:'POST',fields:[]},
{name:'退出登录',endpoint:'/settings/logout',method:'POST',fields:[]},
{name:'切换账号',endpoint:'/settings/switch-account',method:'POST',fields:[]}
]}
};
function showModule(key){
document.querySelectorAll('.nav-item').forEach(e=>e.classList.remove('active'));
event&&event.target&&event.target.classList.add('active');
const panel=document.getElementById('modulePanel');
if(key==='chain'){panel.innerHTML=renderChainView();return}
if(key==='all_api'){panel.innerHTML=renderAllApi();return}
const m=MODULES[key];if(!m){panel.innerHTML='';return}
let h=`<div class="section"><div class="section-title">${m.title}</div>
<div class="card"><p style="font-size:12px;color:var(--text2);margin-bottom:8px">${m.desc}</p>
<p style="font-size:11px;margin-bottom:12px"><span class="channel-badge ch-hook" style="margin-right:4px">通道</span> ${m.channel}</p></div></div>`;
m.actions.forEach((a,i)=>{
h+=`<div class="card"><div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-weight:600;font-size:13px">${a.name}</span>
<span style="font-size:10px;color:var(--text2)">${a.method} ${a.endpoint}</span></div>
<form onsubmit="return execAction('${key}',${i},this)">`;
a.fields.forEach(f=>{
h+=`<div class="form-group"><label>${f.l}</label>`;
if(f.type==='textarea')h+=`<textarea name="${f.k}" placeholder="${f.ph||''}">${f.v||''}</textarea>`;
else h+=`<input name="${f.k}" placeholder="${f.ph||''}" value="${f.v||''}">`;
h+=`</div>`;
});
h+=`<button type="submit" class="btn btn-primary">执行</button></form></div>`;
});
panel.innerHTML=h;
}
function renderChainView(){
return `<div class="section"><div class="section-title">微信全链路控制架构</div>
<div class="card"><pre style="font-size:12px;line-height:1.8;color:var(--text2)">
网页控制面板 (本页面)
|
| HTTP REST API
v
unified.py (98个API端点 / 29个功能模块)
|
|--- 通道路由 ChannelRouter ---+
| |
v v
WebSocket Hub ADB Engine
(Agent在线) (Agent离线)
| |
v v
Agent (设备端Python) wechat_adb_engine.py
| (96个action, ADB Shell)
|--- 优先 Frida Hook
| (110个RPC, ~50-200ms)
|
|--- 降级 u2 UI自动化
| (WechatSkill 96个action, ~2-15s)
|
v
微信APP (com.tencent.mm)
</pre></div></div>
<div class="section"><div class="section-title">三种控制通道性能对比</div>
<div class="card">
<table style="width:100%;font-size:12px;border-collapse:collapse">
<tr style="border-bottom:1px solid var(--border)"><th style="text-align:left;padding:6px;color:var(--text2)">通道</th><th style="padding:6px;color:var(--text2)">原理</th><th style="padding:6px;color:var(--text2)">延迟</th><th style="padding:6px;color:var(--text2)">稳定性</th><th style="padding:6px;color:var(--text2)">适用</th></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px"><span class="channel-badge ch-hook">Frida Hook</span></td><td style="padding:6px;color:var(--text2)">调用微信内部Java方法/读SQLite</td><td style="padding:6px;color:var(--accent2)">50-200ms</td><td style="padding:6px;color:var(--accent3)">依赖版本偏移表</td><td style="padding:6px;color:var(--text2)">读数据/发消息</td></tr>
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px"><span class="channel-badge ch-u2">u2 UI自动化</span></td><td style="padding:6px;color:var(--text2)">uiautomator2模拟点击/输入</td><td style="padding:6px;color:var(--accent)">2-15s</td><td style="padding:6px;color:var(--accent2)">高,跨版本通用</td><td style="padding:6px;color:var(--text2)">所有UI操作</td></tr>
<tr><td style="padding:6px"><span class="channel-badge ch-adb">ADB Shell</span></td><td style="padding:6px;color:var(--text2)">adb命令+坐标点击</td><td style="padding:6px;color:var(--accent3)">1-10s</td><td style="padding:6px;color:var(--accent3)">中,依赖分辨率</td><td style="padding:6px;color:var(--text2)">兜底/无Agent</td></tr>
</table></div></div>`;
}
function renderAllApi(){
let h='<div class="section"><div class="section-title">全部 98 个 API 端点</div>';
Object.entries(MODULES).forEach(([k,m])=>{
h+=`<div class="card"><div style="font-weight:600;font-size:13px;margin-bottom:8px">${m.title} (${m.actions.length})</div><div class="api-grid">`;
m.actions.forEach((a,i)=>{
h+=`<div class="api-btn" onclick="showModule('${k}')"><div class="method">${a.method}</div>${a.name}</div>`;
});
h+=`</div></div>`;
});
h+=`</div>`;return h;
}
async function execAction(moduleKey,actionIdx,form){
const m=MODULES[moduleKey];const a=m.actions[actionIdx];
const fd=new FormData(form);const body={};
fd.forEach((v,k)=>{if(v)body[k]=v});
body.device_id=currentDevice;body.platform='wechat';
if(body.to_ids)body.to_ids=body.to_ids.split(',').map(s=>s.trim());
if(body.member_ids)body.member_ids=body.member_ids.split(',').map(s=>s.trim());
if(body.user_ids)body.user_ids=body.user_ids.split(',').map(s=>s.trim());
if(body.tags)body.tags=body.tags.split(',').map(s=>s.trim());
if(body.enable)body.enable=body.enable==='true';
['limit','post_index','index','duration','days','interval'].forEach(k=>{if(body[k])body[k]=Number(body[k])});
const start=Date.now();
updateChain('executing',a.name);
try{
let url=BASE+a.endpoint;
const opts={headers:{'Content-Type':'application/json'}};
if(a.method==='GET'){
const qs=Object.entries(body).map(([k,v])=>`${k}=${encodeURIComponent(v)}`).join('&');
if(qs)url+='?'+qs;
opts.method='GET';
}else{
opts.method='POST';opts.body=JSON.stringify(body);
}
const res=await fetch(url,opts);const data=await res.json();
const elapsed=Date.now()-start;
stats.total++;
const ok=data.code===200&&(data.data?.success!==false);
if(ok)stats.success++;
stats.totalTime+=elapsed;
const ch=data.channel_used||'sdk_control';
updateStats(ch,elapsed);
updateChain(ok?'success':'error',a.name,ch,elapsed);
showResult(data,elapsed,ch);
addLog(a.name,ok,ch,elapsed);
}catch(e){
const elapsed=Date.now()-start;
stats.total++;stats.totalTime+=elapsed;
updateStats('-',elapsed);
updateChain('error',a.name,'-',elapsed);
showResult({error:e.message},elapsed,'-');
addLog(a.name,false,'-',elapsed);
}
return false;
}
function updateChain(state,action,channel,elapsed){
const dots=['cd1','cd2','cd3','cd4','cd5'];
const labels=['cs1','cs2','cs3','cs4','cs5'];
if(state==='executing'){
dots.forEach(id=>{document.getElementById(id).className='chain-dot active'});
document.getElementById('cs1').textContent=`unified.py → ${action}`;
document.getElementById('cs2').textContent='通道路由中...';
document.getElementById('cs3').textContent='执行中...';
document.getElementById('cs4').textContent='...';
document.getElementById('cs5').textContent='微信APP';
}else{
const cls=state==='success'?'chain-dot active':'chain-dot';
dots.forEach(id=>{document.getElementById(id).className=cls});
document.getElementById('cs1').textContent=`unified.py → ${action}`;
document.getElementById('cs2').textContent=`通道: ${channel}`;
document.getElementById('cs3').textContent=state==='success'?'执行成功':'执行失败';
document.getElementById('cs4').textContent=`${channel} (${elapsed}ms)`;
document.getElementById('cs5').textContent='微信APP';
const badge=document.getElementById('channelBadge');
if(channel?.includes('hook')){badge.className='channel-badge ch-hook';badge.textContent='Hook'}
else if(channel?.includes('adb')){badge.className='channel-badge ch-adb';badge.textContent='ADB'}
else{badge.className='channel-badge ch-u2';badge.textContent='u2'}
}
}
function updateStats(ch){
document.getElementById('totalOps').textContent=stats.total;
document.getElementById('successRate').textContent=stats.total?Math.round(stats.success/stats.total*100)+'%':'-';
document.getElementById('avgTime').textContent=stats.total?Math.round(stats.totalTime/stats.total)+'ms':'-';
document.getElementById('lastChannel').textContent=ch||'-';
}
function showResult(data,elapsed,ch){
const box=document.getElementById('resultBox');
box.textContent=`// ${ch} | ${elapsed}ms\n`+JSON.stringify(data,null,2);
}
function addLog(action,ok,ch,elapsed){
const box=document.getElementById('logBox');
const t=new Date().toLocaleTimeString();
const cls=ok?'ok':'fail';
box.innerHTML=`<div class="log-entry"><span class="time">${t}</span> <span class="action">${action}</span> <span class="${cls}">${ok?'OK':'FAIL'}</span> ${ch} ${elapsed}ms</div>`+box.innerHTML;
if(box.children.length>50)box.removeChild(box.lastChild);
}
async function loadDevices(){
try{
const res=await fetch(BASE+'/devices');
const data=await res.json();
const sel=document.getElementById('deviceSelect');
sel.innerHTML='';
let devices=normalizeDeviceArray(data);
if(!devices.length){
try{
const adbRes=await fetch('/api/v3/adb/devices');
const adbData=await adbRes.json();
normalizeDeviceArray(adbData).forEach(d=>{
const did=d.device_id||d.serial;
if(!did)return;
const st=(d.status==='online'||d.status==='adb')?'adb':'adb';
devices.push({device_id:did,status:st,model:d.model||did});
});
}catch(e){}
}
if(!devices.length){sel.innerHTML='<option value="">无设备(请 USB 连接手机并 adb devices 授权,或启动 Agent</option>';loadedDeviceList=[];updateDeviceStatus(null);return}
loadedDeviceList=devices;
const prevPick=currentDevice;
devices.forEach(d=>{
const did=d.device_id||d.serial||d;
const lbl=d.model?`${did} (${d.model})`:did;
sel.innerHTML+=`<option value="${did}">${lbl}</option>`;
});
const first=devices[0];
const firstId=first.device_id||first.serial;
if(prevPick&&devices.some(x=>(x.device_id||x.serial)===prevPick))currentDevice=prevPick;
else currentDevice=firstId;
sel.value=currentDevice;
updateDeviceStatus(devices.find(x=>(x.device_id||x.serial)===currentDevice)||first);
}catch(e){
document.getElementById('deviceSelect').innerHTML='<option value="">连接失败</option>';
loadedDeviceList=[];
updateDeviceStatus(null);
}
}
function switchDevice(){
currentDevice=document.getElementById('deviceSelect').value;
const d=loadedDeviceList.find(x=>(x.device_id||x.serial)===currentDevice);
updateDeviceStatus(d||null);
}
function updateDeviceStatus(d){
const el=document.getElementById('deviceStatus');
if(!d){el.className='status-pill status-offline';el.textContent='离线';return}
const s=d.status||'offline';
if(s==='online'){el.className='status-pill status-online';el.textContent='在线(WS)'}
else if(s==='adb'){el.className='status-pill status-online';el.textContent='在线(ADB)'}
else{el.className='status-pill status-offline';el.textContent='离线'}
}
loadDevices();
showModule('message');
</script>
</body>
</html>