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:
chenxin committed 2026-08-27 20:10:11 +08:00
1 parent 684261f4b4
commit 7e23d6fa84
108 files changed
+2600 -9732

No files matched your search

+212
View File
@@ -0,0 +1,212 @@
"""道体铸魂与分灵(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,
}