feat: 动作库查看/管理(接口 + AI 控制台「🎬 动作库」面板)+ 未沉淀灰卡提示
- 后端:GET /api/agent/actions(列表)、POST /api/agent/actions/delete、 POST /api/agent/actions/save(新增/编辑,steps 走 _sanitize_actions 校验: type 白名单 + 必填 + **拒绝坐标 click_xy**),均 @admin_required - 前端:AI 控制台右上角新增「🎬 动作库」按钮与模态框——列出 名称/app/别名/ 步骤摘要(含元素定位)/命中次数/更新时间;支持编辑(表单 + steps JSON)、删除、 手动新建;保存失败(如写坐标)即时 toast 原因 - 未沉淀提示:本轮无可沉淀动作时也推送一张 🧠 动作经验 灰卡 「本轮未沉淀动作(步骤以坐标定位为主,缺少可复用的元素定位信息)」 - doc/API.md:登记三个新接口 实测:列表返回自动沉淀的动作;保存合法动作 200;提交 click_xy 步骤被 400 拒绝; 删除生效
This commit is contained in:
@@ -829,6 +829,94 @@ def agent_experience_keep():
|
||||
db.session.commit()
|
||||
return jsonify({"ok": True, "msg": f"经验 #{exp_id} 已保留"})
|
||||
|
||||
# ================== 动作经验库(agent_action:查看/删除/编辑) ==================
|
||||
def _action_row_to_dict(r):
|
||||
def _j(s, d):
|
||||
try:
|
||||
return json.loads(s) if s else d
|
||||
except Exception:
|
||||
return d
|
||||
return {"id": r[0], "name": r[1], "app": r[2], "aliases": _j(r[3], []),
|
||||
"params": _j(r[4], []), "steps": _j(r[5], []), "preconditions": r[6],
|
||||
"hits": r[7] or 0, "updated_at": r[8] or ""}
|
||||
|
||||
|
||||
@bp.route("/api/agent/actions")
|
||||
@admin_required
|
||||
def agent_actions_list():
|
||||
"""动作经验库列表(命名动作 + 编辑器 schema 步骤 + 元素定位,禁坐标)。"""
|
||||
with _flask_app.app_context():
|
||||
_ensure_action_table()
|
||||
rows = db.session.execute(db.text(
|
||||
"SELECT id, name, app, aliases, params, steps, preconditions, hits, updated_at "
|
||||
"FROM agent_action ORDER BY hits DESC, id DESC")).fetchall()
|
||||
return jsonify({"ok": True, "actions": [_action_row_to_dict(r) for r in rows]})
|
||||
|
||||
|
||||
@bp.route("/api/agent/actions/delete", methods=["POST"])
|
||||
@admin_required
|
||||
def agent_actions_delete():
|
||||
aid = int((request.json or {}).get("id") or 0)
|
||||
if aid <= 0:
|
||||
return jsonify({"ok": False, "error": "缺少 id"}), 400
|
||||
with _flask_app.app_context():
|
||||
_ensure_action_table()
|
||||
db.session.execute(db.text("DELETE FROM agent_action WHERE id=:i"), {"i": aid})
|
||||
db.session.commit()
|
||||
return jsonify({"ok": True, "msg": f"动作 #{aid} 已删除"})
|
||||
|
||||
|
||||
@bp.route("/api/agent/actions/save", methods=["POST"])
|
||||
@admin_required
|
||||
def agent_actions_save():
|
||||
"""新增/编辑动作:{id?, name, app?, aliases?, params?, steps, preconditions?}。
|
||||
|
||||
steps 可为数组或 JSON 字符串;经 _sanitize_actions 校验(白名单/必填/禁坐标)。
|
||||
"""
|
||||
from datetime import datetime
|
||||
d = request.json or {}
|
||||
steps = d.get("steps")
|
||||
if isinstance(steps, str):
|
||||
try:
|
||||
steps = json.loads(steps)
|
||||
except Exception:
|
||||
return jsonify({"ok": False, "error": "steps 不是合法 JSON"}), 400
|
||||
item = _sanitize_actions([{"name": d.get("name"), "app": d.get("app", ""),
|
||||
"aliases": d.get("aliases") or [],
|
||||
"params": d.get("params") or [],
|
||||
"preconditions": d.get("preconditions", ""),
|
||||
"steps": steps if isinstance(steps, list) else []}])
|
||||
if not item:
|
||||
return jsonify({"ok": False, "error": "动作无效:需要 name,且 steps 需为受支持的步骤"
|
||||
"(click/open_app/input_text… 且带元素定位;不接受坐标)"}), 400
|
||||
a = item[0]
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
with _flask_app.app_context():
|
||||
_ensure_action_table()
|
||||
aid = int(d.get("id") or 0)
|
||||
if aid > 0:
|
||||
db.session.execute(db.text(
|
||||
"UPDATE agent_action SET name=:n, app=:a, aliases=:al, params=:p, steps=:s, "
|
||||
"preconditions=:pc, updated_at=:t WHERE id=:i"),
|
||||
{"n": a["name"], "a": a["app"],
|
||||
"al": json.dumps(a["aliases"], ensure_ascii=False),
|
||||
"p": json.dumps(a["params"], ensure_ascii=False),
|
||||
"s": json.dumps(a["steps"], ensure_ascii=False),
|
||||
"pc": a["preconditions"], "t": now, "i": aid})
|
||||
else:
|
||||
db.session.execute(db.text(
|
||||
"INSERT INTO agent_action(name, app, aliases, params, steps, preconditions, "
|
||||
"hits, source_prompt, created_at, updated_at) "
|
||||
"VALUES(:n,:a,:al,:p,:s,:pc,0,'(人工新增)',:t,:t)"),
|
||||
{"n": a["name"], "a": a["app"],
|
||||
"al": json.dumps(a["aliases"], ensure_ascii=False),
|
||||
"p": json.dumps(a["params"], ensure_ascii=False),
|
||||
"s": json.dumps(a["steps"], ensure_ascii=False),
|
||||
"pc": a["preconditions"], "t": now})
|
||||
db.session.commit()
|
||||
return jsonify({"ok": True, "msg": "动作已保存"})
|
||||
|
||||
|
||||
# ================== 历史会话(DeepSeek 式:多会话持久化) ==================
|
||||
# 会话 = 消息序列(JSON),一轮 run 的 user/assistant 文本完成后追加落库。
|
||||
# 单表 JSON 存储(每会话几十 KB 内,无需独立消息表)。
|
||||
@@ -1341,6 +1429,11 @@ def _agent_thread(run_id, prompt, serial, cfg):
|
||||
"args": f"已沉淀 {n_act} 个可复用动作"
|
||||
"(含元素定位,下次同类任务可直接复用)",
|
||||
"image": None}))
|
||||
else:
|
||||
q.put(("step", {"tool": "🧠 动作经验",
|
||||
"args": "本轮未沉淀动作(步骤以坐标定位为主,"
|
||||
"缺少可复用的元素定位信息)",
|
||||
"image": None}))
|
||||
except Exception as e:
|
||||
_log.warning(f"经验保存异常: {e}")
|
||||
q.put(("done", {"answer": answer}))
|
||||
|
||||
Reference in New Issue
Block a user