File size: 3,400 Bytes
d646dd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
"""Exception aggregation — 输入异常(落盘)∪ 运行失败(聚合 runs 生成)。

upload_mismatch/input_missing:上传输入不匹配或部分缺失时写入 data/state/exceptions.json
(可删除);run_failed:从 data/state/runs/ 里 status=="failed" 的记录动态生成
(id="run-<run_id>", message=error_log),不落盘、不可删除。两者按 created_at
倒序合并展示,全部 try/except 容错,坏 JSON/已删 episode 不 500。
"""

from __future__ import annotations

from fastapi import APIRouter, HTTPException, Query

from app.localstore import (
    list_exceptions, delete_exception, clear_exceptions,
    list_runs, scan_sessions, list_projects,
)

router = APIRouter(prefix="/api/v1/exceptions", tags=["exceptions"])


def _aggregate_exceptions(project_id: str | None = None) -> list[dict]:
    """合并两类异常:落盘的 upload_mismatch + 动态生成的 run_failed。"""
    items: list[dict] = []
    try:
        for e in list_exceptions():
            if e.get("kind") in {"upload_mismatch", "input_missing"}:
                items.append(e)
    except Exception as exc:
        print(f"[Exceptions] Failed to read exceptions.json: {exc}")
    try:
        eps_by_id = {e["id"]: e for e in scan_sessions()}
        proj_by_name = {p.get("name"): p for p in list_projects()}
        for run in list_runs():
            if not isinstance(run, dict) or run.get("status") != "failed":
                continue
            ep = eps_by_id.get(run.get("episode_id")) or {}
            pname = ep.get("project")
            proj = proj_by_name.get(pname)
            items.append({
                "id": f"run-{run.get('id')}",
                "kind": "run_failed",
                "project_id": proj.get("id") if proj else None,
                "project_name": pname,
                "episode_id": run.get("episode_id"),
                "workflow_id": run.get("workflow_id"),
                "workflow_name": run.get("workflow_name"),
                "wanted": [],
                "available": [],
                "message": run.get("error_log") or "Run failed",
                "run_id": run.get("id"),
                "created_at": run.get("finished_at") or run.get("created_at"),
            })
    except Exception as exc:
        print(f"[Exceptions] Failed to aggregate run failures: {exc}")
    if project_id:
        items = [x for x in items if x.get("project_id") == project_id]
    items.sort(key=lambda x: x.get("created_at") or "", reverse=True)
    return items


@router.get("")
def list_exceptions_api(
    project_id: str | None = Query(None),
    limit: int = Query(50, ge=1, le=200),
    offset: int = Query(0, ge=0),
):
    items = _aggregate_exceptions(project_id)
    return {"exceptions": items[offset:offset + limit], "total": len(items)}


@router.delete("/{exception_id}")
def delete_exception_api(exception_id: str):
    # run_failed 是聚合生成(无落盘),不能删除
    if exception_id.startswith("run-"):
        raise HTTPException(status_code=404, detail="Exception not found")
    if not delete_exception(exception_id):
        raise HTTPException(status_code=404, detail="Exception not found")
    return {"message": "Exception deleted"}


@router.delete("", status_code=200)
def clear_exceptions_api():
    """清空所有落盘的输入异常(不影响动态生成的 run_failed)。"""
    return {"deleted": clear_exceptions()}