Files
loom/backend/app/api/v1/callbacks.py
T
2026-04-24 17:46:03 +08:00

70 lines
2.4 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.
"""龙虾回调端点:接收外部任务状态更新。
签名校验的 secret 来自 DB(lobster_config)优先,.env 兜底。未配置时跳过校验以便本地自测。
"""
from __future__ import annotations
import hashlib
import hmac
from fastapi import APIRouter, Header, Request
from sqlalchemy import select
from app.core.deps import DbDep
from app.core.exceptions import AppError, ok
from app.models import Task
from app.services import lobster_config_service, task_service
router = APIRouter(prefix="/callbacks", tags=["callback"])
async def _verify_signature(db, body: bytes, signature: str | None) -> None:
eff = await lobster_config_service.load_effective(db)
secret = eff.callback_secret
if not secret:
# 未配置 secret 时跳过校验,便于本地自测
return
if not signature:
raise AppError("签名缺失", code=401, http_status=401)
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
raise AppError("签名无效", code=401, http_status=401)
@router.post("/lobster")
async def lobster_callback(
request: Request,
db: DbDep,
x_loom_signature: str | None = Header(default=None),
) -> dict:
body = await request.body()
await _verify_signature(db, body, x_loom_signature)
payload = await request.json()
run_id = str(payload.get("run_id") or "")
status_ = payload.get("status")
if not run_id:
raise AppError("run_id 缺失", code=400, http_status=400)
t = (await db.execute(select(Task).where(Task.external_ref == run_id))).scalar_one_or_none()
if t is None:
return ok({"handled": False, "reason": "unknown run_id"})
for log in payload.get("logs") or []:
await task_service._append_log(
db, t.id,
str(log.get("level", "INFO")),
str(log.get("message", "")),
str(log.get("event_id")) if log.get("event_id") else None,
)
if status_ == "SUCCEEDED":
t.result_data = payload.get("output")
await task_service._transition(db, t, "SUCCEEDED")
elif status_ == "FAILED":
t.error_message = payload.get("message")
await task_service._transition(db, t, "FAILED")
elif status_ == "TIMEOUT":
t.error_message = payload.get("message") or "timeout"
await task_service._transition(db, t, "TIMEOUT")
return ok({"handled": True})