初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,678 @@
|
||||
"""人物服务。"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError
|
||||
from app.core.time import utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.character import Character
|
||||
from app.models.personality import PersonalityVersion
|
||||
from app.models.time_anniversary import TimeAnniversary
|
||||
from app.modules.character import (
|
||||
MODEL_POLICY_CREATE_DEFAULTS,
|
||||
SYSTEM_TEMPLATE_OWNER_SCOPE,
|
||||
extract_model_hint,
|
||||
sanitize_profile,
|
||||
)
|
||||
from app.repositories import characters as character_repo
|
||||
from app.repositories import personality as personality_repo
|
||||
from app.repositories import time_anniversaries as anniversary_repo
|
||||
from app.repositories.json_utils import dumps, loads_dict
|
||||
from app.schemas.character import (
|
||||
ActivateCharacterResponse,
|
||||
CharacterResponse,
|
||||
CharacterSummaryResponse,
|
||||
CharacterTemplateSummaryResponse,
|
||||
CreateCharacterRequest,
|
||||
UpdateModelPolicyRequest,
|
||||
UpdateProfileRequest,
|
||||
)
|
||||
from app.schemas.common import Page
|
||||
from app.services.audit import record_audit_event
|
||||
from app.services.character_templates import CharacterTemplateConfig, get_default_template
|
||||
from app.services.serializers import character_to_response, character_to_summary
|
||||
|
||||
|
||||
async def get_active_character_model(
|
||||
session: AsyncSession, owner_scope: str = settings.default_owner_scope
|
||||
) -> Character:
|
||||
character = await character_repo.get_active_character(session, owner_scope)
|
||||
if character is None:
|
||||
raise AppError("not_found", "未找到当前激活人物。")
|
||||
return character
|
||||
|
||||
|
||||
async def resolve_active_character(
|
||||
session: AsyncSession,
|
||||
character_id: str | None = None,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> Character:
|
||||
"""读取当前激活人物;传入 character_id 时校验归属。"""
|
||||
character = await get_active_character_model(session, owner_scope)
|
||||
if character_id is not None and character_id != character.id:
|
||||
raise AppError("not_found", "人物不存在。")
|
||||
return character
|
||||
|
||||
|
||||
async def get_active_character(
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> CharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
return character_to_response(await get_active_character_model(session, owner_scope))
|
||||
|
||||
|
||||
def default_model_hint(character: Character) -> str:
|
||||
return extract_model_hint(character.model_policy_json, settings.default_model_hint)
|
||||
|
||||
|
||||
async def _create_template_from_config(
|
||||
session: AsyncSession, config: CharacterTemplateConfig
|
||||
) -> Character:
|
||||
"""从配置构建系统模板 ORM(使用 config/character_templates.yaml 内容)。"""
|
||||
model_policy = dict(config.model_policy)
|
||||
model_policy.setdefault("default_chat_model_hint", settings.default_model_hint)
|
||||
profile = dict(config.profile)
|
||||
sanitize_profile(profile)
|
||||
character = Character(
|
||||
name=config.name,
|
||||
display_name=config.display_name,
|
||||
status="active",
|
||||
language=config.language,
|
||||
profile_json=dumps(profile),
|
||||
model_policy_json=dumps(model_policy),
|
||||
privacy_level=config.privacy_level,
|
||||
owner_scope=SYSTEM_TEMPLATE_OWNER_SCOPE,
|
||||
is_system_template=True,
|
||||
source_template_id=None,
|
||||
template_key=config.key,
|
||||
)
|
||||
session.add(character)
|
||||
await session.flush()
|
||||
await _ensure_default_birthday(session, character)
|
||||
return character
|
||||
|
||||
|
||||
async def create_default_character(
|
||||
session: AsyncSession, owner_scope: str = settings.default_owner_scope
|
||||
) -> Character:
|
||||
template = await ensure_default_system_template(session)
|
||||
return await create_character_from_template(session, template, owner_scope)
|
||||
|
||||
|
||||
async def ensure_character_personality(
|
||||
session: AsyncSession, character: Character
|
||||
) -> PersonalityVersion:
|
||||
from app.services.personality import create_personality_from_profile
|
||||
|
||||
personality = None
|
||||
if character.current_personality_version_id:
|
||||
personality = await personality_repo.get_personality(
|
||||
session, character.current_personality_version_id
|
||||
)
|
||||
if personality is None:
|
||||
personality = await personality_repo.get_latest_personality(session, character.id)
|
||||
if personality is None:
|
||||
personality = await create_personality_from_profile(session, character)
|
||||
if character.current_personality_version_id != personality.id:
|
||||
character.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
return personality
|
||||
|
||||
|
||||
async def ensure_default_system_template(session: AsyncSession) -> Character:
|
||||
"""按 config/character_templates.yaml 确保系统模板存在且最新。
|
||||
|
||||
Config 为单一事实源:DB 行缺失时 insert,字段不一致时覆盖更新。
|
||||
子人物(is_system_template=False)不受影响。
|
||||
"""
|
||||
config = get_default_template()
|
||||
template = await character_repo.get_system_template_by_key(session, config.key)
|
||||
if template is None:
|
||||
template = await _create_template_from_config(session, config)
|
||||
else:
|
||||
await _sync_template_from_config(session, template, config)
|
||||
# create 路径已由 _create_template_from_config 兜底 birthday,
|
||||
# sync 路径可能因旧数据缺失 birthday,再兜一次确保存在。
|
||||
await _ensure_default_birthday(session, template)
|
||||
await ensure_character_personality(session, template)
|
||||
return template
|
||||
|
||||
|
||||
async def ensure_all_system_templates(session: AsyncSession) -> list[Character]:
|
||||
"""确保 ``config/character_templates.yaml`` 中定义的所有系统模板在 DB 中存在且最新。
|
||||
|
||||
遍历所有配置的模板(包括 ``key='default'``),
|
||||
缺失则创建、字段不一致则覆盖更新,并确保 birthday 兜底与人格初始化。
|
||||
供 ``seed_default_data`` 在应用启动时调用。
|
||||
"""
|
||||
from app.services.character_templates import load_character_templates
|
||||
|
||||
configs = load_character_templates()
|
||||
if not configs:
|
||||
return []
|
||||
|
||||
results: list[Character] = []
|
||||
for key, config in configs.items():
|
||||
template = await character_repo.get_system_template_by_key(session, key)
|
||||
if template is None:
|
||||
template = await _create_template_from_config(session, config)
|
||||
else:
|
||||
await _sync_template_from_config(session, template, config)
|
||||
await _ensure_default_birthday(session, template)
|
||||
await ensure_character_personality(session, template)
|
||||
results.append(template)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
async def _sync_template_from_config(
|
||||
session: AsyncSession, template: Character, config: CharacterTemplateConfig
|
||||
) -> None:
|
||||
"""比对 DB 字段与 config,不一致则覆盖更新并写入审计事件。
|
||||
|
||||
Config 可显式定义 birthday(含有效值时优先),缺失或不合法时
|
||||
由 ``_ensure_default_birthday`` 用 created_at 兜底。
|
||||
"""
|
||||
model_policy = dict(config.model_policy)
|
||||
model_policy.setdefault("default_chat_model_hint", settings.default_model_hint)
|
||||
profile = dict(config.profile)
|
||||
sanitize_profile(profile)
|
||||
|
||||
changed = False
|
||||
|
||||
if template.name != config.name:
|
||||
template.name = config.name
|
||||
changed = True
|
||||
if template.display_name != config.display_name:
|
||||
template.display_name = config.display_name
|
||||
changed = True
|
||||
if template.language != config.language:
|
||||
template.language = config.language
|
||||
changed = True
|
||||
if template.privacy_level != config.privacy_level:
|
||||
template.privacy_level = config.privacy_level
|
||||
changed = True
|
||||
|
||||
# profile_json 比对:比较时排除 birthday(config 可能显式设、也可能留空兜底),
|
||||
# 写入时 config 有值优先,否则保留 DB 端已有值(含 _ensure_default_birthday 兜底值)。
|
||||
current_profile = loads_dict(template.profile_json)
|
||||
config_birthday = profile.pop("birthday", None)
|
||||
existing_birthday = current_profile.pop("birthday", None)
|
||||
if current_profile != profile:
|
||||
if config_birthday:
|
||||
profile["birthday"] = config_birthday
|
||||
elif existing_birthday:
|
||||
profile["birthday"] = existing_birthday
|
||||
template.profile_json = dumps(profile)
|
||||
changed = True
|
||||
|
||||
# model_policy_json 比对
|
||||
current_policy = loads_dict(template.model_policy_json)
|
||||
if current_policy != model_policy:
|
||||
template.model_policy_json = dumps(model_policy)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="character.template_resynced",
|
||||
character_id=template.id,
|
||||
actor="system",
|
||||
decision="auto",
|
||||
input_summary=f"模板 '{config.key}' 由 config 文件覆盖更新",
|
||||
resource_type="character_template",
|
||||
resource_id=template.id,
|
||||
)
|
||||
|
||||
|
||||
async def create_character_from_template(
|
||||
session: AsyncSession, template: Character, owner_scope: str
|
||||
) -> Character:
|
||||
# 模板的 birthday 仅在兜底(空/缺失)时不继承;显式设置的有效生日原样保留,
|
||||
# 由 _ensure_default_birthday 写入子角色的 profile 与 time_anniversaries 表。
|
||||
profile = loads_dict(template.profile_json)
|
||||
birthday = profile.get("birthday")
|
||||
if not (isinstance(birthday, str) and birthday.strip()):
|
||||
profile.pop("birthday", None)
|
||||
character = Character(
|
||||
name=template.name,
|
||||
display_name=template.display_name,
|
||||
status="active",
|
||||
language=template.language,
|
||||
profile_json=dumps(profile),
|
||||
model_policy_json=template.model_policy_json,
|
||||
safety_policy_id=template.safety_policy_id,
|
||||
privacy_level=template.privacy_level,
|
||||
owner_scope=owner_scope,
|
||||
is_system_template=False,
|
||||
source_template_id=template.id,
|
||||
template_key=None,
|
||||
)
|
||||
session.add(character)
|
||||
await session.flush()
|
||||
await _ensure_default_birthday(session, character)
|
||||
return character
|
||||
|
||||
|
||||
async def ensure_default_character_for_owner(session: AsyncSession, owner_scope: str) -> Character:
|
||||
"""确保当前 owner 有默认模板对应的用户人物。
|
||||
|
||||
只处理 ``character_templates.yaml`` 中标记 ``is_default: true`` 的模板。
|
||||
不存在任何默认模板时回退到 ``key='default'``。
|
||||
"""
|
||||
from app.services.character_templates import load_character_templates
|
||||
|
||||
configs = load_character_templates()
|
||||
if not configs:
|
||||
template = await ensure_default_system_template(session)
|
||||
character = await create_character_from_template(session, template, owner_scope)
|
||||
await _ensure_character_personality_from_template(session, template, character)
|
||||
return character
|
||||
|
||||
# 先确保所有系统模板在 DB 中存在
|
||||
templates: dict[str, Character] = {}
|
||||
for key in configs:
|
||||
tpl = await character_repo.get_system_template_by_key(session, key)
|
||||
if tpl is None:
|
||||
config = configs[key]
|
||||
tpl = await _create_template_from_config(session, config)
|
||||
await _ensure_default_birthday(session, tpl)
|
||||
await ensure_character_personality(session, tpl)
|
||||
templates[key] = tpl
|
||||
|
||||
# 筛选 is_default=true 的配置 key
|
||||
default_keys = [k for k, c in configs.items() if c.is_default]
|
||||
if not default_keys:
|
||||
default_keys = ["default"] if "default" in configs else []
|
||||
|
||||
primary: Character | None = None
|
||||
for key in default_keys:
|
||||
tpl = templates.get(key)
|
||||
if tpl is None:
|
||||
continue
|
||||
existing = await character_repo.get_character_by_source_template(
|
||||
session, tpl.id, owner_scope
|
||||
)
|
||||
if existing is not None:
|
||||
await ensure_character_personality(session, existing)
|
||||
if primary is None:
|
||||
primary = existing
|
||||
continue
|
||||
|
||||
character = await create_character_from_template(session, tpl, owner_scope)
|
||||
from app.services.personality import copy_personality_to_character
|
||||
|
||||
template_personality = await ensure_character_personality(session, tpl)
|
||||
personality = await copy_personality_to_character(
|
||||
session, template_personality, character.id
|
||||
)
|
||||
character.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="character.created_from_template",
|
||||
character_id=character.id,
|
||||
actor="system",
|
||||
decision="auto",
|
||||
input_summary=f"启动时从模板 '{key}' 自动创建用户人物",
|
||||
resource_type="character",
|
||||
resource_id=character.id,
|
||||
)
|
||||
if primary is None:
|
||||
primary = character
|
||||
|
||||
# 兜底:没有找到主人物时从 default 模板创建
|
||||
if primary is None:
|
||||
template = await ensure_default_system_template(session)
|
||||
primary = await create_character_from_template(session, template, owner_scope)
|
||||
template_personality = await ensure_character_personality(session, template)
|
||||
from app.services.personality import copy_personality_to_character
|
||||
|
||||
personality = await copy_personality_to_character(session, template_personality, primary.id)
|
||||
primary.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
|
||||
return primary
|
||||
|
||||
|
||||
async def _ensure_character_personality_from_template(
|
||||
session: AsyncSession,
|
||||
template: Character,
|
||||
character: Character,
|
||||
) -> None:
|
||||
"""从模板复制人格到目标人物。"""
|
||||
from app.services.personality import copy_personality_to_character
|
||||
|
||||
template_personality = await ensure_character_personality(session, template)
|
||||
personality = await copy_personality_to_character(session, template_personality, character.id)
|
||||
character.current_personality_version_id = personality.id
|
||||
await session.flush()
|
||||
|
||||
|
||||
# —— 人物管理 API(api-spec §6)——
|
||||
|
||||
|
||||
async def get_owned_character(
|
||||
session: AsyncSession, character_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> Character:
|
||||
"""按 ID 取当前归属范围下的可操作人物;系统模板与跨范围 ID 一律视为不存在。"""
|
||||
character = await character_repo.get_character(session, character_id)
|
||||
if character is None or character.is_system_template or character.owner_scope != owner_scope:
|
||||
raise AppError("not_found", "人物不存在。")
|
||||
return character
|
||||
|
||||
|
||||
async def _personality_version_number(session: AsyncSession, character: Character) -> int:
|
||||
version_id = character.current_personality_version_id
|
||||
if not version_id:
|
||||
return 0
|
||||
personality = await personality_repo.get_personality(session, version_id)
|
||||
return personality.version if personality else 0
|
||||
|
||||
|
||||
async def _audit_character(session: AsyncSession, character: Character, action: str) -> None:
|
||||
await record_audit_event(
|
||||
session,
|
||||
action=action,
|
||||
character_id=character.id,
|
||||
actor="user",
|
||||
decision="auto",
|
||||
input_summary=character.name,
|
||||
resource_type="character",
|
||||
resource_id=character.id,
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_default_birthday(session: AsyncSession, character: Character) -> None:
|
||||
"""birthday 缺失时用 created_at 兜底:写 profile.birthday 并建一条生日纪念日。
|
||||
|
||||
- 仅在 profile.birthday 为空/缺失时触发默认兜底;用户或模板已设则不覆盖。
|
||||
- 无论 birthday 是否已设,只要角色尚无 birthday 纪念日,就建一条。
|
||||
- profile.birthday 与 TimeAnniversary.date 取 birthday 值(显式)或 created_at 的 ISO 日期。
|
||||
- 须在 character flush 后调用(依赖 id 与 created_at);同一事务内随外层 commit 落库。
|
||||
"""
|
||||
profile = loads_dict(character.profile_json)
|
||||
birthday = profile.get("birthday")
|
||||
if isinstance(birthday, str) and birthday.strip():
|
||||
# birthday 已设但可能无对应纪念日(如从模板继承),仅 ISO 格式生日补建
|
||||
existing = await anniversary_repo.list_anniversaries(session, character.id)
|
||||
if not any(a.kind == "birthday" for a in existing):
|
||||
try:
|
||||
datetime.strptime(birthday, "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
else:
|
||||
anniversary = TimeAnniversary(
|
||||
character_id=character.id,
|
||||
name=f"{character.display_name or character.name}·生辰",
|
||||
calendar="solar",
|
||||
date=birthday,
|
||||
recurs_yearly=True,
|
||||
kind="birthday",
|
||||
subject_type="character_self",
|
||||
source="auto",
|
||||
)
|
||||
await anniversary_repo.create_anniversary(session, anniversary)
|
||||
return
|
||||
birthday_iso = character.created_at.strftime("%Y-%m-%d")
|
||||
profile["birthday"] = birthday_iso
|
||||
character.profile_json = dumps(profile)
|
||||
anniversary = TimeAnniversary(
|
||||
character_id=character.id,
|
||||
name=f"{character.display_name or character.name}·生辰",
|
||||
calendar="solar",
|
||||
date=birthday_iso,
|
||||
recurs_yearly=True,
|
||||
kind="birthday",
|
||||
subject_type="character_self",
|
||||
source="auto",
|
||||
)
|
||||
await anniversary_repo.create_anniversary(session, anniversary)
|
||||
|
||||
|
||||
async def create_character(
|
||||
session: AsyncSession,
|
||||
payload: CreateCharacterRequest,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> CharacterResponse:
|
||||
"""API 创建人物。
|
||||
|
||||
- 传 ``template_key`` 时从系统模板复制(继承 profile/model_policy,status=active);
|
||||
- 不传时创建空白人物(status=wuji,不激活、不建人格)。
|
||||
"""
|
||||
if payload.template_key:
|
||||
template = await character_repo.get_system_template_by_key(session, payload.template_key)
|
||||
if template is None:
|
||||
raise AppError("not_found", f"模板 '{payload.template_key}' 不存在。")
|
||||
|
||||
profile = loads_dict(template.profile_json)
|
||||
profile.update(dict(payload.profile or {}))
|
||||
|
||||
model_policy = loads_dict(template.model_policy_json)
|
||||
model_policy.update(dict(payload.model_policy or {}))
|
||||
model_policy.setdefault("default_chat_model_hint", settings.default_model_hint)
|
||||
|
||||
character = Character(
|
||||
name=payload.name,
|
||||
display_name=payload.display_name or template.display_name,
|
||||
status="active",
|
||||
language=template.language,
|
||||
profile_json=dumps(profile),
|
||||
model_policy_json=dumps(model_policy),
|
||||
privacy_level=template.privacy_level,
|
||||
owner_scope=owner_scope,
|
||||
is_system_template=False,
|
||||
source_template_id=template.id,
|
||||
template_key=None,
|
||||
)
|
||||
await character_repo.add_character(session, character)
|
||||
await _ensure_default_birthday(session, character)
|
||||
await ensure_character_personality(session, character)
|
||||
await _audit_character(session, character, "character.created_from_template")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return character_to_response(character)
|
||||
|
||||
# ── 空白创建(原有逻辑)──
|
||||
model_policy = MODEL_POLICY_CREATE_DEFAULTS | dict(payload.model_policy or {})
|
||||
model_policy["default_chat_model_hint"] = (
|
||||
model_policy.get("default_chat_model_hint") or settings.default_model_hint
|
||||
)
|
||||
character = Character(
|
||||
name=payload.name,
|
||||
display_name=payload.display_name,
|
||||
status="wuji",
|
||||
language="zh-CN",
|
||||
profile_json=dumps(dict(payload.profile or {})),
|
||||
model_policy_json=dumps(model_policy),
|
||||
privacy_level=model_policy.get("privacy_level", "normal"),
|
||||
owner_scope=owner_scope,
|
||||
is_system_template=False,
|
||||
source_template_id=None,
|
||||
template_key=None,
|
||||
)
|
||||
await character_repo.add_character(session, character)
|
||||
await _ensure_default_birthday(session, character)
|
||||
await _audit_character(session, character, "character.created")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return character_to_response(character)
|
||||
|
||||
|
||||
async def list_characters_page(
|
||||
*,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
status: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
) -> Page[CharacterSummaryResponse]:
|
||||
async with AsyncSessionLocal() as session:
|
||||
safe_page = max(page, 1)
|
||||
safe_page_size = min(max(page_size, 1), 100)
|
||||
items, total = await character_repo.list_characters(
|
||||
session,
|
||||
owner_scope,
|
||||
status=status,
|
||||
page=safe_page,
|
||||
page_size=safe_page_size,
|
||||
)
|
||||
summaries = [
|
||||
character_to_summary(
|
||||
character,
|
||||
memory_count=await character_repo.count_memories(session, character.id),
|
||||
personality_version=await _personality_version_number(session, character),
|
||||
)
|
||||
for character in items
|
||||
]
|
||||
return Page(
|
||||
items=summaries,
|
||||
page=safe_page,
|
||||
page_size=safe_page_size,
|
||||
total=total,
|
||||
)
|
||||
|
||||
|
||||
async def get_character_by_id(
|
||||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> CharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
return character_to_response(await get_owned_character(session, character_id, owner_scope))
|
||||
|
||||
|
||||
async def update_character_profile(
|
||||
character_id: str,
|
||||
payload: UpdateProfileRequest,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> CharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_owned_character(session, character_id, owner_scope)
|
||||
profile = loads_dict(character.profile_json)
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
if key == "display_name":
|
||||
if value is not None:
|
||||
character.display_name = value
|
||||
continue
|
||||
if value is not None:
|
||||
profile[key] = value
|
||||
character.profile_json = dumps(profile)
|
||||
await _audit_character(session, character, "character.profile_updated")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return character_to_response(character)
|
||||
|
||||
|
||||
async def update_character_model_policy(
|
||||
character_id: str,
|
||||
payload: UpdateModelPolicyRequest,
|
||||
owner_scope: str = settings.default_owner_scope,
|
||||
) -> CharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_owned_character(session, character_id, owner_scope)
|
||||
policy = loads_dict(character.model_policy_json)
|
||||
for key, value in payload.model_dump(exclude_unset=True).items():
|
||||
if value is None:
|
||||
continue
|
||||
if key == "privacy_level":
|
||||
character.privacy_level = value
|
||||
policy[key] = value
|
||||
character.model_policy_json = dumps(policy)
|
||||
await _audit_character(session, character, "character.model_policy_updated")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return character_to_response(character)
|
||||
|
||||
|
||||
async def _deactivate_others(
|
||||
session: AsyncSession, character: Character, owner_scope: str = settings.default_owner_scope
|
||||
) -> None:
|
||||
"""A1 不变量:一个 owner_scope 至多一个 active;激活时把其余改 paused。"""
|
||||
others = await character_repo.list_active_in_scope(session, owner_scope, except_id=character.id)
|
||||
for other in others:
|
||||
other.status = "paused"
|
||||
|
||||
|
||||
async def activate_character(
|
||||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> ActivateCharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_owned_character(session, character_id, owner_scope)
|
||||
if character.status == "archived":
|
||||
raise AppError("conflict", "已归档人物不可直接激活,请先恢复。")
|
||||
await _deactivate_others(session, character, owner_scope)
|
||||
character.status = "active"
|
||||
character.activated_at = utc_now()
|
||||
await ensure_character_personality(session, character)
|
||||
await _audit_character(session, character, "character.activated")
|
||||
await session.commit()
|
||||
return ActivateCharacterResponse(active_character_id=character.id, status="active")
|
||||
|
||||
|
||||
async def pause_character(
|
||||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> CharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_owned_character(session, character_id, owner_scope)
|
||||
if character.status not in ("active", "initialized"):
|
||||
raise AppError("conflict", "仅运行中人物可暂停。")
|
||||
character.status = "paused"
|
||||
await _audit_character(session, character, "character.paused")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return character_to_response(character)
|
||||
|
||||
|
||||
async def resume_character(
|
||||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> CharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_owned_character(session, character_id, owner_scope)
|
||||
if character.status != "paused":
|
||||
raise AppError("conflict", "仅暂停状态的人物可恢复。")
|
||||
await _deactivate_others(session, character, owner_scope)
|
||||
character.status = "active"
|
||||
character.activated_at = utc_now()
|
||||
await ensure_character_personality(session, character)
|
||||
await _audit_character(session, character, "character.resumed")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return character_to_response(character)
|
||||
|
||||
|
||||
async def archive_character(
|
||||
character_id: str, owner_scope: str = settings.default_owner_scope
|
||||
) -> CharacterResponse:
|
||||
async with AsyncSessionLocal() as session:
|
||||
character = await get_owned_character(session, character_id, owner_scope)
|
||||
if character.status == "archived":
|
||||
raise AppError("conflict", "人物已归档。")
|
||||
character.status = "archived"
|
||||
character.archived_at = utc_now()
|
||||
await _audit_character(session, character, "character.archived")
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return character_to_response(character)
|
||||
|
||||
|
||||
async def list_system_templates_page() -> list[CharacterTemplateSummaryResponse]:
|
||||
"""列出所有系统模板(供前端模板选择器使用)。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
templates = await character_repo.list_system_templates(session)
|
||||
result: list[CharacterTemplateSummaryResponse] = []
|
||||
for t in templates:
|
||||
profile = loads_dict(t.profile_json)
|
||||
result.append(
|
||||
CharacterTemplateSummaryResponse(
|
||||
key=t.template_key or "",
|
||||
name=t.name,
|
||||
display_name=t.display_name,
|
||||
description=profile.get("description"),
|
||||
temperament=profile.get("temperament"),
|
||||
speaking_style=profile.get("speaking_style"),
|
||||
language=t.language,
|
||||
)
|
||||
)
|
||||
return result
|
||||
Reference in new issue
Block a user