118 lines
4.4 KiB
Python
118 lines
4.4 KiB
Python
"""角色卡导入 / 导出服务。"""
|
|
|
|
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)
|