feat(账号): 账号台账(web 页 + 任务取号 + 设备端身份页);fix(剪贴板): 注入通道改走设备端 Agent

一、账号台账(新表 device_account,schema v8)
- core/ledger.py:CRUD、Excel 粘贴解析(Tab 分隔 / 表头乱序缺列 / 是·否布尔 / 逐行留痕)、
  按范围取号(本机台账 / 全部 / 按设备分组)、设备端账号块、dry_run 预览
- 新「账号」顶级 Tab(权限 devices):列表 + 搜索 + 设备筛选 + 增删改 +
  粘贴导入(默认先预览再写,逐行显示 新增/覆盖/跳过/失败 + 留痕)
- 设备池加「账号 N」列(0 个显示"未登记"),点开看该设备账号明细
- 任务「条件判断」新增 cmp_source / cmp_group:候选值可直接来自台账,
  与手填值**合并(OR)**;取不到号时回落手填值,并把原因写进步骤明细与日志
  (否则"永远走 else 分支"而任务照样显示成功,最难查)
- 设备端契约:身份页多推 accounts_b64(base64 JSON;默认不含手机号;
  超预算按整条丢且绝不算字节切),见 doc/DEVICE_AGENT.md §5.2.1
- 备份/文档红线:TABLE_LABELS 加「账号台账」;DATA_MODEL/API/ARCHITECTURE/DEPLOY/
  TASK_DEV/DEVICE_AGENT/DEVELOPMENT/README 同步;顺手补上 DATA_MODEL 漏列的 done_mark

⚠ 台账里的抖音号是**纯号**,只当"比对用的候选值":绝不能拿去填「去重」的身份元素
  (身份是元素原文逐字算 key,格式不同会让去重静默失效)。代码与文档都写明了。

二、剪贴板注入通道(修:平台还在调早期的独立 APK)
- 改为按顺序尝试:设备端 Agent(com.example.deviceagent/.ClipActivity)→
  旧版独立 ClipInject 兜底;两个都没有时报"需要设备端 Agent"(不再只说 ClipInject)
- 读回验证改成**轮询到 3 秒**:透明 Activity 要等窗口拿到焦点才写,
  原来只睡 0.5s 会读到上一次的内容 → 误报"写入可能被拒"(实测内容已写入却回失败)
This commit is contained in:
2026-09-24 16:12:58 +08:00
parent e82fd64695
commit 7ce4ae9016
24 changed files with 1455 additions and 62 deletions
+39 -4
View File
@@ -19,7 +19,7 @@ var STEP_LIB=[
{type:'wait_el',label:'等待元素',icon:'⏳',cat:'flow',params:{selector_type:'xpath',selector_value:'',timeout:10}},
{type:'loop',label:'循环块',icon:'↻',cat:'flow',params:{loop_mode:'rounds',max_iterations:10,loop_duration:600,children:[]}},
{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:'',ident_type:'text',ident_value:'',then:[],else:[]}},
{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:'notify',label:'发通知',icon:'🔔',cat:'flow',params:{title:'',message:'',level:'info'}},
{type:'stop_self',label:'停止本设备',icon:'⛔',cat:'flow',params:{reason:''}},
@@ -464,16 +464,36 @@ 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 cmpRow=dyn?'':'<div class="form-row"><div class="form-group"><label>文本比对</label>'+
'<select class="form-control" data-key="cmp_op">'+
[['','不比对(只看元素在不在)'],['等于','等于'],['不等于','不等于'],['包含','包含'],['不包含','不包含']]
.map(function(o){return '<option value="'+o[0]+'"'+((p.cmp_op||'')===o[0]?' selected':'')+'>'+o[1]+'</option>';}).join('')+
'</select></div>'+
'<div class="form-group" style="grid-column:span 2"><label>比对的值(可多行,任一命中即可)</label>'+
'<div class="form-group"><label>候选值来源</label>'+
'<select class="form-control" data-key="cmp_source" onchange="_stepEditor._cmpSourceChanged(this)">'+
[['','手填(默认)'],['device','本机台账'],['all','全部台账'],['group','按设备分组']]
.map(function(o){return '<option value="'+o[0]+'"'+((p.cmp_source||'')===o[0]?' selected':'')+'>'+o[1]+'</option>';}).join('')+
'</select><div class="help">台账在「账号」页维护</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 存在与否收集的,
// 删了就等于把用户手填的值悄悄抹掉(同类坑见本文件 927 行附近的注释)
'<div class="form-group" style="grid-column:span 2'+(fromLedger?';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>——多个号的场景把号都列上。'+
'<b>逗号不当分隔符</b>,因为要比对的文本本身常带逗号</div></div></div>';
'<b>逗号不当分隔符</b>,因为要比对的文本本身常带逗号'
+(fromLedger?'<br><b>候选值来自台账</b>:这台设备在台账里登记的抖音号会作为候选,'
+'手填的值<b>追加</b>在后面(台账取不到号时只用手填值,不会把任务打哑)。'
+'<br>⚠ 台账只提供<b>比对用的号</b>;<b>不要</b>把台账号填到「去重」的身份元素里'
+'(身份是元素原文、逐字比,格式不同会让去重静默失效)。':'')
+'</div></div></div>';
// 「去重」条件:身份元素用 ident_type/ident_value(主选择器那行只剩类型下拉)
var identRow=isDedup?_selRowHtml(p,
'<div class="help"><b>身份元素</b>:填"账号"那个元素(点「抓取元素」选,如抖音号那个 TextView)。'
@@ -814,6 +834,13 @@ var _stepEditor={
g.style.display=g.dataset.lmode===mode?'block':'none';
});
},
// 条件判断:候选值来源切到台账时,把"比对的值"藏起来(按分组还要显示分组名输入框)
_cmpSourceChanged:function(sel){
// 同 _condTypeChanged:先把 DOM 的值同步回 _steps,否则重渲染会吞掉刚改的东西
this._syncFromDom();
var self=this;
setTimeout(function(){self._renderCanvas();},0);
},
// 条件判断:选择器类型切到 OCR 时显示"命中后点击"开关
_condTypeChanged:function(sel){
// 条件类型一变,"值"控件也要跟着换(元素值输入框 / 屏幕亮熄下拉 / 包名输入),
@@ -930,10 +957,18 @@ var _stepEditor={
if(s.params.selector_type!=='dedup'&&!s.params.selector_value){
warnings.push(name+' "'+label+'": 条件判断的选择器为空,会直接走"未找到"分支。');
}
if(cmpUsed&&s.params.cmp_op&&!String(s.params.cmp_value||'').trim()){
var cmpSrcV=(s.params.cmp_source||'');
var fromLedger=(cmpSrcV==='device'||cmpSrcV==='all'||cmpSrcV==='group');
if(cmpUsed&&s.params.cmp_op&&!String(s.params.cmp_value||'').trim()&&!fromLedger){
// 候选值来自台账时允许手填留空(台账取号就够)→ 不报这条
warnings.push(name+' "'+label+'": 选了"文本比对('+s.params.cmp_op+')"但没填比对的值,'
+'执行时永远不命中(相当于每次都走"未找到时"分支)。');
}
if(cmpUsed&&fromLedger&&s.params.cmp_op){
warnings.push(name+' "'+label+'": 候选值来自台账 —— 台账里没有这台设备'
+'(或设备池里的设备名与台账「设备号」对不上)时会取不到号,'
+'这一步将永远走"未找到"分支。请到「账号」页核对设备号。');
}
if(s.params.selector_type==='dedup'){
hasDedupCheck=true;
// 空值也收进来:一边填了、另一边没填 = 两处身份不一致(去重会对不上),