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+'": 条件判断的选择器为空,会直接走"未找到"分支。');
|
||||
|
||||
+92
-49
@@ -1,12 +1,17 @@
|
||||
// ================== 动作录制器 ==================
|
||||
// 在设备截图上操作(点/划/按键/输入),自动翻译成**步骤**:
|
||||
// · 点 → 命中元素就记「点击元素」(选择器),抓不到就退化成「点击坐标」(百分比)
|
||||
// · 划 → 记「滑动」:方向 + 幅度(屏占比) + 实际时长
|
||||
// · 划 → 记**「录制手势」**:完整轨迹点列 [[x,y,t_ms],…],**纯回放**,不再套滑动逻辑
|
||||
// · 停顿 → 可选记成「等待」(按真实间隔 ±15%)
|
||||
//
|
||||
// 两种用法(同一个录窗口):
|
||||
// 两个录制来源:
|
||||
// · 网页上录:在这个窗口里按住鼠标拖(采样路径 + 时间)
|
||||
// · 手机上录:点「手机上录」后**用手指在真机上划**——后端读 getevent 抓真触屏,
|
||||
// 录到的是人手的真实轨迹(比在网页上模拟更真)
|
||||
//
|
||||
// 两种用法(同一个窗口):
|
||||
// mode='action' —— 录一段,存成「自定义动作」(进动作库,能拖进任何任务)
|
||||
// mode='gesture' —— 只取最后一个手势,回填当前滑动步骤的参数
|
||||
// mode='gesture' —— 录到的手势直接回填给调用方(步骤编辑器里的「录制手势」步骤)
|
||||
//
|
||||
// 元素树用 /api/uiauto/snapshot 一次取齐(截图 + 元素),**每次操作后自动刷新**,
|
||||
// 这样下一次点击就能用最新的树翻译选择器(抖音这种没有 id 的界面靠 text/desc)。
|
||||
@@ -26,6 +31,7 @@ async function openRecorder(opts) {
|
||||
elements: [],
|
||||
devW: 0, devH: 0, imgURL: '',
|
||||
lastTs: 0, dragFrom: null, busy: false, keepPause: true,
|
||||
samples: [], phone: null, autoPhone: !!opts.autoStartPhone,
|
||||
shots: 0, unstable: false,
|
||||
};
|
||||
const ov = document.createElement('div');
|
||||
@@ -66,6 +72,7 @@ async function openRecorder(opts) {
|
||||
document.body.appendChild(ov);
|
||||
await recLoadDevices();
|
||||
recRender();
|
||||
if (_rec.autoPhone && _rec.serial) recStartPhone();
|
||||
}
|
||||
|
||||
function closeRecorder() {
|
||||
@@ -88,15 +95,25 @@ async function recLoadDevices() {
|
||||
});
|
||||
if (_rec.serial) { sel.value = _rec.serial; }
|
||||
else if ((r.items || []).length) { sel.value = r.items[0].serial; _rec.serial = r.items[0].serial; }
|
||||
if (_rec.serial) recRefresh();
|
||||
if (_rec.serial) { await recWake(); recRefresh(); }
|
||||
}
|
||||
|
||||
function recPickDevice() {
|
||||
async function recPickDevice() {
|
||||
const sel = document.getElementById('rec-device');
|
||||
_rec.serial = sel ? sel.value : '';
|
||||
_rec.elements = [];
|
||||
recRender();
|
||||
if (_rec.serial) recRefresh();
|
||||
if (_rec.serial) { await recWake(); recRefresh(); }
|
||||
}
|
||||
|
||||
// 录制前先唤醒设备:**息屏时 u2 抓 UI 树会退化到几十秒**(实测 65s,唤醒后 3.3s),
|
||||
// 截图也是一片黑。唤醒是幂等的,多调一次没有副作用。
|
||||
async function recWake() {
|
||||
if (!_rec || !_rec.serial) return;
|
||||
try {
|
||||
const r = await apiPost('/api/device/screen_all', { mode: 'on', serials: [_rec.serial] });
|
||||
if (r && r.ok) await new Promise(function (z) { setTimeout(z, 800); });
|
||||
} catch (e) { /* 唤醒失败也继续,用户自己会看到黑屏 */ }
|
||||
}
|
||||
|
||||
// 取一张快照:截图 + 元素树(同一个连接背靠背取,避免框错位)
|
||||
@@ -168,6 +185,8 @@ function recDown(e) {
|
||||
const p = _recPos(e);
|
||||
if (!p) return;
|
||||
_rec.dragFrom = p; _rec.dragTo = null; _rec.dragT0 = Date.now();
|
||||
// 采样:拖动过程中把每个点连时间一起记下来(这就是"轨迹")
|
||||
_rec.samples = [[p.x, p.y, 0]];
|
||||
recPaintOverlay();
|
||||
}
|
||||
|
||||
@@ -176,6 +195,11 @@ function recMove(e) {
|
||||
const p = _recPos(e);
|
||||
if (!p) return;
|
||||
_rec.dragTo = p;
|
||||
const t = Date.now() - _rec.dragT0;
|
||||
const last = _rec.samples[_rec.samples.length - 1];
|
||||
if (!last || t - last[2] >= 8 || Math.abs(p.x - last[0]) + Math.abs(p.y - last[1]) >= 3) {
|
||||
if (_rec.samples.length < 400) _rec.samples.push([p.x, p.y, t]);
|
||||
}
|
||||
recPaintOverlay();
|
||||
}
|
||||
|
||||
@@ -188,12 +212,60 @@ async function recUp(e) {
|
||||
recPaintOverlay();
|
||||
const dist = Math.hypot(b.x - a.x, b.y - a.y);
|
||||
if (dist < 12 || durMs < 60) { // 视为点击
|
||||
_rec.samples = [];
|
||||
await recTap(a.x, a.y);
|
||||
} else {
|
||||
await recSwipe(a, b, durMs);
|
||||
const pts = _rec.samples.slice();
|
||||
pts.push([b.x, b.y, durMs]); // 末点补上(决定手势收尾位置)
|
||||
_rec.samples = [];
|
||||
recPushGesture(pts, 'web');
|
||||
await recRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
// 一条录到的轨迹 → 「录制手势」步骤(**纯回放**:点列+时长原样存下,不再套滑动逻辑)
|
||||
function recPushGesture(points, src) {
|
||||
if (!points || points.length < 2) { showToast('没录到有效轨迹(太短了)', 'error'); return; }
|
||||
if (_rec.mode === 'gesture') { // 回填模式:直接交给调用方,收工
|
||||
const cb = _rec.onDone;
|
||||
closeRecorder();
|
||||
if (cb) cb({ points: points, speed: 1.0 }, src);
|
||||
return;
|
||||
}
|
||||
const dur = points[points.length - 1][2] || 0;
|
||||
_recPush({ type: 'gesture',
|
||||
label: '录制手势 ' + points.length + '点/' + (dur / 1000).toFixed(1) + 's',
|
||||
params: { points: points, speed: 1.0 } });
|
||||
}
|
||||
|
||||
// ---------- 手机端录制(getevent 抓真手指) ----------
|
||||
async function recStartPhone() {
|
||||
if (!_rec.serial) { showToast('先选设备', 'error'); return; }
|
||||
const r = await apiPost('/api/gesture/record/start', { serial: _rec.serial });
|
||||
if (!r || !r.ok) { showToast((r && r.error) || '开始失败', 'error'); return; }
|
||||
_rec.phone = { token: r.token, t0: Date.now() };
|
||||
showToast('开始录制——现在用手指在手机上划', 'success');
|
||||
recRenderFoot();
|
||||
}
|
||||
|
||||
async function recStopPhone() {
|
||||
if (!_rec.phone) return;
|
||||
const token = _rec.phone.token;
|
||||
_rec.phone = null;
|
||||
const r = await apiPost('/api/gesture/record/stop', { token: token });
|
||||
if (!r || !r.ok) { showToast((r && r.error) || '停止失败', 'error'); recRenderFoot(); return; }
|
||||
const gs = r.gestures || [];
|
||||
if (!gs.length) {
|
||||
showToast('没录到手势——请在手机屏幕上用指划一下(不是点)', 'error');
|
||||
recRenderFoot(); return;
|
||||
}
|
||||
if (_rec.mode === 'gesture') { recPushGesture(gs[gs.length - 1], 'phone'); return; }
|
||||
gs.forEach(function (g) { recPushGesture(g, 'phone'); });
|
||||
showToast('录到 ' + gs.length + ' 个手势', 'success');
|
||||
recRenderFoot();
|
||||
recRefresh();
|
||||
}
|
||||
|
||||
// ---------- 翻译:点击 → 步骤 ----------
|
||||
function _recFindElement(x, y) {
|
||||
// 取包含该点的最小元素;优先可点击的(和 /api/screen/tap 的 snap 同一口径)
|
||||
@@ -237,33 +309,6 @@ function _recShort(el) {
|
||||
return s ? ('「' + String(s).slice(0, 12) + '」') : '';
|
||||
}
|
||||
|
||||
// ---------- 翻译:滑动 → 步骤 ----------
|
||||
async function recSwipe(a, b, durMs) {
|
||||
if (!_rec || _rec.busy) return;
|
||||
_rec.busy = true;
|
||||
try {
|
||||
const dx = b.x - a.x, dy = b.y - a.y;
|
||||
const vertical = Math.abs(dy) >= Math.abs(dx);
|
||||
const direction = vertical ? (dy < 0 ? 'up' : 'down') : (dx < 0 ? 'left' : 'right');
|
||||
const span = Math.abs(vertical ? dy : dx);
|
||||
const base = vertical ? _rec.devH : _rec.devW;
|
||||
const ratio = Math.max(0.05, Math.min(0.9, span / base));
|
||||
const dur = Math.max(0.08, durMs / 1000);
|
||||
const step = { type: 'swipe', label: '滑动 ' + direction,
|
||||
params: { direction: direction,
|
||||
duration_min: +(dur * 0.9).toFixed(2),
|
||||
duration_max: +(dur * 1.15).toFixed(2),
|
||||
distance_ratio: +ratio.toFixed(2),
|
||||
jitter: 0.15, humanize: true } };
|
||||
_recPush(step);
|
||||
const r = await apiPost('/api/screen/swipe',
|
||||
{ serial: _rec.serial, x1: a.x, y1: a.y, x2: b.x, y2: b.y,
|
||||
duration: Math.round(durMs) / 1000 });
|
||||
if (r && !r.ok) showToast((r.error || '滑动失败'), 'error');
|
||||
await recRefresh();
|
||||
} finally { _rec.busy = false; }
|
||||
}
|
||||
|
||||
async function recKey(key) {
|
||||
if (!_rec || !_rec.serial) { showToast('先选设备', 'error'); return; }
|
||||
_recPush({ type: 'key_event', label: '按键 ' + key, params: { key: key } });
|
||||
@@ -338,26 +383,24 @@ function recRenderFoot() {
|
||||
if (!foot) return;
|
||||
const steps = _rec.steps;
|
||||
if (_rec.mode === 'gesture') {
|
||||
const sw = steps.filter(function (s) { return s.type === 'swipe'; });
|
||||
foot.innerHTML = sw.length
|
||||
? '<button class="btn btn-primary" onclick="recApplyGesture()">用这个手势(共录到 ' + sw.length + ' 个滑动,取最后一个)</button>' +
|
||||
'<div class="help">把方向/幅度/时长回填到当前滑动步骤</div>'
|
||||
: '<div class="text-muted" style="font-size:12px">在画面上划一下(按住拖动),然后点这里</div>';
|
||||
// 回填模式:录到就立刻填回那个步骤,不用再点确认
|
||||
foot.innerHTML = _rec.phone
|
||||
? '<button class="btn btn-danger" onclick="recStopPhone()">■ 停止并填入</button>'
|
||||
: '<button class="btn btn-primary" onclick="recStartPhone()">● 手机上录</button>' +
|
||||
'<div class="help">或在左边画面上按住鼠标拖一下——录到就立刻填进那一步</div>';
|
||||
return;
|
||||
}
|
||||
const phoneBtn = _rec.phone
|
||||
? '<button class="btn btn-danger" onclick="recStopPhone()">■ 停止手机录制</button>'
|
||||
: '<button class="btn btn-primary" onclick="recStartPhone()">● 手机上录</button>';
|
||||
foot.innerHTML =
|
||||
'<div class="rec-row" style="margin-bottom:6px">' + phoneBtn +
|
||||
'<span class="help" style="margin:0">' + (_rec.phone
|
||||
? '正在录:请用手指在手机屏幕上划,划完点「停止」'
|
||||
: '手机上用手指划 = 录真手指轨迹;在左边画面上拖 = 网页录制') + '</span></div>' +
|
||||
'<div class="rec-row"><input id="rec-name" class="form-control" placeholder="动作名,如「抖音-进评论」">' +
|
||||
'<button class="btn btn-primary" onclick="recSaveAsAction()">存成动作</button></div>' +
|
||||
'<div class="help">存进「自定义动作」库,之后能拖进任何任务的步骤画布</div>';
|
||||
}
|
||||
|
||||
function recApplyGesture() {
|
||||
const sw = _rec.steps.filter(function (s) { return s.type === 'swipe'; });
|
||||
if (!sw.length) return;
|
||||
const p = sw[sw.length - 1].params;
|
||||
const cb = _rec.onDone;
|
||||
closeRecorder();
|
||||
if (cb) cb(p);
|
||||
'<div class="help">划出来的每一段都是一条「录制手势」步骤(按原路径与时长回放,不再套滑动逻辑)</div>';
|
||||
}
|
||||
|
||||
async function recSaveAsAction() {
|
||||
|
||||
Reference in New Issue
Block a user