feat: 新增通用步骤任务+元素抓取,完善全部项目文档
- 新增 generic_steps 通用步骤任务(可视化步骤编辑器编排流程,支持 open_app/click/swipe/input_text/wait/loop/group) - 新增 core/uiauto_helper.py 封装 uiautodev 元素抓取客户端 - web_server 新增元素抓取/截图/设备列表等 API - monitor.html 新增步骤编辑器、元素抓取模态框、独立关闭逻辑 - 新增 README.md 项目总览(快速上手/架构/配置/FAQ) - 新增 doc/ARCHITECTURE.md 架构详解、doc/DEPLOY.md 部署指南、doc/API.md 接口文档 - 修复 doc/TASK_DEV.md:移除已删除的 comment 引用,补充 generic 包,更新注册示例 - .gitignore 忽略 .claude/ 工具产物
This commit is contained in:
+173
-1
@@ -13,7 +13,12 @@
|
||||
用户数据存 data/users.db(SQLite)。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import atexit
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Flask, jsonify, request, redirect, url_for, render_template, Response, make_response
|
||||
from flask_login import (LoginManager, login_user, logout_user,
|
||||
@@ -22,9 +27,10 @@ from flask_login import (LoginManager, login_user, logout_user,
|
||||
from core.stf_client import STFClient
|
||||
from core.task_manager import TaskManager
|
||||
from core.logger import get_logger, _LOG_DIR, _MODULE_FILES
|
||||
from core.models import db, init_db, User, DeviceGroup, TaskJob
|
||||
from core.models import db, init_db, User, DeviceGroup, TaskJob, CustomAction
|
||||
from core.apk_manager import ApkManager
|
||||
from core.adb_helper import screenshot
|
||||
from core import uiauto_helper
|
||||
from tasks import list_task_types, get_task_class
|
||||
|
||||
_log = get_logger("web")
|
||||
@@ -364,6 +370,121 @@ def api_device_screenshot():
|
||||
return jsonify({"ok": False, "error": data}), 500
|
||||
|
||||
|
||||
# ================== 自定义动作(步骤打包) ==================
|
||||
@app.route("/api/custom_actions")
|
||||
@login_required
|
||||
def api_custom_actions_list():
|
||||
rows = CustomAction.query.order_by(CustomAction.created_at.desc()).all()
|
||||
return jsonify({"ok": True, "actions": [r.to_dict() for r in rows]})
|
||||
|
||||
|
||||
@app.route("/api/custom_actions", methods=["POST"])
|
||||
@login_required
|
||||
def api_custom_actions_create():
|
||||
data = request.json or {}
|
||||
name = (data.get("name") or "").strip()
|
||||
if not name:
|
||||
return jsonify({"ok": False, "error": "动作名不能为空"}), 400
|
||||
steps = data.get("steps", [])
|
||||
if not steps:
|
||||
return jsonify({"ok": False, "error": "至少需要1个步骤"}), 400
|
||||
row = CustomAction(id=uuid.uuid4().hex[:8], name=name,
|
||||
icon=data.get("icon") or "📦",
|
||||
created_at=datetime.now().strftime("%Y-%m-%d %H:%M"))
|
||||
row.set_steps(steps)
|
||||
db.session.add(row)
|
||||
db.session.commit()
|
||||
return jsonify({"ok": True, "msg": "动作已保存", "action": row.to_dict()})
|
||||
|
||||
|
||||
@app.route("/api/custom_actions/<action_id>", methods=["PUT"])
|
||||
@login_required
|
||||
def api_custom_actions_update(action_id):
|
||||
row = CustomAction.query.get(action_id)
|
||||
if not row:
|
||||
return jsonify({"ok": False, "error": "动作不存在"}), 404
|
||||
data = request.json or {}
|
||||
if "name" in data:
|
||||
name = (data["name"] or "").strip()
|
||||
if not name:
|
||||
return jsonify({"ok": False, "error": "动作名不能为空"}), 400
|
||||
row.name = name
|
||||
if "icon" in data:
|
||||
row.icon = data["icon"] or "📦"
|
||||
if "steps" in data:
|
||||
if not data["steps"]:
|
||||
return jsonify({"ok": False, "error": "至少需要1个步骤"}), 400
|
||||
row.set_steps(data["steps"])
|
||||
db.session.commit()
|
||||
return jsonify({"ok": True, "msg": "已更新", "action": row.to_dict()})
|
||||
|
||||
|
||||
@app.route("/api/custom_actions/<action_id>", methods=["DELETE"])
|
||||
@login_required
|
||||
def api_custom_actions_delete(action_id):
|
||||
row = CustomAction.query.get(action_id)
|
||||
if not row:
|
||||
return jsonify({"ok": False, "error": "动作不存在"}), 404
|
||||
db.session.delete(row)
|
||||
db.session.commit()
|
||||
return jsonify({"ok": True, "msg": "已删除"})
|
||||
|
||||
|
||||
@app.route("/api/uiauto/elements")
|
||||
@login_required
|
||||
def api_uiauto_elements():
|
||||
"""获取设备当前 UI 元素树(供步骤编辑器"抓取元素"用)。
|
||||
|
||||
依赖本地运行的 uiautodev 服务(端口 20242)。
|
||||
?serial=xxx 设备 serial
|
||||
返回:
|
||||
200 — {ok:true, elements:[...]}
|
||||
503 — {ok:false, error:"..."}(uiauto2 未启动)
|
||||
"""
|
||||
serial = request.args.get("serial", "")
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||
ok, data = uiauto_helper.get_elements(serial)
|
||||
if ok:
|
||||
return jsonify({"ok": True, "elements": data})
|
||||
return jsonify({"ok": False, "error": data}), 503
|
||||
|
||||
|
||||
@app.route("/api/uiauto/status")
|
||||
@login_required
|
||||
def api_uiauto_status():
|
||||
"""探测 uiauto2 本地服务是否运行(前端按钮禁启用)。"""
|
||||
return jsonify({"ok": True, "running": uiauto_helper.is_running()})
|
||||
|
||||
|
||||
@app.route("/api/uiauto/devices")
|
||||
@login_required
|
||||
def api_uiauto_devices():
|
||||
"""获取 uiauto2 已连接的设备列表(供抓取元素时选择设备)。"""
|
||||
ok, data = uiauto_helper.list_devices()
|
||||
if ok:
|
||||
return jsonify({"ok": True, "devices": data})
|
||||
return jsonify({"ok": False, "error": data}), 503
|
||||
|
||||
|
||||
@app.route("/api/uiauto/screenshot")
|
||||
@login_required
|
||||
def api_uiauto_screenshot():
|
||||
"""通过 uiauto2 获取设备截图(供抓取元素时显示设备画面)。
|
||||
|
||||
?serial=xxx 设备 serial
|
||||
直接返回 image/jpeg,前端用 <img> 加载。
|
||||
"""
|
||||
serial = request.args.get("serial", "")
|
||||
if not serial:
|
||||
return jsonify({"ok": False, "error": "缺少 serial"}), 400
|
||||
ok, data = uiauto_helper.get_screenshot(serial)
|
||||
if ok:
|
||||
return Response(data, mimetype="image/jpeg",
|
||||
headers={"Cache-Control": "no-store"})
|
||||
return jsonify({"ok": False, "error": data}), 503
|
||||
|
||||
|
||||
# ================== API:应用管理(APK 上传/安装)==================
|
||||
@app.route("/api/apks")
|
||||
@login_required
|
||||
@@ -417,6 +538,54 @@ def api_apks_install_status():
|
||||
return jsonify({"ok": True, "status": status})
|
||||
|
||||
|
||||
# ================== uiautodev 自动启动 ==================
|
||||
_uiauto_proc = None
|
||||
|
||||
|
||||
def _ensure_uiauto_running():
|
||||
"""确保 uiautodev 本地服务在运行(元素抓取功能依赖,端口 20242)。
|
||||
|
||||
已在运行则跳过;未运行则用子进程启动 `uiautodev server --no-browser`。
|
||||
启动失败只记日志,不影响主服务。
|
||||
"""
|
||||
global _uiauto_proc
|
||||
if uiauto_helper.is_running():
|
||||
_log.info("uiauto2 服务已在运行 (端口 20242)")
|
||||
return
|
||||
try:
|
||||
_log.info("正在自动启动 uiautodev 服务 (端口 20242)...")
|
||||
kwargs = dict(stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
if sys.platform == "win32":
|
||||
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
_uiauto_proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uiautodev", "server", "--no-browser"],
|
||||
**kwargs,
|
||||
)
|
||||
_log.info(f"uiautodev 服务已启动 (PID={_uiauto_proc.pid})")
|
||||
atexit.register(_stop_uiauto)
|
||||
except FileNotFoundError:
|
||||
_log.warning("uiautodev 未安装,元素抓取功能不可用。请运行: pip install uiautodev")
|
||||
except Exception as e:
|
||||
_log.warning(f"自动启动 uiautodev 失败: {e}(元素抓取功能不可用)")
|
||||
|
||||
|
||||
def _stop_uiauto():
|
||||
"""停止自动启动的 uiautodev 子进程。"""
|
||||
global _uiauto_proc
|
||||
if not _uiauto_proc:
|
||||
return
|
||||
try:
|
||||
_uiauto_proc.terminate()
|
||||
_uiauto_proc.wait(timeout=5)
|
||||
_log.info("uiautodev 服务已停止")
|
||||
except Exception:
|
||||
try:
|
||||
_uiauto_proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
_uiauto_proc = None
|
||||
|
||||
|
||||
def _run_server(host, port):
|
||||
"""启动 Flask,自动处理端口冲突和权限问题。
|
||||
|
||||
@@ -453,8 +622,11 @@ def _run_server(host, port):
|
||||
|
||||
if __name__ == "__main__":
|
||||
from config import WEB_HOST, WEB_PORT
|
||||
# 自动启动 uiautodev 服务(元素抓取功能依赖,端口 20242)
|
||||
_ensure_uiauto_running()
|
||||
_log.info(f"管理后台: http://localhost:{WEB_PORT}/ (admin/admin123)")
|
||||
try:
|
||||
_run_server(WEB_HOST, WEB_PORT)
|
||||
finally:
|
||||
mgr.shutdown()
|
||||
_stop_uiauto()
|
||||
|
||||
Reference in New Issue
Block a user