"""百工与司命 API(契约词典 §5.9 pivot=capability_registry)。 - 能力目录与调用:GET/POST /capabilities、POST /capabilities/{id}/invoke; - 授契与行契:GET/POST /mandates、DELETE /mandates/{id}(撤销立即生效); - 请命:GET /petitions、POST /petitions/{id}/approve|deny(超时=拒绝执行 HJ-1302); - 行动账:GET /tool-calls(净化摘要+观察标证)。 """ from datetime import datetime from fastapi import APIRouter, Depends, Request, status from pydantic import BaseModel, Field from app.api.v1.deps import get_current_user from app.domains.baigong import service from app.schemas.auth import CurrentUser cap_router = APIRouter(prefix="/capabilities", tags=["baigong"]) mandate_router = APIRouter(prefix="/mandates", tags=["baigong"]) petition_router = APIRouter(prefix="/petitions", tags=["baigong"]) ledger_router = APIRouter(prefix="/tool-calls", tags=["baigong"]) class InvokeRequest(BaseModel): params: dict = Field(default_factory=dict, description="能力参数。") run_id: str | None = None character_id: str | None = None trace_id: str | None = None confirmed: bool = Field(default=False, description="请命批准后的当次执行回执。") class CreateCapabilityRequest(BaseModel): name: str = Field(min_length=1, max_length=120) description: str = "" executor_type: str = Field(description="builtin / qiankun / mcp / http") executor_ref: str = "" params_schema: dict = Field(default_factory=dict) risk_dims: dict = Field(default_factory=dict) data_scope: str = "" destinations: str = "" consent_mode: str = "allow" source: str = "user_custom" class GrantMandateRequest(BaseModel): capability_id: str kind: str = Field(default="grant", description="grant 授契 / charter 行契") character_id: str | None = None scope: dict = Field(default_factory=dict) budget_total: int = Field(default=1, ge=0) expires_at: datetime | None = None # ── 能力目录 ── @cap_router.get("") async def list_capabilities(user: CurrentUser = Depends(get_current_user)) -> list[dict]: return await service.list_capabilities(user.owner_scope) @cap_router.post("", status_code=status.HTTP_201_CREATED) async def create_capability( payload: CreateCapabilityRequest, user: CurrentUser = Depends(get_current_user) ) -> dict: import json from app.db.session import AsyncSessionLocal from app.domains.baigong.models import Capability if payload.executor_type not in ("builtin", "qiankun", "mcp", "http"): from app.core.errors import AppError raise AppError("invalid_request", "executor_type 须为 builtin/qiankun/mcp/http") async with AsyncSessionLocal() as session: cap = Capability( name=payload.name, owner_scope=user.owner_scope, description=payload.description, executor_type=payload.executor_type, executor_ref=payload.executor_ref, params_schema_json=json.dumps(payload.params_schema, ensure_ascii=False), risk_dims_json=json.dumps(payload.risk_dims, ensure_ascii=False), data_scope=payload.data_scope, destinations=payload.destinations, consent_mode=payload.consent_mode, source=payload.source, ) session.add(cap) try: await session.commit() except Exception as exc: # noqa: BLE001 from app.core.errors import AppError await session.rollback() raise AppError("invalid_request", f"能力登记失败(名称重复?):{exc}") from None return {"id": cap.id, "name": cap.name, "status": "registered"} @cap_router.post("/{capability_id}/invoke") async def invoke_capability( capability_id: str, payload: InvokeRequest, request: Request, user: CurrentUser = Depends(get_current_user), ) -> dict: return await service.invoke( request, capability_id, payload.params, user=user, run_id=payload.run_id, character_id=payload.character_id, trace_id=payload.trace_id, confirmed=payload.confirmed, ) # ── 授契与行契 ── @mandate_router.get("") async def list_mandates(user: CurrentUser = Depends(get_current_user)) -> list[dict]: return await service.list_mandates(user.owner_scope) @mandate_router.post("", status_code=status.HTTP_201_CREATED) async def grant_mandate( payload: GrantMandateRequest, user: CurrentUser = Depends(get_current_user) ) -> dict: return await service.grant_mandate( user.owner_scope, capability_id=payload.capability_id, kind=payload.kind, character_id=payload.character_id, scope=payload.scope, budget_total=payload.budget_total, expires_at=payload.expires_at, ) @mandate_router.delete("/{mandate_id}", status_code=status.HTTP_204_NO_CONTENT) async def revoke_mandate( mandate_id: str, user: CurrentUser = Depends(get_current_user) ) -> None: await service.revoke_mandate(user.owner_scope, mandate_id) # ── 请命 ── @petition_router.get("") async def list_petitions(user: CurrentUser = Depends(get_current_user)) -> list[dict]: return await service.list_petitions() @petition_router.post("/{petition_id}/approve") async def approve_petition( petition_id: str, user: CurrentUser = Depends(get_current_user) ) -> dict: return await service.resolve_petition(petition_id, approved=True) @petition_router.post("/{petition_id}/deny") async def deny_petition(petition_id: str, user: CurrentUser = Depends(get_current_user)) -> dict: return await service.resolve_petition(petition_id, approved=False) # ── 行动账 ── @ledger_router.get("") async def list_tool_calls( user: CurrentUser = Depends(get_current_user), limit: int = 100 ) -> list[dict]: return await service.list_tool_calls(user.owner_scope, limit=limit) routers = [cap_router, mandate_router, petition_router, ledger_router]