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
@@ -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,
|
||||
}
|
||||
Reference in new issue
Block a user