mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-11-05 20:30:03 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a6d9a58b1 | ||
|
|
55377483c0 | ||
|
|
bba90e62db |
Generated
+5578
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ feedparser = "^6.0.11"
|
|||||||
imagehash = "^4.3.1"
|
imagehash = "^4.3.1"
|
||||||
cn2an = "^0.5.22"
|
cn2an = "^0.5.22"
|
||||||
dateparser = "^1.2.0"
|
dateparser = "^1.2.0"
|
||||||
|
bilireq = ">=0.2.10"
|
||||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||||
python-multipart = "^0.0.9"
|
python-multipart = "^0.0.9"
|
||||||
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
||||||
@@ -46,10 +47,10 @@ nonebot-plugin-uninfo = ">=0.7.3"
|
|||||||
nonebot-plugin-waiter = "^0.8.1"
|
nonebot-plugin-waiter = "^0.8.1"
|
||||||
multidict = ">=6.0.0,!=6.3.2"
|
multidict = ">=6.0.0,!=6.3.2"
|
||||||
pydantic = ">=1.0.0, <2.0.0"
|
pydantic = ">=1.0.0, <2.0.0"
|
||||||
|
|
||||||
redis = { version = ">=5", optional = true }
|
redis = { version = ">=5", optional = true }
|
||||||
asyncpg = { version = ">=0.20.0", optional = true }
|
asyncpg = { version = ">=0.20.0", optional = true }
|
||||||
alibabacloud-devops20210625 = "^5.0.2"
|
alibabacloud-devops20210625 = "^5.0.2"
|
||||||
json_repair = "^0.54.0"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
nonebug = "^0.4"
|
nonebug = "^0.4"
|
||||||
|
|||||||
Generated
+5688
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,7 @@ feedparser = "^6.0.11"
|
|||||||
imagehash = "^4.3.1"
|
imagehash = "^4.3.1"
|
||||||
cn2an = "^0.5.22"
|
cn2an = "^0.5.22"
|
||||||
dateparser = "^1.2.0"
|
dateparser = "^1.2.0"
|
||||||
|
bilireq = ">=0.2.10"
|
||||||
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
python-jose = { extras = ["cryptography"], version = "^3.3.0" }
|
||||||
python-multipart = "^0.0.9"
|
python-multipart = "^0.0.9"
|
||||||
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
aiocache = {extras = ["redis"], version = "^0.12.3"}
|
||||||
@@ -46,10 +47,10 @@ nonebot-plugin-uninfo = ">=0.7.3"
|
|||||||
nonebot-plugin-waiter = "^0.8.1"
|
nonebot-plugin-waiter = "^0.8.1"
|
||||||
multidict = ">=6.0.0,!=6.3.2"
|
multidict = ">=6.0.0,!=6.3.2"
|
||||||
pydantic = ">=2.0.0, <3.0.0"
|
pydantic = ">=2.0.0, <3.0.0"
|
||||||
|
|
||||||
redis = { version = ">=5", optional = true }
|
redis = { version = ">=5", optional = true }
|
||||||
asyncpg = { version = ">=0.20.0", optional = true }
|
asyncpg = { version = ">=0.20.0", optional = true }
|
||||||
alibabacloud-devops20210625 = "^5.0.2"
|
alibabacloud-devops20210625 = "^5.0.2"
|
||||||
json_repair = "^0.54.0"
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
nonebug = "^0.4"
|
nonebug = "^0.4"
|
||||||
|
|||||||
+2
-1
@@ -21,6 +21,7 @@ feedparser>=6.0.11,<7.0.0
|
|||||||
ImageHash>=4.3.1,<5.0.0
|
ImageHash>=4.3.1,<5.0.0
|
||||||
cn2an>=0.5.22,<0.6.0
|
cn2an>=0.5.22,<0.6.0
|
||||||
dateparser>=1.2.0,<2.0.0
|
dateparser>=1.2.0,<2.0.0
|
||||||
|
bilireq>=0.2.10
|
||||||
python-jose[cryptography]>=3.3.0,<4.0.0
|
python-jose[cryptography]>=3.3.0,<4.0.0
|
||||||
python-multipart>=0.0.9,<0.1.0
|
python-multipart>=0.0.9,<0.1.0
|
||||||
aiocache[redis]>=0.12.3,<0.13.0
|
aiocache[redis]>=0.12.3,<0.13.0
|
||||||
@@ -31,6 +32,6 @@ nonebot-plugin-uninfo>=0.7.3
|
|||||||
nonebot-plugin-waiter>=0.8.1,<0.9.0
|
nonebot-plugin-waiter>=0.8.1,<0.9.0
|
||||||
multidict>=6.0.0,<7.0.0,!=6.3.2
|
multidict>=6.0.0,<7.0.0,!=6.3.2
|
||||||
alibabacloud-devops20210625>=5.0.2,<6.0.0
|
alibabacloud-devops20210625>=5.0.2,<6.0.0
|
||||||
json_repair>=0.54.0,<0.55.0
|
|
||||||
redis>=5
|
redis>=5
|
||||||
asyncpg>=0.20.0
|
asyncpg>=0.20.0
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ from nonebot_plugin_session import EventSession
|
|||||||
|
|
||||||
from zhenxun.configs.config import Config
|
from zhenxun.configs.config import Config
|
||||||
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
|
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.services.log import logger
|
||||||
from zhenxun.utils.enum import PluginType
|
from zhenxun.utils.enum import PluginType
|
||||||
from zhenxun.utils.message import MessageUtils
|
from zhenxun.utils.message import MessageUtils
|
||||||
@@ -56,8 +54,6 @@ _matcher = on_alconna(
|
|||||||
@_matcher.handle()
|
@_matcher.handle()
|
||||||
async def _(session: EventSession, arparma: Arparma):
|
async def _(session: EventSession, arparma: Arparma):
|
||||||
Config.reload()
|
Config.reload()
|
||||||
get_llm_config.cache_clear()
|
|
||||||
clear_model_cache()
|
|
||||||
logger.debug("自动重载配置文件", arparma.header_result, session=session)
|
logger.debug("自动重载配置文件", arparma.header_result, session=session)
|
||||||
await MessageUtils.build_message("重载完成!").send(reply_to=True)
|
await MessageUtils.build_message("重载完成!").send(reply_to=True)
|
||||||
|
|
||||||
@@ -69,6 +65,4 @@ async def _(session: EventSession, arparma: Arparma):
|
|||||||
async def _():
|
async def _():
|
||||||
if Config.get_config("reload_setting", "AUTO_RELOAD"):
|
if Config.get_config("reload_setting", "AUTO_RELOAD"):
|
||||||
Config.reload()
|
Config.reload()
|
||||||
get_llm_config.cache_clear()
|
|
||||||
clear_model_cache()
|
|
||||||
logger.debug("已自动重载配置文件...")
|
logger.debug("已自动重载配置文件...")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -7,7 +7,6 @@ Zhenxun Bot - 核心服务模块
|
|||||||
- LLM服务 (llm): 提供与大语言模型交互的统一API。
|
- LLM服务 (llm): 提供与大语言模型交互的统一API。
|
||||||
- 插件生命周期管理 (plugin_init): 支持插件安装和卸载时的钩子函数。
|
- 插件生命周期管理 (plugin_init): 支持插件安装和卸载时的钩子函数。
|
||||||
- 定时任务调度器 (scheduler): 提供持久化的、可管理的定时任务服务。
|
- 定时任务调度器 (scheduler): 提供持久化的、可管理的定时任务服务。
|
||||||
- 页面模板服务 (page_template_service): 用于构建前端页面(表格、表单等)并处理数据提交。
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from nonebot import require
|
from nonebot import require
|
||||||
@@ -45,15 +44,6 @@ from .llm import (
|
|||||||
set_global_default_model_name,
|
set_global_default_model_name,
|
||||||
)
|
)
|
||||||
from .log import logger
|
from .log import logger
|
||||||
from .page_template import (
|
|
||||||
ColumnAlign,
|
|
||||||
FieldConfig,
|
|
||||||
FieldType,
|
|
||||||
PageTemplateConfig,
|
|
||||||
PageTemplateManager,
|
|
||||||
PageTemplateService,
|
|
||||||
template_manager,
|
|
||||||
)
|
|
||||||
from .plugin_init import PluginInit, PluginInitManager
|
from .plugin_init import PluginInit, PluginInitManager
|
||||||
from .renderer import renderer_service
|
from .renderer import renderer_service
|
||||||
from .scheduler import (
|
from .scheduler import (
|
||||||
@@ -66,19 +56,13 @@ from .scheduler import (
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"AI",
|
"AI",
|
||||||
"AIConfig",
|
"AIConfig",
|
||||||
"ColumnAlign",
|
|
||||||
"CommonOverrides",
|
"CommonOverrides",
|
||||||
"ExecutionPolicy",
|
"ExecutionPolicy",
|
||||||
"FieldConfig",
|
|
||||||
"FieldType",
|
|
||||||
"LLMContentPart",
|
"LLMContentPart",
|
||||||
"LLMException",
|
"LLMException",
|
||||||
"LLMGenerationConfig",
|
"LLMGenerationConfig",
|
||||||
"LLMMessage",
|
"LLMMessage",
|
||||||
"Model",
|
"Model",
|
||||||
"PageTemplateConfig",
|
|
||||||
"PageTemplateManager",
|
|
||||||
"PageTemplateService",
|
|
||||||
"PluginInit",
|
"PluginInit",
|
||||||
"PluginInitManager",
|
"PluginInitManager",
|
||||||
"ScheduleContext",
|
"ScheduleContext",
|
||||||
@@ -102,6 +86,5 @@ __all__ = [
|
|||||||
"scheduler_manager",
|
"scheduler_manager",
|
||||||
"search",
|
"search",
|
||||||
"set_global_default_model_name",
|
"set_global_default_model_name",
|
||||||
"template_manager",
|
|
||||||
"with_db_timeout",
|
"with_db_timeout",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -34,13 +34,7 @@ from .manager import (
|
|||||||
list_model_identifiers,
|
list_model_identifiers,
|
||||||
set_global_default_model_name,
|
set_global_default_model_name,
|
||||||
)
|
)
|
||||||
from .memory import (
|
from .session import AI, AIConfig, MemoryProcessor, set_default_memory_backend
|
||||||
AIConfig,
|
|
||||||
BaseMemory,
|
|
||||||
MemoryProcessor,
|
|
||||||
set_default_memory_backend,
|
|
||||||
)
|
|
||||||
from .session import AI
|
|
||||||
from .tools import RunContext, ToolInvoker, function_tool, tool_provider_manager
|
from .tools import RunContext, ToolInvoker, function_tool, tool_provider_manager
|
||||||
from .types import (
|
from .types import (
|
||||||
EmbeddingTaskType,
|
EmbeddingTaskType,
|
||||||
@@ -68,7 +62,6 @@ from .utils import create_multimodal_message, message_to_unimessage, unimsg_to_l
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
"AI",
|
"AI",
|
||||||
"AIConfig",
|
"AIConfig",
|
||||||
"BaseMemory",
|
|
||||||
"CommonOverrides",
|
"CommonOverrides",
|
||||||
"EmbeddingTaskType",
|
"EmbeddingTaskType",
|
||||||
"GeminiCodeExecution",
|
"GeminiCodeExecution",
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from .types import (
|
|||||||
ToolChoice,
|
ToolChoice,
|
||||||
)
|
)
|
||||||
from .types.exceptions import get_user_friendly_error_message
|
from .types.exceptions import get_user_friendly_error_message
|
||||||
from .types.models import GeminiGoogleSearch
|
|
||||||
from .utils import create_multimodal_message
|
from .utils import create_multimodal_message
|
||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
T = TypeVar("T", bound=BaseModel)
|
||||||
@@ -183,7 +182,7 @@ async def embed_documents(
|
|||||||
|
|
||||||
|
|
||||||
async def generate_structured(
|
async def generate_structured(
|
||||||
message: str | UniMessage | LLMMessage | list[LLMContentPart],
|
message: str | LLMMessage | list[LLMContentPart],
|
||||||
response_model: type[T],
|
response_model: type[T],
|
||||||
*,
|
*,
|
||||||
model: ModelName = None,
|
model: ModelName = None,
|
||||||
@@ -303,6 +302,13 @@ async def _generate_image_from_message(
|
|||||||
messages = await normalize_to_llm_messages(message)
|
messages = await normalize_to_llm_messages(message)
|
||||||
|
|
||||||
async with await get_model_instance(model) as model_instance:
|
async with await get_model_instance(model) as model_instance:
|
||||||
|
if not model_instance.can_generate_images:
|
||||||
|
raise LLMException(
|
||||||
|
f"模型 '{model_instance.provider_name}/{model_instance.model_name}'"
|
||||||
|
f"不支持图片生成",
|
||||||
|
code=LLMErrorCode.CONFIGURATION_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
response = await model_instance.generate_response(messages, config=config)
|
response = await model_instance.generate_response(messages, config=config)
|
||||||
|
|
||||||
if not response.images:
|
if not response.images:
|
||||||
@@ -403,5 +409,4 @@ async def search(
|
|||||||
model=model,
|
model=model,
|
||||||
instruction=instruction,
|
instruction=instruction,
|
||||||
config=final_config,
|
config=final_config,
|
||||||
tools=[GeminiGoogleSearch()],
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
@@ -4,8 +4,11 @@ LLM 服务 - 会话客户端
|
|||||||
提供一个有状态的、面向会话的 LLM 客户端,用于进行多轮对话和复杂交互。
|
提供一个有状态的、面向会话的 LLM 客户端,用于进行多轮对话和复杂交互。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections import defaultdict
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import copy
|
import copy
|
||||||
|
from dataclasses import dataclass, field
|
||||||
import json
|
import json
|
||||||
from typing import Any, TypeVar, cast
|
from typing import Any, TypeVar, cast
|
||||||
import uuid
|
import uuid
|
||||||
@@ -25,14 +28,8 @@ from .config import (
|
|||||||
LLMGenerationConfig,
|
LLMGenerationConfig,
|
||||||
)
|
)
|
||||||
from .config.generation import OutputConfig
|
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 .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 .tools import tool_provider_manager
|
||||||
from .types import (
|
from .types import (
|
||||||
LLMContentPart,
|
LLMContentPart,
|
||||||
@@ -45,6 +42,7 @@ from .types import (
|
|||||||
StructuredOutputStrategy,
|
StructuredOutputStrategy,
|
||||||
ToolChoice,
|
ToolChoice,
|
||||||
ToolExecutable,
|
ToolExecutable,
|
||||||
|
ToolProvider,
|
||||||
)
|
)
|
||||||
from .types.models import (
|
from .types.models import (
|
||||||
GeminiCodeExecution,
|
GeminiCodeExecution,
|
||||||
@@ -59,6 +57,105 @@ from .utils import (
|
|||||||
|
|
||||||
T = TypeVar("T", bound=BaseModel)
|
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 = (
|
DEFAULT_IVR_TEMPLATE = (
|
||||||
"你的响应未能通过结构校验。\n"
|
"你的响应未能通过结构校验。\n"
|
||||||
"错误详情: {error_msg}\n\n"
|
"错误详情: {error_msg}\n\n"
|
||||||
@@ -89,8 +186,7 @@ class AI:
|
|||||||
参数:
|
参数:
|
||||||
session_id: 唯一的会话ID,用于隔离记忆。
|
session_id: 唯一的会话ID,用于隔离记忆。
|
||||||
config: AI 配置.
|
config: AI 配置.
|
||||||
memory: 可选的自定义记忆后端。如果为None,则使用默认的 ChatMemory
|
memory: 可选的自定义记忆后端。如果为None,则使用默认的InMemoryMemory。
|
||||||
(InMemoryMessageStore)。
|
|
||||||
default_generation_config: 此AI实例的默认生成配置。
|
default_generation_config: 此AI实例的默认生成配置。
|
||||||
processors: 记忆处理器列表,在添加记忆后触发。
|
processors: 记忆处理器列表,在添加记忆后触发。
|
||||||
"""
|
"""
|
||||||
@@ -445,12 +541,11 @@ class AI:
|
|||||||
instruction=instruction,
|
instruction=instruction,
|
||||||
template_vars=template_vars,
|
template_vars=template_vars,
|
||||||
config=search_config,
|
config=search_config,
|
||||||
tools=[GeminiGoogleSearch()],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def generate_structured(
|
async def generate_structured(
|
||||||
self,
|
self,
|
||||||
message: str | UniMessage | LLMMessage | list[LLMContentPart] | None,
|
message: str | LLMMessage | list[LLMContentPart] | None,
|
||||||
response_model: type[T],
|
response_model: type[T],
|
||||||
*,
|
*,
|
||||||
model: ModelName = None,
|
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()
|
|
||||||
@@ -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}")
|
|
||||||
@@ -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)
|
|
||||||
Reference in New Issue
Block a user