""" API 路由集合 ============ 把业务接口统一收口到 APIRouter,再由 main.py 通过 include_router 挂载。 当前包含的接口: GET /api/algorithms 列出已注册的所有去耀斑算法(供前端下拉框) POST /api/process/deglint 提交去耀斑处理任务,立即返回 task_id GET /api/tasks/{task_id} 查询指定任务的状态与结果 派发链: POST /api/process/deglint └─ BackgroundTasks.add_task(execute_glint_removal_task, ...) └─ get_remover(method) 从注册表拿到算法类 └─ remover.process(input_zarr, output_zarr, **params) """ import traceback import uuid from datetime import datetime from typing import Any, Dict from fastapi import APIRouter, BackgroundTasks, HTTPException from pydantic import BaseModel, Field # 并发安全的任务状态存储(替代旧版的 MOCK_TASK_DB) from app.core.task_store import get_task, set_task, update_task # 算法注册表 API from app.core.algorithms import get_remover, list_removers # --------------------------------------------------------------------------- # 路由实例 # --------------------------------------------------------------------------- # prefix 不在此处设置,统一在 main.py 挂载时给定,便于将来按版本拆分 # (例如 /api/v1、/api/v2 共存时复用同一个 router 对象)。 # --------------------------------------------------------------------------- router = APIRouter(tags=["deglint"]) # --------------------------------------------------------------------------- # 请求 / 响应数据模型 # --------------------------------------------------------------------------- class DeglintRequest(BaseModel): """POST /api/process/deglint 的请求体""" method: str = Field( ..., description="去耀斑方法名称,必须是已注册算法,例如 'kutser' / 'goodman'", examples=["kutser"], ) params: Dict[str, Any] = Field( default_factory=dict, description=( "传递给算法 process() 的超参数字典,例如 " "Kutser: {'band_lower': 773, 'band_oxy': 845, 'band_upper': 893}; " "Goodman: {'band_ref': 750, 'band_diff': 640, 'A': 0.0, 'B': 0.0}" ), examples=[{"band_lower": 773, "band_oxy": 845, "band_upper": 893}], ) class TaskAcceptedResponse(BaseModel): """提交任务成功后立即返回的响应""" task_id: str status: str # 一定是 PENDING class AlgorithmListResponse(BaseModel): """GET /api/algorithms 的响应""" algorithms: list # 已注册算法名列表 count: int # 算法总数 # --------------------------------------------------------------------------- # 后台任务执行器(真实派发链) # --------------------------------------------------------------------------- # 注意:这里使用 async def。 # FastAPI / Starlette 的 BackgroundTasks 支持 async function, # 会在响应返回后自动 await 它,不影响主请求链路。 # --------------------------------------------------------------------------- async def execute_glint_removal_task( task_id: str, method: str, params: Dict[str, Any], ) -> None: """ 后台异步执行器:按 method 名字从注册表取出算法类,实例化并运行 process()。 状态机: PENDING -> PROCESSING -> SUCCESS └──> FAILED(含 error / traceback) """ # 0. 安全检查:任务记录必须已存在(POST 阶段已写入) record = await get_task(task_id) if record is None: print(f"[{task_id}] 任务不存在, 跳过") return # 1. 状态推进到 PROCESSING await update_task( task_id, status="PROCESSING", updated_at=datetime.now().isoformat(), ) print(f"[{task_id}] 开始处理 method={method} params={params}") # 2. 临时硬编码 IO 路径(未来由数据管理层提供) # TODO: 替换为真实的数据管理服务返回的 zarr 路径 input_zarr_path = "./data/temp_in.zarr" output_zarr_path = f"./data/{task_id}_out.zarr" try: # 3. 按 method 名字从注册表取算法类并实例化 # get_remover 找不到时会抛 KeyError,下面的 except 会兜住 algorithm_cls = get_remover(method) remover = algorithm_cls() # 4. 调用算法(注意 await,因为 BaseGlintRemover.process 是 async) await remover.process(input_zarr_path, output_zarr_path, **params) # 5. 成功:写回结果路径与状态 await update_task( task_id, status="SUCCESS", output_zarr_path=output_zarr_path, error=None, updated_at=datetime.now().isoformat(), ) print(f"[{task_id}] 处理完成 -> SUCCESS, output={output_zarr_path}") except Exception as exc: # noqa: BLE001 顶层兜底,绝不让后台任务静默失败 # 6. 失败:记录错误信息与堆栈,便于前端排查 await update_task( task_id, status="FAILED", output_zarr_path=None, error=f"{type(exc).__name__}: {exc}", traceback=traceback.format_exc(), updated_at=datetime.now().isoformat(), ) print(f"[{task_id}] 处理失败 -> {type(exc).__name__}: {exc}") # --------------------------------------------------------------------------- # GET /algorithms # --------------------------------------------------------------------------- # 返回当前已注册的所有算法名,供前端动态渲染下拉框 / 选择器。 # --------------------------------------------------------------------------- @router.get("/algorithms", response_model=AlgorithmListResponse) async def list_registered_algorithms() -> Dict[str, Any]: """列出已注册的去耀斑算法。""" names = list(list_removers().keys()) return {"algorithms": names, "count": len(names)} # --------------------------------------------------------------------------- # POST /process/deglint # --------------------------------------------------------------------------- # 提交去耀斑处理任务。FastAPI 在函数返回后才会把响应发给前端, # 因此通过 BackgroundTasks 把耗时操作丢到后台,接口本身立刻返回 task_id。 # --------------------------------------------------------------------------- @router.post("/process/deglint", response_model=TaskAcceptedResponse) async def submit_deglint( payload: DeglintRequest, background_tasks: BackgroundTasks, ) -> Dict[str, Any]: """提交一个去耀斑处理任务,并立即返回 task_id。""" # 1. 生成唯一任务 ID(UUID4 足以保证全局唯一性) task_id = str(uuid.uuid4()) # 2. 在任务库中登记一条 PENDING 记录(并发安全) # 注意:output_zarr_path / error / traceback 字段在执行过程中被填充 await set_task( task_id, { "task_id": task_id, "method": payload.method, "params": payload.params, "status": "PENDING", "output_zarr_path": None, "error": None, "traceback": None, "created_at": datetime.now().isoformat(), "updated_at": datetime.now().isoformat(), }, ) # 3. 把真实执行器丢到后台 background_tasks.add_task( execute_glint_removal_task, task_id, payload.method, payload.params, ) # 4. 立即返回 task_id 与 PENDING 状态 return {"task_id": task_id, "status": "PENDING"} # --------------------------------------------------------------------------- # GET /tasks/{task_id} # --------------------------------------------------------------------------- # 前端轮询此接口获取任务状态。PENDING / PROCESSING 表示仍在跑, # SUCCESS 表示成功(含 output_zarr_path),FAILED 表示失败(含 error / traceback)。 # --------------------------------------------------------------------------- @router.get("/tasks/{task_id}") async def get_task_status(task_id: str) -> Dict[str, Any]: """查询指定任务的当前状态与结果。""" record = await get_task(task_id) if record is None: # 找不到 task_id 通常意味着客户端拼错了 ID,或者记录已被清理 raise HTTPException(status_code=404, detail=f"task_id 不存在: {task_id}") # 直接返回字典,FastAPI 会自动 JSON 序列化 return record