Files
hunjing/backend/app/api/v1/endpoints/admin.py
T
chenxin c63b75152a M8:天工阁、叩心与辰正——受托任务后台并行、叩心四约束、时态感知投影
- 天工阁域 domains/tiangong:backstage_tasks(委托包+验收标准+结果证据);有限分解(parent 链)与受控并行(各自后台 Run);子任务全终态级联父任务进入待验收;按证据 accept/reject
- kernel.start_backstage_run(kind=background_task):loop 后台分支不占前台席位、与前台对话并行;断言放宽容纳无人物功能任务;结果只经回调回呈任务——无身份写入路径
- 叩心域 domains/kouxin:knocking_candidates(event/time_anchor/manual 触发 + 草稿 + 裁决快照);四约束裁决(总开关→静默窗口【本地时区判定】→24h 频率→预算),dispatch 未过 429 且快照留痕;候选默认不自动发送;expire_stale 过期推进不触碰缘契
- 辰正 domains/zhengchen:temporal_context() 复用 time_sense 计算核(快照/农历/季节),产出【时态感知】投影段接入镜树生成 prompt(客观时间语境,不附情绪臆测——守一纪律)
- 退役:modules/goal_orchestrator|planner|perception|big_cycle、application/planning、services/governance|goal_persist、candidates 表链(imports 候选抽取 API 面)、goals/plans/agent_roster/perception_channels/big_cycles/旧候选表(迁移 009);前端 admin goals/plans/agents/perception/world 页与六组件;governance 只留 GenerationPreferences;admin 治理面收敛为司命+百工两枢;pivots 健康以域表计数重建
- 验收(test_m8_tiangong 五用例):后台任务无身份写入(记忆/魂籍/缘契零新增);后台与前台并行;分解级联;叩心四约束逐项;不回应不降低关系(缘契维度/状态/版本零变化)
- 四套件:后端 345、前端 203、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 009 净库干净
2026-08-28 09:13:39 +08:00

397 lines
12 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;
聚合视图自持查询(百工域常驻),无注入运行时。
"""
import json
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from app.api.v1.deps import (
get_current_user,
require_admin,
)
from app.models.runtime import Event
from app.schemas.admin_character import (
AdminCreateTemplateRequest,
AdminTemplateSummaryResponse,
AdminUpdateTemplateRequest,
)
from app.schemas.admin import (
AdminCreateUserRequest,
AdminUpdateUserRequest,
AdminUserResponse,
AuditEntry,
PendingApproval,
PivotHealthReport,
TimeEventListResponse,
TimeEventResponse,
TraceDetail,
TraceListResponse,
)
from app.schemas.auth import CurrentUser
from app.schemas.common import Page
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(
current_user: CurrentUser = Depends(get_current_user),
) -> PivotHealthReport:
return await get_pivots_health(
owner_scope=current_user.owner_scope,
)
# —— §19.2 朱批待办 ——
@router.get("/safety/pending", response_model=Page[PendingApproval])
async def safety_pending(
) -> Page[PendingApproval]:
items = await get_pending_approvals()
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 太一枢 · 目标 ——
# —— §19.4 烛照枢 · 感知通道 ——
# —— §19.4 洛书枢 · 计划 ——
# —— §19.4 天工阁枢 · 角色在岗 ——
# —— §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)