diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 47fdf5b7f..fb5142917 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -11,12 +11,13 @@ on: - "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: @@ -69,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: @@ -85,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: @@ -127,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 diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index 66140d742..3d2e7d1f3 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,12 +1,12 @@ name: Ruff on: - push: - branches: - - main - - dev - - dev-refactor # 例如:匹配所有以 feature/ 开头的分支 - # 添加你希望触发此 workflow 的其他分支 + # push: + # branches: + # - main + # - dev + # - dev-refactor # 例如:匹配所有以 feature/ 开头的分支 + # # 添加你希望触发此 workflow 的其他分支 workflow_dispatch: # 允许手动触发工作流 branches: - main diff --git a/.gitignore b/.gitignore index 4db85eab8..c26f8d2c9 100644 --- a/.gitignore +++ b/.gitignore @@ -321,4 +321,5 @@ run_pet.bat config.toml -interested_rates.txt \ No newline at end of file +interested_rates.txt +MaiBot.code-workspace diff --git a/changelogs/changelog.md b/changelogs/changelog.md index c56426a72..a510b51e0 100644 --- a/changelogs/changelog.md +++ b/changelogs/changelog.md @@ -1,14 +1,28 @@ # Changelog -## [0.9.1] - 2025-7-25 +## [0.9.1] - 2025-7-26 +### 主要修复和优化 + +- 优化回复意愿 +- 优化专注模式回复频率 +- 优化关键词提取 +- 修复部分模型产生的400问题 + +### 细节优化 + +- 修复reply导致的planner异常空跳 - 修复表达方式迁移空目录问题 - 修复reply_to空字段问题 +- 无可用动作导致的空plan问题 +- 修复人格未压缩导致产生句号分割 - 将metioned bot 和 at应用到focus prompt中 +- 更好的兴趣度计算 +- 修复部分模型由于enable_thinking导致的400问题 +- 移除dependency_manager - -## [0.9.0] - 2025-7-25 +## [0.9.0] - 2025-7-24 ### 摘要 MaiBot 0.9.0 重磅升级!本版本带来两大核心突破:**全面重构的插件系统**提供更强大的扩展能力和管理功能;**normal和focus模式统一化处理**大幅简化架构并提升性能。同时新增s4u prompt模式优化、语音消息支持、全新情绪系统和mais4u直播互动功能,为MaiBot带来更自然、更智能的交互体验! diff --git a/changes.md b/changes.md index 7d4f2ae8f..b776991de 100644 --- a/changes.md +++ b/changes.md @@ -23,6 +23,8 @@ 6. 增加了插件和组件管理的API。 7. `BaseCommand`的`execute`方法现在返回一个三元组,包含是否执行成功、可选的回复消息和是否拦截消息。 - 这意味着你终于可以动态控制是否继续后续消息的处理了。 +8. 移除了dependency_manager,但是依然保留了`python_dependencies`属性,等待后续重构。 + - 一并移除了文档有关manager的内容。 # 插件系统修改 1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)** diff --git a/docs/plugins/action-components.md b/docs/plugins/action-components.md index 3953c79c2..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**。 @@ -268,7 +270,6 @@ action_message为一个字典,包含的键值对如下(省略了不必要的 ## Action 内置方法说明 ```python class BaseAction: - # 配置相关 def get_config(self, key: str, default=None): """获取插件配置值,使用嵌套键访问""" diff --git a/docs/plugins/api/chat-api.md b/docs/plugins/api/chat-api.md index 496a58623..b9b95e274 100644 --- a/docs/plugins/api/chat-api.md +++ b/docs/plugins/api/chat-api.md @@ -5,147 +5,126 @@ ## 导入方式 ```python -from src.plugin_system.apis import chat_api +from src.plugin_system import chat_api # 或者 -from src.plugin_system.apis.chat_api import ChatManager as chat +from src.plugin_system.apis import chat_api +``` + +一种**Deprecated**方式: +```python +from src.plugin_system.apis.chat_api import ChatManager ``` ## 主要功能 -### 1. 获取聊天流 +### 1. 获取所有的聊天流 -#### `get_all_streams(platform: str = "qq") -> List[ChatStream]` -获取所有聊天流 +```python +def get_all_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: +``` -**参数:** -- `platform`:平台筛选,默认为"qq" +**Args**: +- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的聊天流。 -**返回:** +**Returns**: - `List[ChatStream]`:聊天流列表 -**示例:** +### 2. 获取群聊聊天流 + ```python -streams = chat_api.get_all_streams() -for stream in streams: - print(f"聊天流ID: {stream.stream_id}") +def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: ``` -#### `get_group_streams(platform: str = "qq") -> List[ChatStream]` -获取所有群聊聊天流 +**Args**: +- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。 -**参数:** -- `platform`:平台筛选,默认为"qq" - -**返回:** +**Returns**: - `List[ChatStream]`:群聊聊天流列表 -#### `get_private_streams(platform: str = "qq") -> List[ChatStream]` -获取所有私聊聊天流 +### 3. 获取私聊聊天流 -**参数:** -- `platform`:平台筛选,默认为"qq" +```python +def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: +``` -**返回:** +**Args**: +- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。 + +**Returns**: - `List[ChatStream]`:私聊聊天流列表 -### 2. 查找特定聊天流 +### 4. 根据群ID获取聊天流 -#### `get_stream_by_group_id(group_id: str, platform: str = "qq") -> Optional[ChatStream]` -根据群ID获取聊天流 +```python +def get_stream_by_group_id(group_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]: +``` -**参数:** +**Args**: - `group_id`:群聊ID -- `platform`:平台,默认为"qq" +- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。 -**返回:** +**Returns**: - `Optional[ChatStream]`:聊天流对象,如果未找到返回None -**示例:** +### 5. 根据用户ID获取私聊流 + ```python -chat_stream = chat_api.get_stream_by_group_id("123456789") -if chat_stream: - print(f"找到群聊: {chat_stream.group_info.group_name}") +def get_stream_by_user_id(user_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]: ``` -#### `get_stream_by_user_id(user_id: str, platform: str = "qq") -> Optional[ChatStream]` -根据用户ID获取私聊流 - -**参数:** +**Args**: - `user_id`:用户ID -- `platform`:平台,默认为"qq" +- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。 -**返回:** +**Returns**: - `Optional[ChatStream]`:聊天流对象,如果未找到返回None -### 3. 聊天流信息查询 +### 6. 获取聊天流类型 -#### `get_stream_type(chat_stream: ChatStream) -> str` -获取聊天流类型 +```python +def get_stream_type(chat_stream: ChatStream) -> str: +``` -**参数:** +**Args**: - `chat_stream`:聊天流对象 -**返回:** -- `str`:聊天类型 ("group", "private", "unknown") +**Returns**: +- `str`:聊天流类型,可能的值包括`private`(私聊流),`group`(群聊流)以及`unknown`(未知类型)。 -#### `get_stream_info(chat_stream: ChatStream) -> Dict[str, Any]` -获取聊天流详细信息 +### 7. 获取聊天流信息 -**参数:** +```python +def get_stream_info(chat_stream: ChatStream) -> Dict[str, Any]: +``` + +**Args**: - `chat_stream`:聊天流对象 -**返回:** -- `Dict[str, Any]`:聊天流信息字典,包含stream_id、platform、type等信息 +**Returns**: +- `Dict[str, Any]`:聊天流的详细信息,包括但不限于: + - `stream_id`:聊天流ID + - `platform`:平台名称 + - `type`:聊天流类型 + - `group_id`:群聊ID + - `group_name`:群聊名称 + - `user_id`:用户ID + - `user_name`:用户名称 + +### 8. 获取聊天流统计摘要 -**示例:** ```python -info = chat_api.get_stream_info(chat_stream) -print(f"聊天类型: {info['type']}") -print(f"平台: {info['platform']}") -if info['type'] == 'group': - print(f"群ID: {info['group_id']}") - print(f"群名: {info['group_name']}") +def get_streams_summary() -> Dict[str, int]: ``` -#### `get_streams_summary() -> Dict[str, int]` -获取聊天流统计信息 +**Returns**: +- `Dict[str, int]`:聊天流统计信息摘要,包含以下键: + - `total_streams`:总聊天流数量 + - `group_streams`:群聊流数量 + - `private_streams`:私聊流数量 + - `qq_streams`:QQ平台流数量 -**返回:** -- `Dict[str, int]`:包含各平台群聊和私聊数量的统计字典 - -## 使用示例 - -### 基础用法 -```python -from src.plugin_system.apis import chat_api - -# 获取所有群聊 -group_streams = chat_api.get_group_streams() -print(f"共有 {len(group_streams)} 个群聊") - -# 查找特定群聊 -target_group = chat_api.get_stream_by_group_id("123456789") -if target_group: - group_info = chat_api.get_stream_info(target_group) - print(f"群名: {group_info['group_name']}") -``` - -### 遍历所有聊天流 -```python -# 获取所有聊天流并分类处理 -all_streams = chat_api.get_all_streams() - -for stream in all_streams: - stream_type = chat_api.get_stream_type(stream) - if stream_type == "group": - print(f"群聊: {stream.group_info.group_name}") - elif stream_type == "private": - print(f"私聊: {stream.user_info.user_nickname}") -``` ## 注意事项 -1. 所有函数都有错误处理,失败时会记录日志 -2. 查询函数返回None或空列表时表示未找到结果 -3. `platform`参数通常为"qq",也可能支持其他平台 -4. `ChatStream`对象包含了聊天的完整信息,包括用户信息、群信息等 \ No newline at end of file +1. 大部分函数在参数不合法时候会抛出异常,请确保你的程序进行了捕获。 +2. `ChatStream`对象包含了聊天的完整信息,包括用户信息、群信息等。 \ No newline at end of file diff --git a/docs/plugins/api/component-manage-api.md b/docs/plugins/api/component-manage-api.md new file mode 100644 index 000000000..f6da2adcc --- /dev/null +++ b/docs/plugins/api/component-manage-api.md @@ -0,0 +1,180 @@ +# 组件管理API + +组件管理API模块提供了对插件组件的查询和管理功能,使得插件能够获取和使用组件相关的信息。 + +## 导入方式 +```python +from src.plugin_system.apis import component_manage_api +# 或者 +from src.plugin_system import component_manage_api +``` + +## 功能概述 + +组件管理API主要提供以下功能: +- **插件信息查询** - 获取所有插件或指定插件的信息。 +- **组件查询** - 按名称或类型查询组件信息。 +- **组件管理** - 启用或禁用组件,支持全局和局部操作。 + +## 主要功能 + +### 1. 获取所有插件信息 +```python +def get_all_plugin_info() -> Dict[str, PluginInfo]: +``` +获取所有插件的信息。 + +**Returns:** +- `Dict[str, PluginInfo]` - 包含所有插件信息的字典,键为插件名称,值为 `PluginInfo` 对象。 + +### 2. 获取指定插件信息 +```python +def get_plugin_info(plugin_name: str) -> Optional[PluginInfo]: +``` +获取指定插件的信息。 + +**Args:** +- `plugin_name` (str): 插件名称。 + +**Returns:** +- `Optional[PluginInfo]`: 插件信息对象,如果插件不存在则返回 `None`。 + +### 3. 获取指定组件信息 +```python +def get_component_info(component_name: str, component_type: ComponentType) -> Optional[Union[CommandInfo, ActionInfo, EventHandlerInfo]]: +``` +获取指定组件的信息。 + +**Args:** +- `component_name` (str): 组件名称。 +- `component_type` (ComponentType): 组件类型。 + +**Returns:** +- `Optional[Union[CommandInfo, ActionInfo, EventHandlerInfo]]`: 组件信息对象,如果组件不存在则返回 `None`。 + +### 4. 获取指定类型的所有组件信息 +```python +def get_components_info_by_type(component_type: ComponentType) -> Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]: +``` +获取指定类型的所有组件信息。 + +**Args:** +- `component_type` (ComponentType): 组件类型。 + +**Returns:** +- `Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]`: 包含指定类型组件信息的字典,键为组件名称,值为对应的组件信息对象。 + +### 5. 获取指定类型的所有启用的组件信息 +```python +def get_enabled_components_info_by_type(component_type: ComponentType) -> Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]: +``` +获取指定类型的所有启用的组件信息。 + +**Args:** +- `component_type` (ComponentType): 组件类型。 + +**Returns:** +- `Dict[str, Union[CommandInfo, ActionInfo, EventHandlerInfo]]`: 包含指定类型启用组件信息的字典,键为组件名称,值为对应的组件信息对象。 + +### 6. 获取指定 Action 的注册信息 +```python +def get_registered_action_info(action_name: str) -> Optional[ActionInfo]: +``` +获取指定 Action 的注册信息。 + +**Args:** +- `action_name` (str): Action 名称。 + +**Returns:** +- `Optional[ActionInfo]` - Action 信息对象,如果 Action 不存在则返回 `None`。 + +### 7. 获取指定 Command 的注册信息 +```python +def get_registered_command_info(command_name: str) -> Optional[CommandInfo]: +``` +获取指定 Command 的注册信息。 + +**Args:** +- `command_name` (str): Command 名称。 + +**Returns:** +- `Optional[CommandInfo]` - Command 信息对象,如果 Command 不存在则返回 `None`。 + +### 8. 获取指定 EventHandler 的注册信息 +```python +def get_registered_event_handler_info(event_handler_name: str) -> Optional[EventHandlerInfo]: +``` +获取指定 EventHandler 的注册信息。 + +**Args:** +- `event_handler_name` (str): EventHandler 名称。 + +**Returns:** +- `Optional[EventHandlerInfo]` - EventHandler 信息对象,如果 EventHandler 不存在则返回 `None`。 + +### 9. 全局启用指定组件 +```python +def globally_enable_component(component_name: str, component_type: ComponentType) -> bool: +``` +全局启用指定组件。 + +**Args:** +- `component_name` (str): 组件名称。 +- `component_type` (ComponentType): 组件类型。 + +**Returns:** +- `bool` - 启用成功返回 `True`,否则返回 `False`。 + +### 10. 全局禁用指定组件 +```python +async def globally_disable_component(component_name: str, component_type: ComponentType) -> bool: +``` +全局禁用指定组件。 + +**Args:** +- `component_name` (str): 组件名称。 +- `component_type` (ComponentType): 组件类型。 + +**Returns:** +- `bool` - 禁用成功返回 `True`,否则返回 `False`。 + +### 11. 局部启用指定组件 +```python +def locally_enable_component(component_name: str, component_type: ComponentType, stream_id: str) -> bool: +``` +局部启用指定组件。 + +**Args:** +- `component_name` (str): 组件名称。 +- `component_type` (ComponentType): 组件类型。 +- `stream_id` (str): 消息流 ID。 + +**Returns:** +- `bool` - 启用成功返回 `True`,否则返回 `False`。 + +### 12. 局部禁用指定组件 +```python +def locally_disable_component(component_name: str, component_type: ComponentType, stream_id: str) -> bool: +``` +局部禁用指定组件。 + +**Args:** +- `component_name` (str): 组件名称。 +- `component_type` (ComponentType): 组件类型。 +- `stream_id` (str): 消息流 ID。 + +**Returns:** +- `bool` - 禁用成功返回 `True`,否则返回 `False`。 + +### 13. 获取指定消息流中禁用的组件列表 +```python +def get_locally_disabled_components(stream_id: str, component_type: ComponentType) -> list[str]: +``` +获取指定消息流中禁用的组件列表。 + +**Args:** +- `stream_id` (str): 消息流 ID。 +- `component_type` (ComponentType): 组件类型。 + +**Returns:** +- `list[str]` - 禁用的组件名称列表。 diff --git a/docs/plugins/api/config-api.md b/docs/plugins/api/config-api.md index e61bb6962..2a5691fc4 100644 --- a/docs/plugins/api/config-api.md +++ b/docs/plugins/api/config-api.md @@ -6,178 +6,47 @@ ```python from src.plugin_system.apis import config_api +# 或者 +from src.plugin_system import config_api ``` ## 主要功能 -### 1. 配置访问 +### 1. 访问全局配置 -#### `get_global_config(key: str, default: Any = None) -> Any` -安全地从全局配置中获取一个值 - -**参数:** -- `key`:配置键名,支持嵌套访问如 "section.subsection.key" -- `default`:如果配置不存在时返回的默认值 - -**返回:** -- `Any`:配置值或默认值 - -**示例:** ```python -# 获取机器人昵称 +def get_global_config(key: str, default: Any = None) -> Any: +``` + +**Args**: +- `key`: 命名空间式配置键名,使用嵌套访问,如 "section.subsection.key",大小写敏感 +- `default`: 如果配置不存在时返回的默认值 + +**Returns**: +- `Any`: 配置值或默认值 + +#### 示例: +获取机器人昵称 +```python bot_name = config_api.get_global_config("bot.nickname", "MaiBot") - -# 获取嵌套配置 -llm_model = config_api.get_global_config("model.default.model_name", "gpt-3.5-turbo") - -# 获取不存在的配置 -unknown_config = config_api.get_global_config("unknown.config", "默认值") ``` -#### `get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any` -从插件配置中获取值,支持嵌套键访问 +### 2. 获取插件配置 -**参数:** -- `plugin_config`:插件配置字典 -- `key`:配置键名,支持嵌套访问如 "section.subsection.key" -- `default`:如果配置不存在时返回的默认值 - -**返回:** -- `Any`:配置值或默认值 - -**示例:** ```python -# 在插件中使用 -class MyPlugin(BasePlugin): - async def handle_action(self, action_data, chat_stream): - # 获取插件配置 - api_key = config_api.get_plugin_config(self.config, "api.key", "") - timeout = config_api.get_plugin_config(self.config, "timeout", 30) - - if not api_key: - logger.warning("API密钥未配置") - return False +def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any: ``` +**Args**: +- `plugin_config`: 插件配置字典 +- `key`: 配置键名,支持嵌套访问如 "section.subsection.key",大小写敏感 +- `default`: 如果配置不存在时返回的默认值 -### 2. 用户信息API - -#### `get_user_id_by_person_name(person_name: str) -> tuple[str, str]` -根据用户名获取用户ID - -**参数:** -- `person_name`:用户名 - -**返回:** -- `tuple[str, str]`:(平台, 用户ID) - -**示例:** -```python -platform, user_id = await config_api.get_user_id_by_person_name("张三") -if platform and user_id: - print(f"用户张三在{platform}平台的ID是{user_id}") -``` - -#### `get_person_info(person_id: str, key: str, default: Any = None) -> Any` -获取用户信息 - -**参数:** -- `person_id`:用户ID -- `key`:信息键名 -- `default`:默认值 - -**返回:** -- `Any`:用户信息值或默认值 - -**示例:** -```python -# 获取用户昵称 -nickname = await config_api.get_person_info(person_id, "nickname", "未知用户") - -# 获取用户印象 -impression = await config_api.get_person_info(person_id, "impression", "") -``` - -## 使用示例 - -### 配置驱动的插件开发 -```python -from src.plugin_system.apis import config_api -from src.plugin_system.base import BasePlugin - -class WeatherPlugin(BasePlugin): - async def handle_action(self, action_data, chat_stream): - # 从全局配置获取API配置 - api_endpoint = config_api.get_global_config("weather.api_endpoint", "") - default_city = config_api.get_global_config("weather.default_city", "北京") - - # 从插件配置获取特定设置 - api_key = config_api.get_plugin_config(self.config, "api_key", "") - timeout = config_api.get_plugin_config(self.config, "timeout", 10) - - if not api_key: - return {"success": False, "message": "Weather API密钥未配置"} - - # 使用配置进行天气查询... - return {"success": True, "message": f"{default_city}今天天气晴朗"} -``` - -### 用户信息查询 -```python -async def get_user_by_name(user_name: str): - """根据用户名获取完整的用户信息""" - - # 获取用户的平台和ID - platform, user_id = await config_api.get_user_id_by_person_name(user_name) - - if not platform or not user_id: - return None - - # 构建person_id - from src.person_info.person_info import PersonInfoManager - person_id = PersonInfoManager.get_person_id(platform, user_id) - - # 获取用户详细信息 - nickname = await config_api.get_person_info(person_id, "nickname", user_name) - impression = await config_api.get_person_info(person_id, "impression", "") - - return { - "platform": platform, - "user_id": user_id, - "nickname": nickname, - "impression": impression - } -``` - -## 配置键名说明 - -### 常用全局配置键 -- `bot.nickname`:机器人昵称 -- `bot.qq_account`:机器人QQ号 -- `model.default`:默认LLM模型配置 -- `database.path`:数据库路径 - -### 嵌套配置访问 -配置支持点号分隔的嵌套访问: -```python -# config.toml 中的配置: -# [bot] -# nickname = "MaiBot" -# qq_account = "123456" -# -# [model.default] -# model_name = "gpt-3.5-turbo" -# temperature = 0.7 - -# API调用: -bot_name = config_api.get_global_config("bot.nickname") -model_name = config_api.get_global_config("model.default.model_name") -temperature = config_api.get_global_config("model.default.temperature") -``` +**Returns**: +- `Any`: 配置值或默认值 ## 注意事项 1. **只读访问**:配置API只提供读取功能,插件不能修改全局配置 -2. **异步函数**:用户信息相关的函数是异步的,需要使用`await` -3. **错误处理**:所有函数都有错误处理,失败时会记录日志并返回默认值 -4. **安全性**:插件通过此API访问配置是安全和隔离的 -5. **性能**:频繁访问的配置建议在插件初始化时获取并缓存 \ No newline at end of file +2. **错误处理**:所有函数都有错误处理,失败时会记录日志并返回默认值 +3. **安全性**:插件通过此API访问配置是安全和隔离的 +4. **性能**:频繁访问的配置建议在插件初始化时获取并缓存 \ No newline at end of file diff --git a/docs/plugins/api/database-api.md b/docs/plugins/api/database-api.md index 174bef158..5b6b4468f 100644 --- a/docs/plugins/api/database-api.md +++ b/docs/plugins/api/database-api.md @@ -6,72 +6,51 @@ ```python from src.plugin_system.apis import database_api +# 或者 +from src.plugin_system import database_api ``` ## 主要功能 -### 1. 通用数据库查询 - -#### `db_query(model_class, query_type="get", filters=None, data=None, limit=None, order_by=None, single_result=False)` -执行数据库查询操作的通用接口 - -**参数:** -- `model_class`:Peewee模型类,如ActionRecords、Messages等 -- `query_type`:查询类型,可选值: "get", "create", "update", "delete", "count" -- `filters`:过滤条件字典,键为字段名,值为要匹配的值 -- `data`:用于创建或更新的数据字典 -- `limit`:限制结果数量 -- `order_by`:排序字段列表,使用字段名,前缀'-'表示降序 -- `single_result`:是否只返回单个结果 - -**返回:** -根据查询类型返回不同的结果: -- "get":返回查询结果列表或单个结果 -- "create":返回创建的记录 -- "update":返回受影响的行数 -- "delete":返回受影响的行数 -- "count":返回记录数量 - -### 2. 便捷查询函数 - -#### `db_save(model_class, data, key_field=None, key_value=None)` -保存数据到数据库(创建或更新) - -**参数:** -- `model_class`:Peewee模型类 -- `data`:要保存的数据字典 -- `key_field`:用于查找现有记录的字段名 -- `key_value`:用于查找现有记录的字段值 - -**返回:** -- `Dict[str, Any]`:保存后的记录数据,失败时返回None - -#### `db_get(model_class, filters=None, order_by=None, limit=None)` -简化的查询函数 - -**参数:** -- `model_class`:Peewee模型类 -- `filters`:过滤条件字典 -- `order_by`:排序字段 -- `limit`:限制结果数量 - -**返回:** -- `Union[List[Dict], Dict, None]`:查询结果 - -### 3. 专用函数 - -#### `store_action_info(...)` -存储动作信息的专用函数 - -## 使用示例 - -### 1. 基本查询操作 +### 1. 通用数据库操作 ```python -from src.plugin_system.apis import database_api -from src.common.database.database_model import Messages, ActionRecords +async def db_query( + model_class: Type[Model], + data: Optional[Dict[str, Any]] = None, + query_type: Optional[str] = "get", + filters: Optional[Dict[str, Any]] = None, + limit: Optional[int] = None, + order_by: Optional[List[str]] = None, + single_result: Optional[bool] = False, +) -> Union[List[Dict[str, Any]], Dict[str, Any], None]: +``` +执行数据库查询操作的通用接口。 -# 查询最近10条消息 +**Args:** +- `model_class`: Peewee模型类。 + - Peewee模型类可以在`src.common.database.database_model`模块中找到,如`ActionRecords`、`Messages`等。 +- `data`: 用于创建或更新的数据 +- `query_type`: 查询类型 + - 可选值: `get`, `create`, `update`, `delete`, `count`。 +- `filters`: 过滤条件字典,键为字段名,值为要匹配的值。 +- `limit`: 限制结果数量。 +- `order_by`: 排序字段列表,使用字段名,前缀'-'表示降序。 + - 排序字段,前缀`-`表示降序,例如`-time`表示按时间字段(即`time`字段)降序 +- `single_result`: 是否只返回单个结果。 + +**Returns:** +- 根据查询类型返回不同的结果: + - `get`: 返回查询结果列表或单个结果。(如果 `single_result=True`) + - `create`: 返回创建的记录。 + - `update`: 返回受影响的行数。 + - `delete`: 返回受影响的行数。 + - `count`: 返回记录数量。 + +#### 示例 + +1. 查询最近10条消息 +```python messages = await database_api.db_query( Messages, query_type="get", @@ -79,180 +58,159 @@ messages = await database_api.db_query( limit=10, order_by=["-time"] ) - -# 查询单条记录 -message = await database_api.db_query( - Messages, - query_type="get", - filters={"message_id": "msg_123"}, - single_result=True -) ``` - -### 2. 创建记录 - +2. 创建一条记录 ```python -# 创建新的动作记录 new_record = await database_api.db_query( ActionRecords, + data={"action_id": "123", "time": time.time(), "action_name": "TestAction"}, query_type="create", - data={ - "action_id": "action_123", - "time": time.time(), - "action_name": "TestAction", - "action_done": True - } ) - -print(f"创建了记录: {new_record['id']}") ``` - -### 3. 更新记录 - +3. 更新记录 ```python -# 更新动作状态 updated_count = await database_api.db_query( ActionRecords, + data={"action_done": True}, query_type="update", - filters={"action_id": "action_123"}, - data={"action_done": True, "completion_time": time.time()} + filters={"action_id": "123"}, ) - -print(f"更新了 {updated_count} 条记录") ``` - -### 4. 删除记录 - +4. 删除记录 ```python -# 删除过期记录 deleted_count = await database_api.db_query( ActionRecords, query_type="delete", - filters={"time__lt": time.time() - 86400} # 删除24小时前的记录 + filters={"action_id": "123"} ) - -print(f"删除了 {deleted_count} 条过期记录") ``` - -### 5. 统计查询 - +5. 计数 ```python -# 统计消息数量 -message_count = await database_api.db_query( +count = await database_api.db_query( Messages, query_type="count", filters={"chat_id": chat_stream.stream_id} ) - -print(f"该聊天有 {message_count} 条消息") ``` -### 6. 使用便捷函数 - +### 2. 数据库保存 +```python +async def db_save( + model_class: Type[Model], data: Dict[str, Any], key_field: Optional[str] = None, key_value: Optional[Any] = None +) -> Optional[Dict[str, Any]]: +``` +保存数据到数据库(创建或更新) + +如果提供了key_field和key_value,会先尝试查找匹配的记录进行更新; + +如果没有找到匹配记录,或未提供key_field和key_value,则创建新记录。 + +**Args:** +- `model_class`: Peewee模型类。 +- `data`: 要保存的数据字典。 +- `key_field`: 用于查找现有记录的字段名,例如"action_id"。 +- `key_value`: 用于查找现有记录的字段值。 + +**Returns:** +- `Optional[Dict[str, Any]]`: 保存后的记录数据,失败时返回None。 + +#### 示例 +创建或更新一条记录 ```python -# 使用db_save进行创建或更新 record = await database_api.db_save( ActionRecords, { - "action_id": "action_123", + "action_id": "123", "time": time.time(), "action_name": "TestAction", "action_done": True }, key_field="action_id", - key_value="action_123" + key_value="123" ) +``` -# 使用db_get进行简单查询 -recent_messages = await database_api.db_get( +### 3. 数据库获取 +```python +async def db_get( + model_class: Type[Model], + filters: Optional[Dict[str, Any]] = None, + limit: Optional[int] = None, + order_by: Optional[str] = None, + single_result: Optional[bool] = False, +) -> Union[List[Dict[str, Any]], Dict[str, Any], None]: +``` + +从数据库获取记录 + +这是db_query方法的简化版本,专注于数据检索操作。 + +**Args:** +- `model_class`: Peewee模型类。 +- `filters`: 过滤条件字典,键为字段名,值为要匹配的值。 +- `limit`: 限制结果数量。 +- `order_by`: 排序字段,使用字段名,前缀'-'表示降序。 +- `single_result`: 是否只返回单个结果,如果为True,则返回单个记录字典或None;否则返回记录字典列表或空列表 + +**Returns:** +- `Union[List[Dict], Dict, None]`: 查询结果列表或单个结果(如果`single_result=True`),失败时返回None。 + +#### 示例 +1. 获取单个记录 +```python +record = await database_api.db_get( + ActionRecords, + filters={"action_id": "123"}, + limit=1 +) +``` +2. 获取最近10条记录 +```python +records = await database_api.db_get( Messages, filters={"chat_id": chat_stream.stream_id}, + limit=10, order_by="-time", - limit=5 ) ``` -## 高级用法 - -### 复杂查询示例 - +### 4. 动作信息存储 ```python -# 查询特定用户在特定时间段的消息 -user_messages = await database_api.db_query( - Messages, - query_type="get", - filters={ - "user_id": "123456", - "time__gte": start_time, # 大于等于开始时间 - "time__lt": end_time # 小于结束时间 - }, - order_by=["-time"], - limit=50 +async def store_action_info( + chat_stream=None, + action_build_into_prompt: bool = False, + action_prompt_display: str = "", + action_done: bool = True, + thinking_id: str = "", + action_data: Optional[dict] = None, + action_name: str = "", +) -> Optional[Dict[str, Any]]: +``` +存储动作信息到数据库,是一种针对 Action 的 `db_save()` 的封装函数。 + +将Action执行的相关信息保存到ActionRecords表中,用于后续的记忆和上下文构建。 + +**Args:** +- `chat_stream`: 聊天流对象,包含聊天ID等信息。 +- `action_build_into_prompt`: 是否将动作信息构建到提示中。 +- `action_prompt_display`: 动作提示的显示文本。 +- `action_done`: 动作是否完成。 +- `thinking_id`: 思考过程的ID。 +- `action_data`: 动作的数据字典。 +- `action_name`: 动作的名称。 + +**Returns:** +- `Optional[Dict[str, Any]]`: 存储后的记录数据,失败时返回None。 + +#### 示例 +```python +record = await database_api.store_action_info( + chat_stream=chat_stream, + action_build_into_prompt=True, + action_prompt_display="执行了回复动作", + action_done=True, + thinking_id="thinking_123", + action_data={"content": "Hello"}, + action_name="reply_action" ) - -# 批量处理 -for message in user_messages: - print(f"消息内容: {message['plain_text']}") - print(f"发送时间: {message['time']}") -``` - -### 插件中的数据持久化 - -```python -from src.plugin_system.base import BasePlugin -from src.plugin_system.apis import database_api - -class DataPlugin(BasePlugin): - async def handle_action(self, action_data, chat_stream): - # 保存插件数据 - plugin_data = { - "plugin_name": self.plugin_name, - "chat_id": chat_stream.stream_id, - "data": json.dumps(action_data), - "created_time": time.time() - } - - # 使用自定义表模型(需要先定义) - record = await database_api.db_save( - PluginData, # 假设的插件数据模型 - plugin_data, - key_field="plugin_name", - key_value=self.plugin_name - ) - - return {"success": True, "record_id": record["id"]} -``` - -## 数据模型 - -### 常用模型类 -系统提供了以下常用的数据模型: - -- `Messages`:消息记录 -- `ActionRecords`:动作记录 -- `UserInfo`:用户信息 -- `GroupInfo`:群组信息 - -### 字段说明 - -#### Messages模型主要字段 -- `message_id`:消息ID -- `chat_id`:聊天ID -- `user_id`:用户ID -- `plain_text`:纯文本内容 -- `time`:时间戳 - -#### ActionRecords模型主要字段 -- `action_id`:动作ID -- `action_name`:动作名称 -- `action_done`:是否完成 -- `time`:创建时间 - -## 注意事项 - -1. **异步操作**:所有数据库API都是异步的,必须使用`await` -2. **错误处理**:函数内置错误处理,失败时返回None或空列表 -3. **数据类型**:返回的都是字典格式的数据,不是模型对象 -4. **性能考虑**:使用`limit`参数避免查询大量数据 -5. **过滤条件**:支持简单的等值过滤,复杂查询需要使用原生Peewee语法 -6. **事务**:如需事务支持,建议直接使用Peewee的事务功能 \ No newline at end of file +``` \ No newline at end of file diff --git a/docs/plugins/api/emoji-api.md b/docs/plugins/api/emoji-api.md index 6dd071b9a..ce9dd0c81 100644 --- a/docs/plugins/api/emoji-api.md +++ b/docs/plugins/api/emoji-api.md @@ -6,11 +6,13 @@ ```python from src.plugin_system.apis import emoji_api +# 或者 +from src.plugin_system import emoji_api ``` -## 🆕 **二步走识别优化** +## 二步走识别优化 -从最新版本开始,表情包识别系统采用了**二步走识别 + 智能缓存**的优化方案: +从新版本开始,表情包识别系统采用了**二步走识别 + 智能缓存**的优化方案: ### **收到表情包时的识别流程** 1. **第一步**:VLM视觉分析 - 生成详细描述 @@ -30,217 +32,84 @@ from src.plugin_system.apis import emoji_api ## 主要功能 ### 1. 表情包获取 - -#### `get_by_description(description: str) -> Optional[Tuple[str, str, str]]` +```python +async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]: +``` 根据场景描述选择表情包 -**参数:** -- `description`:场景描述文本,例如"开心的大笑"、"轻微的讽刺"、"表示无奈和沮丧"等 +**Args:** +- `description`:表情包的描述文本,例如"开心"、"难过"、"愤怒"等 -**返回:** -- `Optional[Tuple[str, str, str]]`:(base64编码, 表情包描述, 匹配的场景) 或 None +**Returns:** +- `Optional[Tuple[str, str, str]]`:一个元组: (表情包的base64编码, 描述, 情感标签),如果未找到匹配的表情包则返回None -**示例:** +#### 示例 ```python -emoji_result = await emoji_api.get_by_description("开心的大笑") +emoji_result = await emoji_api.get_by_description("大笑") if emoji_result: emoji_base64, description, matched_scene = emoji_result print(f"获取到表情包: {description}, 场景: {matched_scene}") # 可以将emoji_base64用于发送表情包 ``` -#### `get_random() -> Optional[Tuple[str, str, str]]` -随机获取表情包 - -**返回:** -- `Optional[Tuple[str, str, str]]`:(base64编码, 表情包描述, 随机场景) 或 None - -**示例:** +### 2. 随机获取表情包 ```python -random_emoji = await emoji_api.get_random() -if random_emoji: - emoji_base64, description, scene = random_emoji - print(f"随机表情包: {description}") +async def get_random(count: Optional[int] = 1) -> List[Tuple[str, str, str]]: ``` +随机获取指定数量的表情包 -#### `get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]` -根据场景关键词获取表情包 +**Args:** +- `count`:要获取的表情包数量,默认为1 -**参数:** -- `emotion`:场景关键词,如"大笑"、"讽刺"、"无奈"等 +**Returns:** +- `List[Tuple[str, str, str]]`:一个包含多个表情包的列表,每个元素是一个元组: (表情包的base64编码, 描述, 情感标签),如果未找到或出错则返回空列表 -**返回:** -- `Optional[Tuple[str, str, str]]`:(base64编码, 表情包描述, 匹配的场景) 或 None - -**示例:** +### 3. 根据情感获取表情包 ```python -emoji_result = await emoji_api.get_by_emotion("讽刺") -if emoji_result: - emoji_base64, description, scene = emoji_result - # 发送讽刺表情包 +async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]: ``` +根据情感标签获取表情包 -### 2. 表情包信息查询 +**Args:** +- `emotion`:情感标签,例如"开心"、"悲伤"、"愤怒"等 -#### `get_count() -> int` -获取表情包数量 +**Returns:** +- `Optional[Tuple[str, str, str]]`:一个元组: (表情包的base64编码, 描述, 情感标签),如果未找到则返回None -**返回:** -- `int`:当前可用的表情包数量 +### 4. 获取表情包数量 +```python +def get_count() -> int: +``` +获取当前可用表情包的数量 -#### `get_info() -> dict` -获取表情包系统信息 +### 5. 获取表情包系统信息 +```python +def get_info() -> Dict[str, Any]: +``` +获取表情包系统的基本信息 -**返回:** -- `dict`:包含表情包数量、最大数量等信息 +**Returns:** +- `Dict[str, Any]`:包含表情包数量、描述等信息的字典,包含以下键: + - `current_count`:当前表情包数量 + - `max_count`:最大表情包数量 + - `available_emojis`:当前可用的表情包数量 -**返回字典包含:** -- `current_count`:当前表情包数量 -- `max_count`:最大表情包数量 -- `available_emojis`:可用表情包数量 +### 6. 获取所有可用的情感标签 +```python +def get_emotions() -> List[str]: +``` +获取所有可用的情感标签 **(已经去重)** -#### `get_emotions() -> list` -获取所有可用的场景关键词 - -**返回:** -- `list`:所有表情包的场景关键词列表(去重) - -#### `get_descriptions() -> list` +### 7. 获取所有表情包描述 +```python +def get_descriptions() -> List[str]: +``` 获取所有表情包的描述列表 -**返回:** -- `list`:所有表情包的描述文本列表 - -## 使用示例 - -### 1. 智能表情包选择 - -```python -from src.plugin_system.apis import emoji_api - -async def send_emotion_response(message_text: str, chat_stream): - """根据消息内容智能选择表情包回复""" - - # 分析消息场景 - if "哈哈" in message_text or "好笑" in message_text: - emoji_result = await emoji_api.get_by_description("开心的大笑") - elif "无语" in message_text or "算了" in message_text: - emoji_result = await emoji_api.get_by_description("表示无奈和沮丧") - elif "呵呵" in message_text or "是吗" in message_text: - emoji_result = await emoji_api.get_by_description("轻微的讽刺") - elif "生气" in message_text or "愤怒" in message_text: - emoji_result = await emoji_api.get_by_description("愤怒和不满") - else: - # 随机选择一个表情包 - emoji_result = await emoji_api.get_random() - - if emoji_result: - emoji_base64, description, scene = emoji_result - # 使用send_api发送表情包 - from src.plugin_system.apis import send_api - success = await send_api.emoji_to_group(emoji_base64, chat_stream.group_info.group_id) - return success - - return False -``` - -### 2. 表情包管理功能 - -```python -async def show_emoji_stats(): - """显示表情包统计信息""" - - # 获取基本信息 - count = emoji_api.get_count() - info = emoji_api.get_info() - scenes = emoji_api.get_emotions() # 实际返回的是场景关键词 - - stats = f""" -📊 表情包统计信息: -- 总数量: {count} -- 可用数量: {info['available_emojis']} -- 最大容量: {info['max_count']} -- 支持场景: {len(scenes)}种 - -🎭 支持的场景关键词: {', '.join(scenes[:10])}{'...' if len(scenes) > 10 else ''} - """ - - return stats -``` - -### 3. 表情包测试功能 - -```python -async def test_emoji_system(): - """测试表情包系统的各种功能""" - - print("=== 表情包系统测试 ===") - - # 测试场景描述查找 - test_descriptions = ["开心的大笑", "轻微的讽刺", "表示无奈和沮丧", "愤怒和不满"] - for desc in test_descriptions: - result = await emoji_api.get_by_description(desc) - if result: - _, description, scene = result - print(f"✅ 场景'{desc}' -> {description} ({scene})") - else: - print(f"❌ 场景'{desc}' -> 未找到") - - # 测试关键词查找 - scenes = emoji_api.get_emotions() - if scenes: - test_scene = scenes[0] - result = await emoji_api.get_by_emotion(test_scene) - if result: - print(f"✅ 关键词'{test_scene}' -> 找到匹配表情包") - - # 测试随机获取 - random_result = await emoji_api.get_random() - if random_result: - print("✅ 随机获取 -> 成功") - - print(f"📊 系统信息: {emoji_api.get_info()}") -``` - -### 4. 在Action中使用表情包 - -```python -from src.plugin_system.base import BaseAction - -class EmojiAction(BaseAction): - async def execute(self, action_data, chat_stream): - # 从action_data获取场景描述或关键词 - scene_keyword = action_data.get("scene", "") - scene_description = action_data.get("description", "") - - emoji_result = None - - # 优先使用具体的场景描述 - if scene_description: - emoji_result = await emoji_api.get_by_description(scene_description) - # 其次使用场景关键词 - elif scene_keyword: - emoji_result = await emoji_api.get_by_emotion(scene_keyword) - # 最后随机选择 - else: - emoji_result = await emoji_api.get_random() - - if emoji_result: - emoji_base64, description, scene = emoji_result - return { - "success": True, - "emoji_base64": emoji_base64, - "description": description, - "scene": scene - } - - return {"success": False, "message": "未找到合适的表情包"} -``` - ## 场景描述说明 ### 常用场景描述 -表情包系统支持多种具体的场景描述,常见的包括: +表情包系统支持多种具体的场景描述,举例如下: - **开心类场景**:开心的大笑、满意的微笑、兴奋的手舞足蹈 - **无奈类场景**:表示无奈和沮丧、轻微的讽刺、无语的摇头 @@ -248,8 +117,8 @@ class EmojiAction(BaseAction): - **惊讶类场景**:震惊的表情、意外的发现、困惑的思考 - **可爱类场景**:卖萌的表情、撒娇的动作、害羞的样子 -### 场景关键词示例 -系统支持的场景关键词包括: +### 情感关键词示例 +系统支持的情感关键词举例如下: - 大笑、微笑、兴奋、手舞足蹈 - 无奈、沮丧、讽刺、无语、摇头 - 愤怒、不满、生气、瞪视、抓狂 @@ -263,9 +132,9 @@ class EmojiAction(BaseAction): ## 注意事项 -1. **异步函数**:获取表情包的函数都是异步的,需要使用 `await` +1. **异步函数**:部分函数是异步的,需要使用 `await` 2. **返回格式**:表情包以base64编码返回,可直接用于发送 -3. **错误处理**:所有函数都有错误处理,失败时返回None或默认值 +3. **错误处理**:所有函数都有错误处理,失败时返回None,空列表或默认值 4. **使用统计**:系统会记录表情包的使用次数 5. **文件依赖**:表情包依赖于本地文件,确保表情包文件存在 6. **编码格式**:返回的是base64编码的图片数据,可直接用于网络传输 diff --git a/docs/plugins/api/generator-api.md b/docs/plugins/api/generator-api.md index 964fff84a..690283df0 100644 --- a/docs/plugins/api/generator-api.md +++ b/docs/plugins/api/generator-api.md @@ -6,241 +6,150 @@ ```python from src.plugin_system.apis import generator_api +# 或者 +from src.plugin_system import generator_api ``` ## 主要功能 ### 1. 回复器获取 - -#### `get_replyer(chat_stream=None, platform=None, chat_id=None, is_group=True)` +```python +def get_replyer( + chat_stream: Optional[ChatStream] = None, + chat_id: Optional[str] = None, + model_configs: Optional[List[Dict[str, Any]]] = None, + request_type: str = "replyer", +) -> Optional[DefaultReplyer]: +``` 获取回复器对象 -**参数:** -- `chat_stream`:聊天流对象(优先) -- `platform`:平台名称,如"qq" -- `chat_id`:聊天ID(群ID或用户ID) -- `is_group`:是否为群聊 +优先使用chat_stream,如果没有则使用chat_id直接查找。 -**返回:** -- `DefaultReplyer`:回复器对象,如果获取失败则返回None +使用 ReplyerManager 来管理实例,避免重复创建。 -**示例:** +**Args:** +- `chat_stream`: 聊天流对象 +- `chat_id`: 聊天ID(实际上就是`stream_id`) +- `model_configs`: 模型配置 +- `request_type`: 请求类型,用于记录LLM使用情况,可以不写 + +**Returns:** +- `DefaultReplyer`: 回复器对象,如果获取失败则返回None + +#### 示例 ```python # 使用聊天流获取回复器 replyer = generator_api.get_replyer(chat_stream=chat_stream) -# 使用平台和ID获取回复器 -replyer = generator_api.get_replyer( - platform="qq", - chat_id="123456789", - is_group=True -) +# 使用平台和ID获取回复器 +replyer = generator_api.get_replyer(chat_id="123456789") ``` ### 2. 回复生成 - -#### `generate_reply(chat_stream=None, action_data=None, platform=None, chat_id=None, is_group=True)` +```python +async def generate_reply( + chat_stream: Optional[ChatStream] = None, + chat_id: Optional[str] = None, + action_data: Optional[Dict[str, Any]] = None, + reply_to: str = "", + extra_info: str = "", + available_actions: Optional[Dict[str, ActionInfo]] = None, + enable_tool: bool = False, + enable_splitter: bool = True, + enable_chinese_typo: bool = True, + return_prompt: bool = False, + model_configs: Optional[List[Dict[str, Any]]] = None, + request_type: str = "", +) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: +``` 生成回复 -**参数:** -- `chat_stream`:聊天流对象(优先) -- `action_data`:动作数据 -- `platform`:平台名称(备用) -- `chat_id`:聊天ID(备用) -- `is_group`:是否为群聊(备用) +优先使用chat_stream,如果没有则使用chat_id直接查找。 -**返回:** -- `Tuple[bool, List[Tuple[str, Any]]]`:(是否成功, 回复集合) +**Args:** +- `chat_stream`: 聊天流对象 +- `chat_id`: 聊天ID(实际上就是`stream_id`) +- `action_data`: 动作数据(向下兼容,包含`reply_to`和`extra_info`) +- `reply_to`: 回复目标,格式为 `{发送者的person_name:消息内容}` +- `extra_info`: 附加信息 +- `available_actions`: 可用动作字典,格式为 `{"action_name": ActionInfo}` +- `enable_tool`: 是否启用工具 +- `enable_splitter`: 是否启用分割器 +- `enable_chinese_typo`: 是否启用中文错别字 +- `return_prompt`: 是否返回提示词 +- `model_configs`: 模型配置,可选 +- `request_type`: 请求类型,用于记录LLM使用情况 -**示例:** +**Returns:** +- `Tuple[bool, List[Tuple[str, Any]], Optional[str]]`: (是否成功, 回复集合, 提示词) + +#### 示例 ```python -success, reply_set = await generator_api.generate_reply( +success, reply_set, prompt = await generator_api.generate_reply( chat_stream=chat_stream, - action_data={"message": "你好", "intent": "greeting"} + action_data=action_data, + reply_to="麦麦:你好", + available_actions=action_info, + enable_tool=True, + return_prompt=True ) - if success: for reply_type, reply_content in reply_set: print(f"回复类型: {reply_type}, 内容: {reply_content}") + if prompt: + print(f"使用的提示词: {prompt}") ``` -#### `rewrite_reply(chat_stream=None, reply_data=None, platform=None, chat_id=None, is_group=True)` -重写回复 - -**参数:** -- `chat_stream`:聊天流对象(优先) -- `reply_data`:回复数据 -- `platform`:平台名称(备用) -- `chat_id`:聊天ID(备用) -- `is_group`:是否为群聊(备用) - -**返回:** -- `Tuple[bool, List[Tuple[str, Any]]]`:(是否成功, 回复集合) - -**示例:** +### 3. 回复重写 ```python -success, reply_set = await generator_api.rewrite_reply( +async def rewrite_reply( + chat_stream: Optional[ChatStream] = None, + reply_data: Optional[Dict[str, Any]] = None, + chat_id: Optional[str] = None, + enable_splitter: bool = True, + enable_chinese_typo: bool = True, + model_configs: Optional[List[Dict[str, Any]]] = None, + raw_reply: str = "", + reason: str = "", + reply_to: str = "", + return_prompt: bool = False, +) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: +``` +重写回复,使用新的内容替换旧的回复内容。 + +优先使用chat_stream,如果没有则使用chat_id直接查找。 + +**Args:** +- `chat_stream`: 聊天流对象 +- `reply_data`: 回复数据,包含`raw_reply`, `reason`和`reply_to`,**(向下兼容备用,当其他参数缺失时从此获取)** +- `chat_id`: 聊天ID(实际上就是`stream_id`) +- `enable_splitter`: 是否启用分割器 +- `enable_chinese_typo`: 是否启用中文错别字 +- `model_configs`: 模型配置,可选 +- `raw_reply`: 原始回复内容 +- `reason`: 重写原因 +- `reply_to`: 回复目标,格式为 `{发送者的person_name:消息内容}` + +**Returns:** +- `Tuple[bool, List[Tuple[str, Any]], Optional[str]]`: (是否成功, 回复集合, 提示词) + +#### 示例 +```python +success, reply_set, prompt = await generator_api.rewrite_reply( chat_stream=chat_stream, - reply_data={"original_text": "原始回复", "style": "more_friendly"} + raw_reply="原始回复内容", + reason="重写原因", + reply_to="麦麦:你好", + return_prompt=True ) +if success: + for reply_type, reply_content in reply_set: + print(f"回复类型: {reply_type}, 内容: {reply_content}") + if prompt: + print(f"使用的提示词: {prompt}") ``` -## 使用示例 - -### 1. 基础回复生成 - -```python -from src.plugin_system.apis import generator_api - -async def generate_greeting_reply(chat_stream, user_name): - """生成问候回复""" - - action_data = { - "intent": "greeting", - "user_name": user_name, - "context": "morning_greeting" - } - - success, reply_set = await generator_api.generate_reply( - chat_stream=chat_stream, - action_data=action_data - ) - - if success and reply_set: - # 获取第一个回复 - reply_type, reply_content = reply_set[0] - return reply_content - - return "你好!" # 默认回复 -``` - -### 2. 在Action中使用回复生成器 - -```python -from src.plugin_system.base import BaseAction - -class ChatAction(BaseAction): - async def execute(self, action_data, chat_stream): - # 准备回复数据 - reply_context = { - "message_type": "response", - "user_input": action_data.get("user_message", ""), - "intent": action_data.get("intent", ""), - "entities": action_data.get("entities", {}), - "context": self.get_conversation_context(chat_stream) - } - - # 生成回复 - success, reply_set = await generator_api.generate_reply( - chat_stream=chat_stream, - action_data=reply_context - ) - - if success: - return { - "success": True, - "replies": reply_set, - "generated_count": len(reply_set) - } - - return { - "success": False, - "error": "回复生成失败", - "fallback_reply": "抱歉,我现在无法理解您的消息。" - } -``` - -### 3. 多样化回复生成 - -```python -async def generate_diverse_replies(chat_stream, topic, count=3): - """生成多个不同风格的回复""" - - styles = ["formal", "casual", "humorous"] - all_replies = [] - - for i, style in enumerate(styles[:count]): - action_data = { - "topic": topic, - "style": style, - "variation": i - } - - success, reply_set = await generator_api.generate_reply( - chat_stream=chat_stream, - action_data=action_data - ) - - if success and reply_set: - all_replies.extend(reply_set) - - return all_replies -``` - -### 4. 回复重写功能 - -```python -async def improve_reply(chat_stream, original_reply, improvement_type="more_friendly"): - """改进原始回复""" - - reply_data = { - "original_text": original_reply, - "improvement_type": improvement_type, - "target_audience": "young_users", - "tone": "positive" - } - - success, improved_replies = await generator_api.rewrite_reply( - chat_stream=chat_stream, - reply_data=reply_data - ) - - if success and improved_replies: - # 返回改进后的第一个回复 - _, improved_content = improved_replies[0] - return improved_content - - return original_reply # 如果改进失败,返回原始回复 -``` - -### 5. 条件回复生成 - -```python -async def conditional_reply_generation(chat_stream, user_message, user_emotion): - """根据用户情感生成条件回复""" - - # 根据情感调整回复策略 - if user_emotion == "sad": - action_data = { - "intent": "comfort", - "tone": "empathetic", - "style": "supportive" - } - elif user_emotion == "angry": - action_data = { - "intent": "calm", - "tone": "peaceful", - "style": "understanding" - } - else: - action_data = { - "intent": "respond", - "tone": "neutral", - "style": "helpful" - } - - action_data["user_message"] = user_message - action_data["user_emotion"] = user_emotion - - success, reply_set = await generator_api.generate_reply( - chat_stream=chat_stream, - action_data=action_data - ) - - return reply_set if success else [] -``` - -## 回复集合格式 +## 回复集合`reply_set`格式 ### 回复类型 生成的回复集合包含多种类型的回复: @@ -260,82 +169,32 @@ reply_set = [ ] ``` -## 高级用法 - -### 1. 自定义回复器配置 - +### 4. 自定义提示词回复 ```python -async def generate_with_custom_config(chat_stream, action_data): - """使用自定义配置生成回复""" - - # 获取回复器 - replyer = generator_api.get_replyer(chat_stream=chat_stream) - - if replyer: - # 可以访问回复器的内部方法 - success, reply_set = await replyer.generate_reply_with_context( - reply_data=action_data, - # 可以传递额外的配置参数 - ) - return success, reply_set - - return False, [] +async def generate_response_custom( + chat_stream: Optional[ChatStream] = None, + chat_id: Optional[str] = None, + model_configs: Optional[List[Dict[str, Any]]] = None, + prompt: str = "", +) -> Optional[str]: ``` +生成自定义提示词回复 -### 2. 回复质量评估 +优先使用chat_stream,如果没有则使用chat_id直接查找。 -```python -async def generate_and_evaluate_replies(chat_stream, action_data): - """生成回复并评估质量""" - - success, reply_set = await generator_api.generate_reply( - chat_stream=chat_stream, - action_data=action_data - ) - - if success: - evaluated_replies = [] - for reply_type, reply_content in reply_set: - # 简单的质量评估 - quality_score = evaluate_reply_quality(reply_content) - evaluated_replies.append({ - "type": reply_type, - "content": reply_content, - "quality": quality_score - }) - - # 按质量排序 - evaluated_replies.sort(key=lambda x: x["quality"], reverse=True) - return evaluated_replies - - return [] +**Args:** +- `chat_stream`: 聊天流对象 +- `chat_id`: 聊天ID(备用) +- `model_configs`: 模型配置列表 +- `prompt`: 自定义提示词 -def evaluate_reply_quality(reply_content): - """简单的回复质量评估""" - if not reply_content: - return 0 - - score = 50 # 基础分 - - # 长度适中加分 - if 5 <= len(reply_content) <= 100: - score += 20 - - # 包含积极词汇加分 - positive_words = ["好", "棒", "不错", "感谢", "开心"] - for word in positive_words: - if word in reply_content: - score += 10 - break - - return min(score, 100) -``` +**Returns:** +- `Optional[str]`: 生成的自定义回复内容,如果生成失败则返回None ## 注意事项 -1. **异步操作**:所有生成函数都是异步的,必须使用`await` -2. **错误处理**:函数内置错误处理,失败时返回False和空列表 -3. **聊天流依赖**:需要有效的聊天流对象才能正常工作 -4. **性能考虑**:回复生成可能需要一些时间,特别是使用LLM时 -5. **回复格式**:返回的回复集合是元组列表,包含类型和内容 -6. **上下文感知**:生成器会考虑聊天上下文和历史消息 \ No newline at end of file +1. **异步操作**:部分函数是异步的,须使用`await` +2. **聊天流依赖**:需要有效的聊天流对象才能正常工作 +3. **性能考虑**:回复生成可能需要一些时间,特别是使用LLM时 +4. **回复格式**:返回的回复集合是元组列表,包含类型和内容 +5. **上下文感知**:生成器会考虑聊天上下文和历史消息,除非你用的是自定义提示词。 \ No newline at end of file diff --git a/docs/plugins/api/llm-api.md b/docs/plugins/api/llm-api.md index e0879ddfc..d778ec8d8 100644 --- a/docs/plugins/api/llm-api.md +++ b/docs/plugins/api/llm-api.md @@ -6,239 +6,34 @@ LLM API模块提供与大语言模型交互的功能,让插件能够使用系 ```python from src.plugin_system.apis import llm_api +# 或者 +from src.plugin_system import llm_api ``` ## 主要功能 -### 1. 模型管理 - -#### `get_available_models() -> Dict[str, Any]` -获取所有可用的模型配置 - -**返回:** -- `Dict[str, Any]`:模型配置字典,key为模型名称,value为模型配置 - -**示例:** +### 1. 查询可用模型 ```python -models = llm_api.get_available_models() -for model_name, model_config in models.items(): - print(f"模型: {model_name}") - print(f"配置: {model_config}") +def get_available_models() -> Dict[str, Any]: ``` +获取所有可用的模型配置。 -### 2. 内容生成 +**Return:** +- `Dict[str, Any]`:模型配置字典,key为模型名称,value为模型配置。 -#### `generate_with_model(prompt, model_config, request_type="plugin.generate", **kwargs)` -使用指定模型生成内容 - -**参数:** -- `prompt`:提示词 -- `model_config`:模型配置(从 get_available_models 获取) -- `request_type`:请求类型标识 -- `**kwargs`:其他模型特定参数,如temperature、max_tokens等 - -**返回:** -- `Tuple[bool, str, str, str]`:(是否成功, 生成的内容, 推理过程, 模型名称) - -**示例:** +### 2. 使用模型生成内容 ```python -models = llm_api.get_available_models() -default_model = models.get("default") - -if default_model: - success, response, reasoning, model_name = await llm_api.generate_with_model( - prompt="请写一首关于春天的诗", - model_config=default_model, - temperature=0.7, - max_tokens=200 - ) - - if success: - print(f"生成内容: {response}") - print(f"使用模型: {model_name}") +async def generate_with_model( + prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs +) -> Tuple[bool, str]: ``` +使用指定模型生成内容。 -## 使用示例 +**Args:** +- `prompt`:提示词。 +- `model_config`:模型配置(从 `get_available_models` 获取)。 +- `request_type`:请求类型标识,默认为 `"plugin.generate"`。 +- `**kwargs`:其他模型特定参数,如 `temperature`、`max_tokens` 等。 -### 1. 基础文本生成 - -```python -from src.plugin_system.apis import llm_api - -async def generate_story(topic: str): - """生成故事""" - models = llm_api.get_available_models() - model = models.get("default") - - if not model: - return "未找到可用模型" - - prompt = f"请写一个关于{topic}的短故事,大约100字左右。" - - success, story, reasoning, model_name = await llm_api.generate_with_model( - prompt=prompt, - model_config=model, - request_type="story.generate", - temperature=0.8, - max_tokens=150 - ) - - return story if success else "故事生成失败" -``` - -### 2. 在Action中使用LLM - -```python -from src.plugin_system.base import BaseAction - -class LLMAction(BaseAction): - async def execute(self, action_data, chat_stream): - # 获取用户输入 - user_input = action_data.get("user_message", "") - intent = action_data.get("intent", "chat") - - # 获取模型配置 - models = llm_api.get_available_models() - model = models.get("default") - - if not model: - return {"success": False, "error": "未配置LLM模型"} - - # 构建提示词 - prompt = self.build_prompt(user_input, intent) - - # 生成回复 - success, response, reasoning, model_name = await llm_api.generate_with_model( - prompt=prompt, - model_config=model, - request_type=f"plugin.{self.plugin_name}", - temperature=0.7 - ) - - if success: - return { - "success": True, - "response": response, - "model_used": model_name, - "reasoning": reasoning - } - - return {"success": False, "error": response} - - def build_prompt(self, user_input: str, intent: str) -> str: - """构建提示词""" - base_prompt = "你是一个友善的AI助手。" - - if intent == "question": - return f"{base_prompt}\n\n用户问题:{user_input}\n\n请提供准确、有用的回答:" - elif intent == "chat": - return f"{base_prompt}\n\n用户说:{user_input}\n\n请进行自然的对话:" - else: - return f"{base_prompt}\n\n用户输入:{user_input}\n\n请回复:" -``` - -### 3. 多模型对比 - -```python -async def compare_models(prompt: str): - """使用多个模型生成内容并对比""" - models = llm_api.get_available_models() - results = {} - - for model_name, model_config in models.items(): - success, response, reasoning, actual_model = await llm_api.generate_with_model( - prompt=prompt, - model_config=model_config, - request_type="comparison.test" - ) - - results[model_name] = { - "success": success, - "response": response, - "model": actual_model, - "reasoning": reasoning - } - - return results -``` - -### 4. 智能对话插件 - -```python -class ChatbotPlugin(BasePlugin): - async def handle_action(self, action_data, chat_stream): - user_message = action_data.get("message", "") - - # 获取历史对话上下文 - context = self.get_conversation_context(chat_stream) - - # 构建对话提示词 - prompt = self.build_conversation_prompt(user_message, context) - - # 获取模型配置 - models = llm_api.get_available_models() - chat_model = models.get("chat", models.get("default")) - - if not chat_model: - return {"success": False, "message": "聊天模型未配置"} - - # 生成回复 - success, response, reasoning, model_name = await llm_api.generate_with_model( - prompt=prompt, - model_config=chat_model, - request_type="chat.conversation", - temperature=0.8, - max_tokens=500 - ) - - if success: - # 保存对话历史 - self.save_conversation(chat_stream, user_message, response) - - return { - "success": True, - "reply": response, - "model": model_name - } - - return {"success": False, "message": "回复生成失败"} - - def build_conversation_prompt(self, user_message: str, context: list) -> str: - """构建对话提示词""" - prompt = "你是一个有趣、友善的聊天机器人。请自然地回复用户的消息。\n\n" - - # 添加历史对话 - if context: - prompt += "对话历史:\n" - for msg in context[-5:]: # 只保留最近5条 - prompt += f"用户: {msg['user']}\n机器人: {msg['bot']}\n" - prompt += "\n" - - prompt += f"用户: {user_message}\n机器人: " - return prompt -``` - -## 模型配置说明 - -### 常用模型类型 -- `default`:默认模型 -- `chat`:聊天专用模型 -- `creative`:创意生成模型 -- `code`:代码生成模型 - -### 配置参数 -LLM模型支持的常用参数: -- `temperature`:控制输出随机性(0.0-1.0) -- `max_tokens`:最大生成长度 -- `top_p`:核采样参数 -- `frequency_penalty`:频率惩罚 -- `presence_penalty`:存在惩罚 - -## 注意事项 - -1. **异步操作**:LLM生成是异步的,必须使用`await` -2. **错误处理**:生成失败时返回False和错误信息 -3. **配置依赖**:需要正确配置模型才能使用 -4. **请求类型**:建议为不同用途设置不同的request_type -5. **性能考虑**:LLM调用可能较慢,考虑超时和缓存 -6. **成本控制**:注意控制max_tokens以控制成本 \ No newline at end of file +**Return:** +- `Tuple[bool, str]`:返回一个元组,第一个元素表示是否成功,第二个元素为生成的内容或错误信息。 \ No newline at end of file diff --git a/docs/plugins/api/logging-api.md b/docs/plugins/api/logging-api.md new file mode 100644 index 000000000..5576bf5cd --- /dev/null +++ b/docs/plugins/api/logging-api.md @@ -0,0 +1,29 @@ +# Logging API + +Logging API模块提供了获取本体logger的功能,允许插件记录日志信息。 + +## 导入方式 + +```python +from src.plugin_system.apis import get_logger +# 或者 +from src.plugin_system import get_logger +``` + +## 主要功能 +### 1. 获取本体logger +```python +def get_logger(name: str) -> structlog.stdlib.BoundLogger: +``` +获取本体logger实例。 + +**Args:** +- `name` (str): 日志记录器的名称。 + +**Returns:** +- 一个logger实例,有以下方法: + - `debug` + - `info` + - `warning` + - `error` + - `critical` \ No newline at end of file diff --git a/docs/plugins/api/message-api.md b/docs/plugins/api/message-api.md index c95a9cc6f..85d83a9bc 100644 --- a/docs/plugins/api/message-api.md +++ b/docs/plugins/api/message-api.md @@ -1,11 +1,13 @@ # 消息API -> 消息API提供了强大的消息查询、计数和格式化功能,让你轻松处理聊天消息数据。 +消息API提供了强大的消息查询、计数和格式化功能,让你轻松处理聊天消息数据。 ## 导入方式 ```python from src.plugin_system.apis import message_api +# 或者 +from src.plugin_system import message_api ``` ## 功能概述 @@ -15,297 +17,356 @@ from src.plugin_system.apis import message_api - **消息计数** - 统计新消息数量 - **消息格式化** - 将消息转换为可读格式 ---- +## 主要功能 -## 消息查询API +### 1. 按照事件查询消息 +```python +def get_messages_by_time( + start_time: float, end_time: float, limit: int = 0, limit_mode: str = "latest", filter_mai: bool = False +) -> List[Dict[str, Any]]: +``` +获取指定时间范围内的消息。 -### 按时间查询消息 - -#### `get_messages_by_time(start_time, end_time, limit=0, limit_mode="latest")` - -获取指定时间范围内的消息 - -**参数:** +**Args:** - `start_time` (float): 开始时间戳 -- `end_time` (float): 结束时间戳 +- `end_time` (float): 结束时间戳 - `limit` (int): 限制返回消息数量,0为不限制 - `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 +- `filter_mai` (bool): 是否过滤掉机器人的消息,默认False -**返回:** `List[Dict[str, Any]]` - 消息列表 +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 -**示例:** +消息列表中包含的键与`Messages`类的属性一致。(位于`src.common.database.database_model`) + +### 2. 获取指定聊天中指定时间范围内的信息 ```python -import time - -# 获取最近24小时的消息 -now = time.time() -yesterday = now - 24 * 3600 -messages = message_api.get_messages_by_time(yesterday, now, limit=50) +def get_messages_by_time_in_chat( + chat_id: str, + start_time: float, + end_time: float, + limit: int = 0, + limit_mode: str = "latest", + filter_mai: bool = False, +) -> List[Dict[str, Any]]: ``` +获取指定聊天中指定时间范围内的消息。 -### 按聊天查询消息 - -#### `get_messages_by_time_in_chat(chat_id, start_time, end_time, limit=0, limit_mode="latest")` - -获取指定聊天中指定时间范围内的消息 - -**参数:** -- `chat_id` (str): 聊天ID -- 其他参数同上 - -**示例:** -```python -# 获取某个群聊最近的100条消息 -messages = message_api.get_messages_by_time_in_chat( - chat_id="123456789", - start_time=yesterday, - end_time=now, - limit=100 -) -``` - -#### `get_messages_by_time_in_chat_inclusive(chat_id, start_time, end_time, limit=0, limit_mode="latest")` - -获取指定聊天中指定时间范围内的消息(包含边界时间点) - -与 `get_messages_by_time_in_chat` 类似,但包含边界时间戳的消息。 - -#### `get_recent_messages(chat_id, hours=24.0, limit=100, limit_mode="latest")` - -获取指定聊天中最近一段时间的消息(便捷方法) - -**参数:** -- `chat_id` (str): 聊天ID -- `hours` (float): 最近多少小时,默认24小时 -- `limit` (int): 限制返回消息数量,默认100条 -- `limit_mode` (str): 限制模式 - -**示例:** -```python -# 获取最近6小时的消息 -recent_messages = message_api.get_recent_messages( - chat_id="123456789", - hours=6.0, - limit=50 -) -``` - -### 按用户查询消息 - -#### `get_messages_by_time_in_chat_for_users(chat_id, start_time, end_time, person_ids, limit=0, limit_mode="latest")` - -获取指定聊天中指定用户在指定时间范围内的消息 - -**参数:** +**Args:** - `chat_id` (str): 聊天ID - `start_time` (float): 开始时间戳 - `end_time` (float): 结束时间戳 -- `person_ids` (list): 用户ID列表 -- `limit` (int): 限制返回消息数量 -- `limit_mode` (str): 限制模式 +- `limit` (int): 限制返回消息数量,0为不限制 +- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 +- `filter_mai` (bool): 是否过滤掉机器人的消息,默认False -**示例:** +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 + + +### 3. 获取指定聊天中指定时间范围内的信息(包含边界) ```python -# 获取特定用户的消息 -user_messages = message_api.get_messages_by_time_in_chat_for_users( - chat_id="123456789", - start_time=yesterday, - end_time=now, - person_ids=["user1", "user2"] -) +def get_messages_by_time_in_chat_inclusive( + chat_id: str, + start_time: float, + end_time: float, + limit: int = 0, + limit_mode: str = "latest", + filter_mai: bool = False, + filter_command: bool = False, +) -> List[Dict[str, Any]]: ``` +获取指定聊天中指定时间范围内的消息(包含边界)。 -#### `get_messages_by_time_for_users(start_time, end_time, person_ids, limit=0, limit_mode="latest")` +**Args:** +- `chat_id` (str): 聊天ID +- `start_time` (float): 开始时间戳(包含) +- `end_time` (float): 结束时间戳(包含) +- `limit` (int): 限制返回消息数量,0为不限制 +- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 +- `filter_mai` (bool): 是否过滤掉机器人的消息,默认False +- `filter_command` (bool): 是否过滤命令消息,默认False -获取指定用户在所有聊天中指定时间范围内的消息 +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 -### 其他查询方法 -#### `get_random_chat_messages(start_time, end_time, limit=0, limit_mode="latest")` +### 4. 获取指定聊天中指定用户在指定时间范围内的消息 +```python +def get_messages_by_time_in_chat_for_users( + chat_id: str, + start_time: float, + end_time: float, + person_ids: List[str], + limit: int = 0, + limit_mode: str = "latest", +) -> List[Dict[str, Any]]: +``` +获取指定聊天中指定用户在指定时间范围内的消息。 -随机选择一个聊天,返回该聊天在指定时间范围内的消息 - -#### `get_messages_before_time(timestamp, limit=0)` - -获取指定时间戳之前的消息 - -#### `get_messages_before_time_in_chat(chat_id, timestamp, limit=0)` - -获取指定聊天中指定时间戳之前的消息 - -#### `get_messages_before_time_for_users(timestamp, person_ids, limit=0)` - -获取指定用户在指定时间戳之前的消息 - ---- - -## 消息计数API - -### `count_new_messages(chat_id, start_time=0.0, end_time=None)` - -计算指定聊天中从开始时间到结束时间的新消息数量 - -**参数:** +**Args:** - `chat_id` (str): 聊天ID - `start_time` (float): 开始时间戳 -- `end_time` (float): 结束时间戳,如果为None则使用当前时间 +- `end_time` (float): 结束时间戳 +- `person_ids` (List[str]): 用户ID列表 +- `limit` (int): 限制返回消息数量,0为不限制 +- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 -**返回:** `int` - 新消息数量 +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 -**示例:** + +### 5. 随机选择一个聊天,返回该聊天在指定时间范围内的消息 ```python -# 计算最近1小时的新消息数 -import time -now = time.time() -hour_ago = now - 3600 -new_count = message_api.count_new_messages("123456789", hour_ago, now) -print(f"最近1小时有{new_count}条新消息") +def get_random_chat_messages( + start_time: float, + end_time: float, + limit: int = 0, + limit_mode: str = "latest", + filter_mai: bool = False, +) -> List[Dict[str, Any]]: ``` +随机选择一个聊天,返回该聊天在指定时间范围内的消息。 -### `count_new_messages_for_users(chat_id, start_time, end_time, person_ids)` +**Args:** +- `start_time` (float): 开始时间戳 +- `end_time` (float): 结束时间戳 +- `limit` (int): 限制返回消息数量,0为不限制 +- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 +- `filter_mai` (bool): 是否过滤掉机器人的消息,默认False -计算指定聊天中指定用户从开始时间到结束时间的新消息数量 +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 ---- -## 消息格式化API +### 6. 获取指定用户在所有聊天中指定时间范围内的消息 +```python +def get_messages_by_time_for_users( + start_time: float, + end_time: float, + person_ids: List[str], + limit: int = 0, + limit_mode: str = "latest", +) -> List[Dict[str, Any]]: +``` +获取指定用户在所有聊天中指定时间范围内的消息。 -### `build_readable_messages_to_str(messages, **options)` +**Args:** +- `start_time` (float): 开始时间戳 +- `end_time` (float): 结束时间戳 +- `person_ids` (List[str]): 用户ID列表 +- `limit` (int): 限制返回消息数量,0为不限制 +- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 -将消息列表构建成可读的字符串 +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 -**参数:** + +### 7. 获取指定时间戳之前的消息 +```python +def get_messages_before_time( + timestamp: float, + limit: int = 0, + filter_mai: bool = False, +) -> List[Dict[str, Any]]: +``` +获取指定时间戳之前的消息。 + +**Args:** +- `timestamp` (float): 时间戳 +- `limit` (int): 限制返回消息数量,0为不限制 +- `filter_mai` (bool): 是否过滤掉机器人的消息,默认False + +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 + + +### 8. 获取指定聊天中指定时间戳之前的消息 +```python +def get_messages_before_time_in_chat( + chat_id: str, + timestamp: float, + limit: int = 0, + filter_mai: bool = False, +) -> List[Dict[str, Any]]: +``` +获取指定聊天中指定时间戳之前的消息。 + +**Args:** +- `chat_id` (str): 聊天ID +- `timestamp` (float): 时间戳 +- `limit` (int): 限制返回消息数量,0为不限制 +- `filter_mai` (bool): 是否过滤掉机器人的消息,默认False + +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 + + +### 9. 获取指定用户在指定时间戳之前的消息 +```python +def get_messages_before_time_for_users( + timestamp: float, + person_ids: List[str], + limit: int = 0, +) -> List[Dict[str, Any]]: +``` +获取指定用户在指定时间戳之前的消息。 + +**Args:** +- `timestamp` (float): 时间戳 +- `person_ids` (List[str]): 用户ID列表 +- `limit` (int): 限制返回消息数量,0为不限制 + +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 + + +### 10. 获取指定聊天中最近一段时间的消息 +```python +def get_recent_messages( + chat_id: str, + hours: float = 24.0, + limit: int = 100, + limit_mode: str = "latest", + filter_mai: bool = False, +) -> List[Dict[str, Any]]: +``` +获取指定聊天中最近一段时间的消息。 + +**Args:** +- `chat_id` (str): 聊天ID +- `hours` (float): 最近多少小时,默认24小时 +- `limit` (int): 限制返回消息数量,默认100条 +- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 +- `filter_mai` (bool): 是否过滤掉机器人的消息,默认False + +**Returns:** +- `List[Dict[str, Any]]` - 消息列表 + + +### 11. 计算指定聊天中从开始时间到结束时间的新消息数量 +```python +def count_new_messages( + chat_id: str, + start_time: float = 0.0, + end_time: Optional[float] = None, +) -> int: +``` +计算指定聊天中从开始时间到结束时间的新消息数量。 + +**Args:** +- `chat_id` (str): 聊天ID +- `start_time` (float): 开始时间戳 +- `end_time` (Optional[float]): 结束时间戳,如果为None则使用当前时间 + +**Returns:** +- `int` - 新消息数量 + + +### 12. 计算指定聊天中指定用户从开始时间到结束时间的新消息数量 +```python +def count_new_messages_for_users( + chat_id: str, + start_time: float, + end_time: float, + person_ids: List[str], +) -> int: +``` +计算指定聊天中指定用户从开始时间到结束时间的新消息数量。 + +**Args:** +- `chat_id` (str): 聊天ID +- `start_time` (float): 开始时间戳 +- `end_time` (float): 结束时间戳 +- `person_ids` (List[str]): 用户ID列表 + +**Returns:** +- `int` - 新消息数量 + + +### 13. 将消息列表构建成可读的字符串 +```python +def build_readable_messages_to_str( + messages: List[Dict[str, Any]], + replace_bot_name: bool = True, + merge_messages: bool = False, + timestamp_mode: str = "relative", + read_mark: float = 0.0, + truncate: bool = False, + show_actions: bool = False, +) -> str: +``` +将消息列表构建成可读的字符串。 + +**Args:** - `messages` (List[Dict[str, Any]]): 消息列表 -- `replace_bot_name` (bool): 是否将机器人的名称替换为"你",默认True -- `merge_messages` (bool): 是否合并连续消息,默认False -- `timestamp_mode` (str): 时间戳显示模式,`"relative"`或`"absolute"`,默认`"relative"` -- `read_mark` (float): 已读标记时间戳,用于分割已读和未读消息,默认0.0 -- `truncate` (bool): 是否截断长消息,默认False -- `show_actions` (bool): 是否显示动作记录,默认False +- `replace_bot_name` (bool): 是否将机器人的名称替换为"你" +- `merge_messages` (bool): 是否合并连续消息 +- `timestamp_mode` (str): 时间戳显示模式,`"relative"`或`"absolute"` +- `read_mark` (float): 已读标记时间戳,用于分割已读和未读消息 +- `truncate` (bool): 是否截断长消息 +- `show_actions` (bool): 是否显示动作记录 -**返回:** `str` - 格式化后的可读字符串 +**Returns:** +- `str` - 格式化后的可读字符串 -**示例:** + +### 14. 将消息列表构建成可读的字符串,并返回详细信息 ```python -# 获取消息并格式化为可读文本 -messages = message_api.get_recent_messages("123456789", hours=2) -readable_text = message_api.build_readable_messages_to_str( - messages, - replace_bot_name=True, - merge_messages=True, - timestamp_mode="relative" -) -print(readable_text) +async def build_readable_messages_with_details( + messages: List[Dict[str, Any]], + replace_bot_name: bool = True, + merge_messages: bool = False, + timestamp_mode: str = "relative", + truncate: bool = False, +) -> Tuple[str, List[Tuple[float, str, str]]]: ``` +将消息列表构建成可读的字符串,并返回详细信息。 -### `build_readable_messages_with_details(messages, **options)` 异步 +**Args:** +- `messages` (List[Dict[str, Any]]): 消息列表 +- `replace_bot_name` (bool): 是否将机器人的名称替换为"你" +- `merge_messages` (bool): 是否合并连续消息 +- `timestamp_mode` (str): 时间戳显示模式,`"relative"`或`"absolute"` +- `truncate` (bool): 是否截断长消息 -将消息列表构建成可读的字符串,并返回详细信息 +**Returns:** +- `Tuple[str, List[Tuple[float, str, str]]]` - 格式化后的可读字符串和详细信息元组列表(时间戳, 昵称, 内容) -**参数:** 与 `build_readable_messages_to_str` 类似,但不包含 `read_mark` 和 `show_actions` -**返回:** `Tuple[str, List[Tuple[float, str, str]]]` - 格式化字符串和详细信息元组列表(时间戳, 昵称, 内容) - -**示例:** +### 15. 从消息列表中提取不重复的用户ID列表 ```python -# 异步获取详细格式化信息 -readable_text, details = await message_api.build_readable_messages_with_details( - messages, - timestamp_mode="absolute" -) - -for timestamp, nickname, content in details: - print(f"{timestamp}: {nickname} 说: {content}") +async def get_person_ids_from_messages( + messages: List[Dict[str, Any]], +) -> List[str]: ``` +从消息列表中提取不重复的用户ID列表。 -### `get_person_ids_from_messages(messages)` 异步 - -从消息列表中提取不重复的用户ID列表 - -**参数:** +**Args:** - `messages` (List[Dict[str, Any]]): 消息列表 -**返回:** `List[str]` - 用户ID列表 +**Returns:** +- `List[str]` - 用户ID列表 -**示例:** + +### 16. 从消息列表中移除机器人的消息 ```python -# 获取参与对话的所有用户ID -messages = message_api.get_recent_messages("123456789") -person_ids = await message_api.get_person_ids_from_messages(messages) -print(f"参与对话的用户: {person_ids}") +def filter_mai_messages( + messages: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: ``` +从消息列表中移除机器人的消息。 ---- +**Args:** +- `messages` (List[Dict[str, Any]]): 消息列表,每个元素是消息字典 -## 完整使用示例 - -### 场景1:统计活跃度 - -```python -import time -from src.plugin_system.apis import message_api - -async def analyze_chat_activity(chat_id: str): - """分析聊天活跃度""" - now = time.time() - day_ago = now - 24 * 3600 - - # 获取最近24小时的消息 - messages = message_api.get_recent_messages(chat_id, hours=24) - - # 统计消息数量 - total_count = len(messages) - - # 获取参与用户 - person_ids = await message_api.get_person_ids_from_messages(messages) - - # 格式化消息内容 - readable_text = message_api.build_readable_messages_to_str( - messages[-10:], # 最后10条消息 - merge_messages=True, - timestamp_mode="relative" - ) - - return { - "total_messages": total_count, - "active_users": len(person_ids), - "recent_chat": readable_text - } -``` - -### 场景2:查看特定用户的历史消息 - -```python -def get_user_history(chat_id: str, user_id: str, days: int = 7): - """获取用户最近N天的消息历史""" - now = time.time() - start_time = now - days * 24 * 3600 - - # 获取特定用户的消息 - user_messages = message_api.get_messages_by_time_in_chat_for_users( - chat_id=chat_id, - start_time=start_time, - end_time=now, - person_ids=[user_id], - limit=100 - ) - - # 格式化为可读文本 - readable_history = message_api.build_readable_messages_to_str( - user_messages, - replace_bot_name=False, - timestamp_mode="absolute" - ) - - return readable_history -``` - ---- +**Returns:** +- `List[Dict[str, Any]]` - 过滤后的消息列表 ## 注意事项 1. **时间戳格式**:所有时间参数都使用Unix时间戳(float类型) -2. **异步函数**:`build_readable_messages_with_details` 和 `get_person_ids_from_messages` 是异步函数,需要使用 `await` +2. **异步函数**:部分函数是异步函数,需要使用 `await` 3. **性能考虑**:查询大量消息时建议设置合理的 `limit` 参数 4. **消息格式**:返回的消息是字典格式,包含时间戳、发送者、内容等信息 5. **用户ID**:`person_ids` 参数接受字符串列表,用于筛选特定用户的消息 \ No newline at end of file diff --git a/docs/plugins/api/person-api.md b/docs/plugins/api/person-api.md index 3e1bafaf7..f97498dcc 100644 --- a/docs/plugins/api/person-api.md +++ b/docs/plugins/api/person-api.md @@ -6,59 +6,65 @@ ```python from src.plugin_system.apis import person_api +# 或者 +from src.plugin_system import person_api ``` ## 主要功能 -### 1. Person ID管理 - -#### `get_person_id(platform: str, user_id: int) -> str` +### 1. Person ID 获取 +```python +def get_person_id(platform: str, user_id: int) -> str: +``` 根据平台和用户ID获取person_id -**参数:** +**Args:** - `platform`:平台名称,如 "qq", "telegram" 等 - `user_id`:用户ID -**返回:** +**Returns:** - `str`:唯一的person_id(MD5哈希值) -**示例:** +#### 示例 ```python person_id = person_api.get_person_id("qq", 123456) -print(f"Person ID: {person_id}") ``` ### 2. 用户信息查询 +```python +async def get_person_value(person_id: str, field_name: str, default: Any = None) -> Any: +``` +查询单个用户信息字段值 -#### `get_person_value(person_id: str, field_name: str, default: Any = None) -> Any` -根据person_id和字段名获取某个值 - -**参数:** +**Args:** - `person_id`:用户的唯一标识ID -- `field_name`:要获取的字段名,如 "nickname", "impression" 等 -- `default`:当字段不存在或获取失败时返回的默认值 +- `field_name`:要获取的字段名 +- `default`:字段值不存在时的默认值 -**返回:** +**Returns:** - `Any`:字段值或默认值 -**示例:** +#### 示例 ```python nickname = await person_api.get_person_value(person_id, "nickname", "未知用户") impression = await person_api.get_person_value(person_id, "impression") ``` -#### `get_person_values(person_id: str, field_names: list, default_dict: dict = None) -> dict` +### 3. 批量用户信息查询 +```python +async def get_person_values(person_id: str, field_names: list, default_dict: Optional[dict] = None) -> dict: +``` 批量获取用户信息字段值 -**参数:** +**Args:** - `person_id`:用户的唯一标识ID - `field_names`:要获取的字段名列表 - `default_dict`:默认值字典,键为字段名,值为默认值 -**返回:** +**Returns:** - `dict`:字段名到值的映射字典 -**示例:** +#### 示例 ```python values = await person_api.get_person_values( person_id, @@ -67,204 +73,31 @@ values = await person_api.get_person_values( ) ``` -### 3. 用户状态查询 - -#### `is_person_known(platform: str, user_id: int) -> bool` +### 4. 判断用户是否已知 +```python +async def is_person_known(platform: str, user_id: int) -> bool: +``` 判断是否认识某个用户 -**参数:** +**Args:** - `platform`:平台名称 - `user_id`:用户ID -**返回:** +**Returns:** - `bool`:是否认识该用户 -**示例:** +### 5. 根据用户名获取Person ID ```python -known = await person_api.is_person_known("qq", 123456) -if known: - print("这个用户我认识") +def get_person_id_by_name(person_name: str) -> str: ``` - -### 4. 用户名查询 - -#### `get_person_id_by_name(person_name: str) -> str` 根据用户名获取person_id -**参数:** +**Args:** - `person_name`:用户名 -**返回:** +**Returns:** - `str`:person_id,如果未找到返回空字符串 -**示例:** -```python -person_id = person_api.get_person_id_by_name("张三") -if person_id: - print(f"找到用户: {person_id}") -``` - -## 使用示例 - -### 1. 基础用户信息获取 - -```python -from src.plugin_system.apis import person_api - -async def get_user_info(platform: str, user_id: int): - """获取用户基本信息""" - - # 获取person_id - person_id = person_api.get_person_id(platform, user_id) - - # 获取用户信息 - user_info = await person_api.get_person_values( - person_id, - ["nickname", "impression", "know_times", "last_seen"], - { - "nickname": "未知用户", - "impression": "", - "know_times": 0, - "last_seen": 0 - } - ) - - return { - "person_id": person_id, - "nickname": user_info["nickname"], - "impression": user_info["impression"], - "know_times": user_info["know_times"], - "last_seen": user_info["last_seen"] - } -``` - -### 2. 在Action中使用用户信息 - -```python -from src.plugin_system.base import BaseAction - -class PersonalizedAction(BaseAction): - async def execute(self, action_data, chat_stream): - # 获取发送者信息 - user_id = chat_stream.user_info.user_id - platform = chat_stream.platform - - # 获取person_id - person_id = person_api.get_person_id(platform, user_id) - - # 获取用户昵称和印象 - nickname = await person_api.get_person_value(person_id, "nickname", "朋友") - impression = await person_api.get_person_value(person_id, "impression", "") - - # 根据用户信息个性化回复 - if impression: - response = f"你好 {nickname}!根据我对你的了解:{impression}" - else: - response = f"你好 {nickname}!很高兴见到你。" - - return { - "success": True, - "response": response, - "user_info": { - "nickname": nickname, - "impression": impression - } - } -``` - -### 3. 用户识别和欢迎 - -```python -async def welcome_user(chat_stream): - """欢迎用户,区分新老用户""" - - user_id = chat_stream.user_info.user_id - platform = chat_stream.platform - - # 检查是否认识这个用户 - is_known = await person_api.is_person_known(platform, user_id) - - if is_known: - # 老用户,获取详细信息 - person_id = person_api.get_person_id(platform, user_id) - nickname = await person_api.get_person_value(person_id, "nickname", "老朋友") - know_times = await person_api.get_person_value(person_id, "know_times", 0) - - welcome_msg = f"欢迎回来,{nickname}!我们已经聊过 {know_times} 次了。" - else: - # 新用户 - welcome_msg = "你好!很高兴认识你,我是MaiBot。" - - return welcome_msg -``` - -### 4. 用户搜索功能 - -```python -async def find_user_by_name(name: str): - """根据名字查找用户""" - - person_id = person_api.get_person_id_by_name(name) - - if not person_id: - return {"found": False, "message": f"未找到名为 '{name}' 的用户"} - - # 获取用户详细信息 - user_info = await person_api.get_person_values( - person_id, - ["nickname", "platform", "user_id", "impression", "know_times"], - {} - ) - - return { - "found": True, - "person_id": person_id, - "info": user_info - } -``` - -### 5. 用户印象分析 - -```python -async def analyze_user_relationship(chat_stream): - """分析用户关系""" - - user_id = chat_stream.user_info.user_id - platform = chat_stream.platform - person_id = person_api.get_person_id(platform, user_id) - - # 获取关系相关信息 - relationship_info = await person_api.get_person_values( - person_id, - ["nickname", "impression", "know_times", "relationship_level", "last_interaction"], - { - "nickname": "未知", - "impression": "", - "know_times": 0, - "relationship_level": "stranger", - "last_interaction": 0 - } - ) - - # 分析关系程度 - know_times = relationship_info["know_times"] - if know_times == 0: - relationship = "陌生人" - elif know_times < 5: - relationship = "新朋友" - elif know_times < 20: - relationship = "熟人" - else: - relationship = "老朋友" - - return { - "nickname": relationship_info["nickname"], - "relationship": relationship, - "impression": relationship_info["impression"], - "interaction_count": know_times - } -``` - ## 常用字段说明 ### 基础信息字段 @@ -274,69 +107,13 @@ async def analyze_user_relationship(chat_stream): ### 关系信息字段 - `impression`:对用户的印象 -- `know_times`:交互次数 -- `relationship_level`:关系等级 -- `last_seen`:最后见面时间 -- `last_interaction`:最后交互时间 +- `points`: 用户特征点 -### 个性化字段 -- `preferences`:用户偏好 -- `interests`:兴趣爱好 -- `mood_history`:情绪历史 -- `topic_interests`:话题兴趣 - -## 最佳实践 - -### 1. 错误处理 -```python -async def safe_get_user_info(person_id: str, field: str): - """安全获取用户信息""" - try: - value = await person_api.get_person_value(person_id, field) - return value if value is not None else "未设置" - except Exception as e: - logger.error(f"获取用户信息失败: {e}") - return "获取失败" -``` - -### 2. 批量操作 -```python -async def get_complete_user_profile(person_id: str): - """获取完整用户档案""" - - # 一次性获取所有需要的字段 - fields = [ - "nickname", "impression", "know_times", - "preferences", "interests", "relationship_level" - ] - - defaults = { - "nickname": "用户", - "impression": "", - "know_times": 0, - "preferences": "{}", - "interests": "[]", - "relationship_level": "stranger" - } - - profile = await person_api.get_person_values(person_id, fields, defaults) - - # 处理JSON字段 - try: - profile["preferences"] = json.loads(profile["preferences"]) - profile["interests"] = json.loads(profile["interests"]) - except: - profile["preferences"] = {} - profile["interests"] = [] - - return profile -``` +其他字段可以参考`PersonInfo`类的属性(位于`src.common.database.database_model`) ## 注意事项 -1. **异步操作**:大部分查询函数都是异步的,需要使用`await` -2. **错误处理**:所有函数都有错误处理,失败时记录日志并返回默认值 -3. **数据类型**:返回的数据可能是字符串、数字或JSON,需要适当处理 -4. **性能考虑**:批量查询优于单个查询 -5. **隐私保护**:确保用户信息的使用符合隐私政策 -6. **数据一致性**:person_id是用户的唯一标识,应妥善保存和使用 \ No newline at end of file +1. **异步操作**:部分查询函数都是异步的,需要使用`await` +2. **性能考虑**:批量查询优于单个查询 +3. **隐私保护**:确保用户信息的使用符合隐私政策 +4. **数据一致性**:person_id是用户的唯一标识,应妥善保存和使用 \ No newline at end of file diff --git a/docs/plugins/api/plugin-manage-api.md b/docs/plugins/api/plugin-manage-api.md new file mode 100644 index 000000000..688ea9ef8 --- /dev/null +++ b/docs/plugins/api/plugin-manage-api.md @@ -0,0 +1,105 @@ +# 插件管理API + +插件管理API模块提供了对插件的加载、卸载、重新加载以及目录管理功能。 + +## 导入方式 +```python +from src.plugin_system.apis import plugin_manage_api +# 或者 +from src.plugin_system import plugin_manage_api +``` + +## 功能概述 + +插件管理API主要提供以下功能: +- **插件查询** - 列出当前加载的插件或已注册的插件。 +- **插件管理** - 加载、卸载、重新加载插件。 +- **插件目录管理** - 添加插件目录并重新扫描。 + +## 主要功能 + +### 1. 列出当前加载的插件 +```python +def list_loaded_plugins() -> List[str]: +``` +列出所有当前加载的插件。 + +**Returns:** +- `List[str]` - 当前加载的插件名称列表。 + +### 2. 列出所有已注册的插件 +```python +def list_registered_plugins() -> List[str]: +``` +列出所有已注册的插件。 + +**Returns:** +- `List[str]` - 已注册的插件名称列表。 + +### 3. 获取插件路径 +```python +def get_plugin_path(plugin_name: str) -> str: +``` +获取指定插件的路径。 + +**Args:** +- `plugin_name` (str): 要查询的插件名称。 +**Returns:** +- `str` - 插件的路径,如果插件不存在则 raise ValueError。 + +### 4. 卸载指定的插件 +```python +async def remove_plugin(plugin_name: str) -> bool: +``` +卸载指定的插件。 + +**Args:** +- `plugin_name` (str): 要卸载的插件名称。 + +**Returns:** +- `bool` - 卸载是否成功。 + +### 5. 重新加载指定的插件 +```python +async def reload_plugin(plugin_name: str) -> bool: +``` +重新加载指定的插件。 + +**Args:** +- `plugin_name` (str): 要重新加载的插件名称。 + +**Returns:** +- `bool` - 重新加载是否成功。 + +### 6. 加载指定的插件 +```python +def load_plugin(plugin_name: str) -> Tuple[bool, int]: +``` +加载指定的插件。 + +**Args:** +- `plugin_name` (str): 要加载的插件名称。 + +**Returns:** +- `Tuple[bool, int]` - 加载是否成功,成功或失败的个数。 + +### 7. 添加插件目录 +```python +def add_plugin_directory(plugin_directory: str) -> bool: +``` +添加插件目录。 + +**Args:** +- `plugin_directory` (str): 要添加的插件目录路径。 + +**Returns:** +- `bool` - 添加是否成功。 + +### 8. 重新扫描插件目录 +```python +def rescan_plugin_directory() -> Tuple[int, int]: +``` +重新扫描插件目录,加载新插件。 + +**Returns:** +- `Tuple[int, int]` - 成功加载的插件数量和失败的插件数量。 \ No newline at end of file diff --git a/docs/plugins/api/send-api.md b/docs/plugins/api/send-api.md index 79335c61a..8b3c607fa 100644 --- a/docs/plugins/api/send-api.md +++ b/docs/plugins/api/send-api.md @@ -6,86 +6,108 @@ ```python from src.plugin_system.apis import send_api +# 或者 +from src.plugin_system import send_api ``` ## 主要功能 -### 1. 文本消息发送 +### 1. 发送文本消息 +```python +async def text_to_stream( + text: str, + stream_id: str, + typing: bool = False, + reply_to: str = "", + storage_message: bool = True, +) -> bool: +``` +发送文本消息到指定的流 -#### `text_to_group(text, group_id, platform="qq", typing=False, reply_to="", storage_message=True)` -向群聊发送文本消息 +**Args:** +- `text` (str): 要发送的文本内容 +- `stream_id` (str): 聊天流ID +- `typing` (bool): 是否显示正在输入 +- `reply_to` (str): 回复消息,格式为"发送者:消息内容" +- `storage_message` (bool): 是否存储消息到数据库 -**参数:** -- `text`:要发送的文本内容 -- `group_id`:群聊ID -- `platform`:平台,默认为"qq" -- `typing`:是否显示正在输入 -- `reply_to`:回复消息的格式,如"发送者:消息内容" -- `storage_message`:是否存储到数据库 +**Returns:** +- `bool` - 是否发送成功 -**返回:** -- `bool`:是否发送成功 +### 2. 发送表情包 +```python +async def emoji_to_stream(emoji_base64: str, stream_id: str, storage_message: bool = True) -> bool: +``` +向指定流发送表情包。 -#### `text_to_user(text, user_id, platform="qq", typing=False, reply_to="", storage_message=True)` -向用户发送私聊文本消息 +**Args:** +- `emoji_base64` (str): 表情包的base64编码 +- `stream_id` (str): 聊天流ID +- `storage_message` (bool): 是否存储消息到数据库 -**参数与返回值同上** +**Returns:** +- `bool` - 是否发送成功 -### 2. 表情包发送 +### 3. 发送图片 +```python +async def image_to_stream(image_base64: str, stream_id: str, storage_message: bool = True) -> bool: +``` +向指定流发送图片。 -#### `emoji_to_group(emoji_base64, group_id, platform="qq", storage_message=True)` -向群聊发送表情包 +**Args:** +- `image_base64` (str): 图片的base64编码 +- `stream_id` (str): 聊天流ID +- `storage_message` (bool): 是否存储消息到数据库 -**参数:** -- `emoji_base64`:表情包的base64编码 -- `group_id`:群聊ID -- `platform`:平台,默认为"qq" -- `storage_message`:是否存储到数据库 +**Returns:** +- `bool` - 是否发送成功 -#### `emoji_to_user(emoji_base64, user_id, platform="qq", storage_message=True)` -向用户发送表情包 +### 4. 发送命令 +```python +async def command_to_stream(command: Union[str, dict], stream_id: str, storage_message: bool = True, display_message: str = "") -> bool: +``` +向指定流发送命令。 -### 3. 图片发送 +**Args:** +- `command` (Union[str, dict]): 命令内容 +- `stream_id` (str): 聊天流ID +- `storage_message` (bool): 是否存储消息到数据库 +- `display_message` (str): 显示消息 -#### `image_to_group(image_base64, group_id, platform="qq", storage_message=True)` -向群聊发送图片 +**Returns:** +- `bool` - 是否发送成功 -#### `image_to_user(image_base64, user_id, platform="qq", storage_message=True)` -向用户发送图片 +### 5. 发送自定义类型消息 +```python +async def custom_to_stream( + message_type: str, + content: str, + stream_id: str, + display_message: str = "", + typing: bool = False, + reply_to: str = "", + storage_message: bool = True, + show_log: bool = True, +) -> bool: +``` +向指定流发送自定义类型消息。 -### 4. 命令发送 +**Args:** +- `message_type` (str): 消息类型,如"text"、"image"、"emoji"、"video"、"file"等 +- `content` (str): 消息内容(通常是base64编码或文本) +- `stream_id` (str): 聊天流ID +- `display_message` (str): 显示消息 +- `typing` (bool): 是否显示正在输入 +- `reply_to` (str): 回复消息,格式为"发送者:消息内容" +- `storage_message` (bool): 是否存储消息到数据库 +- `show_log` (bool): 是否显示日志 -#### `command_to_group(command, group_id, platform="qq", storage_message=True)` -向群聊发送命令 - -#### `command_to_user(command, user_id, platform="qq", storage_message=True)` -向用户发送命令 - -### 5. 自定义消息发送 - -#### `custom_to_group(message_type, content, group_id, platform="qq", display_message="", typing=False, reply_to="", storage_message=True)` -向群聊发送自定义类型消息 - -#### `custom_to_user(message_type, content, user_id, platform="qq", display_message="", typing=False, reply_to="", storage_message=True)` -向用户发送自定义类型消息 - -#### `custom_message(message_type, content, target_id, is_group=True, platform="qq", display_message="", typing=False, reply_to="", storage_message=True)` -通用的自定义消息发送 - -**参数:** -- `message_type`:消息类型,如"text"、"image"、"emoji"等 -- `content`:消息内容 -- `target_id`:目标ID(群ID或用户ID) -- `is_group`:是否为群聊 -- `platform`:平台 -- `display_message`:显示消息 -- `typing`:是否显示正在输入 -- `reply_to`:回复消息 -- `storage_message`:是否存储 +**Returns:** +- `bool` - 是否发送成功 ## 使用示例 -### 1. 基础文本发送 +### 1. 基础文本发送,并回复消息 ```python from src.plugin_system.apis import send_api @@ -93,57 +115,23 @@ from src.plugin_system.apis import send_api async def send_hello(chat_stream): """发送问候消息""" - if chat_stream.group_info: - # 群聊 - success = await send_api.text_to_group( - text="大家好!", - group_id=chat_stream.group_info.group_id, - typing=True - ) - else: - # 私聊 - success = await send_api.text_to_user( - text="你好!", - user_id=chat_stream.user_info.user_id, - typing=True - ) + success = await send_api.text_to_stream( + text="Hello, world!", + stream_id=chat_stream.stream_id, + typing=True, + reply_to="User:How are you?", + storage_message=True + ) return success ``` -### 2. 回复特定消息 - -```python -async def reply_to_message(chat_stream, reply_text, original_sender, original_message): - """回复特定消息""" - - # 构建回复格式 - reply_to = f"{original_sender}:{original_message}" - - if chat_stream.group_info: - success = await send_api.text_to_group( - text=reply_text, - group_id=chat_stream.group_info.group_id, - reply_to=reply_to - ) - else: - success = await send_api.text_to_user( - text=reply_text, - user_id=chat_stream.user_info.user_id, - reply_to=reply_to - ) - - return success -``` - -### 3. 发送表情包 +### 2. 发送表情包 ```python +from src.plugin_system.apis import emoji_api async def send_emoji_reaction(chat_stream, emotion): """根据情感发送表情包""" - - from src.plugin_system.apis import emoji_api - # 获取表情包 emoji_result = await emoji_api.get_by_emotion(emotion) if not emoji_result: @@ -152,107 +140,10 @@ async def send_emoji_reaction(chat_stream, emotion): emoji_base64, description, matched_emotion = emoji_result # 发送表情包 - if chat_stream.group_info: - success = await send_api.emoji_to_group( - emoji_base64=emoji_base64, - group_id=chat_stream.group_info.group_id - ) - else: - success = await send_api.emoji_to_user( - emoji_base64=emoji_base64, - user_id=chat_stream.user_info.user_id - ) - - return success -``` - -### 4. 在Action中发送消息 - -```python -from src.plugin_system.base import BaseAction - -class MessageAction(BaseAction): - async def execute(self, action_data, chat_stream): - message_type = action_data.get("type", "text") - content = action_data.get("content", "") - - if message_type == "text": - success = await self.send_text(chat_stream, content) - elif message_type == "emoji": - success = await self.send_emoji(chat_stream, content) - elif message_type == "image": - success = await self.send_image(chat_stream, content) - else: - success = False - - return {"success": success} - - async def send_text(self, chat_stream, text): - if chat_stream.group_info: - return await send_api.text_to_group(text, chat_stream.group_info.group_id) - else: - return await send_api.text_to_user(text, chat_stream.user_info.user_id) - - async def send_emoji(self, chat_stream, emoji_base64): - if chat_stream.group_info: - return await send_api.emoji_to_group(emoji_base64, chat_stream.group_info.group_id) - else: - return await send_api.emoji_to_user(emoji_base64, chat_stream.user_info.user_id) - - async def send_image(self, chat_stream, image_base64): - if chat_stream.group_info: - return await send_api.image_to_group(image_base64, chat_stream.group_info.group_id) - else: - return await send_api.image_to_user(image_base64, chat_stream.user_info.user_id) -``` - -### 5. 批量发送消息 - -```python -async def broadcast_message(message: str, target_groups: list): - """向多个群组广播消息""" - - results = {} - - for group_id in target_groups: - try: - success = await send_api.text_to_group( - text=message, - group_id=group_id, - typing=True - ) - results[group_id] = success - except Exception as e: - results[group_id] = False - print(f"发送到群 {group_id} 失败: {e}") - - return results -``` - -### 6. 智能消息发送 - -```python -async def smart_send(chat_stream, message_data): - """智能发送不同类型的消息""" - - message_type = message_data.get("type", "text") - content = message_data.get("content", "") - options = message_data.get("options", {}) - - # 根据聊天流类型选择发送方法 - target_id = (chat_stream.group_info.group_id if chat_stream.group_info - else chat_stream.user_info.user_id) - is_group = chat_stream.group_info is not None - - # 使用通用发送方法 - success = await send_api.custom_message( - message_type=message_type, - content=content, - target_id=target_id, - is_group=is_group, - typing=options.get("typing", False), - reply_to=options.get("reply_to", ""), - display_message=options.get("display_message", "") + success = await send_api.emoji_to_stream( + emoji_base64=emoji_base64, + stream_id=chat_stream.stream_id, + storage_message=False # 不存储到数据库 ) return success @@ -273,90 +164,6 @@ async def smart_send(chat_stream, message_data): 系统会自动查找匹配的原始消息并进行回复。 -## 高级用法 - -### 1. 消息发送队列 - -```python -import asyncio - -class MessageQueue: - def __init__(self): - self.queue = asyncio.Queue() - self.running = False - - async def add_message(self, chat_stream, message_type, content, options=None): - """添加消息到队列""" - message_item = { - "chat_stream": chat_stream, - "type": message_type, - "content": content, - "options": options or {} - } - await self.queue.put(message_item) - - async def process_queue(self): - """处理消息队列""" - self.running = True - - while self.running: - try: - message_item = await asyncio.wait_for(self.queue.get(), timeout=1.0) - - # 发送消息 - success = await smart_send( - message_item["chat_stream"], - { - "type": message_item["type"], - "content": message_item["content"], - "options": message_item["options"] - } - ) - - # 标记任务完成 - self.queue.task_done() - - # 发送间隔 - await asyncio.sleep(0.5) - - except asyncio.TimeoutError: - continue - except Exception as e: - print(f"处理消息队列出错: {e}") -``` - -### 2. 消息模板系统 - -```python -class MessageTemplate: - def __init__(self): - self.templates = { - "welcome": "欢迎 {nickname} 加入群聊!", - "goodbye": "{nickname} 离开了群聊。", - "notification": "🔔 通知:{message}", - "error": "❌ 错误:{error_message}", - "success": "✅ 成功:{message}" - } - - def format_message(self, template_name: str, **kwargs) -> str: - """格式化消息模板""" - template = self.templates.get(template_name, "{message}") - return template.format(**kwargs) - - async def send_template(self, chat_stream, template_name: str, **kwargs): - """发送模板消息""" - message = self.format_message(template_name, **kwargs) - - if chat_stream.group_info: - return await send_api.text_to_group(message, chat_stream.group_info.group_id) - else: - return await send_api.text_to_user(message, chat_stream.user_info.user_id) - -# 使用示例 -template_system = MessageTemplate() -await template_system.send_template(chat_stream, "welcome", nickname="张三") -``` - ## 注意事项 1. **异步操作**:所有发送函数都是异步的,必须使用`await` diff --git a/docs/plugins/api/utils-api.md b/docs/plugins/api/utils-api.md deleted file mode 100644 index bbab092e6..000000000 --- a/docs/plugins/api/utils-api.md +++ /dev/null @@ -1,435 +0,0 @@ -# 工具API - -工具API模块提供了各种辅助功能,包括文件操作、时间处理、唯一ID生成等常用工具函数。 - -## 导入方式 - -```python -from src.plugin_system.apis import utils_api -``` - -## 主要功能 - -### 1. 文件操作 - -#### `get_plugin_path(caller_frame=None) -> str` -获取调用者插件的路径 - -**参数:** -- `caller_frame`:调用者的栈帧,默认为None(自动获取) - -**返回:** -- `str`:插件目录的绝对路径 - -**示例:** -```python -plugin_path = utils_api.get_plugin_path() -print(f"插件路径: {plugin_path}") -``` - -#### `read_json_file(file_path: str, default: Any = None) -> Any` -读取JSON文件 - -**参数:** -- `file_path`:文件路径,可以是相对于插件目录的路径 -- `default`:如果文件不存在或读取失败时返回的默认值 - -**返回:** -- `Any`:JSON数据或默认值 - -**示例:** -```python -# 读取插件配置文件 -config = utils_api.read_json_file("config.json", {}) -settings = utils_api.read_json_file("data/settings.json", {"enabled": True}) -``` - -#### `write_json_file(file_path: str, data: Any, indent: int = 2) -> bool` -写入JSON文件 - -**参数:** -- `file_path`:文件路径,可以是相对于插件目录的路径 -- `data`:要写入的数据 -- `indent`:JSON缩进 - -**返回:** -- `bool`:是否写入成功 - -**示例:** -```python -data = {"name": "test", "value": 123} -success = utils_api.write_json_file("output.json", data) -``` - -### 2. 时间相关 - -#### `get_timestamp() -> int` -获取当前时间戳 - -**返回:** -- `int`:当前时间戳(秒) - -#### `format_time(timestamp: Optional[int] = None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str` -格式化时间 - -**参数:** -- `timestamp`:时间戳,如果为None则使用当前时间 -- `format_str`:时间格式字符串 - -**返回:** -- `str`:格式化后的时间字符串 - -#### `parse_time(time_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> int` -解析时间字符串为时间戳 - -**参数:** -- `time_str`:时间字符串 -- `format_str`:时间格式字符串 - -**返回:** -- `int`:时间戳(秒) - -### 3. 其他工具 - -#### `generate_unique_id() -> str` -生成唯一ID - -**返回:** -- `str`:唯一ID - -## 使用示例 - -### 1. 插件数据管理 - -```python -from src.plugin_system.apis import utils_api - -class DataPlugin(BasePlugin): - def __init__(self): - self.plugin_path = utils_api.get_plugin_path() - self.data_file = "plugin_data.json" - self.load_data() - - def load_data(self): - """加载插件数据""" - default_data = { - "users": {}, - "settings": {"enabled": True}, - "stats": {"message_count": 0} - } - self.data = utils_api.read_json_file(self.data_file, default_data) - - def save_data(self): - """保存插件数据""" - return utils_api.write_json_file(self.data_file, self.data) - - async def handle_action(self, action_data, chat_stream): - # 更新统计信息 - self.data["stats"]["message_count"] += 1 - self.data["stats"]["last_update"] = utils_api.get_timestamp() - - # 保存数据 - if self.save_data(): - return {"success": True, "message": "数据已保存"} - else: - return {"success": False, "message": "数据保存失败"} -``` - -### 2. 日志记录系统 - -```python -class PluginLogger: - def __init__(self, plugin_name: str): - self.plugin_name = plugin_name - self.log_file = f"{plugin_name}_log.json" - self.logs = utils_api.read_json_file(self.log_file, []) - - def log_event(self, event_type: str, message: str, data: dict = None): - """记录事件""" - log_entry = { - "id": utils_api.generate_unique_id(), - "timestamp": utils_api.get_timestamp(), - "formatted_time": utils_api.format_time(), - "event_type": event_type, - "message": message, - "data": data or {} - } - - self.logs.append(log_entry) - - # 保持最新的100条记录 - if len(self.logs) > 100: - self.logs = self.logs[-100:] - - # 保存到文件 - utils_api.write_json_file(self.log_file, self.logs) - - def get_logs_by_type(self, event_type: str) -> list: - """获取指定类型的日志""" - return [log for log in self.logs if log["event_type"] == event_type] - - def get_recent_logs(self, count: int = 10) -> list: - """获取最近的日志""" - return self.logs[-count:] - -# 使用示例 -logger = PluginLogger("my_plugin") -logger.log_event("user_action", "用户发送了消息", {"user_id": "123", "message": "hello"}) -``` - -### 3. 配置管理系统 - -```python -class ConfigManager: - def __init__(self, config_file: str = "plugin_config.json"): - self.config_file = config_file - self.default_config = { - "enabled": True, - "debug": False, - "max_users": 100, - "response_delay": 1.0, - "features": { - "auto_reply": True, - "logging": True - } - } - self.config = self.load_config() - - def load_config(self) -> dict: - """加载配置""" - return utils_api.read_json_file(self.config_file, self.default_config) - - def save_config(self) -> bool: - """保存配置""" - return utils_api.write_json_file(self.config_file, self.config, indent=4) - - def get(self, key: str, default=None): - """获取配置值,支持嵌套访问""" - keys = key.split('.') - value = self.config - - for k in keys: - if isinstance(value, dict) and k in value: - value = value[k] - else: - return default - - return value - - def set(self, key: str, value): - """设置配置值,支持嵌套设置""" - keys = key.split('.') - config = self.config - - for k in keys[:-1]: - if k not in config: - config[k] = {} - config = config[k] - - config[keys[-1]] = value - - def update_config(self, updates: dict): - """批量更新配置""" - def deep_update(base, updates): - for key, value in updates.items(): - if isinstance(value, dict) and key in base and isinstance(base[key], dict): - deep_update(base[key], value) - else: - base[key] = value - - deep_update(self.config, updates) - -# 使用示例 -config = ConfigManager() -print(f"调试模式: {config.get('debug', False)}") -print(f"自动回复: {config.get('features.auto_reply', True)}") - -config.set('features.new_feature', True) -config.save_config() -``` - -### 4. 缓存系统 - -```python -class PluginCache: - def __init__(self, cache_file: str = "plugin_cache.json", ttl: int = 3600): - self.cache_file = cache_file - self.ttl = ttl # 缓存过期时间(秒) - self.cache = self.load_cache() - - def load_cache(self) -> dict: - """加载缓存""" - return utils_api.read_json_file(self.cache_file, {}) - - def save_cache(self): - """保存缓存""" - return utils_api.write_json_file(self.cache_file, self.cache) - - def get(self, key: str): - """获取缓存值""" - if key not in self.cache: - return None - - item = self.cache[key] - current_time = utils_api.get_timestamp() - - # 检查是否过期 - if current_time - item["timestamp"] > self.ttl: - del self.cache[key] - return None - - return item["value"] - - def set(self, key: str, value): - """设置缓存值""" - self.cache[key] = { - "value": value, - "timestamp": utils_api.get_timestamp() - } - self.save_cache() - - def clear_expired(self): - """清理过期缓存""" - current_time = utils_api.get_timestamp() - expired_keys = [] - - for key, item in self.cache.items(): - if current_time - item["timestamp"] > self.ttl: - expired_keys.append(key) - - for key in expired_keys: - del self.cache[key] - - if expired_keys: - self.save_cache() - - return len(expired_keys) - -# 使用示例 -cache = PluginCache(ttl=1800) # 30分钟过期 -cache.set("user_data_123", {"name": "张三", "score": 100}) -user_data = cache.get("user_data_123") -``` - -### 5. 时间处理工具 - -```python -class TimeHelper: - @staticmethod - def get_time_info(): - """获取当前时间的详细信息""" - timestamp = utils_api.get_timestamp() - return { - "timestamp": timestamp, - "datetime": utils_api.format_time(timestamp), - "date": utils_api.format_time(timestamp, "%Y-%m-%d"), - "time": utils_api.format_time(timestamp, "%H:%M:%S"), - "year": utils_api.format_time(timestamp, "%Y"), - "month": utils_api.format_time(timestamp, "%m"), - "day": utils_api.format_time(timestamp, "%d"), - "weekday": utils_api.format_time(timestamp, "%A") - } - - @staticmethod - def time_ago(timestamp: int) -> str: - """计算时间差""" - current = utils_api.get_timestamp() - diff = current - timestamp - - if diff < 60: - return f"{diff}秒前" - elif diff < 3600: - return f"{diff // 60}分钟前" - elif diff < 86400: - return f"{diff // 3600}小时前" - else: - return f"{diff // 86400}天前" - - @staticmethod - def parse_duration(duration_str: str) -> int: - """解析时间段字符串,返回秒数""" - import re - - pattern = r'(\d+)([smhd])' - matches = re.findall(pattern, duration_str.lower()) - - total_seconds = 0 - for value, unit in matches: - value = int(value) - if unit == 's': - total_seconds += value - elif unit == 'm': - total_seconds += value * 60 - elif unit == 'h': - total_seconds += value * 3600 - elif unit == 'd': - total_seconds += value * 86400 - - return total_seconds - -# 使用示例 -time_info = TimeHelper.get_time_info() -print(f"当前时间: {time_info['datetime']}") - -last_seen = 1699000000 -print(f"最后见面: {TimeHelper.time_ago(last_seen)}") - -duration = TimeHelper.parse_duration("1h30m") # 1小时30分钟 = 5400秒 -``` - -## 最佳实践 - -### 1. 错误处理 -```python -def safe_file_operation(file_path: str, data: dict): - """安全的文件操作""" - try: - success = utils_api.write_json_file(file_path, data) - if not success: - logger.warning(f"文件写入失败: {file_path}") - return success - except Exception as e: - logger.error(f"文件操作出错: {e}") - return False -``` - -### 2. 路径处理 -```python -import os - -def get_data_path(filename: str) -> str: - """获取数据文件的完整路径""" - plugin_path = utils_api.get_plugin_path() - data_dir = os.path.join(plugin_path, "data") - - # 确保数据目录存在 - os.makedirs(data_dir, exist_ok=True) - - return os.path.join(data_dir, filename) -``` - -### 3. 定期清理 -```python -async def cleanup_old_files(): - """清理旧文件""" - plugin_path = utils_api.get_plugin_path() - current_time = utils_api.get_timestamp() - - for filename in os.listdir(plugin_path): - if filename.endswith('.tmp'): - file_path = os.path.join(plugin_path, filename) - file_time = os.path.getmtime(file_path) - - # 删除超过24小时的临时文件 - if current_time - file_time > 86400: - os.remove(file_path) -``` - -## 注意事项 - -1. **相对路径**:文件路径支持相对于插件目录的路径 -2. **自动创建目录**:写入文件时会自动创建必要的目录 -3. **错误处理**:所有函数都有错误处理,失败时返回默认值 -4. **编码格式**:文件读写使用UTF-8编码 -5. **时间格式**:时间戳使用秒为单位 -6. **JSON格式**:JSON文件使用可读性好的缩进格式 \ No newline at end of file diff --git a/docs/plugins/command-components.md b/docs/plugins/command-components.md index d3eb20032..77cc8accf 100644 --- a/docs/plugins/command-components.md +++ b/docs/plugins/command-components.md @@ -2,7 +2,9 @@ ## 📖 什么是Command -Command是直接响应用户明确指令的组件,与Action不同,Command是**被动触发**的,当用户输入特定格式的命令时立即执行。Command通过正则表达式匹配用户输入,提供确定性的功能服务。 +Command是直接响应用户明确指令的组件,与Action不同,Command是**被动触发**的,当用户输入特定格式的命令时立即执行。 + +Command通过正则表达式匹配用户输入,提供确定性的功能服务。 ### 🎯 Command的特点 @@ -12,501 +14,76 @@ Command是直接响应用户明确指令的组件,与Action不同,Command是 - 🛑 **拦截控制**:可以控制是否阻止消息继续处理 - 📝 **参数解析**:支持从用户输入中提取参数 -## 🆚 Action vs Command 核心区别 +--- -| 特征 | Action | Command | -| ------------------ | --------------------- | ---------------- | -| **触发方式** | 麦麦主动决策使用 | 用户主动触发 | -| **决策机制** | 两层决策(激活+使用) | 直接匹配执行 | -| **随机性** | 有随机性和智能性 | 确定性执行 | -| **用途** | 增强麦麦行为拟人化 | 提供具体功能服务 | -| **性能影响** | 需要LLM决策 | 正则匹配,性能好 | +## 🛠️ Command组件的基本结构 -## 🏗️ Command基本结构 - -### 必须属性 +首先,Command组件需要继承自`BaseCommand`类,并实现必要的方法。 ```python -from src.plugin_system import BaseCommand +class ExampleCommand(BaseCommand): + command_name = "example" # 命令名称,作为唯一标识符 + command_description = "这是一个示例命令" # 命令描述 + command_pattern = r"" # 命令匹配的正则表达式 -class MyCommand(BaseCommand): - # 正则表达式匹配模式 - command_pattern = r"^/help\s+(?P\w+)$" - - # 命令帮助说明 - command_help = "显示指定主题的帮助信息" - - # 使用示例 - command_examples = ["/help action", "/help command"] - - # 是否拦截后续处理 - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行命令逻辑""" - # 命令执行逻辑 - return True, "执行成功" + async def execute(self) -> Tuple[bool, Optional[str], bool]: + """ + 执行Command的主要逻辑 + + Returns: + Tuple[bool, str, bool]: + - 第一个bool表示是否成功执行 + - 第二个str是执行结果消息 + - 第三个bool表示是否需要阻止消息继续处理 + """ + # ---- 执行命令的逻辑 ---- + return True, "执行成功", False ``` +**`command_pattern`**: 该Command匹配的正则表达式,用于精确匹配用户输入。 -### 属性说明 +请注意:如果希望能获取到命令中的参数,请在正则表达式中使用有命名的捕获组,例如`(?Ppattern)`。 -| 属性 | 类型 | 说明 | -| --------------------- | --------- | -------------------- | -| `command_pattern` | str | 正则表达式匹配模式 | -| `command_help` | str | 命令帮助说明 | -| `command_examples` | List[str] | 使用示例列表 | -| `intercept_message` | bool | 是否拦截消息继续处理 | +这样在匹配时,内部实现可以使用`re.match.groupdict()`方法获取到所有捕获组的参数,并以字典的形式存储在`self.matched_groups`中。 -## 🔍 正则表达式匹配 - -### 基础匹配 +### 匹配样例 +假设我们有一个命令`/example param1=value1 param2=value2`,对应的正则表达式可以是: ```python -class SimpleCommand(BaseCommand): - # 匹配 /ping - command_pattern = r"^/ping$" - - async def execute(self) -> Tuple[bool, Optional[str]]: - await self.send_text("Pong!") - return True, "发送了Pong回复" +class ExampleCommand(BaseCommand): + command_name = "example" + command_description = "这是一个示例命令" + command_pattern = r"/example (?P\w+) (?P\w+)" + + async def execute(self) -> Tuple[bool, Optional[str], bool]: + # 获取匹配的参数 + param1 = self.matched_groups.get("param1") + param2 = self.matched_groups.get("param2") + + # 执行逻辑 + return True, f"参数1: {param1}, 参数2: {param2}", False ``` -### 参数捕获 - -使用命名组 `(?Ppattern)` 捕获参数: +--- +## Command 内置方法说明 ```python -class UserCommand(BaseCommand): - # 匹配 /user add 张三 或 /user del 李四 - command_pattern = r"^/user\s+(?Padd|del|info)\s+(?P\w+)$" - - async def execute(self) -> Tuple[bool, Optional[str]]: - # 通过 self.matched_groups 获取捕获的参数 - action = self.matched_groups.get("action") - username = self.matched_groups.get("username") - - if action == "add": - await self.send_text(f"添加用户:{username}") - elif action == "del": - await self.send_text(f"删除用户:{username}") - elif action == "info": - await self.send_text(f"用户信息:{username}") - - return True, f"执行了{action}操作" +class BaseCommand: + def get_config(self, key: str, default=None): + """获取插件配置值,使用嵌套键访问""" + + async def send_text(self, content: str, reply_to: str = "") -> bool: + """发送回复消息""" + + async def send_type(self, message_type: str, content: str, display_message: str = "", typing: bool = False, reply_to: str = "") -> bool: + """发送指定类型的回复消息到当前聊天环境""" + + async def send_command(self, command_name: str, args: Optional[dict] = None, display_message: str = "", storage_message: bool = True) -> bool: + """发送命令消息""" + + async def send_emoji(self, emoji_base64: str) -> bool: + """发送表情包""" + + async def send_image(self, image_base64: str) -> bool: + """发送图片""" ``` - -### 可选参数 - -```python -class HelpCommand(BaseCommand): - # 匹配 /help 或 /help topic - command_pattern = r"^/help(?:\s+(?P\w+))?$" - - async def execute(self) -> Tuple[bool, Optional[str]]: - topic = self.matched_groups.get("topic") - - if topic: - await self.send_text(f"显示{topic}的帮助") - else: - await self.send_text("显示总体帮助") - - return True, "显示了帮助信息" -``` - -## 🛑 拦截控制详解 - -### 拦截消息 (intercept_message = True) - -```python -class AdminCommand(BaseCommand): - command_pattern = r"^/admin\s+.+" - command_help = "管理员命令" - intercept_message = True # 拦截,不继续处理 - - async def execute(self) -> Tuple[bool, Optional[str]]: - # 执行管理操作 - await self.send_text("执行管理命令") - # 消息不会继续传递给其他组件 - return True, "管理命令执行完成" -``` - -### 不拦截消息 (intercept_message = False) - -```python -class LogCommand(BaseCommand): - command_pattern = r"^/log\s+.+" - command_help = "记录日志" - intercept_message = False # 不拦截,继续处理 - - async def execute(self) -> Tuple[bool, Optional[str]]: - # 记录日志但不阻止后续处理 - await self.send_text("已记录到日志") - # 消息会继续传递,可能触发Action等其他组件 - return True, "日志记录完成" -``` - -### 拦截控制的用途 - -| 场景 | intercept_message | 说明 | -| -------- | ----------------- | -------------------------- | -| 系统命令 | True | 防止命令被当作普通消息处理 | -| 查询命令 | True | 直接返回结果,无需后续处理 | -| 日志命令 | False | 记录但允许消息继续流转 | -| 监控命令 | False | 监控但不影响正常聊天 | - -## 🎨 完整Command示例 - -### 用户管理Command - -```python -from src.plugin_system import BaseCommand -from typing import Tuple, Optional - -class UserManagementCommand(BaseCommand): - """用户管理Command - 展示复杂参数处理""" - - command_pattern = r"^/user\s+(?Padd|del|list|info)\s*(?P\w+)?(?:\s+--(?P.+))?$" - command_help = "用户管理命令,支持添加、删除、列表、信息查询" - command_examples = [ - "/user add 张三", - "/user del 李四", - "/user list", - "/user info 王五", - "/user add 赵六 --role=admin" - ] - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行用户管理命令""" - try: - action = self.matched_groups.get("action") - username = self.matched_groups.get("username") - options = self.matched_groups.get("options") - - # 解析选项 - parsed_options = self._parse_options(options) if options else {} - - if action == "add": - return await self._add_user(username, parsed_options) - elif action == "del": - return await self._delete_user(username) - elif action == "list": - return await self._list_users() - elif action == "info": - return await self._show_user_info(username) - else: - await self.send_text("❌ 不支持的操作") - return False, f"不支持的操作: {action}" - - except Exception as e: - await self.send_text(f"❌ 命令执行失败: {str(e)}") - return False, f"执行失败: {e}" - - def _parse_options(self, options_str: str) -> dict: - """解析命令选项""" - options = {} - if options_str: - for opt in options_str.split(): - if "=" in opt: - key, value = opt.split("=", 1) - options[key] = value - return options - - async def _add_user(self, username: str, options: dict) -> Tuple[bool, str]: - """添加用户""" - if not username: - await self.send_text("❌ 请指定用户名") - return False, "缺少用户名参数" - - # 检查用户是否已存在 - existing_users = await self._get_user_list() - if username in existing_users: - await self.send_text(f"❌ 用户 {username} 已存在") - return False, f"用户已存在: {username}" - - # 添加用户逻辑 - role = options.get("role", "user") - await self.send_text(f"✅ 成功添加用户 {username},角色: {role}") - return True, f"添加用户成功: {username}" - - async def _delete_user(self, username: str) -> Tuple[bool, str]: - """删除用户""" - if not username: - await self.send_text("❌ 请指定用户名") - return False, "缺少用户名参数" - - await self.send_text(f"✅ 用户 {username} 已删除") - return True, f"删除用户成功: {username}" - - async def _list_users(self) -> Tuple[bool, str]: - """列出所有用户""" - users = await self._get_user_list() - if users: - user_list = "\n".join([f"• {user}" for user in users]) - await self.send_text(f"📋 用户列表:\n{user_list}") - else: - await self.send_text("📋 暂无用户") - return True, "显示用户列表" - - async def _show_user_info(self, username: str) -> Tuple[bool, str]: - """显示用户信息""" - if not username: - await self.send_text("❌ 请指定用户名") - return False, "缺少用户名参数" - - # 模拟用户信息 - user_info = f""" -👤 用户信息: {username} -📧 邮箱: {username}@example.com -🕒 注册时间: 2024-01-01 -🎯 角色: 普通用户 - """.strip() - - await self.send_text(user_info) - return True, f"显示用户信息: {username}" - - async def _get_user_list(self) -> list: - """获取用户列表(示例)""" - return ["张三", "李四", "王五"] -``` - -### 系统信息Command - -```python -class SystemInfoCommand(BaseCommand): - """系统信息Command - 展示系统查询功能""" - - command_pattern = r"^/(?:status|info)(?:\s+(?Psystem|memory|plugins|all))?$" - command_help = "查询系统状态信息" - command_examples = [ - "/status", - "/info system", - "/status memory", - "/info plugins" - ] - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行系统信息查询""" - info_type = self.matched_groups.get("type", "all") - - try: - if info_type in ["system", "all"]: - await self._show_system_info() - - if info_type in ["memory", "all"]: - await self._show_memory_info() - - if info_type in ["plugins", "all"]: - await self._show_plugin_info() - - return True, f"显示了{info_type}类型的系统信息" - - except Exception as e: - await self.send_text(f"❌ 获取系统信息失败: {str(e)}") - return False, f"查询失败: {e}" - - async def _show_system_info(self): - """显示系统信息""" - import platform - import datetime - - system_info = f""" -🖥️ **系统信息** -📱 平台: {platform.system()} {platform.release()} -🐍 Python: {platform.python_version()} -⏰ 运行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - """.strip() - - await self.send_text(system_info) - - async def _show_memory_info(self): - """显示内存信息""" - import psutil - - memory = psutil.virtual_memory() - memory_info = f""" -💾 **内存信息** -📊 总内存: {memory.total // (1024**3)} GB -🟢 可用内存: {memory.available // (1024**3)} GB -📈 使用率: {memory.percent}% - """.strip() - - await self.send_text(memory_info) - - async def _show_plugin_info(self): - """显示插件信息""" - # 通过配置获取插件信息 - plugins = await self._get_loaded_plugins() - - plugin_info = f""" -🔌 **插件信息** -📦 已加载插件: {len(plugins)} -🔧 活跃插件: {len([p for p in plugins if p.get('active', False)])} - """.strip() - - await self.send_text(plugin_info) - - async def _get_loaded_plugins(self) -> list: - """获取已加载的插件列表""" - # 这里可以通过配置或API获取实际的插件信息 - return [ - {"name": "core_actions", "active": True}, - {"name": "example_plugin", "active": True}, - ] -``` - -### 自定义前缀Command - -```python -class CustomPrefixCommand(BaseCommand): - """自定义前缀Command - 展示非/前缀的命令""" - - # 使用!前缀而不是/前缀 - command_pattern = r"^[!!](?Proll|dice)\s*(?P\d+)?$" - command_help = "骰子命令,使用!前缀" - command_examples = ["!roll", "!dice 6", "!roll 20"] - intercept_message = True - - async def execute(self) -> Tuple[bool, Optional[str]]: - """执行骰子命令""" - import random - - command = self.matched_groups.get("command") - count = int(self.matched_groups.get("count", "6")) - - # 限制骰子面数 - if count > 100: - await self.send_text("❌ 骰子面数不能超过100") - return False, "骰子面数超限" - - result = random.randint(1, count) - await self.send_text(f"🎲 投掷{count}面骰子,结果: {result}") - - return True, f"投掷了{count}面骰子,结果{result}" -``` - -## 📊 性能优化建议 - -### 1. 正则表达式优化 - -```python -# ✅ 好的做法 - 简单直接 -command_pattern = r"^/ping$" - -# ❌ 避免 - 过于复杂 -command_pattern = r"^/(?:ping|pong|test|check|status|info|help|...)" - -# ✅ 好的做法 - 分离复杂逻辑 -``` - -### 2. 参数验证 - -```python -# ✅ 好的做法 - 早期验证 -async def execute(self) -> Tuple[bool, Optional[str]]: - username = self.matched_groups.get("username") - if not username: - await self.send_text("❌ 请提供用户名") - return False, "缺少参数" - - # 继续处理... -``` - -### 3. 错误处理 - -```python -# ✅ 好的做法 - 完整错误处理 -async def execute(self) -> Tuple[bool, Optional[str]]: - try: - # 主要逻辑 - result = await self._process_command() - return True, "执行成功" - except ValueError as e: - await self.send_text(f"❌ 参数错误: {e}") - return False, f"参数错误: {e}" - except Exception as e: - await self.send_text(f"❌ 执行失败: {e}") - return False, f"执行失败: {e}" -``` - -## 🎯 最佳实践 - -### 1. 命令设计原则 - -```python -# ✅ 好的命令设计 -"/user add 张三" # 动作 + 对象 + 参数 -"/config set key=value" # 动作 + 子动作 + 参数 -"/help command" # 动作 + 可选参数 - -# ❌ 避免的设计 -"/add_user_with_name_张三" # 过于冗长 -"/u a 张三" # 过于简写 -``` - -### 2. 帮助信息 - -```python -class WellDocumentedCommand(BaseCommand): - command_pattern = r"^/example\s+(?P\w+)$" - command_help = "示例命令:处理指定参数并返回结果" - command_examples = [ - "/example test", - "/example debug", - "/example production" - ] -``` - -### 3. 错误处理 - -```python -async def execute(self) -> Tuple[bool, Optional[str]]: - param = self.matched_groups.get("param") - - # 参数验证 - if param not in ["test", "debug", "production"]: - await self.send_text("❌ 无效的参数,支持: test, debug, production") - return False, "无效参数" - - # 执行逻辑 - try: - result = await self._process_param(param) - await self.send_text(f"✅ 处理完成: {result}") - return True, f"处理{param}成功" - except Exception as e: - await self.send_text("❌ 处理失败,请稍后重试") - return False, f"处理失败: {e}" -``` - -### 4. 配置集成 - -```python -async def execute(self) -> Tuple[bool, Optional[str]]: - # 从配置读取设置 - max_items = self.get_config("command.max_items", 10) - timeout = self.get_config("command.timeout", 30) - - # 使用配置进行处理 - ... -``` - -## 📝 Command vs Action 选择指南 - -### 使用Command的场景 - -- ✅ 用户需要明确调用特定功能 -- ✅ 需要精确的参数控制 -- ✅ 管理和配置操作 -- ✅ 查询和信息显示 -- ✅ 系统维护命令 - -### 使用Action的场景 - -- ✅ 增强麦麦的智能行为 -- ✅ 根据上下文自动触发 -- ✅ 情绪和表情表达 -- ✅ 智能建议和帮助 -- ✅ 随机化的互动 - - +具体参数与用法参见`BaseCommand`基类的定义。 \ No newline at end of file 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/dependency-management.md b/docs/plugins/dependency-management.md index 9b9695846..4bb4ed000 100644 --- a/docs/plugins/dependency-management.md +++ b/docs/plugins/dependency-management.md @@ -1,93 +1,6 @@ # 📦 插件依赖管理系统 -> 🎯 **简介**:MaiBot插件系统提供了强大的Python包依赖管理功能,让插件开发更加便捷和可靠。 - -## ✨ 功能概述 - -### 🎯 核心能力 -- **声明式依赖**:插件可以明确声明需要的Python包 -- **智能检查**:自动检查依赖包的安装状态 -- **版本控制**:精确的版本要求管理 -- **可选依赖**:区分必需依赖和可选依赖 -- **自动安装**:可选的自动安装功能 -- **批量管理**:生成统一的requirements文件 -- **安全控制**:防止意外安装和版本冲突 - -### 🔄 工作流程 -1. **声明依赖** → 在插件中声明所需的Python包 -2. **加载检查** → 插件加载时自动检查依赖状态 -3. **状态报告** → 详细报告缺失或版本不匹配的依赖 -4. **智能安装** → 可选择自动安装或手动安装 -5. **运行时处理** → 插件运行时优雅处理依赖缺失 - -## 🚀 快速开始 - -### 步骤1:声明依赖 - -在你的插件类中添加`python_dependencies`字段: - -```python -from src.plugin_system import BasePlugin, PythonDependency, register_plugin - -@register_plugin -class MyPlugin(BasePlugin): - name = "my_plugin" - - # 声明Python包依赖 - python_dependencies = [ - PythonDependency( - package_name="requests", - version=">=2.25.0", - description="HTTP请求库,用于网络通信" - ), - PythonDependency( - package_name="numpy", - version=">=1.20.0", - optional=True, - description="数值计算库(可选功能)" - ), - ] - - def get_plugin_components(self): - # 返回插件组件 - return [] -``` - -### 步骤2:处理依赖 - -在组件代码中优雅处理依赖缺失: - -```python -class MyAction(BaseAction): - async def execute(self, action_input, context=None): - try: - import requests - # 使用requests进行网络请求 - response = requests.get("https://api.example.com") - return {"status": "success", "data": response.json()} - except ImportError: - return { - "status": "error", - "message": "功能不可用:缺少requests库", - "hint": "请运行: pip install requests>=2.25.0" - } -``` - -### 步骤3:检查和管理 - -使用依赖管理API: - -```python -from src.plugin_system import plugin_manager - -# 检查所有插件的依赖状态 -result = plugin_manager.check_all_dependencies() -print(f"检查了 {result['total_plugins_checked']} 个插件") -print(f"缺少必需依赖的插件: {result['plugins_with_missing_required']} 个") - -# 生成requirements文件 -plugin_manager.generate_plugin_requirements("plugin_requirements.txt") -``` +现在的Python依赖包管理依然存在问题,请保留你的`python_dependencies`属性,等待后续重构。 ## 📚 详细教程 @@ -97,11 +10,11 @@ plugin_manager.generate_plugin_requirements("plugin_requirements.txt") ```python PythonDependency( - package_name="requests", # 导入时的包名 - version=">=2.25.0", # 版本要求 - optional=False, # 是否为可选依赖 - description="HTTP请求库", # 依赖描述 - install_name="" # pip安装时的包名(可选) + package_name="PIL", # 导入时的包名 + version=">=11.2.0", # 版本要求 + optional=False, # 是否为可选依赖 + description="图像处理库", # 依赖描述 + install_name="pillow" # pip安装时的包名(可选) ) ``` @@ -110,10 +23,10 @@ PythonDependency( | 参数 | 类型 | 必需 | 说明 | |------|------|------|------| | `package_name` | str | ✅ | Python导入时使用的包名(如`requests`) | -| `version` | str | ❌ | 版本要求,支持pip格式(如`>=1.0.0`, `==2.1.3`) | +| `version` | str | ❌ | 版本要求,使用pip格式(如`>=1.0.0`, `==2.1.3`) | | `optional` | bool | ❌ | 是否为可选依赖,默认`False` | | `description` | str | ❌ | 依赖的用途描述 | -| `install_name` | str | ❌ | pip安装时的包名,默认与`package_name`相同 | +| `install_name` | str | ❌ | pip安装时的包名,默认与`package_name`相同,用于处理安装名称和导入名称不一致的情况 | #### 版本格式示例 @@ -125,201 +38,3 @@ PythonDependency("pillow", "==8.3.2") # 精确版本 PythonDependency("scipy", ">=1.7.0,!=1.8.0") # 排除特定版本 ``` -#### 特殊情况处理 - -**导入名与安装名不同的包:** - -```python -PythonDependency( - package_name="PIL", # import PIL - install_name="Pillow", # pip install Pillow - version=">=8.0.0" -) -``` - -**可选依赖示例:** - -```python -python_dependencies = [ - # 必需依赖 - 核心功能 - PythonDependency( - package_name="requests", - version=">=2.25.0", - description="HTTP库,插件核心功能必需" - ), - - # 可选依赖 - 增强功能 - PythonDependency( - package_name="numpy", - version=">=1.20.0", - optional=True, - description="数值计算库,用于高级数学运算" - ), - PythonDependency( - package_name="matplotlib", - version=">=3.0.0", - optional=True, - description="绘图库,用于数据可视化功能" - ), -] -``` - -### 依赖检查机制 - -系统在以下时机会自动检查依赖: - -1. **插件加载时**:检查插件声明的所有依赖 -2. **手动调用时**:通过API主动检查 -3. **运行时检查**:在组件执行时动态检查 - -#### 检查结果状态 - -| 状态 | 描述 | 处理建议 | -|------|------|----------| -| `no_dependencies` | 插件未声明任何依赖 | 无需处理 | -| `ok` | 所有依赖都已满足 | 正常使用 | -| `missing_optional` | 缺少可选依赖 | 部分功能不可用,考虑安装 | -| `missing_required` | 缺少必需依赖 | 插件功能受限,需要安装 | - -## 🎯 最佳实践 - -### 1. 依赖声明原则 - -#### ✅ 推荐做法 - -```python -python_dependencies = [ - # 明确的版本要求 - PythonDependency( - package_name="requests", - version=">=2.25.0,<3.0.0", # 主版本兼容 - description="HTTP请求库,用于API调用" - ), - - # 合理的可选依赖 - PythonDependency( - package_name="numpy", - version=">=1.20.0", - optional=True, - description="数值计算库,用于数据处理功能" - ), -] -``` - -#### ❌ 避免的做法 - -```python -python_dependencies = [ - # 过于宽泛的版本要求 - PythonDependency("requests"), # 没有版本限制 - - # 过于严格的版本要求 - PythonDependency("numpy", "==1.21.0"), # 精确版本过于严格 - - # 缺少描述 - PythonDependency("matplotlib", ">=3.0.0"), # 没有说明用途 -] -``` - -### 2. 错误处理模式 - -#### 优雅降级模式 - -```python -class SmartAction(BaseAction): - async def execute(self, action_input, context=None): - # 检查可选依赖 - try: - import numpy as np - # 使用numpy的高级功能 - return await self._advanced_processing(action_input, np) - except ImportError: - # 降级到基础功能 - return await self._basic_processing(action_input) - - async def _advanced_processing(self, input_data, np): - """使用numpy的高级处理""" - result = np.array(input_data).mean() - return {"result": result, "method": "advanced"} - - async def _basic_processing(self, input_data): - """基础处理(不依赖外部库)""" - result = sum(input_data) / len(input_data) - return {"result": result, "method": "basic"} -``` - -## 🔧 使用API - -### 检查依赖状态 - -```python -from src.plugin_system import plugin_manager - -# 检查所有插件依赖(仅检查,不安装) -result = plugin_manager.check_all_dependencies(auto_install=False) - -# 检查并自动安装缺失的必需依赖 -result = plugin_manager.check_all_dependencies(auto_install=True) -``` - -### 生成requirements文件 - -```python -# 生成包含所有插件依赖的requirements文件 -plugin_manager.generate_plugin_requirements("plugin_requirements.txt") -``` - -### 获取依赖状态报告 - -```python -# 获取详细的依赖检查报告 -result = plugin_manager.check_all_dependencies() -for plugin_name, status in result['plugin_status'].items(): - print(f"插件 {plugin_name}: {status['status']}") - if status['missing']: - print(f" 缺失必需依赖: {status['missing']}") - if status['optional_missing']: - print(f" 缺失可选依赖: {status['optional_missing']}") -``` - -## 🛡️ 安全考虑 - -### 1. 自动安装控制 -- 🛡️ **默认手动**: 自动安装默认关闭,需要明确启用 -- 🔍 **依赖审查**: 安装前会显示将要安装的包列表 -- ⏱️ **超时控制**: 安装操作有超时限制(5分钟) - -### 2. 权限管理 -- 📁 **环境隔离**: 推荐在虚拟环境中使用 -- 🔒 **版本锁定**: 支持精确的版本控制 -- 📝 **安装日志**: 记录所有安装操作 - -## 📊 故障排除 - -### 常见问题 - -1. **依赖检查失败** - ```python - # 手动检查包是否可导入 - try: - import package_name - print("包可用") - except ImportError: - print("包不可用,需要安装") - ``` - -2. **版本冲突** - ```python - # 检查已安装的包版本 - import package_name - print(f"当前版本: {package_name.__version__}") - ``` - -3. **安装失败** - ```python - # 查看安装日志 - from src.plugin_system import dependency_manager - result = dependency_manager.get_install_summary() - print("安装日志:", result['install_log']) - print("失败详情:", result['failed_installs']) - ``` diff --git a/docs/plugins/image/quick-start/1750332444690.png b/docs/plugins/image/quick-start/1750332444690.png deleted file mode 100644 index aefbbb3e0..000000000 Binary files a/docs/plugins/image/quick-start/1750332444690.png and /dev/null differ diff --git a/docs/plugins/index.md b/docs/plugins/index.md index 2e025fd62..2ca4bb364 100644 --- a/docs/plugins/index.md +++ b/docs/plugins/index.md @@ -4,15 +4,34 @@ ## 新手入门 -- [📖 快速开始指南](quick-start.md) - 5分钟创建你的第一个插件 +- [📖 快速开始指南](quick-start.md) - 快速创建你的第一个插件 ## 组件功能详解 - [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件 - [💻 Command组件详解](command-components.md) - 学习直接响应命令的组件 -- [⚙️ 配置管理指南](configuration-guide.md) - 学会使用自动生成的插件配置文件 +- [⚙️ 配置文件系统指南](configuration-guide.md) - 学会使用自动生成的插件配置文件 - [📄 Manifest系统指南](manifest-guide.md) - 了解插件元数据管理和配置架构 +Command vs Action 选择指南 + +1. 使用Command的场景 + +- ✅ 用户需要明确调用特定功能 +- ✅ 需要精确的参数控制 +- ✅ 管理和配置操作 +- ✅ 查询和信息显示 +- ✅ 系统维护命令 + +2. 使用Action的场景 + +- ✅ 增强麦麦的智能行为 +- ✅ 根据上下文自动触发 +- ✅ 情绪和表情表达 +- ✅ 智能建议和帮助 +- ✅ 随机化的互动 + + ## API浏览 ### 消息发送与处理API @@ -24,19 +43,22 @@ - [LLM API](api/llm-api.md) - 大语言模型交互接口,可以使用内置LLM生成内容 - [✨ 回复生成器API](api/generator-api.md) - 智能回复生成接口,可以使用内置风格化生成器 -### 表情包api +### 表情包API - [😊 表情包API](api/emoji-api.md) - 表情包选择和管理接口 -### 关系系统api +### 关系系统API - [人物信息API](api/person-api.md) - 用户信息,处理麦麦认识的人和关系的接口 ### 数据与配置API - [🗄️ 数据库API](api/database-api.md) - 数据库操作接口 - [⚙️ 配置API](api/config-api.md) - 配置读取和用户信息接口 -### 工具API -- [工具API](api/utils-api.md) - 文件操作、时间处理等工具函数 +### 插件和组件管理API +- [🔌 插件API](api/plugin-manage-api.md) - 插件加载和管理接口 +- [🧩 组件API](api/component-manage-api.md) - 组件注册和管理接口 +### 日志API +- [📜 日志API](api/logging-api.md) - logger实例获取接口 ## 实验性 @@ -53,3 +75,9 @@ 2. 查看相关示例代码 3. 参考其他类似插件 4. 提交文档仓库issue + +## 一个方便的小设计 + +我们在`__init__.py`中定义了一个`__all__`变量,包含了所有需要导出的类和函数。 +这样在其他地方导入时,可以直接使用 `from src.plugin_system import *` 来导入所有插件相关的类和函数。 +或者你可以直接使用 `from src.plugin_system import BasePlugin, register_plugin, ComponentInfo` 之类的方式来导入你需要的部分。 \ No newline at end of file diff --git a/docs/plugins/manifest-guide.md b/docs/plugins/manifest-guide.md index 5c5d7e3fb..d3dd746af 100644 --- a/docs/plugins/manifest-guide.md +++ b/docs/plugins/manifest-guide.md @@ -147,7 +147,7 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin ## 📋 字段说明 ### 基本信息 -- `manifest_version`: manifest格式版本,当前为3 +- `manifest_version`: manifest格式版本,当前为1 - `name`: 插件显示名称(必需) - `version`: 插件版本号(必需) - `description`: 插件功能描述(必需) @@ -165,10 +165,12 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin - `categories`: 分类数组(可选,建议填写) ### 兼容性 -- `host_application`: 主机应用兼容性(可选) +- `host_application`: 主机应用兼容性(可选,建议填写) - `min_version`: 最低兼容版本 - `max_version`: 最高兼容版本 +⚠️ 在不填写的情况下,插件将默认支持所有版本。**(由于我们在不同版本对插件系统进行了大量的重构,这种情况几乎不可能。)** + ### 国际化 - `default_locale`: 默认语言(可选) - `locales_path`: 语言文件目录(可选) @@ -185,24 +187,13 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin 2. **编码格式**:manifest文件必须使用UTF-8编码 3. **JSON格式**:文件必须是有效的JSON格式 4. **必需字段**:`manifest_version`、`name`、`version`、`description`、`author.name`是必需的 -5. **版本兼容**:当前只支持manifest_version = 3 +5. **版本兼容**:当前只支持`manifest_version = 1` ## 🔍 常见问题 -### Q: 为什么要强制要求manifest文件? -A: Manifest文件提供了插件的标准化元数据,使得插件管理、依赖检查、版本兼容性验证等功能成为可能。 - ### Q: 可以不填写可选字段吗? A: 可以。所有标记为"可选"的字段都可以不填写,但建议至少填写`license`和`keywords`。 -### Q: 如何快速为所有插件创建manifest? -A: 可以编写脚本批量处理: -```bash -# 扫描并为每个缺少manifest的插件创建最小化manifest -python scripts/manifest_tool.py scan src/plugins -# 然后手动为每个插件运行create-minimal命令 -``` - ### Q: manifest验证失败怎么办? A: 根据验证器的错误提示修复相应问题。错误会导致插件加载失败,警告不会。 @@ -210,5 +201,5 @@ A: 根据验证器的错误提示修复相应问题。错误会导致插件加 查看内置插件的manifest文件作为参考: - `src/plugins/built_in/core_actions/_manifest.json` -- `src/plugins/built_in/doubao_pic_plugin/_manifest.json` - `src/plugins/built_in/tts_plugin/_manifest.json` +- `src/plugins/hello_world_plugin/_manifest.json` diff --git a/docs/plugins/quick-start.md b/docs/plugins/quick-start.md index 509438308..48eff603d 100644 --- a/docs/plugins/quick-start.md +++ b/docs/plugins/quick-start.md @@ -1,20 +1,20 @@ # 🚀 快速开始指南 -本指南将带你用5分钟时间,从零开始创建一个功能完整的MaiCore插件。 +本指南将带你从零开始创建一个功能完整的MaiCore插件。 ## 📖 概述 -这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。无需阅读其他文档,跟着本指南就能完成! +这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件,展示插件系统的基本概念。 -## 🎯 学习目标 +以下代码都在我们的`plugins/hello_world_plugin/`目录下。 -- 理解插件的基本结构 -- 从最简单的插件开始,循序渐进 -- 学会创建Action组件(智能动作) -- 学会创建Command组件(命令响应) -- 掌握配置Schema定义和配置文件自动生成(可选) +### 一个方便的小设计 -## 📂 准备工作 +在开发中,我们在`__init__.py`中定义了一个`__all__`变量,包含了所有需要导出的类和函数。 +这样在其他地方导入时,可以直接使用 `from src.plugin_system import *` 来导入所有插件相关的类和函数。 +或者你可以直接使用 `from src.plugin_system import BasePlugin, register_plugin, ComponentInfo` 之类的方式来导入你需要的部分。 + +### 📂 准备工作 确保你已经: @@ -26,16 +26,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 +56,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 +92,7 @@ class HelloWorldPlugin(BasePlugin): ![1750326700269](image/quick-start/1750326700269.png) -### 4. 添加第一个功能:问候Action +### 5. 添加第一个功能:问候Action 现在我们要给插件加入一个有用的功能,我们从最好玩的Action做起 @@ -107,40 +119,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 +156,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 +184,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 +206,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 +259,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 +409,20 @@ async def execute(self) -> Tuple[bool, str]: 3. **用户修改**: 用户可以修改生成的配置文件 4. **代码读取**: 使用 `self.get_config()` 读取配置值 -**配置功能解释:** +**绝对不要手动创建 `config.toml` 文件!** -- `self.get_config()` 可以读取配置文件中的值 -- 第一个参数是配置路径(用点分隔),第二个参数是默认值 -- 配置文件会包含详细的注释和说明,用户可以轻松理解和修改 -- **绝不要手动创建配置文件**,让系统自动生成 +更详细的配置系统介绍请参考 [配置指南](./configuration-guide.md)。 -### 9. 创建说明文档(可选) +### 2. 创建说明文档 -创建 `README.md` 文件来说明你的插件: +你可以创建一个 `README.md` 文件,描述插件的功能和使用方法。 -```markdown -# Hello World 插件 +### 3. 发布到插件市场 -## 概述 -我的第一个MaiCore插件,包含问候和时间查询功能。 +如果你想让更多人使用你的插件,可以将它发布到MaiCore的插件市场。 -## 功能 -- **问候功能**: 当用户说"你好"、"hello"、"hi"时自动回复 -- **时间查询**: 发送 `/time` 命令查询当前时间 +这部分请参考 [plugin-repo](https://github.com/Maim-with-u/plugin-repo) 的文档。 -## 使用方法 -### 问候功能 -发送包含以下关键词的消息: -- "你好" -- "hello" -- "hi" +--- -### 时间查询 -发送命令:`/time` - -## 配置文件 -插件会自动生成 `config.toml` 配置文件,用户可以修改: -- 问候消息内容 -- 时间显示格式 -- 插件启用状态 - -注意:配置文件是自动生成的,不要手动创建! -``` - - -``` - -``` +🎉 恭喜你!你已经成功的创建了自己的插件了! diff --git a/docs/plugins/tool-system.md b/docs/plugins/tool-system.md index d9093c89f..eab560734 100644 --- a/docs/plugins/tool-system.md +++ b/docs/plugins/tool-system.md @@ -1,13 +1,12 @@ # 🔧 工具系统详解 -## 📖 什么是工具系统 +## 📖 什么是工具 -工具系统是MaiBot的信息获取能力扩展组件,**专门用于在Focus模式下扩宽麦麦能够获得的信息量**。如果说Action组件功能五花八门,可以拓展麦麦能做的事情,那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。 +工具是MaiBot的信息获取能力扩展组件。如果说Action组件功能五花八门,可以拓展麦麦能做的事情,那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。 -### 🎯 工具系统的特点 +### 🎯 工具的特点 - 🔍 **信息获取增强**:扩展麦麦获取外部信息的能力 -- 🎯 **Focus模式专用**:仅在专注聊天模式下工作,必须开启工具处理器 - 📊 **数据丰富**:帮助麦麦获得更多背景信息和实时数据 - 🔌 **插件式架构**:支持独立开发和注册新工具 - ⚡ **自动发现**:工具会被系统自动识别和注册 @@ -17,19 +16,15 @@ | 特征 | Action | Command | Tool | |-----|-------|---------|------| | **主要用途** | 扩展麦麦行为能力 | 响应用户指令 | 扩展麦麦信息获取 | -| **适用模式** | 所有模式 | 所有模式 | 仅Focus模式 | | **触发方式** | 麦麦智能决策 | 用户主动触发 | LLM根据需要调用 | | **目标** | 让麦麦做更多事情 | 提供具体功能 | 让麦麦知道更多信息 | | **使用场景** | 增强交互体验 | 功能服务 | 信息查询和分析 | -## 🏗️ 工具基本结构 - -### 必要组件 +## 🏗️ Tool组件的基本结构 每个工具必须继承 `BaseTool` 基类并实现以下属性和方法: - ```python -from src.tools.tool_can_use.base_tool import BaseTool, register_tool +from src.plugin_system import BaseTool class MyTool(BaseTool): # 工具名称,必须唯一 @@ -53,8 +48,10 @@ class MyTool(BaseTool): }, "required": ["query"] } + + available_for_llm = True # 是否对LLM可用 - async def execute(self, function_args, message_txt=""): + async def execute(self, function_args: Dict[str, Any]): """执行工具逻辑""" # 实现工具功能 result = f"查询结果: {function_args.get('query')}" @@ -63,9 +60,6 @@ class MyTool(BaseTool): "name": self.name, "content": result } - -# 注册工具 -register_tool(MyTool) ``` ### 属性说明 @@ -80,39 +74,16 @@ register_tool(MyTool) | 方法 | 参数 | 返回值 | 说明 | |-----|------|--------|------| -| `execute` | `function_args`, `message_txt` | `dict` | 执行工具核心逻辑 | +| `execute` | `function_args` | `dict` | 执行工具核心逻辑 | -## 🔄 自动注册机制 - -工具系统采用自动发现和注册机制: - -1. **文件扫描**:系统自动遍历 `tool_can_use` 目录中的所有Python文件 -2. **类识别**:寻找继承自 `BaseTool` 的工具类 -3. **自动注册**:调用 `register_tool()` 的工具会被注册到系统中 -4. **即用即加载**:工具在需要时被实例化和调用 - -### 注册流程 - -```python -# 1. 创建工具类 -class WeatherTool(BaseTool): - name = "weather_query" - description = "查询指定城市的天气信息" - # ... - -# 2. 注册工具(在文件末尾) -register_tool(WeatherTool) - -# 3. 系统自动发现(无需手动操作) -# discover_tools() 函数会自动完成注册 -``` +--- ## 🎨 完整工具示例 -### 天气查询工具 +完成一个天气查询工具 ```python -from src.tools.tool_can_use.base_tool import BaseTool, register_tool +from src.plugin_system import BaseTool import aiohttp import json @@ -192,217 +163,16 @@ class WeatherTool(BaseTool): 💧 湿度: {humidity}% ━━━━━━━━━━━━━━━━━━ """.strip() - -# 注册工具 -register_tool(WeatherTool) ``` -### 知识查询工具 - -```python -from src.tools.tool_can_use.base_tool import BaseTool, register_tool - -class KnowledgeSearchTool(BaseTool): - """知识搜索工具 - 查询百科知识和专业信息""" - - name = "knowledge_search" - description = "搜索百科知识、专业术语解释、历史事件等信息" - - parameters = { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "要搜索的知识关键词或问题" - }, - "category": { - "type": "string", - "description": "知识分类:science(科学)、history(历史)、technology(技术)、general(通用)等", - "enum": ["science", "history", "technology", "general"] - }, - "language": { - "type": "string", - "description": "结果语言:zh(中文)、en(英文)", - "enum": ["zh", "en"] - } - }, - "required": ["query"] - } - - async def execute(self, function_args, message_txt=""): - """执行知识搜索""" - try: - query = function_args.get("query") - category = function_args.get("category", "general") - language = function_args.get("language", "zh") - - # 执行搜索逻辑 - search_results = await self._search_knowledge(query, category, language) - - # 格式化结果 - result = self._format_search_results(query, search_results) - - return { - "name": self.name, - "content": result - } - - except Exception as e: - return { - "name": self.name, - "content": f"知识搜索失败: {str(e)}" - } - - async def _search_knowledge(self, query: str, category: str, language: str) -> list: - """执行知识搜索""" - # 这里实现实际的搜索逻辑 - # 可以对接维基百科API、百度百科API等 - - # 示例返回数据 - return [ - { - "title": f"{query}的定义", - "summary": f"关于{query}的详细解释...", - "source": "Wikipedia" - } - ] - - def _format_search_results(self, query: str, results: list) -> str: - """格式化搜索结果""" - if not results: - return f"未找到关于 '{query}' 的相关信息" - - formatted_text = f"📚 关于 '{query}' 的搜索结果:\n\n" - - for i, result in enumerate(results[:3], 1): # 限制显示前3条 - title = result.get("title", "无标题") - summary = result.get("summary", "无摘要") - source = result.get("source", "未知来源") - - formatted_text += f"{i}. **{title}**\n" - formatted_text += f" {summary}\n" - formatted_text += f" 📖 来源: {source}\n\n" - - return formatted_text.strip() - -# 注册工具 -register_tool(KnowledgeSearchTool) -``` - -## 📊 工具开发步骤 - -### 1. 创建工具文件 - -在 `src/tools/tool_can_use/` 目录下创建新的Python文件: - -```bash -# 例如创建 my_new_tool.py -touch src/tools/tool_can_use/my_new_tool.py -``` - -### 2. 实现工具类 - -```python -from src.tools.tool_can_use.base_tool import BaseTool, register_tool - -class MyNewTool(BaseTool): - name = "my_new_tool" - description = "新工具的功能描述" - - parameters = { - "type": "object", - "properties": { - # 定义参数 - }, - "required": [] - } - - async def execute(self, function_args, message_txt=""): - # 实现工具逻辑 - return { - "name": self.name, - "content": "执行结果" - } - -register_tool(MyNewTool) -``` - -### 3. 测试工具 - -创建测试文件验证工具功能: - -```python -import asyncio -from my_new_tool import MyNewTool - -async def test_tool(): - tool = MyNewTool() - result = await tool.execute({"param": "value"}) - print(result) - -asyncio.run(test_tool()) -``` - -### 4. 系统集成 - -工具创建完成后,系统会自动发现和注册,无需额外配置。 - -## ⚙️ 工具处理器配置 - -### 启用工具处理器 - -工具系统仅在Focus模式下工作,需要确保工具处理器已启用: - -```python -# 在Focus模式配置中 -focus_config = { - "enable_tool_processor": True, # 必须启用 - "tool_timeout": 30, # 工具执行超时时间(秒) - "max_tools_per_message": 3 # 单次消息最大工具调用数 -} -``` - -### 工具使用流程 - -1. **用户发送消息**:在Focus模式下发送需要信息查询的消息 -2. **LLM判断需求**:麦麦分析消息,判断是否需要使用工具获取信息 -3. **选择工具**:根据需求选择合适的工具 -4. **调用工具**:执行工具获取信息 -5. **整合回复**:将工具获取的信息整合到回复中 - -### 使用示例 - -```python -# 用户消息示例 -"今天北京的天气怎么样?" - -# 系统处理流程: -# 1. 麦麦识别这是天气查询需求 -# 2. 调用 weather_query 工具 -# 3. 获取北京天气信息 -# 4. 整合信息生成回复 - -# 最终回复: -"根据最新天气数据,北京今天晴天,温度22°C,湿度45%,适合外出活动。" -``` +--- ## 🚨 注意事项和限制 ### 当前限制 -1. **模式限制**:仅在Focus模式下可用 -2. **独立开发**:需要单独编写,暂未完全融入插件系统 -3. **适用范围**:主要适用于信息获取场景 -4. **配置要求**:必须开启工具处理器 - -### 未来改进 - -工具系统在之后可能会面临以下修改: - -1. **插件系统融合**:更好地集成到插件系统中 -2. **模式扩展**:可能扩展到其他聊天模式 -3. **配置简化**:简化配置和部署流程 -4. **性能优化**:提升工具调用效率 +1. **适用范围**:主要适用于信息获取场景 +2. **配置要求**:必须开启工具处理器 ### 开发建议 diff --git a/plugins/hello_world_plugin/plugin.py b/plugins/hello_world_plugin/plugin.py index 8ede9616a..cab135c09 100644 --- a/plugins/hello_world_plugin/plugin.py +++ b/plugins/hello_world_plugin/plugin.py @@ -1,9 +1,10 @@ -from typing import List, Tuple, Type +from typing import List, Tuple, Type, Any from src.plugin_system import ( BasePlugin, register_plugin, BaseAction, BaseCommand, + BaseTool, ComponentInfo, ActionActivationType, ConfigField, @@ -13,6 +14,45 @@ from src.plugin_system import ( ) +class CompareNumbersTool(BaseTool): + """比较两个数大小的工具""" + + name = "compare_numbers" + description = "使用工具 比较两个数的大小,返回较大的数" + parameters = { + "type": "object", + "properties": { + "num1": {"type": "number", "description": "第一个数字"}, + "num2": {"type": "number", "description": "第二个数字"}, + }, + "required": ["num1", "num2"], + } + + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: + """执行比较两个数的大小 + + Args: + function_args: 工具参数 + + Returns: + dict: 工具执行结果 + """ + num1: int | float = function_args.get("num1") # type: ignore + num2: int | float = function_args.get("num2") # type: ignore + + try: + if num1 > num2: + result = f"{num1} 大于 {num2}" + elif num1 < num2: + result = f"{num1} 小于 {num2}" + else: + result = f"{num1} 等于 {num2}" + + return {"name": self.name, "content": result} + except Exception as e: + return {"name": self.name, "content": f"比较数字失败,炸了: {str(e)}"} + + # ===== Action组件 ===== class HelloAction(BaseAction): """问候Action - 简单的问候动作""" @@ -132,7 +172,9 @@ class HelloWorldPlugin(BasePlugin): "enabled": ConfigField(type=bool, default=False, description="是否启用插件"), }, "greeting": { - "message": ConfigField(type=str, default="嗨!很开心见到你!😊", description="默认问候消息"), + "message": ConfigField( + type=list, default=["嗨!很开心见到你!😊", "Ciallo~(∠・ω< )⌒★"], description="默认问候消息" + ), "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"), }, "time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")}, @@ -142,6 +184,7 @@ class HelloWorldPlugin(BasePlugin): def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: return [ (HelloAction.get_action_info(), HelloAction), + (CompareNumbersTool.get_tool_info(), CompareNumbersTool), # 添加比较数字工具 (ByeAction.get_action_info(), ByeAction), # 添加告别Action (TimeCommand.get_command_info(), TimeCommand), (PrintMessage.get_handler_info(), PrintMessage), diff --git a/scripts/log_viewer_optimized.py b/scripts/log_viewer_optimized.py index 8f19fb6cf..d93f50166 100644 --- a/scripts/log_viewer_optimized.py +++ b/scripts/log_viewer_optimized.py @@ -1425,3 +1425,4 @@ def main(): if __name__ == "__main__": main() + diff --git a/scripts/text_length_analysis.py b/scripts/text_length_analysis.py new file mode 100644 index 000000000..2ca596e2f --- /dev/null +++ b/scripts/text_length_analysis.py @@ -0,0 +1,394 @@ +import time +import sys +import os +import re +from typing import Dict, List, Tuple, Optional +from datetime import datetime +# Add project root to Python path +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, project_root) +from src.common.database.database_model import Messages, ChatStreams #noqa + + +def contains_emoji_or_image_tags(text: str) -> bool: + """Check if text contains [表情包xxxxx] or [图片xxxxx] tags""" + if not text: + return False + + # 检查是否包含 [表情包] 或 [图片] 标记 + emoji_pattern = r'\[表情包[^\]]*\]' + image_pattern = r'\[图片[^\]]*\]' + + return bool(re.search(emoji_pattern, text) or re.search(image_pattern, text)) + + +def clean_reply_text(text: str) -> str: + """Remove reply references like [回复 xxxx...] from text""" + if not text: + return text + + # 匹配 [回复 xxxx...] 格式的内容 + # 使用非贪婪匹配,匹配到第一个 ] 就停止 + cleaned_text = re.sub(r'\[回复[^\]]*\]', '', text) + + # 去除多余的空白字符 + cleaned_text = cleaned_text.strip() + + return cleaned_text + + +def get_chat_name(chat_id: str) -> str: + """Get chat name from chat_id by querying ChatStreams table directly""" + try: + chat_stream = ChatStreams.get_or_none(ChatStreams.stream_id == chat_id) + if chat_stream is None: + return f"未知聊天 ({chat_id})" + + if chat_stream.group_name: + return f"{chat_stream.group_name} ({chat_id})" + elif chat_stream.user_nickname: + return f"{chat_stream.user_nickname}的私聊 ({chat_id})" + else: + return f"未知聊天 ({chat_id})" + except Exception: + return f"查询失败 ({chat_id})" + + +def format_timestamp(timestamp: float) -> str: + """Format timestamp to readable date string""" + try: + return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + return "未知时间" + + +def calculate_text_length_distribution(messages) -> Dict[str, int]: + """Calculate distribution of processed_plain_text length""" + distribution = { + '0': 0, # 空文本 + '1-5': 0, # 极短文本 + '6-10': 0, # 很短文本 + '11-20': 0, # 短文本 + '21-30': 0, # 较短文本 + '31-50': 0, # 中短文本 + '51-70': 0, # 中等文本 + '71-100': 0, # 较长文本 + '101-150': 0, # 长文本 + '151-200': 0, # 很长文本 + '201-300': 0, # 超长文本 + '301-500': 0, # 极长文本 + '501-1000': 0, # 巨长文本 + '1000+': 0 # 超巨长文本 + } + + for msg in messages: + if msg.processed_plain_text is None: + continue + + # 排除包含表情包或图片标记的消息 + if contains_emoji_or_image_tags(msg.processed_plain_text): + continue + + # 清理文本中的回复引用 + cleaned_text = clean_reply_text(msg.processed_plain_text) + length = len(cleaned_text) + + if length == 0: + distribution['0'] += 1 + elif length <= 5: + distribution['1-5'] += 1 + elif length <= 10: + distribution['6-10'] += 1 + elif length <= 20: + distribution['11-20'] += 1 + elif length <= 30: + distribution['21-30'] += 1 + elif length <= 50: + distribution['31-50'] += 1 + elif length <= 70: + distribution['51-70'] += 1 + elif length <= 100: + distribution['71-100'] += 1 + elif length <= 150: + distribution['101-150'] += 1 + elif length <= 200: + distribution['151-200'] += 1 + elif length <= 300: + distribution['201-300'] += 1 + elif length <= 500: + distribution['301-500'] += 1 + elif length <= 1000: + distribution['501-1000'] += 1 + else: + distribution['1000+'] += 1 + + return distribution + + +def get_text_length_stats(messages) -> Dict[str, float]: + """Calculate basic statistics for processed_plain_text length""" + lengths = [] + null_count = 0 + excluded_count = 0 # 被排除的消息数量 + + for msg in messages: + if msg.processed_plain_text is None: + null_count += 1 + elif contains_emoji_or_image_tags(msg.processed_plain_text): + # 排除包含表情包或图片标记的消息 + excluded_count += 1 + else: + # 清理文本中的回复引用 + cleaned_text = clean_reply_text(msg.processed_plain_text) + lengths.append(len(cleaned_text)) + + if not lengths: + return { + 'count': 0, + 'null_count': null_count, + 'excluded_count': excluded_count, + 'min': 0, + 'max': 0, + 'avg': 0, + 'median': 0 + } + + lengths.sort() + count = len(lengths) + + return { + 'count': count, + 'null_count': null_count, + 'excluded_count': excluded_count, + 'min': min(lengths), + 'max': max(lengths), + 'avg': sum(lengths) / count, + 'median': lengths[count // 2] if count % 2 == 1 else (lengths[count // 2 - 1] + lengths[count // 2]) / 2 + } + + +def get_available_chats() -> List[Tuple[str, str, int]]: + """Get all available chats with message counts""" + try: + # 获取所有有消息的chat_id,排除特殊类型消息 + chat_counts = {} + for msg in Messages.select(Messages.chat_id).distinct(): + chat_id = msg.chat_id + count = Messages.select().where( + (Messages.chat_id == chat_id) & + (Messages.is_emoji != 1) & + (Messages.is_picid != 1) & + (Messages.is_command != 1) + ).count() + if count > 0: + chat_counts[chat_id] = count + + # 获取聊天名称 + result = [] + for chat_id, count in chat_counts.items(): + chat_name = get_chat_name(chat_id) + result.append((chat_id, chat_name, count)) + + # 按消息数量排序 + result.sort(key=lambda x: x[2], reverse=True) + return result + except Exception as e: + print(f"获取聊天列表失败: {e}") + return [] + + +def get_time_range_input() -> Tuple[Optional[float], Optional[float]]: + """Get time range input from user""" + print("\n时间范围选择:") + print("1. 最近1天") + print("2. 最近3天") + print("3. 最近7天") + print("4. 最近30天") + print("5. 自定义时间范围") + print("6. 不限制时间") + + choice = input("请选择时间范围 (1-6): ").strip() + + now = time.time() + + if choice == "1": + return now - 24*3600, now + elif choice == "2": + return now - 3*24*3600, now + elif choice == "3": + return now - 7*24*3600, now + elif choice == "4": + return now - 30*24*3600, now + elif choice == "5": + print("请输入开始时间 (格式: YYYY-MM-DD HH:MM:SS):") + start_str = input().strip() + print("请输入结束时间 (格式: YYYY-MM-DD HH:MM:SS):") + end_str = input().strip() + + try: + start_time = datetime.strptime(start_str, "%Y-%m-%d %H:%M:%S").timestamp() + end_time = datetime.strptime(end_str, "%Y-%m-%d %H:%M:%S").timestamp() + return start_time, end_time + except ValueError: + print("时间格式错误,将不限制时间范围") + return None, None + else: + return None, None + + +def get_top_longest_messages(messages, top_n: int = 10) -> List[Tuple[str, int, str, str]]: + """Get top N longest messages""" + message_lengths = [] + + for msg in messages: + if msg.processed_plain_text is not None: + # 排除包含表情包或图片标记的消息 + if contains_emoji_or_image_tags(msg.processed_plain_text): + continue + + # 清理文本中的回复引用 + cleaned_text = clean_reply_text(msg.processed_plain_text) + length = len(cleaned_text) + chat_name = get_chat_name(msg.chat_id) + time_str = format_timestamp(msg.time) + # 截取前100个字符作为预览 + preview = cleaned_text[:100] + "..." if len(cleaned_text) > 100 else cleaned_text + message_lengths.append((chat_name, length, time_str, preview)) + + # 按长度排序,取前N个 + message_lengths.sort(key=lambda x: x[1], reverse=True) + return message_lengths[:top_n] + + +def analyze_text_lengths(chat_id: Optional[str] = None, start_time: Optional[float] = None, end_time: Optional[float] = None) -> None: + """Analyze processed_plain_text lengths with optional filters""" + + # 构建查询条件,排除特殊类型的消息 + query = Messages.select().where( + (Messages.is_emoji != 1) & + (Messages.is_picid != 1) & + (Messages.is_command != 1) + ) + + if chat_id: + query = query.where(Messages.chat_id == chat_id) + + if start_time: + query = query.where(Messages.time >= start_time) + + if end_time: + query = query.where(Messages.time <= end_time) + + messages = list(query) + + if not messages: + print("没有找到符合条件的消息") + return + + # 计算统计信息 + distribution = calculate_text_length_distribution(messages) + stats = get_text_length_stats(messages) + top_longest = get_top_longest_messages(messages, 10) + + # 显示结果 + print("\n=== Processed Plain Text 长度分析结果 ===") + print("(已排除表情、图片ID、命令类型消息,已排除[表情包]和[图片]标记消息,已清理回复引用)") + if chat_id: + print(f"聊天: {get_chat_name(chat_id)}") + else: + print("聊天: 全部聊天") + + if start_time and end_time: + print(f"时间范围: {format_timestamp(start_time)} 到 {format_timestamp(end_time)}") + elif start_time: + print(f"时间范围: {format_timestamp(start_time)} 之后") + elif end_time: + print(f"时间范围: {format_timestamp(end_time)} 之前") + else: + print("时间范围: 不限制") + + print("\n基本统计:") + print(f"总消息数量: {len(messages)}") + print(f"有文本消息数量: {stats['count']}") + print(f"空文本消息数量: {stats['null_count']}") + print(f"被排除的消息数量: {stats['excluded_count']}") + if stats['count'] > 0: + print(f"最短长度: {stats['min']} 字符") + print(f"最长长度: {stats['max']} 字符") + print(f"平均长度: {stats['avg']:.2f} 字符") + print(f"中位数长度: {stats['median']:.2f} 字符") + + print("\n文本长度分布:") + total = stats['count'] + if total > 0: + for range_name, count in distribution.items(): + if count > 0: + percentage = count / total * 100 + print(f"{range_name} 字符: {count} ({percentage:.2f}%)") + + # 显示最长的消息 + if top_longest: + print(f"\n最长的 {len(top_longest)} 条消息:") + for i, (chat_name, length, time_str, preview) in enumerate(top_longest, 1): + print(f"{i}. [{chat_name}] {time_str}") + print(f" 长度: {length} 字符") + print(f" 预览: {preview}") + print() + + +def interactive_menu() -> None: + """Interactive menu for text length analysis""" + + while True: + print("\n" + "="*50) + print("Processed Plain Text 长度分析工具") + print("="*50) + print("1. 分析全部聊天") + print("2. 选择特定聊天分析") + print("q. 退出") + + choice = input("\n请选择分析模式 (1-2, q): ").strip() + + if choice.lower() == 'q': + print("再见!") + break + + chat_id = None + + if choice == "2": + # 显示可用的聊天列表 + chats = get_available_chats() + if not chats: + print("没有找到聊天数据") + continue + + print(f"\n可用的聊天 (共{len(chats)}个):") + for i, (_cid, name, count) in enumerate(chats, 1): + print(f"{i}. {name} ({count}条消息)") + + try: + chat_choice = int(input(f"\n请选择聊天 (1-{len(chats)}): ").strip()) + if 1 <= chat_choice <= len(chats): + chat_id = chats[chat_choice - 1][0] + else: + print("无效选择") + continue + except ValueError: + print("请输入有效数字") + continue + + elif choice != "1": + print("无效选择") + continue + + # 获取时间范围 + start_time, end_time = get_time_range_input() + + # 执行分析 + analyze_text_lengths(chat_id, start_time, end_time) + + input("\n按回车键继续...") + + +if __name__ == "__main__": + interactive_menu() \ No newline at end of file diff --git a/src/chat/chat_loop/heartFC_chat.py b/src/chat/chat_loop/heartFC_chat.py index 41101b2dd..efa8f69b9 100644 --- a/src/chat/chat_loop/heartFC_chat.py +++ b/src/chat/chat_loop/heartFC_chat.py @@ -2,7 +2,7 @@ import asyncio import time import traceback import random -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, Tuple from rich.traceback import install from src.config.config import global_config @@ -18,11 +18,12 @@ from src.chat.chat_loop.hfc_utils import CycleDetail from src.person_info.relationship_builder_manager import relationship_builder_manager from src.person_info.person_info import get_person_info_manager from src.plugin_system.base.component_types import ActionInfo, ChatMode -from src.plugin_system.apis import generator_api, send_api, message_api +from src.plugin_system.apis import generator_api, send_api, message_api, database_api from src.chat.willing.willing_manager import get_willing_manager from src.mais4u.mai_think import mai_thinking_manager -from maim_message.message_base import GroupInfo from src.mais4u.constant_s4u import ENABLE_S4U +from src.plugins.built_in.core_actions.no_reply import NoReplyAction +from src.chat.chat_loop.hfc_utils import send_typing, stop_typing ERROR_LOOP_INFO = { "loop_plan_info": { @@ -88,11 +89,6 @@ class HeartFChatting: self.loop_mode = ChatMode.NORMAL # 初始循环模式为普通模式 - # 新增:消息计数器和疲惫阈值 - self._message_count = 0 # 发送的消息计数 - self._message_threshold = max(10, int(30 * global_config.chat.focus_value)) - self._fatigue_triggered = False # 是否已触发疲惫退出 - self.action_manager = ActionManager() self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager) self.action_modifier = ActionModifier(action_manager=self.action_manager, chat_id=self.stream_id) @@ -112,7 +108,6 @@ class HeartFChatting: self.last_read_time = time.time() - 1 - self.willing_amplifier = 1 self.willing_manager = get_willing_manager() logger.info(f"{self.log_prefix} HeartFChatting 初始化完成") @@ -182,6 +177,9 @@ class HeartFChatting: if self.loop_mode == ChatMode.NORMAL: self.energy_value -= 0.3 self.energy_value = max(self.energy_value, 0.3) + if self.loop_mode == ChatMode.FOCUS: + self.energy_value -= 0.6 + self.energy_value = max(self.energy_value, 0.3) def print_cycle_info(self, cycle_timers): # 记录循环信息和计时器结果 @@ -200,9 +198,9 @@ class HeartFChatting: async def _loopbody(self): if self.loop_mode == ChatMode.FOCUS: if await self._observe(): - self.energy_value -= 1 * global_config.chat.focus_value + self.energy_value -= 1 / global_config.chat.focus_value else: - self.energy_value -= 3 * global_config.chat.focus_value + self.energy_value -= 3 / global_config.chat.focus_value if self.energy_value <= 1: self.energy_value = 1 self.loop_mode = ChatMode.NORMAL @@ -218,15 +216,17 @@ class HeartFChatting: limit_mode="earliest", filter_bot=True, ) + if global_config.chat.focus_value != 0: + if len(new_messages_data) > 3 / pow(global_config.chat.focus_value, 0.5): + self.loop_mode = ChatMode.FOCUS + self.energy_value = ( + 10 + (len(new_messages_data) / (3 / pow(global_config.chat.focus_value, 0.5))) * 10 + ) + return True - if len(new_messages_data) > 3 * global_config.chat.focus_value: - self.loop_mode = ChatMode.FOCUS - self.energy_value = 10 + (len(new_messages_data) / (3 * global_config.chat.focus_value)) * 10 - return True - - if self.energy_value >= 30 * global_config.chat.focus_value: - self.loop_mode = ChatMode.FOCUS - return True + if self.energy_value >= 30: + self.loop_mode = ChatMode.FOCUS + return True if new_messages_data: earliest_messages_data = new_messages_data[0] @@ -235,10 +235,10 @@ class HeartFChatting: if_think = await self.normal_response(earliest_messages_data) if if_think: factor = max(global_config.chat.focus_value, 0.1) - self.energy_value *= 1.1 / factor + self.energy_value *= 1.1 * factor logger.info(f"{self.log_prefix} 进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}") else: - self.energy_value += 0.1 / global_config.chat.focus_value + self.energy_value += 0.1 * global_config.chat.focus_value logger.debug(f"{self.log_prefix} 没有进行思考,能量值线性增加,当前能量值:{self.energy_value:.1f}") logger.debug(f"{self.log_prefix} 当前能量值:{self.energy_value:.1f}") @@ -257,44 +257,69 @@ class HeartFChatting: person_name = await person_info_manager.get_value(person_id, "person_name") return f"{person_name}:{message_data.get('processed_plain_text')}" - async def send_typing(self): - group_info = GroupInfo(platform="amaidesu_default", group_id="114514", group_name="内心") + async def _send_and_store_reply( + self, + response_set, + reply_to_str, + loop_start_time, + action_message, + cycle_timers: Dict[str, float], + thinking_id, + plan_result, + ) -> Tuple[Dict[str, Any], str, Dict[str, float]]: + with Timer("回复发送", cycle_timers): + reply_text = await self._send_response(response_set, reply_to_str, loop_start_time, action_message) - chat = await get_chat_manager().get_or_create_stream( - platform="amaidesu_default", - user_info=None, - group_info=group_info, + # 存储reply action信息 + person_info_manager = get_person_info_manager() + person_id = person_info_manager.get_person_id( + action_message.get("chat_info_platform", ""), + action_message.get("user_id", ""), + ) + person_name = await person_info_manager.get_value(person_id, "person_name") + action_prompt_display = f"你对{person_name}进行了回复:{reply_text}" + + await database_api.store_action_info( + chat_stream=self.chat_stream, + action_build_into_prompt=False, + action_prompt_display=action_prompt_display, + action_done=True, + thinking_id=thinking_id, + action_data={"reply_text": reply_text, "reply_to": reply_to_str}, + action_name="reply", ) - await send_api.custom_to_stream( - message_type="state", content="typing", stream_id=chat.stream_id, storage_message=False - ) + # 构建循环信息 + loop_info: Dict[str, Any] = { + "loop_plan_info": { + "action_result": plan_result.get("action_result", {}), + }, + "loop_action_info": { + "action_taken": True, + "reply_text": reply_text, + "command": "", + "taken_time": time.time(), + }, + } - async def stop_typing(self): - group_info = GroupInfo(platform="amaidesu_default", group_id="114514", group_name="内心") - - chat = await get_chat_manager().get_or_create_stream( - platform="amaidesu_default", - user_info=None, - group_info=group_info, - ) - - await send_api.custom_to_stream( - message_type="state", content="stop_typing", stream_id=chat.stream_id, storage_message=False - ) + return loop_info, reply_text, cycle_timers async def _observe(self, message_data: Optional[Dict[str, Any]] = None): # sourcery skip: hoist-statement-from-if, merge-comparisons, reintroduce-else if not message_data: message_data = {} action_type = "no_action" + reply_text = "" # 初始化reply_text变量,避免UnboundLocalError + gen_task = None # 初始化gen_task变量,避免UnboundLocalError + reply_to_str = "" # 初始化reply_to_str变量 + # 创建新的循环信息 cycle_timers, thinking_id = self.start_cycle() logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考[模式:{self.loop_mode}]") - + if ENABLE_S4U: - await self.send_typing() + await send_typing() async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()): loop_start_time = time.time() @@ -310,95 +335,254 @@ class HeartFChatting: except Exception as e: logger.error(f"{self.log_prefix} 动作修改失败: {e}") - # 如果normal,开始一个回复生成进程,先准备好回复(其实是和planer同时进行的) + # 检查是否在normal模式下没有可用动作(除了reply相关动作) + skip_planner = False if self.loop_mode == ChatMode.NORMAL: - reply_to_str = await self.build_reply_to_str(message_data) - gen_task = asyncio.create_task(self._generate_response(message_data, available_actions, reply_to_str)) + # 过滤掉reply相关的动作,检查是否还有其他动作 + non_reply_actions = { + k: v for k, v in available_actions.items() if k not in ["reply", "no_reply", "no_action"] + } - with Timer("规划器", cycle_timers): - plan_result, target_message = await self.action_planner.plan(mode=self.loop_mode) + if not non_reply_actions: + skip_planner = True + logger.info(f"{self.log_prefix} Normal模式下没有可用动作,直接回复") - action_result: dict = plan_result.get("action_result", {}) # type: ignore - action_type, action_data, reasoning, is_parallel = ( - action_result.get("action_type", "error"), - action_result.get("action_data", {}), - action_result.get("reasoning", "未提供理由"), - action_result.get("is_parallel", True), - ) + # 直接设置为reply动作 + action_type = "reply" + reasoning = "" + action_data = {"loop_start_time": loop_start_time} + is_parallel = False - action_data["loop_start_time"] = loop_start_time + # 构建plan_result用于后续处理 + plan_result = { + "action_result": { + "action_type": action_type, + "action_data": action_data, + "reasoning": reasoning, + "timestamp": time.time(), + "is_parallel": is_parallel, + }, + "action_prompt": "", + } + target_message = message_data - if self.loop_mode == ChatMode.NORMAL: - if action_type == "no_action": - logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复") - elif is_parallel: - logger.info( - f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作" + # 如果normal模式且不跳过规划器,开始一个回复生成进程,先准备好回复(其实是和planer同时进行的) + if not skip_planner: + reply_to_str = await self.build_reply_to_str(message_data) + gen_task = asyncio.create_task( + self._generate_response( + message_data=message_data, + available_actions=available_actions, + reply_to=reply_to_str, + request_type="chat.replyer.normal", + ) ) - else: - logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定执行{action_type}动作") - if action_type == "no_action": + if not skip_planner: + with Timer("规划器", cycle_timers): + plan_result, target_message = await self.action_planner.plan(mode=self.loop_mode) + + action_result: Dict[str, Any] = plan_result.get("action_result", {}) # type: ignore + action_type, action_data, reasoning, is_parallel = ( + action_result.get("action_type", "error"), + action_result.get("action_data", {}), + action_result.get("reasoning", "未提供理由"), + action_result.get("is_parallel", True), + ) + + action_data["loop_start_time"] = loop_start_time + + if action_type == "reply": + logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复") + elif is_parallel: + logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作") + else: + # 只有在gen_task存在时才进行相关操作 + if gen_task: + if not gen_task.done(): + gen_task.cancel() + logger.debug(f"{self.log_prefix} 已取消预生成的回复任务") + logger.info( + f"{self.log_prefix}{global_config.bot.nickname} 原本想要回复,但选择执行{action_type},不发表回复" + ) + elif generation_result := gen_task.result(): + content = " ".join([item[1] for item in generation_result if item[0] == "text"]) + logger.debug(f"{self.log_prefix} 预生成的回复任务已完成") + logger.info( + f"{self.log_prefix}{global_config.bot.nickname} 原本想要回复:{content},但选择执行{action_type},不发表回复" + ) + else: + logger.warning(f"{self.log_prefix} 预生成的回复任务未生成有效内容") + + action_message: Dict[str, Any] = message_data or target_message # type: ignore + if action_type == "reply": # 等待回复生成完毕 - gather_timeout = global_config.chat.thinking_timeout - try: - response_set = await asyncio.wait_for(gen_task, timeout=gather_timeout) - except asyncio.TimeoutError: - response_set = None + if self.loop_mode == ChatMode.NORMAL: + # 只有在gen_task存在时才等待 + if not gen_task: + reply_to_str = await self.build_reply_to_str(message_data) + gen_task = asyncio.create_task( + self._generate_response( + message_data=message_data, + available_actions=available_actions, + reply_to=reply_to_str, + request_type="chat.replyer.normal", + ) + ) - if response_set: - content = " ".join([item[1] for item in response_set if item[0] == "text"]) + gather_timeout = global_config.chat.thinking_timeout + try: + response_set = await asyncio.wait_for(gen_task, timeout=gather_timeout) + except asyncio.TimeoutError: + logger.warning(f"{self.log_prefix} 回复生成超时>{global_config.chat.thinking_timeout}s,已跳过") + response_set = None - # 模型炸了,没有回复内容生成 - if not response_set: - logger.warning(f"[{self.log_prefix}] 模型未生成回复内容") - return False - elif action_type not in ["no_action"] and not is_parallel: - logger.info( - f"[{self.log_prefix}] {global_config.bot.nickname} 原本想要回复:{content},但选择执行{action_type},不发表回复" - ) - return False + # 模型炸了或超时,没有回复内容生成 + if not response_set: + logger.warning(f"{self.log_prefix}模型未生成回复内容") + return False + else: + logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复 (focus模式)") - logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定的回复内容: {content}") + # 构建reply_to字符串 + reply_to_str = await self.build_reply_to_str(action_message) - # 发送回复 (不再需要传入 chat) - reply_text = await self._send_response(response_set, reply_to_str, loop_start_time,message_data) - - - - - - if ENABLE_S4U: - await self.stop_typing() - await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text) + # 生成回复 + with Timer("回复生成", cycle_timers): + response_set = await self._generate_response( + message_data=action_message, + available_actions=available_actions, + reply_to=reply_to_str, + request_type="chat.replyer.focus", + ) + + if not response_set: + logger.warning(f"{self.log_prefix}模型未生成回复内容") + return False + + loop_info, reply_text, cycle_timers = await self._send_and_store_reply( + response_set, reply_to_str, loop_start_time, action_message, cycle_timers, thinking_id, plan_result + ) return True else: - action_message: Dict[str, Any] = message_data or target_message # type: ignore + # 并行执行:同时进行回复发送和动作执行 + # 先置空防止未定义错误 + background_reply_task = None + background_action_task = None + # 如果是并行执行且在normal模式下,需要等待预生成的回复任务完成并发送回复 + if self.loop_mode == ChatMode.NORMAL and is_parallel and gen_task: - # 动作执行计时 - with Timer("动作执行", cycle_timers): - success, reply_text, command = await self._handle_action( - action_type, reasoning, action_data, cycle_timers, thinking_id, action_message + async def handle_reply_task() -> Tuple[Optional[Dict[str, Any]], str, Dict[str, float]]: + # 等待预生成的回复任务完成 + gather_timeout = global_config.chat.thinking_timeout + try: + response_set = await asyncio.wait_for(gen_task, timeout=gather_timeout) + + except asyncio.TimeoutError: + logger.warning( + f"{self.log_prefix} 并行执行:回复生成超时>{global_config.chat.thinking_timeout}s,已跳过" + ) + return None, "", {} + except asyncio.CancelledError: + logger.debug(f"{self.log_prefix} 并行执行:回复生成任务已被取消") + return None, "", {} + + if not response_set: + logger.warning(f"{self.log_prefix} 模型超时或生成回复内容为空") + return None, "", {} + + reply_to_str = await self.build_reply_to_str(action_message) + loop_info, reply_text, cycle_timers_reply = await self._send_and_store_reply( + response_set, + reply_to_str, + loop_start_time, + action_message, + cycle_timers, + thinking_id, + plan_result, + ) + return loop_info, reply_text, cycle_timers_reply + + # 执行回复任务并赋值到变量 + background_reply_task = asyncio.create_task(handle_reply_task()) + + # 动作执行任务 + async def handle_action_task(): + with Timer("动作执行", cycle_timers): + success, reply_text, command = await self._handle_action( + action_type, reasoning, action_data, cycle_timers, thinking_id, action_message + ) + return success, reply_text, command + + # 执行动作任务并赋值到变量 + background_action_task = asyncio.create_task(handle_action_task()) + + reply_loop_info = None + reply_text_from_reply = "" + action_success = False + action_reply_text = "" + action_command = "" + + # 并行执行所有任务 + if background_reply_task: + results = await asyncio.gather( + background_reply_task, background_action_task, return_exceptions=True ) + # 处理回复任务结果 + reply_result = results[0] + if isinstance(reply_result, BaseException): + logger.error(f"{self.log_prefix} 回复任务执行异常: {reply_result}") + elif reply_result and reply_result[0] is not None: + reply_loop_info, reply_text_from_reply, _ = reply_result - loop_info = { - "loop_plan_info": { - "action_result": plan_result.get("action_result", {}), - }, - "loop_action_info": { - "action_taken": success, - "reply_text": reply_text, - "command": command, - "taken_time": time.time(), - }, - } + # 处理动作任务结果 + action_task_result = results[1] + if isinstance(action_task_result, BaseException): + logger.error(f"{self.log_prefix} 动作任务执行异常: {action_task_result}") + else: + action_success, action_reply_text, action_command = action_task_result + else: + results = await asyncio.gather(background_action_task, return_exceptions=True) + # 只有动作任务 + action_task_result = results[0] + if isinstance(action_task_result, BaseException): + logger.error(f"{self.log_prefix} 动作任务执行异常: {action_task_result}") + else: + action_success, action_reply_text, action_command = action_task_result - if loop_info["loop_action_info"]["command"] == "stop_focus_chat": - logger.info(f"{self.log_prefix} 麦麦决定停止专注聊天") - return False - # 停止该聊天模式的循环 + # 构建最终的循环信息 + if reply_loop_info: + # 如果有回复信息,使用回复的loop_info作为基础 + loop_info = reply_loop_info + # 更新动作执行信息 + loop_info["loop_action_info"].update( + { + "action_taken": action_success, + "command": action_command, + "taken_time": time.time(), + } + ) + reply_text = reply_text_from_reply + else: + # 没有回复信息,构建纯动作的loop_info + loop_info = { + "loop_plan_info": { + "action_result": plan_result.get("action_result", {}), + }, + "loop_action_info": { + "action_taken": action_success, + "reply_text": action_reply_text, + "command": action_command, + "taken_time": time.time(), + }, + } + reply_text = action_reply_text + + if ENABLE_S4U: + await stop_typing() + await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text) self.end_cycle(loop_info, cycle_timers) self.print_cycle_info(cycle_timers) @@ -406,8 +590,16 @@ class HeartFChatting: if self.loop_mode == ChatMode.NORMAL: await self.willing_manager.after_generate_reply_handle(message_data.get("message_id", "")) + # 管理no_reply计数器:当执行了非no_reply动作时,重置计数器 if action_type != "no_reply" and action_type != "no_action": + # 导入NoReplyAction并重置计数器 + NoReplyAction.reset_consecutive_count() + logger.info(f"{self.log_prefix} 执行了{action_type}动作,重置no_reply计数器") return True + elif action_type == "no_action": + # 当执行回复动作时,也重置no_reply计数器s + NoReplyAction.reset_consecutive_count() + logger.info(f"{self.log_prefix} 执行了回复动作,重置no_reply计数器") return True @@ -435,7 +627,7 @@ class HeartFChatting: action: str, reasoning: str, action_data: dict, - cycle_timers: dict, + cycle_timers: Dict[str, float], thinking_id: str, action_message: dict, ) -> tuple[bool, str, str]: @@ -501,7 +693,7 @@ class HeartFChatting: 在"兴趣"模式下,判断是否回复并生成内容。 """ - interested_rate = (message_data.get("interest_value") or 0.0) * self.willing_amplifier + interested_rate = (message_data.get("interest_value") or 0.0) * global_config.chat.willing_amplifier self.willing_manager.setup(message_data, self.chat_stream) @@ -515,8 +707,8 @@ class HeartFChatting: reply_probability += additional_config["maimcore_reply_probability_gain"] reply_probability = min(max(reply_probability, 0), 1) # 确保概率在 0-1 之间 - talk_frequency = global_config.chat.get_current_talk_frequency(self.stream_id) - reply_probability = talk_frequency * reply_probability + talk_frequency = global_config.chat.get_current_talk_frequency(self.stream_id) + reply_probability = talk_frequency * reply_probability # 处理表情包 if message_data.get("is_emoji") or message_data.get("is_picid"): @@ -544,7 +736,11 @@ class HeartFChatting: return False async def _generate_response( - self, message_data: dict, available_actions: Optional[Dict[str, ActionInfo]], reply_to: str + self, + message_data: dict, + available_actions: Optional[Dict[str, ActionInfo]], + reply_to: str, + request_type: str = "chat.replyer.normal", ) -> Optional[list]: """生成普通回复""" try: @@ -552,8 +748,8 @@ class HeartFChatting: chat_stream=self.chat_stream, reply_to=reply_to, available_actions=available_actions, - enable_tool=global_config.tool.enable_in_normal_chat, - request_type="chat.replyer.normal", + enable_tool=global_config.tool.enable_tool, + request_type=request_type, ) if not success or not reply_set: @@ -563,10 +759,10 @@ class HeartFChatting: return reply_set except Exception as e: - logger.error(f"[{self.log_prefix}] 回复生成出现错误:{str(e)} {traceback.format_exc()}") + logger.error(f"{self.log_prefix}回复生成出现错误:{str(e)} {traceback.format_exc()}") return None - async def _send_response(self, reply_set, reply_to, thinking_start_time, message_data): + async def _send_response(self, reply_set, reply_to, thinking_start_time, message_data) -> str: current_time = time.time() new_message_count = message_api.count_new_messages( chat_id=self.chat_stream.stream_id, start_time=thinking_start_time, end_time=current_time @@ -578,13 +774,9 @@ class HeartFChatting: need_reply = new_message_count >= random.randint(2, 4) if need_reply: - logger.info( - f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复" - ) + logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复") else: - logger.debug( - f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,不使用引用回复" - ) + logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,不使用引用回复") reply_text = "" first_replied = False diff --git a/src/chat/chat_loop/hfc_utils.py b/src/chat/chat_loop/hfc_utils.py index a24656665..973c4f948 100644 --- a/src/chat/chat_loop/hfc_utils.py +++ b/src/chat/chat_loop/hfc_utils.py @@ -1,10 +1,13 @@ import time - from typing import Optional, Dict, Any from src.config.config import global_config -from src.common.message_repository import count_messages from src.common.logger import get_logger +from src.chat.message_receive.chat_stream import get_chat_manager +from src.plugin_system.apis import send_api +from maim_message.message_base import GroupInfo + +from src.common.message_repository import count_messages logger = get_logger(__name__) @@ -106,3 +109,30 @@ def get_recent_message_stats(minutes: float = 30, chat_id: Optional[str] = None) bot_reply_count = count_messages(bot_filter) return {"bot_reply_count": bot_reply_count, "total_message_count": total_message_count} + + +async def send_typing(): + group_info = GroupInfo(platform="amaidesu_default", group_id="114514", group_name="内心") + + chat = await get_chat_manager().get_or_create_stream( + platform="amaidesu_default", + user_info=None, + group_info=group_info, + ) + + await send_api.custom_to_stream( + message_type="state", content="typing", stream_id=chat.stream_id, storage_message=False + ) + +async def stop_typing(): + group_info = GroupInfo(platform="amaidesu_default", group_id="114514", group_name="内心") + + chat = await get_chat_manager().get_or_create_stream( + platform="amaidesu_default", + user_info=None, + group_info=group_info, + ) + + await send_api.custom_to_stream( + message_type="state", content="stop_typing", stream_id=chat.stream_id, storage_message=False + ) \ No newline at end of file diff --git a/src/chat/emoji_system/emoji_manager.py b/src/chat/emoji_system/emoji_manager.py index b3c2493d3..918b83969 100644 --- a/src/chat/emoji_system/emoji_manager.py +++ b/src/chat/emoji_system/emoji_manager.py @@ -525,9 +525,9 @@ class EmojiManager: 如果文件已被删除,则执行对象的删除方法并从列表中移除 """ try: - if not self.emoji_objects: - logger.warning("[检查] emoji_objects为空,跳过完整性检查") - return + # if not self.emoji_objects: + # logger.warning("[检查] emoji_objects为空,跳过完整性检查") + # return total_count = len(self.emoji_objects) self.emoji_num = total_count @@ -707,6 +707,38 @@ class EmojiManager: return emoji return None # 如果循环结束还没找到,则返回 None + async def get_emoji_description_by_hash(self, emoji_hash: str) -> Optional[str]: + """根据哈希值获取已注册表情包的描述 + + Args: + emoji_hash: 表情包的哈希值 + + Returns: + Optional[str]: 表情包描述,如果未找到则返回None + """ + try: + # 先从内存中查找 + emoji = await self.get_emoji_from_manager(emoji_hash) + if emoji and emoji.description: + logger.info(f"[缓存命中] 从内存获取表情包描述: {emoji.description[:50]}...") + return emoji.description + + # 如果内存中没有,从数据库查找 + self._ensure_db() + try: + emoji_record = Emoji.get_or_none(Emoji.emoji_hash == emoji_hash) + if emoji_record and emoji_record.description: + logger.info(f"[缓存命中] 从数据库获取表情包描述: {emoji_record.description[:50]}...") + return emoji_record.description + except Exception as e: + logger.error(f"从数据库查询表情包描述时出错: {e}") + + return None + + except Exception as e: + logger.error(f"获取表情包描述失败 (Hash: {emoji_hash}): {str(e)}") + return None + async def delete_emoji(self, emoji_hash: str) -> bool: """根据哈希值删除表情包 diff --git a/src/chat/express/expression_learner.py b/src/chat/express/expression_learner.py index ac41b12a3..1870c470a 100644 --- a/src/chat/express/expression_learner.py +++ b/src/chat/express/expression_learner.py @@ -51,7 +51,7 @@ def init_prompt() -> None: 当"想说明某个具体的事实观点,但懒得明说,或者不便明说,或表达一种默契",使用"懂的都懂" 当"当涉及游戏相关时,表示意外的夸赞,略带戏谑意味"时,使用"这么强!" -注意不要总结你自己(SELF)的发言 +请注意:不要总结你自己(SELF)的发言 现在请你概括 """ Prompt(learn_style_prompt, "learn_style_prompt") @@ -330,48 +330,8 @@ class ExpressionLearner: """ current_time = time.time() - # 全局衰减所有已存储的表达方式 - for type in ["style", "grammar"]: - base_dir = os.path.join("data", "expression", f"learnt_{type}") - if not os.path.exists(base_dir): - logger.debug(f"目录不存在,跳过衰减: {base_dir}") - continue - - try: - chat_ids = os.listdir(base_dir) - logger.debug(f"在 {base_dir} 中找到 {len(chat_ids)} 个聊天ID目录进行衰减") - except Exception as e: - logger.error(f"读取目录失败 {base_dir}: {e}") - continue - - for chat_id in chat_ids: - file_path = os.path.join(base_dir, chat_id, "expressions.json") - if not os.path.exists(file_path): - continue - - try: - with open(file_path, "r", encoding="utf-8") as f: - expressions = json.load(f) - - if not isinstance(expressions, list): - logger.warning(f"表达方式文件格式错误,跳过衰减: {file_path}") - continue - - # 应用全局衰减 - decayed_expressions = self.apply_decay_to_expressions(expressions, current_time) - - # 保存衰减后的结果 - with open(file_path, "w", encoding="utf-8") as f: - json.dump(decayed_expressions, f, ensure_ascii=False, indent=2) - - logger.debug(f"已对 {file_path} 应用衰减,剩余 {len(decayed_expressions)} 个表达方式") - except json.JSONDecodeError as e: - logger.error(f"JSON解析失败,跳过衰减 {file_path}: {e}") - except PermissionError as e: - logger.error(f"权限不足,无法更新 {file_path}: {e}") - except Exception as e: - logger.error(f"全局衰减{type}表达方式失败 {file_path}: {e}") - continue + # 全局衰减所有已存储的表达方式(直接操作数据库) + self._apply_global_decay_to_database(current_time) learnt_style: Optional[List[Tuple[str, str, str]]] = [] learnt_grammar: Optional[List[Tuple[str, str, str]]] = [] @@ -388,6 +348,42 @@ class ExpressionLearner: return learnt_style, learnt_grammar + def _apply_global_decay_to_database(self, current_time: float) -> None: + """ + 对数据库中的所有表达方式应用全局衰减 + """ + try: + # 获取所有表达方式 + all_expressions = Expression.select() + + updated_count = 0 + deleted_count = 0 + + for expr in all_expressions: + # 计算时间差 + last_active = expr.last_active_time + time_diff_days = (current_time - last_active) / (24 * 3600) # 转换为天 + + # 计算衰减值 + decay_value = self.calculate_decay_factor(time_diff_days) + new_count = max(0.01, expr.count - decay_value) + + if new_count <= 0.01: + # 如果count太小,删除这个表达方式 + expr.delete_instance() + deleted_count += 1 + else: + # 更新count + expr.count = new_count + expr.save() + updated_count += 1 + + if updated_count > 0 or deleted_count > 0: + logger.info(f"全局衰减完成:更新了 {updated_count} 个表达方式,删除了 {deleted_count} 个表达方式") + + except Exception as e: + logger.error(f"数据库全局衰减失败: {e}") + def calculate_decay_factor(self, time_diff_days: float) -> float: """ 计算衰减值 @@ -410,30 +406,6 @@ class ExpressionLearner: return min(0.01, decay) - def apply_decay_to_expressions( - self, expressions: List[Dict[str, Any]], current_time: float - ) -> List[Dict[str, Any]]: - """ - 对表达式列表应用衰减 - 返回衰减后的表达式列表,移除count小于0的项 - """ - result = [] - for expr in expressions: - # 确保last_active_time存在,如果不存在则使用current_time - if "last_active_time" not in expr: - expr["last_active_time"] = current_time - - last_active = expr["last_active_time"] - time_diff_days = (current_time - last_active) / (24 * 3600) # 转换为天 - - decay_value = self.calculate_decay_factor(time_diff_days) - expr["count"] = max(0.01, expr.get("count", 1) - decay_value) - - if expr["count"] > 0: - result.append(expr) - - return result - async def learn_and_store(self, type: str, num: int = 10) -> List[Tuple[str, str, str]]: # sourcery skip: use-join """ diff --git a/src/chat/express/expression_selector.py b/src/chat/express/expression_selector.py index d83d3a472..910b43c24 100644 --- a/src/chat/express/expression_selector.py +++ b/src/chat/express/expression_selector.py @@ -2,7 +2,7 @@ import json import time import random -from typing import List, Dict, Tuple, Optional +from typing import List, Dict, Tuple, Optional, Any from json_repair import repair_json from src.llm_models.utils_model import LLMRequest @@ -117,36 +117,42 @@ class ExpressionSelector: def get_random_expressions( self, chat_id: str, total_num: int, style_percentage: float, grammar_percentage: float - ) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]: + ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: # 支持多chat_id合并抽选 related_chat_ids = self.get_related_chat_ids(chat_id) - style_exprs = [] - grammar_exprs = [] - for cid in related_chat_ids: - style_query = Expression.select().where((Expression.chat_id == cid) & (Expression.type == "style")) - grammar_query = Expression.select().where((Expression.chat_id == cid) & (Expression.type == "grammar")) - style_exprs.extend([ - { - "situation": expr.situation, - "style": expr.style, - "count": expr.count, - "last_active_time": expr.last_active_time, - "source_id": cid, - "type": "style", - "create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, - } for expr in style_query - ]) - grammar_exprs.extend([ - { - "situation": expr.situation, - "style": expr.style, - "count": expr.count, - "last_active_time": expr.last_active_time, - "source_id": cid, - "type": "grammar", - "create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, - } for expr in grammar_query - ]) + + # 优化:一次性查询所有相关chat_id的表达方式 + style_query = Expression.select().where( + (Expression.chat_id.in_(related_chat_ids)) & (Expression.type == "style") + ) + grammar_query = Expression.select().where( + (Expression.chat_id.in_(related_chat_ids)) & (Expression.type == "grammar") + ) + + style_exprs = [ + { + "situation": expr.situation, + "style": expr.style, + "count": expr.count, + "last_active_time": expr.last_active_time, + "source_id": expr.chat_id, + "type": "style", + "create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, + } for expr in style_query + ] + + grammar_exprs = [ + { + "situation": expr.situation, + "style": expr.style, + "count": expr.count, + "last_active_time": expr.last_active_time, + "source_id": expr.chat_id, + "type": "grammar", + "create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, + } for expr in grammar_query + ] + style_num = int(total_num * style_percentage) grammar_num = int(total_num * grammar_percentage) # 按权重抽样(使用count作为权重) @@ -162,7 +168,7 @@ class ExpressionSelector: selected_grammar = [] return selected_style, selected_grammar - def update_expressions_count_batch(self, expressions_to_update: List[Dict[str, str]], increment: float = 0.1): + def update_expressions_count_batch(self, expressions_to_update: List[Dict[str, Any]], increment: float = 0.1): """对一批表达方式更新count值,按chat_id+type分组后一次性写入数据库""" if not expressions_to_update: return @@ -203,7 +209,7 @@ class ExpressionSelector: max_num: int = 10, min_num: int = 5, target_message: Optional[str] = None, - ) -> List[Dict[str, str]]: + ) -> List[Dict[str, Any]]: # sourcery skip: inline-variable, list-comprehension """使用LLM选择适合的表达方式""" @@ -273,6 +279,7 @@ class ExpressionSelector: if not isinstance(result, dict) or "selected_situations" not in result: logger.error("LLM返回格式错误") + logger.info(f"LLM返回结果: \n{content}") return [] selected_indices = result["selected_situations"] diff --git a/src/chat/heart_flow/heartflow_message_processor.py b/src/chat/heart_flow/heartflow_message_processor.py index 3aa174bb5..406d0e6d0 100644 --- a/src/chat/heart_flow/heartflow_message_processor.py +++ b/src/chat/heart_flow/heartflow_message_processor.py @@ -12,6 +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_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 @@ -56,16 +57,41 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]: with Timer("记忆激活"): interested_rate = await hippocampus_manager.get_activate_from_text( message.processed_plain_text, + max_depth= 5, fast_retrieval=False, ) logger.debug(f"记忆激活率: {interested_rate:.2f}") text_len = len(message.processed_plain_text) - # 根据文本长度调整兴趣度,长度越大兴趣度越高,但增长率递减,最低0.01,最高0.05 - # 采用对数函数实现递减增长 - - base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) - base_interest = min(max(base_interest, 0.01), 0.05) + # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算 + # 基于实际分布:0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%) + + if text_len == 0: + base_interest = 0.01 # 空消息最低兴趣度 + elif text_len <= 5: + # 1-5字符:线性增长 0.01 -> 0.03 + base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4 + elif text_len <= 10: + # 6-10字符:线性增长 0.03 -> 0.06 + base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5 + elif text_len <= 20: + # 11-20字符:线性增长 0.06 -> 0.12 + base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10 + elif text_len <= 30: + # 21-30字符:线性增长 0.12 -> 0.18 + base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10 + elif text_len <= 50: + # 31-50字符:线性增长 0.18 -> 0.22 + base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20 + elif text_len <= 100: + # 51-100字符:线性增长 0.22 -> 0.26 + base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50 + else: + # 100+字符:对数增长 0.26 -> 0.3,增长率递减 + base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901 + + # 确保在范围内 + base_interest = min(max(base_interest, 0.01), 0.3) interested_rate += base_interest @@ -123,8 +149,15 @@ class HeartFCMessageReceiver: # 如果消息中包含图片标识,则将 [picid:...] 替换为 [图片] picid_pattern = r"\[picid:([^\]]+)\]" processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text) + + # 应用用户引用格式替换,将回复和@格式转换为可读格式 + processed_plain_text = replace_user_references_sync( + processed_plain_text, + message.message_info.platform, # type: ignore + replace_bot_name=True + ) - logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}") # type: ignore + logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}]") # type: ignore logger.debug(f"[{mes_name}][当前时段回复频率: {current_talk_frequency}]") diff --git a/src/chat/memory_system/Hippocampus.py b/src/chat/memory_system/Hippocampus.py index ad0384160..26660e5c3 100644 --- a/src/chat/memory_system/Hippocampus.py +++ b/src/chat/memory_system/Hippocampus.py @@ -224,10 +224,16 @@ class Hippocampus: return hash((source, target)) @staticmethod - def find_topic_llm(text, topic_num): + 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}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," + f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num_str}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" f"如果确定找不出主题或者没有明显主题,返回。" ) @@ -300,6 +306,60 @@ 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。 + 如果为True,使用jieba分词提取关键词,速度更快但可能不够准确。 + 如果为False,使用LLM提取关键词,速度较慢但更准确。 + """ + if not text: + return [] + + # 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算 + text_length = len(text) + topic_num: int | list[int] = 0 + if text_length <= 5: + words = jieba.cut(text) + keywords = [word for word in words if len(word) > 1] + keywords = list(set(keywords))[:3] # 限制最多3个关键词 + if keywords: + logger.info(f"提取关键词: {keywords}") + return keywords + elif text_length <= 10: + topic_num = [1, 3] # 6-10字符: 1个关键词 (27.18%的文本) + elif text_length <= 20: + topic_num = [2, 4] # 11-20字符: 2个关键词 (22.76%的文本) + elif text_length <= 30: + topic_num = [3, 5] # 21-30字符: 3个关键词 (10.33%的文本) + elif text_length <= 50: + 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) + ) + + # 提取关键词 + keywords = re.findall(r"<([^>]+)>", topics_response) + if not keywords: + keywords = [] + else: + keywords = [ + keyword.strip() + for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") + if keyword.strip() + ] + + if keywords: + logger.info(f"提取关键词: {keywords}") + + return keywords + + async def get_memory_from_text( self, text: str, @@ -325,39 +385,7 @@ class Hippocampus: - memory_items: list, 该主题下的记忆项列表 - similarity: float, 与文本的相似度 """ - if not text: - return [] - - if fast_retrieval: - # 使用jieba分词提取关键词 - words = jieba.cut(text) - # 过滤掉停用词和单字词 - keywords = [word for word in words if len(word) > 1] - # 去重 - keywords = list(set(keywords)) - # 限制关键词数量 - logger.debug(f"提取关键词: {keywords}") - - else: - # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 - # logger.info(f"提取关键词数量: {topic_num}") - topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( - self.find_topic_llm(text, topic_num) - ) - - # 提取关键词 - keywords = re.findall(r"<([^>]+)>", topics_response) - if not keywords: - keywords = [] - else: - keywords = [ - keyword.strip() - for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if keyword.strip() - ] - - # logger.info(f"提取的关键词: {', '.join(keywords)}") + keywords = await self.get_keywords_from_text(text) # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] @@ -679,38 +707,7 @@ class Hippocampus: Returns: float: 激活节点数与总节点数的比值 """ - if not text: - return 0 - - if fast_retrieval: - # 使用jieba分词提取关键词 - words = jieba.cut(text) - # 过滤掉停用词和单字词 - keywords = [word for word in words if len(word) > 1] - # 去重 - keywords = list(set(keywords)) - # 限制关键词数量 - keywords = keywords[:5] - else: - # 使用LLM提取关键词 - topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量 - # logger.info(f"提取关键词数量: {topic_num}") - topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async( - self.find_topic_llm(text, topic_num) - ) - - # 提取关键词 - keywords = re.findall(r"<([^>]+)>", topics_response) - if not keywords: - keywords = [] - else: - keywords = [ - keyword.strip() - for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",") - if keyword.strip() - ] - - # logger.info(f"提取的关键词: {', '.join(keywords)}") + keywords = await self.get_keywords_from_text(text) # 过滤掉不存在于记忆图中的关键词 valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] @@ -727,7 +724,7 @@ class Hippocampus: for keyword in valid_keywords: logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") # 初始化激活值 - activation_values = {keyword: 1.0} + activation_values = {keyword: 1.5} # 记录已访问的节点 visited_nodes = {keyword} # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) @@ -1315,6 +1312,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/chat/message_receive/message.py b/src/chat/message_receive/message.py index 7a18dcf07..56ccd33d0 100644 --- a/src/chat/message_receive/message.py +++ b/src/chat/message_receive/message.py @@ -444,7 +444,7 @@ class MessageSending(MessageProcessBase): is_emoji: bool = False, thinking_start_time: float = 0, apply_set_reply_logic: bool = False, - reply_to: str = None, # type: ignore + reply_to: Optional[str] = None, ): # 调用父类初始化 super().__init__( diff --git a/src/chat/planner_actions/action_manager.py b/src/chat/planner_actions/action_manager.py index 37f939b92..21d47c75d 100644 --- a/src/chat/planner_actions/action_manager.py +++ b/src/chat/planner_actions/action_manager.py @@ -3,7 +3,7 @@ from src.plugin_system.base.base_action import BaseAction from src.chat.message_receive.chat_stream import ChatStream from src.common.logger import get_logger from src.plugin_system.core.component_registry import component_registry -from src.plugin_system.base.component_types import ComponentType, ActionActivationType, ChatMode, ActionInfo +from src.plugin_system.base.component_types import ComponentType, ActionInfo logger = get_logger("action_manager") @@ -15,11 +15,6 @@ class ActionManager: 现在统一使用新插件系统,简化了原有的新旧兼容逻辑。 """ - # 类常量 - DEFAULT_RANDOM_PROBABILITY = 0.3 - DEFAULT_MODE = ChatMode.ALL - DEFAULT_ACTIVATION_TYPE = ActionActivationType.ALWAYS - def __init__(self): """初始化动作管理器""" diff --git a/src/chat/planner_actions/action_modifier.py b/src/chat/planner_actions/action_modifier.py index dce706783..da11c54f6 100644 --- a/src/chat/planner_actions/action_modifier.py +++ b/src/chat/planner_actions/action_modifier.py @@ -174,7 +174,7 @@ class ActionModifier: continue # 总是激活,无需处理 elif activation_type == ActionActivationType.RANDOM: - probability = action_info.random_activation_probability or ActionManager.DEFAULT_RANDOM_PROBABILITY + probability = action_info.random_activation_probability if random.random() >= probability: reason = f"RANDOM类型未触发(概率{probability})" deactivated_actions.append((action_name, reason)) diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index a679c4953..0b26a97d0 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -33,10 +33,11 @@ def init_prompt(): {time_block} {identity_block} 你现在需要根据聊天内容,选择的合适的action来参与聊天。 -{chat_context_description},以下是具体的聊天内容: +{chat_context_description},以下是具体的聊天内容 {chat_content_block} + {moderation_prompt} 现在请你根据{by_what}选择合适的action和触发action的消息: @@ -45,7 +46,7 @@ def init_prompt(): {no_action_block} {action_options_text} -你必须从上面列出的可用action中选择一个,并说明触发action的消息id和原因。 +你必须从上面列出的可用action中选择一个,并说明触发action的消息id(不是消息原文)和选择该action的原因。 请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: """, @@ -128,20 +129,6 @@ class ActionPlanner: else: logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到") - # 如果没有可用动作或只有no_reply动作,直接返回no_reply - # 因为现在reply是永远激活,所以不需要空跳判定 - # if not current_available_actions: - # action = "no_reply" if mode == ChatMode.FOCUS else "no_action" - # reasoning = "没有可用的动作" - # logger.info(f"{self.log_prefix}{reasoning}") - # return { - # "action_result": { - # "action_type": action, - # "action_data": action_data, - # "reasoning": reasoning, - # }, - # }, None - # --- 构建提示词 (调用修改后的 PromptBuilder 方法) --- prompt, message_id_list = await self.build_planner_prompt( is_group_chat=is_group_chat, # <-- Pass HFC state @@ -224,7 +211,7 @@ class ActionPlanner: reasoning = f"Planner 内部处理错误: {outer_e}" is_parallel = False - if action in current_available_actions: + if mode == ChatMode.NORMAL and action in current_available_actions: is_parallel = current_available_actions[action].parallel_action action_result = { @@ -268,7 +255,7 @@ class ActionPlanner: actions_before_now = get_actions_by_timestamp_with_chat( chat_id=self.chat_id, - timestamp_start=time.time()-3600, + timestamp_start=time.time() - 3600, timestamp_end=time.time(), limit=5, ) @@ -276,7 +263,7 @@ class ActionPlanner: actions_before_now_block = build_readable_actions( actions=actions_before_now, ) - + actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}" self.last_obs_time_mark = time.time() @@ -288,7 +275,6 @@ class ActionPlanner: if global_config.chat.at_bot_inevitable_reply: mentioned_bonus = "\n- 有人提到你,或者at你" - by_what = "聊天内容" target_prompt = '\n "target_message_id":"触发action的消息id"' no_action_block = f"""重要说明: @@ -311,7 +297,7 @@ class ActionPlanner: by_what = "聊天内容和用户的最新消息" target_prompt = "" no_action_block = """重要说明: -- 'no_action' 表示只进行普通聊天回复,不执行任何额外动作 +- 'reply' 表示只进行普通聊天回复,不执行任何额外动作 - 其他action表示在普通回复的基础上,执行相应的额外动作""" chat_context_description = "你现在正在一个群聊中" diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index bb2aa34e3..dd691e484 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 +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 @@ -25,42 +29,16 @@ from src.chat.memory_system.instant_memory import InstantMemory from src.mood.mood_manager import mood_manager from src.person_info.relationship_fetcher import relationship_fetcher_manager from src.person_info.person_info import get_person_info_manager -from src.tools.tool_executor import ToolExecutor 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") Prompt("在群里聊天", "chat_target_group2") Prompt("和{sender_name}聊天", "chat_target_private2") - Prompt("\n你有以下这些**知识**:\n{prompt_info}\n请你**记住上面的知识**,之后可能会用到。\n", "knowledge_prompt") - - Prompt( - """ -{expression_habits_block} -{tool_info_block} -{knowledge_prompt} -{memory_block} -{relation_info_block} -{extra_info_block} - -{chat_target} -{time_block} -{chat_info} -{reply_target_block} -{identity} - -{action_descriptions} -你正在{chat_target_2},你现在的心情是:{mood_state} -现在请你读读之前的聊天记录,并给出回复 -{config_expression_style}。注意不要复读你说过的话 -{keywords_reaction_prompt} -{moderation_prompt} -不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容""", - "default_generator_prompt", - ) Prompt( """ @@ -109,7 +87,8 @@ def init_prompt(): {core_dialogue_prompt} {reply_target_block} -对方最新发送的内容:{message_txt} + + 你现在的心情是:{mood_state} {config_expression_style} 注意不要复读你说过的话 @@ -159,6 +138,8 @@ class DefaultReplyer: self.heart_fc_sender = HeartFCSender() self.memory_activator = MemoryActivator() self.instant_memory = InstantMemory(chat_id=self.chat_stream.stream_id) + + from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor,不然会循环依赖 self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id, enable_cache=True, cache_ttl=3) def _select_weighted_model_config(self) -> Dict[str, Any]: @@ -171,67 +152,49 @@ class DefaultReplyer: async def generate_reply_with_context( self, - reply_data: Optional[Dict[str, Any]] = None, reply_to: str = "", extra_info: str = "", available_actions: Optional[Dict[str, ActionInfo]] = None, enable_tool: bool = True, - enable_timeout: bool = False, ) -> Tuple[bool, Optional[str], Optional[str]]: """ - 回复器 (Replier): 核心逻辑,负责生成回复文本。 - (已整合原 HeartFCGenerator 的功能) + 回复器 (Replier): 负责生成回复文本的核心逻辑。 + + Args: + reply_to: 回复对象,格式为 "发送者:消息内容" + extra_info: 额外信息,用于补充上下文 + available_actions: 可用的动作信息字典 + enable_tool: 是否启用工具调用 + + Returns: + Tuple[bool, Optional[str], Optional[str]]: (是否成功, 生成的回复内容, 使用的prompt) """ prompt = None if available_actions is None: available_actions = {} try: - if not reply_data: - reply_data = { - "reply_to": reply_to, - "extra_info": extra_info, - } - for key, value in reply_data.items(): - if not value: - logger.debug(f"回复数据跳过{key},生成回复时将忽略。") - # 3. 构建 Prompt with Timer("构建Prompt", {}): # 内部计时器,可选保留 prompt = await self.build_prompt_reply_context( - reply_data=reply_data, # 传递action_data + reply_to=reply_to, + extra_info=extra_info, available_actions=available_actions, - enable_timeout=enable_timeout, enable_tool=enable_tool, ) + if not prompt: + logger.warning("构建prompt失败,跳过回复生成") + return False, None, None + # 4. 调用 LLM 生成回复 content = None - reasoning_content = None - model_name = "unknown_model" + # TODO: 复活这里 + # reasoning_content = None + # model_name = "unknown_model" try: - with Timer("LLM生成", {}): # 内部计时器,可选保留 - # 加权随机选择一个模型配置 - selected_model_config = self._select_weighted_model_config() - # 兼容新旧格式的模型名称获取 - model_display_name = selected_model_config.get('model_name', selected_model_config.get('name', 'N/A')) - logger.info( - f"使用模型生成回复: {model_display_name} (选中概率: {selected_model_config.get('weight', 1.0)})" - ) - - express_model = LLMRequest( - model=selected_model_config, - request_type=self.request_type, - ) - - if global_config.debug.show_prompt: - logger.info(f"\n{prompt}\n") - else: - logger.debug(f"\n{prompt}\n") - - content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt) - - logger.debug(f"replyer生成内容: {content}") + content = await self.llm_generate_content(prompt) + logger.debug(f"replyer生成内容: {content}") except Exception as llm_e: # 精简报错信息 @@ -247,73 +210,62 @@ class DefaultReplyer: async def rewrite_reply_with_context( self, - reply_data: Dict[str, Any], raw_reply: str = "", reason: str = "", reply_to: str = "", - relation_info: str = "", - ) -> Tuple[bool, Optional[str]]: + return_prompt: bool = False, + ) -> Tuple[bool, Optional[str], Optional[str]]: """ - 表达器 (Expressor): 核心逻辑,负责生成回复文本。 + 表达器 (Expressor): 负责重写和优化回复文本。 + + Args: + raw_reply: 原始回复内容 + reason: 回复原因 + reply_to: 回复对象,格式为 "发送者:消息内容" + relation_info: 关系信息 + + Returns: + Tuple[bool, Optional[str]]: (是否成功, 重写后的回复内容) """ try: - if not reply_data: - reply_data = { - "reply_to": reply_to, - "relation_info": relation_info, - } - with Timer("构建Prompt", {}): # 内部计时器,可选保留 prompt = await self.build_prompt_rewrite_context( - reply_data=reply_data, + raw_reply=raw_reply, + reason=reason, + reply_to=reply_to, ) content = None - reasoning_content = None - model_name = "unknown_model" + # TODO: 复活这里 + # reasoning_content = None + # model_name = "unknown_model" if not prompt: logger.error("Prompt 构建失败,无法生成回复。") - return False, None + return False, None, None try: - with Timer("LLM生成", {}): # 内部计时器,可选保留 - # 加权随机选择一个模型配置 - selected_model_config = self._select_weighted_model_config() - # 兼容新旧格式的模型名称获取 - model_display_name = selected_model_config.get('model_name', selected_model_config.get('name', 'N/A')) - logger.info( - f"使用模型重写回复: {model_display_name} (选中概率: {selected_model_config.get('weight', 1.0)})" - ) - - express_model = LLMRequest( - model=selected_model_config, - request_type=self.request_type, - ) - - content, (reasoning_content, model_name) = await express_model.generate_response_async(prompt) - - logger.info(f"想要表达:{raw_reply}||理由:{reason}||生成回复: {content}\n") + content = await self.llm_generate_content(prompt) + logger.info(f"想要表达:{raw_reply}||理由:{reason}||生成回复: {content}\n") except Exception as llm_e: # 精简报错信息 logger.error(f"LLM 生成失败: {llm_e}") - return False, None # LLM 调用失败则无法生成回复 + return False, None, prompt if return_prompt else None # LLM 调用失败则无法生成回复 - return True, content + return True, content, prompt if return_prompt else None except Exception as e: logger.error(f"回复生成意外失败: {e}") traceback.print_exc() - return False, None + return False, None, prompt if return_prompt else None - async def build_relation_info(self, reply_data=None): + async def build_relation_info(self, reply_to: str = ""): if not global_config.relationship.enable_relationship: return "" relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_stream.stream_id) - if not reply_data: + if not reply_to: return "" - reply_to = reply_data.get("reply_to", "") sender, text = self._parse_reply_target(reply_to) if not sender or not text: return "" @@ -327,7 +279,16 @@ class DefaultReplyer: return await relationship_fetcher.build_relation_info(person_id, points_num=5) - async def build_expression_habits(self, chat_history, target): + async def build_expression_habits(self, chat_history: str, target: str) -> str: + """构建表达习惯块 + + Args: + chat_history: 聊天历史记录 + target: 目标消息内容 + + Returns: + str: 表达习惯信息字符串 + """ if not global_config.expression.enable_expression: return "" @@ -360,54 +321,65 @@ 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 + return f"{expression_habits_title}\n{expression_habits_block}" - async def build_memory_block(self, chat_history, target): + async def build_memory_block(self, chat_history: str, target: str) -> str: + """构建记忆块 + + Args: + chat_history: 聊天历史记录 + target: 目标消息内容 + + Returns: + str: 记忆信息字符串 + """ if not global_config.memory.enable_memory: 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): + async def build_tool_info(self, chat_history: str, reply_to: str = "", enable_tool: bool = True) -> str: """构建工具信息块 Args: - reply_data: 回复数据,包含要回复的消息内容 - chat_history: 聊天历史 + chat_history: 聊天历史记录 + reply_to: 回复对象,格式为 "发送者:消息内容" + enable_tool: 是否启用工具调用 Returns: str: 工具信息字符串 @@ -416,10 +388,9 @@ class DefaultReplyer: if not enable_tool: return "" - if not reply_data: + if not reply_to: return "" - reply_to = reply_data.get("reply_to", "") sender, text = self._parse_reply_target(reply_to) if not text: @@ -442,7 +413,7 @@ class DefaultReplyer: tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。" logger.info(f"获取到 {len(tool_results)} 个工具结果") - + return tool_info_str else: logger.debug("未获取到任何工具结果") @@ -452,7 +423,15 @@ class DefaultReplyer: logger.error(f"工具信息获取失败: {e}") return "" - def _parse_reply_target(self, target_message: str) -> tuple: + def _parse_reply_target(self, target_message: str) -> Tuple[str, str]: + """解析回复目标消息 + + Args: + target_message: 目标消息,格式为 "发送者:消息内容" 或 "发送者:消息内容" + + Returns: + Tuple[str, str]: (发送者名称, 消息内容) + """ sender = "" target = "" # 添加None检查,防止NoneType错误 @@ -466,14 +445,22 @@ class DefaultReplyer: target = parts[1].strip() return sender, target - async def build_keywords_reaction_prompt(self, target): + async def build_keywords_reaction_prompt(self, target: Optional[str]) -> str: + """构建关键词反应提示 + + Args: + target: 目标消息内容 + + Returns: + str: 关键词反应提示字符串 + """ # 关键词检测与反应 keywords_reaction_prompt = "" try: # 添加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): @@ -500,15 +487,25 @@ class DefaultReplyer: return keywords_reaction_prompt - async def _time_and_run_task(self, coroutine, name: str): - """一个简单的帮助函数,用于计时和运行异步任务,返回任务名、结果和耗时""" + async def _time_and_run_task(self, coroutine, name: str) -> Tuple[str, Any, float]: + """计时并运行异步任务的辅助函数 + + Args: + coroutine: 要执行的协程 + name: 任务名称 + + Returns: + Tuple[str, Any, float]: (任务名称, 任务结果, 执行耗时) + """ start_time = time.time() result = await coroutine end_time = time.time() duration = end_time - start_time return name, result, duration - def build_s4u_chat_history_prompts(self, message_list_before_now: list, target_user_id: str) -> tuple[str, str]: + def build_s4u_chat_history_prompts( + self, message_list_before_now: List[Dict[str, Any]], target_user_id: str + ) -> Tuple[str, str]: """ 构建 s4u 风格的分离对话 prompt @@ -517,7 +514,7 @@ class DefaultReplyer: target_user_id: 目标用户ID(当前对话对象) Returns: - tuple: (核心对话prompt, 背景对话prompt) + Tuple[str, str]: (核心对话prompt, 背景对话prompt) """ core_dialogue_list = [] background_dialogue_list = [] @@ -536,7 +533,7 @@ class DefaultReplyer: # 其他用户的对话 background_dialogue_list.append(msg_dict) except Exception as e: - logger.error(f"![1753364551656](image/default_generator/1753364551656.png)记录: {msg_dict}, 错误: {e}") + logger.error(f"处理消息记录时出错: {msg_dict}, 错误: {e}") # 构建背景对话 prompt background_dialogue_prompt = "" @@ -581,8 +578,25 @@ class DefaultReplyer: sender: str, target: str, chat_info: str, - ): - """构建 mai_think 上下文信息""" + ) -> Any: + """构建 mai_think 上下文信息 + + Args: + chat_id: 聊天ID + memory_block: 记忆块内容 + relation_info: 关系信息 + time_block: 时间块内容 + chat_target_1: 聊天目标1 + chat_target_2: 聊天目标2 + mood_prompt: 情绪提示 + identity_block: 身份块内容 + sender: 发送者名称 + target: 目标消息内容 + chat_info: 聊天信息 + + Returns: + Any: mai_think 实例 + """ mai_think = mai_thinking_manager.get_mai_think(chat_id) mai_think.memory_block = memory_block mai_think.relation_info_block = relation_info @@ -598,21 +612,20 @@ class DefaultReplyer: async def build_prompt_reply_context( self, - reply_data: Dict[str, Any], + reply_to: str, + extra_info: str = "", available_actions: Optional[Dict[str, ActionInfo]] = None, - enable_timeout: bool = False, enable_tool: bool = True, ) -> str: # sourcery skip: merge-else-if-into-elif, remove-redundant-if """ 构建回复器上下文 Args: - reply_data: 回复数据 - replay_data 包含以下字段: - structured_info: 结构化信息,一般是工具调用获得的信息 - reply_to: 回复对象 - extra_info/extra_info_block: 额外信息 + reply_to: 回复对象,格式为 "发送者:消息内容" + extra_info: 额外信息,用于补充上下文 available_actions: 可用动作 + enable_timeout: 是否启用超时处理 + enable_tool: 是否启用工具调用 Returns: str: 构建好的上下文 @@ -623,9 +636,7 @@ class DefaultReplyer: chat_id = chat_stream.stream_id person_info_manager = get_person_info_manager() 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 @@ -633,6 +644,15 @@ class DefaultReplyer: mood_prompt = "" sender, target = self._parse_reply_target(reply_to) + person_info_manager = get_person_info_manager() + person_id = person_info_manager.get_person_id_by_person_name(sender) + user_id = person_info_manager.get_value_sync(person_id, "user_id") + platform = chat_stream.platform + if user_id == global_config.bot.qq_account and platform == global_config.bot.platform: + logger.warning("选取了自身作为回复对象,跳过构建prompt") + return "" + + target = replace_user_references_sync(target, chat_stream.platform, replace_bot_name=True) # 构建action描述 (如果启用planner) action_descriptions = "" @@ -649,21 +669,6 @@ class DefaultReplyer: limit=global_config.chat.max_context_size * 2, ) - message_list_before_now = get_raw_msg_before_timestamp_with_chat( - chat_id=chat_id, - timestamp=time.time(), - limit=global_config.chat.max_context_size, - ) - chat_talking_prompt = build_readable_messages( - message_list_before_now, - replace_bot_name=True, - merge_messages=False, - timestamp_mode="normal_no_YMD", - read_mark=0.0, - truncate=True, - show_actions=True, - ) - message_list_before_short = get_raw_msg_before_timestamp_with_chat( chat_id=chat_id, timestamp=time.time(), @@ -683,25 +688,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_to), "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.build_tool_info(chat_talking_prompt_short, reply_to, enable_tool=enable_tool), "tool_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": "获取知识", } # 处理结果 @@ -723,8 +724,8 @@ class DefaultReplyer: keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) - if extra_info_block: - extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策" + if extra_info: + extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策" else: extra_info_block = "" @@ -779,116 +780,74 @@ class DefaultReplyer: # 根据sender通过person_info_manager反向查找person_id,再获取user_id person_id = person_info_manager.get_person_id_by_person_name(sender) - # 根据配置选择使用哪种 prompt 构建模式 - if global_config.chat.use_s4u_prompt_mode and person_id: - # 使用 s4u 对话构建模式:分离当前对话对象和其他对话 - try: - user_id_value = await person_info_manager.get_value(person_id, "user_id") - if user_id_value: - target_user_id = str(user_id_value) - except Exception as e: - logger.warning(f"无法从person_id {person_id} 获取user_id: {e}") - target_user_id = "" + # 使用 s4u 对话构建模式:分离当前对话对象和其他对话 + try: + user_id_value = await person_info_manager.get_value(person_id, "user_id") + if user_id_value: + target_user_id = str(user_id_value) + except Exception as e: + logger.warning(f"无法从person_id {person_id} 获取user_id: {e}") + target_user_id = "" - # 构建分离的对话 prompt - 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, - relation_info=relation_info, - time_block=time_block, - chat_target_1=chat_target_1, - chat_target_2=chat_target_2, - mood_prompt=mood_prompt, - identity_block=identity_block, - sender=sender, - target=target, - chat_info=f""" + # 构建分离的对话 prompt + 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, + relation_info=relation_info, + time_block=time_block, + chat_target_1=chat_target_1, + chat_target_2=chat_target_2, + mood_prompt=mood_prompt, + identity_block=identity_block, + sender=sender, + target=target, + chat_info=f""" {background_dialogue_prompt} -------------------------------- {time_block} 这是你和{sender}的对话,你们正在交流中: -{core_dialogue_prompt}""" - ) - +{core_dialogue_prompt}""", + ) - # 使用 s4u 风格的模板 - template_name = "s4u_style_prompt" + # 使用 s4u 风格的模板 + template_name = "s4u_style_prompt" - return await global_prompt_manager.format_prompt( - template_name, - expression_habits_block=expression_habits_block, - tool_info_block=tool_info, - knowledge_prompt=prompt_info, - memory_block=memory_block, - relation_info_block=relation_info, - extra_info_block=extra_info_block, - identity=identity_block, - action_descriptions=action_descriptions, - sender_name=sender, - mood_state=mood_prompt, - background_dialogue_prompt=background_dialogue_prompt, - time_block=time_block, - core_dialogue_prompt=core_dialogue_prompt, - reply_target_block=reply_target_block, - message_txt=target, - config_expression_style=global_config.expression.expression_style, - keywords_reaction_prompt=keywords_reaction_prompt, - moderation_prompt=moderation_prompt_block, - ) - else: - self.build_mai_think_context( - chat_id=chat_id, - memory_block=memory_block, - relation_info=relation_info, - time_block=time_block, - chat_target_1=chat_target_1, - chat_target_2=chat_target_2, - mood_prompt=mood_prompt, - identity_block=identity_block, - sender=sender, - target=target, - chat_info=chat_talking_prompt - ) - - # 使用原有的模式 - return await global_prompt_manager.format_prompt( - template_name, - expression_habits_block=expression_habits_block, - chat_target=chat_target_1, - chat_info=chat_talking_prompt, - memory_block=memory_block, - tool_info_block=tool_info, - knowledge_prompt=prompt_info, - extra_info_block=extra_info_block, - relation_info_block=relation_info, - time_block=time_block, - reply_target_block=reply_target_block, - moderation_prompt=moderation_prompt_block, - keywords_reaction_prompt=keywords_reaction_prompt, - identity=identity_block, - target_message=target, - sender_name=sender, - config_expression_style=global_config.expression.expression_style, - action_descriptions=action_descriptions, - chat_target_2=chat_target_2, - mood_state=mood_prompt, - ) + return await global_prompt_manager.format_prompt( + template_name, + expression_habits_block=expression_habits_block, + tool_info_block=tool_info, + knowledge_prompt=prompt_info, + memory_block=memory_block, + relation_info_block=relation_info, + extra_info_block=extra_info_block, + identity=identity_block, + action_descriptions=action_descriptions, + sender_name=sender, + mood_state=mood_prompt, + background_dialogue_prompt=background_dialogue_prompt, + time_block=time_block, + core_dialogue_prompt=core_dialogue_prompt, + reply_target_block=reply_target_block, + message_txt=target, + config_expression_style=global_config.expression.expression_style, + keywords_reaction_prompt=keywords_reaction_prompt, + moderation_prompt=moderation_prompt_block, + ) async def build_prompt_rewrite_context( self, - reply_data: Dict[str, Any], + raw_reply: str, + reason: str, + reply_to: str, ) -> str: chat_stream = self.chat_stream chat_id = chat_stream.stream_id is_group_chat = bool(chat_stream.group_info) - reply_to = reply_data.get("reply_to", "none") - raw_reply = reply_data.get("raw_reply", "") - reason = reply_data.get("reason", "") sender, target = self._parse_reply_target(reply_to) # 添加情绪状态获取 @@ -915,7 +874,7 @@ class DefaultReplyer: # 并行执行2个构建任务 expression_habits_block, relation_info = await asyncio.gather( self.build_expression_habits(chat_talking_prompt_half, target), - self.build_relation_info(reply_data), + self.build_relation_info(reply_to), ) keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) @@ -1018,6 +977,31 @@ class DefaultReplyer: display_message=display_message, ) + async def llm_generate_content(self, prompt: str) -> str: + with Timer("LLM生成", {}): # 内部计时器,可选保留 + # 加权随机选择一个模型配置 + selected_model_config = self._select_weighted_model_config() + model_display_name = selected_model_config.get('model_name') or selected_model_config.get('name', 'N/A') + logger.info( + f"使用模型生成回复: {model_display_name} (选中概率: {selected_model_config.get('weight', 1.0)})" + ) + + express_model = LLMRequest( + model=selected_model_config, + request_type=self.request_type, + ) + + if global_config.debug.show_prompt: + logger.info(f"\n{prompt}\n") + else: + logger.debug(f"\n{prompt}\n") + + # TODO: 这里的_应该做出替换 + content, _ = await express_model.generate_response_async(prompt) + + logger.debug(f"replyer生成内容: {content}") + return content + def weighted_sample_no_replacement(items, weights, k) -> list: """ @@ -1075,10 +1059,8 @@ 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) - return formatted_prompt_info + + return f"你有以下这些**知识**:\n{related_info}\n请你**记住上面的知识**,之后可能会用到。\n" else: logger.debug("从LPMM知识库获取知识失败,可能是从未导入过知识,返回空知识...") return "" diff --git a/src/chat/utils/chat_message_builder.py b/src/chat/utils/chat_message_builder.py index 3a08ca72b..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 +from typing import List, Dict, Any, Tuple, Optional, Callable from rich.traceback import install from src.config.config import global_config @@ -10,11 +10,161 @@ 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_sync( + content: str, + platform: str, + 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 + replace_bot_name: 是否将机器人的user_id替换为"机器人昵称(你)" + + Returns: + 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 # type: ignore + + name_resolver = default_resolver + + # 处理回复格式 + reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" + match = re.search(reply_pattern, content) + if match: + aaa = match[1] + bbb = match[2] + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + reply_person_name = f"{global_config.bot.nickname}(你)" + else: + reply_person_name = name_resolver(platform, bbb) or aaa + content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1) + except Exception: + # 如果解析失败,使用原始昵称 + content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1) + + # 处理@格式 + at_pattern = r"@<([^:<>]+):([^:<>]+)>" + at_matches = list(re.finditer(at_pattern, content)) + if at_matches: + new_content = "" + last_end = 0 + for m in at_matches: + new_content += content[last_end : m.start()] + aaa = m.group(1) + bbb = m.group(2) + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + at_person_name = f"{global_config.bot.nickname}(你)" + else: + at_person_name = name_resolver(platform, bbb) or aaa + new_content += f"@{at_person_name}" + except Exception: + # 如果解析失败,使用原始昵称 + new_content += f"@{aaa}" + last_end = m.end() + new_content += content[last_end:] + content = new_content + + return content + + +async def replace_user_references_async( + content: str, + platform: str, + name_resolver: Optional[Callable[[str, str], Any]] = None, + 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 # 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) + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + reply_person_name = f"{global_config.bot.nickname}(你)" + else: + reply_person_name = await name_resolver(platform, bbb) or aaa + content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1) + except Exception: + # 如果解析失败,使用原始昵称 + content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1) + + # 处理@格式 + at_pattern = r"@<([^:<>]+):([^:<>]+)>" + at_matches = list(re.finditer(at_pattern, content)) + if at_matches: + new_content = "" + last_end = 0 + for m in at_matches: + new_content += content[last_end : m.start()] + aaa = m.group(1) + bbb = m.group(2) + try: + # 检查是否是机器人自己 + if replace_bot_name and bbb == global_config.bot.qq_account: + at_person_name = f"{global_config.bot.nickname}(你)" + else: + at_person_name = await name_resolver(platform, bbb) or aaa + new_content += f"@{at_person_name}" + except Exception: + # 如果解析失败,使用原始昵称 + new_content += f"@{aaa}" + last_end = m.end() + new_content += content[last_end:] + content = new_content + + return content + + def get_raw_msg_by_timestamp( timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest" ) -> List[Dict[str, Any]]: @@ -374,33 +524,8 @@ def _build_readable_messages_internal( else: person_name = "某人" - # 检查是否有 回复 字段 - reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" - match = re.search(reply_pattern, content) - if match: - aaa: str = match[1] - bbb: str = match[2] - reply_person_id = PersonInfoManager.get_person_id(platform, bbb) - reply_person_name = person_info_manager.get_value_sync(reply_person_id, "person_name") or aaa - # 在内容前加上回复信息 - content = re.sub(reply_pattern, lambda m, name=reply_person_name: f"回复 {name}", content, count=1) - - # 检查是否有 @ 字段 @<{member_info.get('nickname')}:{member_info.get('user_id')}> - at_pattern = r"@<([^:<>]+):([^:<>]+)>" - at_matches = list(re.finditer(at_pattern, content)) - if at_matches: - new_content = "" - last_end = 0 - for m in at_matches: - new_content += content[last_end : m.start()] - aaa = m.group(1) - bbb = m.group(2) - at_person_id = PersonInfoManager.get_person_id(platform, bbb) - at_person_name = person_info_manager.get_value_sync(at_person_id, "person_name") or aaa - new_content += f"@{at_person_name}" - last_end = m.end() - new_content += content[last_end:] - content = new_content + # 使用独立函数处理用户引用格式 + 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: @@ -654,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, @@ -669,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, @@ -682,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( @@ -770,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, ) # 生成图片映射信息并添加到最前面 @@ -893,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 = "" @@ -916,38 +1045,14 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str: anon_name = get_anon_name(platform, user_id) # print(f"anon_name:{anon_name}") - # 处理 回复 - reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" - match = re.search(reply_pattern, content) - if match: - # print(f"发现回复match:{match}") - bbb = match.group(2) + # 使用独立函数处理用户引用格式,传入自定义的匿名名称解析器 + def anon_name_resolver(platform: str, user_id: str) -> str: try: - anon_reply = get_anon_name(platform, bbb) - # print(f"anon_reply:{anon_reply}") + return get_anon_name(platform, user_id) except Exception: - anon_reply = "?" - content = re.sub(reply_pattern, f"回复 {anon_reply}", content, count=1) + return "?" - # 处理 @,无嵌套def - at_pattern = r"@<([^:<>]+):([^:<>]+)>" - at_matches = list(re.finditer(at_pattern, content)) - if at_matches: - # print(f"发现@match:{at_matches}") - new_content = "" - last_end = 0 - for m in at_matches: - new_content += content[last_end : m.start()] - bbb = m.group(2) - try: - anon_at = get_anon_name(platform, bbb) - # print(f"anon_at:{anon_at}") - except Exception: - anon_at = "?" - new_content += f"@{anon_at}" - last_end = m.end() - new_content += content[last_end:] - content = new_content + 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/chat/utils/utils_image.py b/src/chat/utils/utils_image.py index 858d95aa3..7f14aa6d4 100644 --- a/src/chat/utils/utils_image.py +++ b/src/chat/utils/utils_image.py @@ -37,7 +37,7 @@ class ImageManager: self._ensure_image_dir() self._initialized = True - self._llm = LLMRequest(model=global_config.model.vlm, temperature=0.4, max_tokens=300, request_type="image") + self.vlm = LLMRequest(model=global_config.model.vlm, temperature=0.4, max_tokens=300, request_type="image") try: db.connect(reuse_if_open=True) @@ -94,7 +94,7 @@ class ImageManager: logger.error(f"保存描述到数据库失败 (Peewee): {str(e)}") async def get_emoji_description(self, image_base64: str) -> str: - """获取表情包描述,使用二步走识别并带缓存优化""" + """获取表情包描述,优先使用Emoji表中的缓存数据""" try: # 计算图片哈希 # 确保base64字符串只包含ASCII字符 @@ -104,9 +104,21 @@ class ImageManager: image_hash = hashlib.md5(image_bytes).hexdigest() image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # type: ignore - # 查询缓存的描述 + # 优先使用EmojiManager查询已注册表情包的描述 + try: + from src.chat.emoji_system.emoji_manager import get_emoji_manager + emoji_manager = get_emoji_manager() + cached_emoji_description = await emoji_manager.get_emoji_description_by_hash(image_hash) + if cached_emoji_description: + logger.info(f"[缓存命中] 使用已注册表情包描述: {cached_emoji_description[:50]}...") + return cached_emoji_description + except Exception as e: + logger.debug(f"查询EmojiManager时出错: {e}") + + # 查询ImageDescriptions表的缓存描述 cached_description = self._get_description_from_db(image_hash, "emoji") if cached_description: + logger.info(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...") return f"[表情包:{cached_description}]" # === 二步走识别流程 === @@ -118,10 +130,10 @@ class ImageManager: logger.warning("GIF转换失败,无法获取描述") return "[表情包(GIF处理失败)]" vlm_prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析" - detailed_description, _ = await self._llm.generate_response_for_image(vlm_prompt, image_base64_processed, "jpg") + detailed_description, _ = await self.vlm.generate_response_for_image(vlm_prompt, image_base64_processed, "jpg") else: vlm_prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,描述细节,从互联网梗,meme的角度去分析" - detailed_description, _ = await self._llm.generate_response_for_image(vlm_prompt, image_base64, image_format) + detailed_description, _ = await self.vlm.generate_response_for_image(vlm_prompt, image_base64, image_format) if detailed_description is None: logger.warning("VLM未能生成表情包详细描述") @@ -158,7 +170,7 @@ class ImageManager: if len(emotions) > 1 and emotions[1] != emotions[0]: final_emotion = f"{emotions[0]},{emotions[1]}" - logger.info(f"[二步走识别] 详细描述: {detailed_description[:50]}... -> 情感标签: {final_emotion}") + logger.info(f"[emoji识别] 详细描述: {detailed_description[:50]}... -> 情感标签: {final_emotion}") # 再次检查缓存,防止并发写入时重复生成 cached_description = self._get_description_from_db(image_hash, "emoji") @@ -201,13 +213,13 @@ class ImageManager: self._save_description_to_db(image_hash, final_emotion, "emoji") return f"[表情包:{final_emotion}]" - + except Exception as e: logger.error(f"获取表情包描述失败: {str(e)}") - return "[表情包]" + return "[表情包(处理失败)]" async def get_image_description(self, image_base64: str) -> str: - """获取普通图片描述,带查重和保存功能""" + """获取普通图片描述,优先使用Images表中的缓存数据""" try: # 计算图片哈希 if isinstance(image_base64, str): @@ -215,7 +227,7 @@ class ImageManager: image_bytes = base64.b64decode(image_base64) image_hash = hashlib.md5(image_bytes).hexdigest() - # 检查图片是否已存在 + # 优先检查Images表中是否已有完整的描述 existing_image = Images.get_or_none(Images.emoji_hash == image_hash) if existing_image: # 更新计数 @@ -227,18 +239,20 @@ class ImageManager: # 如果已有描述,直接返回 if existing_image.description: + logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...") return f"[图片:{existing_image.description}]" - # 查询缓存的描述 + # 查询ImageDescriptions表的缓存描述 cached_description = self._get_description_from_db(image_hash, "image") if cached_description: - logger.debug(f"图片描述缓存中 {cached_description}") + logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...") return f"[图片:{cached_description}]" # 调用AI获取描述 image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # type: ignore prompt = global_config.custom_prompt.image_prompt - description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) + logger.info(f"[VLM调用] 为图片生成新描述 (Hash: {image_hash[:8]}...)") + description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) if description is None: logger.warning("AI未能生成图片描述") @@ -266,6 +280,7 @@ class ImageManager: if not hasattr(existing_image, "vlm_processed") or existing_image.vlm_processed is None: existing_image.vlm_processed = True existing_image.save() + logger.debug(f"[数据库] 更新已有图片记录: {image_hash[:8]}...") else: Images.create( image_id=str(uuid.uuid4()), @@ -277,16 +292,18 @@ class ImageManager: vlm_processed=True, count=1, ) + logger.debug(f"[数据库] 创建新图片记录: {image_hash[:8]}...") except Exception as e: logger.error(f"保存图片文件或元数据失败: {str(e)}") - # 保存描述到ImageDescriptions表 + # 保存描述到ImageDescriptions表作为备用缓存 self._save_description_to_db(image_hash, description, "image") + logger.info(f"[VLM完成] 图片描述生成: {description[:50]}...") return f"[图片:{description}]" except Exception as e: logger.error(f"获取图片描述失败: {str(e)}") - return "[图片]" + return "[图片(处理失败)]" @staticmethod def transform_gif(gif_base64: str, similarity_threshold: float = 1000.0, max_frames: int = 15) -> Optional[str]: @@ -502,12 +519,28 @@ class ImageManager: image_bytes = base64.b64decode(image_base64) image_hash = hashlib.md5(image_bytes).hexdigest() - # 先检查缓存的描述 + # 获取当前图片记录 + image = Images.get(Images.image_id == image_id) + + # 优先检查是否已有其他相同哈希的图片记录包含描述 + existing_with_description = Images.get_or_none( + (Images.emoji_hash == image_hash) & + (Images.description.is_null(False)) & + (Images.description != "") + ) + if existing_with_description and existing_with_description.id != image.id: + logger.debug(f"[缓存复用] 从其他相同图片记录复用描述: {existing_with_description.description[:50]}...") + image.description = existing_with_description.description + image.vlm_processed = True + image.save() + # 同时保存到ImageDescriptions表作为备用缓存 + self._save_description_to_db(image_hash, existing_with_description.description, "image") + return + + # 检查ImageDescriptions表的缓存描述 cached_description = self._get_description_from_db(image_hash, "image") if cached_description: - logger.debug(f"VLM处理时发现缓存描述: {cached_description}") - # 更新数据库 - image = Images.get(Images.image_id == image_id) + logger.debug(f"[缓存复用] 从ImageDescriptions表复用描述: {cached_description[:50]}...") image.description = cached_description image.vlm_processed = True image.save() @@ -520,7 +553,8 @@ class ImageManager: prompt = global_config.custom_prompt.image_prompt # 获取VLM描述 - description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format) + logger.info(f"[VLM异步调用] 为图片生成描述 (ID: {image_id}, Hash: {image_hash[:8]}...)") + description, _ = await self.vlm.generate_response_for_image(prompt, image_base64, image_format) if description is None: logger.warning("VLM未能生成图片描述") @@ -533,14 +567,15 @@ class ImageManager: description = cached_description # 更新数据库 - image = Images.get(Images.image_id == image_id) image.description = description image.vlm_processed = True image.save() - # 保存描述到ImageDescriptions表 + # 保存描述到ImageDescriptions表作为备用缓存 self._save_description_to_db(image_hash, description, "image") + logger.info(f"[VLM异步完成] 图片描述生成: {description[:50]}...") + except Exception as e: logger.error(f"VLM处理图片失败: {str(e)}") diff --git a/src/chat/willing/mode_classical.py b/src/chat/willing/mode_classical.py index 57400c44d..4ffbbcea8 100644 --- a/src/chat/willing/mode_classical.py +++ b/src/chat/willing/mode_classical.py @@ -28,7 +28,7 @@ class ClassicalWillingManager(BaseWillingManager): # print(f"[{chat_id}] 回复意愿: {current_willing}") - interested_rate = willing_info.interested_rate * global_config.normal_chat.response_interested_rate_amplifier + interested_rate = willing_info.interested_rate # print(f"[{chat_id}] 兴趣值: {interested_rate}") @@ -36,20 +36,18 @@ class ClassicalWillingManager(BaseWillingManager): current_willing += interested_rate - 0.2 if willing_info.is_mentioned_bot and global_config.chat.mentioned_bot_inevitable_reply and current_willing < 2: - current_willing += 1 if current_willing < 1.0 else 0.05 + current_willing += 1 if current_willing < 1.0 else 0.2 self.chat_reply_willing[chat_id] = min(current_willing, 1.0) - reply_probability = min(max((current_willing - 0.5), 0.01) * 2, 1) + reply_probability = min(max((current_willing - 0.5), 0.01) * 2, 1.5) # print(f"[{chat_id}] 回复概率: {reply_probability}") return reply_probability async def before_generate_reply_handle(self, message_id): - chat_id = self.ongoing_messages[message_id].chat_id - current_willing = self.chat_reply_willing.get(chat_id, 0) - self.chat_reply_willing[chat_id] = max(0.0, current_willing - 1.8) + pass async def after_generate_reply_handle(self, message_id): if message_id not in self.ongoing_messages: @@ -58,7 +56,7 @@ class ClassicalWillingManager(BaseWillingManager): chat_id = self.ongoing_messages[message_id].chat_id current_willing = self.chat_reply_willing.get(chat_id, 0) if current_willing < 1: - self.chat_reply_willing[chat_id] = min(1.0, current_willing + 0.4) + self.chat_reply_willing[chat_id] = min(1.0, current_willing + 0.3) async def not_reply_handle(self, message_id): return await super().not_reply_handle(message_id) diff --git a/src/common/logger.py b/src/common/logger.py index a6bfc2634..78446decb 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -390,7 +390,7 @@ MODULE_COLORS = { "tts_action": "\033[38;5;58m", # 深黄色 "doubao_pic_plugin": "\033[38;5;64m", # 深绿色 # Action组件 - "no_reply_action": "\033[38;5;196m", # 亮红色,更显眼 + "no_reply_action": "\033[38;5;214m", # 亮橙色,显眼但不像警告 "reply_action": "\033[38;5;46m", # 亮绿色 "base_action": "\033[38;5;250m", # 浅灰色 # 数据库和消息 diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 335a2be59..08acf97c6 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -69,6 +69,8 @@ class ChatConfig(ConfigBase): max_context_size: int = 18 """上下文长度""" + + willing_amplifier: float = 1.0 replyer_random_probability: float = 0.5 """ @@ -76,15 +78,12 @@ class ChatConfig(ConfigBase): 选择普通模型的概率为 1 - reasoning_normal_model_probability """ - thinking_timeout: int = 30 + thinking_timeout: int = 40 """麦麦最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢)""" talk_frequency: float = 1 """回复频率阈值""" - use_s4u_prompt_mode: bool = False - """是否使用 s4u 对话构建模式,该模式会分开处理当前对话对象和其他所有对话的内容进行 prompt 构建""" - mentioned_bot_inevitable_reply: bool = False """提及 bot 必然回复""" @@ -274,12 +273,6 @@ class NormalChatConfig(ConfigBase): willing_mode: str = "classical" """意愿模式""" - response_interested_rate_amplifier: float = 1.0 - """回复兴趣度放大系数""" - - - - @dataclass class ExpressionConfig(ConfigBase): """表达配置类""" @@ -307,11 +300,8 @@ class ExpressionConfig(ConfigBase): class ToolConfig(ConfigBase): """工具配置类""" - enable_in_normal_chat: bool = False - """是否在普通聊天中启用工具""" - - enable_in_focus_chat: bool = True - """是否在专注聊天中启用工具""" + enable_tool: bool = False + """是否在聊天中启用工具""" @dataclass class VoiceConfig(ConfigBase): diff --git a/src/individuality/individuality.py b/src/individuality/individuality.py index fc7156e14..4c8fcac50 100644 --- a/src/individuality/individuality.py +++ b/src/individuality/individuality.py @@ -273,15 +273,19 @@ class Individuality: prompt=prompt, ) - if response.strip(): + if response and response.strip(): personality_parts.append(response.strip()) logger.info(f"精简人格侧面: {response.strip()}") else: logger.error(f"使用LLM压缩人设时出错: {response}") + # 压缩失败时使用原始内容 + if personality_side: + personality_parts.append(personality_side) + if personality_parts: personality_result = "。".join(personality_parts) else: - personality_result = personality_core + personality_result = personality_core or "友好活泼" else: personality_result = personality_core if personality_side: @@ -308,13 +312,14 @@ class Individuality: prompt=prompt, ) - if response.strip(): + if response and response.strip(): identity_result = response.strip() logger.info(f"精简身份: {identity_result}") else: logger.error(f"使用LLM压缩身份时出错: {response}") + identity_result = identity else: - identity_result = "。".join(identity) + identity_result = identity return identity_result diff --git a/src/mais4u/mais4u_chat/s4u_msg_processor.py b/src/mais4u/mais4u_chat/s4u_msg_processor.py index cbc7d3fac..c5ad9ca1f 100644 --- a/src/mais4u/mais4u_chat/s4u_msg_processor.py +++ b/src/mais4u/mais4u_chat/s4u_msg_processor.py @@ -47,11 +47,35 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]: logger.debug(f"记忆激活率: {interested_rate:.2f}") text_len = len(message.processed_plain_text) - # 根据文本长度调整兴趣度,长度越大兴趣度越高,但增长率递减,最低0.01,最高0.05 - # 采用对数函数实现递减增长 - - base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) - base_interest = min(max(base_interest, 0.01), 0.05) + # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算 + # 基于实际分布:0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%) + + if text_len == 0: + base_interest = 0.01 # 空消息最低兴趣度 + elif text_len <= 5: + # 1-5字符:线性增长 0.01 -> 0.03 + base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4 + elif text_len <= 10: + # 6-10字符:线性增长 0.03 -> 0.06 + base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5 + elif text_len <= 20: + # 11-20字符:线性增长 0.06 -> 0.12 + base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10 + elif text_len <= 30: + # 21-30字符:线性增长 0.12 -> 0.18 + base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10 + elif text_len <= 50: + # 31-50字符:线性增长 0.18 -> 0.22 + base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20 + elif text_len <= 100: + # 51-100字符:线性增长 0.22 -> 0.26 + base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50 + else: + # 100+字符:对数增长 0.26 -> 0.3,增长率递减 + base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901 + + # 确保在范围内 + base_interest = min(max(base_interest, 0.01), 0.3) interested_rate += base_interest diff --git a/src/mood/mood_manager.py b/src/mood/mood_manager.py index 38ed39bcc..eae0ea713 100644 --- a/src/mood/mood_manager.py +++ b/src/mood/mood_manager.py @@ -78,7 +78,7 @@ class ChatMood: if interested_rate <= 0: interest_multiplier = 0 else: - interest_multiplier = 3 * math.pow(interested_rate, 0.25) + interest_multiplier = 2 * math.pow(interested_rate, 0.25) logger.debug( f"base_probability: {base_probability}, time_multiplier: {time_multiplier}, interest_multiplier: {interest_multiplier}" diff --git a/src/person_info/relationship_manager.py b/src/person_info/relationship_manager.py index 01cc89e9a..6c2693572 100644 --- a/src/person_info/relationship_manager.py +++ b/src/person_info/relationship_manager.py @@ -139,7 +139,7 @@ class RelationshipManager: 请用json格式输出,引起了你的兴趣,或者有什么需要你记忆的点。 并为每个点赋予1-10的权重,权重越高,表示越重要。 格式如下: -{{ +[ {{ "point": "{person_name}想让我记住他的生日,我回答确认了,他的生日是11月23日", "weight": 10 @@ -156,13 +156,10 @@ class RelationshipManager: "point": "{person_name}喜欢吃辣,具体来说,没有辣的食物ta都不喜欢吃,可能是因为ta是湖南人。", "weight": 7 }} -}} +] -如果没有,就输出none,或points为空: -{{ - "point": "none", - "weight": 0 -}} +如果没有,就输出none,或返回空数组: +[] """ # 调用LLM生成印象 @@ -184,17 +181,25 @@ class RelationshipManager: try: points = repair_json(points) points_data = json.loads(points) - if points_data == "none" or not points_data or points_data.get("point") == "none": + + # 只处理正确的格式,错误格式直接跳过 + if points_data == "none" or not points_data: points_list = [] + elif isinstance(points_data, str) and points_data.lower() == "none": + points_list = [] + elif isinstance(points_data, list): + # 正确格式:数组格式 [{"point": "...", "weight": 10}, ...] + if not points_data: # 空数组 + points_list = [] + else: + points_list = [(item["point"], float(item["weight"]), current_time) for item in points_data] else: - # logger.info(f"points_data: {points_data}") - if isinstance(points_data, dict) and "points" in points_data: - points_data = points_data["points"] - if not isinstance(points_data, list): - points_data = [points_data] - # 添加可读时间到每个point - points_list = [(item["point"], float(item["weight"]), current_time) for item in points_data] + # 错误格式,直接跳过不解析 + logger.warning(f"LLM返回了错误的JSON格式,跳过解析: {type(points_data)}, 内容: {points_data}") + points_list = [] + # 权重过滤逻辑 + if points_list: original_points_list = list(points_list) points_list.clear() discarded_count = 0 diff --git a/src/plugin_system/__init__.py b/src/plugin_system/__init__.py index eb07dbc92..f8c71af42 100644 --- a/src/plugin_system/__init__.py +++ b/src/plugin_system/__init__.py @@ -9,6 +9,7 @@ from .base import ( BasePlugin, BaseAction, BaseCommand, + BaseTool, ConfigField, ComponentType, ActionActivationType, @@ -34,6 +35,7 @@ from .utils import ( from .apis import ( chat_api, + tool_api, component_manage_api, config_api, database_api, @@ -44,17 +46,17 @@ from .apis import ( person_api, plugin_manage_api, send_api, - utils_api, register_plugin, get_logger, ) -__version__ = "1.0.0" +__version__ = "2.0.0" __all__ = [ # API 模块 "chat_api", + "tool_api", "component_manage_api", "config_api", "database_api", @@ -65,13 +67,13 @@ __all__ = [ "person_api", "plugin_manage_api", "send_api", - "utils_api", "register_plugin", "get_logger", # 基础类 "BasePlugin", "BaseAction", "BaseCommand", + "BaseTool", "BaseEventHandler", # 类型定义 "ComponentType", diff --git a/src/plugin_system/apis/__init__.py b/src/plugin_system/apis/__init__.py index 0882fbdc6..362c98581 100644 --- a/src/plugin_system/apis/__init__.py +++ b/src/plugin_system/apis/__init__.py @@ -17,7 +17,7 @@ from src.plugin_system.apis import ( person_api, plugin_manage_api, send_api, - utils_api, + tool_api, ) from .logging_api import get_logger from .plugin_register_api import register_plugin @@ -35,7 +35,7 @@ __all__ = [ "person_api", "plugin_manage_api", "send_api", - "utils_api", "get_logger", "register_plugin", + "tool_api", ] diff --git a/src/plugin_system/apis/chat_api.py b/src/plugin_system/apis/chat_api.py index 35a210faa..9e995d36f 100644 --- a/src/plugin_system/apis/chat_api.py +++ b/src/plugin_system/apis/chat_api.py @@ -32,6 +32,7 @@ class ChatManager: @staticmethod def get_all_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: + # sourcery skip: for-append-to-extend """获取所有聊天流 Args: @@ -57,6 +58,7 @@ class ChatManager: @staticmethod def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: + # sourcery skip: for-append-to-extend """获取所有群聊聊天流 Args: @@ -79,6 +81,7 @@ class ChatManager: @staticmethod def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: + # sourcery skip: for-append-to-extend """获取所有私聊聊天流 Args: @@ -105,7 +108,7 @@ class ChatManager: @staticmethod def get_group_stream_by_group_id( group_id: str, platform: Optional[str] | SpecialTypes = "qq" - ) -> Optional[ChatStream]: + ) -> Optional[ChatStream]: # sourcery skip: remove-unnecessary-cast """根据群ID获取聊天流 Args: @@ -142,7 +145,7 @@ class ChatManager: @staticmethod def get_private_stream_by_user_id( user_id: str, platform: Optional[str] | SpecialTypes = "qq" - ) -> Optional[ChatStream]: + ) -> Optional[ChatStream]: # sourcery skip: remove-unnecessary-cast """根据用户ID获取私聊流 Args: @@ -207,7 +210,7 @@ class ChatManager: chat_stream: 聊天流对象 Returns: - Dict[str, Any]: 聊天流信息字典 + Dict ({str: Any}): 聊天流信息字典 Raises: TypeError: 如果 chat_stream 不是 ChatStream 类型 @@ -282,41 +285,41 @@ class ChatManager: # ============================================================================= -def get_all_streams(platform: Optional[str] | SpecialTypes = "qq"): +def get_all_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: """获取所有聊天流的便捷函数""" return ChatManager.get_all_streams(platform) -def get_group_streams(platform: Optional[str] | SpecialTypes = "qq"): +def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: """获取群聊聊天流的便捷函数""" return ChatManager.get_group_streams(platform) -def get_private_streams(platform: Optional[str] | SpecialTypes = "qq"): +def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: """获取私聊聊天流的便捷函数""" return ChatManager.get_private_streams(platform) -def get_stream_by_group_id(group_id: str, platform: Optional[str] | SpecialTypes = "qq"): +def get_stream_by_group_id(group_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]: """根据群ID获取聊天流的便捷函数""" return ChatManager.get_group_stream_by_group_id(group_id, platform) -def get_stream_by_user_id(user_id: str, platform: Optional[str] | SpecialTypes = "qq"): +def get_stream_by_user_id(user_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]: """根据用户ID获取私聊流的便捷函数""" return ChatManager.get_private_stream_by_user_id(user_id, platform) -def get_stream_type(chat_stream: ChatStream): +def get_stream_type(chat_stream: ChatStream) -> str: """获取聊天流类型的便捷函数""" return ChatManager.get_stream_type(chat_stream) -def get_stream_info(chat_stream: ChatStream): +def get_stream_info(chat_stream: ChatStream) -> Dict[str, Any]: """获取聊天流信息的便捷函数""" return ChatManager.get_stream_info(chat_stream) -def get_streams_summary(): +def get_streams_summary() -> Dict[str, int]: """获取聊天流统计摘要的便捷函数""" return ChatManager.get_streams_summary() diff --git a/src/plugin_system/apis/component_manage_api.py b/src/plugin_system/apis/component_manage_api.py index d9ea051d9..1ffa0833e 100644 --- a/src/plugin_system/apis/component_manage_api.py +++ b/src/plugin_system/apis/component_manage_api.py @@ -5,6 +5,7 @@ from src.plugin_system.base.component_types import ( EventHandlerInfo, PluginInfo, ComponentType, + ToolInfo, ) @@ -119,6 +120,21 @@ def get_registered_command_info(command_name: str) -> Optional[CommandInfo]: return component_registry.get_registered_command_info(command_name) +def get_registered_tool_info(tool_name: str) -> Optional[ToolInfo]: + """ + 获取指定 Tool 的注册信息。 + + Args: + tool_name (str): Tool 名称。 + + Returns: + ToolInfo: Tool 信息对象,如果 Tool 不存在则返回 None。 + """ + from src.plugin_system.core.component_registry import component_registry + + return component_registry.get_registered_tool_info(tool_name) + + # === EventHandler 特定查询方法 === def get_registered_event_handler_info( event_handler_name: str, @@ -191,6 +207,8 @@ def locally_enable_component(component_name: str, component_type: ComponentType, return global_announcement_manager.enable_specific_chat_action(stream_id, component_name) case ComponentType.COMMAND: return global_announcement_manager.enable_specific_chat_command(stream_id, component_name) + case ComponentType.TOOL: + return global_announcement_manager.enable_specific_chat_tool(stream_id, component_name) case ComponentType.EVENT_HANDLER: return global_announcement_manager.enable_specific_chat_event_handler(stream_id, component_name) case _: @@ -216,11 +234,14 @@ def locally_disable_component(component_name: str, component_type: ComponentType return global_announcement_manager.disable_specific_chat_action(stream_id, component_name) case ComponentType.COMMAND: return global_announcement_manager.disable_specific_chat_command(stream_id, component_name) + case ComponentType.TOOL: + return global_announcement_manager.disable_specific_chat_tool(stream_id, component_name) case ComponentType.EVENT_HANDLER: return global_announcement_manager.disable_specific_chat_event_handler(stream_id, component_name) case _: raise ValueError(f"未知 component type: {component_type}") + def get_locally_disabled_components(stream_id: str, component_type: ComponentType) -> list[str]: """ 获取指定消息流中禁用的组件列表。 @@ -239,7 +260,9 @@ def get_locally_disabled_components(stream_id: str, component_type: ComponentTyp return global_announcement_manager.get_disabled_chat_actions(stream_id) case ComponentType.COMMAND: return global_announcement_manager.get_disabled_chat_commands(stream_id) + case ComponentType.TOOL: + return global_announcement_manager.get_disabled_chat_tools(stream_id) case ComponentType.EVENT_HANDLER: return global_announcement_manager.get_disabled_chat_event_handlers(stream_id) case _: - raise ValueError(f"未知 component type: {component_type}") \ No newline at end of file + raise ValueError(f"未知 component type: {component_type}") diff --git a/src/plugin_system/apis/config_api.py b/src/plugin_system/apis/config_api.py index 6ec492caf..05556414e 100644 --- a/src/plugin_system/apis/config_api.py +++ b/src/plugin_system/apis/config_api.py @@ -10,7 +10,6 @@ from typing import Any from src.common.logger import get_logger from src.config.config import global_config -from src.person_info.person_info import get_person_info_manager logger = get_logger("config_api") @@ -26,7 +25,7 @@ def get_global_config(key: str, default: Any = None) -> Any: 插件应使用此方法读取全局配置,以保证只读和隔离性。 Args: - key: 命名空间式配置键名,支持嵌套访问,如 "section.subsection.key",大小写敏感 + key: 命名空间式配置键名,使用嵌套访问,如 "section.subsection.key",大小写敏感 default: 如果配置不存在时返回的默认值 Returns: @@ -76,50 +75,3 @@ def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any except Exception as e: logger.warning(f"[ConfigAPI] 获取插件配置 {key} 失败: {e}") return default - - -# ============================================================================= -# 用户信息API函数 -# ============================================================================= - - -async def get_user_id_by_person_name(person_name: str) -> tuple[str, str]: - """根据内部用户名获取用户ID - - Args: - person_name: 用户名 - - Returns: - tuple[str, str]: (平台, 用户ID) - """ - try: - person_info_manager = get_person_info_manager() - person_id = person_info_manager.get_person_id_by_person_name(person_name) - user_id: str = await person_info_manager.get_value(person_id, "user_id") # type: ignore - platform: str = await person_info_manager.get_value(person_id, "platform") # type: ignore - return platform, user_id - except Exception as e: - logger.error(f"[ConfigAPI] 根据用户名获取用户ID失败: {e}") - return "", "" - - -async def get_person_info(person_id: str, key: str, default: Any = None) -> Any: - """获取用户信息 - - Args: - person_id: 用户ID - key: 信息键名 - default: 默认值 - - Returns: - Any: 用户信息值或默认值 - """ - try: - person_info_manager = get_person_info_manager() - response = await person_info_manager.get_value(person_id, key) - if not response: - raise ValueError(f"[ConfigAPI] 获取用户 {person_id} 的信息 '{key}' 失败,返回默认值") - return response - except Exception as e: - logger.error(f"[ConfigAPI] 获取用户信息失败: {e}") - return default diff --git a/src/plugin_system/apis/database_api.py b/src/plugin_system/apis/database_api.py index d46bfba39..8b253806b 100644 --- a/src/plugin_system/apis/database_api.py +++ b/src/plugin_system/apis/database_api.py @@ -152,10 +152,7 @@ async def db_query( except DoesNotExist: # 记录不存在 - if query_type == "get" and single_result: - return None - return [] - + return None if query_type == "get" and single_result else [] except Exception as e: logger.error(f"[DatabaseAPI] 数据库操作出错: {e}") traceback.print_exc() @@ -170,7 +167,8 @@ async def db_query( async def db_save( model_class: Type[Model], data: Dict[str, Any], key_field: Optional[str] = None, key_value: Optional[Any] = None -) -> Union[Dict[str, Any], None]: +) -> Optional[Dict[str, Any]]: + # sourcery skip: inline-immediately-returned-variable """保存数据到数据库(创建或更新) 如果提供了key_field和key_value,会先尝试查找匹配的记录进行更新; @@ -203,10 +201,9 @@ async def db_save( try: # 如果提供了key_field和key_value,尝试更新现有记录 if key_field and key_value is not None: - # 查找现有记录 - existing_records = list(model_class.select().where(getattr(model_class, key_field) == key_value).limit(1)) - - if existing_records: + if existing_records := list( + model_class.select().where(getattr(model_class, key_field) == key_value).limit(1) + ): # 更新现有记录 existing_record = existing_records[0] for field, value in data.items(): @@ -244,8 +241,8 @@ async def db_get( Args: model_class: Peewee模型类 filters: 过滤条件,字段名和值的字典 - order_by: 排序字段,前缀'-'表示降序,例如'-time'表示按时间字段(即time字段)降序 limit: 结果数量限制 + order_by: 排序字段,前缀'-'表示降序,例如'-time'表示按时间字段(即time字段)降序 single_result: 是否只返回单个结果,如果为True,则返回单个记录字典或None;否则返回记录字典列表或空列表 Returns: @@ -310,7 +307,7 @@ async def store_action_info( thinking_id: str = "", action_data: Optional[dict] = None, action_name: str = "", -) -> Union[Dict[str, Any], None]: +) -> Optional[Dict[str, Any]]: """存储动作信息到数据库 将Action执行的相关信息保存到ActionRecords表中,用于后续的记忆和上下文构建。 diff --git a/src/plugin_system/apis/emoji_api.py b/src/plugin_system/apis/emoji_api.py index cafb52df8..479f3aec1 100644 --- a/src/plugin_system/apis/emoji_api.py +++ b/src/plugin_system/apis/emoji_api.py @@ -65,14 +65,14 @@ async def get_by_description(description: str) -> Optional[Tuple[str, str, str]] return None -async def get_random(count: Optional[int] = 1) -> Optional[List[Tuple[str, str, str]]]: +async def get_random(count: Optional[int] = 1) -> List[Tuple[str, str, str]]: """随机获取指定数量的表情包 Args: count: 要获取的表情包数量,默认为1 Returns: - Optional[List[Tuple[str, str, str]]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表,如果失败则为None + List[Tuple[str, str, str]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表,失败则返回空列表 Raises: TypeError: 如果count不是整数类型 @@ -94,13 +94,13 @@ async def get_random(count: Optional[int] = 1) -> Optional[List[Tuple[str, str, if not all_emojis: logger.warning("[EmojiAPI] 没有可用的表情包") - return None + return [] # 过滤有效表情包 valid_emojis = [emoji for emoji in all_emojis if not emoji.is_deleted] if not valid_emojis: logger.warning("[EmojiAPI] 没有有效的表情包") - return None + return [] if len(valid_emojis) < count: logger.warning( @@ -127,14 +127,14 @@ async def get_random(count: Optional[int] = 1) -> Optional[List[Tuple[str, str, if not results and count > 0: logger.warning("[EmojiAPI] 随机获取表情包失败,没有一个可以成功处理") - return None + return [] logger.info(f"[EmojiAPI] 成功获取 {len(results)} 个随机表情包") return results except Exception as e: logger.error(f"[EmojiAPI] 获取随机表情包失败: {e}") - return None + return [] async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]: @@ -162,10 +162,11 @@ async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]: # 筛选匹配情感的表情包 matching_emojis = [] - for emoji_obj in all_emojis: - if not emoji_obj.is_deleted and emotion.lower() in [e.lower() for e in emoji_obj.emotion]: - matching_emojis.append(emoji_obj) - + matching_emojis.extend( + emoji_obj + for emoji_obj in all_emojis + if not emoji_obj.is_deleted and emotion.lower() in [e.lower() for e in emoji_obj.emotion] + ) if not matching_emojis: logger.warning(f"[EmojiAPI] 未找到匹配情感 '{emotion}' 的表情包") return None @@ -256,10 +257,11 @@ def get_descriptions() -> List[str]: emoji_manager = get_emoji_manager() descriptions = [] - for emoji_obj in emoji_manager.emoji_objects: - if not emoji_obj.is_deleted and emoji_obj.description: - descriptions.append(emoji_obj.description) - + descriptions.extend( + emoji_obj.description + for emoji_obj in emoji_manager.emoji_objects + if not emoji_obj.is_deleted and emoji_obj.description + ) return descriptions except Exception as e: logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}") diff --git a/src/plugin_system/apis/generator_api.py b/src/plugin_system/apis/generator_api.py index cbb1336ce..f8752ac4e 100644 --- a/src/plugin_system/apis/generator_api.py +++ b/src/plugin_system/apis/generator_api.py @@ -84,18 +84,23 @@ async def generate_reply( enable_chinese_typo: bool = True, return_prompt: bool = False, model_configs: Optional[List[Dict[str, Any]]] = None, - request_type: str = "", - enable_timeout: bool = False, + request_type: str = "generator_api", ) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: """生成回复 Args: chat_stream: 聊天流对象(优先) chat_id: 聊天ID(备用) - action_data: 动作数据 + action_data: 动作数据(向下兼容,包含reply_to和extra_info) + reply_to: 回复对象,格式为 "发送者:消息内容" + extra_info: 额外信息,用于补充上下文 + available_actions: 可用动作 + enable_tool: 是否启用工具调用 enable_splitter: 是否启用消息分割器 enable_chinese_typo: 是否启用错字生成器 return_prompt: 是否返回提示词 + model_configs: 模型配置列表 + request_type: 请求类型(可选,记录LLM使用) Returns: Tuple[bool, List[Tuple[str, Any]], Optional[str]]: (是否成功, 回复集合, 提示词) """ @@ -108,13 +113,16 @@ async def generate_reply( logger.debug("[GeneratorAPI] 开始生成回复") + if not reply_to and action_data: + reply_to = action_data.get("reply_to", "") + if not extra_info and action_data: + extra_info = action_data.get("extra_info", "") + # 调用回复器生成回复 success, content, prompt = await replyer.generate_reply_with_context( - reply_data=action_data or {}, reply_to=reply_to, extra_info=extra_info, available_actions=available_actions, - enable_timeout=enable_timeout, enable_tool=enable_tool, ) reply_set = [] @@ -136,6 +144,7 @@ async def generate_reply( except Exception as e: logger.error(f"[GeneratorAPI] 生成回复时出错: {e}") + logger.error(traceback.format_exc()) return False, [], None @@ -146,15 +155,24 @@ async def rewrite_reply( enable_splitter: bool = True, enable_chinese_typo: bool = True, model_configs: Optional[List[Dict[str, Any]]] = None, -) -> Tuple[bool, List[Tuple[str, Any]]]: + raw_reply: str = "", + reason: str = "", + reply_to: str = "", + return_prompt: bool = False, +) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: """重写回复 Args: chat_stream: 聊天流对象(优先) - reply_data: 回复数据 + reply_data: 回复数据字典(向下兼容备用,当其他参数缺失时从此获取) chat_id: 聊天ID(备用) enable_splitter: 是否启用消息分割器 enable_chinese_typo: 是否启用错字生成器 + model_configs: 模型配置列表 + raw_reply: 原始回复内容 + reason: 回复原因 + reply_to: 回复对象 + return_prompt: 是否返回提示词 Returns: Tuple[bool, List[Tuple[str, Any]]]: (是否成功, 回复集合) @@ -164,12 +182,23 @@ async def rewrite_reply( replyer = get_replyer(chat_stream, chat_id, model_configs=model_configs) if not replyer: logger.error("[GeneratorAPI] 无法获取回复器") - return False, [] + return False, [], None logger.info("[GeneratorAPI] 开始重写回复") + # 如果参数缺失,从reply_data中获取 + if reply_data: + raw_reply = raw_reply or reply_data.get("raw_reply", "") + reason = reason or reply_data.get("reason", "") + reply_to = reply_to or reply_data.get("reply_to", "") + # 调用回复器重写回复 - success, content = await replyer.rewrite_reply_with_context(reply_data=reply_data or {}) + success, content, prompt = await replyer.rewrite_reply_with_context( + raw_reply=raw_reply, + reason=reason, + reply_to=reply_to, + return_prompt=return_prompt, + ) reply_set = [] if content: reply_set = await process_human_text(content, enable_splitter, enable_chinese_typo) @@ -179,14 +208,14 @@ async def rewrite_reply( else: logger.warning("[GeneratorAPI] 重写回复失败") - return success, reply_set + return success, reply_set, prompt if return_prompt else None except ValueError as ve: raise ve except Exception as e: logger.error(f"[GeneratorAPI] 重写回复时出错: {e}") - return False, [] + return False, [], None async def process_human_text(content: str, enable_splitter: bool, enable_chinese_typo: bool) -> List[Tuple[str, Any]]: @@ -212,3 +241,27 @@ async def process_human_text(content: str, enable_splitter: bool, enable_chinese except Exception as e: logger.error(f"[GeneratorAPI] 处理人形文本时出错: {e}") return [] + +async def generate_response_custom( + chat_stream: Optional[ChatStream] = None, + chat_id: Optional[str] = None, + model_configs: Optional[List[Dict[str, Any]]] = None, + prompt: str = "", +) -> Optional[str]: + replyer = get_replyer(chat_stream, chat_id, model_configs=model_configs) + if not replyer: + logger.error("[GeneratorAPI] 无法获取回复器") + return None + + try: + logger.debug("[GeneratorAPI] 开始生成自定义回复") + response = await replyer.llm_generate_content(prompt) + if response: + logger.debug("[GeneratorAPI] 自定义回复生成成功") + return response + else: + logger.warning("[GeneratorAPI] 自定义回复生成失败") + return None + except Exception as e: + logger.error(f"[GeneratorAPI] 生成自定义回复时出错: {e}") + return None \ No newline at end of file diff --git a/src/plugin_system/apis/llm_api.py b/src/plugin_system/apis/llm_api.py index 72b865b89..4e9d884fa 100644 --- a/src/plugin_system/apis/llm_api.py +++ b/src/plugin_system/apis/llm_api.py @@ -54,7 +54,7 @@ def get_available_models() -> Dict[str, Any]: async def generate_with_model( prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs -) -> Tuple[bool, str, str, str]: +) -> Tuple[bool, str]: """使用指定模型生成内容 Args: @@ -73,10 +73,11 @@ async def generate_with_model( llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs) - response, (reasoning, model_name) = await llm_request.generate_response_async(prompt) - return True, response, reasoning, model_name + # TODO: 复活这个_ + response, _ = await llm_request.generate_response_async(prompt) + return True, response except Exception as e: error_msg = f"生成内容时出错: {str(e)}" logger.error(f"[LLMAPI] {error_msg}") - return False, error_msg, "", "" + return False, error_msg diff --git a/src/plugin_system/apis/message_api.py b/src/plugin_system/apis/message_api.py index 7794ee819..7cf9dc04f 100644 --- a/src/plugin_system/apis/message_api.py +++ b/src/plugin_system/apis/message_api.py @@ -207,7 +207,7 @@ def get_random_chat_messages( def get_messages_by_time_for_users( - start_time: float, end_time: float, person_ids: list, limit: int = 0, limit_mode: str = "latest" + start_time: float, end_time: float, person_ids: List[str], limit: int = 0, limit_mode: str = "latest" ) -> List[Dict[str, Any]]: """ 获取指定用户在所有聊天中指定时间范围内的消息 @@ -287,7 +287,7 @@ def get_messages_before_time_in_chat( return get_raw_msg_before_timestamp_with_chat(chat_id, timestamp, limit) -def get_messages_before_time_for_users(timestamp: float, person_ids: list, limit: int = 0) -> List[Dict[str, Any]]: +def get_messages_before_time_for_users(timestamp: float, person_ids: List[str], limit: int = 0) -> List[Dict[str, Any]]: """ 获取指定用户在指定时间戳之前的消息 @@ -372,7 +372,7 @@ def count_new_messages(chat_id: str, start_time: float = 0.0, end_time: Optional return num_new_messages_since(chat_id, start_time, end_time) -def count_new_messages_for_users(chat_id: str, start_time: float, end_time: float, person_ids: list) -> int: +def count_new_messages_for_users(chat_id: str, start_time: float, end_time: float, person_ids: List[str]) -> int: """ 计算指定聊天中指定用户从开始时间到结束时间的新消息数量 diff --git a/src/plugin_system/apis/plugin_manage_api.py b/src/plugin_system/apis/plugin_manage_api.py index 1c01119b2..693e42b44 100644 --- a/src/plugin_system/apis/plugin_manage_api.py +++ b/src/plugin_system/apis/plugin_manage_api.py @@ -1,10 +1,12 @@ from typing import Tuple, List + + def list_loaded_plugins() -> List[str]: """ 列出所有当前加载的插件。 Returns: - list: 当前加载的插件名称列表。 + List[str]: 当前加载的插件名称列表。 """ from src.plugin_system.core.plugin_manager import plugin_manager @@ -16,17 +18,38 @@ def list_registered_plugins() -> List[str]: 列出所有已注册的插件。 Returns: - list: 已注册的插件名称列表。 + List[str]: 已注册的插件名称列表。 """ from src.plugin_system.core.plugin_manager import plugin_manager return plugin_manager.list_registered_plugins() +def get_plugin_path(plugin_name: str) -> str: + """ + 获取指定插件的路径。 + + Args: + plugin_name (str): 插件名称。 + + Returns: + str: 插件目录的绝对路径。 + + Raises: + ValueError: 如果插件不存在。 + """ + from src.plugin_system.core.plugin_manager import plugin_manager + + if plugin_path := plugin_manager.get_plugin_path(plugin_name): + return plugin_path + else: + raise ValueError(f"插件 '{plugin_name}' 不存在。") + + async def remove_plugin(plugin_name: str) -> bool: """ 卸载指定的插件。 - + **此函数是异步的,确保在异步环境中调用。** Args: @@ -43,7 +66,7 @@ async def remove_plugin(plugin_name: str) -> bool: async def reload_plugin(plugin_name: str) -> bool: """ 重新加载指定的插件。 - + **此函数是异步的,确保在异步环境中调用。** Args: @@ -71,6 +94,7 @@ def load_plugin(plugin_name: str) -> Tuple[bool, int]: return plugin_manager.load_registered_plugin_classes(plugin_name) + def add_plugin_directory(plugin_directory: str) -> bool: """ 添加插件目录。 @@ -84,6 +108,7 @@ def add_plugin_directory(plugin_directory: str) -> bool: return plugin_manager.add_plugin_directory(plugin_directory) + def rescan_plugin_directory() -> Tuple[int, int]: """ 重新扫描插件目录,加载新插件。 @@ -92,4 +117,4 @@ def rescan_plugin_directory() -> Tuple[int, int]: """ from src.plugin_system.core.plugin_manager import plugin_manager - return plugin_manager.rescan_plugin_directory() \ No newline at end of file + return plugin_manager.rescan_plugin_directory() diff --git a/src/plugin_system/apis/send_api.py b/src/plugin_system/apis/send_api.py index 352ccdb45..46b3bddd7 100644 --- a/src/plugin_system/apis/send_api.py +++ b/src/plugin_system/apis/send_api.py @@ -22,7 +22,6 @@ import traceback import time import difflib -import re from typing import Optional, Union from src.common.logger import get_logger @@ -30,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 +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 @@ -50,7 +49,7 @@ async def _send_to_target( display_message: str = "", typing: bool = False, reply_to: str = "", - reply_to_platform_id: str = "", + reply_to_platform_id: Optional[str] = None, storage_message: bool = True, show_log: bool = True, ) -> bool: @@ -61,8 +60,11 @@ async def _send_to_target( content: 消息内容 stream_id: 目标流ID display_message: 显示消息 - typing: 是否显示正在输入 - reply_to: 回复消息的格式,如"发送者:消息内容" + typing: 是否模拟打字等待。 + reply_to: 回复消息,格式为"发送者:消息内容" + reply_to_platform_id: 回复消息,格式为"平台:用户ID",如果不提供则自动查找(插件开发者禁用!) + storage_message: 是否存储消息到数据库 + show_log: 发送是否显示日志 Returns: bool: 是否发送成功 @@ -98,6 +100,10 @@ async def _send_to_target( anchor_message = None if reply_to: anchor_message = await _find_reply_message(target_stream, reply_to) + if anchor_message and anchor_message.message_info.user_info and not reply_to_platform_id: + reply_to_platform_id = ( + f"{anchor_message.message_info.platform}:{anchor_message.message_info.user_info.user_id}" + ) # 构建发送消息对象 bot_message = MessageSending( @@ -183,32 +189,8 @@ async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageR if person_name == sender: translate_text = message["processed_plain_text"] - # 检查是否有 回复 字段 - reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" - if match := re.search(reply_pattern, translate_text): - aaa = match.group(1) - bbb = match.group(2) - reply_person_id = get_person_info_manager().get_person_id(platform, bbb) - reply_person_name = await get_person_info_manager().get_value(reply_person_id, "person_name") or aaa - # 在内容前加上回复信息 - translate_text = re.sub(reply_pattern, f"回复 {reply_person_name}", translate_text, count=1) - - # 检查是否有 @ 字段 - at_pattern = r"@<([^:<>]+):([^:<>]+)>" - at_matches = list(re.finditer(at_pattern, translate_text)) - if at_matches: - new_content = "" - last_end = 0 - for m in at_matches: - new_content += translate_text[last_end : m.start()] - aaa = m.group(1) - bbb = m.group(2) - at_person_id = get_person_info_manager().get_person_id(platform, bbb) - at_person_name = await get_person_info_manager().get_value(at_person_id, "person_name") or aaa - new_content += f"@{at_person_name}" - last_end = m.end() - new_content += translate_text[last_end:] - translate_text = new_content + # 使用独立函数处理用户引用格式 + translate_text = await replace_user_references_async(translate_text, platform) similarity = difflib.SequenceMatcher(None, text, translate_text).ratio() if similarity >= 0.9: @@ -287,12 +269,22 @@ async def text_to_stream( stream_id: 聊天流ID typing: 是否显示正在输入 reply_to: 回复消息,格式为"发送者:消息内容" + reply_to_platform_id: 回复消息,格式为"平台:用户ID",如果不提供则自动查找(插件开发者禁用!) storage_message: 是否存储消息到数据库 Returns: bool: 是否发送成功 """ - return await _send_to_target("text", text, stream_id, "", typing, reply_to, reply_to_platform_id, storage_message) + return await _send_to_target( + "text", + text, + stream_id, + "", + typing, + reply_to, + reply_to_platform_id=reply_to_platform_id, + storage_message=storage_message, + ) async def emoji_to_stream(emoji_base64: str, stream_id: str, storage_message: bool = True) -> bool: @@ -375,249 +367,3 @@ async def custom_to_stream( storage_message=storage_message, show_log=show_log, ) - - -async def text_to_group( - text: str, - group_id: str, - platform: str = "qq", - typing: bool = False, - reply_to: str = "", - storage_message: bool = True, -) -> bool: - """向群聊发送文本消息 - - Args: - text: 要发送的文本内容 - group_id: 群聊ID - platform: 平台,默认为"qq" - typing: 是否显示正在输入 - reply_to: 回复消息,格式为"发送者:消息内容" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, group_id, True) - - return await _send_to_target("text", text, stream_id, "", typing, reply_to, storage_message=storage_message) - - -async def text_to_user( - text: str, - user_id: str, - platform: str = "qq", - typing: bool = False, - reply_to: str = "", - storage_message: bool = True, -) -> bool: - """向用户发送私聊文本消息 - - Args: - text: 要发送的文本内容 - user_id: 用户ID - platform: 平台,默认为"qq" - typing: 是否显示正在输入 - reply_to: 回复消息,格式为"发送者:消息内容" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, user_id, False) - return await _send_to_target("text", text, stream_id, "", typing, reply_to, storage_message=storage_message) - - -async def emoji_to_group(emoji_base64: str, group_id: str, platform: str = "qq", storage_message: bool = True) -> bool: - """向群聊发送表情包 - - Args: - emoji_base64: 表情包的base64编码 - group_id: 群聊ID - platform: 平台,默认为"qq" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, group_id, True) - return await _send_to_target("emoji", emoji_base64, stream_id, "", typing=False, storage_message=storage_message) - - -async def emoji_to_user(emoji_base64: str, user_id: str, platform: str = "qq", storage_message: bool = True) -> bool: - """向用户发送表情包 - - Args: - emoji_base64: 表情包的base64编码 - user_id: 用户ID - platform: 平台,默认为"qq" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, user_id, False) - return await _send_to_target("emoji", emoji_base64, stream_id, "", typing=False, storage_message=storage_message) - - -async def image_to_group(image_base64: str, group_id: str, platform: str = "qq", storage_message: bool = True) -> bool: - """向群聊发送图片 - - Args: - image_base64: 图片的base64编码 - group_id: 群聊ID - platform: 平台,默认为"qq" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, group_id, True) - return await _send_to_target("image", image_base64, stream_id, "", typing=False, storage_message=storage_message) - - -async def image_to_user(image_base64: str, user_id: str, platform: str = "qq", storage_message: bool = True) -> bool: - """向用户发送图片 - - Args: - image_base64: 图片的base64编码 - user_id: 用户ID - platform: 平台,默认为"qq" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, user_id, False) - return await _send_to_target("image", image_base64, stream_id, "", typing=False) - - -async def command_to_group(command: str, group_id: str, platform: str = "qq", storage_message: bool = True) -> bool: - """向群聊发送命令 - - Args: - command: 命令 - group_id: 群聊ID - platform: 平台,默认为"qq" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, group_id, True) - return await _send_to_target("command", command, stream_id, "", typing=False, storage_message=storage_message) - - -async def command_to_user(command: str, user_id: str, platform: str = "qq", storage_message: bool = True) -> bool: - """向用户发送命令 - - Args: - command: 命令 - user_id: 用户ID - platform: 平台,默认为"qq" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, user_id, False) - return await _send_to_target("command", command, stream_id, "", typing=False, storage_message=storage_message) - - -# ============================================================================= -# 通用发送函数 - 支持任意消息类型 -# ============================================================================= - - -async def custom_to_group( - message_type: str, - content: str, - group_id: str, - platform: str = "qq", - display_message: str = "", - typing: bool = False, - reply_to: str = "", - storage_message: bool = True, -) -> bool: - """向群聊发送自定义类型消息 - - Args: - message_type: 消息类型,如"text"、"image"、"emoji"、"video"、"file"等 - content: 消息内容(通常是base64编码或文本) - group_id: 群聊ID - platform: 平台,默认为"qq" - display_message: 显示消息 - typing: 是否显示正在输入 - reply_to: 回复消息,格式为"发送者:消息内容" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, group_id, True) - return await _send_to_target( - message_type, content, stream_id, display_message, typing, reply_to, storage_message=storage_message - ) - - -async def custom_to_user( - message_type: str, - content: str, - user_id: str, - platform: str = "qq", - display_message: str = "", - typing: bool = False, - reply_to: str = "", - storage_message: bool = True, -) -> bool: - """向用户发送自定义类型消息 - - Args: - message_type: 消息类型,如"text"、"image"、"emoji"、"video"、"file"等 - content: 消息内容(通常是base64编码或文本) - user_id: 用户ID - platform: 平台,默认为"qq" - display_message: 显示消息 - typing: 是否显示正在输入 - reply_to: 回复消息,格式为"发送者:消息内容" - - Returns: - bool: 是否发送成功 - """ - stream_id = get_chat_manager().get_stream_id(platform, user_id, False) - return await _send_to_target( - message_type, content, stream_id, display_message, typing, reply_to, storage_message=storage_message - ) - - -async def custom_message( - message_type: str, - content: str, - target_id: str, - is_group: bool = True, - platform: str = "qq", - display_message: str = "", - typing: bool = False, - reply_to: str = "", - storage_message: bool = True, -) -> bool: - """发送自定义消息的通用接口 - - Args: - message_type: 消息类型,如"text"、"image"、"emoji"、"video"、"file"、"audio"等 - content: 消息内容 - target_id: 目标ID(群ID或用户ID) - is_group: 是否为群聊,True为群聊,False为私聊 - platform: 平台,默认为"qq" - display_message: 显示消息 - typing: 是否显示正在输入 - reply_to: 回复消息,格式为"发送者:消息内容" - - Returns: - bool: 是否发送成功 - - 示例: - # 发送视频到群聊 - await send_api.custom_message("video", video_base64, "123456", True) - - # 发送文件到用户 - await send_api.custom_message("file", file_base64, "987654", False) - - # 发送音频到群聊并回复特定消息 - await send_api.custom_message("audio", audio_base64, "123456", True, reply_to="张三:你好") - """ - stream_id = get_chat_manager().get_stream_id(platform, target_id, is_group) - return await _send_to_target( - message_type, content, stream_id, display_message, typing, reply_to, storage_message=storage_message - ) diff --git a/src/plugin_system/apis/tool_api.py b/src/plugin_system/apis/tool_api.py new file mode 100644 index 000000000..a6704126d --- /dev/null +++ b/src/plugin_system/apis/tool_api.py @@ -0,0 +1,27 @@ +from typing import Optional, Type +from src.plugin_system.base.base_tool import BaseTool +from src.plugin_system.base.component_types import ComponentType + +from src.common.logger import get_logger + +logger = get_logger("tool_api") + + +def get_tool_instance(tool_name: str) -> Optional[BaseTool]: + """获取公开工具实例""" + from src.plugin_system.core import component_registry + + tool_class: Type[BaseTool] = component_registry.get_component_class(tool_name, ComponentType.TOOL) # type: ignore + return tool_class() if tool_class else None + + +def get_llm_available_tool_definitions(): + """获取LLM可用的工具定义列表 + + Returns: + List[Tuple[str, Dict[str, Any]]]: 工具定义列表,为[("tool_name", 定义)] + """ + from src.plugin_system.core import component_registry + + llm_available_tools = component_registry.get_llm_available_tools() + return [(name, tool_class.get_tool_definition()) for name, tool_class in llm_available_tools.items()] diff --git a/src/plugin_system/apis/utils_api.py b/src/plugin_system/apis/utils_api.py deleted file mode 100644 index 45996df5c..000000000 --- a/src/plugin_system/apis/utils_api.py +++ /dev/null @@ -1,168 +0,0 @@ -"""工具类API模块 - -提供了各种辅助功能 -使用方式: - from src.plugin_system.apis import utils_api - plugin_path = utils_api.get_plugin_path() - data = utils_api.read_json_file("data.json") - timestamp = utils_api.get_timestamp() -""" - -import os -import json -import time -import inspect -import datetime -import uuid -from typing import Any, Optional -from src.common.logger import get_logger - -logger = get_logger("utils_api") - - -# ============================================================================= -# 文件操作API函数 -# ============================================================================= - - -def get_plugin_path(caller_frame=None) -> str: - """获取调用者插件的路径 - - Args: - caller_frame: 调用者的栈帧,默认为None(自动获取) - - Returns: - str: 插件目录的绝对路径 - """ - try: - if caller_frame is None: - caller_frame = inspect.currentframe().f_back # type: ignore - - plugin_module_path = inspect.getfile(caller_frame) # type: ignore - plugin_dir = os.path.dirname(plugin_module_path) - return plugin_dir - except Exception as e: - logger.error(f"[UtilsAPI] 获取插件路径失败: {e}") - return "" - - -def read_json_file(file_path: str, default: Any = None) -> Any: - """读取JSON文件 - - Args: - file_path: 文件路径,可以是相对于插件目录的路径 - default: 如果文件不存在或读取失败时返回的默认值 - - Returns: - Any: JSON数据或默认值 - """ - try: - # 如果是相对路径,则相对于调用者的插件目录 - if not os.path.isabs(file_path): - caller_frame = inspect.currentframe().f_back # type: ignore - plugin_dir = get_plugin_path(caller_frame) - file_path = os.path.join(plugin_dir, file_path) - - if not os.path.exists(file_path): - logger.warning(f"[UtilsAPI] 文件不存在: {file_path}") - return default - - with open(file_path, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - logger.error(f"[UtilsAPI] 读取JSON文件出错: {e}") - return default - - -def write_json_file(file_path: str, data: Any, indent: int = 2) -> bool: - """写入JSON文件 - - Args: - file_path: 文件路径,可以是相对于插件目录的路径 - data: 要写入的数据 - indent: JSON缩进 - - Returns: - bool: 是否写入成功 - """ - try: - # 如果是相对路径,则相对于调用者的插件目录 - if not os.path.isabs(file_path): - caller_frame = inspect.currentframe().f_back # type: ignore - plugin_dir = get_plugin_path(caller_frame) - file_path = os.path.join(plugin_dir, file_path) - - # 确保目录存在 - os.makedirs(os.path.dirname(file_path), exist_ok=True) - - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=indent) - return True - except Exception as e: - logger.error(f"[UtilsAPI] 写入JSON文件出错: {e}") - return False - - -# ============================================================================= -# 时间相关API函数 -# ============================================================================= - - -def get_timestamp() -> int: - """获取当前时间戳 - - Returns: - int: 当前时间戳(秒) - """ - return int(time.time()) - - -def format_time(timestamp: Optional[int | float] = None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str: - """格式化时间 - - Args: - timestamp: 时间戳,如果为None则使用当前时间 - format_str: 时间格式字符串 - - Returns: - str: 格式化后的时间字符串 - """ - try: - if timestamp is None: - timestamp = time.time() - return datetime.datetime.fromtimestamp(timestamp).strftime(format_str) - except Exception as e: - logger.error(f"[UtilsAPI] 格式化时间失败: {e}") - return "" - - -def parse_time(time_str: str, format_str: str = "%Y-%m-%d %H:%M:%S") -> int: - """解析时间字符串为时间戳 - - Args: - time_str: 时间字符串 - format_str: 时间格式字符串 - - Returns: - int: 时间戳(秒) - """ - try: - dt = datetime.datetime.strptime(time_str, format_str) - return int(dt.timestamp()) - except Exception as e: - logger.error(f"[UtilsAPI] 解析时间失败: {e}") - return 0 - - -# ============================================================================= -# 其他工具函数 -# ============================================================================= - - -def generate_unique_id() -> str: - """生成唯一ID - - Returns: - str: 唯一ID - """ - return str(uuid.uuid4()) diff --git a/src/plugin_system/base/__init__.py b/src/plugin_system/base/__init__.py index a95e05aed..b9a2893e4 100644 --- a/src/plugin_system/base/__init__.py +++ b/src/plugin_system/base/__init__.py @@ -6,6 +6,7 @@ from .base_plugin import BasePlugin from .base_action import BaseAction +from .base_tool import BaseTool from .base_command import BaseCommand from .base_events_handler import BaseEventHandler from .component_types import ( @@ -15,6 +16,7 @@ from .component_types import ( ComponentInfo, ActionInfo, CommandInfo, + ToolInfo, PluginInfo, PythonDependency, EventHandlerInfo, @@ -27,12 +29,14 @@ __all__ = [ "BasePlugin", "BaseAction", "BaseCommand", + "BaseTool", "ComponentType", "ActionActivationType", "ChatMode", "ComponentInfo", "ActionInfo", "CommandInfo", + "ToolInfo", "PluginInfo", "PythonDependency", "ConfigField", diff --git a/src/plugin_system/base/base_action.py b/src/plugin_system/base/base_action.py index c108c5d86..66d723f5e 100644 --- a/src/plugin_system/base/base_action.py +++ b/src/plugin_system/base/base_action.py @@ -208,7 +208,7 @@ class BaseAction(ABC): return False, f"等待新消息失败: {str(e)}" async def send_text( - self, content: str, reply_to: str = "", reply_to_platform_id: str = "", typing: bool = False + self, content: str, reply_to: str = "", typing: bool = False ) -> bool: """发送文本消息 @@ -227,7 +227,6 @@ class BaseAction(ABC): text=content, stream_id=self.chat_id, reply_to=reply_to, - reply_to_platform_id=reply_to_platform_id, typing=typing, ) @@ -384,7 +383,7 @@ class BaseAction(ABC): keyword_case_sensitive=getattr(cls, "keyword_case_sensitive", False), mode_enable=getattr(cls, "mode_enable", ChatMode.ALL), parallel_action=getattr(cls, "parallel_action", True), - random_activation_probability=getattr(cls, "random_activation_probability", 0.3), + random_activation_probability=getattr(cls, "random_activation_probability", 0.0), llm_judge_prompt=getattr(cls, "llm_judge_prompt", ""), # 使用正确的字段名 action_parameters=getattr(cls, "action_parameters", {}).copy(), diff --git a/src/plugin_system/base/base_command.py b/src/plugin_system/base/base_command.py index 60ee99add..652acb4c4 100644 --- a/src/plugin_system/base/base_command.py +++ b/src/plugin_system/base/base_command.py @@ -60,10 +60,10 @@ class BaseCommand(ABC): pass def get_config(self, key: str, default=None): - """获取插件配置值,支持嵌套键访问 + """获取插件配置值,使用嵌套键访问 Args: - key: 配置键名,支持嵌套访问如 "section.subsection.key" + key: 配置键名,使用嵌套访问如 "section.subsection.key" default: 默认值 Returns: diff --git a/src/plugin_system/base/base_tool.py b/src/plugin_system/base/base_tool.py new file mode 100644 index 000000000..3e21e25a6 --- /dev/null +++ b/src/plugin_system/base/base_tool.py @@ -0,0 +1,85 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict +from rich.traceback import install + +from src.common.logger import get_logger +from src.plugin_system.base.component_types import ComponentType, ToolInfo + +install(extra_lines=3) + +logger = get_logger("base_tool") + + +class BaseTool(ABC): + """所有工具的基类""" + + name: str = "" + """工具的名称""" + description: str = "" + """工具的描述""" + parameters: Dict[str, Any] = {} + """工具的参数定义""" + available_for_llm: bool = False + """是否可供LLM使用""" + + @classmethod + def get_tool_definition(cls) -> dict[str, Any]: + """获取工具定义,用于LLM工具调用 + + Returns: + dict: 工具定义字典 + """ + if not cls.name or not cls.description or not cls.parameters: + raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性") + + return { + "type": "function", + "function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters}, + } + + @classmethod + def get_tool_info(cls) -> ToolInfo: + """获取工具信息""" + if not cls.name or not cls.description or not cls.parameters: + raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性") + + return ToolInfo( + name=cls.name, + tool_description=cls.description, + enabled=cls.available_for_llm, + tool_parameters=cls.parameters, + component_type=ComponentType.TOOL, + ) + + @abstractmethod + async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: + """执行工具函数(供llm调用) + 通过该方法,maicore会通过llm的tool call来调用工具 + 传入的是json格式的参数,符合parameters定义的格式 + + Args: + function_args: 工具调用参数 + + Returns: + dict: 工具执行结果 + """ + raise NotImplementedError("子类必须实现execute方法") + + async def direct_execute(self, **function_args: dict[str, Any]) -> dict[str, Any]: + """直接执行工具函数(供插件调用) + 通过该方法,插件可以直接调用工具,而不需要传入字典格式的参数 + 插件可以直接调用此方法,用更加明了的方式传入参数 + 示例: result = await tool.direct_execute(arg1="参数",arg2="参数2") + + 工具开发者可以重写此方法以实现与llm调用差异化的执行逻辑 + + Args: + **function_args: 工具调用参数 + + Returns: + dict: 工具执行结果 + """ + if self.parameters and (missing := [p for p in self.parameters.get("required", []) if p not in function_args]): + raise ValueError(f"工具类 {self.__class__.__name__} 缺少必要参数: {', '.join(missing)}") + + return await self.execute(function_args) diff --git a/src/plugin_system/base/component_types.py b/src/plugin_system/base/component_types.py index eeb2a5a08..aeeccde5a 100644 --- a/src/plugin_system/base/component_types.py +++ b/src/plugin_system/base/component_types.py @@ -10,6 +10,7 @@ class ComponentType(Enum): ACTION = "action" # 动作组件 COMMAND = "command" # 命令组件 + TOOL = "tool" # 服务组件(预留) SCHEDULER = "scheduler" # 定时任务组件(预留) EVENT_HANDLER = "event_handler" # 事件处理组件(预留) @@ -144,7 +145,17 @@ class CommandInfo(ComponentInfo): def __post_init__(self): super().__post_init__() self.component_type = ComponentType.COMMAND + +@dataclass +class ToolInfo(ComponentInfo): + """工具组件信息""" + tool_parameters: Dict[str, Any] = field(default_factory=dict) # 工具参数定义 + tool_description: str = "" # 工具描述 + + def __post_init__(self): + super().__post_init__() + self.component_type = ComponentType.TOOL @dataclass class EventHandlerInfo(ComponentInfo): diff --git a/src/plugin_system/core/__init__.py b/src/plugin_system/core/__init__.py index 3193828bf..eb794a30b 100644 --- a/src/plugin_system/core/__init__.py +++ b/src/plugin_system/core/__init__.py @@ -6,14 +6,12 @@ from src.plugin_system.core.plugin_manager import plugin_manager from src.plugin_system.core.component_registry import component_registry -from src.plugin_system.core.dependency_manager import dependency_manager from src.plugin_system.core.events_manager import events_manager from src.plugin_system.core.global_announcement_manager import global_announcement_manager __all__ = [ "plugin_manager", "component_registry", - "dependency_manager", "events_manager", "global_announcement_manager", ] diff --git a/src/plugin_system/core/component_registry.py b/src/plugin_system/core/component_registry.py index 2ea89b880..59a03b73c 100644 --- a/src/plugin_system/core/component_registry.py +++ b/src/plugin_system/core/component_registry.py @@ -6,6 +6,7 @@ from src.common.logger import get_logger from src.plugin_system.base.component_types import ( ComponentInfo, ActionInfo, + ToolInfo, CommandInfo, EventHandlerInfo, PluginInfo, @@ -13,6 +14,7 @@ from src.plugin_system.base.component_types import ( ) from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.base_action import BaseAction +from src.plugin_system.base.base_tool import BaseTool from src.plugin_system.base.base_events_handler import BaseEventHandler logger = get_logger("component_registry") @@ -30,7 +32,7 @@ class ComponentRegistry: """组件注册表 命名空间式组件名 -> 组件信息""" self._components_by_type: Dict[ComponentType, Dict[str, ComponentInfo]] = {types: {} for types in ComponentType} """类型 -> 组件原名称 -> 组件信息""" - self._components_classes: Dict[str, Type[Union[BaseCommand, BaseAction, BaseEventHandler]]] = {} + self._components_classes: Dict[str, Type[Union[BaseCommand, BaseAction, BaseTool, BaseEventHandler]]] = {} """命名空间式组件名 -> 组件类""" # 插件注册表 @@ -49,6 +51,10 @@ class ComponentRegistry: self._command_patterns: Dict[Pattern, str] = {} """编译后的正则 -> command名""" + # 工具特定注册表 + self._tool_registry: Dict[str, Type[BaseTool]] = {} # 工具名 -> 工具类 + self._llm_available_tools: Dict[str, Type[BaseTool]] = {} # llm可用的工具名 -> 工具类 + # EventHandler特定注册表 self._event_handler_registry: Dict[str, Type[BaseEventHandler]] = {} """event_handler名 -> event_handler类""" @@ -79,7 +85,9 @@ class ComponentRegistry: return True def register_component( - self, component_info: ComponentInfo, component_class: Type[Union[BaseCommand, BaseAction, BaseEventHandler]] + self, + component_info: ComponentInfo, + component_class: Type[Union[BaseCommand, BaseAction, BaseEventHandler, BaseTool]], ) -> bool: """注册组件 @@ -125,6 +133,10 @@ class ComponentRegistry: assert isinstance(component_info, CommandInfo) assert issubclass(component_class, BaseCommand) ret = self._register_command_component(component_info, component_class) + case ComponentType.TOOL: + assert isinstance(component_info, ToolInfo) + assert issubclass(component_class, BaseTool) + ret = self._register_tool_component(component_info, component_class) case ComponentType.EVENT_HANDLER: assert isinstance(component_info, EventHandlerInfo) assert issubclass(component_class, BaseEventHandler) @@ -180,6 +192,18 @@ class ComponentRegistry: return True + def _register_tool_component(self, tool_info: ToolInfo, tool_class: Type[BaseTool]) -> bool: + """注册Tool组件到Tool特定注册表""" + tool_name = tool_info.name + + self._tool_registry[tool_name] = tool_class + + # 如果是llm可用的且启用的工具,添加到 llm可用工具列表 + if tool_info.enabled: + self._llm_available_tools[tool_name] = tool_class + + return True + def _register_event_handler_component( self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler] ) -> bool: @@ -222,6 +246,9 @@ class ComponentRegistry: keys_to_remove = [k for k, v in self._command_patterns.items() if v == component_name] for key in keys_to_remove: self._command_patterns.pop(key) + case ComponentType.TOOL: + self._tool_registry.pop(component_name) + self._llm_available_tools.pop(component_name) case ComponentType.EVENT_HANDLER: from .events_manager import events_manager # 延迟导入防止循环导入问题 @@ -234,13 +261,13 @@ class ComponentRegistry: self._components_classes.pop(namespaced_name) logger.info(f"组件 {component_name} 已移除") return True - except KeyError: - logger.warning(f"移除组件时未找到组件: {component_name}") + except KeyError as e: + logger.warning(f"移除组件时未找到组件: {component_name}, 发生错误: {e}") return False except Exception as e: logger.error(f"移除组件 {component_name} 时发生错误: {e}") return False - + def remove_plugin_registry(self, plugin_name: str) -> bool: """移除插件注册信息 @@ -281,6 +308,10 @@ class ComponentRegistry: assert isinstance(target_component_info, CommandInfo) pattern = target_component_info.command_pattern self._command_patterns[re.compile(pattern)] = component_name + case ComponentType.TOOL: + assert isinstance(target_component_info, ToolInfo) + assert issubclass(target_component_class, BaseTool) + self._llm_available_tools[component_name] = target_component_class case ComponentType.EVENT_HANDLER: assert isinstance(target_component_info, EventHandlerInfo) assert issubclass(target_component_class, BaseEventHandler) @@ -308,20 +339,29 @@ class ComponentRegistry: logger.warning(f"组件 {component_name} 未注册,无法禁用") return False target_component_info.enabled = False - match component_type: - case ComponentType.ACTION: - self._default_actions.pop(component_name, None) - case ComponentType.COMMAND: - self._command_patterns = {k: v for k, v in self._command_patterns.items() if v != component_name} - case ComponentType.EVENT_HANDLER: - self._enabled_event_handlers.pop(component_name, None) - from .events_manager import events_manager # 延迟导入防止循环导入问题 + try: + match component_type: + case ComponentType.ACTION: + self._default_actions.pop(component_name) + case ComponentType.COMMAND: + self._command_patterns = {k: v for k, v in self._command_patterns.items() if v != component_name} + case ComponentType.TOOL: + self._llm_available_tools.pop(component_name) + case ComponentType.EVENT_HANDLER: + self._enabled_event_handlers.pop(component_name) + from .events_manager import events_manager # 延迟导入防止循环导入问题 - await events_manager.unregister_event_subscriber(component_name) - self._components[component_name].enabled = False - self._components_by_type[component_type][component_name].enabled = False - logger.info(f"组件 {component_name} 已禁用") - return True + await events_manager.unregister_event_subscriber(component_name) + self._components[component_name].enabled = False + self._components_by_type[component_type][component_name].enabled = False + logger.info(f"组件 {component_name} 已禁用") + return True + except KeyError as e: + logger.warning(f"禁用组件时未找到组件或已禁用: {component_name}, 发生错误: {e}") + return False + except Exception as e: + logger.error(f"禁用组件 {component_name} 时发生错误: {e}") + return False # === 组件查询方法 === def get_component_info( @@ -371,7 +411,7 @@ class ComponentRegistry: self, component_name: str, component_type: Optional[ComponentType] = None, - ) -> Optional[Union[Type[BaseCommand], Type[BaseAction], Type[BaseEventHandler]]]: + ) -> Optional[Union[Type[BaseCommand], Type[BaseAction], Type[BaseEventHandler], Type[BaseTool]]]: """获取组件类,支持自动命名空间解析 Args: @@ -476,6 +516,27 @@ class ComponentRegistry: command_info, ) + # === Tool 特定查询方法 === + def get_tool_registry(self) -> Dict[str, Type[BaseTool]]: + """获取Tool注册表""" + return self._tool_registry.copy() + + def get_llm_available_tools(self) -> Dict[str, Type[BaseTool]]: + """获取LLM可用的Tool列表""" + return self._llm_available_tools.copy() + + def get_registered_tool_info(self, tool_name: str) -> Optional[ToolInfo]: + """获取Tool信息 + + Args: + tool_name: 工具名称 + + Returns: + ToolInfo: 工具信息对象,如果工具不存在则返回 None + """ + info = self.get_component_info(tool_name, ComponentType.TOOL) + return info if isinstance(info, ToolInfo) else None + # === EventHandler 特定查询方法 === def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]: @@ -529,17 +590,21 @@ class ComponentRegistry: """获取注册中心统计信息""" action_components: int = 0 command_components: int = 0 + tool_components: int = 0 events_handlers: int = 0 for component in self._components.values(): if component.component_type == ComponentType.ACTION: action_components += 1 elif component.component_type == ComponentType.COMMAND: command_components += 1 + elif component.component_type == ComponentType.TOOL: + tool_components += 1 elif component.component_type == ComponentType.EVENT_HANDLER: events_handlers += 1 return { "action_components": action_components, "command_components": command_components, + "tool_components": tool_components, "event_handlers": events_handlers, "total_components": len(self._components), "total_plugins": len(self._plugins), diff --git a/src/plugin_system/core/dependency_manager.py b/src/plugin_system/core/dependency_manager.py deleted file mode 100644 index 266254e72..000000000 --- a/src/plugin_system/core/dependency_manager.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -插件依赖管理器 - -负责检查和安装插件的Python包依赖 -""" - -import subprocess -import sys -import importlib -from typing import List, Dict, Tuple, Any - -from src.common.logger import get_logger -from src.plugin_system.base.component_types import PythonDependency - -logger = get_logger("dependency_manager") - - -class DependencyManager: - """依赖管理器""" - - def __init__(self): - self.install_log: List[str] = [] - self.failed_installs: Dict[str, str] = {} - - def check_dependencies( - self, dependencies: List[PythonDependency] - ) -> Tuple[List[PythonDependency], List[PythonDependency]]: - """检查依赖包状态 - - Args: - dependencies: 依赖包列表 - - Returns: - Tuple[List[PythonDependency], List[PythonDependency]]: (缺失的依赖, 可选缺失的依赖) - """ - missing_required = [] - missing_optional = [] - - for dep in dependencies: - if self._is_package_available(dep.package_name): - logger.debug(f"依赖包已存在: {dep.package_name}") - elif dep.optional: - missing_optional.append(dep) - logger.warning(f"可选依赖包缺失: {dep.package_name} - {dep.description}") - else: - missing_required.append(dep) - logger.error(f"必需依赖包缺失: {dep.package_name} - {dep.description}") - return missing_required, missing_optional - - def _is_package_available(self, package_name: str) -> bool: - """检查包是否可用""" - try: - importlib.import_module(package_name) - return True - except ImportError: - return False - - def install_dependencies(self, dependencies: List[PythonDependency], auto_install: bool = False) -> bool: - """安装依赖包 - - Args: - dependencies: 需要安装的依赖包列表 - auto_install: 是否自动安装(True时不询问用户) - - Returns: - bool: 安装是否成功 - """ - if not dependencies: - return True - - logger.info(f"需要安装 {len(dependencies)} 个依赖包") - - # 显示将要安装的包 - for dep in dependencies: - install_cmd = dep.get_pip_requirement() - logger.info(f" - {install_cmd} {'(可选)' if dep.optional else '(必需)'}") - if dep.description: - logger.info(f" 说明: {dep.description}") - - if not auto_install: - # 这里可以添加用户确认逻辑 - logger.warning("手动安装模式:请手动运行 pip install 命令安装依赖包") - return False - - # 执行安装 - success_count = 0 - for dep in dependencies: - if self._install_single_package(dep): - success_count += 1 - else: - self.failed_installs[dep.package_name] = f"安装失败: {dep.get_pip_requirement()}" - - logger.info(f"依赖安装完成: {success_count}/{len(dependencies)} 个成功") - return success_count == len(dependencies) - - def _install_single_package(self, dependency: PythonDependency) -> bool: - """安装单个包""" - pip_requirement = dependency.get_pip_requirement() - - try: - logger.info(f"正在安装: {pip_requirement}") - - # 使用subprocess安装包 - cmd = [sys.executable, "-m", "pip", "install", pip_requirement] - result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=300, # 5分钟超时 - ) - - if result.returncode == 0: - logger.info(f"✅ 成功安装: {pip_requirement}") - self.install_log.append(f"成功安装: {pip_requirement}") - return True - else: - logger.error(f"❌ 安装失败: {pip_requirement}") - logger.error(f"错误输出: {result.stderr}") - self.install_log.append(f"安装失败: {pip_requirement} - {result.stderr}") - return False - - except subprocess.TimeoutExpired: - logger.error(f"❌ 安装超时: {pip_requirement}") - return False - except Exception as e: - logger.error(f"❌ 安装异常: {pip_requirement} - {str(e)}") - return False - - def generate_requirements_file( - self, plugins_dependencies: List[List[PythonDependency]], output_path: str = "plugin_requirements.txt" - ) -> bool: - """生成插件依赖的requirements文件 - - Args: - plugins_dependencies: 所有插件的依赖列表 - output_path: 输出文件路径 - - Returns: - bool: 生成是否成功 - """ - try: - all_deps = {} - - # 合并所有插件的依赖 - for plugin_deps in plugins_dependencies: - for dep in plugin_deps: - key = dep.install_name - if key in all_deps: - # 如果已存在,可以添加版本兼容性检查逻辑 - existing = all_deps[key] - if dep.version and existing.version != dep.version: - logger.warning(f"依赖版本冲突: {key} ({existing.version} vs {dep.version})") - else: - all_deps[key] = dep - - # 写入requirements文件 - with open(output_path, "w", encoding="utf-8") as f: - f.write("# 插件依赖包自动生成\n") - f.write("# Auto-generated plugin dependencies\n\n") - - # 按包名排序 - sorted_deps = sorted(all_deps.values(), key=lambda x: x.install_name) - - for dep in sorted_deps: - requirement = dep.get_pip_requirement() - if dep.description: - f.write(f"# {dep.description}\n") - if dep.optional: - f.write("# Optional dependency\n") - f.write(f"{requirement}\n\n") - - logger.info(f"已生成插件依赖文件: {output_path} ({len(all_deps)} 个包)") - return True - - except Exception as e: - logger.error(f"生成requirements文件失败: {str(e)}") - return False - - def get_install_summary(self) -> Dict[str, Any]: - """获取安装摘要""" - return { - "install_log": self.install_log.copy(), - "failed_installs": self.failed_installs.copy(), - "total_attempts": len(self.install_log), - "failed_count": len(self.failed_installs), - } - - -# 全局依赖管理器实例 -dependency_manager = DependencyManager() diff --git a/src/plugin_system/core/global_announcement_manager.py b/src/plugin_system/core/global_announcement_manager.py index 9f7052f5d..bb6f06b4f 100644 --- a/src/plugin_system/core/global_announcement_manager.py +++ b/src/plugin_system/core/global_announcement_manager.py @@ -13,6 +13,8 @@ class GlobalAnnouncementManager: self._user_disabled_commands: Dict[str, List[str]] = {} # 用户禁用的事件处理器,chat_id -> [handler_name] self._user_disabled_event_handlers: Dict[str, List[str]] = {} + # 用户禁用的工具,chat_id -> [tool_name] + self._user_disabled_tools: Dict[str, List[str]] = {} def disable_specific_chat_action(self, chat_id: str, action_name: str) -> bool: """禁用特定聊天的某个动作""" @@ -77,6 +79,27 @@ class GlobalAnnouncementManager: return False return False + def disable_specific_chat_tool(self, chat_id: str, tool_name: str) -> bool: + """禁用特定聊天的某个工具""" + if chat_id not in self._user_disabled_tools: + self._user_disabled_tools[chat_id] = [] + if tool_name in self._user_disabled_tools[chat_id]: + logger.warning(f"工具 {tool_name} 已经被禁用") + return False + self._user_disabled_tools[chat_id].append(tool_name) + return True + + def enable_specific_chat_tool(self, chat_id: str, tool_name: str) -> bool: + """启用特定聊天的某个工具""" + if chat_id in self._user_disabled_tools: + try: + self._user_disabled_tools[chat_id].remove(tool_name) + return True + except ValueError: + logger.warning(f"工具 {tool_name} 不在禁用列表中") + return False + return False + def get_disabled_chat_actions(self, chat_id: str) -> List[str]: """获取特定聊天禁用的所有动作""" return self._user_disabled_actions.get(chat_id, []).copy() @@ -88,6 +111,10 @@ class GlobalAnnouncementManager: def get_disabled_chat_event_handlers(self, chat_id: str) -> List[str]: """获取特定聊天禁用的所有事件处理器""" return self._user_disabled_event_handlers.get(chat_id, []).copy() + + def get_disabled_chat_tools(self, chat_id: str) -> List[str]: + """获取特定聊天禁用的所有工具""" + return self._user_disabled_tools.get(chat_id, []).copy() global_announcement_manager = GlobalAnnouncementManager() diff --git a/src/plugin_system/core/plugin_manager.py b/src/plugin_system/core/plugin_manager.py index 8bb005a94..014b7a0cc 100644 --- a/src/plugin_system/core/plugin_manager.py +++ b/src/plugin_system/core/plugin_manager.py @@ -8,10 +8,9 @@ from pathlib import Path from src.common.logger import get_logger from src.plugin_system.base.plugin_base import PluginBase -from src.plugin_system.base.component_types import ComponentType, PythonDependency +from src.plugin_system.base.component_types import ComponentType from src.plugin_system.utils.manifest_utils import VersionComparator from .component_registry import component_registry -from .dependency_manager import dependency_manager logger = get_logger("plugin_manager") @@ -207,104 +206,6 @@ class PluginManager: """ return self.loaded_plugins.get(plugin_name) - def check_all_dependencies(self, auto_install: bool = False) -> Dict[str, Any]: - """检查所有插件的Python依赖包 - - Args: - auto_install: 是否自动安装缺失的依赖包 - - Returns: - Dict[str, any]: 检查结果摘要 - """ - logger.info("开始检查所有插件的Python依赖包...") - - all_required_missing: List[PythonDependency] = [] - all_optional_missing: List[PythonDependency] = [] - plugin_status = {} - - for plugin_name in self.loaded_plugins: - plugin_info = component_registry.get_plugin_info(plugin_name) - if not plugin_info or not plugin_info.python_dependencies: - plugin_status[plugin_name] = {"status": "no_dependencies", "missing": []} - continue - - logger.info(f"检查插件 {plugin_name} 的依赖...") - - missing_required, missing_optional = dependency_manager.check_dependencies(plugin_info.python_dependencies) - - if missing_required: - all_required_missing.extend(missing_required) - plugin_status[plugin_name] = { - "status": "missing_required", - "missing": [dep.package_name for dep in missing_required], - "optional_missing": [dep.package_name for dep in missing_optional], - } - logger.error(f"插件 {plugin_name} 缺少必需依赖: {[dep.package_name for dep in missing_required]}") - elif missing_optional: - all_optional_missing.extend(missing_optional) - plugin_status[plugin_name] = { - "status": "missing_optional", - "missing": [], - "optional_missing": [dep.package_name for dep in missing_optional], - } - logger.warning(f"插件 {plugin_name} 缺少可选依赖: {[dep.package_name for dep in missing_optional]}") - else: - plugin_status[plugin_name] = {"status": "ok", "missing": []} - logger.info(f"插件 {plugin_name} 依赖检查通过") - - # 汇总结果 - total_missing = len({dep.package_name for dep in all_required_missing}) - total_optional_missing = len({dep.package_name for dep in all_optional_missing}) - - logger.info(f"依赖检查完成 - 缺少必需包: {total_missing}个, 缺少可选包: {total_optional_missing}个") - - # 如果需要自动安装 - install_success = True - if auto_install and all_required_missing: - unique_required = {dep.package_name: dep for dep in all_required_missing} - logger.info(f"开始自动安装 {len(unique_required)} 个必需依赖包...") - install_success = dependency_manager.install_dependencies(list(unique_required.values()), auto_install=True) - - return { - "total_plugins_checked": len(plugin_status), - "plugins_with_missing_required": len( - [p for p in plugin_status.values() if p["status"] == "missing_required"] - ), - "plugins_with_missing_optional": len( - [p for p in plugin_status.values() if p["status"] == "missing_optional"] - ), - "total_missing_required": total_missing, - "total_missing_optional": total_optional_missing, - "plugin_status": plugin_status, - "auto_install_attempted": auto_install and bool(all_required_missing), - "auto_install_success": install_success, - "install_summary": dependency_manager.get_install_summary(), - } - - def generate_plugin_requirements(self, output_path: str = "plugin_requirements.txt") -> bool: - """生成所有插件依赖的requirements文件 - - Args: - output_path: 输出文件路径 - - Returns: - bool: 生成是否成功 - """ - logger.info("开始生成插件依赖requirements文件...") - - all_dependencies = [] - - for plugin_name in self.loaded_plugins: - plugin_info = component_registry.get_plugin_info(plugin_name) - if plugin_info and plugin_info.python_dependencies: - all_dependencies.append(plugin_info.python_dependencies) - - if not all_dependencies: - logger.info("没有找到任何插件依赖") - return False - - return dependency_manager.generate_requirements_file(all_dependencies, output_path) - # === 查询方法 === def list_loaded_plugins(self) -> List[str]: """ @@ -323,6 +224,18 @@ class PluginManager: list: 已注册的插件类名称列表。 """ return list(self.plugin_classes.keys()) + + def get_plugin_path(self, plugin_name: str) -> Optional[str]: + """ + 获取指定插件的路径。 + + Args: + plugin_name: 插件名称 + + Returns: + Optional[str]: 插件目录的绝对路径,如果插件不存在则返回None。 + """ + return self.plugin_paths.get(plugin_name) # === 私有方法 === # == 目录管理 == @@ -388,6 +301,7 @@ class PluginManager: return False module = module_from_spec(spec) + module.__package__ = module_name # 设置模块包名 spec.loader.exec_module(module) logger.debug(f"插件模块加载成功: {plugin_file}") @@ -444,6 +358,7 @@ class PluginManager: stats = component_registry.get_registry_stats() action_count = stats.get("action_components", 0) command_count = stats.get("command_components", 0) + tool_count = stats.get("tool_components", 0) event_handler_count = stats.get("event_handlers", 0) total_components = stats.get("total_components", 0) @@ -451,7 +366,7 @@ class PluginManager: if total_registered > 0: logger.info("🎉 插件系统加载完成!") logger.info( - f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, EventHandler: {event_handler_count})" + f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, Tool: {tool_count}, EventHandler: {event_handler_count})" ) # 显示详细的插件列表 @@ -486,6 +401,9 @@ class PluginManager: command_components = [ c for c in plugin_info.components if c.component_type == ComponentType.COMMAND ] + tool_components = [ + c for c in plugin_info.components if c.component_type == ComponentType.TOOL + ] event_handler_components = [ c for c in plugin_info.components if c.component_type == ComponentType.EVENT_HANDLER ] @@ -497,7 +415,9 @@ class PluginManager: if command_components: command_names = [c.name for c in command_components] logger.info(f" ⚡ Command组件: {', '.join(command_names)}") - + if tool_components: + tool_names = [c.name for c in tool_components] + logger.info(f" 🛠️ Tool组件: {', '.join(tool_names)}") if event_handler_components: event_handler_names = [c.name for c in event_handler_components] logger.info(f" 📢 EventHandler组件: {', '.join(event_handler_names)}") diff --git a/src/tools/tool_executor.py b/src/plugin_system/core/tool_use.py similarity index 84% rename from src/tools/tool_executor.py rename to src/plugin_system/core/tool_use.py index 403ed554f..d7b86b8d6 100644 --- a/src/tools/tool_executor.py +++ b/src/plugin_system/core/tool_use.py @@ -1,14 +1,16 @@ +import json +import time +from typing import List, Dict, Tuple, Optional, Any +from src.plugin_system.apis.tool_api import get_llm_available_tool_definitions, get_tool_instance +from src.plugin_system.core.global_announcement_manager import global_announcement_manager from src.llm_models.utils_model import LLMRequest from src.config.config import global_config -import time -from src.common.logger import get_logger from src.chat.utils.prompt_builder import Prompt, global_prompt_manager -from src.tools.tool_use import ToolUser from src.chat.utils.json_utils import process_llm_tool_calls -from typing import List, Dict, Tuple, Optional from src.chat.message_receive.chat_stream import get_chat_manager +from src.common.logger import get_logger -logger = get_logger("tool_executor") +logger = get_logger("tool_use") def init_tool_executor_prompt(): @@ -28,6 +30,10 @@ If you need to use a tool, please directly call the corresponding tool function. Prompt(tool_executor_prompt, "tool_executor_prompt") +# 初始化提示词 +init_tool_executor_prompt() + + class ToolExecutor: """独立的工具执行器组件 @@ -51,9 +57,6 @@ class ToolExecutor: request_type="tool_executor", ) - # 初始化工具实例 - self.tool_instance = ToolUser() - # 缓存配置 self.enable_cache = enable_cache self.cache_ttl = cache_ttl @@ -73,7 +76,7 @@ class ToolExecutor: return_details: 是否返回详细信息(使用的工具列表和提示词) Returns: - 如果return_details为False: List[Dict] - 工具执行结果列表 + 如果return_details为False: Tuple[List[Dict], List[str], str] - (工具执行结果列表, 空, 空) 如果return_details为True: Tuple[List[Dict], List[str], str] - (结果列表, 使用的工具, 提示词) """ @@ -82,15 +85,15 @@ class ToolExecutor: if cached_result := self._get_from_cache(cache_key): logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行") if not return_details: - return cached_result, [], "使用缓存结果" + return cached_result, [], "" # 从缓存结果中提取工具名称 used_tools = [result.get("tool_name", "unknown") for result in cached_result] - return cached_result, used_tools, "使用缓存结果" + return cached_result, used_tools, "" # 缓存未命中,执行工具调用 # 获取可用工具 - tools = self.tool_instance._define_tools() + tools = self._get_tool_definitions() # 获取当前时间 time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) @@ -112,6 +115,7 @@ class ToolExecutor: # 调用LLM进行工具决策 response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools) + # TODO: 在APIADA加入后完全修复这里! # 解析LLM响应 if len(other_info) == 3: reasoning_content, model_name, tool_calls = other_info @@ -133,6 +137,11 @@ class ToolExecutor: return tool_results, used_tools, prompt else: return tool_results, [], "" + + def _get_tool_definitions(self) -> List[Dict[str, Any]]: + all_tools = get_llm_available_tool_definitions() + user_disabled_tools = global_announcement_manager.get_disabled_chat_tools(self.chat_id) + return [parameters for name, parameters in all_tools if name not in user_disabled_tools] async def _execute_tool_calls(self, tool_calls) -> Tuple[List[Dict], List[str]]: """执行工具调用 @@ -172,7 +181,7 @@ class ToolExecutor: logger.debug(f"{self.log_prefix}执行工具: {tool_name}") # 执行工具 - result = await self.tool_instance._execute_tool_call(tool_call) + result = await self._execute_tool_call(tool_call) if result: tool_info = { @@ -205,6 +214,45 @@ class ToolExecutor: return tool_results, used_tools + async def _execute_tool_call(self, tool_call: Dict[str, Any]) -> Optional[Dict]: + # sourcery skip: use-assigned-variable + """执行单个工具调用 + + Args: + tool_call: 工具调用对象 + + Returns: + Optional[Dict]: 工具调用结果,如果失败则返回None + """ + try: + function_name = tool_call["function"]["name"] + function_args = json.loads(tool_call["function"]["arguments"]) + function_args["llm_called"] = True # 标记为LLM调用 + + # 获取对应工具实例 + tool_instance = get_tool_instance(function_name) + if not tool_instance: + logger.warning(f"未知工具名称: {function_name}") + return None + + # 执行工具 + result = await tool_instance.execute(function_args) + if result: + # 直接使用 function_name 作为 tool_type + tool_type = function_name + + return { + "tool_call_id": tool_call["id"], + "role": "tool", + "name": function_name, + "type": tool_type, + "content": result["content"], + } + return None + except Exception as e: + logger.error(f"执行工具调用时发生错误: {str(e)}") + return None + def _generate_cache_key(self, target_message: str, chat_history: str, sender: str) -> str: """生成缓存键 @@ -272,15 +320,6 @@ class ToolExecutor: if expired_keys: logger.debug(f"{self.log_prefix}清理了{len(expired_keys)}个过期缓存") - def get_available_tools(self) -> List[str]: - """获取可用工具列表 - - Returns: - List[str]: 可用工具名称列表 - """ - tools = self.tool_instance._define_tools() - return [tool.get("function", {}).get("name", "unknown") for tool in tools] - async def execute_specific_tool( self, tool_name: str, tool_args: Dict, validate_args: bool = True ) -> Optional[Dict]: @@ -299,7 +338,7 @@ class ToolExecutor: logger.info(f"{self.log_prefix}直接执行工具: {tool_name}") - result = await self.tool_instance._execute_tool_call(tool_call) + result = await self._execute_tool_call(tool_call) if result: tool_info = { @@ -366,12 +405,8 @@ class ToolExecutor: logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}") -# 初始化提示词 -init_tool_executor_prompt() - - """ -使用示例: +ToolExecutor使用示例: # 1. 基础使用 - 从聊天消息执行工具(启用缓存,默认TTL=3) executor = ToolExecutor(executor_id="my_executor") @@ -400,7 +435,6 @@ result = await executor.execute_specific_tool( ) # 6. 缓存管理 -available_tools = executor.get_available_tools() cache_status = executor.get_cache_status() # 查看缓存状态 executor.clear_cache() # 清空缓存 executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置 diff --git a/src/plugin_system/utils/manifest_utils.py b/src/plugin_system/utils/manifest_utils.py index 6a8aa804b..d070b733c 100644 --- a/src/plugin_system/utils/manifest_utils.py +++ b/src/plugin_system/utils/manifest_utils.py @@ -163,12 +163,11 @@ class VersionComparator: version_normalized, max_normalized ) - if is_compatible: - logger.info(f"版本兼容性检查:{compat_msg}") - return True, compat_msg - else: + if not is_compatible: return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized},且无兼容性映射" + logger.info(f"版本兼容性检查:{compat_msg}") + return True, compat_msg return True, "" @staticmethod @@ -358,14 +357,10 @@ class ManifestValidator: if self.validation_errors: report.append("❌ 验证错误:") - for error in self.validation_errors: - report.append(f" - {error}") - + report.extend(f" - {error}" for error in self.validation_errors) if self.validation_warnings: report.append("⚠️ 验证警告:") - for warning in self.validation_warnings: - report.append(f" - {warning}") - + report.extend(f" - {warning}" for warning in self.validation_warnings) if not self.validation_errors and not self.validation_warnings: report.append("✅ Manifest文件验证通过") diff --git a/src/plugins/built_in/core_actions/_manifest.json b/src/plugins/built_in/core_actions/_manifest.json index ba1b20d6b..d7446497c 100644 --- a/src/plugins/built_in/core_actions/_manifest.json +++ b/src/plugins/built_in/core_actions/_manifest.json @@ -24,11 +24,6 @@ "is_built_in": true, "plugin_type": "action_provider", "components": [ - { - "type": "action", - "name": "reply", - "description": "参与聊天回复,发送文本进行表达" - }, { "type": "action", "name": "no_reply", diff --git a/src/plugins/built_in/core_actions/emoji.py b/src/plugins/built_in/core_actions/emoji.py index 4563b47f8..257686b18 100644 --- a/src/plugins/built_in/core_actions/emoji.py +++ b/src/plugins/built_in/core_actions/emoji.py @@ -9,7 +9,8 @@ from src.common.logger import get_logger # 导入API模块 - 标准Python包方式 from src.plugin_system.apis import emoji_api, llm_api, message_api -from src.plugins.built_in.core_actions.no_reply import NoReplyAction +# 注释:不再需要导入NoReplyAction,因为计数器管理已移至heartFC_chat.py +# from src.plugins.built_in.core_actions.no_reply import NoReplyAction from src.config.config import global_config @@ -20,10 +21,14 @@ class EmojiAction(BaseAction): """表情动作 - 发送表情包""" # 激活设置 - activation_type = ActionActivationType.RANDOM + if global_config.emoji.emoji_activate_type == "llm": + activation_type = ActionActivationType.LLM_JUDGE + random_activation_probability = 0 + else: + activation_type = ActionActivationType.RANDOM + random_activation_probability = global_config.emoji.emoji_chance mode_enable = ChatMode.ALL parallel_action = True - random_activation_probability = 0.2 # 默认值,可通过配置覆盖 # 动作基本信息 action_name = "emoji" @@ -115,7 +120,7 @@ class EmojiAction(BaseAction): logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置,无法调用LLM") return False, "未找到'utils_small'模型配置" - success, chosen_emotion, _, _ = await llm_api.generate_with_model( + success, chosen_emotion = await llm_api.generate_with_model( prompt, model_config=chat_model_config, request_type="emoji" ) @@ -143,8 +148,8 @@ class EmojiAction(BaseAction): logger.error(f"{self.log_prefix} 表情包发送失败") return False, "表情包发送失败" - # 重置NoReplyAction的连续计数器 - NoReplyAction.reset_consecutive_count() + # 注释:重置NoReplyAction的连续计数器现在由heartFC_chat.py统一管理 + # NoReplyAction.reset_consecutive_count() return True, f"发送表情包: {emoji_description}" diff --git a/src/plugins/built_in/core_actions/no_reply.py b/src/plugins/built_in/core_actions/no_reply.py index e9fad9107..f23f4ac74 100644 --- a/src/plugins/built_in/core_actions/no_reply.py +++ b/src/plugins/built_in/core_actions/no_reply.py @@ -1,6 +1,7 @@ import random import time -from typing import Tuple +from typing import Tuple, List +from collections import deque # 导入新插件系统 from src.plugin_system import BaseAction, ActionActivationType, ChatMode @@ -17,11 +18,15 @@ logger = get_logger("no_reply_action") class NoReplyAction(BaseAction): - """不回复动作,根据新消息的兴趣值或数量决定何时结束等待. + """不回复动作,支持waiting和breaking两种形式. - 新的等待逻辑: - 1. 新消息累计兴趣值超过阈值 (默认10) 则结束等待 - 2. 累计新消息数量达到随机阈值 (默认5-10条) 则结束等待 + waiting形式: + - 只要有新消息就结束动作 + - 记录新消息的兴趣度到列表(最多保留最近三项) + - 如果最近三次动作都是no_reply,且最近新消息列表兴趣度之和小于阈值,就进入breaking形式 + + breaking形式: + - 和原有逻辑一致,需要消息满足一定数量或累计一定兴趣值才结束动作 """ focus_activation_type = ActionActivationType.NEVER @@ -35,112 +40,45 @@ class NoReplyAction(BaseAction): # 连续no_reply计数器 _consecutive_count = 0 + + # 最近三次no_reply的新消息兴趣度记录 + _recent_interest_records: deque = deque(maxlen=3) - # 新增:兴趣值退出阈值 + # 兴趣值退出阈值 _interest_exit_threshold = 3.0 - # 新增:消息数量退出阈值 - _min_exit_message_count = 5 - _max_exit_message_count = 10 + # 消息数量退出阈值 + _min_exit_message_count = 3 + _max_exit_message_count = 6 # 动作参数定义 action_parameters = {} # 动作使用场景 - action_require = ["你发送了消息,目前无人回复"] + action_require = [""] # 关联类型 associated_types = [] async def execute(self) -> Tuple[bool, str]: """执行不回复动作""" - import asyncio try: - # 增加连续计数 - NoReplyAction._consecutive_count += 1 - count = NoReplyAction._consecutive_count - reason = self.action_data.get("reason", "") start_time = self.action_data.get("loop_start_time", time.time()) - check_interval = 0.6 # 每秒检查一次 + check_interval = 0.6 - # 随机生成本次等待需要的新消息数量阈值 - exit_message_count_threshold = random.randint(self._min_exit_message_count, self._max_exit_message_count) - logger.info( - f"{self.log_prefix} 本次no_reply需要 {exit_message_count_threshold} 条新消息或累计兴趣值超过 {self._interest_exit_threshold} 才能打断" - ) + # 判断使用哪种形式 + form_type = self._determine_form_type() + + logger.info(f"{self.log_prefix} 选择不回复(第{NoReplyAction._consecutive_count + 1}次),使用{form_type}形式,原因: {reason}") - logger.info(f"{self.log_prefix} 选择不回复(第{count}次),开始摸鱼,原因: {reason}") + # 增加连续计数(在确定要执行no_reply时才增加) + NoReplyAction._consecutive_count += 1 - # 进入等待状态 - while True: - current_time = time.time() - elapsed_time = current_time - start_time - - # 1. 检查新消息 - recent_messages_dict = message_api.get_messages_by_time_in_chat( - chat_id=self.chat_id, - start_time=start_time, - end_time=current_time, - filter_mai=True, - filter_command=True, - ) - new_message_count = len(recent_messages_dict) - - # 2. 检查消息数量是否达到阈值 - talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id) - if new_message_count >= exit_message_count_threshold / talk_frequency: - logger.info( - f"{self.log_prefix} 累计消息数量达到{new_message_count}条(>{exit_message_count_threshold / talk_frequency}),结束等待" - ) - exit_reason = f"{global_config.bot.nickname}(你)看到了{new_message_count}条新消息,可以考虑一下是否要进行回复" - await self.store_action_info( - action_build_into_prompt=False, - action_prompt_display=exit_reason, - action_done=True, - ) - return True, f"累计消息数量达到{new_message_count}条,结束等待 (等待时间: {elapsed_time:.1f}秒)" - - # 3. 检查累计兴趣值 - if new_message_count > 0: - accumulated_interest = 0.0 - for msg_dict in recent_messages_dict: - text = msg_dict.get("processed_plain_text", "") - interest_value = msg_dict.get("interest_value", 0.0) - if text: - accumulated_interest += interest_value - - talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id) - # 只在兴趣值变化时输出log - if not hasattr(self, "_last_accumulated_interest") or accumulated_interest != self._last_accumulated_interest: - logger.info(f"{self.log_prefix} 当前累计兴趣值: {accumulated_interest:.2f}, 当前聊天频率: {talk_frequency:.2f}") - self._last_accumulated_interest = accumulated_interest - - if accumulated_interest >= self._interest_exit_threshold / talk_frequency: - logger.info( - f"{self.log_prefix} 累计兴趣值达到{accumulated_interest:.2f}(>{self._interest_exit_threshold / talk_frequency}),结束等待" - ) - exit_reason = f"{global_config.bot.nickname}(你)感觉到了大家浓厚的兴趣(兴趣值{accumulated_interest:.1f}),决定重新加入讨论" - await self.store_action_info( - action_build_into_prompt=False, - action_prompt_display=exit_reason, - action_done=True, - ) - return ( - True, - f"累计兴趣值达到{accumulated_interest:.2f},结束等待 (等待时间: {elapsed_time:.1f}秒)", - ) - - # 每10秒输出一次等待状态 - if int(elapsed_time) > 0 and int(elapsed_time) % 10 == 0: - logger.debug( - f"{self.log_prefix} 已等待{elapsed_time:.0f}秒,累计{new_message_count}条消息,继续等待..." - ) - # 使用 asyncio.sleep(1) 来避免在同一秒内重复打印日志 - await asyncio.sleep(1) - - # 短暂等待后继续检查 - await asyncio.sleep(check_interval) + if form_type == "waiting": + return await self._execute_waiting_form(start_time, check_interval) + else: + return await self._execute_breaking_form(start_time, check_interval) except Exception as e: logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}") @@ -153,8 +91,191 @@ class NoReplyAction(BaseAction): ) return False, f"不回复动作执行失败: {e}" + def _determine_form_type(self) -> str: + """判断使用哪种形式的no_reply""" + # 如果连续no_reply次数少于3次,使用waiting形式 + if NoReplyAction._consecutive_count < 3: + return "waiting" + + # 如果最近三次记录不足,使用waiting形式 + if len(NoReplyAction._recent_interest_records) < 3: + return "waiting" + + # 计算最近三次记录的兴趣度总和 + total_recent_interest = sum(NoReplyAction._recent_interest_records) + + # 获取当前聊天频率和意愿系数 + talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id) + willing_amplifier = global_config.chat.willing_amplifier + + # 计算调整后的阈值 + adjusted_threshold = self._interest_exit_threshold / talk_frequency / willing_amplifier + + logger.info(f"{self.log_prefix} 最近三次兴趣度总和: {total_recent_interest:.2f}, 调整后阈值: {adjusted_threshold:.2f}") + + # 如果兴趣度总和小于阈值,进入breaking形式 + if total_recent_interest < adjusted_threshold: + logger.info(f"{self.log_prefix} 兴趣度不足,进入breaking形式") + return "breaking" + else: + logger.info(f"{self.log_prefix} 兴趣度充足,继续使用waiting形式") + return "waiting" + + async def _execute_waiting_form(self, start_time: float, check_interval: float) -> Tuple[bool, str]: + """执行waiting形式的no_reply""" + import asyncio + + logger.info(f"{self.log_prefix} 进入waiting形式,等待任何新消息") + + while True: + current_time = time.time() + elapsed_time = current_time - start_time + + # 检查新消息 + recent_messages_dict = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_id, + start_time=start_time, + end_time=current_time, + filter_mai=True, + filter_command=True, + ) + new_message_count = len(recent_messages_dict) + + # waiting形式:只要有新消息就结束 + if new_message_count > 0: + # 计算新消息的总兴趣度 + total_interest = 0.0 + for msg_dict in recent_messages_dict: + interest_value = msg_dict.get("interest_value", 0.0) + if msg_dict.get("processed_plain_text", ""): + total_interest += interest_value * global_config.chat.willing_amplifier + + # 记录到最近兴趣度列表 + NoReplyAction._recent_interest_records.append(total_interest) + + logger.info( + f"{self.log_prefix} waiting形式检测到{new_message_count}条新消息,总兴趣度: {total_interest:.2f},结束等待" + ) + + exit_reason = f"{global_config.bot.nickname}(你)看到了{new_message_count}条新消息,可以考虑一下是否要进行回复" + await self.store_action_info( + action_build_into_prompt=False, + action_prompt_display=exit_reason, + action_done=True, + ) + return True, f"waiting形式检测到{new_message_count}条新消息,结束等待 (等待时间: {elapsed_time:.1f}秒)" + + # 每10秒输出一次等待状态 + if int(elapsed_time) > 0 and int(elapsed_time) % 10 == 0: + logger.debug(f"{self.log_prefix} waiting形式已等待{elapsed_time:.0f}秒,继续等待新消息...") + await asyncio.sleep(1) + + # 短暂等待后继续检查 + await asyncio.sleep(check_interval) + + async def _execute_breaking_form(self, start_time: float, check_interval: float) -> Tuple[bool, str]: + """执行breaking形式的no_reply(原有逻辑)""" + import asyncio + + # 随机生成本次等待需要的新消息数量阈值 + exit_message_count_threshold = random.randint(self._min_exit_message_count, self._max_exit_message_count) + + logger.info(f"{self.log_prefix} 进入breaking形式,需要{exit_message_count_threshold}条消息或足够兴趣度") + + while True: + current_time = time.time() + elapsed_time = current_time - start_time + + # 检查新消息 + recent_messages_dict = message_api.get_messages_by_time_in_chat( + chat_id=self.chat_id, + start_time=start_time, + end_time=current_time, + filter_mai=True, + filter_command=True, + ) + new_message_count = len(recent_messages_dict) + + # 检查消息数量是否达到阈值 + talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id) + modified_exit_count_threshold = (exit_message_count_threshold / talk_frequency) / global_config.chat.willing_amplifier + + if new_message_count >= modified_exit_count_threshold: + # 记录兴趣度到列表 + total_interest = 0.0 + for msg_dict in recent_messages_dict: + interest_value = msg_dict.get("interest_value", 0.0) + if msg_dict.get("processed_plain_text", ""): + total_interest += interest_value * global_config.chat.willing_amplifier + + NoReplyAction._recent_interest_records.append(total_interest) + + logger.info( + f"{self.log_prefix} breaking形式累计消息数量达到{new_message_count}条(>{modified_exit_count_threshold}),结束等待" + ) + exit_reason = f"{global_config.bot.nickname}(你)看到了{new_message_count}条新消息,可以考虑一下是否要进行回复" + await self.store_action_info( + action_build_into_prompt=False, + action_prompt_display=exit_reason, + action_done=True, + ) + return True, f"breaking形式累计消息数量达到{new_message_count}条,结束等待 (等待时间: {elapsed_time:.1f}秒)" + + # 检查累计兴趣值 + if new_message_count > 0: + accumulated_interest = 0.0 + for msg_dict in recent_messages_dict: + text = msg_dict.get("processed_plain_text", "") + interest_value = msg_dict.get("interest_value", 0.0) + if text: + accumulated_interest += interest_value * global_config.chat.willing_amplifier + + # 只在兴趣值变化时输出log + if not hasattr(self, "_last_accumulated_interest") or accumulated_interest != self._last_accumulated_interest: + logger.info(f"{self.log_prefix} breaking形式当前累计兴趣值: {accumulated_interest:.2f}, 当前聊天频率: {talk_frequency:.2f}") + self._last_accumulated_interest = accumulated_interest + + if accumulated_interest >= self._interest_exit_threshold / talk_frequency: + # 记录兴趣度到列表 + NoReplyAction._recent_interest_records.append(accumulated_interest) + + logger.info( + f"{self.log_prefix} breaking形式累计兴趣值达到{accumulated_interest:.2f}(>{self._interest_exit_threshold / talk_frequency}),结束等待" + ) + exit_reason = f"{global_config.bot.nickname}(你)感觉到了大家浓厚的兴趣(兴趣值{accumulated_interest:.1f}),决定重新加入讨论" + await self.store_action_info( + action_build_into_prompt=False, + action_prompt_display=exit_reason, + action_done=True, + ) + return ( + True, + f"breaking形式累计兴趣值达到{accumulated_interest:.2f},结束等待 (等待时间: {elapsed_time:.1f}秒)", + ) + + # 每10秒输出一次等待状态 + if int(elapsed_time) > 0 and int(elapsed_time) % 10 == 0: + logger.debug( + f"{self.log_prefix} breaking形式已等待{elapsed_time:.0f}秒,累计{new_message_count}条消息,继续等待..." + ) + await asyncio.sleep(1) + + # 短暂等待后继续检查 + await asyncio.sleep(check_interval) + @classmethod def reset_consecutive_count(cls): - """重置连续计数器""" + """重置连续计数器和兴趣度记录""" cls._consecutive_count = 0 - logger.debug("NoReplyAction连续计数器已重置") + cls._recent_interest_records.clear() + logger.debug("NoReplyAction连续计数器和兴趣度记录已重置") + + @classmethod + def get_recent_interest_records(cls) -> List[float]: + """获取最近的兴趣度记录""" + return list(cls._recent_interest_records) + + @classmethod + def get_consecutive_count(cls) -> int: + """获取连续计数""" + return cls._consecutive_count diff --git a/src/plugins/built_in/core_actions/plugin.py b/src/plugins/built_in/core_actions/plugin.py index 99bff18aa..9323153d5 100644 --- a/src/plugins/built_in/core_actions/plugin.py +++ b/src/plugins/built_in/core_actions/plugin.py @@ -8,9 +8,8 @@ from typing import List, Tuple, Type # 导入新插件系统 -from src.plugin_system import BasePlugin, register_plugin, ComponentInfo, ActionActivationType +from src.plugin_system import BasePlugin, register_plugin, ComponentInfo from src.plugin_system.base.config_types import ConfigField -from src.config.config import global_config # 导入依赖的系统组件 from src.common.logger import get_logger @@ -18,7 +17,6 @@ from src.common.logger import get_logger # 导入API模块 - 标准Python包方式 from src.plugins.built_in.core_actions.no_reply import NoReplyAction from src.plugins.built_in.core_actions.emoji import EmojiAction -from src.plugins.built_in.core_actions.reply import ReplyAction logger = get_logger("core_actions") @@ -52,10 +50,9 @@ class CoreActionsPlugin(BasePlugin): config_schema: dict = { "plugin": { "enabled": ConfigField(type=bool, default=True, description="是否启用插件"), - "config_version": ConfigField(type=str, default="0.4.0", description="配置文件版本"), + "config_version": ConfigField(type=str, default="0.5.0", description="配置文件版本"), }, "components": { - "enable_reply": ConfigField(type=bool, default=True, description="是否启用回复动作"), "enable_no_reply": ConfigField(type=bool, default=True, description="是否启用不回复动作"), "enable_emoji": ConfigField(type=bool, default=True, description="是否启用发送表情/图片动作"), }, @@ -64,24 +61,12 @@ class CoreActionsPlugin(BasePlugin): def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: """返回插件包含的组件列表""" - if global_config.emoji.emoji_activate_type == "llm": - EmojiAction.random_activation_probability = 0.0 - EmojiAction.focus_activation_type = ActionActivationType.LLM_JUDGE - EmojiAction.normal_activation_type = ActionActivationType.LLM_JUDGE - - elif global_config.emoji.emoji_activate_type == "random": - EmojiAction.random_activation_probability = global_config.emoji.emoji_chance - EmojiAction.focus_activation_type = ActionActivationType.RANDOM - EmojiAction.normal_activation_type = ActionActivationType.RANDOM # --- 根据配置注册组件 --- components = [] - if self.get_config("components.enable_reply", True): - components.append((ReplyAction.get_action_info(), ReplyAction)) if self.get_config("components.enable_no_reply", True): components.append((NoReplyAction.get_action_info(), NoReplyAction)) if self.get_config("components.enable_emoji", True): components.append((EmojiAction.get_action_info(), EmojiAction)) - # components.append((DeepReplyAction.get_action_info(), DeepReplyAction)) return components diff --git a/src/plugins/built_in/core_actions/reply.py b/src/plugins/built_in/core_actions/reply.py deleted file mode 100644 index d73337b29..000000000 --- a/src/plugins/built_in/core_actions/reply.py +++ /dev/null @@ -1,149 +0,0 @@ -# 导入新插件系统 -from src.plugin_system import BaseAction, ActionActivationType, ChatMode -from src.config.config import global_config -import random -import time -from typing import Tuple -import asyncio -import re -import traceback - -# 导入依赖的系统组件 -from src.common.logger import get_logger - -# 导入API模块 - 标准Python包方式 -from src.plugin_system.apis import generator_api, message_api -from src.plugins.built_in.core_actions.no_reply import NoReplyAction -from src.person_info.person_info import get_person_info_manager -from src.mais4u.mai_think import mai_thinking_manager -from src.mais4u.constant_s4u import ENABLE_S4U - -logger = get_logger("reply_action") - - -class ReplyAction(BaseAction): - """回复动作 - 参与聊天回复""" - - # 激活设置 - focus_activation_type = ActionActivationType.NEVER - normal_activation_type = ActionActivationType.NEVER - mode_enable = ChatMode.FOCUS - parallel_action = False - - # 动作基本信息 - action_name = "reply" - action_description = "" - - # 动作参数定义 - action_parameters = {} - - # 动作使用场景 - action_require = [""] - - # 关联类型 - associated_types = ["text"] - - def _parse_reply_target(self, target_message: str) -> tuple: - sender = "" - target = "" - # 添加None检查,防止NoneType错误 - if target_message is None: - return sender, target - if ":" in target_message or ":" in target_message: - # 使用正则表达式匹配中文或英文冒号 - parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1) - if len(parts) == 2: - sender = parts[0].strip() - target = parts[1].strip() - return sender, target - - async def execute(self) -> Tuple[bool, str]: - """执行回复动作""" - logger.debug(f"{self.log_prefix} 决定进行回复") - start_time = self.action_data.get("loop_start_time", time.time()) - - user_id = self.user_id - platform = self.platform - # logger.info(f"{self.log_prefix} 用户ID: {user_id}, 平台: {platform}") - person_id = get_person_info_manager().get_person_id(platform, user_id) # type: ignore - # logger.info(f"{self.log_prefix} 人物ID: {person_id}") - person_name = get_person_info_manager().get_value_sync(person_id, "person_name") - reply_to = f"{person_name}:{self.action_message.get('processed_plain_text', '')}" # type: ignore - logger.info(f"{self.log_prefix} 决定进行回复,目标: {reply_to}") - - try: - if prepared_reply := self.action_data.get("prepared_reply", ""): - reply_text = prepared_reply - else: - try: - success, reply_set, _ = await asyncio.wait_for( - generator_api.generate_reply( - extra_info="", - reply_to=reply_to, - chat_id=self.chat_id, - request_type="chat.replyer.focus", - enable_tool=global_config.tool.enable_in_focus_chat, - ), - timeout=global_config.chat.thinking_timeout, - ) - except asyncio.TimeoutError: - logger.warning(f"{self.log_prefix} 回复生成超时 ({global_config.chat.thinking_timeout}s)") - return False, "timeout" - - # 检查从start_time以来的新消息数量 - # 获取动作触发时间或使用默认值 - current_time = time.time() - new_message_count = message_api.count_new_messages( - chat_id=self.chat_id, start_time=start_time, end_time=current_time - ) - - # 根据新消息数量决定是否使用reply_to - need_reply = new_message_count >= random.randint(2, 4) - if need_reply: - logger.info( - f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复" - ) - else: - logger.debug( - f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,不使用引用回复" - ) - - # 构建回复文本 - reply_text = "" - first_replied = False - reply_to_platform_id = f"{platform}:{user_id}" - for reply_seg in reply_set: - data = reply_seg[1] - if not first_replied: - if need_reply: - await self.send_text( - content=data, reply_to=reply_to, reply_to_platform_id=reply_to_platform_id, typing=False - ) - else: - await self.send_text(content=data, reply_to_platform_id=reply_to_platform_id, typing=False) - first_replied = True - else: - await self.send_text(content=data, reply_to_platform_id=reply_to_platform_id, typing=True) - reply_text += data - - # 存储动作记录 - reply_text = f"你对{person_name}进行了回复:{reply_text}" - - if ENABLE_S4U: - await mai_thinking_manager.get_mai_think(self.chat_id).do_think_after_response(reply_text) - - await self.store_action_info( - action_build_into_prompt=False, - action_prompt_display=reply_text, - action_done=True, - ) - - # 重置NoReplyAction的连续计数器 - NoReplyAction.reset_consecutive_count() - - return success, reply_text - - except Exception as e: - logger.error(f"{self.log_prefix} 回复动作执行失败: {e}") - traceback.print_exc() - return False, f"回复失败: {str(e)}" diff --git a/src/tools/not_using/get_knowledge.py b/src/plugins/built_in/knowledge/get_knowledge.py similarity index 96% rename from src/tools/not_using/get_knowledge.py rename to src/plugins/built_in/knowledge/get_knowledge.py index c436d7742..4e662235a 100644 --- a/src/tools/not_using/get_knowledge.py +++ b/src/plugins/built_in/knowledge/get_knowledge.py @@ -1,4 +1,4 @@ -from src.tools.tool_can_use.base_tool import BaseTool +from src.plugin_system.base.base_tool import BaseTool from src.chat.utils.utils import get_embedding from src.common.database.database_model import Knowledges # Updated import from src.common.logger import get_logger @@ -77,7 +77,7 @@ class SearchKnowledgeTool(BaseTool): Union[str, list]: 格式化的信息字符串或原始结果列表 """ if not query_embedding: - return "" if not return_raw else [] + return [] if return_raw else "" similar_items = [] try: @@ -115,10 +115,10 @@ class SearchKnowledgeTool(BaseTool): except Exception as e: logger.error(f"从 Peewee 数据库获取知识信息失败: {str(e)}") - return "" if not return_raw else [] + return [] if return_raw else "" if not results: - return "" if not return_raw else [] + return [] if return_raw else "" if return_raw: # Peewee 模型实例不能直接序列化为 JSON,如果需要原始模型,调用者需要处理 diff --git a/src/tools/not_using/lpmm_get_knowledge.py b/src/plugins/built_in/knowledge/lpmm_get_knowledge.py similarity index 97% rename from src/tools/not_using/lpmm_get_knowledge.py rename to src/plugins/built_in/knowledge/lpmm_get_knowledge.py index 467db6ed1..0c8a32d78 100644 --- a/src/tools/not_using/lpmm_get_knowledge.py +++ b/src/plugins/built_in/knowledge/lpmm_get_knowledge.py @@ -1,4 +1,4 @@ -from src.tools.tool_can_use.base_tool import BaseTool +from src.plugin_system.base.base_tool import BaseTool # from src.common.database import db from src.common.logger import get_logger 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 cbdf567ac..c2489a380 100644 --- a/src/plugins/built_in/plugin_management/plugin.py +++ b/src/plugins/built_in/plugin_management/plugin.py @@ -11,6 +11,7 @@ from src.plugin_system import ( component_manage_api, ComponentInfo, ComponentType, + send_api, ) @@ -27,8 +28,15 @@ class ManagementCommand(BaseCommand): or not self.message.message_info.user_info or str(self.message.message_info.user_info.user_id) not in self.get_config("plugin.permission", []) # type: ignore ): - await self.send_text("你没有权限使用插件管理命令") + await self._send_message("你没有权限使用插件管理命令") return False, "没有权限", True + if not self.message.chat_stream: + await self._send_message("无法获取聊天流信息") + return False, "无法获取聊天流信息", True + self.stream_id = self.message.chat_stream.stream_id + if not self.stream_id: + await self._send_message("无法获取聊天流信息") + return False, "无法获取聊天流信息", True command_list = self.matched_groups["manage_command"].strip().split(" ") if len(command_list) == 1: await self.show_help("all") @@ -42,7 +50,7 @@ class ManagementCommand(BaseCommand): case "help": await self.show_help("all") case _: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if len(command_list) == 3: if command_list[1] == "plugin": @@ -56,7 +64,7 @@ class ManagementCommand(BaseCommand): case "rescan": await self._rescan_plugin_dirs() case _: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True elif command_list[1] == "component": if command_list[2] == "list": @@ -64,10 +72,10 @@ class ManagementCommand(BaseCommand): elif command_list[2] == "help": await self.show_help("component") else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if len(command_list) == 4: if command_list[1] == "plugin": @@ -81,28 +89,28 @@ class ManagementCommand(BaseCommand): case "add_dir": await self._add_dir(command_list[3]) case _: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True elif command_list[1] == "component": if command_list[2] != "list": - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if command_list[3] == "enabled": await self._list_enabled_components() elif command_list[3] == "disabled": await self._list_disabled_components() else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if len(command_list) == 5: if command_list[1] != "component": - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if command_list[2] != "list": - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if command_list[3] == "enabled": await self._list_enabled_components(target_type=command_list[4]) @@ -111,11 +119,11 @@ class ManagementCommand(BaseCommand): elif command_list[3] == "type": await self._list_registered_components_by_type(command_list[4]) else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if len(command_list) == 6: if command_list[1] != "component": - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True if command_list[2] == "enable": if command_list[3] == "global": @@ -123,7 +131,7 @@ class ManagementCommand(BaseCommand): elif command_list[3] == "local": await self._locally_enable_component(command_list[4], command_list[5]) else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True elif command_list[2] == "disable": if command_list[3] == "global": @@ -131,10 +139,10 @@ class ManagementCommand(BaseCommand): elif command_list[3] == "local": await self._locally_disable_component(command_list[4], command_list[5]) else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True else: - await self.send_text("插件管理命令不合法") + await self._send_message("插件管理命令不合法") return False, "命令不合法", True return True, "命令执行完成", True @@ -180,51 +188,51 @@ class ManagementCommand(BaseCommand): ) case _: return - await self.send_text(help_msg) + await self._send_message(help_msg) async def _list_loaded_plugins(self): plugins = plugin_manage_api.list_loaded_plugins() - await self.send_text(f"已加载的插件: {', '.join(plugins)}") + await self._send_message(f"已加载的插件: {', '.join(plugins)}") async def _list_registered_plugins(self): plugins = plugin_manage_api.list_registered_plugins() - await self.send_text(f"已注册的插件: {', '.join(plugins)}") + await self._send_message(f"已注册的插件: {', '.join(plugins)}") async def _rescan_plugin_dirs(self): plugin_manage_api.rescan_plugin_directory() - await self.send_text("插件目录重新扫描执行中") + await self._send_message("插件目录重新扫描执行中") async def _load_plugin(self, plugin_name: str): success, count = plugin_manage_api.load_plugin(plugin_name) if success: - await self.send_text(f"插件加载成功: {plugin_name}") + await self._send_message(f"插件加载成功: {plugin_name}") else: if count == 0: - await self.send_text(f"插件{plugin_name}为禁用状态") - await self.send_text(f"插件加载失败: {plugin_name}") + await self._send_message(f"插件{plugin_name}为禁用状态") + await self._send_message(f"插件加载失败: {plugin_name}") async def _unload_plugin(self, plugin_name: str): success = await plugin_manage_api.remove_plugin(plugin_name) if success: - await self.send_text(f"插件卸载成功: {plugin_name}") + await self._send_message(f"插件卸载成功: {plugin_name}") else: - await self.send_text(f"插件卸载失败: {plugin_name}") + await self._send_message(f"插件卸载失败: {plugin_name}") async def _reload_plugin(self, plugin_name: str): success = await plugin_manage_api.reload_plugin(plugin_name) if success: - await self.send_text(f"插件重新加载成功: {plugin_name}") + await self._send_message(f"插件重新加载成功: {plugin_name}") else: - await self.send_text(f"插件重新加载失败: {plugin_name}") + await self._send_message(f"插件重新加载失败: {plugin_name}") async def _add_dir(self, dir_path: str): - await self.send_text(f"正在添加插件目录: {dir_path}") + await self._send_message(f"正在添加插件目录: {dir_path}") success = plugin_manage_api.add_plugin_directory(dir_path) await asyncio.sleep(0.5) # 防止乱序发送 if success: - await self.send_text(f"插件目录添加成功: {dir_path}") + await self._send_message(f"插件目录添加成功: {dir_path}") else: - await self.send_text(f"插件目录添加失败: {dir_path}") + await self._send_message(f"插件目录添加失败: {dir_path}") def _fetch_all_registered_components(self) -> List[ComponentInfo]: all_plugin_info = component_manage_api.get_all_plugin_info() @@ -255,29 +263,29 @@ class ManagementCommand(BaseCommand): async def _list_all_registered_components(self): components_info = self._fetch_all_registered_components() if not components_info: - await self.send_text("没有注册的组件") + await self._send_message("没有注册的组件") return all_components_str = ", ".join( f"{component.name} ({component.component_type})" for component in components_info ) - await self.send_text(f"已注册的组件: {all_components_str}") + await self._send_message(f"已注册的组件: {all_components_str}") async def _list_enabled_components(self, target_type: str = "global"): components_info = self._fetch_all_registered_components() if not components_info: - await self.send_text("没有注册的组件") + await self._send_message("没有注册的组件") return if target_type == "global": enabled_components = [component for component in components_info if component.enabled] if not enabled_components: - await self.send_text("没有满足条件的已启用全局组件") + await self._send_message("没有满足条件的已启用全局组件") return enabled_components_str = ", ".join( f"{component.name} ({component.component_type})" for component in enabled_components ) - await self.send_text(f"满足条件的已启用全局组件: {enabled_components_str}") + await self._send_message(f"满足条件的已启用全局组件: {enabled_components_str}") elif target_type == "local": locally_disabled_components = self._fetch_locally_disabled_components() enabled_components = [ @@ -286,28 +294,28 @@ class ManagementCommand(BaseCommand): if (component.name not in locally_disabled_components and component.enabled) ] if not enabled_components: - await self.send_text("本聊天没有满足条件的已启用组件") + await self._send_message("本聊天没有满足条件的已启用组件") return enabled_components_str = ", ".join( f"{component.name} ({component.component_type})" for component in enabled_components ) - await self.send_text(f"本聊天满足条件的已启用组件: {enabled_components_str}") + await self._send_message(f"本聊天满足条件的已启用组件: {enabled_components_str}") async def _list_disabled_components(self, target_type: str = "global"): components_info = self._fetch_all_registered_components() if not components_info: - await self.send_text("没有注册的组件") + await self._send_message("没有注册的组件") return if target_type == "global": disabled_components = [component for component in components_info if not component.enabled] if not disabled_components: - await self.send_text("没有满足条件的已禁用全局组件") + await self._send_message("没有满足条件的已禁用全局组件") return disabled_components_str = ", ".join( f"{component.name} ({component.component_type})" for component in disabled_components ) - await self.send_text(f"满足条件的已禁用全局组件: {disabled_components_str}") + await self._send_message(f"满足条件的已禁用全局组件: {disabled_components_str}") elif target_type == "local": locally_disabled_components = self._fetch_locally_disabled_components() disabled_components = [ @@ -316,12 +324,12 @@ class ManagementCommand(BaseCommand): if (component.name in locally_disabled_components or not component.enabled) ] if not disabled_components: - await self.send_text("本聊天没有满足条件的已禁用组件") + await self._send_message("本聊天没有满足条件的已禁用组件") return disabled_components_str = ", ".join( f"{component.name} ({component.component_type})" for component in disabled_components ) - await self.send_text(f"本聊天满足条件的已禁用组件: {disabled_components_str}") + await self._send_message(f"本聊天满足条件的已禁用组件: {disabled_components_str}") async def _list_registered_components_by_type(self, target_type: str): match target_type: @@ -332,18 +340,18 @@ class ManagementCommand(BaseCommand): case "event_handler": component_type = ComponentType.EVENT_HANDLER case _: - await self.send_text(f"未知组件类型: {target_type}") + await self._send_message(f"未知组件类型: {target_type}") return components_info = component_manage_api.get_components_info_by_type(component_type) if not components_info: - await self.send_text(f"没有注册的 {target_type} 组件") + await self._send_message(f"没有注册的 {target_type} 组件") return components_str = ", ".join( f"{name} ({component.component_type})" for name, component in components_info.items() ) - await self.send_text(f"注册的 {target_type} 组件: {components_str}") + await self._send_message(f"注册的 {target_type} 组件: {components_str}") async def _globally_enable_component(self, component_name: str, component_type: str): match component_type: @@ -354,12 +362,12 @@ class ManagementCommand(BaseCommand): case "event_handler": target_component_type = ComponentType.EVENT_HANDLER case _: - await self.send_text(f"未知组件类型: {component_type}") + await self._send_message(f"未知组件类型: {component_type}") return if component_manage_api.globally_enable_component(component_name, target_component_type): - await self.send_text(f"全局启用组件成功: {component_name}") + await self._send_message(f"全局启用组件成功: {component_name}") else: - await self.send_text(f"全局启用组件失败: {component_name}") + await self._send_message(f"全局启用组件失败: {component_name}") async def _globally_disable_component(self, component_name: str, component_type: str): match component_type: @@ -370,13 +378,13 @@ class ManagementCommand(BaseCommand): case "event_handler": target_component_type = ComponentType.EVENT_HANDLER case _: - await self.send_text(f"未知组件类型: {component_type}") + await self._send_message(f"未知组件类型: {component_type}") return success = await component_manage_api.globally_disable_component(component_name, target_component_type) if success: - await self.send_text(f"全局禁用组件成功: {component_name}") + await self._send_message(f"全局禁用组件成功: {component_name}") else: - await self.send_text(f"全局禁用组件失败: {component_name}") + await self._send_message(f"全局禁用组件失败: {component_name}") async def _locally_enable_component(self, component_name: str, component_type: str): match component_type: @@ -387,16 +395,16 @@ class ManagementCommand(BaseCommand): case "event_handler": target_component_type = ComponentType.EVENT_HANDLER case _: - await self.send_text(f"未知组件类型: {component_type}") + await self._send_message(f"未知组件类型: {component_type}") return if component_manage_api.locally_enable_component( component_name, target_component_type, self.message.chat_stream.stream_id, ): - await self.send_text(f"本地启用组件成功: {component_name}") + await self._send_message(f"本地启用组件成功: {component_name}") else: - await self.send_text(f"本地启用组件失败: {component_name}") + await self._send_message(f"本地启用组件失败: {component_name}") async def _locally_disable_component(self, component_name: str, component_type: str): match component_type: @@ -407,34 +415,40 @@ class ManagementCommand(BaseCommand): case "event_handler": target_component_type = ComponentType.EVENT_HANDLER case _: - await self.send_text(f"未知组件类型: {component_type}") + await self._send_message(f"未知组件类型: {component_type}") return if component_manage_api.locally_disable_component( component_name, target_component_type, self.message.chat_stream.stream_id, ): - await self.send_text(f"本地禁用组件成功: {component_name}") + await self._send_message(f"本地禁用组件成功: {component_name}") else: - await self.send_text(f"本地禁用组件失败: {component_name}") + await self._send_message(f"本地禁用组件失败: {component_name}") + + async def _send_message(self, message: str): + await send_api.text_to_stream(message, self.stream_id, typing=False, storage_message=False) @register_plugin class PluginManagementPlugin(BasePlugin): plugin_name: str = "plugin_management_plugin" - enable_plugin: bool = True + enable_plugin: bool = False dependencies: list[str] = [] python_dependencies: list[str] = [] config_file_name: str = "config.toml" config_schema: dict = { "plugin": { - "enable": ConfigField(bool, default=True, description="是否启用插件"), - "permission": ConfigField(list, default=[], description="有权限使用插件管理命令的用户列表"), + "enabled": ConfigField(bool, default=False, description="是否启用插件"), + "config_version": ConfigField(type=str, default="1.1.0", description="配置文件版本"), + "permission": ConfigField( + list, default=[], description="有权限使用插件管理命令的用户列表,请填写字符串形式的用户ID" + ), }, } def get_plugin_components(self) -> List[Tuple[CommandInfo, Type[BaseCommand]]]: components = [] - if self.get_config("plugin.enable", True): + if self.get_config("plugin.enabled", True): components.append((ManagementCommand.get_command_info(), ManagementCommand)) return components diff --git a/src/tools/tool_can_use/__init__.py b/src/tools/tool_can_use/__init__.py deleted file mode 100644 index 14bae04c0..000000000 --- a/src/tools/tool_can_use/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from src.tools.tool_can_use.base_tool import ( - BaseTool, - register_tool, - discover_tools, - get_all_tool_definitions, - get_tool_instance, - TOOL_REGISTRY, -) - -__all__ = [ - "BaseTool", - "register_tool", - "discover_tools", - "get_all_tool_definitions", - "get_tool_instance", - "TOOL_REGISTRY", -] - -# 自动发现并注册工具 -discover_tools() diff --git a/src/tools/tool_can_use/base_tool.py b/src/tools/tool_can_use/base_tool.py deleted file mode 100644 index 89d051dc5..000000000 --- a/src/tools/tool_can_use/base_tool.py +++ /dev/null @@ -1,115 +0,0 @@ -from typing import List, Any, Optional, Type -import inspect -import importlib -import pkgutil -import os -from src.common.logger import get_logger -from rich.traceback import install - -install(extra_lines=3) - -logger = get_logger("base_tool") - -# 工具注册表 -TOOL_REGISTRY = {} - - -class BaseTool: - """所有工具的基类""" - - # 工具名称,子类必须重写 - name = None - # 工具描述,子类必须重写 - description = None - # 工具参数定义,子类必须重写 - parameters = None - - @classmethod - def get_tool_definition(cls) -> dict[str, Any]: - """获取工具定义,用于LLM工具调用 - - Returns: - dict: 工具定义字典 - """ - if not cls.name or not cls.description or not cls.parameters: - raise NotImplementedError(f"工具类 {cls.__name__} 必须定义 name, description 和 parameters 属性") - - return { - "type": "function", - "function": {"name": cls.name, "description": cls.description, "parameters": cls.parameters}, - } - - async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: - """执行工具函数 - - Args: - function_args: 工具调用参数 - - Returns: - dict: 工具执行结果 - """ - raise NotImplementedError("子类必须实现execute方法") - - -def register_tool(tool_class: Type[BaseTool]): - """注册工具到全局注册表 - - Args: - tool_class: 工具类 - """ - if not issubclass(tool_class, BaseTool): - raise TypeError(f"{tool_class.__name__} 不是 BaseTool 的子类") - - tool_name = tool_class.name - if not tool_name: - raise ValueError(f"工具类 {tool_class.__name__} 没有定义 name 属性") - - TOOL_REGISTRY[tool_name] = tool_class - logger.info(f"已注册: {tool_name}") - - -def discover_tools(): - """自动发现并注册tool_can_use目录下的所有工具""" - # 获取当前目录路径 - current_dir = os.path.dirname(os.path.abspath(__file__)) - package_name = os.path.basename(current_dir) - - # 遍历包中的所有模块 - for _, module_name, _ in pkgutil.iter_modules([current_dir]): - # 跳过当前模块和__pycache__ - if module_name == "base_tool" or module_name.startswith("__"): - continue - - # 导入模块 - module = importlib.import_module(f"src.tools.{package_name}.{module_name}") - - # 查找模块中的工具类 - for _, obj in inspect.getmembers(module): - if inspect.isclass(obj) and issubclass(obj, BaseTool) and obj != BaseTool: - register_tool(obj) - - logger.info(f"工具发现完成,共注册 {len(TOOL_REGISTRY)} 个工具") - - -def get_all_tool_definitions() -> List[dict[str, Any]]: - """获取所有已注册工具的定义 - - Returns: - List[dict]: 工具定义列表 - """ - return [tool_class().get_tool_definition() for tool_class in TOOL_REGISTRY.values()] - - -def get_tool_instance(tool_name: str) -> Optional[BaseTool]: - """获取指定名称的工具实例 - - Args: - tool_name: 工具名称 - - Returns: - Optional[BaseTool]: 工具实例,如果找不到则返回None - """ - tool_class = TOOL_REGISTRY.get(tool_name) - if not tool_class: - return None - return tool_class() diff --git a/src/tools/tool_can_use/compare_numbers_tool.py b/src/tools/tool_can_use/compare_numbers_tool.py deleted file mode 100644 index 2930f8f4b..000000000 --- a/src/tools/tool_can_use/compare_numbers_tool.py +++ /dev/null @@ -1,49 +0,0 @@ -from src.tools.tool_can_use.base_tool import BaseTool -from src.common.logger import get_logger -from typing import Any - -logger = get_logger("compare_numbers_tool") - - -class CompareNumbersTool(BaseTool): - """比较两个数大小的工具""" - - name = "compare_numbers" - description = "使用工具 比较两个数的大小,返回较大的数" - parameters = { - "type": "object", - "properties": { - "num1": {"type": "number", "description": "第一个数字"}, - "num2": {"type": "number", "description": "第二个数字"}, - }, - "required": ["num1", "num2"], - } - - async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]: - """执行比较两个数的大小 - - Args: - function_args: 工具参数 - - Returns: - dict: 工具执行结果 - """ - num1: int | float = function_args.get("num1") # type: ignore - num2: int | float = function_args.get("num2") # type: ignore - - try: - if num1 > num2: - result = f"{num1} 大于 {num2}" - elif num1 < num2: - result = f"{num1} 小于 {num2}" - else: - result = f"{num1} 等于 {num2}" - - return {"type": "comparison_result", "id": f"{num1}_vs_{num2}", "content": result} - except Exception as e: - logger.error(f"比较数字失败: {str(e)}") - return {"type": "info", "id": f"{num1}_vs_{num2}", "content": f"比较数字失败,炸了: {str(e)}"} - - -# 注册工具 -# register_tool(CompareNumbersTool) diff --git a/src/tools/tool_can_use/rename_person_tool.py b/src/tools/tool_can_use/rename_person_tool.py deleted file mode 100644 index 2216b8245..000000000 --- a/src/tools/tool_can_use/rename_person_tool.py +++ /dev/null @@ -1,104 +0,0 @@ -from src.tools.tool_can_use.base_tool import BaseTool -from src.person_info.person_info import get_person_info_manager -from src.common.logger import get_logger -import time - - -logger = get_logger("rename_person_tool") - - -class RenamePersonTool(BaseTool): - name = "rename_person" - description = ( - "这个工具可以改变用户的昵称。你可以选择改变对他人的称呼。你想给人改名,叫别人别的称呼,需要调用这个工具。" - ) - parameters = { - "type": "object", - "properties": { - "person_name": {"type": "string", "description": "需要重新取名的用户的当前昵称"}, - "message_content": { - "type": "string", - "description": "当前的聊天内容或特定要求,用于提供取名建议的上下文,尽可能详细。", - }, - }, - "required": ["person_name"], - } - - async def execute(self, function_args: dict, message_txt=""): - """ - 执行取名工具逻辑 - - Args: - function_args (dict): 包含 'person_name' 和可选 'message_content' 的字典 - message_txt (str): 原始消息文本 (这里未使用,因为 message_content 更明确) - - Returns: - dict: 包含执行结果的字典 - """ - person_name_to_find = function_args.get("person_name") - request_context = function_args.get("message_content", "") # 如果没有提供,则为空字符串 - - if not person_name_to_find: - return {"name": self.name, "content": "错误:必须提供需要重命名的用户昵称 (person_name)。"} - person_info_manager = get_person_info_manager() - try: - # 1. 根据昵称查找用户信息 - logger.debug(f"尝试根据昵称 '{person_name_to_find}' 查找用户...") - person_info = await person_info_manager.get_person_info_by_name(person_name_to_find) - - if not person_info: - logger.info(f"未找到昵称为 '{person_name_to_find}' 的用户。") - return { - "name": self.name, - "content": f"找不到昵称为 '{person_name_to_find}' 的用户。请确保输入的是我之前为该用户取的昵称。", - } - - person_id = person_info.get("person_id") - user_nickname = person_info.get("nickname") # 这是用户原始昵称 - user_cardname = person_info.get("user_cardname") - user_avatar = person_info.get("user_avatar") - - if not person_id: - logger.error(f"找到了用户 '{person_name_to_find}' 但无法获取 person_id") - return {"name": self.name, "content": f"找到了用户 '{person_name_to_find}' 但获取内部ID时出错。"} - - # 2. 调用 qv_person_name 进行取名 - logger.debug( - f"为用户 {person_id} (原昵称: {person_name_to_find}) 调用 qv_person_name,请求上下文: '{request_context}'" - ) - result = await person_info_manager.qv_person_name( - person_id=person_id, - user_nickname=user_nickname, # type: ignore - user_cardname=user_cardname, # type: ignore - user_avatar=user_avatar, # type: ignore - request=request_context, - ) - - # 3. 处理结果 - if result and result.get("nickname"): - new_name = result["nickname"] - # reason = result.get("reason", "未提供理由") - logger.info(f"成功为用户 {person_id} 取了新昵称: {new_name}") - - content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}" - logger.info(content) - return {"type": "info", "id": f"rename_success_{time.time()}", "content": content} - else: - logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。") - # 尝试从内存中获取可能已经更新的名字 - current_name = await person_info_manager.get_value(person_id, "person_name") - if current_name and current_name != person_name_to_find: - return { - "name": self.name, - "content": f"尝试取新昵称时遇到一点小问题,但我已经将 '{person_name_to_find}' 的昵称更新为 '{current_name}' 了。", - } - else: - return { - "name": self.name, - "content": f"尝试为 '{person_name_to_find}' 取新昵称时遇到了问题,未能成功生成。可能需要稍后再试。", - } - - except Exception as e: - error_msg = f"重命名失败: {str(e)}" - logger.error(error_msg, exc_info=True) - return {"type": "info_error", "id": f"rename_error_{time.time()}", "content": error_msg} diff --git a/src/tools/tool_use.py b/src/tools/tool_use.py deleted file mode 100644 index 738eeed48..000000000 --- a/src/tools/tool_use.py +++ /dev/null @@ -1,55 +0,0 @@ -import json -from src.common.logger import get_logger -from src.tools.tool_can_use import get_all_tool_definitions, get_tool_instance - -logger = get_logger("tool_use") - - -class ToolUser: - @staticmethod - def _define_tools(): - """获取所有已注册工具的定义 - - Returns: - list: 工具定义列表 - """ - return get_all_tool_definitions() - - @staticmethod - async def _execute_tool_call(tool_call): - """执行特定的工具调用 - - Args: - tool_call: 工具调用对象 - message_txt: 原始消息文本 - - Returns: - dict: 工具调用结果 - """ - try: - function_name = tool_call["function"]["name"] - function_args = json.loads(tool_call["function"]["arguments"]) - - # 获取对应工具实例 - tool_instance = get_tool_instance(function_name) - if not tool_instance: - logger.warning(f"未知工具名称: {function_name}") - return None - - # 执行工具 - result = await tool_instance.execute(function_args) - if result: - # 直接使用 function_name 作为 tool_type - tool_type = function_name - - return { - "tool_call_id": tool_call["id"], - "role": "tool", - "name": function_name, - "type": tool_type, - "content": result["content"], - } - return None - except Exception as e: - logger.error(f"执行工具调用时发生错误: {str(e)}") - return None diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 7b8c30ecc..fa9466c6d 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -52,26 +52,26 @@ relation_frequency = 1 # 关系频率,麦麦构建关系的频率 [chat] #麦麦的聊天通用设置 focus_value = 1 -# 麦麦的专注思考能力,越低越容易专注,消耗token也越多 +# 麦麦的专注思考能力,越高越容易专注,可能消耗更多token # 专注时能更好把握发言时机,能够进行持久的连续对话 +willing_amplifier = 1 # 麦麦回复意愿 + max_context_size = 25 # 上下文长度 -thinking_timeout = 20 # 麦麦一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢) +thinking_timeout = 40 # 麦麦一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢) replyer_random_probability = 0.5 # 首要replyer模型被选择的概率 mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复 at_bot_inevitable_reply = true # @bot 或 提及bot 大概率回复 -use_s4u_prompt_mode = true # 是否使用 s4u 对话构建模式,该模式会更好的把握当前对话对象的对话内容,但是对群聊整理理解能力较差(测试功能!!可能有未知问题!!) - talk_frequency = 1 # 麦麦回复频率,越高,麦麦回复越频繁 -time_based_talk_frequency = ["8:00,1", "12:00,1.5", "18:00,2", "01:00,0.5"] +time_based_talk_frequency = ["8:00,1", "12:00,1.2", "18:00,1.5", "01:00,0.6"] # 基于时段的回复频率配置(可选) # 格式:time_based_talk_frequency = ["HH:MM,frequency", ...] # 示例: -# time_based_talk_frequency = ["8:00,1", "12:00,2", "18:00,1.5", "00:00,0.5"] +# time_based_talk_frequency = ["8:00,1", "12:00,1.2", "18:00,1.5", "00:00,0.6"] # 说明:表示从该时间开始使用该频率,直到下一个时间点 # 注意:如果没有配置,则使用上面的默认 talk_frequency 值 @@ -105,11 +105,9 @@ ban_msgs_regex = [ [normal_chat] #普通聊天 willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical,mxp模式:mxp,自定义模式:custom(需要你自己实现) -response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数 [tool] -enable_in_normal_chat = false # 是否在普通聊天中启用工具 -enable_in_focus_chat = true # 是否在专注聊天中启用工具 +enable_tool = false # 是否在普通聊天中启用工具 [emoji] emoji_chance = 0.6 # 麦麦激活表情包动作的概率