feat: 人设模板系统、节点调度标签、pydantic-settings收敛、错误处理增强

新增persona_template表和CRUD API,BaseIndividualModel增加node_affinity和template_origin_id字段,
WorkerCluster支持多集群Ray资源调度,环境变量收敛到pydantic-settings统一校验,
数据库异常转换为结构化BusinessError/RetryableError,系统节点支持custom_system_prompt。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 06:07:46 +00:00
parent f3a92a793e
commit 8f1398c591
23 changed files with 582 additions and 48 deletions
@@ -14,7 +14,7 @@
from sqlalchemy.exc import IntegrityError, OperationalError
from pydantic import ValidationError
from kilostar.utils.error import UserNotExistError
from kilostar.utils.error import UserNotExistError, BusinessError, RetryableError
from kilostar.utils.logger import get_logger
@@ -31,14 +31,16 @@ def database_exception(func):
logger.error(f"对象校验失败:{e}")
raise e
except IntegrityError as e:
logger.error(f"数据库完整性错误 (如重复记录): {e}")
raise e
logger.warning(f"数据库完整性冲突: {e.orig}")
err = BusinessError(str(e.orig))
err.http_status = 409
err.code = "conflict"
raise err from e
except OperationalError as e:
logger.error(f"数据库连接异常: {e}")
raise e
except UserNotExistError as e:
logger.error(f"更改密码失败,用户不存在:{e}")
raise e
raise RetryableError(f"数据库暂时不可用,请稍后重试: {e}") from e
except (UserNotExistError, BusinessError):
raise
except Exception as e:
logger.exception(f"未预期的数据库错误: {e}")
raise e
@@ -34,6 +34,7 @@ from kilostar.core.postgres_database.model.mcp_server import MCPServerModel
from kilostar.core.postgres_database.model.tool_config import ToolConfigModel
from kilostar.core.postgres_database.model.custom_toolset import CustomToolsetModel
from kilostar.core.postgres_database.model.system_event_log import SystemEventLog
from kilostar.core.postgres_database.model.persona_template import PersonaTemplate
# 兼容旧代码的别名
Provider = ProviderModel
@@ -63,5 +64,6 @@ __all__ = [
"ToolConfigModel",
"CustomToolsetModel",
"SystemEventLog",
"PersonaTemplate",
"AgentType",
]
@@ -43,6 +43,12 @@ class BaseIndividualModel(BaseDataModel):
owner_id: Mapped[str] = mapped_column(String(64), index=True)
agent_type: Mapped[str] = mapped_column(String(32))
node_affinity: Mapped[str] = mapped_column(String(32), nullable=False, default="cpu")
template_origin_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("persona_template.template_id", ondelete="SET NULL"),
nullable=True,
index=True,
)
__mapper_args__ = {"polymorphic_on": "agent_type", "polymorphic_identity": "base"}
@@ -0,0 +1,26 @@
from typing import List, Optional
from sqlalchemy import String, Text, Boolean, text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column
from .base import BaseDataModel
class PersonaTemplate(BaseDataModel):
__tablename__ = "persona_template"
template_id: Mapped[str] = mapped_column(String(64), primary_key=True)
name: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
description: Mapped[str] = mapped_column(Text, nullable=False, default="")
system_prompt: Mapped[str] = mapped_column(Text, nullable=False, default="")
agent_type: Mapped[str] = mapped_column(String(32), nullable=False, default="ordinary")
provider_title: Mapped[Optional[str]] = mapped_column(String(50))
model_id: Mapped[Optional[str]] = mapped_column(String(100))
tools: Mapped[Optional[List[str]]] = mapped_column(
JSONB, default=list, server_default=text("'[]'::jsonb")
)
tags: Mapped[Optional[List[str]]] = mapped_column(
JSONB, default=list, server_default=text("'[]'::jsonb")
)
is_builtin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
owner_id: Mapped[Optional[str]] = mapped_column(String(64), index=True)
@@ -0,0 +1,73 @@
from sqlalchemy import select
from ulid import ULID
from kilostar.core.postgres_database.model.persona_template import PersonaTemplate
from kilostar.core.postgres_database.database_exception import database_exception
class PersonaTemplateDatabase:
def __init__(self, async_session_maker):
self.async_session_maker = async_session_maker
@database_exception
async def add_template(self, **kwargs) -> PersonaTemplate:
async with self.async_session_maker() as session:
tpl = PersonaTemplate(template_id=str(ULID()), **kwargs)
session.add(tpl)
await session.commit()
await session.refresh(tpl)
return tpl
@database_exception
async def get_template(self, template_id: str):
async with self.async_session_maker() as session:
result = await session.execute(
select(PersonaTemplate).where(PersonaTemplate.template_id == template_id)
)
return result.scalar_one_or_none()
@database_exception
async def list_templates(self, owner_id: str = None, include_builtin: bool = True):
async with self.async_session_maker() as session:
stmt = select(PersonaTemplate)
if owner_id and include_builtin:
from sqlalchemy import or_
stmt = stmt.where(
or_(PersonaTemplate.owner_id == owner_id, PersonaTemplate.is_builtin == True)
)
elif owner_id:
stmt = stmt.where(PersonaTemplate.owner_id == owner_id)
elif include_builtin:
stmt = stmt.where(PersonaTemplate.is_builtin == True)
result = await session.execute(stmt)
return list(result.scalars().all())
@database_exception
async def update_template(self, template_id: str, **kwargs):
async with self.async_session_maker() as session:
result = await session.execute(
select(PersonaTemplate).where(PersonaTemplate.template_id == template_id)
)
tpl = result.scalar_one_or_none()
if not tpl:
return None
for k, v in kwargs.items():
if v is not None:
setattr(tpl, k, v)
session.add(tpl)
await session.commit()
await session.refresh(tpl)
return tpl
@database_exception
async def delete_template(self, template_id: str) -> bool:
async with self.async_session_maker() as session:
result = await session.execute(
select(PersonaTemplate).where(PersonaTemplate.template_id == template_id)
)
tpl = result.scalar_one_or_none()
if not tpl:
return False
await session.delete(tpl)
await session.commit()
return True