"""镜生域服务:灵胚生命周期、启镜事务、死亡请求与冷静期。""" from __future__ import annotations import json from datetime import datetime, UTC from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession from app.core.errors import AppError from app.core.time import as_utc, utc_now from app.db.session import AsyncSessionLocal from app.domains.jingsheng.identity import build_identity_record from app.models.character import Character from app.models.jingsheng import Custodianship, EmbryoDraft, IdentityRecord, RuntimeSeat from app.services.event_store import DbEventStore from app.domains import one_qi def bind_event_store(store: DbEventStore) -> None: """lifespan 装配时注入事件存储(委托共享绑定)。""" one_qi.bind_event_store(store) async def _emit( event_type: str, *, character_id: str | None, owner_scope: str, payload: dict, visibility: str = "public", ) -> None: await one_qi.emit( event_type, character_id=character_id, payload=payload, visibility=visibility, source="jingsheng", ) def _owned(embryo: EmbryoDraft, owner_scope: str) -> EmbryoDraft: if embryo.owner_scope != owner_scope: raise AppError("not_found", "灵胚不存在。") return embryo async def list_embryos(owner_scope: str) -> list[EmbryoDraft]: async with AsyncSessionLocal() as session: result = await session.execute( select(EmbryoDraft) .where(EmbryoDraft.owner_scope == owner_scope, EmbryoDraft.status != "abandoned") .order_by(EmbryoDraft.created_at.desc()) ) return list(result.scalars().all()) async def create_embryo( *, owner_scope: str, source: str, name: str, description: str | None = None, identity_baseline: dict | None = None, ) -> EmbryoDraft: if source not in ("original", "adopted", "imported", "serendipity"): raise AppError("invalid_request", "灵胚来源取值不合法。") embryo = EmbryoDraft( owner_scope=owner_scope, source=source, name=name, description=description, identity_baseline_json=json.dumps(identity_baseline or {}, ensure_ascii=False), ) async with AsyncSessionLocal() as session: session.add(embryo) await session.commit() await session.refresh(embryo) await _emit( "embryo.created", character_id=None, owner_scope=owner_scope, payload={"embryo_id": embryo.id, "source": source}, ) return embryo async def get_embryo(embryo_id: str, owner_scope: str) -> EmbryoDraft: async with AsyncSessionLocal() as session: embryo = await session.get(EmbryoDraft, embryo_id) if embryo is None or embryo.status == "abandoned": raise AppError("not_found", "灵胚不存在。") return _owned(embryo, owner_scope) async def patch_embryo( embryo_id: str, owner_scope: str, *, name: str | None = None, description: str | None = None, identity_baseline: dict | None = None, ) -> EmbryoDraft: async with AsyncSessionLocal() as session: embryo = await session.get(EmbryoDraft, embryo_id) if embryo is None or embryo.status == "abandoned": raise AppError("not_found", "灵胚不存在。") _owned(embryo, owner_scope) if embryo.status == "activated": raise AppError( "HJ-1103", "灵胚已启镜,草稿不可再编辑。", status_code=409 ) if name is not None: embryo.name = name if description is not None: embryo.description = description if identity_baseline is not None: embryo.identity_baseline_json = json.dumps(identity_baseline, ensure_ascii=False) await session.commit() await session.refresh(embryo) return embryo async def record_rehearsal(embryo_id: str, owner_scope: str, *, run_id: str, prompt: str) -> None: """照心试演引用落灵胚(内容在事件日志,不构成人物经历)。""" async with AsyncSessionLocal() as session: embryo = await session.get(EmbryoDraft, embryo_id) if embryo is None or embryo.status == "abandoned": raise AppError("not_found", "灵胚不存在。") _owned(embryo, owner_scope) rehearsals = json.loads(embryo.rehearsals_json) rehearsals.append({"run_id": run_id, "prompt": prompt, "performed_at": utc_now().isoformat()}) embryo.rehearsals_json = json.dumps(rehearsals, ensure_ascii=False) if embryo.status == "drafting": embryo.status = "previewing" await session.commit() async def abandon_embryo(embryo_id: str, owner_scope: str) -> None: async with AsyncSessionLocal() as session: embryo = await session.get(EmbryoDraft, embryo_id) if embryo is None: raise AppError("not_found", "灵胚不存在。") _owned(embryo, owner_scope) if embryo.status == "activated": raise AppError("HJ-1103", "灵胚已启镜,不能放弃;请走人物死亡流程。", status_code=409) embryo.status = "abandoned" await session.commit() await _emit( "embryo.abandoned", character_id=None, owner_scope=owner_scope, payload={"embryo_id": embryo_id}, ) async def activate_embryo(embryo_id: str, owner_scope: str) -> Character: """启镜:单事务内签发人物、执镜关系、命台与魂籍 v1。 并发防护:以 ``UPDATE embryo_drafts SET status='confirming' WHERE status IN (可启镜态)`` 的原子行数作为闸门——第二个并发启镜拿不到闸门即 409,不产生第二个人物。 """ async with AsyncSessionLocal() as session: embryo = await session.get(EmbryoDraft, embryo_id) if embryo is None or embryo.status == "abandoned": raise AppError("not_found", "灵胚不存在。") _owned(embryo, owner_scope) gate = await session.execute( update(EmbryoDraft) .where(EmbryoDraft.id == embryo_id, EmbryoDraft.status.in_(("drafting", "previewing", "confirming"))) .values(status="confirming") ) if gate.rowcount != 1: raise AppError( "HJ-1103", "灵胚当前状态不允许启镜(可能已被并发启镜)。", status_code=409 ) baseline = json.loads(embryo.identity_baseline_json or "{}") character = Character( name=embryo.name, owner_scope=owner_scope, status="active", source=embryo.source, identity_baseline_json=embryo.identity_baseline_json, profile_json=json.dumps({"description": embryo.description or ""}, ensure_ascii=False), activated_at=utc_now(), ) session.add(character) await session.flush() # 取 character.id(commit 后属性过期,异步下不可再访问) character_id = character.id session.add(Custodianship(character_id=character.id, custodian_scope=owner_scope)) session.add(RuntimeSeat(character_id=character.id, epoch=1)) record = build_identity_record( character_id=character.id, version=1, identity={ "name": character.name, "source": character.source, "identity_baseline": baseline, "activated_at": character.activated_at.isoformat(), }, runtime_state={"status": "active", "seat_epoch": 1}, ) session.add(record) await session.flush() # 取魂籍 ID record_id = record.id source = embryo.source embryo.status = "activated" await session.commit() await _emit( "character.activated", character_id=character_id, owner_scope=owner_scope, payload={"embryo_id": embryo_id, "source": source, "identity_record": record_id}, ) async with AsyncSessionLocal() as session: created = await session.get(Character, character_id) assert created is not None return created async def death_request( character_id: str, owner_scope: str, *, calm_period_hours: int ) -> Character: """死亡请求:进入冷静期(death_pending),可撤销;冷静期内人物默认静修。""" async with AsyncSessionLocal() as session: character = await _owned_character(session, character_id, owner_scope) if character.status == "deceased": raise AppError("HJ-1103", "人物已死亡。", status_code=409) now = utc_now() character.status = "death_pending" character.death_requested_at = now character.calm_period_until = datetime.fromtimestamp( now.timestamp() + calm_period_hours * 3600, tz=UTC ) await session.commit() await session.refresh(character) await _emit( "character.death_requested", character_id=character_id, owner_scope=owner_scope, payload={"calm_period_until": character.calm_period_until.isoformat()}, ) return character async def death_cancel(character_id: str, owner_scope: str) -> Character: """冷静期撤销:恢复 active。人物不得阻挠撤销(宪章)。""" async with AsyncSessionLocal() as session: character = await _owned_character(session, character_id, owner_scope) if character.status != "death_pending": raise AppError("HJ-1103", "人物不在死亡冷静期。", status_code=409) character.status = "active" character.death_requested_at = None character.calm_period_until = None await session.commit() await session.refresh(character) await _emit( "character.death_cancelled", character_id=character_id, owner_scope=owner_scope, payload={}, ) return character async def death_finalize(character_id: str, owner_scope: str) -> Character: """死亡落定:身份时间线闭合(deceased),数据保留——数据清除是独立操作。""" async with AsyncSessionLocal() as session: character = await _owned_character(session, character_id, owner_scope) if character.status != "death_pending": raise AppError("HJ-1103", "人物不在死亡冷静期。", status_code=409) assert character.calm_period_until is not None if utc_now() < as_utc(character.calm_period_until): raise AppError( "HJ-1103", f"冷静期未满(至 {character.calm_period_until.isoformat()}),不能落定死亡。", status_code=422, ) character.status = "deceased" character.died_at = utc_now() seat = await session.scalar(select(RuntimeSeat).where(RuntimeSeat.character_id == character_id)) if seat is not None: seat.holder = None seat.lease_expires_at = None await session.commit() await session.refresh(character) await _emit( "character.deceased", character_id=character_id, owner_scope=owner_scope, payload={"died_at": character.died_at.isoformat() if character.died_at else None}, ) return character async def _owned_character( session: AsyncSession, character_id: str, owner_scope: str ) -> Character: character = await session.get(Character, character_id) if character is None or character.owner_scope != owner_scope: raise AppError("not_found", "人物不存在。") return character