Files
hunjing/backend/app/api/v1/endpoints/admin.py
T

535 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""司命台管理 API(api-spec §19)。
聚合九枢治理视图:九枢健康度 / 朱批待办 / 起居注审计(§19.1–§19.3)
+ 五组治理对象 CRUD(§19.4)。治理写操作由 service 层记审计并入起居注。
路由直接调用 ``services`` 函数(函数内部自管 ``AsyncSessionLocal``),不持有 session;
``ToolRuntime`` / ``ConfirmationStore`` 等运行时对象经 ``deps`` 从 ``app.state`` 注入。
"""
import json
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from app.api.v1.deps import (
get_confirmation_store,
get_current_user,
get_optional_tool_runtime,
require_admin,
)
from app.application.tool_runtime.runtime import ToolRuntime
from app.models.runtime import Event
from app.modules.safety.confirmation import ConfirmationStore
from app.schemas.admin import (
AdminCreateUserRequest,
AdminUpdateUserRequest,
AdminUserResponse,
AgentRosterResponse,
AuditEntry,
CreateAgentRequest,
CreateGoalRequest,
CreatePerceptionChannelRequest,
CreatePlanRequest,
CreateWorldEntityRequest,
GoalNode,
PendingApproval,
PerceptionChannelResponse,
PivotHealthReport,
PlanSummaryResponse,
TimeEventListResponse,
TimeEventResponse,
TraceDetail,
TraceListResponse,
UpdateAgentRequest,
UpdateGoalRequest,
UpdatePerceptionChannelRequest,
UpdatePlanRequest,
UpdateWorldEntityRequest,
WorldEntityResponse,
)
from app.schemas.admin_character import (
AdminCreateTemplateRequest,
AdminTemplateSummaryResponse,
AdminUpdateTemplateRequest,
)
from app.schemas.auth import CurrentUser
from app.schemas.common import Page
from app.services import governance as governance_service
from app.services.admin import (
get_audit_timeline,
get_pending_approvals,
get_pivots_health,
get_trace,
list_time_events,
list_traces,
)
from app.services.admin_characters import (
admin_create_template,
admin_delete_template,
admin_get_template,
admin_list_templates,
admin_update_template,
)
router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(require_admin)])
# —— §19.1 九枢健康度聚合 ——
@router.get("/pivots/health", response_model=PivotHealthReport)
async def pivots_health(
confirmation_store: ConfirmationStore = Depends(get_confirmation_store),
tool_runtime: ToolRuntime | None = Depends(get_optional_tool_runtime),
current_user: CurrentUser = Depends(get_current_user),
) -> PivotHealthReport:
return await get_pivots_health(
confirmation_store=confirmation_store,
tool_runtime=tool_runtime,
owner_scope=current_user.owner_scope,
)
# —— §19.2 朱批待办 ——
@router.get("/safety/pending", response_model=Page[PendingApproval])
async def safety_pending(
confirmation_store: ConfirmationStore = Depends(get_confirmation_store),
tool_runtime: ToolRuntime | None = Depends(get_optional_tool_runtime),
) -> Page[PendingApproval]:
items = await get_pending_approvals(
confirmation_store=confirmation_store, tool_runtime=tool_runtime
)
return Page(items=items, page=1, page_size=len(items), total=len(items))
# —— §19.3 起居注审计 ——
@router.get("/audit/timeline", response_model=Page[AuditEntry])
async def audit_timeline(
pivot: str | None = Query(default=None, description="按枢过滤(契约字典 §5.9)。"),
page: int = Query(default=1, ge=1),
page_size: int = Query(default=50, ge=1, le=200),
) -> Page[AuditEntry]:
return await get_audit_timeline(pivot=pivot, page=page, page_size=page_size)
# —— §19.4 太一枢 · 目标 ——
@router.get("/goals", response_model=list[GoalNode])
async def list_goals(
character_id: str | None = Query(default=None, description="按人物过滤。"),
) -> list[GoalNode]:
if character_id:
return await governance_service.list_goals_by_character(character_id)
return await governance_service.list_goals()
@router.post("/goals", response_model=GoalNode)
async def create_goal(payload: CreateGoalRequest) -> GoalNode:
return await governance_service.create_goal(payload)
@router.patch("/goals/{goal_id}", response_model=GoalNode)
async def update_goal(
goal_id: str,
payload: UpdateGoalRequest,
) -> GoalNode:
return await governance_service.update_goal(goal_id, payload)
@router.delete("/goals/{goal_id}", status_code=204)
async def delete_goal(goal_id: str) -> None:
await governance_service.delete_goal(goal_id)
# —— §19.4 烛照枢 · 感知通道 ——
@router.get("/perception/channels", response_model=list[PerceptionChannelResponse])
async def list_perception_channels() -> list[PerceptionChannelResponse]:
return await governance_service.list_perception_channels()
@router.post("/perception/channels", response_model=PerceptionChannelResponse)
async def create_perception_channel(
payload: CreatePerceptionChannelRequest,
) -> PerceptionChannelResponse:
return await governance_service.create_perception_channel(payload)
@router.patch("/perception/channels/{channel}", response_model=PerceptionChannelResponse)
async def update_perception_channel(
channel: str,
payload: UpdatePerceptionChannelRequest,
) -> PerceptionChannelResponse:
return await governance_service.update_perception_channel(channel, payload)
@router.delete("/perception/channels/{channel}", status_code=204)
async def delete_perception_channel(channel: str) -> None:
await governance_service.delete_perception_channel(channel)
# —— §19.4 河图枢 · 世界实体 ——
@router.get("/world/entities", response_model=list[WorldEntityResponse])
async def list_world_entities() -> list[WorldEntityResponse]:
return await governance_service.list_world_entities()
@router.post("/world/entities", response_model=WorldEntityResponse)
async def create_world_entity(
payload: CreateWorldEntityRequest,
) -> WorldEntityResponse:
return await governance_service.create_world_entity(payload)
@router.patch("/world/entities/{entity_id}", response_model=WorldEntityResponse)
async def update_world_entity(
entity_id: str,
payload: UpdateWorldEntityRequest,
) -> WorldEntityResponse:
return await governance_service.update_world_entity(entity_id, payload)
@router.delete("/world/entities/{entity_id}", status_code=204)
async def delete_world_entity(entity_id: str) -> None:
await governance_service.delete_world_entity(entity_id)
# —— §19.4 洛书枢 · 计划 ——
@router.get("/plans", response_model=list[PlanSummaryResponse])
async def list_plans() -> list[PlanSummaryResponse]:
return await governance_service.list_plans()
@router.post("/plans", response_model=PlanSummaryResponse)
async def create_plan(payload: CreatePlanRequest) -> PlanSummaryResponse:
return await governance_service.create_plan(payload)
@router.patch("/plans/{plan_id}", response_model=PlanSummaryResponse)
async def update_plan(
plan_id: str,
payload: UpdatePlanRequest,
) -> PlanSummaryResponse:
return await governance_service.update_plan(plan_id, payload)
@router.delete("/plans/{plan_id}", status_code=204)
async def delete_plan(plan_id: str) -> None:
await governance_service.delete_plan(plan_id)
# —— §19.4 天工阁枢 · 角色在岗 ——
@router.get("/agents/roster", response_model=list[AgentRosterResponse])
async def list_agent_roster() -> list[AgentRosterResponse]:
return await governance_service.list_agent_roster()
@router.post("/agents/roster", response_model=AgentRosterResponse)
async def create_agent(payload: CreateAgentRequest) -> AgentRosterResponse:
return await governance_service.create_agent(payload)
@router.patch("/agents/roster/{role}", response_model=AgentRosterResponse)
async def update_agent(
role: str,
payload: UpdateAgentRequest,
) -> AgentRosterResponse:
return await governance_service.update_agent(role, payload)
@router.delete("/agents/roster/{role}", status_code=204)
async def delete_agent(role: str) -> None:
await governance_service.delete_agent(role)
# —— §19.5 链路追踪 ——
@router.get("/traces", response_model=TraceListResponse)
async def trace_list(
limit: int = Query(default=50, ge=1, le=500),
) -> TraceListResponse:
"""列出最近活动的 trace_id 概览(跨运行事件 / 安全审计 / 请求幂等聚合)。"""
return await list_traces(limit=limit)
@router.get("/traces/{trace_id}", response_model=TraceDetail)
async def trace_detail(trace_id: str) -> TraceDetail:
"""按 trace_id 聚合运行事件 / 安全审计 / 请求幂等三表为升序时间线。"""
return await get_trace(trace_id)
# —— §8 司命台 · 起居注时间河(辰正 time.* 事件)——
@router.get("/time-events", response_model=TimeEventListResponse)
async def time_event_list(
limit: int = Query(default=100, ge=1, le=200),
offset: int = Query(default=0, ge=0),
) -> TimeEventListResponse:
"""起居注时间河:辰正 time.* 事件(§8 司命台接入,source='time_sense')。"""
items, total = await list_time_events(limit=limit, offset=offset)
return TimeEventListResponse(
items=[_time_event_to_response(item) for item in items],
total=total,
)
def _time_event_to_response(event: Event) -> TimeEventResponse:
"""复用既有 payload 解析模式(json.loads + 异常回退到空 dict,与 _event_to_item 一致)。"""
try:
payload = json.loads(event.payload_json) if event.payload_json else {}
except (json.JSONDecodeError, TypeError):
payload = {}
return TimeEventResponse(
id=event.id,
type=event.type,
payload=payload,
run_id=event.run_id,
character_id=event.character_id,
sensitivity=event.sensitivity,
created_at=event.created_at.isoformat() if event.created_at else "",
)
# —— §19.6 用户管理 ——
@router.get("/users", response_model=list[AdminUserResponse])
async def admin_list_users() -> list[AdminUserResponse]:
"""列出所有用户(管理员用)。"""
from sqlalchemy import select
from app.db.session import AsyncSessionLocal
from app.models.user import User
async with AsyncSessionLocal() as session:
result = await session.execute(select(User).order_by(User.created_at))
users = result.scalars().all()
return [AdminUserResponse.model_validate(u) for u in users]
@router.post("/users", response_model=AdminUserResponse)
async def admin_create_user(payload: AdminCreateUserRequest) -> AdminUserResponse:
"""创建用户(管理员用)。"""
from app.db.session import AsyncSessionLocal
from app.services.auth import create_user
async with AsyncSessionLocal() as session:
user = await create_user(
session,
username=payload.username,
email=payload.email,
password=payload.password,
is_admin=payload.is_admin,
)
await session.commit()
await session.refresh(user)
return AdminUserResponse.model_validate(user)
@router.patch("/users/{user_id}", response_model=AdminUserResponse)
async def admin_update_user(user_id: str, payload: AdminUpdateUserRequest) -> AdminUserResponse:
"""更新用户(启用/禁用、管理员角色)。"""
from sqlalchemy import select
from app.db.session import AsyncSessionLocal
from app.models.user import User
async with AsyncSessionLocal() as session:
result = await session.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if user is None:
from app.core.errors import AppError
raise AppError("not_found", "用户不存在。")
if payload.is_active is not None:
user.is_active = payload.is_active
if payload.is_admin is not None:
user.is_admin = payload.is_admin
await session.commit()
await session.refresh(user)
return AdminUserResponse.model_validate(user)
# —— §19.7 多租户管理视图 ——
class _TenantUserOverview(BaseModel):
"""租户概览(管理员跨租户视角)。"""
user_id: str
username: str
email: str
is_active: bool
is_admin: bool
character_count: int
memory_count: int
@router.get("/tenants/overview", response_model=list[_TenantUserOverview])
async def admin_tenants_overview() -> list[_TenantUserOverview]:
"""跨租户概览:每用户的人物与记忆计数(管理员用)。"""
from sqlalchemy import func, select
from app.db.session import AsyncSessionLocal
from app.models.character import Character
from app.models.xirang import Memory
from app.models.user import User
async with AsyncSessionLocal() as session:
result = await session.execute(select(User).order_by(User.created_at))
users = result.scalars().all()
overview: list[_TenantUserOverview] = []
for user in users:
char_count = await session.scalar(
select(func.count())
.select_from(Character)
.where(
Character.owner_scope == user.id,
Character.is_system_template.is_(False),
)
)
mem_count = await session.scalar(
select(func.count())
.select_from(Memory)
.join(Character, Memory.character_id == Character.id).where(Memory.status != "tombstoned")
.where(
Character.owner_scope == user.id,
Character.is_system_template.is_(False),
)
)
overview.append(
_TenantUserOverview(
user_id=user.id,
username=user.username,
email=user.email,
is_active=user.is_active,
is_admin=user.is_admin,
character_count=char_count or 0,
memory_count=mem_count or 0,
)
)
return overview
@router.get("/tenants/{user_id}/characters")
async def admin_tenant_characters(user_id: str) -> list[dict]:
"""查看指定用户的所有人物(跨租户管理员用)。"""
from sqlalchemy import select
from app.db.session import AsyncSessionLocal
from app.models.character import Character
from app.services.serializers import character_to_response
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Character)
.where(
Character.owner_scope == user_id,
Character.is_system_template.is_(False),
)
.order_by(Character.created_at)
)
return [character_to_response(c).model_dump(mode="json") for c in result.scalars().all()]
@router.get("/tenants/{user_id}/memories")
async def admin_tenant_memories(user_id: str) -> list[dict]:
"""查看指定用户的所有记忆(跨租户管理员用)。"""
from sqlalchemy import select
from app.db.session import AsyncSessionLocal
from app.models.character import Character
from app.models.xirang import Memory
from app.services.serializers import memory_to_response
async with AsyncSessionLocal() as session:
result = await session.execute(
select(Memory)
.join(Character, Memory.character_id == Character.id).where(Memory.status != "tombstoned")
.where(
Character.owner_scope == user_id,
Character.is_system_template.is_(False),
)
.order_by(Memory.created_at.desc())
)
return [memory_to_response(m).model_dump(mode="json") for m in result.scalars().all()]
# —— §19.8 人物模板管理(管理员专用 CRUD) ——
@router.get(
"/characters/templates",
response_model=list[AdminTemplateSummaryResponse],
)
async def list_templates() -> list[AdminTemplateSummaryResponse]:
"""列出所有系统模板(管理端完整视图,含 profile / model_policy 全文)。"""
return await admin_list_templates()
@router.get(
"/characters/templates/{key}",
response_model=AdminTemplateSummaryResponse,
)
async def get_template(key: str) -> AdminTemplateSummaryResponse:
"""获取单个系统模板详情。"""
return await admin_get_template(key)
@router.post(
"/characters/templates",
response_model=AdminTemplateSummaryResponse,
status_code=201,
)
async def create_template(
payload: AdminCreateTemplateRequest,
) -> AdminTemplateSummaryResponse:
"""创建新的系统模板。
如 key 与 YAML 中已存在的 key 相同,下次重启时会被 YAML 配置覆盖。
"""
return await admin_create_template(payload)
@router.patch(
"/characters/templates/{key}",
response_model=AdminTemplateSummaryResponse,
)
async def update_template(
key: str,
payload: AdminUpdateTemplateRequest,
) -> AdminTemplateSummaryResponse:
"""更新系统模板(部分更新)。
仅提供非 None 的字段会被更新。YAML 定义的模板字段会在下次重启时被 YAML 覆盖。
"""
return await admin_update_template(key, payload)
@router.delete(
"/characters/templates/{key}",
status_code=204,
)
async def delete_template(key: str) -> None:
"""删除系统模板。
仅允许删除未被任何用户人物引用的模板。
被引用时返回 409 conflict。
"""
await admin_delete_template(key)