From ca503fbd9b958e91b755812935fa5889cf1ec7c9 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 17:35:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?quick=20start=20=E6=96=87=E6=A1=A3=E5=92=8C?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8F=92=E4=BB=B6=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/action-components.md | 6 +- docs/plugins/quick-start.md | 437 ++++++++---------- .../built_in/plugin_management/_manifest.json | 2 +- .../built_in/plugin_management/plugin.py | 1 + 4 files changed, 191 insertions(+), 255 deletions(-) diff --git a/docs/plugins/action-components.md b/docs/plugins/action-components.md index 4c844df85..30de468dc 100644 --- a/docs/plugins/action-components.md +++ b/docs/plugins/action-components.md @@ -22,7 +22,7 @@ class ExampleAction(BaseAction): action_name = "example_action" # 动作的唯一标识符 action_description = "这是一个示例动作" # 动作描述 activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例 - mode_enable = ChatMode.ALL # 这里以 ALL 为例 + mode_enable = ChatMode.ALL # 一般取ALL,表示在所有聊天模式下都可用 associated_types = ["text", "emoji", ...] # 关联类型 parallel_action = False # 是否允许与其他Action并行执行 action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...} @@ -60,7 +60,7 @@ class ExampleAction(BaseAction): **请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。** #### action_parameters: 该Action的参数说明。 -这是一个字典,键为参数名,值为参数说明。这个字段可以帮助LLM理解如何使用这个Action,并由LLM返回对应的参数,最后传递到 Action 的 action_data 属性中。其格式与你定义的格式完全相同 **(除非LLM哈气了,返回了错误的内容)**。 +这是一个字典,键为参数名,值为参数说明。这个字段可以帮助LLM理解如何使用这个Action,并由LLM返回对应的参数,最后传递到 Action 的 **`action_data`** 属性中。其格式与你定义的格式完全相同 **(除非LLM哈气了,返回了错误的内容)**。 --- @@ -180,6 +180,8 @@ class GreetingAction(BaseAction): return True, "发送了问候" ``` +一个完整的使用`ActionActivationType.KEYWORD`的例子请参考`plugins/hello_world_plugin`中的`ByeAction`。 + #### 第二层:使用决策 **在Action被激活后,使用条件决定麦麦什么时候会"选择"使用这个Action**。 diff --git a/docs/plugins/quick-start.md b/docs/plugins/quick-start.md index 509438308..539e23832 100644 --- a/docs/plugins/quick-start.md +++ b/docs/plugins/quick-start.md @@ -1,20 +1,14 @@ # 🚀 快速开始指南 -本指南将带你用5分钟时间,从零开始创建一个功能完整的MaiCore插件。 +本指南将带你从零开始创建一个功能完整的MaiCore插件。 ## 📖 概述 -这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。无需阅读其他文档,跟着本指南就能完成! +这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。 -## 🎯 学习目标 +以下代码都在我们的`plugins/hello_world_plugin/`目录下。 -- 理解插件的基本结构 -- 从最简单的插件开始,循序渐进 -- 学会创建Action组件(智能动作) -- 学会创建Command组件(命令响应) -- 掌握配置Schema定义和配置文件自动生成(可选) - -## 📂 准备工作 +### 📂 准备工作 确保你已经: @@ -26,16 +20,29 @@ ### 1. 创建插件目录 -在项目根目录的 `plugins/` 文件夹下创建你的插件目录,目录名与插件名保持一致: +在项目根目录的 `plugins/` 文件夹下创建你的插件目录 -可以用以下命令快速创建: +这里我们创建一个名为 `hello_world_plugin` 的目录 -```bash -mkdir plugins/hello_world_plugin -cd plugins/hello_world_plugin +### 2. 创建`_manifest.json`文件 + +在插件目录下面创建一个 `_manifest.json` 文件,内容如下: + +```json +{ + "manifest_version": 1, + "name": "Hello World 插件", + "version": "1.0.0", + "description": "一个简单的 Hello World 插件", + "author": { + "name": "你的名字" + } +} ``` -### 2. 创建最简单的插件 +有关 `_manifest.json` 的详细说明,请参考 [Manifest文件指南](./manifest-guide.md)。 + +### 3. 创建最简单的插件 让我们从最基础的开始!创建 `plugin.py` 文件: @@ -43,34 +50,33 @@ cd plugins/hello_world_plugin from typing import List, Tuple, Type from src.plugin_system import BasePlugin, register_plugin, ComponentInfo -# ===== 插件注册 ===== - -@register_plugin +@register_plugin # 注册插件 class HelloWorldPlugin(BasePlugin): """Hello World插件 - 你的第一个MaiCore插件""" - # 插件基本信息(必须填写) + # 以下是插件基本信息和方法(必须填写) plugin_name = "hello_world_plugin" - plugin_description = "我的第一个MaiCore插件" - plugin_version = "1.0.0" - plugin_author = "你的名字" enable_plugin = True # 启用插件 + dependencies = [] # 插件依赖列表(目前为空) + python_dependencies = [] # Python依赖列表(目前为空) + config_file_name = "config.toml" # 配置文件名 + config_schema = {} # 配置文件模式(目前为空) - def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: + def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: # 获取插件组件 """返回插件包含的组件列表(目前是空的)""" return [] ``` -🎉 **恭喜!你刚刚创建了一个最简单但完整的MaiCore插件!** +🎉 恭喜!你刚刚创建了一个最简单但完整的MaiCore插件! **解释一下这些代码:** -- 首先,我们在plugin.py中定义了一个HelloWorldPulgin插件类,继承自 `BasePlugin` ,提供基本功能。 +- 首先,我们在`plugin.py`中定义了一个HelloWorldPlugin插件类,继承自 `BasePlugin` ,提供基本功能。 - 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件" -- `plugin_name` 等是插件的基本信息,必须填写,**此部分必须与目录名称相同,否则插件无法使用** -- `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何action(动作)或者command(指令),是空的 +- `plugin_name` 等是插件的基本信息,必须填写 +- `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何 Action, Command 或者 EventHandler,所以返回空列表。 -### 3. 测试基础插件 +### 4. 测试基础插件 现在就可以测试这个插件了!启动MaiCore: @@ -80,7 +86,7 @@ class HelloWorldPlugin(BasePlugin): ![1750326700269](image/quick-start/1750326700269.png) -### 4. 添加第一个功能:问候Action +### 5. 添加第一个功能:问候Action 现在我们要给插件加入一个有用的功能,我们从最好玩的Action做起 @@ -107,40 +113,34 @@ class HelloAction(BaseAction): # === 基本信息(必须填写)=== action_name = "hello_greeting" action_description = "向用户发送问候消息" + activation_type = ActionActivationType.ALWAYS # 始终激活 # === 功能描述(必须填写)=== - action_parameters = { - "greeting_message": "要发送的问候消息" - } - action_require = [ - "需要发送友好问候时使用", - "当有人向你问好时使用", - "当你遇见没有见过的人时使用" - ] + action_parameters = {"greeting_message": "要发送的问候消息"} + action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"] associated_types = ["text"] async def execute(self) -> Tuple[bool, str]: """执行问候动作 - 这是核心功能""" # 发送问候消息 - greeting_message = self.action_data.get("greeting_message","") - - message = "嗨!很开心见到你!😊" + greeting_message + greeting_message = self.action_data.get("greeting_message", "") + base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊") + message = base_message + greeting_message await self.send_text(message) return True, "发送了问候消息" -# ===== 插件注册 ===== - @register_plugin class HelloWorldPlugin(BasePlugin): """Hello World插件 - 你的第一个MaiCore插件""" # 插件基本信息 plugin_name = "hello_world_plugin" - plugin_description = "我的第一个MaiCore插件,包含问候功能" - plugin_version = "1.0.0" - plugin_author = "你的名字" enable_plugin = True + dependencies = [] + python_dependencies = [] + config_file_name = "config.toml" + config_schema = {} def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: """返回插件包含的组件列表""" @@ -150,13 +150,17 @@ class HelloWorldPlugin(BasePlugin): ] ``` -**新增内容解释:** +**解释一下这些代码:** -- `HelloAction` 是一个Action组件,MaiCore可能会选择使用它 +- `HelloAction` 是我们定义的问候动作类,继承自 `BaseAction`,并实现了核心功能。 +- 在 `HelloWorldPlugin` 中,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `HelloAction` 注册为插件的一个组件。 +- 这样一来,当插件被加载时,问候动作也会被一并加载,并可以在MaiCore中使用。 - `execute()` 函数是Action的核心,定义了当Action被MaiCore选择后,具体要做什么 - `self.send_text()` 是发送文本消息的便捷方法 -### 5. 测试问候功能 +Action 组件中有关`activation_type`、`action_parameters`、`action_require`、`associated_types` 等的详细说明请参考 [Action组件指南](./action-components.md)。 + +### 6. 测试问候Action 重启MaiCore,然后在聊天中发送任意消息,比如: @@ -174,96 +178,17 @@ MaiCore可能会选择使用你的问候Action,发送回复: > **💡 小提示**:MaiCore会智能地决定什么时候使用它。如果没有立即看到效果,多试几次不同的消息。 -🎉 **太棒了!你的插件已经有实际功能了!** +🎉 太棒了!你的插件已经有实际功能了! -### 5.5. 了解激活系统(重要概念) - -Action固然好用简单,但是现在有个问题,当用户加载了非常多的插件,添加了很多自定义Action,LLM需要选择的Action也会变多 - -而不断增多的Action会加大LLM的消耗和负担,降低Action使用的精准度。而且我们并不需要LLM在所有时候都考虑所有Action - -例如,当群友只是在进行正常的聊天,就没有必要每次都考虑是否要选择“禁言”动作,这不仅影响决策速度,还会增加消耗。 - -那有什么办法,能够让Action有选择的加入MaiCore的决策池呢? - -**什么是激活系统?** -激活系统决定了什么时候你的Action会被MaiCore"考虑"使用: - -- **`ActionActivationType.ALWAYS`** - 总是可用(默认值) -- **`ActionActivationType.KEYWORD`** - 只有消息包含特定关键词时才可用 -- **`ActionActivationType.PROBABILITY`** - 根据概率随机可用 -- **`ActionActivationType.NEVER`** - 永不可用(用于调试) - -> **💡 使用提示**: -> -> - 推荐使用枚举类型(如 `ActionActivationType.ALWAYS`),有代码提示和类型检查 -> - 也可以直接使用字符串(如 `"always"`),系统都支持 - -### 5.6. 进阶:尝试关键词激活(可选) - -现在让我们尝试一个更精确的激活方式!添加一个只在用户说特定关键词时才激活的Action: - -```python -# 在HelloAction后面添加这个新Action -class ByeAction(BaseAction): - """告别Action - 只在用户说再见时激活""" - - action_name = "bye_greeting" - action_description = "向用户发送告别消息" - - # 使用关键词激活 - focus_activation_type = ActionActivationType.KEYWORD - normal_activation_type = ActionActivationType.KEYWORD - - # 关键词设置 - activation_keywords = ["再见", "bye", "88", "拜拜"] - keyword_case_sensitive = False - - action_parameters = {"bye_message": "要发送的告别消息"} - action_require = [ - "用户要告别时使用", - "当有人要离开时使用", - "当有人和你说再见时使用", - ] - associated_types = ["text"] - - async def execute(self) -> Tuple[bool, str]: - bye_message = self.action_data.get("bye_message","") - - message = "再见!期待下次聊天!👋" + bye_message - await self.send_text(message) - return True, "发送了告别消息" -``` - -然后在插件注册中添加这个Action: - -```python -def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - return [ - (HelloAction.get_action_info(), HelloAction), - (ByeAction.get_action_info(), ByeAction), # 添加告别Action - ] -``` - -现在测试:发送"再见",应该会触发告别Action! - -**关键词激活的特点:** - -- 更精确:只在包含特定关键词时才会被考虑 -- 更可预测:用户知道说什么会触发什么功能 -- 更适合:特定场景或命令式的功能 - -### 6. 添加第二个功能:时间查询Command +### 7. 添加第二个功能:时间查询Command 现在让我们添加一个Command组件。Command和Action不同,它是直接响应用户命令的: -Command是最简单,最直接的相应,不由LLM判断选择使用 +Command是最简单,最直接的响应,不由LLM判断选择使用 ```python # 在现有代码基础上,添加Command组件 - -# ===== Command组件 ===== - +import datetime from src.plugin_system import BaseCommand #导入Command基类 @@ -275,53 +200,49 @@ class TimeCommand(BaseCommand): # === 命令设置(必须填写)=== command_pattern = r"^/time$" # 精确匹配 "/time" 命令 - command_help = "查询当前时间" - command_examples = ["/time"] - intercept_message = True # 拦截消息,不让其他组件处理 - async def execute(self) -> Tuple[bool, str]: + async def execute(self) -> Tuple[bool, Optional[str], bool]: """执行时间查询""" - import datetime - # 获取当前时间 - time_format = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") + time_format: str = "%Y-%m-%d %H:%M:%S" now = datetime.datetime.now() time_str = now.strftime(time_format) - + # 发送时间信息 message = f"⏰ 当前时间:{time_str}" await self.send_text(message) - - return True, f"显示了当前时间: {time_str}" -# ===== 插件注册 ===== + return True, f"显示了当前时间: {time_str}", True @register_plugin class HelloWorldPlugin(BasePlugin): """Hello World插件 - 你的第一个MaiCore插件""" + # 插件基本信息 plugin_name = "hello_world_plugin" - plugin_description = "我的第一个MaiCore插件,包含问候和时间查询功能" - plugin_version = "1.0.0" - plugin_author = "你的名字" enable_plugin = True + dependencies = [] + python_dependencies = [] + config_file_name = "config.toml" + config_schema = {} def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: return [ (HelloAction.get_action_info(), HelloAction), - (ByeAction.get_action_info(), ByeAction), (TimeCommand.get_command_info(), TimeCommand), ] ``` +同样的,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `TimeCommand` 注册为插件的一个组件。 + **Command组件解释:** -- Command是直接响应用户命令的组件 - `command_pattern` 使用正则表达式匹配用户输入 - `^/time$` 表示精确匹配 "/time" -- `intercept_message = True` 表示处理完命令后不再让其他组件处理 -### 7. 测试时间查询功能 +有关 Command 组件的更多信息,请参考 [Command组件指南](./command-components.md)。 + +### 8. 测试时间查询Command 重启MaiCore,发送命令: @@ -332,106 +253,147 @@ class HelloWorldPlugin(BasePlugin): 你应该会收到回复: ``` -⏰ 当前时间:2024-01-01 12:30:45 +⏰ 当前时间:2024-01-01 12:00:00 ``` -🎉 **太棒了!现在你的插件有3个功能了!** +🎉 太棒了!现在你已经了解了基本的 Action 和 Command 组件的使用方法。你可以根据自己的需求,继续扩展插件的功能,添加更多的 Action 和 Command 组件,让你的插件更加丰富和强大! -### 8. 添加配置文件(可选进阶) +--- -如果你想让插件更加灵活,可以添加配置支持。 +## 进阶教程 + +如果你想让插件更加灵活和强大,可以参考接下来的进阶教程。 + +### 1. 添加配置文件 + +想要为插件添加配置文件吗?让我们一起来配置`config_schema`属性! > **🚨 重要:不要手动创建config.toml文件!** > > 我们需要在插件代码中定义配置Schema,让系统自动生成配置文件。 -#### 📄 配置架构说明 - -在新的插件系统中,我们采用了**职责分离**的设计: - -- **`_manifest.json`** - 插件元数据(名称、版本、描述、作者等) -- **`config.toml`** - 运行时配置(启用状态、功能参数等) - -这样避免了信息重复,提高了维护性。 - 首先,在插件类中定义配置Schema: ```python -from src.plugin_system.base.config_types import ConfigField +from src.plugin_system import ConfigField @register_plugin class HelloWorldPlugin(BasePlugin): """Hello World插件 - 你的第一个MaiCore插件""" - plugin_name = "hello_world_plugin" - plugin_description = "我的第一个MaiCore插件,包含问候和时间查询功能" - plugin_version = "1.0.0" - plugin_author = "你的名字" - enable_plugin = True - config_file_name = "config.toml" # 配置文件名 - - # 配置节描述 - config_section_descriptions = { - "plugin": "插件启用配置", - "greeting": "问候功能配置", - "time": "时间查询配置" - } + # 插件基本信息 + plugin_name: str = "hello_world_plugin" # 内部标识符 + enable_plugin: bool = True + dependencies: List[str] = [] # 插件依赖列表 + python_dependencies: List[str] = [] # Python包依赖列表 + config_file_name: str = "config.toml" # 配置文件名 # 配置Schema定义 - config_schema = { + config_schema: dict = { "plugin": { - "enabled": ConfigField(type=bool, default=True, description="是否启用插件") + "name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"), + "version": ConfigField(type=str, default="1.0.0", description="插件版本"), + "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), }, "greeting": { - "message": ConfigField( - type=str, - default="嗨!很开心见到你!😊", - description="默认问候消息" - ), - "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号") + "message": ConfigField(type=str, default="嗨!很开心见到你!😊", description="默认问候消息"), + "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"), }, - "time": { - "format": ConfigField( - type=str, - default="%Y-%m-%d %H:%M:%S", - description="时间显示格式" - ) - } + "time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")}, } def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: return [ (HelloAction.get_action_info(), HelloAction), - (ByeAction.get_action_info(), ByeAction), (TimeCommand.get_command_info(), TimeCommand), ] ``` -然后修改Action和Command代码,让它们读取配置: +这会生成一个如下的 `config.toml` 文件: + +```toml +# hello_world_plugin - 自动生成的配置文件 +# 我的第一个MaiCore插件,包含问候功能和时间查询等基础示例 + +# 插件基本信息 +[plugin] + +# 插件名称 +name = "hello_world_plugin" + +# 插件版本 +version = "1.0.0" + +# 是否启用插件 +enabled = false + + +# 问候功能配置 +[greeting] + +# 默认问候消息 +message = "嗨!很开心见到你!😊" + +# 是否启用表情符号 +enable_emoji = true + + +# 时间查询配置 +[time] + +# 时间显示格式 +format = "%Y-%m-%d %H:%M:%S" +``` + +然后修改Action和Command代码,通过 `get_config()` 方法让它们读取配置(配置的键是命名空间式的): ```python -# 在HelloAction的execute方法中: -async def execute(self) -> Tuple[bool, str]: - # 从配置文件读取问候消息 - greeting_message = self.action_data.get("greeting_message", "") - base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊") - - message = base_message + greeting_message - await self.send_text(message) - return True, "发送了问候消息" +class HelloAction(BaseAction): + """问候Action - 简单的问候动作""" -# 在TimeCommand的execute方法中: -async def execute(self) -> Tuple[bool, str]: - import datetime - - # 从配置文件读取时间格式 - time_format = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") - now = datetime.datetime.now() - time_str = now.strftime(time_format) - - message = f"⏰ 当前时间:{time_str}" - await self.send_text(message) - return True, f"显示了当前时间: {time_str}" + # === 基本信息(必须填写)=== + action_name = "hello_greeting" + action_description = "向用户发送问候消息" + activation_type = ActionActivationType.ALWAYS # 始终激活 + + # === 功能描述(必须填写)=== + action_parameters = {"greeting_message": "要发送的问候消息"} + action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"] + associated_types = ["text"] + + async def execute(self) -> Tuple[bool, str]: + """执行问候动作 - 这是核心功能""" + # 发送问候消息 + greeting_message = self.action_data.get("greeting_message", "") + base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊") + message = base_message + greeting_message + await self.send_text(message) + + return True, "发送了问候消息" + +class TimeCommand(BaseCommand): + """时间查询Command - 响应/time命令""" + + command_name = "time" + command_description = "查询当前时间" + + # === 命令设置(必须填写)=== + command_pattern = r"^/time$" # 精确匹配 "/time" 命令 + + async def execute(self) -> Tuple[bool, str, bool]: + """执行时间查询""" + import datetime + + # 获取当前时间 + time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore + now = datetime.datetime.now() + time_str = now.strftime(time_format) + + # 发送时间信息 + message = f"⏰ 当前时间:{time_str}" + await self.send_text(message) + + return True, f"显示了当前时间: {time_str}", True ``` **配置系统工作流程:** @@ -441,47 +403,18 @@ async def execute(self) -> Tuple[bool, str]: 3. **用户修改**: 用户可以修改生成的配置文件 4. **代码读取**: 使用 `self.get_config()` 读取配置值 -**配置功能解释:** +**绝对不要手动创建 `config.toml` 文件!** -- `self.get_config()` 可以读取配置文件中的值 -- 第一个参数是配置路径(用点分隔),第二个参数是默认值 -- 配置文件会包含详细的注释和说明,用户可以轻松理解和修改 -- **绝不要手动创建配置文件**,让系统自动生成 +### 2. 创建说明文档 -### 9. 创建说明文档(可选) +你可以创建一个 `README.md` 文件,描述插件的功能和使用方法。 -创建 `README.md` 文件来说明你的插件: +### 3. 发布到插件市场 -```markdown -# Hello World 插件 +如果你想让更多人使用你的插件,可以将它发布到MaiCore的插件市场。 -## 概述 -我的第一个MaiCore插件,包含问候和时间查询功能。 +这部分请参考 [plugin-repo](https://github.com/Maim-with-u/plugin-repo) 的文档。 -## 功能 -- **问候功能**: 当用户说"你好"、"hello"、"hi"时自动回复 -- **时间查询**: 发送 `/time` 命令查询当前时间 +--- -## 使用方法 -### 问候功能 -发送包含以下关键词的消息: -- "你好" -- "hello" -- "hi" - -### 时间查询 -发送命令:`/time` - -## 配置文件 -插件会自动生成 `config.toml` 配置文件,用户可以修改: -- 问候消息内容 -- 时间显示格式 -- 插件启用状态 - -注意:配置文件是自动生成的,不要手动创建! -``` - - -``` - -``` +🎉 恭喜你!你已经成功的创建了自己的插件了! diff --git a/src/plugins/built_in/plugin_management/_manifest.json b/src/plugins/built_in/plugin_management/_manifest.json index 41b3cd9ce..f394b8677 100644 --- a/src/plugins/built_in/plugin_management/_manifest.json +++ b/src/plugins/built_in/plugin_management/_manifest.json @@ -9,7 +9,7 @@ }, "license": "GPL-v3.0-or-later", "host_application": { - "min_version": "0.9.0" + "min_version": "0.9.1" }, "homepage_url": "https://github.com/MaiM-with-u/maibot", "repository_url": "https://github.com/MaiM-with-u/maibot", diff --git a/src/plugins/built_in/plugin_management/plugin.py b/src/plugins/built_in/plugin_management/plugin.py index 76f1a68bd..f150d8017 100644 --- a/src/plugins/built_in/plugin_management/plugin.py +++ b/src/plugins/built_in/plugin_management/plugin.py @@ -429,6 +429,7 @@ class PluginManagementPlugin(BasePlugin): config_schema: dict = { "plugin": { "enable": ConfigField(bool, default=False, description="是否启用插件"), + "config_version": ConfigField(type=str, default="1.0.0", description="配置文件版本"), "permission": ConfigField(list, default=[], description="有权限使用插件管理命令的用户列表"), }, } From 208e629fafb8643e8a4de3b179ed6096f7a775a5 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 17:41:38 +0800 Subject: [PATCH 2/5] typing --- src/chat/memory_system/Hippocampus.py | 30 +++++++++++++-------------- src/config/auto_update.py | 4 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index 13cf53f22..24f9b215e 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -224,13 +224,14 @@ class Hippocampus: return hash((source, target)) @staticmethod - def find_topic_llm(text:str, topic_num:int|list[int]): + def find_topic_llm(text: str, topic_num: int | list[int]): + # sourcery skip: inline-immediately-returned-variable topic_num_str = "" if isinstance(topic_num, list): topic_num_str = f"{topic_num[0]}-{topic_num[1]}" else: topic_num_str = topic_num - + prompt = ( f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num_str}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" @@ -304,10 +305,10 @@ class Hippocampus: # 按相似度降序排序 memories.sort(key=lambda x: x[2], reverse=True) return memories - + async def get_keywords_from_text(self, text: str) -> list: """从文本中提取关键词。 - + Args: text (str): 输入文本 fast_retrieval (bool, optional): 是否使用快速检索。默认为False。 @@ -319,7 +320,7 @@ class Hippocampus: # 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算 text_length = len(text) - topic_num:str|list[int] = None + topic_num: int | list[int] = 0 if text_length <= 5: words = jieba.cut(text) keywords = [word for word in words if len(word) > 1] @@ -327,17 +328,16 @@ class Hippocampus: logger.info(f"提取关键词: {keywords}") return keywords elif text_length <= 10: - topic_num = [1,3] # 6-10字符: 1个关键词 (27.18%的文本) + topic_num = [1, 3] # 6-10字符: 1个关键词 (27.18%的文本) elif text_length <= 20: - topic_num = [2,4] # 11-20字符: 2个关键词 (22.76%的文本) + topic_num = [2, 4] # 11-20字符: 2个关键词 (22.76%的文本) elif text_length <= 30: - topic_num = [3,5] # 21-30字符: 3个关键词 (10.33%的文本) + topic_num = [3, 5] # 21-30字符: 3个关键词 (10.33%的文本) elif text_length <= 50: - topic_num = [4,5] # 31-50字符: 4个关键词 (9.79%的文本) + topic_num = [4, 5] # 31-50字符: 4个关键词 (9.79%的文本) else: topic_num = 5 # 51+字符: 5个关键词 (其余长文本) - - + topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( self.find_topic_llm(text, topic_num) ) @@ -352,11 +352,10 @@ class Hippocampus: for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") if keyword.strip() ] - + logger.info(f"提取关键词: {keywords}") - - return keywords - + + return keywords async def get_memory_from_text( self, @@ -1310,6 +1309,7 @@ class ParahippocampalGyrus: return compressed_memory, similar_topics_dict async def operation_build_memory(self): + # sourcery skip: merge-list-appends-into-extend logger.info("------------------------------------开始构建记忆--------------------------------------") start_time = time.time() memory_samples = self.hippocampus.entorhinal_cortex.get_memory_sample() diff --git a/src/config/auto_update.py b/src/config/auto_update.py index 8d097ec49..e6471e808 100644 --- a/src/config/auto_update.py +++ b/src/config/auto_update.py @@ -36,7 +36,7 @@ def compare_dicts(new, old, path=None, new_comments=None, old_comments=None, log continue if key not in old: comment = get_key_comment(new, key) - logs.append(f"新增: {'.'.join(path + [str(key)])} 注释: {comment if comment else '无'}") + logs.append(f"新增: {'.'.join(path + [str(key)])} 注释: {comment or '无'}") elif isinstance(new[key], (dict, Table)) and isinstance(old.get(key), (dict, Table)): compare_dicts(new[key], old[key], path + [str(key)], new_comments, old_comments, logs) # 删减项 @@ -45,7 +45,7 @@ def compare_dicts(new, old, path=None, new_comments=None, old_comments=None, log continue if key not in new: comment = get_key_comment(old, key) - logs.append(f"删减: {'.'.join(path + [str(key)])} 注释: {comment if comment else '无'}") + logs.append(f"删减: {'.'.join(path + [str(key)])} 注释: {comment or '无'}") return logs From 178d96440994258a3fcef45ced1a72f7ce3fedbb Mon Sep 17 00:00:00 2001 From: infinitycat Date: Fri, 25 Jul 2025 17:49:57 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=20Docker=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E9=85=8D=E7=BD=AE=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E8=BF=90=E8=A1=8C=E7=8E=AF=E5=A2=83=E8=87=B3=20Ubuntu?= =?UTF-8?q?=2024.04=EF=BC=8C=E4=BC=98=E5=8C=96=E5=88=86=E6=94=AF=E5=92=8C?= =?UTF-8?q?=E6=A0=87=E7=AD=BE=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/docker-image.yml | 31 +++++++++++------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 36f5ba8fd..fb5142917 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,27 +1,23 @@ name: Docker Build and Push on: - # push: - # branches: - # - main - # - classical - # - dev - # tags: - # - "v*.*.*" - # - "v*" - # - "*.*.*" - # - "*.*.*-*" - workflow_dispatch: # 允许手动触发工作流 + push: branches: - main + - classical - dev - - dev-refactor + tags: + - "v*.*.*" + - "v*" + - "*.*.*" + - "*.*.*-*" + workflow_dispatch: # 允许手动触发工作流 # Workflow's jobs jobs: build-amd64: name: Build AMD64 Image - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 outputs: digest: ${{ steps.build.outputs.digest }} steps: @@ -74,7 +70,7 @@ jobs: build-arm64: name: Build ARM64 Image - runs-on: ubuntu-latest + runs-on: ubuntu-24.04-arm outputs: digest: ${{ steps.build.outputs.digest }} steps: @@ -90,11 +86,6 @@ jobs: - name: Clone lpmm run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 with: @@ -132,7 +123,7 @@ jobs: create-manifest: name: Create Multi-Arch Manifest - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: - build-amd64 - build-arm64 From 5862feffcc4d46633d6b23295ae9deafce80b282 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 18:37:06 +0800 Subject: [PATCH 4/5] =?UTF-8?q?config=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plugins/configuration-guide.md | 497 +--------------------------- docs/plugins/quick-start.md | 2 + 2 files changed, 18 insertions(+), 481 deletions(-) diff --git a/docs/plugins/configuration-guide.md b/docs/plugins/configuration-guide.md index add7d138d..ef3344723 100644 --- a/docs/plugins/configuration-guide.md +++ b/docs/plugins/configuration-guide.md @@ -6,34 +6,6 @@ > > 系统会根据你在代码中定义的 `config_schema` 自动生成配置文件。手动创建配置文件会破坏自动化流程,导致配置不一致、缺失注释和文档等问题。 -## 📖 目录 - -1. [配置架构变更说明](#配置架构变更说明) -2. [配置版本管理](#配置版本管理) -3. [配置定义:Schema驱动的配置系统](#配置定义schema驱动的配置系统) -4. [配置访问:在Action和Command中使用配置](#配置访问在action和command中使用配置) -5. [完整示例:从定义到使用](#完整示例从定义到使用) -6. [最佳实践与注意事项](#最佳实践与注意事项) - ---- - -## 配置架构变更说明 - -- **`_manifest.json`** - 负责插件的**元数据信息**(静态) - - 插件名称、版本、描述 - - 作者信息、许可证 - - 仓库链接、关键词、分类 - - 组件列表、兼容性信息 - -- **`config.toml`** - 负责插件的**运行时配置**(动态) - - `enabled` - 是否启用插件 - - 功能参数配置 - - 组件启用开关 - - 用户可调整的行为参数 - - ---- - ## 配置版本管理 ### 🎯 版本管理概述 @@ -103,7 +75,7 @@ config_schema = { 2. **迁移配置值** - 将旧配置文件中的值迁移到新结构中 3. **处理新增字段** - 新增的配置项使用默认值 4. **更新版本号** - `config_version` 字段自动更新为最新版本 -5. **保存配置文件** - 迁移后的配置直接覆盖原文件(不保留备份) +5. **保存配置文件** - 迁移后的配置直接覆盖原文件**(不保留备份)** ### 🔧 实际使用示例 @@ -174,28 +146,13 @@ min_duration = 120 - 跳过版本检查和迁移 - 直接加载现有配置 - 新增的配置项在代码中使用默认值访问 - -### 📝 配置迁移日志 - -系统会详细记录配置迁移过程: - -```log -[MutePlugin] 检测到配置版本需要更新: 当前=v1.0.0, 期望=v1.1.0 -[MutePlugin] 生成新配置结构... -[MutePlugin] 迁移配置值: plugin.enabled = true -[MutePlugin] 更新配置版本: plugin.config_version = 1.1.0 (旧值: 1.0.0) -[MutePlugin] 迁移配置值: mute.min_duration = 120 -[MutePlugin] 迁移配置值: mute.max_duration = 3600 -[MutePlugin] 新增节: permissions -[MutePlugin] 配置文件已从 v1.0.0 更新到 v1.1.0 -``` +- 系统会详细记录配置迁移过程。 ### ⚠️ 重要注意事项 #### 1. 版本号管理 - 当你修改 `config_schema` 时,**必须同步更新** `config_version` -- 建议使用语义化版本号 (例如:`1.0.0`, `1.1.0`, `2.0.0`) -- 配置结构的重大变更应该增加主版本号 +- 请使用语义化版本号 (例如:`1.0.0`, `1.1.0`, `2.0.0`) #### 2. 迁移策略 - **保留原值优先**: 迁移时优先保留用户的原有配置值 @@ -207,45 +164,7 @@ min_duration = 120 - **不保留备份**: 迁移后直接覆盖原配置文件,不保留备份 - **失败安全**: 如果迁移过程中出现错误,会回退到原配置 ---- - -## 配置定义:Schema驱动的配置系统 - -### 核心理念:Schema驱动的配置 - -在新版插件系统中,我们引入了一套 **配置Schema(模式)驱动** 的机制。**你不需要也不应该手动创建和维护 `config.toml` 文件**,而是通过在插件代码中 **声明配置的结构**,系统将为你完成剩下的工作。 - -> **⚠️ 绝对不要手动创建 config.toml 文件!** -> -> - ❌ **错误做法**:手动在插件目录下创建 `config.toml` 文件 -> - ✅ **正确做法**:在插件代码中定义 `config_schema`,让系统自动生成配置文件 - -**核心优势:** - -- **自动化 (Automation)**: 如果配置文件不存在,系统会根据你的声明 **自动生成** 一份包含默认值和详细注释的 `config.toml` 文件。 -- **规范化 (Standardization)**: 所有插件的配置都遵循统一的结构,提升了可维护性。 -- **自带文档 (Self-documenting)**: 配置文件中的每一项都包含详细的注释、类型说明、可选值和示例,极大地降低了用户的使用门槛。 -- **健壮性 (Robustness)**: 在代码中直接定义配置的类型和默认值,减少了因配置错误导致的运行时问题。 -- **易于管理 (Easy Management)**: 生成的配置文件可以方便地加入 `.gitignore`,避免将个人配置(如API Key)提交到版本库。 - -### 配置生成工作流程 - -```mermaid -graph TD - A[编写插件代码] --> B[定义 config_schema] - B --> C[首次加载插件] - C --> D{config.toml 是否存在?} - D -->|不存在| E[系统自动生成 config.toml] - D -->|存在| F[加载现有配置文件] - E --> G[配置完成,插件可用] - F --> G - - style E fill:#90EE90 - style B fill:#87CEEB - style G fill:#DDA0DD -``` - -### 如何定义配置 +## 配置定义 配置的定义在你的插件主类(继承自 `BasePlugin`)中完成,主要通过两个类属性: @@ -257,6 +176,7 @@ graph TD 每个配置项都通过一个 `ConfigField` 对象来定义。 ```python +from dataclasses import dataclass from src.plugin_system.base.config_types import ConfigField @dataclass @@ -270,28 +190,21 @@ class ConfigField: choices: Optional[List[Any]] = None # 可选值列表 (可选) ``` -### 配置定义示例 +### 配置示例 让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。 ```python # src/plugins/built_in/mute_plugin/plugin.py -from src.plugin_system import BasePlugin, register_plugin -from src.plugin_system.base.config_types import ConfigField +from src.plugin_system import BasePlugin, register_plugin, ConfigField from typing import List, Tuple, Type @register_plugin class MutePlugin(BasePlugin): """禁言插件""" - # 插件基本信息 - plugin_name = "mute_plugin" - plugin_description = "群聊禁言管理插件,提供智能禁言功能" - plugin_version = "2.0.0" - plugin_author = "MaiBot开发团队" - enable_plugin = True - config_file_name = "config.toml" + # 这里是插件基本信息,略去 # 步骤1: 定义配置节的描述 config_section_descriptions = { @@ -339,22 +252,9 @@ class MutePlugin(BasePlugin): } } - def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - # 在这里可以通过 self.get_config() 来获取配置值 - enable_smart_mute = self.get_config("components.enable_smart_mute", True) - enable_mute_command = self.get_config("components.enable_mute_command", False) - - components = [] - if enable_smart_mute: - components.append((SmartMuteAction.get_action_info(), SmartMuteAction)) - if enable_mute_command: - components.append((MuteCommand.get_command_info(), MuteCommand)) - - return components + # 这里是插件方法,略去 ``` -### 自动生成的配置文件 - 当 `mute_plugin` 首次加载且其目录中不存在 `config.toml` 时,系统会自动创建以下文件: ```toml @@ -413,317 +313,24 @@ prefix = "[MutePlugin]" --- -## 配置访问:在Action和Command中使用配置 +## 配置访问 -### 问题描述 +如果你想要在你的组件中访问配置,可以通过组件内置的 `get_config()` 方法访问配置。 -在插件开发中,你可能遇到这样的问题: -- 想要在Action或Command中访问插件配置 - -### ✅ 解决方案 - -**直接使用 `self.get_config()` 方法!** - -系统已经自动为你处理了配置传递,你只需要通过组件内置的 `get_config` 方法访问配置即可。 - -### 📖 快速示例 - -#### 在Action中访问配置 +其参数为一个命名空间化的字符串。以上面的 `MutePlugin` 为例,你可以这样访问配置: ```python -from src.plugin_system import BaseAction - -class MyAction(BaseAction): - async def execute(self): - # 方法1: 获取配置值(带默认值) - api_key = self.get_config("api.key", "default_key") - timeout = self.get_config("api.timeout", 30) - - # 方法2: 支持嵌套键访问 - log_level = self.get_config("advanced.logging.level", "INFO") - - # 方法3: 直接访问顶层配置 - enable_feature = self.get_config("features.enable_smart", False) - - # 使用配置值 - if enable_feature: - await self.send_text(f"API密钥: {api_key}") - - return True, "配置访问成功" +enable_smart_mute = self.get_config("components.enable_smart_mute", True) ``` -#### 在Command中访问配置 - -```python -from src.plugin_system import BaseCommand - -class MyCommand(BaseCommand): - async def execute(self): - # 使用方式与Action完全相同 - welcome_msg = self.get_config("messages.welcome", "欢迎!") - max_results = self.get_config("search.max_results", 10) - - # 根据配置执行不同逻辑 - if self.get_config("features.debug_mode", False): - await self.send_text(f"调试模式已启用,最大结果数: {max_results}") - - await self.send_text(welcome_msg) - return True, "命令执行完成" -``` - -### 🔧 API方法详解 - -#### 1. `get_config(key, default=None)` - -获取配置值,支持嵌套键访问: - -```python -# 简单键 -value = self.get_config("timeout", 30) - -# 嵌套键(用点号分隔) -value = self.get_config("database.connection.host", "localhost") -value = self.get_config("features.ai.model", "gpt-3.5-turbo") -``` - -#### 2. 类型安全的配置访问 - -```python -# 确保正确的类型 -max_retries = self.get_config("api.max_retries", 3) -if not isinstance(max_retries, int): - max_retries = 3 # 使用安全的默认值 - -# 布尔值配置 -debug_mode = self.get_config("features.debug_mode", False) -if debug_mode: - # 调试功能逻辑 - pass -``` - -#### 3. 配置驱动的组件行为 - -```python -class ConfigDrivenAction(BaseAction): - async def execute(self): - # 根据配置决定激活行为 - activation_config = { - "use_keywords": self.get_config("activation.use_keywords", True), - "use_llm": self.get_config("activation.use_llm", False), - "keywords": self.get_config("activation.keywords", []), - } - - # 根据配置调整功能 - features = { - "enable_emoji": self.get_config("features.enable_emoji", True), - "enable_llm_reply": self.get_config("features.enable_llm_reply", False), - "max_length": self.get_config("output.max_length", 200), - } - - # 使用配置执行逻辑 - if features["enable_llm_reply"]: - # 使用LLM生成回复 - pass - else: - # 使用模板回复 - pass - - return True, "配置驱动执行完成" -``` - -### 🔄 配置传递机制 - -系统自动处理配置传递,无需手动操作: - -1. **插件初始化** → `BasePlugin`加载`config.toml`到`self.config` -2. **组件注册** → 系统记录插件配置 -3. **组件实例化** → 自动传递`plugin_config`参数给Action/Command -4. **配置访问** → 组件通过`self.get_config()`直接访问配置 - ---- - -## 完整示例:从定义到使用 - -### 插件定义 - -```python -from src.plugin_system.base.config_types import ConfigField - -@register_plugin -class GreetingPlugin(BasePlugin): - """问候插件完整示例""" - - plugin_name = "greeting_plugin" - plugin_description = "智能问候插件,展示配置定义和访问的完整流程" - plugin_version = "1.0.0" - config_file_name = "config.toml" - - # 配置节描述 - config_section_descriptions = { - "plugin": "插件启用配置", - "greeting": "问候功能配置", - "features": "功能开关配置", - "messages": "消息模板配置" - } - - # 配置Schema定义 - config_schema = { - "plugin": { - "enabled": ConfigField(type=bool, default=True, description="是否启用插件") - }, - "greeting": { - "template": ConfigField( - type=str, - default="你好,{username}!欢迎使用问候插件!", - description="问候消息模板" - ), - "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"), - "enable_llm": ConfigField(type=bool, default=False, description="是否使用LLM生成个性化问候") - }, - "features": { - "smart_detection": ConfigField(type=bool, default=True, description="是否启用智能检测"), - "random_greeting": ConfigField(type=bool, default=False, description="是否使用随机问候语"), - "max_greetings_per_hour": ConfigField(type=int, default=5, description="每小时最大问候次数") - }, - "messages": { - "custom_greetings": ConfigField( - type=list, - default=["你好!", "嗨!", "欢迎!"], - description="自定义问候语列表" - ), - "error_message": ConfigField( - type=str, - default="问候功能暂时不可用", - description="错误时显示的消息" - ) - } - } - - def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - """根据配置动态注册组件""" - components = [] - - # 根据配置决定是否注册组件 - if self.get_config("plugin.enabled", True): - components.append((SmartGreetingAction.get_action_info(), SmartGreetingAction)) - components.append((GreetingCommand.get_command_info(), GreetingCommand)) - - return components -``` - -### Action组件使用配置 - -```python -class SmartGreetingAction(BaseAction): - """智能问候Action - 展示配置访问""" - - focus_activation_type = ActionActivationType.KEYWORD - normal_activation_type = ActionActivationType.KEYWORD - activation_keywords = ["你好", "hello", "hi"] - - async def execute(self) -> Tuple[bool, str]: - """执行智能问候,大量使用配置""" - try: - # 检查插件是否启用 - if not self.get_config("plugin.enabled", True): - return False, "插件已禁用" - - # 获取问候配置 - template = self.get_config("greeting.template", "你好,{username}!") - enable_emoji = self.get_config("greeting.enable_emoji", True) - enable_llm = self.get_config("greeting.enable_llm", False) - - # 获取功能配置 - smart_detection = self.get_config("features.smart_detection", True) - random_greeting = self.get_config("features.random_greeting", False) - max_per_hour = self.get_config("features.max_greetings_per_hour", 5) - - # 获取消息配置 - custom_greetings = self.get_config("messages.custom_greetings", []) - error_message = self.get_config("messages.error_message", "问候功能不可用") - - # 根据配置执行不同逻辑 - username = self.action_data.get("username", "用户") - - if random_greeting and custom_greetings: - # 使用随机自定义问候语 - import random - greeting_msg = random.choice(custom_greetings) - elif enable_llm: - # 使用LLM生成个性化问候 - greeting_msg = await self._generate_llm_greeting(username) - else: - # 使用模板问候 - greeting_msg = template.format(username=username) - - # 发送问候消息 - await self.send_text(greeting_msg) - - # 根据配置发送表情 - if enable_emoji: - await self.send_emoji("😊") - - return True, f"向{username}发送了问候" - - except Exception as e: - # 使用配置的错误消息 - await self.send_text(self.get_config("messages.error_message", "出错了")) - return False, f"问候失败: {str(e)}" - - async def _generate_llm_greeting(self, username: str) -> str: - """根据配置使用LLM生成问候语""" - # 这里可以进一步使用配置来定制LLM行为 - llm_style = self.get_config("greeting.llm_style", "friendly") - # ... LLM调用逻辑 - return f"你好 {username}!很高兴见到你!" -``` - -### Command组件使用配置 - -```python -class GreetingCommand(BaseCommand): - """问候命令 - 展示配置访问""" - - command_pattern = r"^/greet(?:\s+(?P\w+))?$" - command_help = "发送问候消息" - command_examples = ["/greet", "/greet Alice"] - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行问候命令""" - # 检查功能是否启用 - if not self.get_config("plugin.enabled", True): - await self.send_text("问候功能已禁用") - return False, "功能禁用" - - # 获取用户名 - username = self.matched_groups.get("username", "用户") - - # 根据配置选择问候方式 - if self.get_config("features.random_greeting", False): - custom_greetings = self.get_config("messages.custom_greetings", ["你好!"]) - import random - greeting = random.choice(custom_greetings) - else: - template = self.get_config("greeting.template", "你好,{username}!") - greeting = template.format(username=username) - - # 发送问候 - await self.send_text(greeting) - - # 根据配置发送表情 - if self.get_config("greeting.enable_emoji", True): - await self.send_text("😊") - - return True, "问候发送成功" -``` +如果尝试访问了一个不存在的配置项,系统会自动返回默认值(你传递的)或者 `None`。 --- ## 最佳实践与注意事项 -### 配置定义最佳实践 -> **🚨 核心原则:永远不要手动创建 config.toml 文件!** +**🚨 核心原则:永远不要手动创建 config.toml 文件!** 1. **🔥 绝不手动创建配置文件**: **任何时候都不要手动创建 `config.toml` 文件**!必须通过在 `plugin.py` 中定义 `config_schema` 让系统自动生成。 - ❌ **禁止**:`touch config.toml`、手动编写配置文件 @@ -737,76 +344,4 @@ class GreetingCommand(BaseCommand): 5. **gitignore**: 将 `plugins/*/config.toml` 或 `src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。 -6. **配置文件只供修改**: 自动生成的 `config.toml` 文件只应该被用户**修改**,而不是从零创建。 - -### 配置访问最佳实践 - -#### 1. 总是提供默认值 - -```python -# ✅ 好的做法 -timeout = self.get_config("api.timeout", 30) - -# ❌ 避免这样做 -timeout = self.get_config("api.timeout") # 可能返回None -``` - -#### 2. 验证配置类型 - -```python -# 获取配置后验证类型 -max_items = self.get_config("list.max_items", 10) -if not isinstance(max_items, int) or max_items <= 0: - max_items = 10 # 使用安全的默认值 -``` - -#### 3. 缓存复杂配置解析 - -```python -class MyAction(BaseAction): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # 在初始化时解析复杂配置,避免重复解析 - self._api_config = self._parse_api_config() - - def _parse_api_config(self): - return { - 'key': self.get_config("api.key", ""), - 'timeout': self.get_config("api.timeout", 30), - 'retries': self.get_config("api.max_retries", 3) - } -``` - -#### 4. 配置驱动的组件注册 - -```python -def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: - """根据配置动态注册组件""" - components = [] - - # 从配置获取组件启用状态 - enable_action = self.get_config("components.enable_action", True) - enable_command = self.get_config("components.enable_command", True) - - if enable_action: - components.append((MyAction.get_action_info(), MyAction)) - if enable_command: - components.append((MyCommand.get_command_info(), MyCommand)) - - return components -``` - -### 🎉 总结 - -现在你掌握了插件配置的完整流程: - -1. **定义配置**: 在插件中使用 `config_schema` 定义配置结构 -2. **访问配置**: 在组件中使用 `self.get_config("key", default_value)` 访问配置 -3. **自动生成**: 系统自动生成带注释的配置文件 -4. **动态行为**: 根据配置动态调整插件行为 - -> **🚨 最后强调:任何时候都不要手动创建 config.toml 文件!** -> -> 让系统根据你的 `config_schema` 自动生成配置文件,这是插件系统的核心设计原则。 - -不需要继承`BasePlugin`,不需要复杂的配置传递,不需要手动创建配置文件,组件内置的`get_config`方法和自动化的配置生成机制已经为你准备好了一切! \ No newline at end of file +6. **配置文件只供修改**: 自动生成的 `config.toml` 文件只应该被用户**修改**,而不是从零创建。 \ No newline at end of file diff --git a/docs/plugins/quick-start.md b/docs/plugins/quick-start.md index 539e23832..dda37ab84 100644 --- a/docs/plugins/quick-start.md +++ b/docs/plugins/quick-start.md @@ -405,6 +405,8 @@ class TimeCommand(BaseCommand): **绝对不要手动创建 `config.toml` 文件!** +更详细的配置系统介绍请参考 [配置指南](./configuration-guide.md)。 + ### 2. 创建说明文档 你可以创建一个 `README.md` 文件,描述插件的功能和使用方法。 From 29229d9a361d2daf776a117e15cded50c3fc8275 Mon Sep 17 00:00:00 2001 From: UnCLAS-Prommer Date: Fri, 25 Jul 2025 19:56:01 +0800 Subject: [PATCH 5/5] typing fix --- .../heart_flow/heartflow_message_processor.py | 9 +- src/chat/replyer/default_generator.py | 73 ++++++------ src/chat/utils/chat_message_builder.py | 107 +++++++++--------- src/plugin_system/apis/send_api.py | 6 +- 4 files changed, 98 insertions(+), 97 deletions(-) diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 95b059892..406d0e6d0 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -12,7 +12,7 @@ from src.chat.message_receive.storage import MessageStorage from src.chat.heart_flow.heartflow import heartflow from src.chat.utils.utils import is_mentioned_bot_in_message from src.chat.utils.timer_calculator import Timer -from src.chat.utils.chat_message_builder import replace_user_references_in_content +from src.chat.utils.chat_message_builder import replace_user_references_sync from src.common.logger import get_logger from src.person_info.relationship_manager import get_relationship_manager from src.mood.mood_manager import mood_manager @@ -151,10 +151,9 @@ class HeartFCMessageReceiver: processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text) # 应用用户引用格式替换,将回复和@格式转换为可读格式 - processed_plain_text = replace_user_references_in_content( - processed_plain_text, - message.message_info.platform, - is_async=False, + processed_plain_text = replace_user_references_sync( + processed_plain_text, + message.message_info.platform, # type: ignore replace_bot_name=True ) diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index efefa0934..2e207c609 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -17,7 +17,11 @@ from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.utils.timer_calculator import Timer # <--- Import Timer from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat, replace_user_references_in_content +from src.chat.utils.chat_message_builder import ( + build_readable_messages, + get_raw_msg_before_timestamp_with_chat, + replace_user_references_sync, +) from src.chat.express.expression_selector import expression_selector from src.chat.knowledge.knowledge_lib import qa_manager from src.chat.memory_system.memory_activator import MemoryActivator @@ -30,6 +34,7 @@ from src.plugin_system.base.component_types import ActionInfo logger = get_logger("replyer") + def init_prompt(): Prompt("你正在qq群里聊天,下面是群里在聊的内容:", "chat_target_group1") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") @@ -356,17 +361,20 @@ class DefaultReplyer: expression_habits_block = "" expression_habits_title = "" if style_habits_str.strip(): - expression_habits_title = "你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:" + expression_habits_title = ( + "你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:" + ) expression_habits_block += f"{style_habits_str}\n" if grammar_habits_str.strip(): - expression_habits_title = "你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:" + expression_habits_title = ( + "你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:" + ) expression_habits_block += f"{grammar_habits_str}\n" - + if style_habits_str.strip() and grammar_habits_str.strip(): expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中:" - + expression_habits_block = f"{expression_habits_title}\n{expression_habits_block}" - return expression_habits_block @@ -375,27 +383,27 @@ class DefaultReplyer: return "" instant_memory = None - + running_memories = await self.memory_activator.activate_memory_with_chat_history( target_message=target, chat_history_prompt=chat_history ) - + if global_config.memory.enable_instant_memory: asyncio.create_task(self.instant_memory.create_and_store_memory(chat_history)) instant_memory = await self.instant_memory.get_memory(target) logger.info(f"即时记忆:{instant_memory}") - + if not running_memories: return "" memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" for running_memory in running_memories: memory_str += f"- {running_memory['content']}\n" - + if instant_memory: memory_str += f"- {instant_memory}\n" - + return memory_str async def build_tool_info(self, chat_history, reply_data: Optional[Dict], enable_tool: bool = True): @@ -438,7 +446,7 @@ class DefaultReplyer: tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。" logger.info(f"获取到 {len(tool_results)} 个工具结果") - + return tool_info_str else: logger.debug("未获取到任何工具结果") @@ -469,7 +477,7 @@ class DefaultReplyer: # 添加None检查,防止NoneType错误 if target is None: return keywords_reaction_prompt - + # 处理关键词规则 for rule in global_config.keyword_reaction.keyword_rules: if any(keyword in target for keyword in rule.keywords): @@ -621,7 +629,7 @@ class DefaultReplyer: is_group_chat = bool(chat_stream.group_info) reply_to = reply_data.get("reply_to", "none") extra_info_block = reply_data.get("extra_info", "") or reply_data.get("extra_info_block", "") - + if global_config.mood.enable_mood: chat_mood = mood_manager.get_mood_by_chat_id(chat_id) mood_prompt = chat_mood.mood_state @@ -629,14 +637,8 @@ class DefaultReplyer: mood_prompt = "" sender, target = self._parse_reply_target(reply_to) - - target = replace_user_references_in_content( - target, - chat_stream.platform, - is_async=False, - replace_bot_name=True - ) - + + target = replace_user_references_sync(target, chat_stream.platform, replace_bot_name=True) # 构建action描述 (如果启用planner) action_descriptions = "" @@ -687,25 +689,21 @@ class DefaultReplyer: self._time_and_run_task( self.build_expression_habits(chat_talking_prompt_short, target), "expression_habits" ), - self._time_and_run_task( - self.build_relation_info(reply_data), "relation_info" - ), + self._time_and_run_task(self.build_relation_info(reply_data), "relation_info"), self._time_and_run_task(self.build_memory_block(chat_talking_prompt_short, target), "memory_block"), self._time_and_run_task( self.build_tool_info(chat_talking_prompt_short, reply_data, enable_tool=enable_tool), "tool_info" ), - self._time_and_run_task( - get_prompt_info(target, threshold=0.38), "prompt_info" - ), + self._time_and_run_task(get_prompt_info(target, threshold=0.38), "prompt_info"), ) # 任务名称中英文映射 task_name_mapping = { "expression_habits": "选取表达方式", - "relation_info": "感受关系", + "relation_info": "感受关系", "memory_block": "回忆", "tool_info": "使用工具", - "prompt_info": "获取知识" + "prompt_info": "获取知识", } # 处理结果 @@ -798,7 +796,7 @@ class DefaultReplyer: core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts( message_list_before_now_long, target_user_id ) - + self.build_mai_think_context( chat_id=chat_id, memory_block=memory_block, @@ -815,9 +813,8 @@ class DefaultReplyer: -------------------------------- {time_block} 这是你和{sender}的对话,你们正在交流中: -{core_dialogue_prompt}""" +{core_dialogue_prompt}""", ) - # 使用 s4u 风格的模板 template_name = "s4u_style_prompt" @@ -855,9 +852,9 @@ class DefaultReplyer: identity_block=identity_block, sender=sender, target=target, - chat_info=chat_talking_prompt + chat_info=chat_talking_prompt, ) - + # 使用原有的模式 return await global_prompt_manager.format_prompt( template_name, @@ -1079,9 +1076,11 @@ async def get_prompt_info(message: str, threshold: float): related_info += found_knowledge_from_lpmm logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}秒") logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") - + # 格式化知识信息 - formatted_prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=related_info) + formatted_prompt_info = await global_prompt_manager.format_prompt( + "knowledge_prompt", prompt_info=related_info + ) return formatted_prompt_info else: logger.debug("从LPMM知识库获取知识失败,可能是从未导入过知识,返回空知识...") diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index 22f56d1df..a4edf33d3 100644 --- a/src/chat/utils/chat_message_builder.py +++ b/src/chat/utils/chat_message_builder.py @@ -2,7 +2,7 @@ import time # 导入 time 模块以获取当前时间 import random import re -from typing import List, Dict, Any, Tuple, Optional, Union, Callable +from typing import List, Dict, Any, Tuple, Optional, Callable from rich.traceback import install from src.config.config import global_config @@ -10,61 +10,48 @@ from src.common.message_repository import find_messages, count_messages from src.common.database.database_model import ActionRecords from src.common.database.database_model import Images from src.person_info.person_info import PersonInfoManager, get_person_info_manager -from src.chat.utils.utils import translate_timestamp_to_human_readable,assign_message_ids +from src.chat.utils.utils import translate_timestamp_to_human_readable, assign_message_ids install(extra_lines=3) -def replace_user_references_in_content( +def replace_user_references_sync( content: str, platform: str, - name_resolver: Union[Callable[[str, str], str], Callable[[str, str], Any]] = None, - is_async: bool = False, - replace_bot_name: bool = True -) -> Union[str, Any]: + name_resolver: Optional[Callable[[str, str], str]] = None, + replace_bot_name: bool = True, +) -> str: """ 替换内容中的用户引用格式,包括回复和@格式 - + Args: content: 要处理的内容字符串 platform: 平台标识 name_resolver: 名称解析函数,接收(platform, user_id)参数,返回用户名称 - 如果为None,则使用默认的person_info_manager - is_async: 是否为异步模式 + 如果为None,则使用默认的person_info_manager replace_bot_name: 是否将机器人的user_id替换为"机器人昵称(你)" - + Returns: - 处理后的内容字符串(同步模式)或awaitable对象(异步模式) + str: 处理后的内容字符串 """ - if is_async: - return _replace_user_references_async(content, platform, name_resolver, replace_bot_name) - else: - return _replace_user_references_sync(content, platform, name_resolver, replace_bot_name) - - -def _replace_user_references_sync( - content: str, - platform: str, - name_resolver: Optional[Callable[[str, str], str]] = None, - replace_bot_name: bool = True -) -> str: - """同步版本的用户引用替换""" if name_resolver is None: person_info_manager = get_person_info_manager() + def default_resolver(platform: str, user_id: str) -> str: # 检查是否是机器人自己 if replace_bot_name and user_id == global_config.bot.qq_account: return f"{global_config.bot.nickname}(你)" person_id = PersonInfoManager.get_person_id(platform, user_id) - return person_info_manager.get_value_sync(person_id, "person_name") or user_id + return person_info_manager.get_value_sync(person_id, "person_name") or user_id # type: ignore + name_resolver = default_resolver - + # 处理回复格式 reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" match = re.search(reply_pattern, content) if match: - aaa = match.group(1) - bbb = match.group(2) + aaa = match[1] + bbb = match[2] try: # 检查是否是机器人自己 if replace_bot_name and bbb == global_config.bot.qq_account: @@ -75,7 +62,7 @@ def _replace_user_references_sync( except Exception: # 如果解析失败,使用原始昵称 content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1) - + # 处理@格式 at_pattern = r"@<([^:<>]+):([^:<>]+)>" at_matches = list(re.finditer(at_pattern, content)) @@ -83,7 +70,7 @@ def _replace_user_references_sync( new_content = "" last_end = 0 for m in at_matches: - new_content += content[last_end:m.start()] + new_content += content[last_end : m.start()] aaa = m.group(1) bbb = m.group(2) try: @@ -99,27 +86,41 @@ def _replace_user_references_sync( last_end = m.end() new_content += content[last_end:] content = new_content - + return content -async def _replace_user_references_async( +async def replace_user_references_async( content: str, platform: str, name_resolver: Optional[Callable[[str, str], Any]] = None, - replace_bot_name: bool = True + replace_bot_name: bool = True, ) -> str: - """异步版本的用户引用替换""" + """ + 替换内容中的用户引用格式,包括回复和@格式 + + Args: + content: 要处理的内容字符串 + platform: 平台标识 + name_resolver: 名称解析函数,接收(platform, user_id)参数,返回用户名称 + 如果为None,则使用默认的person_info_manager + replace_bot_name: 是否将机器人的user_id替换为"机器人昵称(你)" + + Returns: + str: 处理后的内容字符串 + """ if name_resolver is None: person_info_manager = get_person_info_manager() + async def default_resolver(platform: str, user_id: str) -> str: # 检查是否是机器人自己 if replace_bot_name and user_id == global_config.bot.qq_account: return f"{global_config.bot.nickname}(你)" person_id = PersonInfoManager.get_person_id(platform, user_id) - return await person_info_manager.get_value(person_id, "person_name") or user_id + return await person_info_manager.get_value(person_id, "person_name") or user_id # type: ignore + name_resolver = default_resolver - + # 处理回复格式 reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" match = re.search(reply_pattern, content) @@ -136,7 +137,7 @@ async def _replace_user_references_async( except Exception: # 如果解析失败,使用原始昵称 content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1) - + # 处理@格式 at_pattern = r"@<([^:<>]+):([^:<>]+)>" at_matches = list(re.finditer(at_pattern, content)) @@ -144,7 +145,7 @@ async def _replace_user_references_async( new_content = "" last_end = 0 for m in at_matches: - new_content += content[last_end:m.start()] + new_content += content[last_end : m.start()] aaa = m.group(1) bbb = m.group(2) try: @@ -160,7 +161,7 @@ async def _replace_user_references_async( last_end = m.end() new_content += content[last_end:] content = new_content - + return content @@ -524,7 +525,7 @@ def _build_readable_messages_internal( person_name = "某人" # 使用独立函数处理用户引用格式 - content = replace_user_references_in_content(content, platform, is_async=False, replace_bot_name=replace_bot_name) + content = replace_user_references_sync(content, platform, replace_bot_name=replace_bot_name) target_str = "这是QQ的一个功能,用于提及某人,但没那么明显" if target_str in content and random.random() < 0.6: @@ -778,6 +779,7 @@ async def build_readable_messages_with_list( return formatted_string, details_list + def build_readable_messages_with_id( messages: List[Dict[str, Any]], replace_bot_name: bool = True, @@ -793,9 +795,9 @@ def build_readable_messages_with_id( 允许通过参数控制格式化行为。 """ message_id_list = assign_message_ids(messages) - + formatted_string = build_readable_messages( - messages = messages, + messages=messages, replace_bot_name=replace_bot_name, merge_messages=merge_messages, timestamp_mode=timestamp_mode, @@ -806,10 +808,7 @@ def build_readable_messages_with_id( message_id_list=message_id_list, ) - - - - return formatted_string , message_id_list + return formatted_string, message_id_list def build_readable_messages( @@ -894,7 +893,13 @@ def build_readable_messages( if read_mark <= 0: # 没有有效的 read_mark,直接格式化所有消息 formatted_string, _, pic_id_mapping, _ = _build_readable_messages_internal( - copy_messages, replace_bot_name, merge_messages, timestamp_mode, truncate, show_pic=show_pic, message_id_list=message_id_list + copy_messages, + replace_bot_name, + merge_messages, + timestamp_mode, + truncate, + show_pic=show_pic, + message_id_list=message_id_list, ) # 生成图片映射信息并添加到最前面 @@ -1017,7 +1022,7 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str: for msg in messages: try: - platform = msg.get("chat_info_platform") + platform: str = msg.get("chat_info_platform") # type: ignore user_id = msg.get("user_id") _timestamp = msg.get("time") content: str = "" @@ -1046,8 +1051,8 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str: return get_anon_name(platform, user_id) except Exception: return "?" - - content = replace_user_references_in_content(content, platform, anon_name_resolver, is_async=False, replace_bot_name=False) + + content = replace_user_references_sync(content, platform, anon_name_resolver, replace_bot_name=False) header = f"{anon_name}说 " output_lines.append(header) diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index f7b3092ef..f7af02591 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -19,11 +19,9 @@ await send_api.custom_message("video", video_data, "123456", True) """ -import asyncio import traceback import time import difflib -import re from typing import Optional, Union from src.common.logger import get_logger @@ -31,7 +29,7 @@ from src.common.logger import get_logger from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.message_receive.message import MessageSending, MessageRecv -from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, replace_user_references_in_content +from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, replace_user_references_async from src.person_info.person_info import get_person_info_manager from maim_message import Seg, UserInfo from src.config.config import global_config @@ -185,7 +183,7 @@ async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageR translate_text = message["processed_plain_text"] # 使用独立函数处理用户引用格式 - translate_text = await replace_user_references_in_content(translate_text, platform, is_async=True) + translate_text = await replace_user_references_async(translate_text, platform) similarity = difflib.SequenceMatcher(None, text, translate_text).ratio() if similarity >= 0.9: