"""角色卡导入 / 导出服务。""" from __future__ import annotations from sqlalchemy.ext.asyncio import AsyncSession from app.core.errors import AppError from app.db.session import AsyncSessionLocal from app.models.character import Character from app.models.lorebook import Lorebook, LorebookEntry from app.modules.imports.card_parser import parse_character_card from app.modules.imports.export_mapping import character_to_card from app.modules.imports.mapping import ( card_to_daoti, card_to_profile, character_book_to_entries, ) from app.repositories import lorebook as lorebook_repo from app.repositories.json_utils import dumps from app.schemas.character import ImportCardResponse async def import_character_card( session: AsyncSession, raw: bytes, owner_scope: str ) -> Character: """解析角色卡字节 → 建 Character + 世谱(character_book)。返回 Character ORM。 ``personality`` 写入道体 ``personality_summary`` 作初始值; ``character_book`` 转为该角色的世谱书 + 条目。 """ card = parse_character_card(raw) daoti = card_to_daoti(card) character = Character( name=card.name or "未命名角色", display_name=card.nickname or card.name or None, language="zh-CN", profile_json=dumps(card_to_profile(card)), model_policy_json="{}", personality_summary=daoti["personality_summary"], privacy_level="normal", owner_scope=owner_scope, ) session.add(character) await session.flush() entries_data = character_book_to_entries(card.character_book) if entries_data: book = card.character_book or {} lorebook = Lorebook( character_id=character.id, name=book.get("name", "") or card.name, description=book.get("description", ""), scan_depth=book.get("scan_depth"), token_budget=book.get("token_budget"), recursive_scanning=bool(book.get("recursive_scanning", False)), scope="character", owner_scope=owner_scope, ) session.add(lorebook) await session.flush() for entry in entries_data: session.add( LorebookEntry( lorebook_id=lorebook.id, keys_json=dumps(entry["keys"]), content=entry["content"], secondary_keys_json=dumps(entry["secondary_keys"]), constant=entry["constant"], selective=entry["selective"], enabled=entry["enabled"], case_sensitive=entry["case_sensitive"], use_regex=entry["use_regex"], position=entry["position"], insertion_order=entry["insertion_order"], priority=entry["priority"], comment=entry["comment"], ) ) await session.commit() await session.refresh(character) return character async def import_character_card_response( raw: bytes, owner_scope: str ) -> ImportCardResponse: """导入角色卡 → ImportCardResponse(供 API 调用,自管 session)。""" async with AsyncSessionLocal() as session: character = await import_character_card(session, raw, owner_scope) books = await lorebook_repo.list_books(session, character.id, owner_scope) book_id = books[0].id if books else None entries = ( await lorebook_repo.list_book_entries(session, book_id) if book_id else [] ) return ImportCardResponse( character_id=character.id, name=character.name, display_name=character.display_name, lorebook_id=book_id, entry_count=len(entries), ) async def export_character_card(character_id: str, owner_scope: str) -> dict: """导出角色为 ST V3 卡静态快照(丢弃演化维度)。""" 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", "人物不存在。") books = await lorebook_repo.list_books(session, character_id, owner_scope) book = books[0] if books else None entries = ( await lorebook_repo.list_book_entries(session, book.id) if book else [] ) return character_to_card(character, book, entries)