feat(任务): 「动作配置」分栏——步骤默认值 + 动作录制器
需求:滑动要能"我自己录制";在自定义动作旁边加一个分栏,能配动作默认值、
能录制操作当动作、能自己建立动作。
**步骤默认值**(core/step_defaults.py,存 app_meta.step_defaults 单键,不建表):
- 覆盖 滑动(方向/时长区间/幅度/抖动/拟人)、点击(等待超时)、长按(时长/超时)、
等待(区间)、按键、输入文字(模式/候选文案/清空);
- **只影响之后新建的步骤**,已有步骤不动;新建时 `_makeStep` 用出厂值打底 + 默认值覆盖;
- 只存与出厂值不同的字段(以后调出厂默认,没配过的能跟着走);
- 字段白名单 + 范围夹取(SPEC):越界夹边界、非法值丢弃 → 手改 JSON 塞脏值也进不来。
**动作录制器**(static/admin/recorder.js):
- 在设备画面上点/划/按键/输入 → 自动翻译成步骤:
点→命中元素记「点击元素」(选择器)、没命中退化成「点击坐标」(百分比);
划→「滑动」方向/幅度/时长全按你实际操作算;停顿≥1s 可选记成「等待」(±15%);
- 两个入口:动作配置页「录制动作」(存成自定义动作,进动作库复用)、
步骤编辑器滑动那步的「录制手势」(只取手势回填该步骤);
- 画面+元素树走 /api/uiauto/snapshot 一次取齐,**每次操作后自动刷新**,
所以下一次点击用的是最新的树;执行仍走 /api/screen/{tap,swipe,key,text}(与大屏同一套)。
接口:GET/POST /api/step_defaults(读登录、写需 tasks 权限)。
文档:TASK_DEV §3.1(含翻译规则表与三条实现口径)、API §2.3、DATA_MODEL §5、README。
自测(无头浏览器 + CDP 真模拟拖动):默认值保存→回读→新建滑动步骤确实预填 0.42;
录制器取到画面与元素树;模拟"按住往上划"→ 录出
{direction:up, distance_ratio:0.5, duration_min:0.47, humanize:true}(幅度/时长按实际操作算);
恢复出厂链路 0.6 通过;页面无 JS 报错。
This commit is contained in:
@@ -503,3 +503,103 @@ function closeActionModal(){
|
||||
document.getElementById('action-modal-overlay').classList.remove('show');
|
||||
}
|
||||
|
||||
|
||||
// ================== 动作配置:步骤默认值 ==================
|
||||
// 存 app_meta.step_defaults(见 core/step_defaults.py);只影响**之后新建**的步骤。
|
||||
const _CFG_LABELS = {
|
||||
swipe: '滑动', click: '点击元素', long_click: '长按元素', wait: '等待',
|
||||
key_event: '按键', input_text: '输入文字',
|
||||
};
|
||||
const _CFG_FIELD_LABELS = {
|
||||
direction: '方向', duration_min: '最短时长(秒)', duration_max: '最长时长(秒)',
|
||||
distance_ratio: '幅度(屏占比)', jitter: '抖动幅度', humanize: '拟人轨迹',
|
||||
wait_timeout: '等待超时(秒)', duration: '长按时长(秒)', min: '最短(秒)',
|
||||
max: '最长(秒)', key: '按键', mode: '输入模式', texts: '候选文案(每行一条)',
|
||||
fixed_text: '固定文字', clear_first: '输入前清空',
|
||||
};
|
||||
const _CFG_DIR_LABELS = {up:'上滑', down:'下滑', left:'左滑', right:'右滑'};
|
||||
let _cfgPayload = null;
|
||||
|
||||
// 取一次步骤默认值缓存在 _stepDefaults(editor.js 的 _makeStep 用它预填新步骤)
|
||||
async function loadStepDefaults(force) {
|
||||
if (_stepDefaults && !force) return _stepDefaults;
|
||||
const r = await apiGet('/api/step_defaults');
|
||||
_stepDefaults = (r && r.ok) ? (r.defaults || {}) : {};
|
||||
return _stepDefaults;
|
||||
}
|
||||
|
||||
async function loadActionConfig(force) {
|
||||
const box = document.getElementById('cfg-forms');
|
||||
if (!box) return;
|
||||
if (_cfgPayload && !force) { renderStepDefaults(); return; }
|
||||
box.innerHTML = '<div class="text-muted" style="padding:10px">加载中…</div>';
|
||||
const r = await apiGet('/api/step_defaults');
|
||||
if (!r || !r.ok) { box.innerHTML = '<div class="text-muted" style="padding:10px">加载失败</div>'; return; }
|
||||
_cfgPayload = r;
|
||||
_stepDefaults = r.defaults || {}; // 编辑器新建步骤时预填用
|
||||
renderStepDefaults();
|
||||
}
|
||||
|
||||
function renderStepDefaults() {
|
||||
const box = document.getElementById('cfg-forms');
|
||||
if (!box || !_cfgPayload) return;
|
||||
const d = _cfgPayload.defaults || {}, kinds = _cfgPayload.kinds || {}, spec = _cfgPayload.spec || {};
|
||||
box.innerHTML = Object.keys(d).map(function (st) {
|
||||
const fields = Object.keys(kinds[st] || {}).map(function (k) {
|
||||
const kind = kinds[st][k], v = d[st][k], rule = spec[st] ? spec[st][k] : null;
|
||||
let inp;
|
||||
if (kind === 'bool') {
|
||||
inp = '<label class="form-check"><input type="checkbox" data-st="' + st + '" data-k="' + k + '"' +
|
||||
(v ? ' checked' : '') + '><span>' + (_CFG_FIELD_LABELS[k] || k) + '</span></label>';
|
||||
return inp;
|
||||
}
|
||||
if (kind === 'choice') {
|
||||
inp = '<select class="form-control" data-st="' + st + '" data-k="' + k + '">' +
|
||||
(rule || []).map(function (o) {
|
||||
const lab = _CFG_DIR_LABELS[o] || (o === 'random' ? '随机' : o === 'fixed' ? '指定' : o);
|
||||
return '<option value="' + esc(o) + '"' + (v === o ? ' selected' : '') + '>' + esc(lab) + '</option>';
|
||||
}).join('') + '</select>';
|
||||
} else if (kind === 'num') {
|
||||
inp = '<input type="number" class="form-control" data-st="' + st + '" data-k="' + k + '" value="' + esc(v) +
|
||||
'" step="0.05" min="' + (rule ? rule[0] : '') + '" max="' + (rule ? rule[1] : '') + '">';
|
||||
} else if (k === 'texts') {
|
||||
inp = '<textarea class="form-control" rows="3" data-st="' + st + '" data-k="' + k + '">' + esc(v) + '</textarea>';
|
||||
} else {
|
||||
inp = '<input type="text" class="form-control" data-st="' + st + '" data-k="' + k + '" value="' + esc(v) + '">';
|
||||
}
|
||||
return '<div class="pg"><label>' + (_CFG_FIELD_LABELS[k] || k) + '</label>' + inp + '</div>';
|
||||
}).join('');
|
||||
return '<div class="cfg-card"><h4>' + (_CFG_LABELS[st] || st) + ' <span class="text-muted" style="font-weight:400">' + st + '</span></h4>' +
|
||||
'<div class="cfg-row">' + fields + '</div></div>';
|
||||
}).join('');
|
||||
document.getElementById('cfg-hint').textContent = '';
|
||||
}
|
||||
|
||||
function collectStepDefaults() {
|
||||
const out = {};
|
||||
document.querySelectorAll('#cfg-forms [data-st]').forEach(function (el) {
|
||||
const st = el.dataset.st, k = el.dataset.k;
|
||||
out[st] = out[st] || {};
|
||||
if (el.type === 'checkbox') out[st][k] = el.checked;
|
||||
else if (el.type === 'number') out[st][k] = parseFloat(el.value);
|
||||
else out[st][k] = el.value;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
async function saveStepDefaults() {
|
||||
const r = await apiPost('/api/step_defaults', { defaults: collectStepDefaults() });
|
||||
if (!r || !r.ok) { showToast((r && r.error) || '保存失败', 'error'); return; }
|
||||
_cfgPayload = r;
|
||||
_stepDefaults = r.defaults || {};
|
||||
showToast('默认值已保存(只影响之后新建的步骤)', 'success');
|
||||
document.getElementById('cfg-hint').textContent = '已保存 ' + new Date().toLocaleTimeString();
|
||||
renderStepDefaults();
|
||||
}
|
||||
|
||||
function resetStepDefaults() {
|
||||
if (!_cfgPayload) return;
|
||||
if (!confirm('恢复成出厂默认?(只影响这张表单,点「保存默认值」才真正生效)')) return;
|
||||
_cfgPayload.defaults = JSON.parse(JSON.stringify(_cfgPayload.factory || {}));
|
||||
renderStepDefaults();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user