feat: 完成工作流调度与上传下载 API
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""WOV API routers."""
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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}
|
||||
@@ -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
|
||||
]
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user