2026-05-11 03:51:24 +00:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Annotated
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Header, HTTPException, Query, status
|
|
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
|
|
|
|
|
|
|
|
from app.api.deps import get_db
|
|
|
|
|
|
from app.schemas.agent_asset import (
|
|
|
|
|
|
AgentAssetCreate,
|
|
|
|
|
|
AgentAssetListItem,
|
|
|
|
|
|
AgentAssetRead,
|
|
|
|
|
|
AgentAssetReviewCreate,
|
|
|
|
|
|
AgentAssetReviewRead,
|
|
|
|
|
|
AgentAssetUpdate,
|
|
|
|
|
|
AgentAssetVersionCreate,
|
|
|
|
|
|
AgentAssetVersionRead,
|
|
|
|
|
|
)
|
2026-05-11 05:18:16 +00:00
|
|
|
|
from app.schemas.common import ErrorResponse
|
2026-05-11 03:51:24 +00:00
|
|
|
|
from app.services.agent_assets import AgentAssetService
|
|
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/agent-assets")
|
|
|
|
|
|
DbSession = Annotated[Session, Depends(get_db)]
|
2026-05-11 05:18:16 +00:00
|
|
|
|
ActorHeader = Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Header(description="审计操作者。未传时回退到请求体中的 owner / reviewer 或 `system`。"),
|
|
|
|
|
|
]
|
|
|
|
|
|
RequestIdHeader = Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Header(description="外部请求 ID,用于串联审计日志和上游调用链。"),
|
|
|
|
|
|
]
|
2026-05-11 03:51:24 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _handle_asset_error(exc: Exception) -> None:
|
|
|
|
|
|
if isinstance(exc, LookupError):
|
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
|
|
|
|
|
if isinstance(exc, PermissionError):
|
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
|
|
|
|
if isinstance(exc, ValueError):
|
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
|
|
|
|
|
raise exc
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 05:18:16 +00:00
|
|
|
|
@router.get(
|
|
|
|
|
|
"",
|
|
|
|
|
|
response_model=list[AgentAssetListItem],
|
|
|
|
|
|
summary="查询 Agent 资产列表",
|
|
|
|
|
|
description="按资产类型、状态、领域和关键字筛选规则、技能、MCP 与任务资产。",
|
|
|
|
|
|
)
|
2026-05-11 03:51:24 +00:00
|
|
|
|
def list_agent_assets(
|
|
|
|
|
|
db: DbSession,
|
2026-05-11 05:18:16 +00:00
|
|
|
|
asset_type: Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Query(description="资产类型:`rule`、`skill`、`mcp`、`task`。"),
|
|
|
|
|
|
] = None,
|
|
|
|
|
|
status_value: Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Query(alias="status", description="资产状态筛选。"),
|
|
|
|
|
|
] = None,
|
|
|
|
|
|
domain: Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Query(description="业务领域筛选,例如 `expense`、`ar`、`ap`。"),
|
|
|
|
|
|
] = None,
|
|
|
|
|
|
keyword: Annotated[
|
|
|
|
|
|
str | None,
|
|
|
|
|
|
Query(description="资产编码、名称关键字模糊查询。"),
|
|
|
|
|
|
] = None,
|
2026-05-11 03:51:24 +00:00
|
|
|
|
) -> list[AgentAssetListItem]:
|
|
|
|
|
|
return AgentAssetService(db).list_assets(
|
|
|
|
|
|
asset_type=asset_type,
|
|
|
|
|
|
status=status_value,
|
|
|
|
|
|
domain=domain,
|
|
|
|
|
|
keyword=keyword,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 05:18:16 +00:00
|
|
|
|
@router.get(
|
|
|
|
|
|
"/{asset_id}",
|
|
|
|
|
|
response_model=AgentAssetRead,
|
|
|
|
|
|
summary="读取 Agent 资产详情",
|
|
|
|
|
|
description="返回资产当前版本正文、最近版本列表和最近一次审核信息。",
|
|
|
|
|
|
responses={
|
|
|
|
|
|
status.HTTP_404_NOT_FOUND: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "资产不存在。",
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-11 03:51:24 +00:00
|
|
|
|
def get_agent_asset(asset_id: str, db: DbSession) -> AgentAssetRead:
|
|
|
|
|
|
asset = AgentAssetService(db).get_asset(asset_id)
|
|
|
|
|
|
if asset is None:
|
|
|
|
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Asset not found")
|
|
|
|
|
|
return asset
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 05:18:16 +00:00
|
|
|
|
@router.post(
|
|
|
|
|
|
"",
|
|
|
|
|
|
response_model=AgentAssetRead,
|
|
|
|
|
|
status_code=status.HTTP_201_CREATED,
|
|
|
|
|
|
summary="创建 Agent 资产",
|
|
|
|
|
|
description="创建新的规则、技能、MCP 或任务资产,并自动记录审计日志。",
|
|
|
|
|
|
responses={
|
|
|
|
|
|
status.HTTP_400_BAD_REQUEST: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "资产编码冲突或请求字段不合法。",
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-11 03:51:24 +00:00
|
|
|
|
def create_agent_asset(
|
|
|
|
|
|
payload: AgentAssetCreate,
|
|
|
|
|
|
db: DbSession,
|
2026-05-11 05:18:16 +00:00
|
|
|
|
x_actor: ActorHeader = None,
|
|
|
|
|
|
x_request_id: RequestIdHeader = None,
|
2026-05-11 03:51:24 +00:00
|
|
|
|
) -> AgentAssetRead:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return AgentAssetService(db).create_asset(
|
|
|
|
|
|
payload,
|
|
|
|
|
|
actor=(x_actor or payload.owner).strip() or "system",
|
|
|
|
|
|
request_id=x_request_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
_handle_asset_error(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 05:18:16 +00:00
|
|
|
|
@router.patch(
|
|
|
|
|
|
"/{asset_id}",
|
|
|
|
|
|
response_model=AgentAssetRead,
|
|
|
|
|
|
summary="更新 Agent 资产",
|
|
|
|
|
|
description="更新资产基础信息、当前版本、状态和配置,并写入审计日志。",
|
|
|
|
|
|
responses={
|
|
|
|
|
|
status.HTTP_400_BAD_REQUEST: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "状态更新非法或请求字段不合法。",
|
|
|
|
|
|
},
|
|
|
|
|
|
status.HTTP_404_NOT_FOUND: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "资产或指定版本不存在。",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-11 03:51:24 +00:00
|
|
|
|
def update_agent_asset(
|
|
|
|
|
|
asset_id: str,
|
|
|
|
|
|
payload: AgentAssetUpdate,
|
|
|
|
|
|
db: DbSession,
|
2026-05-11 05:18:16 +00:00
|
|
|
|
x_actor: ActorHeader = None,
|
|
|
|
|
|
x_request_id: RequestIdHeader = None,
|
2026-05-11 03:51:24 +00:00
|
|
|
|
) -> AgentAssetRead:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return AgentAssetService(db).update_asset(
|
|
|
|
|
|
asset_id,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
actor=(x_actor or "system").strip() or "system",
|
|
|
|
|
|
request_id=x_request_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
_handle_asset_error(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 05:18:16 +00:00
|
|
|
|
@router.get(
|
|
|
|
|
|
"/{asset_id}/versions",
|
|
|
|
|
|
response_model=list[AgentAssetVersionRead],
|
|
|
|
|
|
summary="查询资产版本列表",
|
|
|
|
|
|
description="返回指定资产的版本历史,默认按最近版本优先排序。",
|
|
|
|
|
|
responses={
|
|
|
|
|
|
status.HTTP_404_NOT_FOUND: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "资产不存在。",
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-11 03:51:24 +00:00
|
|
|
|
def list_agent_asset_versions(
|
2026-05-11 05:18:16 +00:00
|
|
|
|
asset_id: str,
|
|
|
|
|
|
db: DbSession,
|
|
|
|
|
|
limit: Annotated[
|
|
|
|
|
|
int,
|
|
|
|
|
|
Query(ge=1, le=100, description="返回版本数量上限。"),
|
|
|
|
|
|
] = 20,
|
2026-05-11 03:51:24 +00:00
|
|
|
|
) -> list[AgentAssetVersionRead]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return AgentAssetService(db).list_versions(asset_id, limit=limit)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
_handle_asset_error(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
|
"/{asset_id}/versions",
|
|
|
|
|
|
response_model=AgentAssetVersionRead,
|
|
|
|
|
|
status_code=status.HTTP_201_CREATED,
|
2026-05-11 05:18:16 +00:00
|
|
|
|
summary="创建资产版本",
|
|
|
|
|
|
description="为指定资产创建新版本;规则使用 Markdown,其他资产使用 JSON 快照。",
|
|
|
|
|
|
responses={
|
|
|
|
|
|
status.HTTP_400_BAD_REQUEST: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "版本号重复或内容类型不匹配。",
|
|
|
|
|
|
},
|
|
|
|
|
|
status.HTTP_404_NOT_FOUND: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "资产不存在。",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-05-11 03:51:24 +00:00
|
|
|
|
)
|
|
|
|
|
|
def create_agent_asset_version(
|
|
|
|
|
|
asset_id: str,
|
|
|
|
|
|
payload: AgentAssetVersionCreate,
|
|
|
|
|
|
db: DbSession,
|
2026-05-11 05:18:16 +00:00
|
|
|
|
x_actor: ActorHeader = None,
|
|
|
|
|
|
x_request_id: RequestIdHeader = None,
|
2026-05-11 03:51:24 +00:00
|
|
|
|
) -> AgentAssetVersionRead:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return AgentAssetService(db).create_version(
|
|
|
|
|
|
asset_id,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
actor=(x_actor or payload.created_by).strip() or "system",
|
|
|
|
|
|
request_id=x_request_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
_handle_asset_error(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
2026-05-11 05:18:16 +00:00
|
|
|
|
"/{asset_id}/reviews",
|
|
|
|
|
|
response_model=AgentAssetReviewRead,
|
|
|
|
|
|
status_code=status.HTTP_201_CREATED,
|
|
|
|
|
|
summary="创建资产审核记录",
|
|
|
|
|
|
description="为指定资产版本写入审核结果,并联动更新资产状态。",
|
|
|
|
|
|
responses={
|
|
|
|
|
|
status.HTTP_400_BAD_REQUEST: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "审核参数不合法。",
|
|
|
|
|
|
},
|
|
|
|
|
|
status.HTTP_404_NOT_FOUND: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "资产或版本不存在。",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2026-05-11 03:51:24 +00:00
|
|
|
|
)
|
|
|
|
|
|
def create_agent_asset_review(
|
|
|
|
|
|
asset_id: str,
|
|
|
|
|
|
payload: AgentAssetReviewCreate,
|
|
|
|
|
|
db: DbSession,
|
2026-05-11 05:18:16 +00:00
|
|
|
|
x_actor: ActorHeader = None,
|
|
|
|
|
|
x_request_id: RequestIdHeader = None,
|
2026-05-11 03:51:24 +00:00
|
|
|
|
) -> AgentAssetReviewRead:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return AgentAssetService(db).create_review(
|
|
|
|
|
|
asset_id,
|
|
|
|
|
|
payload,
|
|
|
|
|
|
actor=(x_actor or payload.reviewer).strip() or "system",
|
|
|
|
|
|
request_id=x_request_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
_handle_asset_error(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-11 05:18:16 +00:00
|
|
|
|
@router.post(
|
|
|
|
|
|
"/{asset_id}/activate",
|
|
|
|
|
|
response_model=AgentAssetRead,
|
|
|
|
|
|
summary="激活资产当前版本",
|
|
|
|
|
|
description="将资产当前版本切换为上线状态;规则资产必须已有 `approved` 审核记录。",
|
|
|
|
|
|
responses={
|
|
|
|
|
|
status.HTTP_400_BAD_REQUEST: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "审核未通过或当前版本未设置。",
|
|
|
|
|
|
},
|
|
|
|
|
|
status.HTTP_404_NOT_FOUND: {
|
|
|
|
|
|
"model": ErrorResponse,
|
|
|
|
|
|
"description": "资产不存在。",
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-05-11 03:51:24 +00:00
|
|
|
|
def activate_agent_asset(
|
|
|
|
|
|
asset_id: str,
|
|
|
|
|
|
db: DbSession,
|
2026-05-11 05:18:16 +00:00
|
|
|
|
x_actor: ActorHeader = None,
|
|
|
|
|
|
x_request_id: RequestIdHeader = None,
|
2026-05-11 03:51:24 +00:00
|
|
|
|
) -> AgentAssetRead:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return AgentAssetService(db).activate_asset(
|
|
|
|
|
|
asset_id,
|
|
|
|
|
|
actor=(x_actor or "system").strip() or "system",
|
|
|
|
|
|
request_id=x_request_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
_handle_asset_error(exc)
|