初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user