feat(任务): 录制手势改成**纯录制回放**(真手指轨迹)+ 手机端 getevent 录制
用户反馈:录制出来的滑动又被套上滑动那套逻辑(方向/幅度/拟人重新生成),
"导致滑动还是不顺畅"。录制就该是录制——按录下的路径与时间原样重放。
- 新步骤 `gesture`「录制手势」:存完整轨迹点列 `[[x,y,t_ms],…]`,
回放时原样交给设备(`d.swipe_points(points, duration)`),不做任何加工。
与 `swipe` 是两套东西(滑动是参数化的,录制是点列)。
- core/gesture.py(新):
· 手机端录制 `PhoneRecorder`:`getevent` 读**真触屏**设备(自动挑 fts_ts 这类、
排除 uinput/vitural-sar 等合成节点),解析两套协议(BTN_TOUCH / ABS_MT_TRACKING_ID);
**合成的注入事件不会出现在真触屏节点上**(实测),所以录到的只有人手的动作;
· 点列清洗:按 ≥16ms 抽稀但**末点必留**(快划时不能把收尾丢了);
· 回放**一次调用**而不是逐点注入:实测这台设备单次触摸 RPC ≈190ms,
逐点回放 20 点要 3.8 秒——只能让设备自己插值,"时间"由点密度还原。
- 接口:POST /api/gesture/record/{start,stop}(需设备权限;重复开始返回 409)。
- 「录制手势」步骤卡片:录到就回填(手机上录 / 网页上录),显示点数/时长;
**滑动步骤上的「录制手势」按钮已移除**(按用户要求,录制不再走滑动逻辑)。
- 录制前自动唤醒设备:**息屏时 u2 抓 UI 树会从 3 秒退化到 65 秒**(实测),
截图也是黑的——这是本次排查最耗时的坑,已写进文档。
自测:假设备单测(点列/时长/抽稀/失败退回直线,10 项);
无头浏览器 + CDP 真模拟拖动 → 录到 14 点/605ms 并按实际轨迹回填;
手机录制接口起停/重复拦截;
真机回放:用录下的轨迹跑任务 → 步骤 `gesture ok`。
⚠️ 手机端"真手指轨迹"这一段需要人真的去划才能端到端验证(我没有手指)。
文档:TASK_DEV §3.2(两套东西对比 + 三个坑)、步骤表 21 种、API、README。
This commit is contained in:
+52
-16
@@ -21,7 +21,8 @@ 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,then:[],else:[]}},
|
||||
{type:'notify',label:'发通知',icon:'🔔',cat:'flow',params:{title:'',message:'',level:'info'}},
|
||||
{type:'stop_self',label:'停止本设备',icon:'⛔',cat:'flow',params:{reason:''}}
|
||||
{type:'stop_self',label:'停止本设备',icon:'⛔',cat:'flow',params:{reason:''}},
|
||||
{type:'gesture',label:'录制手势',icon:'⏺',cat:'interact',params:{points:[],speed:1.0}}
|
||||
];
|
||||
// 步骤默认值(「任务 → 动作配置」页配置,见 core/step_defaults.py);新建步骤时预填
|
||||
let _stepDefaults = null;
|
||||
@@ -332,8 +333,7 @@ var _stepEditor={
|
||||
h+='<div class="form-group"><label>最短时长(秒)</label><input type="number" class="form-control" data-key="duration_min" value="'+(p.duration_min||0.25)+'" step="0.05" min="0.05"></div>';
|
||||
h+='<div class="form-group"><label>最长时长(秒)</label><input type="number" class="form-control" data-key="duration_max" value="'+(p.duration_max||0.50)+'" step="0.05" min="0.05"></div></div>';
|
||||
h+=_humanizeRowHtml(p,'<div class="form-group"><label>幅度(屏占比)</label><input type="number" class="form-control" data-key="distance_ratio" value="'+(p.distance_ratio!=null?p.distance_ratio:0.6)+'" step="0.05" min="0.1" max="0.9"></div>');
|
||||
h+='<div class="form-row"><div class="form-group">'+(_can('devices')?'<button class="btn btn-sm" onclick="_stepEditor.recordGesture(this)">● 录制手势</button>':'')+''
|
||||
+'<div class="help">在设备画面上按住划一下,按你划的<b>方向/幅度/时长</b>回填这一步(不想手填参数时用)</div></div></div>';
|
||||
h+='<div class="help">想要"照我划的那一下原样重放",请用操作库里的《录制手势》步骤——那个是纯轨迹回放,不套这里的参数</div>';
|
||||
}else if(step.type==='click'){
|
||||
h+=_selRowHtml(p,'<div class="form-group"><label>等待超时(秒)</label><input type="number" class="form-control" data-key="wait_timeout" value="'+(p.wait_timeout||2)+'" step="0.5" min="0.5"></div>');
|
||||
}else if(step.type==='long_click'){
|
||||
@@ -347,7 +347,7 @@ var _stepEditor={
|
||||
'<div class="form-group"><label>幅度(屏占比)</label><input type="number" class="form-control" data-key="distance_ratio" value="'+(p.distance_ratio!=null?p.distance_ratio:0.6)+'" step="0.05" min="0.1" max="0.9"></div></div>'+
|
||||
'<div class="form-group form-check"><input type="checkbox" data-key="click_when_found" '+(p.click_when_found!==false?'checked':'')+'><label>找到后点击</label></div>'+
|
||||
_humanizeChkHtml(p)+
|
||||
(_can('devices')?'<div class="form-group"><button class="btn btn-sm" onclick="_stepEditor.recordGesture(this)">● 录制手势</button></div>':'')+
|
||||
|
||||
'<div class="help">上滑/下滑直到目标元素出现(每次滑动后检查一次),找不到则跳过继续。滑动同样走拟人轨迹</div>');
|
||||
}else if(step.type==='wait_el'){
|
||||
h+=_selRowHtml(p,'<div class="form-group"><label>最长等待(秒)</label><input type="number" class="form-control" data-key="timeout" value="'+(p.timeout||10)+'" min="1" max="120"></div>'+
|
||||
@@ -376,6 +376,22 @@ var _stepEditor={
|
||||
h+='<div class="form-group"><label>停止原因(可留空)</label><input type="text" class="form-control" data-key="reason" value="'+esc(p.reason||'')+'" placeholder="如 连续刷到广告"></div>';
|
||||
h+='<div class="help">只停<b>本设备</b>的任务,其它设备照常;后续步骤不再执行,任务记为「被停止」而不是失败'
|
||||
+'(不会触发失败重试)。常配合「条件判断」用:命中条件 → 先「发通知」再「停止本设备」</div>';
|
||||
}else if(step.type==='gesture'){
|
||||
var pts=(p.points||[]);
|
||||
var dur=pts.length?pts[pts.length-1][2]:0;
|
||||
h+='<div class="form-row"><div class="form-group" style="flex:2"><label>录制状态</label>'+
|
||||
'<div style="padding:6px 0;font-size:13px">'+(pts.length
|
||||
? (pts.length+' 个轨迹点 · 时长 '+(dur/1000).toFixed(2)+' 秒 · 起点 ('+pts[0][0]+','+pts[0][1]+')')
|
||||
: '<span class="text-muted">还没有录制</span>')+'</div></div>';
|
||||
if(_can('devices')){
|
||||
h+='<div class="form-group"><button class="btn btn-sm btn-primary" onclick="_stepEditor.recordInto(this,\'phone\')">● 手机上录</button></div>';
|
||||
h+='<div class="form-group"><button class="btn btn-sm" onclick="_stepEditor.recordInto(this,\'web\')">● 网页上录</button></div>';
|
||||
}
|
||||
h+='<div class="form-group"><label>速度</label><input type="number" class="form-control" data-key="speed" value="'+(p.speed!=null?p.speed:1.0)+'" step="0.1" min="0.1" max="5"></div>';
|
||||
h+='<div class="form-group"><button class="btn btn-sm btn-danger" onclick="_stepEditor.clearGesture(this)">清空</button></div></div>';
|
||||
h+='<div class="help">这是<b>纯录制回放</b>:路径与时长照录制原样重放,不做弧线/抖动/手速那套加工(和「滑动」步骤是两回事)。'
|
||||
+'<b>手机上录</b>=用手指在真机上划(最真实);<b>网页上录</b>=在弹窗画面里按住鼠标拖。'
|
||||
+'轨迹按屏幕绝对像素存,换分辨率不同的设备可能偏,同型号/同分辨率最稳</div>';
|
||||
}else if(step.type==='wait'){
|
||||
h+='<div class="form-row"><div class="form-group"><label>最短(秒)</label><input type="number" class="form-control" data-key="min" value="'+(p.min||1.0)+'" step="0.5" min="0.1"></div>';
|
||||
h+='<div class="form-group"><label>最长(秒)</label><input type="number" class="form-control" data-key="max" value="'+(p.max||3.0)+'" step="0.5" min="0.1"></div></div>';
|
||||
@@ -518,23 +534,39 @@ var _stepEditor={
|
||||
if(d)Object.keys(d).forEach(function(k){ if(d[k]!==undefined)params[k]=d[k]; });
|
||||
return {id:_newStepId(),type:lib.type,label:lib.label,params:params};
|
||||
},
|
||||
// 录制手势:在设备上划一下,按你划的方向/幅度/时长回填这一步
|
||||
recordGesture:function(btn){
|
||||
// 递归找步骤对象(按 id)——写入录音必须改 _steps,不能只改 DOM
|
||||
_findStep:function(id,arr){
|
||||
var self=this, found=null;
|
||||
(arr||this._steps).forEach(function(st){
|
||||
if(found)return;
|
||||
if(st.id===id){found=st;return;}
|
||||
_childArrs(st).forEach(function(a){ if(!found)found=self._findStep(id,a); });
|
||||
});
|
||||
return found;
|
||||
},
|
||||
// 给某个「录制手势」步骤录制轨迹(src: 'phone' 手机上录 / 'web' 网页上录)
|
||||
recordInto:function(btn,src){
|
||||
var card=btn.closest('.step-card');
|
||||
if(!card){return;}
|
||||
var step=card?this._findStep(card.dataset.stepId):null;
|
||||
if(!step){showToast('找不到这个步骤','error');return;}
|
||||
if(typeof openRecorder!=='function'){showToast('录制器未加载','error');return;}
|
||||
var self=this;
|
||||
openRecorder({mode:'gesture',onDone:function(p){
|
||||
card.querySelectorAll('[data-key]').forEach(function(inp){
|
||||
var k=inp.dataset.key;
|
||||
if(p[k]===undefined)return;
|
||||
if(inp.type==='checkbox')inp.checked=!!p[k];
|
||||
else inp.value=p[k];
|
||||
});
|
||||
self._syncFromDom();
|
||||
showToast('已按你划的手势回填:'+p.direction+' 幅度'+p.distance_ratio,'success');
|
||||
openRecorder({mode:'gesture', autoStartPhone: src==='phone', onDone:function(p,from){
|
||||
step.params={points:p.points,speed:p.speed||1.0};
|
||||
var dur=p.points.length?p.points[p.points.length-1][2]:0;
|
||||
step.label='录制手势 '+(from==='phone'?'手机':'网页')+' '+p.points.length+'点';
|
||||
self._renderCanvas();
|
||||
showToast('已录到 '+(p.points.length)+' 个点 / '+(dur/1000).toFixed(2)+' 秒','success');
|
||||
}});
|
||||
},
|
||||
clearGesture:function(btn){
|
||||
var card=btn.closest('.step-card');
|
||||
var step=card?this._findStep(card.dataset.stepId):null;
|
||||
if(!step){return;}
|
||||
step.params={points:[],speed:1.0};
|
||||
this._renderCanvas();
|
||||
showToast('已清空录制的轨迹','success');
|
||||
},
|
||||
// 测试此步骤:在选中设备上试执行当前步骤,验证选择器
|
||||
_testStep:function(btn){
|
||||
var card=btn.closest('.step-card');
|
||||
@@ -807,6 +839,10 @@ var _stepEditor={
|
||||
if(!s.params.text){
|
||||
warnings.push(name+' "'+label+'": 注入内容为空,步骤会跳过。');
|
||||
}
|
||||
}else if(s.type==='gesture'){
|
||||
if(!(s.params.points||[]).length){
|
||||
warnings.push(name+' "'+label+'": 录制手势还没有轨迹,执行时会被跳过。');
|
||||
}
|
||||
}else if(s.type==='if_el'){
|
||||
if(!s.params.selector_value){
|
||||
warnings.push(name+' "'+label+'": 条件判断的选择器为空,会直接走"未找到"分支。');
|
||||
|
||||
Reference in New Issue
Block a user