初始化提交

This commit is contained in:
chenxin committed 2026-08-21 16:00:13 +08:00
commit 887c1f449e
963 files changed
+166869

No files matched your search

+534
View File
@@ -0,0 +1,534 @@
"""司命台管理 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.memory 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(
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.memory 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(
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)