This commit is contained in:
2026-03-27 03:58:57 +08:00
commit 86eba27a24
38 changed files with 4074 additions and 0 deletions

2
app/api/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# api package

41
app/api/routes_config.py Normal file
View File

@@ -0,0 +1,41 @@
"""
app/api/routes_config.py
HTTP endpoints for reading and updating runtime configuration.
"""
from __future__ import annotations
import logging
from fastapi import APIRouter
from app.models import ConfigResponse, UpdateConfigRequest
from app.services import config_service, event_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/config", tags=["config"])
@router.get("", response_model=ConfigResponse)
def get_config():
return config_service.get_config()
@router.patch("", response_model=ConfigResponse)
def update_config(body: UpdateConfigRequest):
cfg = config_service.update_config(
default_wait_seconds=body.default_wait_seconds,
default_empty_response=body.default_empty_response,
agent_stale_after_seconds=body.agent_stale_after_seconds,
)
event_service.broadcast(
"config.updated",
{
"default_wait_seconds": cfg.default_wait_seconds,
"default_empty_response": cfg.default_empty_response,
"agent_stale_after_seconds": cfg.agent_stale_after_seconds,
},
)
return cfg

View File

@@ -0,0 +1,72 @@
"""
app/api/routes_instructions.py
HTTP endpoints for instruction CRUD.
"""
from __future__ import annotations
import logging
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import Response
from app.models import (
CreateInstructionRequest,
InstructionCreateResponse,
InstructionListResponse,
UpdateInstructionRequest,
)
from app.services import event_service, instruction_service
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/instructions", tags=["instructions"])
@router.get("", response_model=InstructionListResponse)
def list_instructions(
status: str = Query(default="all", pattern="^(pending|consumed|all)$")
):
items = instruction_service.list_instructions(status_filter=status)
return InstructionListResponse(items=items)
@router.post("", response_model=InstructionCreateResponse, status_code=201)
def create_instruction(body: CreateInstructionRequest):
item = instruction_service.create_instruction(body.content)
event_service.broadcast(
"instruction.created",
{"item": item.model_dump(mode="json")},
)
return InstructionCreateResponse(item=item)
@router.patch("/{instruction_id}", response_model=InstructionCreateResponse)
def update_instruction(instruction_id: str, body: UpdateInstructionRequest):
try:
item = instruction_service.update_instruction(instruction_id, body.content)
except KeyError:
raise HTTPException(status_code=404, detail="Instruction not found")
except PermissionError as exc:
raise HTTPException(status_code=409, detail=str(exc))
event_service.broadcast(
"instruction.updated",
{"item": item.model_dump(mode="json")},
)
return InstructionCreateResponse(item=item)
@router.delete("/{instruction_id}", status_code=204)
def delete_instruction(instruction_id: str):
try:
instruction_service.delete_instruction(instruction_id)
except KeyError:
raise HTTPException(status_code=404, detail="Instruction not found")
except PermissionError as exc:
raise HTTPException(status_code=409, detail=str(exc))
event_service.broadcast("instruction.deleted", {"id": instruction_id})
return Response(status_code=204)

77
app/api/routes_status.py Normal file
View File

@@ -0,0 +1,77 @@
"""
app/api/routes_status.py
HTTP endpoints for server/agent status and SSE events.
"""
from __future__ import annotations
import logging
from datetime import datetime, timezone
from fastapi import APIRouter
from fastapi.responses import StreamingResponse
from app.models import (
AgentInfo,
HealthResponse,
QueueCounts,
ServerInfo,
StatusResponse,
)
from app.services import config_service, event_service, instruction_service, status_service
logger = logging.getLogger(__name__)
router = APIRouter(tags=["status"])
@router.get("/healthz", response_model=HealthResponse)
def health():
return HealthResponse(status="ok", server_time=datetime.now(timezone.utc))
@router.get("/api/status", response_model=StatusResponse)
def get_status():
agent_row = status_service.get_latest_agent_activity()
connected = status_service.is_agent_connected()
agent_info = AgentInfo(
connected=connected,
last_seen_at=datetime.fromisoformat(agent_row["last_seen_at"]) if agent_row else None,
last_fetch_at=datetime.fromisoformat(agent_row["last_fetch_at"]) if agent_row else None,
agent_id=agent_row["agent_id"] if agent_row else None,
)
counts = instruction_service.get_queue_counts()
cfg = config_service.get_config()
return StatusResponse(
server=ServerInfo(
status="up",
started_at=status_service.server_started_at(),
),
agent=agent_info,
queue=QueueCounts(**counts),
settings=cfg,
)
@router.get("/api/events")
async def sse_events():
q = event_service.subscribe()
async def stream():
async for chunk in event_service.event_generator(q):
yield chunk
return StreamingResponse(
stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive",
},
)