Files
auto_control/static/admin/base.js
T
butubb beda0f3779 feat(发布计划): 改按名字发(文件名 名字_日期_编号)+ 账号管理整页下线 + 自动切号一轮发完
发布计划不再绑设备、不再依赖账号台账:上传文件名 `名字_日期_编号`(标题行
`标题 || 正文_名字_日期_编号`),运行时读本机当前抖音昵称取计划;靠循环块
「一轮把本机待发的号跑完」自动切号,一直发到发完。

· 解析/配对换键:右锚定认名字(不许含下划线、不许纯数字 —— 不合法直接拒收并说清原因),
  删掉三处"手机号必须命中台账"的校验,加索引 ix_video_plan_name_date(启动时补建)
· 新步骤三个:读本机账号名字 / 切换账号(**按名字点控件**、切完回读校验、遇登录页停本设备)/
  循环本机待发的号(试过不回头、跑一轮没进展就换号、停止·超时·上限三个干净出口)
· 发布骨架改 18 步:读名字 → 校验账号(cmp_source=account_target)→ 推送(**推送前重读昵称核对**)
  → 抖音流程 → 标记结果;push_release 的 account 默认 name
· 账号管理整页下线:删 core/ledger.py(serial_of 迁到 core/device_pool.py)、6 个 /api/ledger*、
  「账号」页与 ledger.js、设备池「账号」列、设备端账号块;发布计划升为一级页签;
  device_account 表与 42 行历史数据保留、仍在备份清单内(**停用不等于删数据**)
· 人工入口「按名字找设备」:新增 core/account_probe.py(只读探测、跳过在跑任务的设备)
  + POST /api/video_plan/push_by_account,没设备登着的号逐条报出来;
  推送时把"实际是谁发的"写进 posted_serial / posted_device_name
· 通知:新增 task.account.switch_failed / task.account.need_login
· schema v12(posted_*)/ v13(device.cur_account_name / cur_name_at)
· 文档同步:TASK_DEV / API / DATA_MODEL / ARCHITECTURE / NOTIFY / DEVICE_AGENT / DEPLOY /
  DEVELOPMENT / README / .env.example(两个抖音控件选择器抓一次填 .env)
· 新增 scripts/tests/:6 套用例 + run_all.py(临时库 + 假设备,不碰真实数据、不连真机)
2026-10-12 09:02:40 +08:00

204 lines
9.6 KiB
JavaScript
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.
// 通用基础:esc/CSRF/权限/API/Toast、Tab 切换与页内子分栏、模态框、行内按钮操作
// ================== 通用工具 ==================
const ACTION_LABELS={like:'点赞',comment:'评论',follow:'关注',share:'分享'};
const STATUS_MAP={idle:{t:'未启动',c:'default'},connecting:{t:'连接中',c:'warning'},running:{t:'运行中',c:'success'},done:{t:'完成',c:'primary'},error:{t:'异常',c:'danger'},released:{t:'已释放',c:'default'},failed:{t:'失败',c:'danger'}};
const TARGET_MAP={all:'全部空闲',group:'按分组',serial:'指定设备'};
// 设备显示文案:**名称是设备身份**,地址只是当前连接位置。
// 有名称 → 「名称 · serial」;没名称(历史数据)→ 只显示 serial
function devText(name, serial){
const n=String(name||'').trim();
return n ? (n + ' · ' + serial) : String(serial||'');
}
const APP_NAMES={'com.ss.android.ugc.aweme':'抖音','com.ss.android.ugc.aweme.lite':'抖音极速版','com.smile.gifmaker':'快手','com.kuaishou.nebula':'快手极速版','com.tencent.mm':'微信','com.tencent.mobileqq':'QQ','com.tencent.qqlive':'腾讯视频','com.eg.android.AlipayGphone':'支付宝','com.taobao.taobao':'淘宝','com.tmall.wireless':'天猫','com.jingdong.app.mall':'京东','com.xunmeng.pinduoduo':'拼多多','com.ss.android.article.news':'今日头条','com.zhihu.android':'知乎','com.miui.home':'桌面','com.android.launcher':'桌面','com.android.settings':'设置','com.android.systemui':'系统','com.github.uiautomator':'U2','com.github.uiautomator.test':'U2测试'};
function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
// ===== CSRF =====
let _csrfToken='';
// 页面加载后调用:获取 CSRF token,非 GET 请求需携带
async function initCsrf(){
try{
const r=await fetch('/api/csrf');
const d=await r.json();
_csrfToken=d.token||'';
}catch(e){}
}
function _csrfHeaders(){
return _csrfToken?{'X-CSRF-Token':_csrfToken}:{};
}
// ===== 当前用户与权限 =====
// _me 来自 /api/me:{username, is_admin, perms:[tasks/devices/apks/logs]}
// 管理员 perms 恒为全部。无权限的功能入口(tab/按钮)用 data-perm 标记或 _can() 判断隐藏。
let _me={is_admin:false,perms:[],username:''};
async function loadMe(){
try{
const r=await apiGet('/api/me');
if(r&&r.ok)_me=r.user||{is_admin:false,perms:[],username:''};
const un=document.getElementById('me-username');
if(un)un.textContent=_me.username||'';
// 隐藏无权限的 tab 和按钮(data-perm="admin" 表示仅管理员)
document.querySelectorAll('[data-perm]').forEach(function(el){
const p=el.dataset.perm;
const ok=(p==='admin')?_me.is_admin:(_me.perms||[]).includes(p);
if(!ok)el.style.display='none';
});
}catch(e){}
}
function _can(perm){
return _me.is_admin||(_me.perms||[]).includes(perm);
}
const PERM_LABELS={tasks:'任务管理',devices:'设备控制',apks:'应用管理',logs:'日志查看'};
async function apiGet(url){
const r=await fetch(url);
if(r.status===401){window.location='/login';return null;}
return r.json();
}
async function apiPost(url,body){
const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json',..._csrfHeaders()},body:body?JSON.stringify(body):'{}'});
if(r.status===401){window.location='/login';return null;}
return r.json();
}
async function apiPut(url,body){
const r=await fetch(url,{method:'PUT',headers:{'Content-Type':'application/json',..._csrfHeaders()},body:body?JSON.stringify(body):'{}'});
if(r.status===401){window.location='/login';return null;}
return r.json();
}
async function apiDelete(url){
const r=await fetch(url,{method:'DELETE',headers:_csrfHeaders()});
if(r.status===401){window.location='/login';return null;}
return r.json();
}
function showToast(msg,type){
const t=document.getElementById('toast');
t.className='toast show toast-'+(type||'success');
t.textContent=msg;
setTimeout(()=>t.className='toast',3000);
}
function handleResult(r,callback){
if(!r)return;
if(r.ok){showToast(r.msg||'操作成功','success');if(callback)callback();}
else{showToast(r.error||'操作失败','error');}
}
// ================== Tab 切换 ==================
let _activeTab='monitor';
let _monitorTimer=null;
let _logTimer=null;
function showTab(name){
_activeTab=name;
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
document.querySelectorAll('.navbar .tab').forEach(t=>t.classList.remove('active'));
const panel=document.getElementById('tab-'+name);
if(panel)panel.classList.add('active');
const btn=document.querySelector('.navbar .tab[data-tab="'+name+'"]');
if(btn)btn.classList.add('active');
// 停止所有定时器
if(_monitorTimer){clearInterval(_monitorTimer);_monitorTimer=null;}
if(_logTimer){clearInterval(_logTimer);_logTimer=null;}
if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;}
if(typeof _rpTimer!=='undefined'&&_rpTimer){clearInterval(_rpTimer);_rpTimer=null;}
// 按需加载数据
if(name==='monitor'){loadMonitor();_monitorTimer=setInterval(loadMonitor,5000);}
if(name==='tasks'){showSubTab('tasks',_activeSubs.tasks);loadTasks();loadCustomActions();
if(typeof loadStepDefaults==='function')loadStepDefaults();}
if(name==='tools'){showSubTab('tools',_activeSubs.tools);loadToolsDevices();loadAdbDevices();loadTailscaleDevices();loadApks();}
if(name==='logs'){
showSubTab('logs', _activeSubs.logs||'files'); // 文件日志 / 步骤明细
if((_activeSubs.logs||'files')==='files'
&& document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);
}
if(name==='agent'){
showSubTab('agent', _activeSubs.agent||'chat'); // 聊天 / AI 建任务
if(typeof initAgentChat==='function') initAgentChat();
}
// 发布计划(原「发布计划」子分栏,账号台账下线后升为一级页签)
if(name==='plan'){
if(typeof loadReleasePlan==='function')loadReleasePlan();
if(typeof loadReleaseTasks==='function')loadReleaseTasks();
}
if(name==='users')loadUsers();
if(name==='system'){showSubTab('system',_activeSubs.system||'backup');}
}
// ================== 页内子分栏(任务/工具 通用) ==================
// 每个带子分栏的 Tab 记住上次选中的子分栏,切走再切回来保持原位
let _activeSubs = {tasks: 'plan', tools: 'clipboard', system: 'backup', agent: 'chat',
logs: 'files'};
let _discoveryTimer = null; // 设备自动发现 10s 轮询(仅 devpool 子分栏激活时)
function showSubTab(tabId, name){
_activeSubs[tabId] = name;
const tab = document.getElementById('tab-'+tabId);
tab.querySelectorAll('.sub-tab').forEach(b=>b.classList.toggle('active', b.dataset.sub===name));
tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name));
if(tabId==='agent' && name==='taskgen' && typeof initTaskGen==='function') initTaskGen();
if(tabId==='system' && name==='notify' && typeof loadNotifyPanel==='function') loadNotifyPanel();
if(tabId==='logs' && name==='files' && typeof loadLogs==='function') loadLogs();
if(tabId==='logs' && name==='steps' && typeof loadStepLogs==='function') loadStepLogs(true);
if(tabId==='tasks' && name==='actioncfg' && typeof loadActionConfig==='function') loadActionConfig();
if(tabId==='tasks' && name==='dedup' && typeof loadDedup==='function') loadDedup();
if(name==='groups' && typeof loadGroups==='function') loadGroups();
if(name==='apks' && typeof loadAgentStore==='function') loadAgentStore();
if(name==='devpool' && typeof loadDevPool==='function'){
loadDevPool();
if(typeof loadDiscovery==='function'){
loadDiscovery();
if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;}
_discoveryTimer=setInterval(loadDiscovery,10000); // 10s 轮询:扫完自动出新结果
}
} else if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;}
}
// ================== 模态框 ==================
function showModal(){
document.getElementById('modal-overlay').classList.add('show');
}
function closeModal(){
// 先关闭弹窗(最重要的一步,确保一定执行)
const ov=document.getElementById('modal-overlay');
if(ov)ov.classList.remove('show');
// 再做清理工作,任何异常都不影响弹窗关闭
try{
stopShotTimer();
_shotSerial='';
const box=document.getElementById('modal-box');
if(box){box.classList.remove('shot-modal');box.classList.remove('wide-modal');}
}catch(e){console.warn('closeModal cleanup error:',e);}
}
// 测试此步骤弹窗关闭(editor.js 的测试按钮用;曾随 STF 清理误删)
function closeTestModal(){
const ov=document.getElementById('test-overlay');
if(ov)ov.classList.remove('show');
}
// 关闭元素抓取弹窗(不影响任务编辑窗口)
function closeElPicker(){
var ov=document.getElementById('el-picker-overlay');
if(ov)ov.classList.remove('show');
// 清理状态
try{ _stepEditor._elData=null; _stepEditor._elSerial=''; }catch(e){}
}
// ================== 行内按钮操作(停止/执行/启停等,自动 toast + 刷新)==================
async function apiAction(url,body){
const r=await apiPost(url,body);
if(!r)return;
if(r.ok){showToast(r.msg||'操作成功','success');if(_activeTab==='monitor')loadMonitor();if(_activeTab==='tasks')loadTasks();}
else{showToast(r.error||'操作失败','error');}
}
async function scanForeground(){
const r=await apiPost('/api/scan_foreground');
if(!r)return;
if(r.ok){showToast('扫描已启动,请等待几秒...','success');loadMonitor();}
else{showToast(r.error||'扫描失败','error');}
}