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(临时库 + 假设备,不碰真实数据、不连真机)
This commit is contained in:
2026-10-12 09:02:40 +08:00
parent 75bee21ffb
commit beda0f3779
37 changed files with 2615 additions and 1747 deletions
+24 -25
View File
@@ -21,6 +21,9 @@ var STEP_LIB=[
{type:'group',label:'动作组',icon:'📦',cat:'flow',params:{children:[]}},
{type:'if_el',label:'条件判断',icon:'❓',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:3,ocr_click:false,cmp_op:'',cmp_value:'',cmp_source:'',cmp_group:'',ident_type:'text',ident_value:'',then:[],else:[]}},
{type:'mark_done',label:'记为已做',icon:'✅',cat:'flow',params:{selector_type:'text',selector_value:''}},
{type:'read_account_name',label:'读取本机账号名字',icon:'🏷',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:8,on_missing:'fail'}},
{type:'switch_account',label:'切换账号',icon:'🔁',cat:'flow',params:{target:'var:target_name',target_value:'',switch_selector_type:'xpath',switch_selector_value:'',name_selector_type:'',name_selector_value:'',me_selector_type:'descriptionContains',me_selector_value:'我',timeout:8,verify:true,max_attempts:2,on_login_page:'stop'}},
{type:'foreach_account',label:'循环本机待发的号',icon:'🔂',cat:'flow',params:{date_source:'today',fixed_date:'',retry_failed:true,max_accounts:0,children:[]}},
{type:'notify',label:'发通知',icon:'🔔',cat:'flow',params:{title:'',message:'',level:'info'}},
{type:'stop_self',label:'停止本设备',icon:'⛔',cat:'flow',params:{reason:''}},
{type:'gesture',label:'录制手势',icon:'⏺',cat:'interact',params:{points:[],speed:1.0}}
@@ -478,9 +481,9 @@ var _stepEditor={
var isDedup=(p.selector_type==='dedup');
var dyn=(p.selector_type==='screen'||p.selector_type==='foreground'||isDedup);
// 文本比对:屏幕状态没有文本可比(亮/熄)、去重也不比文本,只有元素/OCR/前台 App 才给这块
// 候选值来源:默认"手填"(行为与以前完全一致);也可以直接吃「账号」页的台账
// 候选值来源:默认"手填"(行为与以前完全一致);也可以取"本次要发的名字"/发布计划
var cmpSrc=p.cmp_source||'';
var fromLedger=(cmpSrc==='device'||cmpSrc==='all'||cmpSrc==='group');
var fromPlatform=(cmpSrc==='account_target'||cmpSrc==='release');
var cmpRow=dyn?'':'<div class="form-row"><div class="form-group"><label>文本比对</label>'+
'<select class="form-control" data-key="cmp_op">'+
[['','不比对(只看元素在不在)'],['等于','等于'],['不等于','不等于'],['包含','包含'],['不包含','不包含']]
@@ -488,28 +491,24 @@ var _stepEditor={
'</select></div>'+
'<div class="form-group"><label>候选值来源</label>'+
'<select class="form-control" data-key="cmp_source" onchange="_stepEditor._cmpSourceChanged(this)">'+
[['','手填(默认)'],['device','本机台账'],['all','全部台账'],['group','按设备分组'],
['release','本机当前发布计划(要发的那个号)']]
[['','手填(默认)'],['account_target','本次要发的名字(读名字/循环块给的)'],
['release','本机当前发布计划(要发的那条)']]
.map(function(o){return '<option value="'+o[0]+'"'+((p.cmp_source||'')===o[0]?' selected':'')+'>'+o[1]+'</option>';}).join('')+
'</select><div class="help">'+(p.cmp_source==='release'
? '取<b>本机下一条要发的发布计划</b>的抖音号 + 账号名称(推送前校验"当前登录的是不是要发的号"就用它)'
: '台账在「账号」页维护')+'</div></div>'+
(cmpSrc==='group'?
'<div class="form-group"><label>设备分组名</label>'+
'<input class="form-control" data-key="cmp_group" value="'+esc(p.cmp_group||'')+'" '+
'placeholder="分组名(工具→设备分组)"></div>':'')+
// 选台账时**只隐藏、不删**这个输入框:_syncParams 是按 DOM 存在与否收集的,
? '取<b>本机下一条要发的发布计划</b>的<b>名字</b>(推送前校验"当前登的是不是要发的号"就用它)'
: (p.cmp_source==='account_target'
? '取<b>本次要发的那个名字</b>(「读取本机账号名字」读到的,或循环块设的目标)。'
+'<b>读不到名字时候选为空 → 不命中 → 走 else(不发)</b>,这是故意的'
: '手填要比对的文本;要用本机账号名就用上面的两个来源'))+'</div></div>'+
// 候选值来自平台时**只隐藏、不删**这个输入框:_syncParams 是按 DOM 存在与否收集的,
// 删了就等于把用户手填的值悄悄抹掉(同类坑见本文件 927 行附近的注释)
'<div class="form-group" style="grid-column:span 2'+(fromLedger?';display:none':'')+'">'+
'<div class="form-group" style="grid-column:span 2'+(fromPlatform?';display:none':'')+'">'+
'<label>比对的值(可多行,任一命中即可)</label>'+
'<textarea class="form-control" data-key="cmp_value" rows="2" style="font-family:var(--mono)" '+
'placeholder="如 35377983067&#10;35377983068">'+esc(p.cmp_value||'')+'</textarea>'+
'<div class="help">一行一个(也认 <code>|</code> 分隔):<b>任意一个命中就算命中</b>——多个号的场景把号都列上。'+
'<div class="help">一行一个(也认 <code>|</code> 分隔):<b>任意一个命中就算命中</b>——多个值的场景把值都列上。'+
'<b>逗号不当分隔符</b>,因为要比对的文本本身常带逗号'
+(fromLedger?'<br><b>候选值来自台账</b>:这台设备在台账里登记的抖音号会作为候选,'
+'手填的值<b>追加</b>在后面(台账取不到号时只用手填值,不会把任务打哑)。'
+'<br>⚠ 台账只提供<b>比对用的号</b>;<b>不要</b>把台账号填到「去重」的身份元素里'
+'(身份是元素原文、逐字比,格式不同会让去重静默失效)。':'')
+(fromPlatform?'<br>候选值由平台给(上面选的来源),手填的值会<b>追加</b>在后面。':'')
+'</div></div></div>';
// 「去重」条件:身份元素用 ident_type/ident_value(主选择器那行只剩类型下拉)
var identRow=isDedup?_selRowHtml(p,
@@ -857,7 +856,7 @@ var _stepEditor={
var self=this;
setTimeout(function(){self._renderCanvas();},0);
},
// 条件判断:候选值来源切到台账时,把"比对的值"藏起来(按分组还要显示分组名输入框)
// 条件判断:候选值来源切到"平台给"(本次要发的名字 / 发布计划)时,把"比对的值"藏起来
_cmpSourceChanged:function(sel){
// 同 _condTypeChanged:先把 DOM 的值同步回 _steps,否则重渲染会吞掉刚改的东西
this._syncFromDom();
@@ -981,17 +980,17 @@ var _stepEditor={
warnings.push(name+' "'+label+'": 条件判断的选择器为空,会直接走"未找到"分支。');
}
var cmpSrcV=(s.params.cmp_source||'');
var fromLedger=(cmpSrcV==='device'||cmpSrcV==='all'||cmpSrcV==='group');
// 候选值"从别处取"的那两档(台账 / 发布计划):手填可以留空,别报"没填值"
var fromOther=(fromLedger||cmpSrcV==='release');
var fromPlatform=(cmpSrcV==='account_target'||cmpSrcV==='release');
// 候选值"平台给"的那两档(本次要发的名字 / 发布计划):手填可以留空,别报"没填值"
var fromOther=fromPlatform;
if(cmpUsed&&s.params.cmp_op&&!String(s.params.cmp_value||'').trim()&&!fromOther){
warnings.push(name+' "'+label+'": 选了"文本比对('+s.params.cmp_op+')"但没填比对的值,'
+'执行时永远不命中(相当于每次都走"未找到时"分支)。');
}
if(cmpUsed&&fromLedger&&s.params.cmp_op){
warnings.push(name+' "'+label+'": 候选值来自台账 —— 台账里没有这台设备'
+'(或设备池里的设备名与台账「设备号」对不上)时会取不到号,'
+'这一步将永远走"未找到"分支。请到「账号」页核对设备号。');
if(cmpUsed&&cmpSrcV==='account_target'&&s.params.cmp_op){
warnings.push(name+' "'+label+'": 候选值来自"本次要发的名字" —— 前面要有'
+'「读取本机账号名字」步骤(或循环块设的目标名字),名字读不到时候选为空、'
+'这一步会走"未找到"分支。这是设计如此(读不到名字 = 不发)。');
}
if(cmpUsed&&cmpSrcV==='release'&&s.params.cmp_op){
warnings.push(name+' "'+label+'": 候选值来自"本机当前发布计划" —— 本机没有待发的计划'