Compare commits

..
Author SHA1 Message Date
pre-commit-ci[bot] 19f60d34c9 🚨 auto fix by pre-commit hooks 2025-12-08 05:26:34 +00:00
webjoin111 61279f8b3d 🐛 (llm): 移除图片生成模型能力预检查 2025-12-08 13:26:19 +08:00
webjoin111 86c748534c 📦️ build(deps): 移除 bilireq 并添加 json_repair 依赖 2025-12-08 10:51:29 +08:00
webjoin111 bba90e62db ♻️ refactor(llm): 重构 LLM 服务架构,引入中间件与组件化适配器
- 【重构】LLM 服务核心架构:
    - 引入中间件管道,统一处理请求生命周期(重试、密钥选择、日志、网络请求)。
    - 适配器重构为组件化设计,分离配置映射、消息转换、响应解析和工具序列化逻辑。
    - 移除 `with_smart_retry` 装饰器,其功能由中间件接管。
    - 移除 `LLMToolExecutor`,工具执行逻辑集成到 `ToolInvoker`。
- 【功能】增强配置系统:
    - `LLMGenerationConfig` 采用组件化结构(Core, Reasoning, Visual, Output, Safety, ToolConfig)。
    - 新增 `GenConfigBuilder` 提供语义化配置构建方式。
    - 新增 `LLMEmbeddingConfig` 用于嵌入专用配置。
    - `CommonOverrides` 迁移并更新至新配置结构。
- 【功能】强化工具系统:
    - 引入 `ToolInvoker` 实现更灵活的工具执行,支持回调与结构化错误。
    - `function_tool` 装饰器支持动态 Pydantic 模型创建和依赖注入 (`ToolParam`, `RunContext`)。
    - 平台原生工具支持 (`GeminiCodeExecution`, `GeminiGoogleSearch`, `GeminiUrlContext`)。
- 【功能】高级生成与嵌入:
    - `generate_structured` 方法支持 In-Context Validation and Repair (IVR) 循环和 AutoCoT (思维链) 包装。
    - 新增 `embed_query` 和 `embed_documents` 便捷嵌入 API。
    - `OpenAIImageAdapter` 支持 OpenAI 兼容的图像生成。
    - `SmartAdapter` 实现模型名称智能路由。
- 【重构】消息与类型系统:
    - `LLMContentPart` 扩展支持更多模态和代码执行相关内容。
    - `LLMMessage` 和 `LLMResponse` 结构更新,支持 `content_parts` 和思维链签名。
    - 统一 `LLMErrorCode` 和用户友好错误消息,提供更详细的网络/代理错误提示。
    - `pyproject.toml` 移除 `bilireq`,新增 `json_repair`。
- 【优化】日志与调试:
    - 引入 `DebugLogOptions`,提供细粒度日志脱敏控制。
    - 增强日志净化器,处理更多敏感数据和长字符串。
- 【清理】删除废弃模块:
    - `zhenxun/services/llm/memory.py`
    - `zhenxun/services/llm/executor.py`
    - `zhenxun/services/llm/config/presets.py`
    - `zhenxun/services/llm/types/content.py`
    - `zhenxun/services/llm/types/enums.py`
    - `zhenxun/services/llm/tools/__init__.py`
    - `zhenxun/services/llm/tools/manager.py`
2025-12-07 18:57:55 +08:00
12 changed files with 108 additions and 1145 deletions
@@ -7,8 +7,6 @@ from nonebot_plugin_session import EventSession
from zhenxun.configs.config import Config
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.llm.config.providers import get_llm_config
from zhenxun.services.llm.manager import clear_model_cache
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
@@ -56,8 +54,6 @@ _matcher = on_alconna(
@_matcher.handle()
async def _(session: EventSession, arparma: Arparma):
Config.reload()
get_llm_config.cache_clear()
clear_model_cache()
logger.debug("自动重载配置文件", arparma.header_result, session=session)
await MessageUtils.build_message("重载完成!").send(reply_to=True)
@@ -69,6 +65,4 @@ async def _(session: EventSession, arparma: Arparma):
async def _():
if Config.get_config("reload_setting", "AUTO_RELOAD"):
Config.reload()
get_llm_config.cache_clear()
clear_model_cache()
logger.debug("已自动重载配置文件...")
-128
View File
@@ -1,128 +0,0 @@
from typing import Any
from nonebot.plugin import PluginMetadata
from nonebot.rule import to_me
from nonebot_plugin_alconna import Alconna, Arparma, on_alconna
from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel, Field
from zhenxun.configs.utils import PluginExtraData
from zhenxun.services.log import logger
from zhenxun.services.page_template import PageTemplateConfig, template_manager
from zhenxun.services.page_template.components import (
Button,
ButtonProps,
Col,
ColProps,
Form,
FormItem,
FormItemProps,
FormProps,
Row,
RowProps,
)
__plugin_meta__ = PluginMetadata(
name="web测试",
description="想要更加了解真寻吗",
usage="""
指令:
关于
""".strip(),
extra=PluginExtraData(author="HibiKier", version="0.1", menu_type="其他").to_dict(),
)
_matcher = on_alconna(Alconna("test"), priority=5, block=True, rule=to_me())
@_matcher.handle()
async def _(session: Uninfo, arparma: Arparma):
logger.info("1")
def temp(a: dict[str, Any]):
pass
class UserFormData(BaseModel):
username: str = Field(..., min_length=3, max_length=20)
email: str
age: int | None = None
def register_user_form_template():
# 使用 list[Any] 避免 list 协变导致的类型告警
layout: list[Any] = [
Row(
props=RowProps(gutter=16),
children=[
Col(
props=ColProps(span=12),
children=[
Form(
props=FormProps(label_width="100px", inline=True),
children=[
FormItem(
props=FormItemProps(
label="用户名", prop="username"
),
children=None,
bind_field="username",
),
FormItem(
props=FormItemProps(label="邮箱", prop="email"),
children=None,
bind_field="email",
),
FormItem(
props=FormItemProps(label="年龄", prop="age"),
children=None,
bind_field="age",
),
FormItem(
props=FormItemProps(label=""),
children=[
Button(
props=ButtonProps(
text="提交",
type="primary",
action="submit",
confirm=True,
confirm_text="确认提交吗?",
),
),
Button(
props=ButtonProps(
text="重置",
type="default",
action="reset", # 前端重置表单
),
),
Button(
props=ButtonProps(
text="取消",
type="danger",
action="cancel", # 前端自行关闭/返回
),
),
],
bind_field=None,
),
],
)
],
)
],
)
]
config = PageTemplateConfig(
template_id="user_form",
title="用户表单示例",
description="包含提交/重置/取消按钮的示例表单",
layout=layout,
callback_handler=temp,
)
template_manager.register(config, data_model=UserFormData)
-17
View File
@@ -7,7 +7,6 @@ Zhenxun Bot - 核心服务模块
- LLM服务 (llm): 提供与大语言模型交互的统一API。
- 插件生命周期管理 (plugin_init): 支持插件安装和卸载时的钩子函数。
- 定时任务调度器 (scheduler): 提供持久化的、可管理的定时任务服务。
- 页面模板服务 (page_template_service): 用于构建前端页面(表格、表单等)并处理数据提交。
"""
from nonebot import require
@@ -45,15 +44,6 @@ from .llm import (
set_global_default_model_name,
)
from .log import logger
from .page_template import (
ColumnAlign,
FieldConfig,
FieldType,
PageTemplateConfig,
PageTemplateManager,
PageTemplateService,
template_manager,
)
from .plugin_init import PluginInit, PluginInitManager
from .renderer import renderer_service
from .scheduler import (
@@ -66,19 +56,13 @@ from .scheduler import (
__all__ = [
"AI",
"AIConfig",
"ColumnAlign",
"CommonOverrides",
"ExecutionPolicy",
"FieldConfig",
"FieldType",
"LLMContentPart",
"LLMException",
"LLMGenerationConfig",
"LLMMessage",
"Model",
"PageTemplateConfig",
"PageTemplateManager",
"PageTemplateService",
"PluginInit",
"PluginInitManager",
"ScheduleContext",
@@ -102,6 +86,5 @@ __all__ = [
"scheduler_manager",
"search",
"set_global_default_model_name",
"template_manager",
"with_db_timeout",
]
+1 -8
View File
@@ -34,13 +34,7 @@ from .manager import (
list_model_identifiers,
set_global_default_model_name,
)
from .memory import (
AIConfig,
BaseMemory,
MemoryProcessor,
set_default_memory_backend,
)
from .session import AI
from .session import AI, AIConfig, MemoryProcessor, set_default_memory_backend
from .tools import RunContext, ToolInvoker, function_tool, tool_provider_manager
from .types import (
EmbeddingTaskType,
@@ -68,7 +62,6 @@ from .utils import create_multimodal_message, message_to_unimessage, unimsg_to_l
__all__ = [
"AI",
"AIConfig",
"BaseMemory",
"CommonOverrides",
"EmbeddingTaskType",
"GeminiCodeExecution",
+1 -3
View File
@@ -30,7 +30,6 @@ from .types import (
ToolChoice,
)
from .types.exceptions import get_user_friendly_error_message
from .types.models import GeminiGoogleSearch
from .utils import create_multimodal_message
T = TypeVar("T", bound=BaseModel)
@@ -183,7 +182,7 @@ async def embed_documents(
async def generate_structured(
message: str | UniMessage | LLMMessage | list[LLMContentPart],
message: str | LLMMessage | list[LLMContentPart],
response_model: type[T],
*,
model: ModelName = None,
@@ -403,5 +402,4 @@ async def search(
model=model,
instruction=instruction,
config=final_config,
tools=[GeminiGoogleSearch()],
)
-243
View File
@@ -1,243 +0,0 @@
"""
LLM 服务 - 会话记忆模块
定义了LLM会话记忆的存储、策略和处理接口。
"""
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Callable
from typing import Any
from pydantic import BaseModel, Field
from zhenxun.services.llm.types import LLMMessage
from zhenxun.services.log import logger
class AIConfig(BaseModel):
"""AI配置类 (为保持独立性而在此处保留一个副本,实际使用中可能来自更高层)"""
model: Any = None
default_embedding_model: Any = None
default_preserve_media_in_history: bool = False
tool_providers: list[Any] = Field(default_factory=list)
def __post_init__(self):
"""初始化后从配置中读取默认值"""
pass
class BaseMessageStore(ABC):
"""
底层存储接口 (DAO - Data Access Object)。
这是一个抽象基类,定义了消息数据最底层的 **持久化与检索 (CRUD)** 接口。
它只关心数据的存取,不涉及任何业务逻辑(如历史记录修剪)。
开发者如果希望将对话历史存储到 Redis、数据库或其他持久化后端,
应当实现这个接口。
"""
@abstractmethod
async def get_messages(self, session_id: str) -> list[LLMMessage]:
"""
根据会话ID获取完整的消息列表。
"""
raise NotImplementedError
@abstractmethod
async def add_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
"""追加消息"""
raise NotImplementedError
@abstractmethod
async def set_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
"""
完全覆盖指定会话ID的消息列表。
主要用于历史记录修剪等场景。
"""
raise NotImplementedError
@abstractmethod
async def clear(self, session_id: str) -> None:
"""清空指定会话ID的所有消息数据。"""
raise NotImplementedError
class InMemoryMessageStore(BaseMessageStore):
"""
一个基于内存的 `BaseMessageStore` 实现。
它使用一个Python字典来存储所有会话的消息,提供了最简单、最快速的存储方案。
这是框架的默认存储方式,实现了开箱即用。
注意:此实现是 **非持久化** 的,当应用程序重启时,所有对话历史都会丢失。
适用于测试、简单应用或不需要长期记忆的场景。
"""
def __init__(self):
self._data: dict[str, list[LLMMessage]] = defaultdict(list)
async def get_messages(self, session_id: str) -> list[LLMMessage]:
"""从内存字典中获取消息列表的副本。"""
return self._data.get(session_id, []).copy()
async def add_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
"""向内存中的消息列表追加消息。"""
self._data[session_id].extend(messages)
async def set_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
"""在内存中直接替换指定会话的消息列表。"""
self._data[session_id] = messages
async def clear(self, session_id: str) -> None:
"""从内存字典中删除指定会话的条目。"""
if session_id in self._data:
del self._data[session_id]
class BaseMemory(ABC):
"""
记忆系统上层逻辑基类 (Strategy Layer)。
此抽象基类定义了记忆系统的 **策略层** 接口。它负责对外提供统一的记忆操作
接口,并封装了具体的记忆管理策略,如历史记录的修剪、摘要生成等。
`AI` 会话客户端直接与此接口交互,而不关心底层的存储实现。
开发者可以通过实现此接口来创建自定义的记忆管理策略,例如:
- `SummarizationMemory`: 在历史记录过长时,自动调用LLM生成摘要来压缩历史。
- `VectorStoreMemory`: 将对话历史向量化并存入向量数据库,实现长期记忆检索。
"""
@abstractmethod
async def get_history(self, session_id: str) -> list[LLMMessage]:
"""获取用于构建模型输入的完整历史消息列表。"""
raise NotImplementedError
async def add_message(self, session_id: str, message: LLMMessage) -> None:
"""向记忆中添加单条消息。默认实现是调用 `add_messages`。"""
await self.add_messages(session_id, [message])
@abstractmethod
async def add_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
"""向记忆中添加多条消息,并可能触发内部的记忆管理策略(如修剪)。"""
raise NotImplementedError
@abstractmethod
async def clear_history(self, session_id: str) -> None:
"""清空指定会话的全部记忆。"""
raise NotImplementedError
class ChatMemory(BaseMemory):
"""
标准聊天记忆实现:组合 Store + 滑动窗口策略。
这是 `BaseMemory` 的默认实现,它通过组合一个 `BaseMessageStore` 实例来
完成实际的数据存储,并在此之上实现了一个简单的“滑动窗口”记忆修剪策略。
"""
def __init__(self, store: BaseMessageStore, max_messages: int = 50):
self.store = store
self._max_messages = max_messages
async def _trim_history(self, session_id: str) -> None:
"""
记忆修剪策略:确保历史记录不超过 `_max_messages` 条。
如果存在系统消息 (System Prompt),它将被永久保留在列表的第一位。
"""
history = await self.store.get_messages(session_id)
if len(history) <= self._max_messages:
return
has_system = history and history[0].role == "system"
new_history: list[LLMMessage] = []
if has_system:
keep_count = max(0, self._max_messages - 1)
new_history = [history[0], *history[-keep_count:]]
else:
new_history = history[-self._max_messages :]
await self.store.set_messages(session_id, new_history)
async def get_history(self, session_id: str) -> list[LLMMessage]:
"""直接从底层存储获取历史记录。"""
return await self.store.get_messages(session_id)
async def add_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
"""添加消息到历史记录,并立即执行修剪策略。"""
await self.store.add_messages(session_id, messages)
await self._trim_history(session_id)
async def clear_history(self, session_id: str) -> None:
"""清空底层存储中的历史记录。"""
await self.store.clear(session_id)
class MemoryProcessor(ABC):
"""
记忆处理器接口 (Hook/Observer)。
这是一个扩展接口,允许开发者创建自定义的“记忆处理器”,以在记忆被修改后
执行额外的操作(“钩子”)。
当 `AI` 实例的记忆更新时,它会依次调用所有注册的 `MemoryProcessor`。
使用场景示例:
- `LoggingMemoryProcessor`: 将每一轮对话异步记录到外部日志系统。
- `SummarizationProcessor`: 在后台任务中检查对话长度,并在需要时生成摘要。
- `EntityExtractionProcessor`: 从对话中提取关键实体(如人名、地名)并存储。
"""
@abstractmethod
async def process(self, session_id: str, new_messages: list[LLMMessage]) -> None:
"""处理新添加到记忆中的消息。"""
pass
_default_memory_factory: Callable[[], BaseMemory] | None = None
def set_default_memory_backend(factory: Callable[[], BaseMemory]):
"""
设置全局默认记忆后端工厂,允许统一替换会话的记忆实现。
这是一个高级依赖注入函数,允许插件或项目在启动时用自定义的 `BaseMemory`
实现替换掉默认的 `ChatMemory(InMemoryMessageStore())`。
Args:
factory: 一个无参数的、返回 `BaseMemory` 实例的函数或类。
"""
global _default_memory_factory
_default_memory_factory = factory
def _get_default_memory() -> BaseMemory:
"""
[内部函数] 获取一个默认的记忆后端实例。
它会首先检查是否有通过 `set_default_memory_backend` 设置的全局工厂,
如果有,则使用该工厂创建实例;否则,返回一个标准的内存记忆实例。
"""
if _default_memory_factory:
logger.debug("使用自定义的默认记忆后端工厂构建实例。")
return _default_memory_factory()
logger.debug("未配置自定义记忆后端,使用默认的 ChatMemory。")
return ChatMemory(store=InMemoryMessageStore())
__all__ = [
"AIConfig",
"BaseMemory",
"BaseMessageStore",
"ChatMemory",
"InMemoryMessageStore",
"MemoryProcessor",
"_get_default_memory",
"set_default_memory_backend",
]
+106 -11
View File
@@ -4,8 +4,11 @@ LLM 服务 - 会话客户端
提供一个有状态的、面向会话的 LLM 客户端,用于进行多轮对话和复杂交互。
"""
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Awaitable, Callable
import copy
from dataclasses import dataclass, field
import json
from typing import Any, TypeVar, cast
import uuid
@@ -25,14 +28,8 @@ from .config import (
LLMGenerationConfig,
)
from .config.generation import OutputConfig
from .config.providers import get_llm_config
from .config.providers import get_ai_config, get_llm_config
from .manager import get_global_default_model_name, get_model_instance
from .memory import (
AIConfig,
BaseMemory,
MemoryProcessor,
_get_default_memory,
)
from .tools import tool_provider_manager
from .types import (
LLMContentPart,
@@ -45,6 +42,7 @@ from .types import (
StructuredOutputStrategy,
ToolChoice,
ToolExecutable,
ToolProvider,
)
from .types.models import (
GeminiCodeExecution,
@@ -59,6 +57,105 @@ from .utils import (
T = TypeVar("T", bound=BaseModel)
@dataclass
class AIConfig:
"""AI配置类"""
model: ModelName = None
default_embedding_model: ModelName = None
default_preserve_media_in_history: bool = False
tool_providers: list[ToolProvider] = field(default_factory=list)
def __post_init__(self):
"""初始化后从配置中读取默认值"""
ai_config = get_ai_config()
if self.model is None:
self.model = ai_config.get("default_model_name")
class BaseMemory(ABC):
"""记忆系统的抽象基类。"""
@abstractmethod
async def get_history(self, session_id: str) -> list[LLMMessage]:
raise NotImplementedError
@abstractmethod
async def add_message(self, session_id: str, message: LLMMessage) -> None:
raise NotImplementedError
@abstractmethod
async def add_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
raise NotImplementedError
@abstractmethod
async def clear_history(self, session_id: str) -> None:
raise NotImplementedError
class InMemoryMemory(BaseMemory):
"""一个简单的、默认的内存记忆后端。"""
def __init__(self, max_messages: int = 50, **kwargs: Any):
self._history: dict[str, list[LLMMessage]] = defaultdict(list)
self._max_messages = max_messages
def _trim_history(self, session_id: str) -> None:
"""修剪历史记录,确保不超过最大长度,同时保留 System Prompt"""
history = self._history[session_id]
if len(history) <= self._max_messages:
return
has_system = history and history[0].role == "system"
if has_system:
keep_count = max(0, self._max_messages - 1)
self._history[session_id] = [history[0], *history[-keep_count:]]
else:
self._history[session_id] = history[-self._max_messages :]
async def get_history(self, session_id: str) -> list[LLMMessage]:
return self._history.get(session_id, []).copy()
async def add_message(self, session_id: str, message: LLMMessage) -> None:
self._history[session_id].append(message)
self._trim_history(session_id)
async def add_messages(self, session_id: str, messages: list[LLMMessage]) -> None:
self._history[session_id].extend(messages)
self._trim_history(session_id)
async def clear_history(self, session_id: str) -> None:
if session_id in self._history:
del self._history[session_id]
class MemoryProcessor(ABC):
"""记忆处理器接口"""
@abstractmethod
async def process(self, session_id: str, new_messages: list[LLMMessage]) -> None:
pass
_default_memory_factory: Callable[[], BaseMemory] | None = None
def set_default_memory_backend(factory: Callable[[], BaseMemory]):
"""
设置全局默认记忆后端工厂,允许统一替换会话的记忆实现。
"""
global _default_memory_factory
_default_memory_factory = factory
def _get_default_memory() -> BaseMemory:
if _default_memory_factory:
return _default_memory_factory()
return InMemoryMemory()
DEFAULT_IVR_TEMPLATE = (
"你的响应未能通过结构校验。\n"
"错误详情: {error_msg}\n\n"
@@ -89,8 +186,7 @@ class AI:
参数:
session_id: 唯一的会话ID,用于隔离记忆。
config: AI 配置.
memory: 可选的自定义记忆后端。如果为None,则使用默认的 ChatMemory
(InMemoryMessageStore)。
memory: 可选的自定义记忆后端。如果为None,则使用默认的InMemoryMemory。
default_generation_config: 此AI实例的默认生成配置。
processors: 记忆处理器列表,在添加记忆后触发。
"""
@@ -445,12 +541,11 @@ class AI:
instruction=instruction,
template_vars=template_vars,
config=search_config,
tools=[GeminiGoogleSearch()],
)
async def generate_structured(
self,
message: str | UniMessage | LLMMessage | list[LLMContentPart] | None,
message: str | LLMMessage | list[LLMContentPart] | None,
response_model: type[T],
*,
model: ModelName = None,
@@ -1,59 +0,0 @@
"""
页面模板服务模块
提供页面模板配置、字段定义和数据验证功能。
"""
from .components import (
Button,
ButtonProps,
Card,
CardProps,
Col,
ColProps,
Component,
ComponentType,
Divider,
Form,
FormItem,
FormItemProps,
FormProps,
Row,
RowProps,
Space,
Table,
TableProps,
Text,
TextProps,
)
from .service import PageTemplateConfig, PageTemplateManager, PageTemplateService
# 创建全局模板管理器实例
template_manager = PageTemplateManager()
__all__ = [
"Button",
"ButtonProps",
"Card",
"CardProps",
"Col",
"ColProps",
"Component",
"ComponentType",
"Divider",
"Form",
"FormItem",
"FormItemProps",
"FormProps",
"PageTemplateConfig",
"PageTemplateManager",
"PageTemplateService",
"Row",
"RowProps",
"Space",
"Table",
"TableProps",
"Text",
"TextProps",
"template_manager",
]
@@ -1,55 +0,0 @@
"""
前端布局组件模型集合
用于以数据形式描述页面布局,标签与前端 Element 组件保持一致:
- el-row, el-col
- el-text
- el-button
- el-card, el-divider, el-space, el-form, el-form-item, el-table
"""
from .layout import (
Button,
ButtonProps,
Card,
CardProps,
Col,
ColProps,
Component,
ComponentType,
Divider,
Form,
FormItem,
FormItemProps,
FormProps,
Row,
RowProps,
Space,
Table,
TableProps,
Text,
TextProps,
)
__all__ = [
"Button",
"ButtonProps",
"Card",
"CardProps",
"Col",
"ColProps",
"Component",
"ComponentType",
"Divider",
"Form",
"FormItem",
"FormItemProps",
"FormProps",
"Row",
"RowProps",
"Space",
"Table",
"TableProps",
"Text",
"TextProps",
]
@@ -1,207 +0,0 @@
from enum import Enum
from typing import Any
from pydantic import BaseModel
class ComponentType(str, Enum):
"""组件类型,名称与前端 tag 保持一致"""
ROW = "row"
COL = "col"
TEXT = "text"
BUTTON = "button"
CARD = "card"
DIVIDER = "divider"
SPACE = "space"
FORM = "form"
FORM_ITEM = "form_item"
TABLE = "table"
class RowProps(BaseModel):
"""行组件属性"""
gutter: int | None = None # 行间距
justify: str | None = None # 主轴对齐方式
align: str | None = None # 交叉轴对齐方式
class ColProps(BaseModel):
"""列组件属性"""
span: int | None = None # 栅格占比
offset: int | None = None # 左侧偏移
push: int | None = None # 向右移动
pull: int | None = None # 向左移动
class TextProps(BaseModel):
"""文本组件属性"""
content: str = "" # 文本内容
tag: str | None = None # HTML 标签,如 h1/h2/p/span
type: str | None = None # 文本类型,对应 el-text 的 type
size: str | None = None # 文本大小
truncated: bool = False # 是否截断
line_clamp: int | None = None # 最多显示行数
class ButtonProps(BaseModel):
"""按钮组件属性"""
text: str # 按钮文本(必填)
type: str | None = None # 按钮类型 primary/success/warning/danger/info/default
size: str | None = None # 按钮尺寸 large/default/small
plain: bool = False # 朴素按钮
round: bool = False # 圆角按钮
circle: bool = False # 圆形按钮
link: bool = False # 文字按钮
icon: str | None = None # 图标名称
action: str | None = None # 按钮行为:submit/reset/cancel/custom
confirm: bool = False # 是否需要二次确认
confirm_text: str | None = None # 确认提示文案
api: str | None = None # 自定义调用的后端 API(action=custom 时使用)
api_method: str = "POST" # 自定义 API 的 HTTP 方法
class CardProps(BaseModel):
"""卡片组件属性"""
header: str | None = None # 卡片标题
shadow: str | None = None # 阴影类型
body_style: dict[str, Any] | None = None # 卡片主体样式
class FormProps(BaseModel):
"""表单组件属性"""
label_width: str | None = None # 标签宽度
inline: bool = False # 行内表单
size: str | None = None # 表单尺寸
class FormItemProps(BaseModel):
"""表单项组件属性"""
label: str | None = None # 标签文本
prop: str | None = None # 绑定字段名
required: bool = False # 是否必填
class TableProps(BaseModel):
"""表格组件属性"""
columns: list[dict[str, Any]] | None = None # 列配置
data: list[dict[str, Any]] | None = None # 数据源
class BaseComponent(BaseModel):
# 这些字段在 BaseModel 中有默认值,调用时都不是必传
# 用 Any 以便可以直接传 TextProps / ButtonProps / FormProps 等模型实例
props: Any | None
# 用 Any 避免 list 协变问题
children: list[Any] | None = None
class Config:
arbitrary_types_allowed = True
def __init__(self, **data: Any):
super().__init__(**data)
_validate_children_impl(self.__dict__)
def _validate_children_impl(values: dict[str, Any]) -> dict[str, Any]:
"""
限定哪些组件可以拥有 children:
- 允许 children: row, col, card, space, form, form_item, table
- 不允许 children: text, button, divider
"""
t = values.get("type")
children = values.get("children") or []
no_children = {
ComponentType.TEXT.value,
ComponentType.BUTTON.value,
ComponentType.DIVIDER.value,
}
if t in no_children and children:
raise ValueError(f"组件 '{t}' 不允许包含 children")
return values
class Row(BaseComponent):
"""行组件(对应 el-row)"""
type = ComponentType.ROW.value
props: RowProps # 必填
class Col(BaseComponent):
"""列组件(对应 el-col)"""
type = ComponentType.COL.value
props: ColProps # 必填
class Text(BaseComponent):
"""文本组件(对应 el-text 或基础标签)"""
type = ComponentType.TEXT.value
props: TextProps # 必填
class Button(BaseComponent):
"""按钮组件(对应 el-button)"""
type = ComponentType.BUTTON.value
props: ButtonProps # 必填
class Card(BaseComponent):
"""卡片组件(对应 el-card)"""
type = ComponentType.CARD.value
props: CardProps # 必填
class Divider(BaseComponent):
"""分割线组件(对应 el-divider)"""
type = ComponentType.DIVIDER.value
class Space(BaseComponent):
"""间距组件(对应 el-space)"""
type = ComponentType.SPACE.value
class Form(BaseComponent):
"""表单组件(对应 el-form)"""
type = ComponentType.FORM.value
props: FormProps # 必填
class FormItem(BaseComponent):
"""表单项组件(对应 el-form-item)"""
type = ComponentType.FORM_ITEM.value
props: FormItemProps # 必填
children: list[Any] | None = None
bind_field: str | None = None
class Table(BaseComponent):
"""表格组件(对应 el-table)"""
type = ComponentType.TABLE.value
Component = Row | Col | Text | Button | Card | Divider | Space | Form | FormItem | Table
try:
BaseComponent.model_rebuild()
except AttributeError:
BaseComponent.update_forward_refs()
-94
View File
@@ -1,94 +0,0 @@
"""
页面模板服务 FastAPI 路由
提供统一的API接口,通过template_id来获取配置和处理数据提交。
"""
from fastapi import APIRouter, Body, Depends, Query
from fastapi.responses import JSONResponse
from zhenxun.builtin_plugins.web_ui.base_model import Result
from zhenxun.builtin_plugins.web_ui.utils import authentication
from zhenxun.services.log import logger
from zhenxun.services.page_template import template_manager
router = APIRouter(prefix="/page_template")
@router.get(
"/template_config",
dependencies=[Depends(authentication())],
response_model=Result[dict],
response_class=JSONResponse,
description="获取页面模板配置(表格配置)",
)
async def get_template_config(template_id: str = Query(..., description="模板ID")):
"""获取页面模板配置"""
try:
config = template_manager.get_template_config(template_id)
if config is None:
return Result.fail(f"模板ID '{template_id}' 不存在")
return Result.ok(config, "获取配置成功")
except Exception as e:
logger.error(f"{router.prefix}/template_config 调用错误", "PageTemplate", e=e)
return Result.fail(f"获取配置失败: {type(e)}: {e}")
@router.get(
"/form_config",
dependencies=[Depends(authentication())],
response_model=Result[dict],
response_class=JSONResponse,
description="获取表单配置",
)
async def get_form_config(template_id: str = Query(..., description="模板ID")):
"""获取表单配置"""
try:
config = template_manager.get_form_config(template_id)
if config is None:
return Result.fail(f"模板ID '{template_id}' 不存在")
return Result.ok(config, "获取表单配置成功")
except Exception as e:
logger.error(f"{router.prefix}/form_config 调用错误", "PageTemplate", e=e)
return Result.fail(f"获取表单配置失败: {type(e)}: {e}")
@router.post(
"/submit",
dependencies=[Depends(authentication())],
response_model=Result,
response_class=JSONResponse,
description="提交数据",
)
async def submit_data(
template_id: str = Query(..., description="模板ID"),
data: dict = Body(..., description="提交的数据"),
):
"""处理数据提交"""
try:
success, message, result = await template_manager.process_submit(
template_id, data
)
if not success:
return Result.fail(message)
return Result.ok(info=message, data=result)
except Exception as e:
logger.error(f"{router.prefix}/submit 调用错误", "PageTemplate", e=e)
return Result.fail(f"提交数据失败: {type(e)}: {e}")
@router.get(
"/list",
dependencies=[Depends(authentication())],
response_model=Result[list[str]],
response_class=JSONResponse,
description="获取所有已注册的模板ID列表",
)
async def list_templates():
"""获取所有已注册的模板ID列表"""
try:
template_ids = template_manager.list_templates()
return Result.ok(template_ids, "获取模板列表成功")
except Exception as e:
logger.error(f"{router.prefix}/list 调用错误", "PageTemplate", e=e)
return Result.fail(f"获取模板列表失败: {type(e)}: {e}")
-314
View File
@@ -1,314 +0,0 @@
"""
页面模板服务
用于构建前端页面(如表格、表单等),支持字段绑定和数据提交处理。
"""
from collections.abc import Callable
from typing import Any, Generic, TypeVar
from pydantic import BaseModel, Field, ValidationError
from zhenxun.services.log import logger
from zhenxun.services.page_template.components import Component
T = TypeVar("T", bound=BaseModel)
class PageTemplateConfig(BaseModel):
"""页面模板配置"""
template_id: str = Field(..., description="模板ID")
"""模板ID"""
title: str = Field(..., description="页面标题")
"""页面标题"""
description: str | None = Field(None, description="页面描述")
"""页面描述"""
callback_handler: Callable[[dict[str, Any]], Any] | None = Field(
None,
description="数据提交后的回调处理方法(异步或同步函数,接收验证后的数据字典)",
)
"""数据提交后的回调处理方法(异步或同步函数,接收验证后的数据字典)"""
layout: list[Component] = Field(
default_factory=list,
description="页面布局组件树,使用 row/col/text/button 等组件描述前端结构",
)
"""页面布局组件树"""
class Config:
arbitrary_types_allowed = True
class PageTemplateService(Generic[T]):
"""页面模板服务"""
def __init__(self, config: PageTemplateConfig, data_model: type[T] | None = None):
"""
初始化页面模板服务
参数:
config: 页面模板配置
data_model: 数据模型类(可选,用于数据验证)
"""
self.config = config
self.data_model = data_model
def get_table_config(self) -> dict[str, Any]:
"""
获取表格配置(用于前端渲染表格)
返回:
包含表格配置的字典
"""
return {
"template_id": self.config.template_id,
"title": self.config.title,
"description": self.config.description,
"layout": self.get_layout_config(),
}
def get_form_config(self) -> dict[str, Any]:
"""
获取表单配置(用于前端渲染表单)
返回:
包含表单配置的字典
"""
return {
"template_id": self.config.template_id,
"title": self.config.title,
"description": self.config.description,
"layout": self.get_layout_config(),
}
def get_layout_config(self) -> list[dict[str, Any]]:
"""
获取页面布局配置(组件树)
返回:
布局组件的列表(字典形式,适合前端直接渲染)
"""
from zhenxun.utils.pydantic_compat import model_dump
return [model_dump(node, exclude_none=True) for node in self.config.layout]
def validate_data(self, data: dict[str, Any]) -> tuple[bool, str | None, T | None]:
"""
验证提交的数据
参数:
data: 待验证的数据字典
返回:
元组 (是否有效, 错误信息, 验证后的数据模型实例)
"""
# 如果提供了数据模型,使用Pydantic验证
if self.data_model:
try:
validated_data = self.data_model(**data)
return True, None, validated_data
except ValidationError as e:
error_messages = []
for error in e.errors():
field_name = ".".join(str(loc) for loc in error["loc"])
error_messages.append(f"{field_name}: {error['msg']}")
return False, "; ".join(error_messages), None
return True, None, None
def process_submit_data(
self, data: dict[str, Any]
) -> tuple[bool, str, dict[str, Any]]:
"""
处理提交的数据(验证并返回处理后的数据)
参数:
data: 提交的数据字典
返回:
元组 (是否成功, 消息, 处理后的数据)
"""
is_valid, error_msg, validated_model = self.validate_data(data)
if not is_valid:
return False, error_msg or "数据验证失败", {}
# 如果验证成功且有模型,返回模型数据
if validated_model:
from zhenxun.utils.pydantic_compat import model_dump
return True, "数据验证成功", model_dump(validated_model)
# 否则返回原始数据(已通过验证)
return True, "数据验证成功", data
class PageTemplateManager:
"""页面模板管理器(全局单例)"""
_instance: "PageTemplateManager | None" = None
_templates: dict[str, PageTemplateService[Any]]
def __new__(cls) -> "PageTemplateManager":
"""单例模式"""
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._templates = {}
return cls._instance
def register(
self,
config: PageTemplateConfig,
data_model: type[T] | None = None,
) -> PageTemplateService[T]:
"""
注册页面模板
参数:
config: 页面模板配置
data_model: 数据模型类(可选,用于数据验证)
返回:
PageTemplateService实例
异常:
ValueError: 如果template_id已存在
"""
if config.template_id in self._templates:
raise ValueError(
f"模板ID '{config.template_id}' 已存在,请使用不同的template_id"
)
service = PageTemplateService(config, data_model)
self._templates[config.template_id] = service
logger.info(f"已注册页面模板: {config.template_id} - {config.title}")
return service
def get(self, template_id: str) -> PageTemplateService[Any] | None:
"""
获取页面模板服务
参数:
template_id: 模板ID
返回:
PageTemplateService实例,如果不存在则返回None
"""
return self._templates.get(template_id)
def unregister(self, template_id: str) -> bool:
"""
注销页面模板
参数:
template_id: 模板ID
返回:
是否成功注销
"""
if template_id in self._templates:
del self._templates[template_id]
logger.info(f"已注销页面模板: {template_id}")
return True
return False
def list_templates(self) -> list[str]:
"""
列出所有已注册的模板ID
返回:
模板ID列表
"""
return list(self._templates.keys())
def get_template_config(self, template_id: str) -> dict[str, Any] | None:
"""
获取模板配置(表格配置)
参数:
template_id: 模板ID
返回:
表格配置字典,如果模板不存在则返回None
"""
service = self.get(template_id)
return service.get_table_config() if service else None
def get_form_config(self, template_id: str) -> dict[str, Any] | None:
"""
获取表单配置
参数:
template_id: 模板ID
返回:
表单配置字典,如果模板不存在则返回None
"""
service = self.get(template_id)
return service.get_form_config() if service else None
async def process_submit(
self, template_id: str, data: dict[str, Any]
) -> tuple[bool, str, dict[str, Any] | None]:
"""
处理数据提交
参数:
template_id: 模板ID
data: 提交的数据字典
返回:
元组 (是否成功, 消息, 处理后的数据或回调结果)
"""
service = self.get(template_id)
if not service:
return False, f"模板ID '{template_id}' 不存在", None
# 验证数据
success, message, processed_data = service.process_submit_data(data)
if not success:
return False, message, None
# 如果有回调处理器,执行回调
if service.config.callback_handler:
try:
result = await self._call_handler(
service.config.callback_handler, processed_data
)
return True, message, result
except Exception as e:
handler_name = getattr(
service.config.callback_handler, "__name__", "unknown"
)
logger.error(
f"执行回调处理器失败: {handler_name}",
"PageTemplate",
e=e,
)
return False, f"执行回调处理器失败: {e!s}", None
return True, message, processed_data
async def _call_handler(
self, handler: Callable[[dict[str, Any]], Any], data: dict[str, Any]
) -> Any:
"""
调用回调处理器
参数:
handler: 回调处理函数
data: 要传递的数据
返回:
处理器的返回值
"""
if not callable(handler):
raise ValueError("回调处理器必须是一个可调用对象")
import inspect
# 检查是否是异步函数
if inspect.iscoroutinefunction(handler):
return await handler(data)
else:
return handler(data)