70 lines
2.4 KiB
Python
70 lines
2.4 KiB
Python
"""龙虾回调端点:接收外部任务状态更新。
|
||
|
||
签名校验的 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})
|