autotrain/nextgen/autotrain_next/runner.py
2026-07-24 10:37:32 +08:00

125 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""任务循环与持久化。运行记录是可审计 JSON不依赖 UI 内存。"""
from __future__ import annotations
import json
import os
import threading
import uuid
from datetime import datetime, timezone
from pathlib import Path
from .paths import TASKS, ensure_workdirs
from .planner import deterministic_plan, llm_plan
from .tools import ToolError, execute
from . import device, profiles
_LOCK_GUARD = threading.Lock()
_TASK_LOCKS: dict[str, threading.Lock] = {}
_DEVICE_LOCKS: dict[str, threading.Lock] = {}
def _lock(pool: dict[str, threading.Lock], key: str) -> threading.Lock:
with _LOCK_GUARD:
return pool.setdefault(key, threading.Lock())
def now() -> str:
return datetime.now(timezone.utc).isoformat()
def create(device_serial: str = "", *, allow_submit: bool = False, roster_path: str = "", success_action: str = "download", qr_image: str = "") -> dict:
ensure_workdirs()
if success_action not in {"download", "email"}:
raise ValueError("success_action 只能是 download 或 email")
task = {"id": uuid.uuid4().hex, "type": "invoice_h5", "status": "created", "device_serial": device_serial, "roster_path": roster_path, "qr_image": qr_image, "navigation": {"phase": "push_qr" if qr_image else "done"}, "allow_submit": allow_submit, "invoice": {"progress": [], "success_action": success_action}, "history": [], "created_at": now(), "updated_at": now()}
save(task)
return task
def task_path(task_id: str) -> Path:
return TASKS / f"{task_id}.json"
def load(task_id: str) -> dict:
return json.loads(task_path(task_id).read_text(encoding="utf-8"))
def save(task: dict) -> None:
ensure_workdirs()
task["updated_at"] = now()
destination = task_path(task["id"])
temporary = destination.with_suffix(".tmp")
temporary.write_text(json.dumps(task, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
os.replace(temporary, destination)
def _run(task: dict, settings: dict) -> dict:
if not bool(settings.get("safety", {}).get("allow_submit", False)):
task["allow_submit"] = False
try:
current = device.fingerprint(device.selected_device(task.get("device_serial", "")).serial)
task["device_serial"] = current["serial"]
profile_check = profiles.compatibility(profiles.load(current["serial"]), current)
except Exception as exc:
task["status"] = "blocked"
task["block_reason"] = str(exc)
save(task)
return task
if profile_check["status"] != "ready":
task["status"] = "needs_sampling"
task["block_reason"] = "".join(profile_check["reasons"]) or "设备档案尚未验证"
save(task)
return task
try:
limit = int(settings.get("safety", {}).get("max_loop_steps", 30))
except (TypeError, ValueError):
limit = 30
task["status"] = "running"
for _ in range(max(1, min(limit, 100))):
try:
plan = deterministic_plan(task)
if not plan:
plan = llm_plan(task, settings)
except Exception as exc:
task["status"] = "failed"
task["block_reason"] = f"规划失败:{exc}"
save(task)
return task
if not plan:
task["status"] = "completed" if task.get("invoice", {}).get("finished") else "waiting_for_review"
break
for step in plan:
try:
result = execute(step.tool, step.arguments, task)
task["history"].append({"at": now(), "tool": step.tool, "arguments": step.arguments, "reason": step.reason, "result": result})
except Exception as exc:
task["history"].append({"at": now(), "tool": step.tool, "arguments": step.arguments, "reason": step.reason, "error": str(exc)})
task["status"] = "failed"
save(task)
return task
save(task)
else:
task["status"] = "step_limit_reached"
save(task)
return task
def run(task: dict, settings: dict) -> dict:
task_lock = _lock(_TASK_LOCKS, task["id"])
if not task_lock.acquire(blocking=False):
task["status"] = "already_running"; task["block_reason"] = "该任务正在运行"; save(task); return task
try:
try:
serial = device.selected_device(task.get("device_serial", "")).serial
except Exception:
return _run(task, settings)
device_lock = _lock(_DEVICE_LOCKS, serial)
if not device_lock.acquire(blocking=False):
task["status"] = "device_busy"; task["block_reason"] = f"设备 {serial} 正在执行另一个任务"; save(task); return task
try:
return _run(task, settings)
finally:
device_lock.release()
finally:
task_lock.release()