- 缘契域 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 干净
213 lines
8.9 KiB
Python
213 lines
8.9 KiB
Python
"""道体铸魂与分灵(identity-continuity.md §3)。
|
||
|
||
- 道体锚点 = characters.identity_baseline_json(M1 已有):最小稳定身份锚点,
|
||
日常对话/模型输出/普通工具不可静默覆盖——修改必须走铸魂确认流;
|
||
- 调节层 = 息壤 persona 记忆(M3):语气/称谓/兴趣等渐进演进,演进有证据
|
||
(source_refs 指向归真),本模块不重复建设;
|
||
- 铸魂(soul_forgings):重大锚点变更 pending → confirm 后应用并签发新魂籍版本;
|
||
- 分灵(split):连续性无法维持时创建新人物——继承资料与谱系、不继承原身份。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import select
|
||
|
||
from app.core.errors import AppError
|
||
from app.core.time import utc_now
|
||
from app.db.session import AsyncSessionLocal
|
||
from app.core.ids import new_character_id
|
||
from app.domains.jingsheng.identity import build_identity_record
|
||
from app.domains import one_qi
|
||
from app.models.character import Character
|
||
from app.models.jingsheng import IdentityRecord, SoulForging
|
||
|
||
FORGING_FIELDS = ("identity_baseline_json",)
|
||
|
||
|
||
async def propose_forging(
|
||
*, character_id: str, owner_scope: str, anchor_changes: dict, reason: str
|
||
) -> dict:
|
||
"""发起铸魂:锚点变更提案(pending),确认前人物锚点不变。"""
|
||
character = await _owned(character_id, owner_scope)
|
||
if not anchor_changes:
|
||
raise AppError("invalid_request", "锚点变更不能为空。")
|
||
unknown = set(anchor_changes) - set(FORGING_FIELDS)
|
||
if unknown:
|
||
raise AppError("invalid_request", f"不可铸魂字段:{sorted(unknown)}(仅道体锚点)。")
|
||
async with AsyncSessionLocal() as session:
|
||
pending = (
|
||
await session.execute(
|
||
select(SoulForging).where(
|
||
SoulForging.character_id == character_id, SoulForging.status == "pending"
|
||
)
|
||
)
|
||
).scalars().first()
|
||
if pending is not None:
|
||
raise AppError("invalid_request", "已有待确认铸魂。", status_code=409)
|
||
forging = SoulForging(
|
||
character_id=character_id,
|
||
proposed_anchor_json=json.dumps(anchor_changes["identity_baseline_json"], ensure_ascii=False),
|
||
reason=reason,
|
||
status="pending",
|
||
)
|
||
session.add(forging)
|
||
await session.commit()
|
||
session.expunge(forging)
|
||
return _forging_dict(forging)
|
||
|
||
|
||
async def issue_record(character_id: str, owner_scope: str) -> dict:
|
||
"""签发下一版魂籍(版本 = 当前最大版本 + 1)。"""
|
||
await _owned(character_id, owner_scope)
|
||
async with AsyncSessionLocal() as session:
|
||
character = await session.get(Character, character_id)
|
||
latest = (
|
||
await session.execute(
|
||
select(IdentityRecord.version)
|
||
.where(IdentityRecord.character_id == character_id)
|
||
.order_by(IdentityRecord.version.desc())
|
||
.limit(1)
|
||
)
|
||
).scalar_one_or_none()
|
||
version = (latest or 0) + 1
|
||
record = build_identity_record(
|
||
character_id=character_id,
|
||
version=version,
|
||
identity={
|
||
"name": character.name,
|
||
"identity_baseline": json.loads(character.identity_baseline_json or "{}"),
|
||
},
|
||
runtime_state={"status": character.status},
|
||
)
|
||
session.add(record)
|
||
await session.commit()
|
||
return {"id": record.id, "version": version}
|
||
|
||
|
||
async def confirm_forging(forging_id: str, owner_scope: str) -> dict:
|
||
"""确认铸魂:应用锚点变更 + 签发新魂籍版本(identity_record +1)。"""
|
||
async with AsyncSessionLocal() as session:
|
||
forging = await session.get(SoulForging, forging_id)
|
||
if forging is None:
|
||
raise AppError("not_found", "铸魂提案不存在。", status_code=404)
|
||
character = await session.get(Character, forging.character_id)
|
||
if character is None or character.owner_scope != owner_scope:
|
||
raise AppError("not_found", "铸魂提案不存在。", status_code=404)
|
||
if forging.status != "pending":
|
||
raise AppError("invalid_request", "铸魂已处理。", status_code=409)
|
||
new_anchor = forging.proposed_anchor_json
|
||
forging.status = "confirmed"
|
||
forging.confirmed_at = utc_now()
|
||
character.identity_baseline_json = new_anchor
|
||
await session.commit()
|
||
forging_id_out, character_id = forging.id, forging.character_id
|
||
await one_qi.emit(
|
||
"soul.forged", character_id=character_id,
|
||
payload={"forging_id": forging_id_out}, source="jingsheng",
|
||
)
|
||
# 新魂籍版本(铸魂必须可追溯到版本链)
|
||
record = await issue_record(character_id, owner_scope)
|
||
return {"forging_id": forging_id_out, "status": "confirmed", "identity_record_version": record["version"]}
|
||
|
||
|
||
async def reject_forging(forging_id: str, owner_scope: str) -> dict:
|
||
async with AsyncSessionLocal() as session:
|
||
forging = await session.get(SoulForging, forging_id)
|
||
if forging is None:
|
||
raise AppError("not_found", "铸魂提案不存在。", status_code=404)
|
||
character = await session.get(Character, forging.character_id)
|
||
if character is None or character.owner_scope != owner_scope:
|
||
raise AppError("not_found", "铸魂提案不存在。", status_code=404)
|
||
if forging.status != "pending":
|
||
raise AppError("invalid_request", "铸魂已处理。", status_code=409)
|
||
forging.status = "rejected"
|
||
await session.commit()
|
||
return {"forging_id": forging_id, "status": "rejected"}
|
||
|
||
|
||
async def list_forgings(character_id: str, owner_scope: str) -> list[dict]:
|
||
await _owned(character_id, owner_scope)
|
||
async with AsyncSessionLocal() as session:
|
||
rows = (
|
||
await session.execute(
|
||
select(SoulForging)
|
||
.where(SoulForging.character_id == character_id)
|
||
.order_by(SoulForging.created_at.desc())
|
||
)
|
||
).scalars().all()
|
||
return [_forging_dict(r) for r in rows]
|
||
|
||
|
||
async def split_soul(
|
||
*, character_id: str, owner_scope: str, reason: str, initial_anchor: dict
|
||
) -> dict:
|
||
"""分灵:以新身份创建新人物,继承资料与谱系、不继承原身份。
|
||
|
||
谱系(lineage):新人物锚点携带 split_from 链(源人物 id 与代数);
|
||
新人物 id 必然不同、魂籍独立从 v1 起。
|
||
"""
|
||
source = await _owned(character_id, owner_scope)
|
||
prior_lineage = json.loads(source.identity_baseline_json or "{}").get("lineage", [])
|
||
anchor = dict(initial_anchor)
|
||
anchor["lineage"] = [*prior_lineage, {"split_from": character_id, "at": utc_now().isoformat()}]
|
||
async with AsyncSessionLocal() as session:
|
||
new_character = Character(
|
||
id=new_character_id(),
|
||
owner_scope=owner_scope,
|
||
name=f"{source.name}·分灵",
|
||
identity_baseline_json=json.dumps(anchor, ensure_ascii=False),
|
||
)
|
||
session.add(new_character)
|
||
await session.commit()
|
||
new_id = new_character.id
|
||
await one_qi.emit(
|
||
"soul.split", character_id=None,
|
||
payload={"from": character_id, "new_character_id": new_id, "reason": reason},
|
||
source="jingsheng",
|
||
)
|
||
record = await issue_record(new_id, owner_scope)
|
||
return {
|
||
"new_character_id": new_id,
|
||
"lineage": anchor["lineage"],
|
||
"identity_record_version": record["version"],
|
||
}
|
||
|
||
|
||
async def get_lineage(character_id: str, owner_scope: str) -> list[dict]:
|
||
"""谱系回溯:从锚点 lineage 链展开(源人物资料可循)。"""
|
||
character = await _owned(character_id, owner_scope)
|
||
chain = json.loads(character.identity_baseline_json or "{}").get("lineage", [])
|
||
# 展开源人物名(存在性核验;已死亡/删除的源仍保留 id 考据)
|
||
out = []
|
||
for link in chain:
|
||
entry = dict(link)
|
||
async with AsyncSessionLocal() as session:
|
||
src = await session.get(Character, link.get("split_from", ""))
|
||
entry["source_name"] = src.name if src else None
|
||
out.append(entry)
|
||
return out
|
||
|
||
|
||
async def _owned(character_id: str, owner_scope: str) -> Character:
|
||
async with AsyncSessionLocal() as session:
|
||
character = await session.get(Character, character_id)
|
||
if character is None or character.owner_scope != owner_scope:
|
||
raise AppError("not_found", "人物不存在。", status_code=404)
|
||
session.expunge(character)
|
||
return character
|
||
|
||
|
||
def _forging_dict(f: SoulForging) -> dict:
|
||
return {
|
||
"id": f.id,
|
||
"character_id": f.character_id,
|
||
"proposed_anchor": json.loads(f.proposed_anchor_json or "{}"),
|
||
"reason": f.reason,
|
||
"status": f.status,
|
||
"created_at": f.created_at.isoformat() if f.created_at else None,
|
||
"confirmed_at": f.confirmed_at.isoformat() if f.confirmed_at else None,
|
||
}
|