"""司命台治理对象 CRUD 服务(api-spec §19.4)。 五组开放集合(目标 / 感知通道 / 世界实体 / 计划 / 天工阁角色)的增删改查; 治理写操作一律记审计事件(audit_),并入起居注(§19.3)。pivot 由 action 前缀推断。 所有函数自管 ``AsyncSessionLocal``(路由直接调用,不传入 session)。 """ from app.core.errors import AppError from app.core.time import utc_now from app.db.session import AsyncSessionLocal from app.models.governance import ( AgentRosterEntry, Goal, PerceptionChannel, Plan, ToolPolicy, WorldEntity, ) from app.modules.runtime.events import Event from app.repositories import governance as governance_repo from app.repositories import runtime as runtime_repo from app.repositories.json_utils import dumps, loads_dict from app.schemas.admin import ( AgentRosterResponse, CreateAgentRequest, CreateGoalRequest, CreatePerceptionChannelRequest, CreatePlanRequest, CreateWorldEntityRequest, GoalNode, PerceptionChannelResponse, PlanSummaryResponse, UpdateAgentRequest, UpdateGoalRequest, UpdatePerceptionChannelRequest, UpdatePlanRequest, UpdateWorldEntityRequest, WorldEntityResponse, ) from app.schemas.safety import ( CreateToolPolicyRequest, SafetyPolicyItem, ToolCallDetail, ToolPolicyResponse, UpdateToolPolicyRequest, ) from app.services.audit import record_audit_event from app.services.serializers import ( agent_to_response, goal_to_response, perception_channel_to_response, plan_to_response, tool_policy_to_response, tool_policy_to_safety_item, world_entity_to_response, ) # —— 太一枢 · 目标 —— async def list_goals() -> list[GoalNode]: async with AsyncSessionLocal() as session: goals = await governance_repo.list_goals(session) return [goal_to_response(goal) for goal in goals] async def list_goals_by_character( character_id: str, status_filter: str | None = None, ) -> list[GoalNode]: async with AsyncSessionLocal() as session: goals = await governance_repo.list_goals_by_character( session, character_id, status_filter=status_filter ) return [goal_to_response(goal) for goal in goals] async def load_active_goals_context(character_id: str) -> str | None: """加载活跃目标并格式化为 system prompt 片段,供运行时注入。 目标按优先级排序(``prioritize_active_goals``),最高优先级在前。 """ from app.modules.goal_orchestrator import prioritize_active_goals from app.modules.runtime.goals import build_goals_context async with AsyncSessionLocal() as session: goals = await governance_repo.list_goals_by_character( session, character_id, status_filter="active" ) if not goals: return None goals_dicts = [ { "title": g.title, "level": g.level, "status": g.status, "priority": g.priority, "progress": g.progress, } for g in goals ] # 优先级排序后注入运行时,使最高优先级目标最先被看到 sorted_goals = prioritize_active_goals(goals_dicts) return build_goals_context(sorted_goals) async def emit_goal_event( goal: Goal, event_type: str, extra_payload: dict | None = None, ) -> None: """向事件存储写入 goal.* 事件。""" from app.main import app # 惰性 import 避免启动循环 event_store = getattr(app.state, "event_store", None) if event_store is None: return payload = { "goal_id": goal.id, "character_id": goal.character_id, "title": goal.title, "level": goal.level, "type": goal.type, "status": goal.status, "priority": goal.priority, "progress": goal.progress, } if extra_payload: payload.update(extra_payload) event = Event( type=f"goal.{event_type}", source="goal_orchestrator", payload=payload, character_id=goal.character_id, visibility="audit", ) await event_store.append(event) async def create_goal(payload: CreateGoalRequest) -> GoalNode: async with AsyncSessionLocal() as session: goal = Goal( title=payload.title, description=payload.description, level=payload.level, type=payload.type, character_id=payload.character_id, status="pending", priority=payload.priority, constraints_json=dumps(payload.constraints), parent_id=payload.parent_id, success_criteria_json=dumps(payload.success_criteria), ) await governance_repo.add_goal(session, goal) await record_audit_event( session, action="goal.created", actor="user", input_summary=goal.title, resource_type="goal", resource_id=goal.id, ) await session.commit() await session.refresh(goal) await emit_goal_event(goal, "created") return goal_to_response(goal) async def update_goal(goal_id: str, payload: UpdateGoalRequest) -> GoalNode: async with AsyncSessionLocal() as session: goal = await governance_repo.get_goal(session, goal_id) if goal is None: raise AppError("not_found", "目标不存在。") old_status = goal.status updates = payload.model_dump(exclude_unset=True) constraints = updates.pop("constraints", None) for key, value in updates.items(): setattr(goal, key, value) if constraints is not None: goal.constraints_json = dumps(constraints) goal.updated_at = utc_now() await record_audit_event( session, action="goal.updated", actor="user", input_summary=goal.title, resource_type="goal", resource_id=goal.id, ) await session.commit() await session.refresh(goal) if old_status != goal.status: if goal.status == "completed": await emit_goal_event(goal, "completed", {"previous_status": old_status}) else: await emit_goal_event(goal, "updated", {"previous_status": old_status}) else: await emit_goal_event(goal, "updated", {"previous_status": old_status}) return goal_to_response(goal) async def delete_goal(goal_id: str) -> None: async with AsyncSessionLocal() as session: goal = await governance_repo.get_goal(session, goal_id) if goal is None: raise AppError("not_found", "目标不存在。") if goal.level == "highest": raise AppError("invalid_request", "最高目标(使命级)不可删除。") await session.delete(goal) await record_audit_event( session, action="goal.deleted", actor="user", input_summary=goal.title, resource_type="goal", resource_id=goal.id, ) await session.commit() # —— 烛照枢 · 感知通道 —— async def list_perception_channels() -> list[PerceptionChannelResponse]: async with AsyncSessionLocal() as session: channels = await governance_repo.list_perception_channels(session) return [perception_channel_to_response(channel) for channel in channels] async def create_perception_channel( payload: CreatePerceptionChannelRequest, ) -> PerceptionChannelResponse: async with AsyncSessionLocal() as session: if await governance_repo.get_perception_channel(session, payload.channel) is not None: raise AppError("conflict", "感知通道已存在。") channel = PerceptionChannel( channel=payload.channel, name=payload.name, description=payload.description, risk_level=payload.risk_level, ) await governance_repo.add_perception_channel(session, channel) await record_audit_event( session, action="perception.created", actor="user", input_summary=channel.name, resource_type="perception_channel", resource_id=channel.channel, ) await session.commit() await session.refresh(channel) return perception_channel_to_response(channel) async def update_perception_channel( channel_id: str, payload: UpdatePerceptionChannelRequest ) -> PerceptionChannelResponse: async with AsyncSessionLocal() as session: channel = await governance_repo.get_perception_channel(session, channel_id) if channel is None: raise AppError("not_found", "感知通道不存在。") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(channel, key, value) channel.updated_at = utc_now() await record_audit_event( session, action="perception.updated", actor="user", input_summary=channel.name, resource_type="perception_channel", resource_id=channel.channel, ) await session.commit() await session.refresh(channel) return perception_channel_to_response(channel) async def delete_perception_channel(channel_id: str) -> None: async with AsyncSessionLocal() as session: channel = await governance_repo.get_perception_channel(session, channel_id) if channel is None: raise AppError("not_found", "感知通道不存在。") await session.delete(channel) await record_audit_event( session, action="perception.deleted", actor="user", input_summary=channel.name, resource_type="perception_channel", resource_id=channel.channel, ) await session.commit() # —— 河图枢 · 世界实体 —— async def list_world_entities() -> list[WorldEntityResponse]: async with AsyncSessionLocal() as session: entities = await governance_repo.list_world_entities(session) return [world_entity_to_response(entity) for entity in entities] async def create_world_entity( payload: CreateWorldEntityRequest, ) -> WorldEntityResponse: async with AsyncSessionLocal() as session: entity = WorldEntity( type=payload.type, name=payload.name, attributes_json=dumps(payload.attributes), state_type=payload.state_type, confidence=payload.confidence, sources_json=dumps(payload.sources), ) await governance_repo.add_world_entity(session, entity) await record_audit_event( session, action="world.created", actor="user", input_summary=entity.name, resource_type="world_entity", resource_id=entity.id, ) await session.commit() await session.refresh(entity) return world_entity_to_response(entity) async def update_world_entity( entity_id: str, payload: UpdateWorldEntityRequest ) -> WorldEntityResponse: async with AsyncSessionLocal() as session: entity = await governance_repo.get_world_entity(session, entity_id) if entity is None: raise AppError("not_found", "世界实体不存在。") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(entity, key, value) entity.updated_at = utc_now() await record_audit_event( session, action="world.updated", actor="user", input_summary=entity.name, resource_type="world_entity", resource_id=entity.id, ) await session.commit() await session.refresh(entity) return world_entity_to_response(entity) async def delete_world_entity(entity_id: str) -> None: async with AsyncSessionLocal() as session: entity = await governance_repo.get_world_entity(session, entity_id) if entity is None: raise AppError("not_found", "世界实体不存在。") await session.delete(entity) await record_audit_event( session, action="world.deleted", actor="user", input_summary=entity.name, resource_type="world_entity", resource_id=entity.id, ) await session.commit() # —— 洛书枢 · 计划 —— async def list_plans() -> list[PlanSummaryResponse]: async with AsyncSessionLocal() as session: plans = await governance_repo.list_plans(session) return [plan_to_response(plan) for plan in plans] async def create_plan(payload: CreatePlanRequest) -> PlanSummaryResponse: async with AsyncSessionLocal() as session: plan = Plan( goal_id=payload.goal_id, title=payload.title, assumptions_json=dumps(payload.assumptions), risks_json=dumps(payload.risks), ) await governance_repo.add_plan(session, plan) await record_audit_event( session, action="plan.created", actor="user", input_summary=plan.title, resource_type="plan", resource_id=plan.id, ) await session.commit() await session.refresh(plan) return plan_to_response(plan) async def update_plan(plan_id: str, payload: UpdatePlanRequest) -> PlanSummaryResponse: async with AsyncSessionLocal() as session: plan = await governance_repo.get_plan(session, plan_id) if plan is None: raise AppError("not_found", "计划不存在。") updates = payload.model_dump(exclude_unset=True) assumptions = updates.pop("assumptions", None) risks = updates.pop("risks", None) for key, value in updates.items(): setattr(plan, key, value) if assumptions is not None: plan.assumptions_json = dumps(assumptions) if risks is not None: plan.risks_json = dumps(risks) plan.updated_at = utc_now() await record_audit_event( session, action="plan.updated", actor="user", input_summary=plan.title, resource_type="plan", resource_id=plan.id, ) await session.commit() await session.refresh(plan) return plan_to_response(plan) async def delete_plan(plan_id: str) -> None: async with AsyncSessionLocal() as session: plan = await governance_repo.get_plan(session, plan_id) if plan is None: raise AppError("not_found", "计划不存在。") await session.delete(plan) await record_audit_event( session, action="plan.deleted", actor="user", input_summary=plan.title, resource_type="plan", resource_id=plan.id, ) await session.commit() # —— 天工阁枢 · 角色在岗 —— async def list_agent_roster() -> list[AgentRosterResponse]: async with AsyncSessionLocal() as session: agents = await governance_repo.list_agent_roster(session) return [agent_to_response(agent) for agent in agents] async def create_agent(payload: CreateAgentRequest) -> AgentRosterResponse: async with AsyncSessionLocal() as session: if await governance_repo.get_agent(session, payload.role) is not None: raise AppError("conflict", "角色已存在。") agent = AgentRosterEntry( role=payload.role, name=payload.name, responsibility=payload.responsibility, ) await governance_repo.add_agent(session, agent) await record_audit_event( session, action="agent.created", actor="user", input_summary=agent.name, resource_type="agent", resource_id=agent.role, ) await session.commit() await session.refresh(agent) return agent_to_response(agent) async def update_agent(role: str, payload: UpdateAgentRequest) -> AgentRosterResponse: async with AsyncSessionLocal() as session: agent = await governance_repo.get_agent(session, role) if agent is None: raise AppError("not_found", "角色不存在。") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(agent, key, value) agent.updated_at = utc_now() await record_audit_event( session, action="agent.updated", actor="user", input_summary=agent.name, resource_type="agent", resource_id=agent.role, ) await session.commit() await session.refresh(agent) return agent_to_response(agent) async def delete_agent(role: str) -> None: async with AsyncSessionLocal() as session: agent = await governance_repo.get_agent(session, role) if agent is None: raise AppError("not_found", "角色不存在。") await session.delete(agent) await record_audit_event( session, action="agent.deleted", actor="user", input_summary=agent.name, resource_type="agent", resource_id=agent.role, ) await session.commit() # —— 百工枢 · 工具策略(§15 / §16 共享)—— def _builtin_tool_names() -> set[str]: """builtin 工具名集合(惰性 import,避免 application→services 循环依赖)。""" from app.application.tool_runtime.builtins import builtin_specs return set(builtin_specs().keys()) async def list_tool_policies() -> list[ToolPolicyResponse]: """GET /tools:完整元数据,executable 按 builtin 派生。""" async with AsyncSessionLocal() as session: policies = await governance_repo.list_tool_policies(session) builtins = _builtin_tool_names() return [tool_policy_to_response(p, executable=p.name in builtins) for p in policies] async def list_safety_policies() -> list[SafetyPolicyItem]: """GET /safety/policies:策略子集。""" async with AsyncSessionLocal() as session: policies = await governance_repo.list_tool_policies(session) return [tool_policy_to_safety_item(p) for p in policies] async def create_tool_policy(payload: CreateToolPolicyRequest) -> ToolPolicyResponse: """POST /tools、POST /safety/policies:注册工具策略(source=custom)。""" async with AsyncSessionLocal() as session: if await governance_repo.get_tool_policy(session, payload.name) is not None: raise AppError("invalid_request", f"工具 {payload.name} 已存在。") policy = ToolPolicy( name=payload.name, description=payload.description, risk_level=payload.risk_level, permission_policy=payload.permission_policy, side_effect=payload.side_effect, audit_required=payload.audit_required, source="custom", enabled=payload.enabled, ) await governance_repo.add_tool_policy(session, policy) await record_audit_event( session, action="tool.policy_created", actor="user", risk_level=payload.risk_level, input_summary=payload.name, resource_type="tool_policy", resource_id=policy.name, ) await session.commit() await session.refresh(policy) return tool_policy_to_response(policy, executable=policy.name in _builtin_tool_names()) async def update_tool_policy(name: str, payload: UpdateToolPolicyRequest) -> ToolPolicyResponse: """PATCH /tools/{name}、PATCH /safety/policies/{tool}:编辑策略元数据。""" async with AsyncSessionLocal() as session: policy = await governance_repo.get_tool_policy(session, name) if policy is None: raise AppError("not_found", f"工具 {name} 不存在。") updates = payload.model_dump(exclude_unset=True) if not updates: raise AppError("invalid_request", "至少需要提供一个待更新字段。") for key, value in updates.items(): setattr(policy, key, value) policy.updated_at = utc_now() await record_audit_event( session, action="tool.policy_updated", actor="user", risk_level=policy.risk_level, input_summary=policy.name, resource_type="tool_policy", resource_id=policy.name, ) await session.commit() await session.refresh(policy) return tool_policy_to_response(policy, executable=policy.name in _builtin_tool_names()) async def delete_tool_policy(name: str) -> None: """DELETE /tools/{name}、DELETE /safety/policies/{tool}:注销策略。 builtin 工具的策略记录不可删除(执行实现仍在代码层);只允许注销 custom 工具。 """ async with AsyncSessionLocal() as session: policy = await governance_repo.get_tool_policy(session, name) if policy is None: raise AppError("not_found", f"工具 {name} 不存在。") if policy.source == "builtin": raise AppError("invalid_request", f"内置工具 {name} 不可删除,可改用 PATCH 禁用。") await session.delete(policy) await record_audit_event( session, action="tool.policy_deleted", actor="user", input_summary=policy.name, resource_type="tool_policy", resource_id=policy.name, ) await session.commit() async def get_tool_call(tool_call_id: str) -> ToolCallDetail: """GET /tools/calls/{tool_call_id}:单条 tool.* 事件详情。 tool_call_id 对应一炁事件存储的事件主键;仅返回 tool.* 类型事件,否则 404。 """ from app.core.time import to_iso async with AsyncSessionLocal() as session: event = await runtime_repo.get_event(session, tool_call_id) if event is None or not event.type.startswith("tool."): raise AppError("not_found", f"工具调用 {tool_call_id} 不存在。") payload = loads_dict(event.payload_json) return ToolCallDetail( event_id=event.id, type=event.type, tool=payload.get("tool"), run_id=event.run_id, character_id=event.character_id, trace_id=event.trace_id, payload=payload, created_at=to_iso(event.created_at), )