M6:缘契、人格与世界——契约版本链、铸魂分灵、河图境界与三界分流
- 缘契域 domains/yuanqi:covenants(当前版)+ covenant_events(不可变历史);提议—问心—铭契—撤回全链;边界变更立即生效;多维关系并存,服务层拒绝 affinity/好感度单一键;浪漫维度成人资格标记位 - 道体铸魂与分灵 domains/jingsheng/soul.py:soul_forgings 确认流(锚点变更 pending→confirmed 应用并签发新魂籍版本,确认前人物锚点不变);分灵创建新人物(继承资料与谱系 lineage 链、不继承原身份,魂籍从 v1 起) - 河图域 domains/hetu:persistent_worlds 境界 + world_entities/world_relations 境界作用域重建 + 世谱挂接;境界正史视图(scope=persistent_world 归真集合);canonize 后置推进正史计数 - 三界分流:reality→息壤提取(不变);persistent_world→境界实录+正史推进;simulation→只记试演实录;factual_records 增 scope 列——境界/幻境实录与真界本相隔离 - 正影打通:引文 target_type=memory 时被引记忆 superseded → 返回 successor_memory_id 提示,不静默替换 - 前端:人物详情页缘契面板(提议维度选择/铭契/撤回/事件史)+ 世界页重建(境界目录/实体/正史概览)+ covenants/worlds api 库 - 退役:旧 personality/drive/evolution/world 模块、服务、端点、仓储、schema、前端五页与组件样式;迁移 007 退役九张旧表与 characters.current_personality_version_id - 验收(test_m6_domains 六用例):多维无单一好感度/铭契版本链/事件不可抹除/撤回立即生效/边界即时/铸魂确认流+魂籍版本/分灵谱系/境界正史不进真界(实录 scope 隔离+真界候选=0)/引文正影后继 - 四套件:后端 379、前端 199、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 007 净库+±1 干净
This commit is contained in:
1 parent
684261f4b4
commit
7e23d6fa84
108 files changed
+2600
-9732
No files matched your search
@@ -17,7 +17,6 @@ from app.models.governance import (
|
||||
Goal,
|
||||
PerceptionChannel,
|
||||
Plan,
|
||||
WorldEntity,
|
||||
)
|
||||
from app.models.idempotency import IdempotencyKey
|
||||
from app.models.xirang import Memory
|
||||
@@ -81,13 +80,15 @@ async def get_pivots_health(
|
||||
)
|
||||
)
|
||||
goals_n = await governance_repo.count(session, Goal)
|
||||
world_n = await governance_repo.count(session, WorldEntity)
|
||||
from app.domains.hetu.models import PersistentWorld
|
||||
|
||||
world_n = await governance_repo.count(session, PersistentWorld)
|
||||
plans_n = await governance_repo.count(session, Plan)
|
||||
perception_n = await governance_repo.count(session, PerceptionChannel)
|
||||
agents_n = await governance_repo.count(session, AgentRosterEntry)
|
||||
memories_n = await governance_repo.count(session, Memory, Memory.status != "tombstoned")
|
||||
character = await character_repo.get_active_character(session, owner_scope)
|
||||
has_personality = bool(character and character.current_personality_version_id)
|
||||
has_personality = bool(character and character.identity_baseline_json not in ("", "{}"))
|
||||
tool_enabled = True # 百工域常驻(乾坤可用性按端点区分)
|
||||
now = to_iso(utc_now())
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from app.core.errors import AppError
|
||||
from app.core.time import utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.character import Character
|
||||
from app.models.personality import PersonalityVersion
|
||||
from app.modules.character import (
|
||||
MODEL_POLICY_CREATE_DEFAULTS,
|
||||
SYSTEM_TEMPLATE_OWNER_SCOPE,
|
||||
@@ -17,7 +16,6 @@ from app.modules.character import (
|
||||
sanitize_profile,
|
||||
)
|
||||
from app.repositories import characters as character_repo
|
||||
from app.repositories import personality as personality_repo
|
||||
from app.repositories.json_utils import dumps, loads_dict
|
||||
from app.schemas.character import (
|
||||
ActivateCharacterResponse,
|
||||
@@ -100,25 +98,6 @@ async def create_default_character(
|
||||
return await create_character_from_template(session, template, owner_scope)
|
||||
|
||||
|
||||
async def ensure_character_personality(
|
||||
session: AsyncSession, character: Character
|
||||
) -> PersonalityVersion:
|
||||
from app.services.personality import create_personality_from_profile
|
||||
|
||||
personality = None
|
||||
if character.current_personality_version_id:
|
||||
personality = await personality_repo.get_personality(
|
||||
session, character.current_personality_version_id
|
||||
)
|
||||
if personality is None:
|
||||
personality = await personality_repo.get_latest_personality(session, character.id)
|
||||
if personality is None:
|
||||
personality = await create_personality_from_profile(session, character)
|
||||
if character.current_personality_version_id != personality.id:
|
||||
character.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
return personality
|
||||
|
||||
|
||||
async def ensure_default_system_template(session: AsyncSession) -> Character:
|
||||
"""按 config/character_templates.yaml 确保系统模板存在且最新。
|
||||
@@ -135,7 +114,6 @@ async def ensure_default_system_template(session: AsyncSession) -> Character:
|
||||
# create 路径已由 _create_template_from_config 兜底 birthday,
|
||||
# sync 路径可能因旧数据缺失 birthday,再兜一次确保存在。
|
||||
await _ensure_default_birthday(session, template)
|
||||
await ensure_character_personality(session, template)
|
||||
return template
|
||||
|
||||
|
||||
@@ -160,7 +138,6 @@ async def ensure_all_system_templates(session: AsyncSession) -> list[Character]:
|
||||
else:
|
||||
await _sync_template_from_config(session, template, config)
|
||||
await _ensure_default_birthday(session, template)
|
||||
await ensure_character_personality(session, template)
|
||||
results.append(template)
|
||||
|
||||
return results
|
||||
@@ -267,7 +244,6 @@ async def ensure_default_character_for_owner(session: AsyncSession, owner_scope:
|
||||
if not configs:
|
||||
template = await ensure_default_system_template(session)
|
||||
character = await create_character_from_template(session, template, owner_scope)
|
||||
await _ensure_character_personality_from_template(session, template, character)
|
||||
return character
|
||||
|
||||
# 先确保所有系统模板在 DB 中存在
|
||||
@@ -278,7 +254,6 @@ async def ensure_default_character_for_owner(session: AsyncSession, owner_scope:
|
||||
config = configs[key]
|
||||
tpl = await _create_template_from_config(session, config)
|
||||
await _ensure_default_birthday(session, tpl)
|
||||
await ensure_character_personality(session, tpl)
|
||||
templates[key] = tpl
|
||||
|
||||
# 筛选 is_default=true 的配置 key
|
||||
@@ -295,19 +270,11 @@ async def ensure_default_character_for_owner(session: AsyncSession, owner_scope:
|
||||
session, tpl.id, owner_scope
|
||||
)
|
||||
if existing is not None:
|
||||
await ensure_character_personality(session, existing)
|
||||
if primary is None:
|
||||
primary = existing
|
||||
continue
|
||||
|
||||
character = await create_character_from_template(session, tpl, owner_scope)
|
||||
from app.services.personality import copy_personality_to_character
|
||||
|
||||
template_personality = await ensure_character_personality(session, tpl)
|
||||
personality = await copy_personality_to_character(
|
||||
session, template_personality, character.id
|
||||
)
|
||||
character.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
|
||||
await record_audit_event(
|
||||
@@ -327,32 +294,11 @@ async def ensure_default_character_for_owner(session: AsyncSession, owner_scope:
|
||||
if primary is None:
|
||||
template = await ensure_default_system_template(session)
|
||||
primary = await create_character_from_template(session, template, owner_scope)
|
||||
template_personality = await ensure_character_personality(session, template)
|
||||
from app.services.personality import copy_personality_to_character
|
||||
|
||||
personality = await copy_personality_to_character(session, template_personality, primary.id)
|
||||
primary.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
|
||||
return primary
|
||||
|
||||
|
||||
async def _ensure_character_personality_from_template(
|
||||
session: AsyncSession,
|
||||
template: Character,
|
||||
character: Character,
|
||||
) -> None:
|
||||
"""从模板复制人格到目标人物。"""
|
||||
from app.services.personality import copy_personality_to_character
|
||||
|
||||
template_personality = await ensure_character_personality(session, template)
|
||||
personality = await copy_personality_to_character(session, template_personality, character.id)
|
||||
character.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
|
||||
|
||||
# —— 人物管理 API(api-spec §6)——
|
||||
|
||||
|
||||
async def get_owned_character(
|
||||
session: AsyncSession, character_id: str, owner_scope: str = settings.default_owner_scope
|
||||
@@ -364,12 +310,10 @@ async def get_owned_character(
|
||||
return character
|
||||
|
||||
|
||||
async def _personality_version_number(session: AsyncSession, character: Character) -> int:
|
||||
version_id = character.current_personality_version_id
|
||||
if not version_id:
|
||||
return 0
|
||||
personality = await personality_repo.get_personality(session, version_id)
|
||||
return personality.version if personality else 0
|
||||
def _personality_version_number(character: Character) -> int:
|
||||
"""人格版本随旧版本表退役——版本语义由魂籍(identity_record)承载,此处恒 0 占位。"""
|
||||
_ = character
|
||||
return 0
|
||||
|
||||
|
||||
async def _audit_character(session: AsyncSession, character: Character, action: str) -> None:
|
||||
@@ -435,7 +379,6 @@ async def create_character(
|
||||
)
|
||||
await character_repo.add_character(session, character)
|
||||
await _ensure_default_birthday(session, character)
|
||||
await ensure_character_personality(session, character)
|
||||
await _audit_character(session, character, "character.created_from_template")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
@@ -488,7 +431,7 @@ async def list_characters_page(
|
||||
character_to_summary(
|
||||
character,
|
||||
memory_count=await character_repo.count_memories(session, character.id),
|
||||
personality_version=await _personality_version_number(session, character),
|
||||
personality_version=_personality_version_number(character),
|
||||
)
|
||||
for character in items
|
||||
]
|
||||
@@ -569,7 +512,6 @@ async def activate_character(
|
||||
await _deactivate_others(session, character, owner_scope)
|
||||
character.status = "active"
|
||||
character.activated_at = utc_now()
|
||||
await ensure_character_personality(session, character)
|
||||
await _audit_character(session, character, "character.activated")
|
||||
await session.commit()
|
||||
return ActivateCharacterResponse(active_character_id=character.id, status="active")
|
||||
@@ -599,7 +541,6 @@ async def resume_character(
|
||||
await _deactivate_others(session, character, owner_scope)
|
||||
character.status = "active"
|
||||
character.activated_at = utc_now()
|
||||
await ensure_character_personality(session, character)
|
||||
await _audit_character(session, character, "character.resumed")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
"""内驱力审核服务(次生目标 approve → 自动创建太一目标)。
|
||||
|
||||
审核 approve 时自动派生 Goal(太一枢任务级目标),
|
||||
延后 / 驳回仅标记不变更目标树。
|
||||
"""
|
||||
|
||||
from app.core.errors import AppError
|
||||
from app.core.ids import new_goal_id
|
||||
from app.core.time import utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.drive import CuriosityQuestion, ExploratorySubgoal
|
||||
from app.models.governance import Goal
|
||||
from app.repositories import drive as drive_repo
|
||||
from app.repositories.json_utils import loads_list
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.drive import CuriosityResponse, SubgoalResponse
|
||||
from app.services.audit import record_audit_event
|
||||
|
||||
|
||||
async def list_curiosities(
|
||||
*,
|
||||
character_id: str,
|
||||
source: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> Page[CuriosityResponse]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
items, total = await drive_repo.list_curiosities(
|
||||
session, character_id, source=source, page=page, page_size=page_size
|
||||
)
|
||||
return Page(
|
||||
items=[_curiosity_to_response(i) for i in items],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
async def list_subgoals(
|
||||
*,
|
||||
character_id: str,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> Page[SubgoalResponse]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
items, total = await drive_repo.list_subgoals(
|
||||
session, character_id, status=status, page=page, page_size=page_size
|
||||
)
|
||||
return Page(
|
||||
items=[_subgoal_to_response(i) for i in items],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
async def approve_subgoal(subgoal_id: str) -> SubgoalResponse:
|
||||
return await _review_subgoal(subgoal_id, "approved", decision="auto", create_goal=True)
|
||||
|
||||
|
||||
async def defer_subgoal(subgoal_id: str) -> SubgoalResponse:
|
||||
return await _review_subgoal(subgoal_id, "deferred", decision="auto", create_goal=False)
|
||||
|
||||
|
||||
async def reject_subgoal(subgoal_id: str) -> SubgoalResponse:
|
||||
return await _review_subgoal(subgoal_id, "rejected", decision="denied", create_goal=False)
|
||||
|
||||
|
||||
async def _review_subgoal(
|
||||
subgoal_id: str, target_status: str, *, decision: str, create_goal: bool
|
||||
) -> SubgoalResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
sub = await drive_repo.get_subgoal(session, subgoal_id)
|
||||
if sub is None:
|
||||
raise AppError("not_found", "探索性次生目标不存在。")
|
||||
if sub.status != "candidate":
|
||||
raise AppError("conflict", f"次生目标当前状态为 {sub.status},不可审核。")
|
||||
sub.status = target_status
|
||||
sub.reviewed_at = utc_now()
|
||||
sub.reviewed_by = "user"
|
||||
|
||||
# approve 时自动创建太一任务级目标
|
||||
if create_goal and sub.character_id:
|
||||
goal = Goal(
|
||||
id=new_goal_id(),
|
||||
character_id=sub.character_id,
|
||||
title=sub.title,
|
||||
description=sub.description or "由次生目标自动生成",
|
||||
level="task",
|
||||
type="exploration",
|
||||
status="active",
|
||||
)
|
||||
session.add(goal)
|
||||
|
||||
await record_audit_event(
|
||||
session,
|
||||
action=f"drive.subgoal.{target_status}",
|
||||
actor="user",
|
||||
risk_level=sub.risk_level,
|
||||
decision=decision,
|
||||
character_id=sub.character_id,
|
||||
input_summary=sub.title[:200],
|
||||
resource_type="exploratory_subgoal",
|
||||
resource_id=sub.id,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(sub)
|
||||
return _subgoal_to_response(sub)
|
||||
|
||||
|
||||
def _curiosity_to_response(c: CuriosityQuestion) -> CuriosityResponse:
|
||||
return CuriosityResponse(
|
||||
id=c.id,
|
||||
character_id=c.character_id,
|
||||
cycle_id=c.cycle_id,
|
||||
question=c.question,
|
||||
source=c.source,
|
||||
related_entities=loads_list(c.related_entities_json),
|
||||
expected_value=c.expected_value,
|
||||
urgency=c.urgency,
|
||||
risk_level=c.risk_level,
|
||||
created_at=c.created_at,
|
||||
)
|
||||
|
||||
|
||||
def _subgoal_to_response(s: ExploratorySubgoal) -> SubgoalResponse:
|
||||
return SubgoalResponse(
|
||||
id=s.id,
|
||||
character_id=s.character_id,
|
||||
cycle_id=s.cycle_id,
|
||||
parent_goal_id=s.parent_goal_id,
|
||||
curiosity_question_id=s.curiosity_question_id,
|
||||
title=s.title,
|
||||
description=s.description,
|
||||
expected_information_gain=s.expected_information_gain,
|
||||
interruption_cost=s.interruption_cost,
|
||||
risk_level=s.risk_level,
|
||||
status=s.status,
|
||||
reviewed_at=s.reviewed_at,
|
||||
reviewed_by=s.reviewed_by,
|
||||
created_at=s.created_at,
|
||||
)
|
||||
@@ -14,7 +14,6 @@ from app.models.governance import (
|
||||
Goal,
|
||||
PerceptionChannel,
|
||||
Plan,
|
||||
WorldEntity,
|
||||
)
|
||||
from app.modules.runtime.events import Event
|
||||
from app.repositories import governance as governance_repo
|
||||
@@ -26,7 +25,6 @@ from app.schemas.admin import (
|
||||
CreateGoalRequest,
|
||||
CreatePerceptionChannelRequest,
|
||||
CreatePlanRequest,
|
||||
CreateWorldEntityRequest,
|
||||
GoalNode,
|
||||
PerceptionChannelResponse,
|
||||
PlanSummaryResponse,
|
||||
@@ -34,8 +32,6 @@ from app.schemas.admin import (
|
||||
UpdateGoalRequest,
|
||||
UpdatePerceptionChannelRequest,
|
||||
UpdatePlanRequest,
|
||||
UpdateWorldEntityRequest,
|
||||
WorldEntityResponse,
|
||||
)
|
||||
from app.schemas.safety import (
|
||||
ToolCallDetail,
|
||||
@@ -46,7 +42,6 @@ from app.services.serializers import (
|
||||
goal_to_response,
|
||||
perception_channel_to_response,
|
||||
plan_to_response,
|
||||
world_entity_to_response,
|
||||
)
|
||||
|
||||
# —— 太一枢 · 目标 ——
|
||||
@@ -281,76 +276,8 @@ async def delete_perception_channel(channel_id: str) -> None:
|
||||
# —— 河图枢 · 世界实体 ——
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
# —— 洛书枢 · 计划 ——
|
||||
|
||||
@@ -1,432 +0,0 @@
|
||||
"""人格服务。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError
|
||||
from app.core.time import utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.character import Character
|
||||
from app.models.personality import PersonalityProposal, PersonalityVersion
|
||||
from app.repositories import personality as personality_repo
|
||||
from app.repositories.json_utils import dumps, loads_dict
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.personality import (
|
||||
PersonalityProposalFields,
|
||||
PersonalityProposalResponse,
|
||||
PersonalityResponse,
|
||||
)
|
||||
from app.services.audit import record_audit_event
|
||||
from app.services.characters import get_active_character_model
|
||||
from app.services.serializers import personality_to_response, proposal_to_response
|
||||
|
||||
|
||||
async def get_current_personality(
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> PersonalityResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_active_character_model(session, owner_scope)
|
||||
personality = None
|
||||
if character.current_personality_version_id:
|
||||
personality = await personality_repo.get_personality(
|
||||
session, character.current_personality_version_id
|
||||
)
|
||||
if personality is None:
|
||||
personality = await personality_repo.get_latest_personality(session, character.id)
|
||||
if personality is None:
|
||||
raise AppError("not_found", "未找到当前人格版本。")
|
||||
return personality_to_response(personality)
|
||||
|
||||
|
||||
async def list_personality_versions(
|
||||
*,
|
||||
character_id: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> Page[PersonalityResponse]:
|
||||
"""人格版本历史(按版本号倒序);character_id 省略时取当前激活人物。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cid = character_id
|
||||
if cid is None:
|
||||
character = await get_active_character_model(session, owner_scope)
|
||||
cid = character.id
|
||||
items, total = await personality_repo.list_personality_versions(
|
||||
session, cid, page=page, page_size=page_size
|
||||
)
|
||||
return Page(
|
||||
items=[personality_to_response(item) for item in items],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
async def list_personality_proposals(
|
||||
*,
|
||||
character_id: str | None = None,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> Page[PersonalityProposalResponse]:
|
||||
"""人格修改建议列表(按创建时间倒序);character_id 省略取当前激活人物。待审审批墙数据源。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cid = character_id
|
||||
if cid is None:
|
||||
cid = (await get_active_character_model(session, owner_scope)).id
|
||||
items, total = await personality_repo.list_proposals(
|
||||
session, character_id=cid, status=status, page=page, page_size=page_size
|
||||
)
|
||||
return Page(
|
||||
items=[proposal_to_response(item) for item in items],
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
class PersonalitySummary(BaseModel):
|
||||
"""人格摘要(注入 RuntimeContract 上下文)。"""
|
||||
|
||||
text: str | None = None
|
||||
|
||||
|
||||
async def build_personality_summary(
|
||||
session: AsyncSession,
|
||||
character: Character,
|
||||
) -> PersonalitySummary:
|
||||
"""组装当前人格摘要文本;无人格时返回空。"""
|
||||
personality = None
|
||||
if character.current_personality_version_id:
|
||||
personality = await personality_repo.get_personality(
|
||||
session, character.current_personality_version_id
|
||||
)
|
||||
if personality is None:
|
||||
personality = await personality_repo.get_latest_personality(session, character.id)
|
||||
if personality is None:
|
||||
return PersonalitySummary()
|
||||
text = "\n".join(
|
||||
item
|
||||
for item in [
|
||||
f"身份:{personality.identity_profile}",
|
||||
f"角色使命:{personality.role_destiny}",
|
||||
f"性格种子:{personality.personality_seed}",
|
||||
f"表达风格:{personality.expression_style}",
|
||||
f"关系模式:{personality.relationship_pattern}",
|
||||
]
|
||||
if item and not item.endswith(":")
|
||||
)
|
||||
return PersonalitySummary(text=text)
|
||||
|
||||
|
||||
async def create_default_personality(
|
||||
session: AsyncSession, character_id: str
|
||||
) -> PersonalityVersion:
|
||||
"""创建通用默认人格版本(无 profile 数据时的兜底)。"""
|
||||
personality = PersonalityVersion(
|
||||
character_id=character_id,
|
||||
version=1,
|
||||
identity_profile="魂镜是一个本地运行的数字生命雏形,当前专注于文本对话、记忆和自我描述。",
|
||||
role_destiny="作为用户的长期陪伴与认知镜像,先从稳定、可信、可审计的对话开始。",
|
||||
personality_seed="温和、诚实、克制、重视隐私与事实边界。",
|
||||
traits=dumps({"warmth": 0.8, "curiosity": 0.6, "caution": 0.7}),
|
||||
expression_style="使用中文,表达清晰,避免夸大能力。",
|
||||
values=dumps({"privacy": "high", "truthfulness": "high", "companionship": "medium"}),
|
||||
relationship_pattern="尊重用户边界,以可解释和可撤回的方式建立长期记忆。",
|
||||
emotion_baseline=dumps({"mood": "neutral", "intensity": 0.2}),
|
||||
reason="system_seed",
|
||||
evidence_ids_json=dumps([]),
|
||||
created_by="system",
|
||||
)
|
||||
session.add(personality)
|
||||
await session.flush()
|
||||
return personality
|
||||
|
||||
|
||||
async def create_personality_from_profile(
|
||||
session: AsyncSession, character: Character
|
||||
) -> PersonalityVersion:
|
||||
"""从人物档案(profile_json)生成个性化人格版本;profile 为空时回退到默认人格。
|
||||
|
||||
映射规则(profile 字段 → PersonalityVersion 字段):
|
||||
- description + background → identity_profile
|
||||
- speaking_style → expression_style
|
||||
- temperament → personality_seed
|
||||
- values → values
|
||||
- hobbies → 参与关系模式描述的构建
|
||||
"""
|
||||
profile = loads_dict(character.profile_json) if character.profile_json else {}
|
||||
|
||||
if not profile:
|
||||
return await create_default_personality(session, character.id)
|
||||
|
||||
# 身份画像:description + background
|
||||
description = profile.get("description", "")
|
||||
background = profile.get("background", "")
|
||||
identity_parts = [p for p in [description, background] if p]
|
||||
identity_profile = (
|
||||
identity_parts[0]
|
||||
if len(identity_parts) == 1
|
||||
else ("。".join(identity_parts) if identity_parts else "魂镜默认人物。")
|
||||
)
|
||||
|
||||
# 表达风格
|
||||
expression_style = profile.get("speaking_style", "使用中文,表达清晰。")
|
||||
|
||||
# 性格种子
|
||||
personality_seed = profile.get("temperament", "温和、诚实、克制。")
|
||||
|
||||
# 价值偏好
|
||||
values_list = profile.get("values", [])
|
||||
values_dict: dict[str, Any] = (
|
||||
{"values": list(values_list)}
|
||||
if values_list
|
||||
else {"privacy": "high", "truthfulness": "high", "companionship": "medium"}
|
||||
)
|
||||
|
||||
# 关系模式:若有兴趣爱好则融入描述
|
||||
hobbies = profile.get("hobbies", [])
|
||||
if hobbies and isinstance(hobbies, list) and len(hobbies) > 0:
|
||||
hobby_text = "、".join(str(h) for h in hobbies[:3])
|
||||
relationship_pattern = f"乐于分享关于{hobby_text}等的见解,以开放与尊重建立对话。"
|
||||
else:
|
||||
relationship_pattern = "以开放与尊重建立关系。"
|
||||
|
||||
# 角色使命
|
||||
role_destiny = f"{description},陪伴用户探索与成长。" if description else "陪伴用户探索与成长。"
|
||||
|
||||
personality = PersonalityVersion(
|
||||
character_id=character.id,
|
||||
version=1,
|
||||
identity_profile=identity_profile,
|
||||
role_destiny=role_destiny,
|
||||
personality_seed=personality_seed,
|
||||
traits=dumps({"warmth": 0.8, "curiosity": 0.6, "caution": 0.7}),
|
||||
expression_style=expression_style,
|
||||
values=dumps(values_dict),
|
||||
relationship_pattern=relationship_pattern,
|
||||
emotion_baseline=dumps({"mood": "neutral", "intensity": 0.2}),
|
||||
reason="system_seed",
|
||||
evidence_ids_json=dumps([]),
|
||||
created_by="system",
|
||||
)
|
||||
session.add(personality)
|
||||
await session.flush()
|
||||
return personality
|
||||
|
||||
|
||||
async def copy_personality_to_character(
|
||||
session: AsyncSession,
|
||||
template_personality: PersonalityVersion,
|
||||
target_character_id: str,
|
||||
) -> PersonalityVersion:
|
||||
personality = PersonalityVersion(
|
||||
character_id=target_character_id,
|
||||
version=1,
|
||||
identity_profile=template_personality.identity_profile,
|
||||
role_destiny=template_personality.role_destiny,
|
||||
personality_seed=template_personality.personality_seed,
|
||||
traits=template_personality.traits,
|
||||
expression_style=template_personality.expression_style,
|
||||
values=template_personality.values,
|
||||
relationship_pattern=template_personality.relationship_pattern,
|
||||
emotion_baseline=template_personality.emotion_baseline,
|
||||
reason="system_template_copy",
|
||||
evidence_ids_json=template_personality.evidence_ids_json,
|
||||
created_by="system",
|
||||
)
|
||||
session.add(personality)
|
||||
await session.flush()
|
||||
return personality
|
||||
|
||||
|
||||
# —— 人格修改建议与版本回滚(api-spec §11) ——
|
||||
#
|
||||
# 本地单用户边界下「手动编辑视为用户确认」(§5.1),但人格属核心资源(L4),
|
||||
# 仍走「提议 pending → 审核」两步:propose 落 pending 建议,approve 派生新版本,
|
||||
# reject 仅留痕;rollback 切换当前指针。所有写操作记审计并入起居注。
|
||||
|
||||
|
||||
async def propose_personality_update(
|
||||
*,
|
||||
character_id: str | None,
|
||||
fields: PersonalityProposalFields,
|
||||
reason: str,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> PersonalityProposalResponse:
|
||||
"""提交人格修改建议;落 pending 提议并记审计,不直接写入人格版本。"""
|
||||
if not fields.has_any():
|
||||
raise AppError("invalid_request", "fields 至少需要覆盖一个人格字段。")
|
||||
async with AsyncSessionLocal() as session:
|
||||
cid = character_id
|
||||
if cid is None:
|
||||
cid = (await get_active_character_model(session, owner_scope)).id
|
||||
proposal = PersonalityProposal(
|
||||
character_id=cid,
|
||||
fields_json=dumps(fields.model_dump(exclude_none=True)),
|
||||
reason=reason,
|
||||
status="pending",
|
||||
)
|
||||
await personality_repo.add_proposal(session, proposal)
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="personality.propose",
|
||||
actor="user",
|
||||
risk_level="L4",
|
||||
character_id=cid,
|
||||
input_summary=reason,
|
||||
resource_type="personality_proposal",
|
||||
resource_id=proposal.id,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(proposal)
|
||||
return proposal_to_response(proposal)
|
||||
|
||||
|
||||
async def approve_personality_proposal(
|
||||
proposal_id: str,
|
||||
owner_scope: str = settings.default_owner_scope, # noqa: ARG001
|
||||
) -> PersonalityProposalResponse:
|
||||
"""审核通过人格建议:基于当前最新版本派生新版本并切换人物当前指针。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
proposal = await personality_repo.get_proposal(session, proposal_id)
|
||||
if proposal is None:
|
||||
raise AppError("not_found", "人格建议不存在。")
|
||||
if proposal.status != "pending":
|
||||
raise AppError("conflict", f"人格建议当前状态为 {proposal.status},不可审核。")
|
||||
base = await personality_repo.get_latest_personality(session, proposal.character_id)
|
||||
if base is None:
|
||||
raise AppError("invalid_request", "人物尚无人格版本,无法应用建议。")
|
||||
new_version = derive_personality_version(
|
||||
base, loads_dict(proposal.fields_json), reason=f"proposal:{proposal.id}"
|
||||
)
|
||||
session.add(new_version)
|
||||
await session.flush()
|
||||
character = await session.get(Character, proposal.character_id)
|
||||
if character is None:
|
||||
raise AppError("not_found", "人物不存在。")
|
||||
character.current_personality_version_id = new_version.id
|
||||
proposal.status = "applied"
|
||||
proposal.resulting_version_id = new_version.id
|
||||
proposal.reviewed_at = utc_now()
|
||||
proposal.reviewed_by = "user"
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="personality.proposal.approved",
|
||||
actor="user",
|
||||
risk_level="L4",
|
||||
decision="approved",
|
||||
character_id=proposal.character_id,
|
||||
input_summary=proposal.reason,
|
||||
output_summary=f"已生成版本 v{new_version.version}",
|
||||
resource_type="personality_proposal",
|
||||
resource_id=proposal.id,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(proposal)
|
||||
return proposal_to_response(proposal)
|
||||
|
||||
|
||||
async def reject_personality_proposal(
|
||||
proposal_id: str,
|
||||
owner_scope: str = settings.default_owner_scope, # noqa: ARG001
|
||||
) -> PersonalityProposalResponse:
|
||||
"""驳回人格建议;标记 rejected 并记审计,不生成新版本。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
proposal = await personality_repo.get_proposal(session, proposal_id)
|
||||
if proposal is None:
|
||||
raise AppError("not_found", "人格建议不存在。")
|
||||
if proposal.status != "pending":
|
||||
raise AppError("conflict", f"人格建议当前状态为 {proposal.status},不可审核。")
|
||||
proposal.status = "rejected"
|
||||
proposal.reviewed_at = utc_now()
|
||||
proposal.reviewed_by = "user"
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="personality.proposal.rejected",
|
||||
actor="user",
|
||||
risk_level="L4",
|
||||
decision="denied",
|
||||
character_id=proposal.character_id,
|
||||
input_summary=proposal.reason,
|
||||
resource_type="personality_proposal",
|
||||
resource_id=proposal.id,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(proposal)
|
||||
return proposal_to_response(proposal)
|
||||
|
||||
|
||||
async def rollback_personality(
|
||||
*,
|
||||
character_id: str | None,
|
||||
version_id: str,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> PersonalityResponse:
|
||||
"""回滚到指定人格版本:切换人物当前指针,标记被取代的旧版本 rolled_back_at。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
cid = character_id
|
||||
if cid is None:
|
||||
cid = (await get_active_character_model(session, owner_scope)).id
|
||||
target = await personality_repo.get_personality(session, version_id)
|
||||
if target is None or target.character_id != cid:
|
||||
raise AppError("not_found", "人格版本不存在或不属于当前人物。")
|
||||
character = await session.get(Character, cid)
|
||||
if character is None:
|
||||
raise AppError("not_found", "人物不存在。")
|
||||
previous_id = character.current_personality_version_id
|
||||
character.current_personality_version_id = target.id
|
||||
if previous_id and previous_id != target.id:
|
||||
previous = await personality_repo.get_personality(session, previous_id)
|
||||
if previous is not None:
|
||||
previous.rolled_back_at = utc_now()
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="personality.rollback",
|
||||
actor="user",
|
||||
risk_level="L4",
|
||||
decision="approved",
|
||||
character_id=cid,
|
||||
input_summary=f"{previous_id} → {target.id}",
|
||||
resource_type="personality_version",
|
||||
resource_id=target.id,
|
||||
)
|
||||
await session.commit()
|
||||
return personality_to_response(target)
|
||||
|
||||
|
||||
def derive_personality_version(
|
||||
base: PersonalityVersion,
|
||||
overrides: dict[str, Any],
|
||||
*,
|
||||
reason: str,
|
||||
created_by: str = "user",
|
||||
) -> PersonalityVersion:
|
||||
"""基于 base 版本与字段覆盖派生新版本;JSON 字段按 dict 序列化落库。
|
||||
|
||||
公开供人格提议审核与大周天演化派生复用(见 services.evolution)。
|
||||
"""
|
||||
kwargs: dict[str, Any] = {
|
||||
"character_id": base.character_id,
|
||||
"version": base.version + 1,
|
||||
"reason": reason,
|
||||
"evidence_ids_json": base.evidence_ids_json,
|
||||
"created_by": created_by,
|
||||
}
|
||||
for field in (
|
||||
"identity_profile",
|
||||
"role_destiny",
|
||||
"personality_seed",
|
||||
"expression_style",
|
||||
"relationship_pattern",
|
||||
):
|
||||
kwargs[field] = overrides.get(field, getattr(base, field))
|
||||
for field in ("traits", "values", "emotion_baseline"):
|
||||
kwargs[field] = dumps(overrides[field]) if field in overrides else getattr(base, field)
|
||||
return PersonalityVersion(**kwargs)
|
||||
@@ -12,13 +12,10 @@ from app.models.governance import (
|
||||
Goal,
|
||||
PerceptionChannel,
|
||||
Plan,
|
||||
WorldEntity,
|
||||
)
|
||||
from app.models.imports import CharacterImportJob, ImportItem
|
||||
from app.models.personality import PersonalityProposal, PersonalityVersion
|
||||
from app.models.runtime import Event, Run
|
||||
from app.models.safety import AuditEvent
|
||||
from app.models.world import CharacterWorldEntity, WorldRelation
|
||||
from app.repositories.json_utils import loads_dict, loads_list
|
||||
from app.schemas.admin import (
|
||||
AgentRosterResponse,
|
||||
@@ -26,20 +23,12 @@ from app.schemas.admin import (
|
||||
GoalNode,
|
||||
PerceptionChannelResponse,
|
||||
PlanSummaryResponse,
|
||||
WorldEntityResponse,
|
||||
)
|
||||
from app.schemas.candidates import MemoryCandidateResponse, ProfileCandidateResponse
|
||||
from app.schemas.character import CharacterResponse, CharacterSummaryResponse
|
||||
from app.schemas.imports import ImportItemResponse, ImportJobResponse
|
||||
from app.schemas.personality import PersonalityProposalResponse, PersonalityResponse
|
||||
from app.schemas.preferences import GenerationPreferencesResponse
|
||||
from app.schemas.runtime import EventResponse, RunResponse
|
||||
from app.schemas.world import (
|
||||
WorldEntityResponse as CharacterWorldEntityResponse,
|
||||
)
|
||||
from app.schemas.world import (
|
||||
WorldRelationResponse,
|
||||
)
|
||||
|
||||
|
||||
def _check_avatar(character_id: str) -> str | None:
|
||||
@@ -60,7 +49,6 @@ def character_to_response(character: Character) -> CharacterResponse:
|
||||
language=character.language,
|
||||
profile=loads_dict(character.profile_json),
|
||||
model_policy=loads_dict(character.model_policy_json),
|
||||
current_personality_version_id=character.current_personality_version_id,
|
||||
privacy_level=character.privacy_level,
|
||||
owner_scope=character.owner_scope,
|
||||
is_system_template=character.is_system_template,
|
||||
@@ -86,37 +74,6 @@ def character_to_summary(
|
||||
)
|
||||
|
||||
|
||||
def personality_to_response(personality: PersonalityVersion) -> PersonalityResponse:
|
||||
return PersonalityResponse(
|
||||
id=personality.id,
|
||||
character_id=personality.character_id,
|
||||
version=personality.version,
|
||||
identity_profile=personality.identity_profile,
|
||||
role_destiny=personality.role_destiny,
|
||||
personality_seed=personality.personality_seed,
|
||||
traits=loads_dict(personality.traits),
|
||||
expression_style=personality.expression_style,
|
||||
values=loads_dict(personality.values),
|
||||
relationship_pattern=personality.relationship_pattern,
|
||||
emotion_baseline=loads_dict(personality.emotion_baseline),
|
||||
reason=personality.reason,
|
||||
evidence_ids=loads_list(personality.evidence_ids_json),
|
||||
created_by=personality.created_by,
|
||||
created_at=to_iso(personality.created_at) or "",
|
||||
)
|
||||
|
||||
|
||||
def proposal_to_response(proposal: PersonalityProposal) -> PersonalityProposalResponse:
|
||||
return PersonalityProposalResponse(
|
||||
proposal_id=proposal.id,
|
||||
character_id=proposal.character_id,
|
||||
status=proposal.status,
|
||||
reason=proposal.reason,
|
||||
fields=loads_dict(proposal.fields_json),
|
||||
resulting_version_id=proposal.resulting_version_id,
|
||||
reviewed_at=to_iso(proposal.reviewed_at),
|
||||
created_at=to_iso(proposal.created_at) or "",
|
||||
)
|
||||
|
||||
|
||||
def run_to_response(run: Run) -> RunResponse:
|
||||
@@ -186,51 +143,6 @@ def perception_channel_to_response(channel: PerceptionChannel) -> PerceptionChan
|
||||
)
|
||||
|
||||
|
||||
def world_entity_to_response(entity: WorldEntity) -> WorldEntityResponse:
|
||||
return WorldEntityResponse(
|
||||
id=entity.id,
|
||||
type=entity.type,
|
||||
name=entity.name,
|
||||
attributes=loads_dict(entity.attributes_json),
|
||||
state_type=entity.state_type,
|
||||
confidence=entity.confidence,
|
||||
sources=loads_list(entity.sources_json),
|
||||
updated_at=to_iso(entity.updated_at),
|
||||
)
|
||||
|
||||
|
||||
def character_world_entity_to_response(
|
||||
entity: CharacterWorldEntity,
|
||||
) -> CharacterWorldEntityResponse:
|
||||
"""人物运行时世界实体(api-spec §12);与治理版 WorldEntity 按 character_id 隔离。"""
|
||||
return CharacterWorldEntityResponse(
|
||||
id=entity.id,
|
||||
character_id=entity.character_id,
|
||||
type=entity.type,
|
||||
name=entity.name,
|
||||
attributes=loads_dict(entity.attributes_json),
|
||||
state_type=entity.state_type,
|
||||
status=entity.status,
|
||||
confidence=entity.confidence,
|
||||
evidence_ids=loads_list(entity.evidence_ids_json),
|
||||
sources=loads_list(entity.sources_json),
|
||||
created_at=to_iso(entity.created_at) or "",
|
||||
updated_at=to_iso(entity.updated_at) or "",
|
||||
)
|
||||
|
||||
|
||||
def world_relation_to_response(relation: WorldRelation) -> WorldRelationResponse:
|
||||
return WorldRelationResponse(
|
||||
id=relation.id,
|
||||
character_id=relation.character_id,
|
||||
subject_id=relation.subject_id,
|
||||
predicate=relation.predicate,
|
||||
object_id=relation.object_id,
|
||||
confidence=relation.confidence,
|
||||
evidence_ids=loads_list(relation.evidence_ids_json),
|
||||
created_at=to_iso(relation.created_at) or "",
|
||||
updated_at=to_iso(relation.updated_at) or "",
|
||||
)
|
||||
|
||||
|
||||
def plan_to_response(plan: Plan) -> PlanSummaryResponse:
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
"""世界模型服务(河图枢运行时,api-spec §12)。
|
||||
|
||||
按人物视角隔离:所有读写经 ``resolve_active_character`` 绑定当前激活人物。
|
||||
自动抽取(世界引擎)属 v1 半自动留后项,本层先承载手动 CRUD。
|
||||
"""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.world import CharacterWorldEntity, WorldRelation
|
||||
from app.repositories import world as world_repo
|
||||
from app.repositories.json_utils import dumps
|
||||
from app.schemas.common import Page
|
||||
from app.schemas.world import (
|
||||
WorldEntityCreateRequest,
|
||||
WorldEntityResponse,
|
||||
WorldGraphResponse,
|
||||
WorldRelationCreateRequest,
|
||||
WorldRelationResponse,
|
||||
)
|
||||
from app.services.audit import record_audit_event
|
||||
from app.services.characters import resolve_active_character
|
||||
from app.services.serializers import (
|
||||
character_world_entity_to_response,
|
||||
world_relation_to_response,
|
||||
)
|
||||
|
||||
|
||||
async def list_entities_page(
|
||||
character_id: str | None = None,
|
||||
*,
|
||||
entity_type: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> Page[WorldEntityResponse]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await resolve_active_character(session, character_id, owner_scope=owner_scope)
|
||||
safe_page = max(page, 1)
|
||||
safe_size = min(max(page_size, 1), 100)
|
||||
items, total = await world_repo.list_entities(
|
||||
session,
|
||||
character.id,
|
||||
entity_type=entity_type,
|
||||
page=safe_page,
|
||||
page_size=safe_size,
|
||||
)
|
||||
return Page(
|
||||
items=[character_world_entity_to_response(item) for item in items],
|
||||
page=safe_page,
|
||||
page_size=safe_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
async def get_entity_by_id(
|
||||
entity_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> WorldEntityResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await resolve_active_character(session, None, owner_scope=owner_scope)
|
||||
entity = await world_repo.get_entity(session, entity_id)
|
||||
if entity is None or entity.character_id != character.id:
|
||||
raise AppError("not_found", "世界实体不存在。")
|
||||
return character_world_entity_to_response(entity)
|
||||
|
||||
|
||||
async def create_entity(
|
||||
payload: WorldEntityCreateRequest, owner_scope: str = settings.default_owner_scope
|
||||
) -> WorldEntityResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await resolve_active_character(
|
||||
session, payload.character_id, owner_scope=owner_scope
|
||||
)
|
||||
entity = CharacterWorldEntity(
|
||||
character_id=character.id,
|
||||
type=payload.type,
|
||||
name=payload.name,
|
||||
attributes_json=dumps(payload.attributes),
|
||||
state_type=payload.state_type,
|
||||
confidence=payload.confidence,
|
||||
evidence_ids_json=dumps(payload.evidence_ids),
|
||||
sources_json=dumps(payload.sources),
|
||||
)
|
||||
await world_repo.add_entity(session, entity)
|
||||
await _audit(session, character.id, "world.entity.created", entity.name, entity.id)
|
||||
await session.commit()
|
||||
await session.refresh(entity)
|
||||
return character_world_entity_to_response(entity)
|
||||
|
||||
|
||||
async def list_relations_page(
|
||||
character_id: str | None = None,
|
||||
*,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> Page[WorldRelationResponse]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await resolve_active_character(session, character_id, owner_scope=owner_scope)
|
||||
safe_page = max(page, 1)
|
||||
safe_size = min(max(page_size, 1), 100)
|
||||
items, total = await world_repo.list_relations(
|
||||
session, character.id, page=safe_page, page_size=safe_size
|
||||
)
|
||||
return Page(
|
||||
items=[world_relation_to_response(item) for item in items],
|
||||
page=safe_page,
|
||||
page_size=safe_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
async def create_relation(
|
||||
payload: WorldRelationCreateRequest, owner_scope: str = settings.default_owner_scope
|
||||
) -> WorldRelationResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await resolve_active_character(
|
||||
session, payload.character_id, owner_scope=owner_scope
|
||||
)
|
||||
subject = await _ensure_owned_entity(session, character.id, payload.subject_id, "主体")
|
||||
obj = await _ensure_owned_entity(session, character.id, payload.object_id, "客体")
|
||||
relation = WorldRelation(
|
||||
character_id=character.id,
|
||||
subject_id=subject.id,
|
||||
predicate=payload.predicate,
|
||||
object_id=obj.id,
|
||||
confidence=payload.confidence,
|
||||
evidence_ids_json=dumps(payload.evidence_ids),
|
||||
)
|
||||
await world_repo.add_relation(session, relation)
|
||||
await _audit(
|
||||
session, character.id, "world.relation.created", relation.predicate, relation.id
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(relation)
|
||||
return world_relation_to_response(relation)
|
||||
|
||||
|
||||
async def delete_relation(
|
||||
relation_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> None:
|
||||
"""删除世界关系。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await resolve_active_character(session, None, owner_scope=owner_scope)
|
||||
relation = await world_repo.get_relation(session, relation_id)
|
||||
if relation is None or relation.character_id != character.id:
|
||||
raise AppError("not_found", "关系不存在。")
|
||||
await world_repo.delete_relation(session, relation_id)
|
||||
await _audit(
|
||||
session, character.id, "world.relation.deleted", relation.predicate, relation_id
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def get_graph(
|
||||
character_id: str | None = None,
|
||||
*,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> WorldGraphResponse:
|
||||
"""返回人物的完整世界图谱(无分页,所有节点与边),供前端渲染力导向图。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await resolve_active_character(session, character_id, owner_scope=owner_scope)
|
||||
entities = await world_repo.list_all_entities(session, character.id)
|
||||
relations = await world_repo.list_all_relations(session, character.id)
|
||||
return WorldGraphResponse(
|
||||
nodes=[character_world_entity_to_response(e) for e in entities],
|
||||
edges=[world_relation_to_response(r) for r in relations],
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_owned_entity(
|
||||
session: AsyncSession, character_id: str, entity_id: str, role: str
|
||||
) -> CharacterWorldEntity:
|
||||
entity = await world_repo.get_entity(session, entity_id)
|
||||
if entity is None or entity.character_id != character_id:
|
||||
raise AppError("not_found", f"关系{role}实体不存在。")
|
||||
return entity
|
||||
|
||||
|
||||
async def _audit(
|
||||
session: AsyncSession,
|
||||
character_id: str,
|
||||
action: str,
|
||||
summary: str,
|
||||
resource_id: str,
|
||||
) -> None:
|
||||
await record_audit_event(
|
||||
session,
|
||||
action=action,
|
||||
character_id=character_id,
|
||||
actor="user",
|
||||
decision="auto",
|
||||
input_summary=summary,
|
||||
resource_type="world_entity" if "entity" in action else "world_relation",
|
||||
resource_id=resource_id,
|
||||
)
|
||||
Reference in new issue
Block a user