添加查看调度运行结果

This commit is contained in:
Winnie
2026-08-05 16:34:35 +08:00
parent c263ae6a5f
commit 30cde4a56c
8 changed files with 1140 additions and 36 deletions
+180 -1
View File
@@ -1,9 +1,20 @@
from __future__ import annotations
import asyncio
from datetime import UTC, datetime
from typing import Any, Literal
from urllib.parse import quote
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status
from fastapi import (
APIRouter,
Depends,
Header,
HTTPException,
Query,
Request,
Response,
status,
)
from pydantic import Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -11,6 +22,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from common.db.models import (
ScheduleNodeRuns,
ScheduleRuns,
StorageObjects,
)
from backend.dependencies import (
RequestContext,
@@ -149,6 +161,75 @@ async def _visible_run(
return item
async def _visible_node_run(
run_id: str,
node_run_id: str,
context: RequestContext,
session: AsyncSession,
) -> ScheduleNodeRuns:
await _visible_run(run_id, context, session)
item = await session.scalar(
select(ScheduleNodeRuns).where(
ScheduleNodeRuns.node_run_id == node_run_id,
ScheduleNodeRuns.run_id == run_id,
)
)
if item is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule node run not found")
return item
async def _visible_artifact(
storage_object_id: str | None,
*,
usage_type: Literal["run_log", "run_result"],
context: RequestContext,
session: AsyncSession,
) -> StorageObjects | None:
if storage_object_id is None:
return None
item = await session.scalar(
select(StorageObjects).where(
StorageObjects.storage_object_id == storage_object_id,
StorageObjects.workspace_id == context.workspace.workspace_id,
StorageObjects.usage_type == usage_type,
StorageObjects.object_status == "available",
StorageObjects.is_deleted == 0,
)
)
if (
item is None
or item.storage_backend != "rustfs"
or not item.bucket_name
or not item.object_key
):
return None
return item
def _artifact_payload(item: StorageObjects | None, *, url: str) -> dict[str, Any] | None:
if item is None:
return None
return {
"url": url,
"file_name": item.file_name,
"mime_type": item.mime_type,
"size_bytes": item.size_bytes,
}
async def _artifact_bytes(
item: StorageObjects,
request: Request,
) -> bytes:
return await asyncio.to_thread(
request.app.state.object_store.get_bytes,
bucket_name=item.bucket_name,
object_key=item.object_key,
)
@router.post(
"/api/v1/schedules/{schedule_id}/run",
status_code=status.HTTP_202_ACCEPTED,
@@ -240,3 +321,101 @@ async def get_schedule_run(
"data": await run_detail(item, session),
"meta": {},
}
@router.get(
"/api/v1/schedule-runs/{run_id}/node-runs/{node_run_id}/artifacts"
)
async def get_schedule_node_run_artifacts(
run_id: str,
node_run_id: str,
context: RequestContext = Depends(request_context),
session: AsyncSession = Depends(database_session),
) -> dict[str, Any]:
node_run = await _visible_node_run(run_id, node_run_id, context, session)
log_artifact = await _visible_artifact(
node_run.logs_object_id,
usage_type="run_log",
context=context,
session=session,
)
result_artifact = await _visible_artifact(
node_run.result_object_id,
usage_type="run_result",
context=context,
session=session,
)
base_path = f"/api/v1/schedule-runs/{run_id}/node-runs/{node_run_id}"
workspace_query = f"workspace_id={context.workspace.workspace_id}"
return {
"request_id": context.request_id,
"data": {
"run_id": run_id,
"node_run_id": node_run_id,
"log": _artifact_payload(
log_artifact,
url=f"{base_path}/logs?{workspace_query}",
),
"result": _artifact_payload(
result_artifact,
url=f"{base_path}/result?{workspace_query}",
),
},
"meta": {},
}
@router.get(
"/api/v1/schedule-runs/{run_id}/node-runs/{node_run_id}/logs"
)
async def read_schedule_node_run_logs(
run_id: str,
node_run_id: str,
request: Request,
context: RequestContext = Depends(request_context),
session: AsyncSession = Depends(database_session),
) -> Response:
node_run = await _visible_node_run(run_id, node_run_id, context, session)
item = await _visible_artifact(
node_run.logs_object_id,
usage_type="run_log",
context=context,
session=session,
)
if item is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule node log not found")
return Response(
content=await _artifact_bytes(item, request),
media_type=item.mime_type or "text/plain; charset=utf-8",
headers={"Cache-Control": "no-store"},
)
@router.get(
"/api/v1/schedule-runs/{run_id}/node-runs/{node_run_id}/result"
)
async def download_schedule_node_run_result(
run_id: str,
node_run_id: str,
request: Request,
context: RequestContext = Depends(request_context),
session: AsyncSession = Depends(database_session),
) -> Response:
node_run = await _visible_node_run(run_id, node_run_id, context, session)
item = await _visible_artifact(
node_run.result_object_id,
usage_type="run_result",
context=context,
session=session,
)
if item is None:
raise HTTPException(status.HTTP_404_NOT_FOUND, "schedule node result not found")
encoded_name = quote(item.file_name, safe="")
return Response(
content=await _artifact_bytes(item, request),
media_type=item.mime_type or "application/octet-stream",
headers={
"Cache-Control": "no-store",
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_name}",
},
)