chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+230
View File
@@ -0,0 +1,230 @@
from __future__ import annotations
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field, field_validator, model_validator
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from ..config import read_int_env
from ..database import get_db
from ..models import AuthUserORM, WorkflowRunORM, WorkflowStepORM
from ..services.workflow_service import workflow_service
from .dependencies import _require_admin
router = APIRouter()
WORKFLOW_MAX_STEPS = read_int_env(
"WORKFLOW_MAX_STEPS",
200,
minimum=1,
maximum=5000,
)
WORKFLOW_MAX_DEPENDS = read_int_env(
"WORKFLOW_MAX_DEPENDS",
32,
minimum=0,
maximum=500,
)
WORKFLOW_TEXT_MAX_LENGTH = read_int_env(
"WORKFLOW_TEXT_MAX_LENGTH",
128,
minimum=16,
maximum=1024,
)
class WorkflowStepCreate(BaseModel):
step_id: str = Field(max_length=WORKFLOW_TEXT_MAX_LENGTH)
step_name: Optional[str] = Field(default=None, max_length=WORKFLOW_TEXT_MAX_LENGTH)
job_type: str = Field(max_length=WORKFLOW_TEXT_MAX_LENGTH)
payload: Optional[dict] = None
depends_on: List[str] = Field(default_factory=list)
optional: bool = False
task_id: Optional[str] = Field(default=None, max_length=WORKFLOW_TEXT_MAX_LENGTH)
@field_validator("step_id", "job_type", mode="before")
@classmethod
def _normalize_required_text(cls, value):
normalized = str(value or "").strip()
if not normalized:
raise ValueError("Field must not be empty.")
return normalized
@field_validator("step_name", "task_id", mode="before")
@classmethod
def _normalize_optional_text(cls, value):
if value is None:
return None
normalized = str(value).strip()
return normalized or None
@field_validator("depends_on", mode="before")
@classmethod
def _validate_depends_on(cls, value):
if value is None:
return []
if not isinstance(value, list):
raise ValueError("depends_on must be a list.")
if len(value) > WORKFLOW_MAX_DEPENDS:
raise ValueError(f"depends_on exceeds max count ({WORKFLOW_MAX_DEPENDS}).")
normalized: List[str] = []
for raw in value:
dep = str(raw or "").strip()
if not dep:
continue
if len(dep) > WORKFLOW_TEXT_MAX_LENGTH:
raise ValueError(
f"depends_on item is too long (max {WORKFLOW_TEXT_MAX_LENGTH} chars)."
)
if dep not in normalized:
normalized.append(dep)
return normalized
class WorkflowRunCreate(BaseModel):
workflow_name: str = Field(max_length=WORKFLOW_TEXT_MAX_LENGTH)
steps: List[WorkflowStepCreate]
params: Optional[dict] = None
tags: Optional[dict] = None
created_by: Optional[str] = Field(default=None, max_length=WORKFLOW_TEXT_MAX_LENGTH)
@field_validator("workflow_name", mode="before")
@classmethod
def _normalize_workflow_name(cls, value):
normalized = str(value or "").strip()
if not normalized:
raise ValueError("workflow_name must not be empty.")
return normalized
@field_validator("created_by", mode="before")
@classmethod
def _normalize_created_by(cls, value):
if value is None:
return None
normalized = str(value).strip()
return normalized or None
@model_validator(mode="after")
def _validate_steps(self):
if not self.steps:
raise ValueError("steps must not be empty.")
if len(self.steps) > WORKFLOW_MAX_STEPS:
raise ValueError(f"steps exceeds max count ({WORKFLOW_MAX_STEPS}).")
step_ids = [step.step_id for step in self.steps]
if len(step_ids) != len(set(step_ids)):
raise ValueError("step_id values must be unique within one workflow run.")
step_id_set = set(step_ids)
graph = {}
for step in self.steps:
graph[step.step_id] = list(step.depends_on or [])
for dep in step.depends_on:
if dep not in step_id_set:
raise ValueError(
f"Step '{step.step_id}' depends on unknown step_id '{dep}'."
)
if dep == step.step_id:
raise ValueError(
f"Step '{step.step_id}' cannot depend on itself."
)
# Detect dependency cycles early to avoid runs stuck in PENDING forever.
visited = set()
in_stack = set()
def _dfs(node: str) -> None:
if node in in_stack:
raise ValueError(f"Workflow dependency cycle detected at step '{node}'.")
if node in visited:
return
in_stack.add(node)
for dep in graph.get(node, []):
_dfs(dep)
in_stack.remove(node)
visited.add(node)
for step_id in step_ids:
_dfs(step_id)
return self
@router.post("/workflow/runs")
async def create_workflow_run(request: WorkflowRunCreate, admin_user: AuthUserORM = Depends(_require_admin)):
"""
创建一个工作流实例并自动入队可执行步骤。
"""
steps_payload = [
{
"step_id": s.step_id,
"step_name": s.step_name or s.step_id,
"job_type": s.job_type,
"payload": s.payload or {},
"depends_on": s.depends_on or [],
"optional": s.optional,
"task_id": s.task_id,
}
for s in request.steps
]
try:
run_id = await workflow_service.create_run(
workflow_name=request.workflow_name,
steps=steps_payload,
params=request.params,
tags=request.tags,
created_by=request.created_by,
)
return {"run_id": run_id}
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.get("/workflow/runs/{run_id}")
async def get_workflow_run(run_id: str, db: AsyncSession = Depends(get_db)):
"""
获取工作流运行状态与步骤列表。
"""
run_res = await db.execute(
select(WorkflowRunORM).where(WorkflowRunORM.run_id == run_id)
)
run = run_res.scalar_one_or_none()
if not run:
raise HTTPException(status_code=404, detail="Workflow run not found.")
steps_res = await db.execute(
select(WorkflowStepORM).where(WorkflowStepORM.run_id == run_id).order_by(WorkflowStepORM.id)
)
steps = steps_res.scalars().all()
return {
"run": {
"run_id": run.run_id,
"workflow_name": run.workflow_name,
"status": run.status,
"params": run.params,
"tags": run.tags,
"created_by": run.created_by,
"created_at": run.created_at,
"updated_at": run.updated_at,
"started_at": run.started_at,
"ended_at": run.ended_at,
},
"steps": [
{
"step_id": s.step_id,
"step_name": s.step_name,
"status": s.status,
"depends_on": s.depends_on,
"params": s.params,
"outputs": s.outputs,
"error": s.error,
"created_at": s.created_at,
"updated_at": s.updated_at,
"started_at": s.started_at,
"ended_at": s.ended_at,
}
for s in steps
],
}