初始化提交
This commit is contained in:
commit
887c1f449e
963 files changed
+166869
No files matched your search
@@ -0,0 +1,216 @@
|
||||
"""司命台 · 人物模板管理聚合服务(api-spec §19 扩展)。
|
||||
|
||||
系统模板 CRUD,所有写操作记审计并入起居注。
|
||||
路由直接调用此服务层函数(函数内部自管 ``AsyncSessionLocal``)。
|
||||
"""
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.errors import AppError
|
||||
from app.core.ids import new_character_id
|
||||
from app.core.time import utc_now
|
||||
from app.db.session import AsyncSessionLocal
|
||||
from app.models.character import Character
|
||||
from app.repositories import characters as character_repo
|
||||
from app.repositories.json_utils import dumps, loads_dict
|
||||
from app.schemas.admin_character import (
|
||||
AdminCreateTemplateRequest,
|
||||
AdminTemplateSummaryResponse,
|
||||
AdminUpdateTemplateRequest,
|
||||
)
|
||||
from app.services.audit import record_audit_event
|
||||
|
||||
|
||||
def _template_to_response(template: Character) -> AdminTemplateSummaryResponse:
|
||||
profile = loads_dict(template.profile_json)
|
||||
model_policy = loads_dict(template.model_policy_json)
|
||||
# 从 profile 中推断 is_default(无独立字段,保留为 False 未映射)
|
||||
return AdminTemplateSummaryResponse(
|
||||
key=template.template_key or "",
|
||||
name=template.name,
|
||||
display_name=template.display_name,
|
||||
language=template.language,
|
||||
privacy_level=template.privacy_level,
|
||||
is_default=False,
|
||||
profile=profile,
|
||||
model_policy=model_policy,
|
||||
created_at=template.created_at.isoformat() if template.created_at else "",
|
||||
updated_at=template.updated_at.isoformat() if template.updated_at else "",
|
||||
)
|
||||
|
||||
|
||||
async def admin_list_templates() -> list[AdminTemplateSummaryResponse]:
|
||||
"""列出所有系统模板(管理端完整视图)。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
templates = await character_repo.list_system_templates(session)
|
||||
return [_template_to_response(t) for t in templates]
|
||||
|
||||
|
||||
async def admin_get_template(key: str) -> AdminTemplateSummaryResponse:
|
||||
"""获取单个系统模板。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
template = await character_repo.get_system_template_by_key(session, key)
|
||||
if template is None:
|
||||
raise AppError("not_found", f"模板 '{key}' 不存在。")
|
||||
return _template_to_response(template)
|
||||
|
||||
|
||||
async def admin_create_template(
|
||||
payload: AdminCreateTemplateRequest,
|
||||
) -> AdminTemplateSummaryResponse:
|
||||
"""创建系统模板。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 检查是否已存在同 key 模板
|
||||
existing = await character_repo.get_system_template_by_key(session, payload.key)
|
||||
if existing is not None:
|
||||
raise AppError("conflict", f"模板 key '{payload.key}' 已存在。")
|
||||
|
||||
profile = dict(payload.profile)
|
||||
model_policy = dict(payload.model_policy)
|
||||
model_policy.setdefault("default_chat_model_hint", settings.default_model_hint)
|
||||
|
||||
character = Character(
|
||||
id=new_character_id(),
|
||||
name=payload.name,
|
||||
display_name=payload.display_name,
|
||||
status="active",
|
||||
language=payload.language,
|
||||
profile_json=dumps(profile),
|
||||
model_policy_json=dumps(model_policy),
|
||||
privacy_level=payload.privacy_level,
|
||||
owner_scope="system",
|
||||
is_system_template=True,
|
||||
source_template_id=None,
|
||||
template_key=payload.key,
|
||||
)
|
||||
session.add(character)
|
||||
await session.flush()
|
||||
|
||||
# birthday 兜底
|
||||
await _ensure_birthday(character)
|
||||
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="admin.character_template.created",
|
||||
character_id=character.id,
|
||||
actor="admin",
|
||||
decision="auto",
|
||||
input_summary=f"管理员创建系统模板 '{payload.key}'",
|
||||
resource_type="character_template",
|
||||
resource_id=character.id,
|
||||
)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(character)
|
||||
return _template_to_response(character)
|
||||
|
||||
|
||||
async def admin_update_template(
|
||||
key: str,
|
||||
payload: AdminUpdateTemplateRequest,
|
||||
) -> AdminTemplateSummaryResponse:
|
||||
"""更新系统模板(部分更新)。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
template = await character_repo.get_system_template_by_key(session, key)
|
||||
if template is None:
|
||||
raise AppError("not_found", f"模板 '{key}' 不存在。")
|
||||
|
||||
changed = False
|
||||
update_fields = payload.model_dump(exclude_unset=True)
|
||||
|
||||
if "name" in update_fields and template.name != update_fields["name"]:
|
||||
template.name = update_fields["name"]
|
||||
changed = True
|
||||
if "display_name" in update_fields and template.display_name != update_fields["display_name"]:
|
||||
template.display_name = update_fields["display_name"]
|
||||
changed = True
|
||||
if "language" in update_fields and template.language != update_fields["language"]:
|
||||
template.language = update_fields["language"]
|
||||
changed = True
|
||||
if "privacy_level" in update_fields and template.privacy_level != update_fields["privacy_level"]:
|
||||
template.privacy_level = update_fields["privacy_level"]
|
||||
changed = True
|
||||
|
||||
if "profile" in update_fields:
|
||||
current_profile = loads_dict(template.profile_json)
|
||||
current_profile.update(update_fields["profile"])
|
||||
template.profile_json = dumps(current_profile)
|
||||
changed = True
|
||||
|
||||
if "model_policy" in update_fields:
|
||||
current_policy = loads_dict(template.model_policy_json)
|
||||
current_policy.update(update_fields["model_policy"])
|
||||
template.model_policy_json = dumps(current_policy)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="admin.character_template.updated",
|
||||
character_id=template.id,
|
||||
actor="admin",
|
||||
decision="auto",
|
||||
input_summary=f"管理员更新系统模板 '{key}'",
|
||||
resource_type="character_template",
|
||||
resource_id=template.id,
|
||||
)
|
||||
template.updated_at = utc_now()
|
||||
await session.commit()
|
||||
await session.refresh(template)
|
||||
|
||||
return _template_to_response(template)
|
||||
|
||||
|
||||
async def admin_delete_template(key: str) -> None:
|
||||
"""删除系统模板。"""
|
||||
async with AsyncSessionLocal() as session:
|
||||
template = await character_repo.get_system_template_by_key(session, key)
|
||||
if template is None:
|
||||
raise AppError("not_found", f"模板 '{key}' 不存在。")
|
||||
|
||||
# 检查是否有关联的子人物
|
||||
await _check_template_in_use(session, template)
|
||||
|
||||
await record_audit_event(
|
||||
session,
|
||||
action="admin.character_template.deleted",
|
||||
character_id=template.id,
|
||||
actor="admin",
|
||||
decision="auto",
|
||||
input_summary=f"管理员删除系统模板 '{key}'",
|
||||
resource_type="character_template",
|
||||
resource_id=template.id,
|
||||
)
|
||||
|
||||
await session.delete(template)
|
||||
await session.commit()
|
||||
|
||||
|
||||
async def _check_template_in_use(session, template: Character) -> None:
|
||||
"""检查模板是否被用户人物引用,是则阻止删除。"""
|
||||
from sqlalchemy import func, select
|
||||
|
||||
count = await session.scalar(
|
||||
select(func.count())
|
||||
.select_from(Character)
|
||||
.where(
|
||||
Character.source_template_id == template.id,
|
||||
Character.is_system_template.is_(False),
|
||||
)
|
||||
)
|
||||
if count and count > 0:
|
||||
raise AppError(
|
||||
"conflict",
|
||||
f"模板 '{template.template_key}' 已被 {count} 个人物引用,无法删除。"
|
||||
"请先确保无用户人物引用此模板后再试。",
|
||||
)
|
||||
|
||||
|
||||
async def _ensure_birthday(character: Character) -> None:
|
||||
"""为模板创建兜底 birthday(profile 中无 birthday 时用 created_at)。"""
|
||||
profile = loads_dict(character.profile_json)
|
||||
birthday = profile.get("birthday")
|
||||
if isinstance(birthday, str) and birthday.strip():
|
||||
return
|
||||
birthday_iso = character.created_at.strftime("%Y-%m-%d")
|
||||
profile["birthday"] = birthday_iso
|
||||
character.profile_json = dumps(profile)
|
||||
Reference in new issue
Block a user