feat: 完成工作流调度与上传下载 API

This commit is contained in:
cat-shark
2026-08-08 21:04:21 +08:00
commit a8d11eafc6
32 changed files with 3675 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""WOV API routers."""
+126
View File
@@ -0,0 +1,126 @@
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import FileResponse
from app.db import Database
router = APIRouter(tags=["apps"])
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _get_db() -> Database:
from app.main import app
return app.state.db
@router.get("/api/apps")
def list_apps(db: Database = Depends(_get_db)) -> list[dict]:
apps = []
for workflow in db.list_workflows():
if not workflow["published"]:
continue
latest = db.get_latest_workflow_version(workflow["id"])
apps.append(
{
"id": workflow["id"],
"name": workflow["name"],
"description": workflow["description"],
"version": workflow["latest_version"],
"definition": latest["definition"] if latest else None,
}
)
return apps
@router.post("/api/apps/{workflow_id}/runs")
async def create_run(
workflow_id: str,
file: UploadFile = File(...),
db: Database = Depends(_get_db),
) -> dict:
workflow = db.get_workflow(workflow_id)
if workflow is None or not workflow["published"]:
raise HTTPException(status_code=404, detail="published workflow not found")
latest = db.get_latest_workflow_version(workflow_id)
if latest is None:
raise HTTPException(status_code=422, detail="workflow has no version")
run_id = f"run_{uuid.uuid4().hex[:12]}"
filename = Path(file.filename or "upload.bin").name
from app.config import STORAGE_DIR
input_dir = STORAGE_DIR / "uploads" / run_id
input_dir.mkdir(parents=True, exist_ok=True)
input_uri = input_dir / filename
content = await file.read()
input_uri.write_bytes(content)
now = _now_iso()
db.create_run(
{
"id": run_id,
"workflow_id": workflow_id,
"workflow_version": latest["version"],
"status": "QUEUED",
"progress": 0,
"input_uri": str(input_uri),
"created_at": now,
"updated_at": now,
}
)
return {
"id": run_id,
"status": "QUEUED",
"progress": 0,
"artifacts": [],
}
@router.get("/api/runs")
def list_runs(db: Database = Depends(_get_db)) -> list[dict]:
return db.list_runs()
@router.get("/api/runs/{run_id}")
def get_run(run_id: str, db: Database = Depends(_get_db)) -> dict:
run = db.get_run(run_id)
if run is None:
raise HTTPException(status_code=404, detail="run not found")
run["artifacts"] = db.list_artifacts(run_id)
return run
@router.get("/api/runs/{run_id}/artifacts")
def list_run_artifacts(run_id: str, db: Database = Depends(_get_db)) -> list[dict]:
if db.get_run(run_id) is None:
raise HTTPException(status_code=404, detail="run not found")
return db.list_artifacts(run_id)
@router.get("/api/runs/{run_id}/artifacts/{artifact_name}")
def download_artifact(
run_id: str,
artifact_name: str,
db: Database = Depends(_get_db),
) -> FileResponse:
artifact = db.get_artifact(run_id, artifact_name)
if artifact is None:
raise HTTPException(status_code=404, detail="artifact not found")
path = Path(artifact["uri"])
if not path.is_file():
raise HTTPException(status_code=404, detail="artifact file missing")
return FileResponse(
path,
media_type=artifact["mime_type"],
filename=path.name,
)
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from app.db import Database
from app.node_manager import NodeManager
router = APIRouter(prefix="/api/admin/node-instances", tags=["node-instances"])
def _get_db() -> Database:
from app.main import app
return app.state.db
def _get_manager() -> NodeManager:
from app.main import app
return app.state.node_manager
@router.get("")
def list_instances(db: Database = Depends(_get_db)) -> list[dict]:
return db.list_instances()
@router.post("/{instance_id}/stop")
def stop_instance(
instance_id: str,
db: Database = Depends(_get_db),
manager: NodeManager = Depends(_get_manager),
) -> dict:
manager.stop_instance(instance_id)
instance = next(
(item for item in db.list_instances() if item["id"] == instance_id),
None,
)
if instance is None:
raise HTTPException(status_code=404, detail="instance not found")
return {"stopped": instance_id}
+95
View File
@@ -0,0 +1,95 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from app.db import Database
from app.node_manager import NodeManager
from app.schemas import InvokePayload, NodeCreate
from wov_sdk.models import InvokeRequest
router = APIRouter(prefix="/api/admin/nodes", tags=["nodes"])
def _get_db() -> Database:
from app.main import app
return app.state.db
def _get_manager() -> NodeManager:
from app.main import app
return app.state.node_manager
@router.post("")
def register_node(payload: NodeCreate, db: Database = Depends(_get_db)) -> dict:
manifest = payload.to_manifest()
try:
manifest.validate()
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
db.upsert_node(manifest)
return manifest.to_dict()
@router.get("")
def list_nodes(db: Database = Depends(_get_db)) -> list[dict]:
return [manifest.to_dict() for manifest in db.list_nodes()]
@router.get("/{node_id}")
def get_node(node_id: str, db: Database = Depends(_get_db)) -> dict:
manifest = db.get_node(node_id)
if manifest is None:
raise HTTPException(status_code=404, detail="node not found")
return manifest.to_dict()
@router.delete("/{node_id}")
def delete_node(
node_id: str,
db: Database = Depends(_get_db),
manager: NodeManager = Depends(_get_manager),
) -> dict:
if db.get_node(node_id) is None:
raise HTTPException(status_code=404, detail="node not found")
manager.stop_all_for_node(node_id)
db.delete_node(node_id)
return {"deleted": node_id}
@router.post("/{node_id}/invoke")
def invoke_node(
node_id: str,
payload: InvokePayload,
manager: NodeManager = Depends(_get_manager),
) -> dict:
from app.config import STORAGE_DIR
output_dir = (
STORAGE_DIR / "runs" / payload.run_id / "steps" / node_id
)
request = InvokeRequest(
run_id=payload.run_id,
node_instance_id="",
inputs=payload.inputs,
params=payload.params,
output_dir=str(output_dir),
)
response = manager.invoke(node_id, request)
return response.to_dict()
@router.get("/{node_id}/instances")
def list_node_instances(
node_id: str,
db: Database = Depends(_get_db),
) -> list[dict]:
if db.get_node(node_id) is None:
raise HTTPException(status_code=404, detail="node not found")
return [
instance
for instance in db.list_instances()
if instance["node_id"] == node_id
]
+121
View File
@@ -0,0 +1,121 @@
from __future__ import annotations
import re
import uuid
from fastapi import APIRouter, Depends, HTTPException
from app.db import Database
from app.schemas import WorkflowCreate
from wov_sdk.models import WorkflowDefinition
router = APIRouter(prefix="/api/admin/workflows", tags=["workflows"])
def _get_db() -> Database:
from app.main import app
return app.state.db
def _slugify(value: str) -> str:
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
return slug or uuid.uuid4().hex[:8]
def _validate_definition(raw: dict) -> WorkflowDefinition:
try:
definition = WorkflowDefinition.from_dict(raw)
definition.validate()
return definition
except (KeyError, TypeError, ValueError) as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@router.get("")
def list_workflows(db: Database = Depends(_get_db)) -> list[dict]:
return db.list_workflows()
@router.post("")
def create_workflow(
payload: WorkflowCreate,
db: Database = Depends(_get_db),
) -> dict:
definition = _validate_definition(payload.definition)
workflow_id = payload.id or _slugify(payload.name)
existing = db.get_workflow(workflow_id)
version = (existing or {}).get("latest_version", 0) + 1
db.upsert_workflow(
{
"id": workflow_id,
"name": payload.name,
"description": payload.description,
"published": 0,
"latest_version": version,
}
)
db.create_workflow_version(workflow_id, version, definition.to_dict())
return {
"id": workflow_id,
"name": payload.name,
"description": payload.description,
"published": False,
"latest_version": version,
}
@router.get("/{workflow_id}")
def get_workflow(workflow_id: str, db: Database = Depends(_get_db)) -> dict:
workflow = db.get_workflow(workflow_id)
if workflow is None:
raise HTTPException(status_code=404, detail="workflow not found")
latest = db.get_latest_workflow_version(workflow_id)
workflow["latest_version_data"] = latest
return workflow
@router.delete("/{workflow_id}")
def delete_workflow(workflow_id: str, db: Database = Depends(_get_db)) -> dict:
if db.get_workflow(workflow_id) is None:
raise HTTPException(status_code=404, detail="workflow not found")
db.delete_workflow(workflow_id)
return {"deleted": workflow_id}
@router.post("/{workflow_id}/validate")
def validate_workflow(
workflow_id: str,
definition: dict,
db: Database = Depends(_get_db),
) -> dict:
if db.get_workflow(workflow_id) is None:
raise HTTPException(status_code=404, detail="workflow not found")
parsed = _validate_definition(definition)
return {"valid": True, "node_ids": [node.id for node in parsed.nodes]}
@router.post("/{workflow_id}/publish")
def publish_workflow(workflow_id: str, db: Database = Depends(_get_db)) -> dict:
workflow = db.get_workflow(workflow_id)
if workflow is None:
raise HTTPException(status_code=404, detail="workflow not found")
if workflow["latest_version"] == 0:
raise HTTPException(status_code=422, detail="workflow has no version")
db.upsert_workflow(
{
"id": workflow_id,
"name": workflow["name"],
"description": workflow["description"],
"published": 1,
"latest_version": workflow["latest_version"],
}
)
return {"published": workflow_id}
@router.get("/{workflow_id}/versions")
def list_versions(workflow_id: str, db: Database = Depends(_get_db)) -> list[dict]:
if db.get_workflow(workflow_id) is None:
raise HTTPException(status_code=404, detail="workflow not found")
return db.list_workflow_versions(workflow_id)