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:
2026-08-08 10:40:06 +08:00
parent 186bded9d9
commit 468e86f0a9
17 changed files with 3202 additions and 146 deletions
+173 -1
View File
@@ -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()