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:
2026-09-20 14:32:35 +08:00
parent 9a44eeba95
commit bd51306a2a
12 changed files with 782 additions and 7 deletions
+51
View File
@@ -464,6 +464,37 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
.agent-inputbar .btn-primary:hover{background:#155e75}
.agent-inputbar .btn-primary:disabled{background:#9ca3af}
.agent-empty{color:#6b7280;font-size:13px;text-align:center;padding:44px 0}
/* ===== 动作配置:步骤默认值表单 ===== */
.cfg-card{border:1px solid var(--border-strong);border-radius:10px;padding:12px;margin-bottom:10px}
.cfg-card>h4{margin:0 0 8px;font-size:13px}
.cfg-row{display:flex;gap:12px;flex-wrap:wrap}
.cfg-row .pg{display:flex;flex-direction:column;gap:2px;min-width:120px}
.cfg-row .pg label{font-size:11px;color:var(--text-light)}
/* ===== 动作录制器 ===== */
.rec-box{width:min(1180px,96vw);max-height:92vh;display:flex;flex-direction:column}
.rec-head{display:flex;align-items:center;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border)}
.rec-head b{font-size:14px}
.rec-body{display:flex;gap:14px;padding:14px 16px;overflow:hidden;flex:1;min-height:0}
.rec-stage{flex:1;min-width:0;position:relative;background:var(--ink);border-radius:10px;
display:flex;align-items:center;justify-content:center;overflow:hidden}
.rec-stage img{max-width:100%;max-height:74vh;display:block;cursor:crosshair;user-select:none}
.rec-hint{color:#8a94a6;font-size:13px;padding:40px}
.rec-warn{position:absolute;left:10px;top:10px;background:#fbbf24;color:#3b2f00;
font-size:11px;padding:2px 8px;border-radius:6px}
.rec-line{position:absolute;border:2px dashed #f97316;border-radius:4px;pointer-events:none}
.rec-side{width:330px;flex:0 0 330px;display:flex;flex-direction:column;gap:8px;min-height:0}
.rec-ops{display:flex;gap:6px;flex-wrap:wrap}
.rec-row{display:flex;gap:6px}
.rec-list-title{font-size:12px;color:var(--text-light);margin-top:4px}
.rec-list{flex:1;overflow-y:auto;border:1px solid var(--border);border-radius:8px;padding:6px;min-height:120px}
.rec-item{display:flex;align-items:center;gap:6px;padding:3px 4px;border-bottom:1px dashed var(--border)}
.rec-item:last-child{border-bottom:0}
.rec-idx{width:18px;color:var(--text-light);font-size:11px;text-align:right}
.rec-label{flex:1;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rec-ops2{display:flex;gap:2px}
.rec-foot{border-top:1px solid var(--border);padding-top:8px}
</style>
</head>
<body>
@@ -556,6 +587,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
<div class="sub-tabs">
<button class="sub-tab active" data-sub="plan" onclick="showSubTab('tasks','plan')">任务计划</button>
<button class="sub-tab" data-sub="actions" onclick="showSubTab('tasks','actions')">自定义动作</button>
<button class="sub-tab" data-sub="actioncfg" onclick="showSubTab('tasks','actioncfg')">动作配置</button>
</div>
<!-- 子分栏 1:任务计划 -->
@@ -588,6 +620,24 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
</table>
<div class="list-pager" id="pager-actions"></div>
</div>
<!-- 子分栏 3:动作配置(步骤默认值 + 录制动作) -->
<div class="sub-panel" id="tasks-sub-actioncfg">
<div class="toolbar">
<button class="btn btn-primary" onclick="saveStepDefaults()" data-perm="tasks">保存默认值</button>
<button class="btn" onclick="resetStepDefaults()">恢复出厂</button>
<button class="btn" onclick="openRecorder({mode:'action'})" data-perm="devices">● 录制动作</button>
<button class="btn" onclick="loadActionConfig()">刷新</button>
<span class="hint" id="cfg-hint"></span>
</div>
<div class="help">
<b>步骤默认值</b>:新建步骤时预填这套参数(<b>只影响之后新建的步骤</b>,已有步骤不动;
单步想改就打开那个任务改那一步)。<br>
<b>录制动作</b>:在设备画面上点/划/按键,自动翻译成步骤 —— 既能存成可复用的「自定义动作」,
也能只取一个手势回填到滑动步骤上(步骤编辑器里滑动那一步有「录制手势」按钮)。
</div>
<div id="cfg-forms"><div class="text-muted" style="padding:10px">加载中…</div></div>
</div>
</div>
<!-- ========== Tab 3: 分组 ========== -->
@@ -1369,5 +1419,6 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
<script src="/static/admin/system.js"></script>
<script src="/static/admin/notify.js"></script>
<script src="/static/admin/steplog.js"></script>
<script src="/static/admin/recorder.js"></script>
</body>
</html>