Merge branch 'dev' into dev-api-ada to resolve conflicts

This commit is contained in:
UnCLAS-Prommer
2025-07-29 10:22:43 +08:00
92 changed files with 3980 additions and 6492 deletions

View File

@@ -11,12 +11,13 @@ on:
- "v*" - "v*"
- "*.*.*" - "*.*.*"
- "*.*.*-*" - "*.*.*-*"
workflow_dispatch: # 允许手动触发工作流
# Workflow's jobs # Workflow's jobs
jobs: jobs:
build-amd64: build-amd64:
name: Build AMD64 Image name: Build AMD64 Image
runs-on: ubuntu-latest runs-on: ubuntu-24.04
outputs: outputs:
digest: ${{ steps.build.outputs.digest }} digest: ${{ steps.build.outputs.digest }}
steps: steps:
@@ -69,7 +70,7 @@ jobs:
build-arm64: build-arm64:
name: Build ARM64 Image name: Build ARM64 Image
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
outputs: outputs:
digest: ${{ steps.build.outputs.digest }} digest: ${{ steps.build.outputs.digest }}
steps: steps:
@@ -85,11 +86,6 @@ jobs:
- name: Clone lpmm - name: Clone lpmm
run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with: with:
@@ -127,7 +123,7 @@ jobs:
create-manifest: create-manifest:
name: Create Multi-Arch Manifest name: Create Multi-Arch Manifest
runs-on: ubuntu-latest runs-on: ubuntu-24.04
needs: needs:
- build-amd64 - build-amd64
- build-arm64 - build-arm64

View File

@@ -1,12 +1,12 @@
name: Ruff name: Ruff
on: on:
push: # push:
branches: # branches:
- main # - main
- dev # - dev
- dev-refactor # 例如:匹配所有以 feature/ 开头的分支 # - dev-refactor # 例如:匹配所有以 feature/ 开头的分支
# 添加你希望触发此 workflow 的其他分支 # # 添加你希望触发此 workflow 的其他分支
workflow_dispatch: # 允许手动触发工作流 workflow_dispatch: # 允许手动触发工作流
branches: branches:
- main - main

1
.gitignore vendored
View File

@@ -322,3 +322,4 @@ run_pet.bat
config.toml config.toml
interested_rates.txt interested_rates.txt
MaiBot.code-workspace

View File

@@ -1,14 +1,28 @@
# Changelog # Changelog
## [0.9.1] - 2025-7-25 ## [0.9.1] - 2025-7-26
### 主要修复和优化
- 优化回复意愿
- 优化专注模式回复频率
- 优化关键词提取
- 修复部分模型产生的400问题
### 细节优化
- 修复reply导致的planner异常空跳
- 修复表达方式迁移空目录问题 - 修复表达方式迁移空目录问题
- 修复reply_to空字段问题 - 修复reply_to空字段问题
- 无可用动作导致的空plan问题
- 修复人格未压缩导致产生句号分割
- 将metioned bot 和 at应用到focus prompt中 - 将metioned bot 和 at应用到focus prompt中
- 更好的兴趣度计算
- 修复部分模型由于enable_thinking导致的400问题
- 移除dependency_manager
## [0.9.0] - 2025-7-24
## [0.9.0] - 2025-7-25
### 摘要 ### 摘要
MaiBot 0.9.0 重磅升级!本版本带来两大核心突破:**全面重构的插件系统**提供更强大的扩展能力和管理功能;**normal和focus模式统一化处理**大幅简化架构并提升性能。同时新增s4u prompt模式优化、语音消息支持、全新情绪系统和mais4u直播互动功能为MaiBot带来更自然、更智能的交互体验 MaiBot 0.9.0 重磅升级!本版本带来两大核心突破:**全面重构的插件系统**提供更强大的扩展能力和管理功能;**normal和focus模式统一化处理**大幅简化架构并提升性能。同时新增s4u prompt模式优化、语音消息支持、全新情绪系统和mais4u直播互动功能为MaiBot带来更自然、更智能的交互体验

View File

@@ -23,6 +23,8 @@
6. 增加了插件和组件管理的API。 6. 增加了插件和组件管理的API。
7. `BaseCommand`的`execute`方法现在返回一个三元组,包含是否执行成功、可选的回复消息和是否拦截消息。 7. `BaseCommand`的`execute`方法现在返回一个三元组,包含是否执行成功、可选的回复消息和是否拦截消息。
- 这意味着你终于可以动态控制是否继续后续消息的处理了。 - 这意味着你终于可以动态控制是否继续后续消息的处理了。
8. 移除了dependency_manager但是依然保留了`python_dependencies`属性,等待后续重构。
- 一并移除了文档有关manager的内容。
# 插件系统修改 # 插件系统修改
1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)** 1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)**

View File

@@ -22,7 +22,7 @@ class ExampleAction(BaseAction):
action_name = "example_action" # 动作的唯一标识符 action_name = "example_action" # 动作的唯一标识符
action_description = "这是一个示例动作" # 动作描述 action_description = "这是一个示例动作" # 动作描述
activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例 activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例
mode_enable = ChatMode.ALL # 这里以 ALL 为例 mode_enable = ChatMode.ALL # 一般取ALL表示在所有聊天模式下都可用
associated_types = ["text", "emoji", ...] # 关联类型 associated_types = ["text", "emoji", ...] # 关联类型
parallel_action = False # 是否允许与其他Action并行执行 parallel_action = False # 是否允许与其他Action并行执行
action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...} action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...}
@@ -60,7 +60,7 @@ class ExampleAction(BaseAction):
**请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。** **请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。**
#### action_parameters: 该Action的参数说明。 #### 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, "发送了问候" return True, "发送了问候"
``` ```
一个完整的使用`ActionActivationType.KEYWORD`的例子请参考`plugins/hello_world_plugin`中的`ByeAction`
#### 第二层:使用决策 #### 第二层:使用决策
**在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action** **在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action**
@@ -268,7 +270,6 @@ action_message为一个字典包含的键值对如下省略了不必要的
## Action 内置方法说明 ## Action 内置方法说明
```python ```python
class BaseAction: class BaseAction:
# 配置相关
def get_config(self, key: str, default=None): def get_config(self, key: str, default=None):
"""获取插件配置值,使用嵌套键访问""" """获取插件配置值,使用嵌套键访问"""

View File

@@ -5,147 +5,126 @@
## 导入方式 ## 导入方式
```python ```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]:
```
**参数:** **Args**:
- `platform`:平台筛选,默认为"qq" - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的聊天流。
**返回:** **Returns**:
- `List[ChatStream]`:聊天流列表 - `List[ChatStream]`:聊天流列表
**示例:** ### 2. 获取群聊聊天流
```python ```python
streams = chat_api.get_all_streams() def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
for stream in streams:
print(f"聊天流ID: {stream.stream_id}")
``` ```
#### `get_group_streams(platform: str = "qq") -> List[ChatStream]` **Args**:
获取所有群聊聊天流 - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。
**参数:** **Returns**:
- `platform`:平台筛选,默认为"qq"
**返回:**
- `List[ChatStream]`:群聊聊天流列表 - `List[ChatStream]`:群聊聊天流列表
#### `get_private_streams(platform: str = "qq") -> List[ChatStream]` ### 3. 获取私聊聊天流
获取所有私聊聊天流
**参数:** ```python
- `platform`:平台筛选,默认为"qq" def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
```
**返回:** **Args**:
- `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。
**Returns**:
- `List[ChatStream]`:私聊聊天流列表 - `List[ChatStream]`:私聊聊天流列表
### 2. 查找特定聊天流 ### 4. 根据群ID获取聊天流
#### `get_stream_by_group_id(group_id: str, platform: str = "qq") -> Optional[ChatStream]` ```python
根据群ID获取聊天流 def get_stream_by_group_id(group_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]:
```
**参数:** **Args**:
- `group_id`群聊ID - `group_id`群聊ID
- `platform`:平台,默认为"qq" - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的群聊流。
**返回:** **Returns**:
- `Optional[ChatStream]`聊天流对象如果未找到返回None - `Optional[ChatStream]`聊天流对象如果未找到返回None
**示例:** ### 5. 根据用户ID获取私聊流
```python ```python
chat_stream = chat_api.get_stream_by_group_id("123456789") def get_stream_by_user_id(user_id: str, platform: Optional[str] | SpecialTypes = "qq") -> Optional[ChatStream]:
if chat_stream:
print(f"找到群聊: {chat_stream.group_info.group_name}")
``` ```
#### `get_stream_by_user_id(user_id: str, platform: str = "qq") -> Optional[ChatStream]` **Args**:
根据用户ID获取私聊流
**参数:**
- `user_id`用户ID - `user_id`用户ID
- `platform`:平台,默认为"qq" - `platform`:平台筛选,默认为"qq",可以使用`SpecialTypes`枚举类中的`SpecialTypes.ALL_PLATFORMS`来获取所有平台的私聊流。
**返回:** **Returns**:
- `Optional[ChatStream]`聊天流对象如果未找到返回None - `Optional[ChatStream]`聊天流对象如果未找到返回None
### 3. 聊天流信息查询 ### 6. 获取聊天流类型
#### `get_stream_type(chat_stream: ChatStream) -> str` ```python
获取聊天流类型 def get_stream_type(chat_stream: ChatStream) -> str:
```
**参数:** **Args**:
- `chat_stream`:聊天流对象 - `chat_stream`:聊天流对象
**返回:** **Returns**:
- `str`:聊天类型 ("group", "private", "unknown") - `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`:聊天流对象 - `chat_stream`:聊天流对象
**返回:** **Returns**:
- `Dict[str, Any]`:聊天流信息字典包含stream_id、platform、type等信息 - `Dict[str, Any]`:聊天流的详细信息,包括但不限于:
- `stream_id`聊天流ID
- `platform`:平台名称
- `type`:聊天流类型
- `group_id`群聊ID
- `group_name`:群聊名称
- `user_id`用户ID
- `user_name`:用户名称
### 8. 获取聊天流统计摘要
**示例:**
```python ```python
info = chat_api.get_stream_info(chat_stream) def get_streams_summary() -> Dict[str, int]:
print(f"聊天类型: {info['type']}")
print(f"平台: {info['platform']}")
if info['type'] == 'group':
print(f"群ID: {info['group_id']}")
print(f"群名: {info['group_name']}")
``` ```
#### `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. 所有函数都有错误处理,失败时会记录日志 1. 大部分函数在参数不合法时候会抛出异常,请确保你的程序进行了捕获。
2. 查询函数返回None或空列表时表示未找到结果 2. `ChatStream`对象包含了聊天的完整信息,包括用户信息、群信息等。
3. `platform`参数通常为"qq",也可能支持其他平台
4. `ChatStream`对象包含了聊天的完整信息,包括用户信息、群信息等

View File

@@ -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]` - 禁用的组件名称列表。

View File

@@ -6,178 +6,47 @@
```python ```python
from src.plugin_system.apis import config_api 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 ```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") 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 ```python
# 在插件中使用 def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any:
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
``` ```
**Args**:
- `plugin_config`: 插件配置字典
- `key`: 配置键名,支持嵌套访问如 "section.subsection.key",大小写敏感
- `default`: 如果配置不存在时返回的默认值
### 2. 用户信息API **Returns**:
- `Any`: 配置值或默认值
#### `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")
```
## 注意事项 ## 注意事项
1. **只读访问**配置API只提供读取功能插件不能修改全局配置 1. **只读访问**配置API只提供读取功能插件不能修改全局配置
2. **异步函数**:用户信息相关的函数是异步的,需要使用`await` 2. **错误处理**:所有函数都有错误处理,失败时会记录日志并返回默认值
3. **错误处理**:所有函数都有错误处理,失败时会记录日志并返回默认值 3. **安全性**插件通过此API访问配置是安全和隔离的
4. **安全性**插件通过此API访问配置是安全和隔离的 4. **性**频繁访问配置建议在插件初始化时获取并缓存
5. **性能**:频繁访问的配置建议在插件初始化时获取并缓存

View File

@@ -6,72 +6,51 @@
```python ```python
from src.plugin_system.apis import database_api from src.plugin_system.apis import database_api
# 或者
from src.plugin_system import database_api
``` ```
## 主要功能 ## 主要功能
### 1. 通用数据库查询 ### 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. 基本查询操作
```python ```python
from src.plugin_system.apis import database_api async def db_query(
from src.common.database.database_model import Messages, ActionRecords 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 = await database_api.db_query(
Messages, Messages,
query_type="get", query_type="get",
@@ -79,180 +58,159 @@ messages = await database_api.db_query(
limit=10, limit=10,
order_by=["-time"] order_by=["-time"]
) )
# 查询单条记录
message = await database_api.db_query(
Messages,
query_type="get",
filters={"message_id": "msg_123"},
single_result=True
)
``` ```
2. 创建一条记录
### 2. 创建记录
```python ```python
# 创建新的动作记录
new_record = await database_api.db_query( new_record = await database_api.db_query(
ActionRecords, ActionRecords,
data={"action_id": "123", "time": time.time(), "action_name": "TestAction"},
query_type="create", 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 ```python
# 更新动作状态
updated_count = await database_api.db_query( updated_count = await database_api.db_query(
ActionRecords, ActionRecords,
data={"action_done": True},
query_type="update", query_type="update",
filters={"action_id": "action_123"}, filters={"action_id": "123"},
data={"action_done": True, "completion_time": time.time()}
) )
print(f"更新了 {updated_count} 条记录")
``` ```
4. 删除记录
### 4. 删除记录
```python ```python
# 删除过期记录
deleted_count = await database_api.db_query( deleted_count = await database_api.db_query(
ActionRecords, ActionRecords,
query_type="delete", query_type="delete",
filters={"time__lt": time.time() - 86400} # 删除24小时前的记录 filters={"action_id": "123"}
) )
print(f"删除了 {deleted_count} 条过期记录")
``` ```
5. 计数
### 5. 统计查询
```python ```python
# 统计消息数量 count = await database_api.db_query(
message_count = await database_api.db_query(
Messages, Messages,
query_type="count", query_type="count",
filters={"chat_id": chat_stream.stream_id} 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 ```python
# 使用db_save进行创建或更新
record = await database_api.db_save( record = await database_api.db_save(
ActionRecords, ActionRecords,
{ {
"action_id": "action_123", "action_id": "123",
"time": time.time(), "time": time.time(),
"action_name": "TestAction", "action_name": "TestAction",
"action_done": True "action_done": True
}, },
key_field="action_id", key_field="action_id",
key_value="action_123" key_value="123"
) )
```
# 使用db_get进行简单查询 ### 3. 数据库获取
recent_messages = await database_api.db_get( ```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, Messages,
filters={"chat_id": chat_stream.stream_id}, filters={"chat_id": chat_stream.stream_id},
limit=10,
order_by="-time", order_by="-time",
limit=5
) )
``` ```
## 高级用法 ### 4. 动作信息存储
### 复杂查询示例
```python ```python
# 查询特定用户在特定时间段的消息 async def store_action_info(
user_messages = await database_api.db_query( chat_stream=None,
Messages, action_build_into_prompt: bool = False,
query_type="get", action_prompt_display: str = "",
filters={ action_done: bool = True,
"user_id": "123456", thinking_id: str = "",
"time__gte": start_time, # 大于等于开始时间 action_data: Optional[dict] = None,
"time__lt": end_time # 小于结束时间 action_name: str = "",
}, ) -> Optional[Dict[str, Any]]:
order_by=["-time"], ```
limit=50 存储动作信息到数据库,是一种针对 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的事务功能

View File

@@ -6,11 +6,13 @@
```python ```python
from src.plugin_system.apis import emoji_api from src.plugin_system.apis import emoji_api
# 或者
from src.plugin_system import emoji_api
``` ```
## 🆕 **二步走识别优化** ## 二步走识别优化
新版本开始,表情包识别系统采用了**二步走识别 + 智能缓存**的优化方案: 从新版本开始,表情包识别系统采用了**二步走识别 + 智能缓存**的优化方案:
### **收到表情包时的识别流程** ### **收到表情包时的识别流程**
1. **第一步**VLM视觉分析 - 生成详细描述 1. **第一步**VLM视觉分析 - 生成详细描述
@@ -30,217 +32,84 @@ from src.plugin_system.apis import emoji_api
## 主要功能 ## 主要功能
### 1. 表情包获取 ### 1. 表情包获取
```python
#### `get_by_description(description: str) -> Optional[Tuple[str, str, str]]` async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]:
```
根据场景描述选择表情包 根据场景描述选择表情包
**参数** **Args**
- `description`场景描述文本,例如"开心的大笑"、"轻微的讽刺"、"表示无奈和沮丧"等 - `description`表情包的描述文本,例如"开心"、"难过"、"愤怒"等
**返回** **Returns**
- `Optional[Tuple[str, str, str]]`(base64编码, 表情包描述, 匹配的场景) 或 None - `Optional[Tuple[str, str, str]]`一个元组: (表情包的base64编码, 描述, 情感标签),如果未找到匹配的表情包则返回None
**示例:** #### 示例
```python ```python
emoji_result = await emoji_api.get_by_description("开心的大笑") emoji_result = await emoji_api.get_by_description("大笑")
if emoji_result: if emoji_result:
emoji_base64, description, matched_scene = emoji_result emoji_base64, description, matched_scene = emoji_result
print(f"获取到表情包: {description}, 场景: {matched_scene}") print(f"获取到表情包: {description}, 场景: {matched_scene}")
# 可以将emoji_base64用于发送表情包 # 可以将emoji_base64用于发送表情包
``` ```
#### `get_random() -> Optional[Tuple[str, str, str]]` ### 2. 随机获取表情包
随机获取表情包
**返回:**
- `Optional[Tuple[str, str, str]]`(base64编码, 表情包描述, 随机场景) 或 None
**示例:**
```python ```python
random_emoji = await emoji_api.get_random() async def get_random(count: Optional[int] = 1) -> List[Tuple[str, str, str]]:
if random_emoji:
emoji_base64, description, scene = random_emoji
print(f"随机表情包: {description}")
``` ```
随机获取指定数量的表情包
#### `get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]` **Args**
根据场景关键词获取表情包 - `count`:要获取表情包数量默认为1
**参数** **Returns**
- `emotion`:场景关键词,如"大笑"、"讽刺"、"无奈"等 - `List[Tuple[str, str, str]]`:一个包含多个表情包的列表,每个元素是一个元组: (表情包的base64编码, 描述, 情感标签),如果未找到或出错则返回空列表
**返回:** ### 3. 根据情感获取表情包
- `Optional[Tuple[str, str, str]]`(base64编码, 表情包描述, 匹配的场景) 或 None
**示例:**
```python ```python
emoji_result = await emoji_api.get_by_emotion("讽刺") async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]:
if emoji_result:
emoji_base64, description, scene = emoji_result
# 发送讽刺表情包
``` ```
根据情感标签获取表情包
### 2. 表情包信息查询 **Args**
- `emotion`:情感标签,例如"开心"、"悲伤"、"愤怒"等
#### `get_count() -> int` **Returns**
获取表情包数量 - `Optional[Tuple[str, str, str]]`:一个元组: (表情包的base64编码, 描述, 情感标签)如果未找到则返回None
**返回:** ### 4. 获取表情包数量
- `int`:当前可用的表情包数量 ```python
def get_count() -> int:
```
获取当前可用表情包的数量
#### `get_info() -> dict` ### 5. 获取表情包系统信息
获取表情包系统信息 ```python
def get_info() -> Dict[str, Any]:
```
获取表情包系统的基本信息
**返回** **Returns**
- `dict`:包含表情包数量、最大数量等信息 - `Dict[str, Any]`:包含表情包数量、描述等信息的字典,包含以下键:
- `current_count`:当前表情包数量
- `max_count`:最大表情包数量
- `available_emojis`:当前可用的表情包数量
**返回字典包含:** ### 6. 获取所有可用的情感标签
- `current_count`:当前表情包数量 ```python
- `max_count`:最大表情包数量 def get_emotions() -> List[str]:
- `available_emojis`:可用表情包数量 ```
获取所有可用的情感标签 **(已经去重)**
#### `get_emotions() -> list` ### 7. 获取所有表情包描述
获取所有可用的场景关键词 ```python
def get_descriptions() -> List[str]:
**返回:** ```
- `list`:所有表情包的场景关键词列表(去重)
#### `get_descriptions() -> list`
获取所有表情包的描述列表 获取所有表情包的描述列表
**返回:**
- `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编码返回可直接用于发送 2. **返回格式**表情包以base64编码返回可直接用于发送
3. **错误处理**所有函数都有错误处理失败时返回None或默认值 3. **错误处理**所有函数都有错误处理失败时返回None,空列表或默认值
4. **使用统计**:系统会记录表情包的使用次数 4. **使用统计**:系统会记录表情包的使用次数
5. **文件依赖**:表情包依赖于本地文件,确保表情包文件存在 5. **文件依赖**:表情包依赖于本地文件,确保表情包文件存在
6. **编码格式**返回的是base64编码的图片数据可直接用于网络传输 6. **编码格式**返回的是base64编码的图片数据可直接用于网络传输

View File

@@ -6,241 +6,150 @@
```python ```python
from src.plugin_system.apis import generator_api from src.plugin_system.apis import generator_api
# 或者
from src.plugin_system import generator_api
``` ```
## 主要功能 ## 主要功能
### 1. 回复器获取 ### 1. 回复器获取
```python
#### `get_replyer(chat_stream=None, platform=None, chat_id=None, is_group=True)` 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如果没有则使用chat_id直接查找。
- `chat_stream`:聊天流对象(优先)
- `platform`:平台名称,如"qq"
- `chat_id`聊天ID群ID或用户ID
- `is_group`:是否为群聊
**返回:** 使用 ReplyerManager 来管理实例,避免重复创建。
- `DefaultReplyer`回复器对象如果获取失败则返回None
**示例:** **Args:**
- `chat_stream`: 聊天流对象
- `chat_id`: 聊天ID实际上就是`stream_id`
- `model_configs`: 模型配置
- `request_type`: 请求类型用于记录LLM使用情况可以不写
**Returns:**
- `DefaultReplyer`: 回复器对象如果获取失败则返回None
#### 示例
```python ```python
# 使用聊天流获取回复器 # 使用聊天流获取回复器
replyer = generator_api.get_replyer(chat_stream=chat_stream) replyer = generator_api.get_replyer(chat_stream=chat_stream)
# 使用平台和ID获取回复器 # 使用平台和ID获取回复器
replyer = generator_api.get_replyer( replyer = generator_api.get_replyer(chat_id="123456789")
platform="qq",
chat_id="123456789",
is_group=True
)
``` ```
### 2. 回复生成 ### 2. 回复生成
```python
#### `generate_reply(chat_stream=None, action_data=None, platform=None, chat_id=None, is_group=True)` 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如果没有则使用chat_id直接查找。
- `chat_stream`:聊天流对象(优先)
- `action_data`:动作数据
- `platform`:平台名称(备用)
- `chat_id`聊天ID备用
- `is_group`:是否为群聊(备用)
**返回:** **Args:**
- `Tuple[bool, List[Tuple[str, Any]]]`(是否成功, 回复集合) - `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 ```python
success, reply_set = await generator_api.generate_reply( success, reply_set, prompt = await generator_api.generate_reply(
chat_stream=chat_stream, 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: if success:
for reply_type, reply_content in reply_set: for reply_type, reply_content in reply_set:
print(f"回复类型: {reply_type}, 内容: {reply_content}") 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)` ### 3. 回复重写
重写回复
**参数:**
- `chat_stream`:聊天流对象(优先)
- `reply_data`:回复数据
- `platform`:平台名称(备用)
- `chat_id`聊天ID备用
- `is_group`:是否为群聊(备用)
**返回:**
- `Tuple[bool, List[Tuple[str, Any]]]`(是否成功, 回复集合)
**示例:**
```python ```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, 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}")
``` ```
## 使用示例 ## 回复集合`reply_set`格式
### 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 []
```
## 回复集合格式
### 回复类型 ### 回复类型
生成的回复集合包含多种类型的回复: 生成的回复集合包含多种类型的回复:
@@ -260,82 +169,32 @@ reply_set = [
] ]
``` ```
## 高级用法 ### 4. 自定义提示词回复
### 1. 自定义回复器配置
```python ```python
async def generate_with_custom_config(chat_stream, action_data): async def generate_response_custom(
"""使用自定义配置生成回复""" chat_stream: Optional[ChatStream] = None,
chat_id: Optional[str] = None,
# 获取回复器 model_configs: Optional[List[Dict[str, Any]]] = None,
replyer = generator_api.get_replyer(chat_stream=chat_stream) prompt: str = "",
) -> Optional[str]:
if replyer:
# 可以访问回复器的内部方法
success, reply_set = await replyer.generate_reply_with_context(
reply_data=action_data,
# 可以传递额外的配置参数
)
return success, reply_set
return False, []
``` ```
生成自定义提示词回复
### 2. 回复质量评估 优先使用chat_stream如果没有则使用chat_id直接查找。
```python **Args:**
async def generate_and_evaluate_replies(chat_stream, action_data): - `chat_stream`: 聊天流对象
"""生成回复并评估质量""" - `chat_id`: 聊天ID备用
- `model_configs`: 模型配置列表
- `prompt`: 自定义提示词
success, reply_set = await generator_api.generate_reply( **Returns:**
chat_stream=chat_stream, - `Optional[str]`: 生成的自定义回复内容如果生成失败则返回None
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 []
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)
```
## 注意事项 ## 注意事项
1. **异步操作**所有生成函数是异步的,须使用`await` 1. **异步操作**部分函数是异步的,须使用`await`
2. **错误处理**函数内置错误处理失败时返回False和空列表 2. **聊天流依赖**:需要有效的聊天流对象才能正常工作
3. **聊天流依赖**:需要有效的聊天流对象才能正常工作 3. **性能考虑**回复生成可能需要一些时间特别是使用LLM时
4. **性能考虑**回复生成可能需要一些时间特别是使用LLM时 4. **回复格式**:返回的回复集合是元组列表,包含类型和内容
5. **回复格式**:返回的回复集合是元组列表,包含类型和内容 5. **上下文感知**:生成器会考虑聊天上下文和历史消息,除非你用的是自定义提示词。
6. **上下文感知**:生成器会考虑聊天上下文和历史消息

View File

@@ -6,239 +6,34 @@ LLM API模块提供与大语言模型交互的功能让插件能够使用系
```python ```python
from src.plugin_system.apis import llm_api from src.plugin_system.apis import llm_api
# 或者
from src.plugin_system import llm_api
``` ```
## 主要功能 ## 主要功能
### 1. 模型管理 ### 1. 查询可用模型
#### `get_available_models() -> Dict[str, Any]`
获取所有可用的模型配置
**返回:**
- `Dict[str, Any]`模型配置字典key为模型名称value为模型配置
**示例:**
```python ```python
models = llm_api.get_available_models() def get_available_models() -> Dict[str, Any]:
for model_name, model_config in models.items():
print(f"模型: {model_name}")
print(f"配置: {model_config}")
``` ```
获取所有可用的模型配置。
### 2. 内容生成 **Return**
- `Dict[str, Any]`模型配置字典key为模型名称value为模型配置。
#### `generate_with_model(prompt, model_config, request_type="plugin.generate", **kwargs)` ### 2. 使用模型生成内容
使用指定模型生成内容
**参数:**
- `prompt`:提示词
- `model_config`:模型配置(从 get_available_models 获取)
- `request_type`:请求类型标识
- `**kwargs`其他模型特定参数如temperature、max_tokens等
**返回:**
- `Tuple[bool, str, str, str]`(是否成功, 生成的内容, 推理过程, 模型名称)
**示例:**
```python ```python
models = llm_api.get_available_models() async def generate_with_model(
default_model = models.get("default") prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs
) -> Tuple[bool, str]:
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}")
``` ```
使用指定模型生成内容。
## 使用示例 **Args:**
- `prompt`:提示词。
- `model_config`:模型配置(从 `get_available_models` 获取)。
- `request_type`:请求类型标识,默认为 `"plugin.generate"`
- `**kwargs`:其他模型特定参数,如 `temperature``max_tokens` 等。
### 1. 基础文本生成 **Return**
- `Tuple[bool, str]`:返回一个元组,第一个元素表示是否成功,第二个元素为生成的内容或错误信息。
```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以控制成本

View File

@@ -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`

View File

@@ -1,11 +1,13 @@
# 消息API # 消息API
> 消息API提供了强大的消息查询、计数和格式化功能让你轻松处理聊天消息数据。 消息API提供了强大的消息查询、计数和格式化功能让你轻松处理聊天消息数据。
## 导入方式 ## 导入方式
```python ```python
from src.plugin_system.apis import message_api 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]]:
```
获取指定时间范围内的消息。
### 按时间查询消息 **Args:**
#### `get_messages_by_time(start_time, end_time, limit=0, limit_mode="latest")`
获取指定时间范围内的消息
**参数:**
- `start_time` (float): 开始时间戳 - `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳 - `end_time` (float): 结束时间戳
- `limit` (int): 限制返回消息数量0为不限制 - `limit` (int): 限制返回消息数量0为不限制
- `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录 - `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 ```python
import time def get_messages_by_time_in_chat(
chat_id: str,
# 获取最近24小时的消息 start_time: float,
now = time.time() end_time: float,
yesterday = now - 24 * 3600 limit: int = 0,
messages = message_api.get_messages_by_time(yesterday, now, limit=50) limit_mode: str = "latest",
filter_mai: bool = False,
) -> List[Dict[str, Any]]:
``` ```
获取指定聊天中指定时间范围内的消息。
### 按聊天查询消息 **Args:**
#### `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")`
获取指定聊天中指定用户在指定时间范围内的消息
**参数:**
- `chat_id` (str): 聊天ID - `chat_id` (str): 聊天ID
- `start_time` (float): 开始时间戳 - `start_time` (float): 开始时间戳
- `end_time` (float): 结束时间戳 - `end_time` (float): 结束时间戳
- `person_ids` (list): 用户ID列表 - `limit` (int): 限制返回消息数量0为不限制
- `limit` (int): 限制返回消息数量 - `limit_mode` (str): 限制模式,`"earliest"`获取最早记录,`"latest"`获取最新记录
- `limit_mode` (str): 限制模式 - `filter_mai` (bool): 是否过滤掉机器人的消息默认False
**示例:** **Returns:**
- `List[Dict[str, Any]]` - 消息列表
### 3. 获取指定聊天中指定时间范围内的信息(包含边界)
```python ```python
# 获取特定用户的消息 def get_messages_by_time_in_chat_inclusive(
user_messages = message_api.get_messages_by_time_in_chat_for_users( chat_id: str,
chat_id="123456789", start_time: float,
start_time=yesterday, end_time: float,
end_time=now, limit: int = 0,
person_ids=["user1", "user2"] 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]]:
```
获取指定聊天中指定用户在指定时间范围内的消息。
随机选择一个聊天,返回该聊天在指定时间范围内的消息 **Args:**
#### `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)`
计算指定聊天中从开始时间到结束时间的新消息数量
**参数:**
- `chat_id` (str): 聊天ID - `chat_id` (str): 聊天ID
- `start_time` (float): 开始时间戳 - `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 ```python
# 计算最近1小时的新消息数 def get_random_chat_messages(
import time start_time: float,
now = time.time() end_time: float,
hour_ago = now - 3600 limit: int = 0,
new_count = message_api.count_new_messages("123456789", hour_ago, now) limit_mode: str = "latest",
print(f"最近1小时有{new_count}条新消息") 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]]): 消息列表 - `messages` (List[Dict[str, Any]]): 消息列表
- `replace_bot_name` (bool): 是否将机器人的名称替换为"你"默认True - `replace_bot_name` (bool): 是否将机器人的名称替换为"你"
- `merge_messages` (bool): 是否合并连续消息默认False - `merge_messages` (bool): 是否合并连续消息
- `timestamp_mode` (str): 时间戳显示模式,`"relative"``"absolute"`,默认`"relative"` - `timestamp_mode` (str): 时间戳显示模式,`"relative"``"absolute"`
- `read_mark` (float): 已读标记时间戳,用于分割已读和未读消息默认0.0 - `read_mark` (float): 已读标记时间戳,用于分割已读和未读消息
- `truncate` (bool): 是否截断长消息默认False - `truncate` (bool): 是否截断长消息
- `show_actions` (bool): 是否显示动作记录默认False - `show_actions` (bool): 是否显示动作记录
**返回:** `str` - 格式化后的可读字符串 **Returns:**
- `str` - 格式化后的可读字符串
**示例:**
### 14. 将消息列表构建成可读的字符串,并返回详细信息
```python ```python
# 获取消息并格式化为可读文本 async def build_readable_messages_with_details(
messages = message_api.get_recent_messages("123456789", hours=2) messages: List[Dict[str, Any]],
readable_text = message_api.build_readable_messages_to_str( replace_bot_name: bool = True,
messages, merge_messages: bool = False,
replace_bot_name=True, timestamp_mode: str = "relative",
merge_messages=True, truncate: bool = False,
timestamp_mode="relative" ) -> Tuple[str, List[Tuple[float, str, str]]]:
)
print(readable_text)
``` ```
将消息列表构建成可读的字符串,并返回详细信息。
### `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 ```python
# 异步获取详细格式化信息 async def get_person_ids_from_messages(
readable_text, details = await message_api.build_readable_messages_with_details( messages: List[Dict[str, Any]],
messages, ) -> List[str]:
timestamp_mode="absolute"
)
for timestamp, nickname, content in details:
print(f"{timestamp}: {nickname} 说: {content}")
``` ```
从消息列表中提取不重复的用户ID列表。
### `get_person_ids_from_messages(messages)` 异步 **Args:**
从消息列表中提取不重复的用户ID列表
**参数:**
- `messages` (List[Dict[str, Any]]): 消息列表 - `messages` (List[Dict[str, Any]]): 消息列表
**返回:** `List[str]` - 用户ID列表 **Returns:**
- `List[str]` - 用户ID列表
**示例:**
### 16. 从消息列表中移除机器人的消息
```python ```python
# 获取参与对话的所有用户ID def filter_mai_messages(
messages = message_api.get_recent_messages("123456789") messages: List[Dict[str, Any]],
person_ids = await message_api.get_person_ids_from_messages(messages) ) -> List[Dict[str, Any]]:
print(f"参与对话的用户: {person_ids}")
``` ```
从消息列表中移除机器人的消息。
--- **Args:**
- `messages` (List[Dict[str, Any]]): 消息列表,每个元素是消息字典
## 完整使用示例 **Returns:**
- `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
```
---
## 注意事项 ## 注意事项
1. **时间戳格式**所有时间参数都使用Unix时间戳float类型 1. **时间戳格式**所有时间参数都使用Unix时间戳float类型
2. **异步函数**`build_readable_messages_with_details``get_person_ids_from_messages` 是异步函数,需要使用 `await` 2. **异步函数**部分函数是异步函数,需要使用 `await`
3. **性能考虑**:查询大量消息时建议设置合理的 `limit` 参数 3. **性能考虑**:查询大量消息时建议设置合理的 `limit` 参数
4. **消息格式**:返回的消息是字典格式,包含时间戳、发送者、内容等信息 4. **消息格式**:返回的消息是字典格式,包含时间戳、发送者、内容等信息
5. **用户ID**`person_ids` 参数接受字符串列表,用于筛选特定用户的消息 5. **用户ID**`person_ids` 参数接受字符串列表,用于筛选特定用户的消息

View File

@@ -6,59 +6,65 @@
```python ```python
from src.plugin_system.apis import person_api from src.plugin_system.apis import person_api
# 或者
from src.plugin_system import person_api
``` ```
## 主要功能 ## 主要功能
### 1. Person ID管理 ### 1. Person ID 获取
```python
#### `get_person_id(platform: str, user_id: int) -> str` def get_person_id(platform: str, user_id: int) -> str:
```
根据平台和用户ID获取person_id 根据平台和用户ID获取person_id
**参数:** **Args:**
- `platform`:平台名称,如 "qq", "telegram" 等 - `platform`:平台名称,如 "qq", "telegram" 等
- `user_id`用户ID - `user_id`用户ID
**返回:** **Returns:**
- `str`唯一的person_idMD5哈希值 - `str`唯一的person_idMD5哈希值
**示例:** #### 示例
```python ```python
person_id = person_api.get_person_id("qq", 123456) person_id = person_api.get_person_id("qq", 123456)
print(f"Person ID: {person_id}")
``` ```
### 2. 用户信息查询 ### 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` **Args:**
根据person_id和字段名获取某个值
**参数:**
- `person_id`用户的唯一标识ID - `person_id`用户的唯一标识ID
- `field_name`:要获取的字段名,如 "nickname", "impression" 等 - `field_name`:要获取的字段名
- `default`字段不存在或获取失败时返回的默认值 - `default`:字段不存在的默认值
**返回:** **Returns:**
- `Any`:字段值或默认值 - `Any`:字段值或默认值
**示例:** #### 示例
```python ```python
nickname = await person_api.get_person_value(person_id, "nickname", "未知用户") nickname = await person_api.get_person_value(person_id, "nickname", "未知用户")
impression = await person_api.get_person_value(person_id, "impression") 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 - `person_id`用户的唯一标识ID
- `field_names`:要获取的字段名列表 - `field_names`:要获取的字段名列表
- `default_dict`:默认值字典,键为字段名,值为默认值 - `default_dict`:默认值字典,键为字段名,值为默认值
**返回:** **Returns:**
- `dict`:字段名到值的映射字典 - `dict`:字段名到值的映射字典
**示例:** #### 示例
```python ```python
values = await person_api.get_person_values( values = await person_api.get_person_values(
person_id, person_id,
@@ -67,204 +73,31 @@ values = await person_api.get_person_values(
) )
``` ```
### 3. 用户状态查询 ### 4. 判断用户是否已知
```python
#### `is_person_known(platform: str, user_id: int) -> bool` async def is_person_known(platform: str, user_id: int) -> bool:
```
判断是否认识某个用户 判断是否认识某个用户
**参数:** **Args:**
- `platform`:平台名称 - `platform`:平台名称
- `user_id`用户ID - `user_id`用户ID
**返回:** **Returns:**
- `bool`:是否认识该用户 - `bool`:是否认识该用户
**示例:** ### 5. 根据用户名获取Person ID
```python ```python
known = await person_api.is_person_known("qq", 123456) def get_person_id_by_name(person_name: str) -> str:
if known:
print("这个用户我认识")
``` ```
### 4. 用户名查询
#### `get_person_id_by_name(person_name: str) -> str`
根据用户名获取person_id 根据用户名获取person_id
**参数:** **Args:**
- `person_name`:用户名 - `person_name`:用户名
**返回:** **Returns:**
- `str`person_id如果未找到返回空字符串 - `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`:对用户的印象 - `impression`:对用户的印象
- `know_times`:交互次数 - `points`: 用户特征点
- `relationship_level`:关系等级
- `last_seen`:最后见面时间
- `last_interaction`:最后交互时间
### 个性化字段 其他字段可以参考`PersonInfo`类的属性(位于`src.common.database.database_model`
- `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
```
## 注意事项 ## 注意事项
1. **异步操作**部分查询函数都是异步的,需要使用`await` 1. **异步操作**:部分查询函数都是异步的,需要使用`await`
2. **错误处理**:所有函数都有错误处理,失败时记录日志并返回默认值 2. **性能考虑**:批量查询优于单个查询
3. **数据类型**返回的数据可能是字符串、数字或JSON需要适当处理 3. **隐私保护**:确保用户信息的使用符合隐私政策
4. **性能考虑**:批量查询优于单个查询 4. **数据一致性**person_id是用户的唯一标识应妥善保存和使用
5. **隐私保护**:确保用户信息的使用符合隐私政策
6. **数据一致性**person_id是用户的唯一标识应妥善保存和使用

View File

@@ -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]` - 成功加载的插件数量和失败的插件数量。

View File

@@ -6,86 +6,108 @@
```python ```python
from src.plugin_system.apis import send_api 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): 是否存储消息到数据库
**参数:** **Returns:**
- `text`:要发送的文本内容 - `bool` - 是否发送成功
- `group_id`群聊ID
- `platform`:平台,默认为"qq"
- `typing`:是否显示正在输入
- `reply_to`:回复消息的格式,如"发送者:消息内容"
- `storage_message`:是否存储到数据库
**返回:** ### 2. 发送表情包
- `bool`:是否发送成功 ```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): 是否存储消息到数据库
**参数:** **Returns:**
- `emoji_base64`表情包的base64编码 - `bool` - 是否发送成功
- `group_id`群聊ID
- `platform`:平台,默认为"qq"
- `storage_message`:是否存储到数据库
#### `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)` **Returns:**
向群聊发送命令 - `bool` - 是否发送成功
#### `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`:是否存储
## 使用示例 ## 使用示例
### 1. 基础文本发送 ### 1. 基础文本发送,并回复消息
```python ```python
from src.plugin_system.apis import send_api 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): async def send_hello(chat_stream):
"""发送问候消息""" """发送问候消息"""
if chat_stream.group_info: success = await send_api.text_to_stream(
# 群聊 text="Hello, world!",
success = await send_api.text_to_group( stream_id=chat_stream.stream_id,
text="大家好!", typing=True,
group_id=chat_stream.group_info.group_id, reply_to="User:How are you?",
typing=True storage_message=True
) )
else:
# 私聊
success = await send_api.text_to_user(
text="你好!",
user_id=chat_stream.user_info.user_id,
typing=True
)
return success return success
``` ```
### 2. 回复特定消息 ### 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. 发送表情包
```python ```python
from src.plugin_system.apis import emoji_api
async def send_emoji_reaction(chat_stream, emotion): 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) emoji_result = await emoji_api.get_by_emotion(emotion)
if not emoji_result: if not emoji_result:
@@ -152,107 +140,10 @@ async def send_emoji_reaction(chat_stream, emotion):
emoji_base64, description, matched_emotion = emoji_result emoji_base64, description, matched_emotion = emoji_result
# 发送表情包 # 发送表情包
if chat_stream.group_info: success = await send_api.emoji_to_stream(
success = await send_api.emoji_to_group( emoji_base64=emoji_base64,
emoji_base64=emoji_base64, stream_id=chat_stream.stream_id,
group_id=chat_stream.group_info.group_id storage_message=False # 不存储到数据库
)
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", "")
) )
return success 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` 1. **异步操作**:所有发送函数都是异步的,必须使用`await`

View File

@@ -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文件使用可读性好的缩进格式

View File

@@ -2,7 +2,9 @@
## 📖 什么是Command ## 📖 什么是Command
Command是直接响应用户明确指令的组件与Action不同Command是**被动触发**的,当用户输入特定格式的命令时立即执行。Command通过正则表达式匹配用户输入提供确定性的功能服务。 Command是直接响应用户明确指令的组件与Action不同Command是**被动触发**的,当用户输入特定格式的命令时立即执行。
Command通过正则表达式匹配用户输入提供确定性的功能服务。
### 🎯 Command的特点 ### 🎯 Command的特点
@@ -12,501 +14,76 @@ Command是直接响应用户明确指令的组件与Action不同Command是
- 🛑 **拦截控制**:可以控制是否阻止消息继续处理 - 🛑 **拦截控制**:可以控制是否阻止消息继续处理
- 📝 **参数解析**:支持从用户输入中提取参数 - 📝 **参数解析**:支持从用户输入中提取参数
## 🆚 Action vs Command 核心区别 ---
| 特征 | Action | Command | ## 🛠️ Command组件的基本结构
| ------------------ | --------------------- | ---------------- |
| **触发方式** | 麦麦主动决策使用 | 用户主动触发 |
| **决策机制** | 两层决策(激活+使用) | 直接匹配执行 |
| **随机性** | 有随机性和智能性 | 确定性执行 |
| **用途** | 增强麦麦行为拟人化 | 提供具体功能服务 |
| **性能影响** | 需要LLM决策 | 正则匹配,性能好 |
## 🏗️ Command基本结构 首先Command组件需要继承自`BaseCommand`类,并实现必要的方法。
### 必须属性
```python ```python
from src.plugin_system import BaseCommand class ExampleCommand(BaseCommand):
command_name = "example" # 命令名称,作为唯一标识符
command_description = "这是一个示例命令" # 命令描述
command_pattern = r"" # 命令匹配的正则表达式
class MyCommand(BaseCommand): async def execute(self) -> Tuple[bool, Optional[str], bool]:
# 正则表达式匹配模式 """
command_pattern = r"^/help\s+(?P<topic>\w+)$" 执行Command的主要逻辑
# 命令帮助说明 Returns:
command_help = "显示指定主题的帮助信息" Tuple[bool, str, bool]:
- 第一个bool表示是否成功执行
# 使用示例 - 第二个str是执行结果消息
command_examples = ["/help action", "/help command"] - 第三个bool表示是否需要阻止消息继续处理
"""
# 是否拦截后续处理 # ---- 执行命令的逻辑 ----
intercept_message = True return True, "执行成功", False
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行命令逻辑"""
# 命令执行逻辑
return True, "执行成功"
``` ```
**`command_pattern`**: 该Command匹配的正则表达式用于精确匹配用户输入。
### 属性说明 请注意:如果希望能获取到命令中的参数,请在正则表达式中使用有命名的捕获组,例如`(?P<param_name>pattern)`
| 属性 | 类型 | 说明 | 这样在匹配时,内部实现可以使用`re.match.groupdict()`方法获取到所有捕获组的参数,并以字典的形式存储在`self.matched_groups`中。
| --------------------- | --------- | -------------------- |
| `command_pattern` | str | 正则表达式匹配模式 |
| `command_help` | str | 命令帮助说明 |
| `command_examples` | List[str] | 使用示例列表 |
| `intercept_message` | bool | 是否拦截消息继续处理 |
## 🔍 正则表达式匹配 ### 匹配样例
假设我们有一个命令`/example param1=value1 param2=value2`,对应的正则表达式可以是:
### 基础匹配
```python ```python
class SimpleCommand(BaseCommand): class ExampleCommand(BaseCommand):
# 匹配 /ping command_name = "example"
command_pattern = r"^/ping$" command_description = "这是一个示例命令"
command_pattern = r"/example (?P<param1>\w+) (?P<param2>\w+)"
async def execute(self) -> Tuple[bool, Optional[str]]: async def execute(self) -> Tuple[bool, Optional[str], bool]:
await self.send_text("Pong!") # 获取匹配的参数
return True, "发送了Pong回复" param1 = self.matched_groups.get("param1")
param2 = self.matched_groups.get("param2")
# 执行逻辑
return True, f"参数1: {param1}, 参数2: {param2}", False
``` ```
### 参数捕获 ---
使用命名组 `(?P<n>pattern)` 捕获参数:
## Command 内置方法说明
```python ```python
class UserCommand(BaseCommand): class BaseCommand:
# 匹配 /user add 张三 或 /user del 李四 def get_config(self, key: str, default=None):
command_pattern = r"^/user\s+(?P<action>add|del|info)\s+(?P<username>\w+)$" """获取插件配置值,使用嵌套键访问"""
async def execute(self) -> Tuple[bool, Optional[str]]: async def send_text(self, content: str, reply_to: str = "") -> bool:
# 通过 self.matched_groups 获取捕获的参数 """发送回复消息"""
action = self.matched_groups.get("action")
username = self.matched_groups.get("username")
if action == "add": async def send_type(self, message_type: str, content: str, display_message: str = "", typing: bool = False, reply_to: str = "") -> bool:
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}操作" 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:
"""发送图片"""
``` ```
具体参数与用法参见`BaseCommand`基类的定义。
### 可选参数
```python
class HelpCommand(BaseCommand):
# 匹配 /help 或 /help topic
command_pattern = r"^/help(?:\s+(?P<topic>\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+(?P<action>add|del|list|info)\s*(?P<username>\w+)?(?:\s+--(?P<options>.+))?$"
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+(?P<type>system|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"^[!](?P<command>roll|dice)\s*(?P<count>\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<param>\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的场景
- ✅ 增强麦麦的智能行为
- ✅ 根据上下文自动触发
- ✅ 情绪和表情表达
- ✅ 智能建议和帮助
- ✅ 随机化的互动

View File

@@ -6,34 +6,6 @@
> >
> 系统会根据你在代码中定义的 `config_schema` 自动生成配置文件。手动创建配置文件会破坏自动化流程,导致配置不一致、缺失注释和文档等问题。 > 系统会根据你在代码中定义的 `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. **迁移配置值** - 将旧配置文件中的值迁移到新结构中 2. **迁移配置值** - 将旧配置文件中的值迁移到新结构中
3. **处理新增字段** - 新增的配置项使用默认值 3. **处理新增字段** - 新增的配置项使用默认值
4. **更新版本号** - `config_version` 字段自动更新为最新版本 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. 版本号管理 #### 1. 版本号管理
- 当你修改 `config_schema` 时,**必须同步更新** `config_version` - 当你修改 `config_schema` 时,**必须同步更新** `config_version`
- 建议使用语义化版本号 (例如:`1.0.0`, `1.1.0`, `2.0.0`) - 使用语义化版本号 (例如:`1.0.0`, `1.1.0`, `2.0.0`)
- 配置结构的重大变更应该增加主版本号
#### 2. 迁移策略 #### 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`)中完成,主要通过两个类属性: 配置的定义在你的插件主类(继承自 `BasePlugin`)中完成,主要通过两个类属性:
@@ -257,6 +176,7 @@ graph TD
每个配置项都通过一个 `ConfigField` 对象来定义。 每个配置项都通过一个 `ConfigField` 对象来定义。
```python ```python
from dataclasses import dataclass
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
@dataclass @dataclass
@@ -270,28 +190,21 @@ class ConfigField:
choices: Optional[List[Any]] = None # 可选值列表 (可选) choices: Optional[List[Any]] = None # 可选值列表 (可选)
``` ```
### 配置定义示例 ### 配置示例
让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。 让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。
```python ```python
# src/plugins/built_in/mute_plugin/plugin.py # src/plugins/built_in/mute_plugin/plugin.py
from src.plugin_system import BasePlugin, register_plugin from src.plugin_system import BasePlugin, register_plugin, ConfigField
from src.plugin_system.base.config_types import ConfigField
from typing import List, Tuple, Type from typing import List, Tuple, Type
@register_plugin @register_plugin
class MutePlugin(BasePlugin): 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: 定义配置节的描述 # 步骤1: 定义配置节的描述
config_section_descriptions = { 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` 时,系统会自动创建以下文件: `mute_plugin` 首次加载且其目录中不存在 `config.toml` 时,系统会自动创建以下文件:
```toml ```toml
@@ -413,317 +313,24 @@ prefix = "[MutePlugin]"
--- ---
## 配置访问在Action和Command中使用配置 ## 配置访问
### 问题描述 如果你想要在你的组件中访问配置,可以通过组件内置的 `get_config()` 方法访问配置。
在插件开发中,你可能遇到这样的问题 其参数为一个命名空间化的字符串。以上面的 `MutePlugin` 为例,你可以这样访问配置
- 想要在Action或Command中访问插件配置
### ✅ 解决方案
**直接使用 `self.get_config()` 方法!**
系统已经自动为你处理了配置传递,你只需要通过组件内置的 `get_config` 方法访问配置即可。
### 📖 快速示例
#### 在Action中访问配置
```python ```python
from src.plugin_system import BaseAction enable_smart_mute = self.get_config("components.enable_smart_mute", True)
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, "配置访问成功"
``` ```
#### 在Command中访问配置 如果尝试访问了一个不存在的配置项,系统会自动返回默认值(你传递的)或者 `None`
```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<username>\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, "问候发送成功"
```
--- ---
## 最佳实践与注意事项 ## 最佳实践与注意事项
### 配置定义最佳实践
> **🚨 核心原则:永远不要手动创建 config.toml 文件!** **🚨 核心原则:永远不要手动创建 config.toml 文件!**
1. **🔥 绝不手动创建配置文件**: **任何时候都不要手动创建 `config.toml` 文件**!必须通过在 `plugin.py` 中定义 `config_schema` 让系统自动生成。 1. **🔥 绝不手动创建配置文件**: **任何时候都不要手动创建 `config.toml` 文件**!必须通过在 `plugin.py` 中定义 `config_schema` 让系统自动生成。
-**禁止**`touch config.toml`、手动编写配置文件 -**禁止**`touch config.toml`、手动编写配置文件
@@ -738,75 +345,3 @@ class GreetingCommand(BaseCommand):
5. **gitignore**: 将 `plugins/*/config.toml``src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。 5. **gitignore**: 将 `plugins/*/config.toml``src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。
6. **配置文件只供修改**: 自动生成的 `config.toml` 文件只应该被用户**修改**,而不是从零创建。 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`方法和自动化的配置生成机制已经为你准备好了一切!

View File

@@ -1,93 +1,6 @@
# 📦 插件依赖管理系统 # 📦 插件依赖管理系统
> 🎯 **简介**MaiBot插件系统提供了强大的Python依赖管理功能,让插件开发更加便捷和可靠 现在的Python依赖管理依然存在问题,请保留你的`python_dependencies`属性,等待后续重构
## ✨ 功能概述
### 🎯 核心能力
- **声明式依赖**插件可以明确声明需要的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")
```
## 📚 详细教程 ## 📚 详细教程
@@ -97,11 +10,11 @@ plugin_manager.generate_plugin_requirements("plugin_requirements.txt")
```python ```python
PythonDependency( PythonDependency(
package_name="requests", # 导入时的包名 package_name="PIL", # 导入时的包名
version=">=2.25.0", # 版本要求 version=">=11.2.0", # 版本要求
optional=False, # 是否为可选依赖 optional=False, # 是否为可选依赖
description="HTTP请求库", # 依赖描述 description="图像处理库", # 依赖描述
install_name="" # pip安装时的包名可选 install_name="pillow" # pip安装时的包名可选
) )
``` ```
@@ -110,10 +23,10 @@ PythonDependency(
| 参数 | 类型 | 必需 | 说明 | | 参数 | 类型 | 必需 | 说明 |
|------|------|------|------| |------|------|------|------|
| `package_name` | str | ✅ | Python导入时使用的包名`requests` | | `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` | | `optional` | bool | ❌ | 是否为可选依赖,默认`False` |
| `description` | str | ❌ | 依赖的用途描述 | | `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") # 排除特定版本 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'])
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -4,15 +4,34 @@
## 新手入门 ## 新手入门
- [📖 快速开始指南](quick-start.md) - 5分钟创建你的第一个插件 - [📖 快速开始指南](quick-start.md) - 快速创建你的第一个插件
## 组件功能详解 ## 组件功能详解
- [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件 - [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件
- [💻 Command组件详解](command-components.md) - 学习直接响应命令的组件 - [💻 Command组件详解](command-components.md) - 学习直接响应命令的组件
- [⚙️ 配置管理指南](configuration-guide.md) - 学会使用自动生成的插件配置文件 - [⚙️ 配置文件系统指南](configuration-guide.md) - 学会使用自动生成的插件配置文件
- [📄 Manifest系统指南](manifest-guide.md) - 了解插件元数据管理和配置架构 - [📄 Manifest系统指南](manifest-guide.md) - 了解插件元数据管理和配置架构
Command vs Action 选择指南
1. 使用Command的场景
- ✅ 用户需要明确调用特定功能
- ✅ 需要精确的参数控制
- ✅ 管理和配置操作
- ✅ 查询和信息显示
- ✅ 系统维护命令
2. 使用Action的场景
- ✅ 增强麦麦的智能行为
- ✅ 根据上下文自动触发
- ✅ 情绪和表情表达
- ✅ 智能建议和帮助
- ✅ 随机化的互动
## API浏览 ## API浏览
### 消息发送与处理API ### 消息发送与处理API
@@ -24,19 +43,22 @@
- [LLM API](api/llm-api.md) - 大语言模型交互接口可以使用内置LLM生成内容 - [LLM API](api/llm-api.md) - 大语言模型交互接口可以使用内置LLM生成内容
- [✨ 回复生成器API](api/generator-api.md) - 智能回复生成接口,可以使用内置风格化生成器 - [✨ 回复生成器API](api/generator-api.md) - 智能回复生成接口,可以使用内置风格化生成器
### 表情包api ### 表情包API
- [😊 表情包API](api/emoji-api.md) - 表情包选择和管理接口 - [😊 表情包API](api/emoji-api.md) - 表情包选择和管理接口
### 关系系统api ### 关系系统API
- [人物信息API](api/person-api.md) - 用户信息,处理麦麦认识的人和关系的接口 - [人物信息API](api/person-api.md) - 用户信息,处理麦麦认识的人和关系的接口
### 数据与配置API ### 数据与配置API
- [🗄️ 数据库API](api/database-api.md) - 数据库操作接口 - [🗄️ 数据库API](api/database-api.md) - 数据库操作接口
- [⚙️ 配置API](api/config-api.md) - 配置读取和用户信息接口 - [⚙️ 配置API](api/config-api.md) - 配置读取和用户信息接口
### 工具API ### 插件和组件管理API
- [工具API](api/utils-api.md) - 文件操作、时间处理等工具函数 - [🔌 插件API](api/plugin-manage-api.md) - 插件加载和管理接口
- [🧩 组件API](api/component-manage-api.md) - 组件注册和管理接口
### 日志API
- [📜 日志API](api/logging-api.md) - logger实例获取接口
## 实验性 ## 实验性
@@ -53,3 +75,9 @@
2. 查看相关示例代码 2. 查看相关示例代码
3. 参考其他类似插件 3. 参考其他类似插件
4. 提交文档仓库issue 4. 提交文档仓库issue
## 一个方便的小设计
我们在`__init__.py`中定义了一个`__all__`变量,包含了所有需要导出的类和函数。
这样在其他地方导入时,可以直接使用 `from src.plugin_system import *` 来导入所有插件相关的类和函数。
或者你可以直接使用 `from src.plugin_system import BasePlugin, register_plugin, ComponentInfo` 之类的方式来导入你需要的部分。

View File

@@ -147,7 +147,7 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin
## 📋 字段说明 ## 📋 字段说明
### 基本信息 ### 基本信息
- `manifest_version`: manifest格式版本当前为3 - `manifest_version`: manifest格式版本当前为1
- `name`: 插件显示名称(必需) - `name`: 插件显示名称(必需)
- `version`: 插件版本号(必需) - `version`: 插件版本号(必需)
- `description`: 插件功能描述(必需) - `description`: 插件功能描述(必需)
@@ -165,10 +165,12 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin
- `categories`: 分类数组(可选,建议填写) - `categories`: 分类数组(可选,建议填写)
### 兼容性 ### 兼容性
- `host_application`: 主机应用兼容性(可选) - `host_application`: 主机应用兼容性(可选,建议填写
- `min_version`: 最低兼容版本 - `min_version`: 最低兼容版本
- `max_version`: 最高兼容版本 - `max_version`: 最高兼容版本
⚠️ 在不填写的情况下,插件将默认支持所有版本。**(由于我们在不同版本对插件系统进行了大量的重构,这种情况几乎不可能。)**
### 国际化 ### 国际化
- `default_locale`: 默认语言(可选) - `default_locale`: 默认语言(可选)
- `locales_path`: 语言文件目录(可选) - `locales_path`: 语言文件目录(可选)
@@ -185,24 +187,13 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin
2. **编码格式**manifest文件必须使用UTF-8编码 2. **编码格式**manifest文件必须使用UTF-8编码
3. **JSON格式**文件必须是有效的JSON格式 3. **JSON格式**文件必须是有效的JSON格式
4. **必需字段**`manifest_version`、`name`、`version`、`description`、`author.name`是必需的 4. **必需字段**`manifest_version`、`name`、`version`、`description`、`author.name`是必需的
5. **版本兼容**当前只支持manifest_version = 3 5. **版本兼容**:当前只支持`manifest_version = 1`
## 🔍 常见问题 ## 🔍 常见问题
### Q: 为什么要强制要求manifest文件
A: Manifest文件提供了插件的标准化元数据使得插件管理、依赖检查、版本兼容性验证等功能成为可能。
### Q: 可以不填写可选字段吗? ### Q: 可以不填写可选字段吗?
A: 可以。所有标记为"可选"的字段都可以不填写,但建议至少填写`license`和`keywords`。 A: 可以。所有标记为"可选"的字段都可以不填写,但建议至少填写`license`和`keywords`。
### Q: 如何快速为所有插件创建manifest
A: 可以编写脚本批量处理:
```bash
# 扫描并为每个缺少manifest的插件创建最小化manifest
python scripts/manifest_tool.py scan src/plugins
# 然后手动为每个插件运行create-minimal命令
```
### Q: manifest验证失败怎么办 ### Q: manifest验证失败怎么办
A: 根据验证器的错误提示修复相应问题。错误会导致插件加载失败,警告不会。 A: 根据验证器的错误提示修复相应问题。错误会导致插件加载失败,警告不会。
@@ -210,5 +201,5 @@ A: 根据验证器的错误提示修复相应问题。错误会导致插件加
查看内置插件的manifest文件作为参考 查看内置插件的manifest文件作为参考
- `src/plugins/built_in/core_actions/_manifest.json` - `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/built_in/tts_plugin/_manifest.json`
- `src/plugins/hello_world_plugin/_manifest.json`

View File

@@ -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. 创建插件目录 ### 1. 创建插件目录
在项目根目录的 `plugins/` 文件夹下创建你的插件目录,目录名与插件名保持一致: 在项目根目录的 `plugins/` 文件夹下创建你的插件目录
可以用以下命令快速创建: 这里我们创建一个名为 `hello_world_plugin` 的目录
```bash ### 2. 创建`_manifest.json`文件
mkdir plugins/hello_world_plugin
cd plugins/hello_world_plugin 在插件目录下面创建一个 `_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` 文件: 让我们从最基础的开始!创建 `plugin.py` 文件:
@@ -43,34 +56,33 @@ cd plugins/hello_world_plugin
from typing import List, Tuple, Type from typing import List, Tuple, Type
from src.plugin_system import BasePlugin, register_plugin, ComponentInfo from src.plugin_system import BasePlugin, register_plugin, ComponentInfo
# ===== 插件注册 ===== @register_plugin # 注册插件
@register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息(必须填写) # 以下是插件基本信息和方法(必须填写)
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True # 启用插件 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 [] return []
``` ```
🎉 **恭喜你刚刚创建了一个最简单但完整的MaiCore插件** 🎉 恭喜你刚刚创建了一个最简单但完整的MaiCore插件
**解释一下这些代码:** **解释一下这些代码:**
- 首先我们在plugin.py中定义了一个HelloWorldPulgin插件类继承自 `BasePlugin` ,提供基本功能。 - 首先,我们在`plugin.py`中定义了一个HelloWorldPlugin插件类继承自 `BasePlugin` ,提供基本功能。
- 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件" - 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件"
- `plugin_name` 等是插件的基本信息,必须填写**此部分必须与目录名称相同,否则插件无法使用** - `plugin_name` 等是插件的基本信息,必须填写
- `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何action动作或者command(指令),是空的 - `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何 Action, Command 或者 EventHandler所以返回空列表。
### 3. 测试基础插件 ### 4. 测试基础插件
现在就可以测试这个插件了启动MaiCore 现在就可以测试这个插件了启动MaiCore
@@ -80,7 +92,7 @@ class HelloWorldPlugin(BasePlugin):
![1750326700269](image/quick-start/1750326700269.png) ![1750326700269](image/quick-start/1750326700269.png)
### 4. 添加第一个功能问候Action ### 5. 添加第一个功能问候Action
现在我们要给插件加入一个有用的功能我们从最好玩的Action做起 现在我们要给插件加入一个有用的功能我们从最好玩的Action做起
@@ -107,40 +119,34 @@ class HelloAction(BaseAction):
# === 基本信息(必须填写)=== # === 基本信息(必须填写)===
action_name = "hello_greeting" action_name = "hello_greeting"
action_description = "向用户发送问候消息" action_description = "向用户发送问候消息"
activation_type = ActionActivationType.ALWAYS # 始终激活
# === 功能描述(必须填写)=== # === 功能描述(必须填写)===
action_parameters = { action_parameters = {"greeting_message": "要发送的问候消息"}
"greeting_message": "要发送的问候消息" action_require = ["要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"]
}
action_require = [
"需要发送友好问候时使用",
"当有人向你问好时使用",
"当你遇见没有见过的人时使用"
]
associated_types = ["text"] associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行问候动作 - 这是核心功能""" """执行问候动作 - 这是核心功能"""
# 发送问候消息 # 发送问候消息
greeting_message = self.action_data.get("greeting_message","") greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = "嗨!很开心见到你!😊" + greeting_message message = base_message + greeting_message
await self.send_text(message) await self.send_text(message)
return True, "发送了问候消息" return True, "发送了问候消息"
# ===== 插件注册 =====
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息 # 插件基本信息
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件包含问候功能"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True enable_plugin = True
dependencies = []
python_dependencies = []
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]]:
"""返回插件包含的组件列表""" """返回插件包含的组件列表"""
@@ -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选择后具体要做什么 - `execute()` 函数是Action的核心定义了当Action被MaiCore选择后具体要做什么
- `self.send_text()` 是发送文本消息的便捷方法 - `self.send_text()` 是发送文本消息的便捷方法
### 5. 测试问候功能 Action 组件中有关`activation_type``action_parameters``action_require``associated_types` 等的详细说明请参考 [Action组件指南](./action-components.md)。
### 6. 测试问候Action
重启MaiCore然后在聊天中发送任意消息比如 重启MaiCore然后在聊天中发送任意消息比如
@@ -174,96 +184,17 @@ MaiCore可能会选择使用你的问候Action发送回复
> **💡 小提示**MaiCore会智能地决定什么时候使用它。如果没有立即看到效果多试几次不同的消息。 > **💡 小提示**MaiCore会智能地决定什么时候使用它。如果没有立即看到效果多试几次不同的消息。
🎉 **太棒了!你的插件已经有实际功能了!** 🎉 太棒了!你的插件已经有实际功能了!
### 5.5. 了解激活系统(重要概念) ### 7. 添加第二个功能时间查询Command
Action固然好用简单但是现在有个问题当用户加载了非常多的插件添加了很多自定义ActionLLM需要选择的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
现在让我们添加一个Command组件。Command和Action不同它是直接响应用户命令的 现在让我们添加一个Command组件。Command和Action不同它是直接响应用户命令的
Command是最简单最直接的不由LLM判断选择使用 Command是最简单最直接的不由LLM判断选择使用
```python ```python
# 在现有代码基础上添加Command组件 # 在现有代码基础上添加Command组件
import datetime
# ===== Command组件 =====
from src.plugin_system import BaseCommand from src.plugin_system import BaseCommand
#导入Command基类 #导入Command基类
@@ -275,16 +206,11 @@ class TimeCommand(BaseCommand):
# === 命令设置(必须填写)=== # === 命令设置(必须填写)===
command_pattern = r"^/time$" # 精确匹配 "/time" 命令 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() now = datetime.datetime.now()
time_str = now.strftime(time_format) time_str = now.strftime(time_format)
@@ -292,36 +218,37 @@ class TimeCommand(BaseCommand):
message = f"⏰ 当前时间:{time_str}" message = f"⏰ 当前时间:{time_str}"
await self.send_text(message) await self.send_text(message)
return True, f"显示了当前时间: {time_str}" return True, f"显示了当前时间: {time_str}", True
# ===== 插件注册 =====
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件包含问候和时间查询功能"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True enable_plugin = True
dependencies = []
python_dependencies = []
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 [ return [
(HelloAction.get_action_info(), HelloAction), (HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction),
(TimeCommand.get_command_info(), TimeCommand), (TimeCommand.get_command_info(), TimeCommand),
] ]
``` ```
同样的,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `TimeCommand` 注册为插件的一个组件。
**Command组件解释** **Command组件解释**
- Command是直接响应用户命令的组件
- `command_pattern` 使用正则表达式匹配用户输入 - `command_pattern` 使用正则表达式匹配用户输入
- `^/time$` 表示精确匹配 "/time" - `^/time$` 表示精确匹配 "/time"
- `intercept_message = True` 表示处理完命令后不再让其他组件处理
### 7. 测试时间查询功能 有关 Command 组件的更多信息,请参考 [Command组件指南](./command-components.md)。
### 8. 测试时间查询Command
重启MaiCore发送命令 重启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文件** > **🚨 重要不要手动创建config.toml文件**
> >
> 我们需要在插件代码中定义配置Schema让系统自动生成配置文件。 > 我们需要在插件代码中定义配置Schema让系统自动生成配置文件。
#### 📄 配置架构说明
在新的插件系统中,我们采用了**职责分离**的设计:
- **`_manifest.json`** - 插件元数据(名称、版本、描述、作者等)
- **`config.toml`** - 运行时配置(启用状态、功能参数等)
这样避免了信息重复,提高了维护性。
首先在插件类中定义配置Schema 首先在插件类中定义配置Schema
```python ```python
from src.plugin_system.base.config_types import ConfigField from src.plugin_system import ConfigField
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
plugin_name = "hello_world_plugin" # 插件基本信息
plugin_description = "我的第一个MaiCore插件包含问候和时间查询功能" plugin_name: str = "hello_world_plugin" # 内部标识符
plugin_version = "1.0.0" enable_plugin: bool = True
plugin_author = "你的名字" dependencies: List[str] = [] # 插件依赖列表
enable_plugin = True python_dependencies: List[str] = [] # Python包依赖列表
config_file_name = "config.toml" # 配置文件名 config_file_name: str = "config.toml" # 配置文件名
# 配置节描述
config_section_descriptions = {
"plugin": "插件启用配置",
"greeting": "问候功能配置",
"time": "时间查询配置"
}
# 配置Schema定义 # 配置Schema定义
config_schema = { config_schema: dict = {
"plugin": { "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": { "greeting": {
"message": ConfigField( "message": ConfigField(type=str, default="嗨!很开心见到你!😊", description="默认问候消息"),
type=str, "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"),
default="嗨!很开心见到你!😊",
description="默认问候消息"
),
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号")
}, },
"time": { "time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")},
"format": ConfigField(
type=str,
default="%Y-%m-%d %H:%M:%S",
description="时间显示格式"
)
}
} }
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [ return [
(HelloAction.get_action_info(), HelloAction), (HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction),
(TimeCommand.get_command_info(), TimeCommand), (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 ```python
# 在HelloAction的execute方法中 class HelloAction(BaseAction):
async def execute(self) -> Tuple[bool, str]: """问候Action - 简单的问候动作"""
# 从配置文件读取问候消息
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) action_name = "hello_greeting"
return True, "发送问候消息" action_description = "向用户发送问候消息"
activation_type = ActionActivationType.ALWAYS # 始终激活
# 在TimeCommand的execute方法中 # === 功能描述(必须填写)===
async def execute(self) -> Tuple[bool, str]: action_parameters = {"greeting_message": "要发送的问候消息"}
import datetime action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"]
associated_types = ["text"]
# 从配置文件读取时间格式 async def execute(self) -> Tuple[bool, str]:
time_format = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") """执行问候动作 - 这是核心功能"""
now = datetime.datetime.now() # 发送问候消息
time_str = now.strftime(time_format) 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)
message = f"⏰ 当前时间:{time_str}" return True, "发送了问候消息"
await self.send_text(message)
return True, f"显示了当前时间: {time_str}" 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. **用户修改**: 用户可以修改生成的配置文件 3. **用户修改**: 用户可以修改生成的配置文件
4. **代码读取**: 使用 `self.get_config()` 读取配置值 4. **代码读取**: 使用 `self.get_config()` 读取配置值
**配置功能解释:** **绝对不要手动创建 `config.toml` 文件!**
- `self.get_config()` 可以读取配置文件中的值 更详细的配置系统介绍请参考 [配置指南](./configuration-guide.md)。
- 第一个参数是配置路径(用点分隔),第二个参数是默认值
- 配置文件会包含详细的注释和说明,用户可以轻松理解和修改
- **绝不要手动创建配置文件**,让系统自动生成
### 9. 创建说明文档(可选) ### 2. 创建说明文档
创建 `README.md` 文件来说明你的插件: 你可以创建一个 `README.md` 文件,描述插件的功能和使用方法。
```markdown ### 3. 发布到插件市场
# Hello World 插件
## 概述 如果你想让更多人使用你的插件可以将它发布到MaiCore的插件市场。
我的第一个MaiCore插件包含问候和时间查询功能。
## 功能 这部分请参考 [plugin-repo](https://github.com/Maim-with-u/plugin-repo) 的文档。
- **问候功能**: 当用户说"你好"、"hello"、"hi"时自动回复
- **时间查询**: 发送 `/time` 命令查询当前时间
## 使用方法 ---
### 问候功能
发送包含以下关键词的消息:
- "你好"
- "hello"
- "hi"
### 时间查询 🎉 恭喜你!你已经成功的创建了自己的插件了!
发送命令:`/time`
## 配置文件
插件会自动生成 `config.toml` 配置文件,用户可以修改:
- 问候消息内容
- 时间显示格式
- 插件启用状态
注意:配置文件是自动生成的,不要手动创建!
```
```
```

View File

@@ -1,13 +1,12 @@
# 🔧 工具系统详解 # 🔧 工具系统详解
## 📖 什么是工具系统 ## 📖 什么是工具
工具系统是MaiBot的信息获取能力扩展组件**专门用于在Focus模式下扩宽麦麦能够获得的信息量**。如果说Action组件功能五花八门可以拓展麦麦能做的事情那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。 工具是MaiBot的信息获取能力扩展组件。如果说Action组件功能五花八门可以拓展麦麦能做的事情那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。
### 🎯 工具系统的特点 ### 🎯 工具的特点
- 🔍 **信息获取增强**:扩展麦麦获取外部信息的能力 - 🔍 **信息获取增强**:扩展麦麦获取外部信息的能力
- 🎯 **Focus模式专用**:仅在专注聊天模式下工作,必须开启工具处理器
- 📊 **数据丰富**:帮助麦麦获得更多背景信息和实时数据 - 📊 **数据丰富**:帮助麦麦获得更多背景信息和实时数据
- 🔌 **插件式架构**:支持独立开发和注册新工具 - 🔌 **插件式架构**:支持独立开发和注册新工具
-**自动发现**:工具会被系统自动识别和注册 -**自动发现**:工具会被系统自动识别和注册
@@ -17,19 +16,15 @@
| 特征 | Action | Command | Tool | | 特征 | Action | Command | Tool |
|-----|-------|---------|------| |-----|-------|---------|------|
| **主要用途** | 扩展麦麦行为能力 | 响应用户指令 | 扩展麦麦信息获取 | | **主要用途** | 扩展麦麦行为能力 | 响应用户指令 | 扩展麦麦信息获取 |
| **适用模式** | 所有模式 | 所有模式 | 仅Focus模式 |
| **触发方式** | 麦麦智能决策 | 用户主动触发 | LLM根据需要调用 | | **触发方式** | 麦麦智能决策 | 用户主动触发 | LLM根据需要调用 |
| **目标** | 让麦麦做更多事情 | 提供具体功能 | 让麦麦知道更多信息 | | **目标** | 让麦麦做更多事情 | 提供具体功能 | 让麦麦知道更多信息 |
| **使用场景** | 增强交互体验 | 功能服务 | 信息查询和分析 | | **使用场景** | 增强交互体验 | 功能服务 | 信息查询和分析 |
## 🏗️ 工具基本结构 ## 🏗️ Tool组件的基本结构
### 必要组件
每个工具必须继承 `BaseTool` 基类并实现以下属性和方法: 每个工具必须继承 `BaseTool` 基类并实现以下属性和方法:
```python ```python
from src.tools.tool_can_use.base_tool import BaseTool, register_tool from src.plugin_system import BaseTool
class MyTool(BaseTool): class MyTool(BaseTool):
# 工具名称,必须唯一 # 工具名称,必须唯一
@@ -54,7 +49,9 @@ class MyTool(BaseTool):
"required": ["query"] "required": ["query"]
} }
async def execute(self, function_args, message_txt=""): available_for_llm = True # 是否对LLM可用
async def execute(self, function_args: Dict[str, Any]):
"""执行工具逻辑""" """执行工具逻辑"""
# 实现工具功能 # 实现工具功能
result = f"查询结果: {function_args.get('query')}" result = f"查询结果: {function_args.get('query')}"
@@ -63,9 +60,6 @@ class MyTool(BaseTool):
"name": self.name, "name": self.name,
"content": result "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 ```python
from src.tools.tool_can_use.base_tool import BaseTool, register_tool from src.plugin_system import BaseTool
import aiohttp import aiohttp
import json import json
@@ -192,217 +163,16 @@ class WeatherTool(BaseTool):
💧 湿度: {humidity}% 💧 湿度: {humidity}%
━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━
""".strip() """.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模式下可用 1. **适用范围**:主要适用于信息获取场景
2. **独立开发**:需要单独编写,暂未完全融入插件系统 2. **配置要求**:必须开启工具处理器
3. **适用范围**:主要适用于信息获取场景
4. **配置要求**:必须开启工具处理器
### 未来改进
工具系统在之后可能会面临以下修改:
1. **插件系统融合**:更好地集成到插件系统中
2. **模式扩展**:可能扩展到其他聊天模式
3. **配置简化**:简化配置和部署流程
4. **性能优化**:提升工具调用效率
### 开发建议 ### 开发建议

View File

@@ -1,9 +1,10 @@
from typing import List, Tuple, Type from typing import List, Tuple, Type, Any
from src.plugin_system import ( from src.plugin_system import (
BasePlugin, BasePlugin,
register_plugin, register_plugin,
BaseAction, BaseAction,
BaseCommand, BaseCommand,
BaseTool,
ComponentInfo, ComponentInfo,
ActionActivationType, ActionActivationType,
ConfigField, 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组件 ===== # ===== Action组件 =====
class HelloAction(BaseAction): class HelloAction(BaseAction):
"""问候Action - 简单的问候动作""" """问候Action - 简单的问候动作"""
@@ -132,7 +172,9 @@ class HelloWorldPlugin(BasePlugin):
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"), "enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
}, },
"greeting": { "greeting": {
"message": ConfigField(type=str, default="嗨!很开心见到你!😊", description="默认问候消息"), "message": ConfigField(
type=list, default=["嗨!很开心见到你!😊", "Ciallo(∠・ω< )⌒★"], description="默认问候消息"
),
"enable_emoji": ConfigField(type=bool, default=True, 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="时间显示格式")},
@@ -142,6 +184,7 @@ class HelloWorldPlugin(BasePlugin):
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [ return [
(HelloAction.get_action_info(), HelloAction), (HelloAction.get_action_info(), HelloAction),
(CompareNumbersTool.get_tool_info(), CompareNumbersTool), # 添加比较数字工具
(ByeAction.get_action_info(), ByeAction), # 添加告别Action (ByeAction.get_action_info(), ByeAction), # 添加告别Action
(TimeCommand.get_command_info(), TimeCommand), (TimeCommand.get_command_info(), TimeCommand),
(PrintMessage.get_handler_info(), PrintMessage), (PrintMessage.get_handler_info(), PrintMessage),

View File

@@ -1425,3 +1425,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -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()

View File

@@ -2,7 +2,7 @@ import asyncio
import time import time
import traceback import traceback
import random import random
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any, Tuple
from rich.traceback import install from rich.traceback import install
from src.config.config import global_config 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.relationship_builder_manager import relationship_builder_manager
from src.person_info.person_info import get_person_info_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.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.chat.willing.willing_manager import get_willing_manager
from src.mais4u.mai_think import mai_thinking_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.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 = { ERROR_LOOP_INFO = {
"loop_plan_info": { "loop_plan_info": {
@@ -88,11 +89,6 @@ class HeartFChatting:
self.loop_mode = ChatMode.NORMAL # 初始循环模式为普通模式 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_manager = ActionManager()
self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager) 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) 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.last_read_time = time.time() - 1
self.willing_amplifier = 1
self.willing_manager = get_willing_manager() self.willing_manager = get_willing_manager()
logger.info(f"{self.log_prefix} HeartFChatting 初始化完成") logger.info(f"{self.log_prefix} HeartFChatting 初始化完成")
@@ -182,6 +177,9 @@ class HeartFChatting:
if self.loop_mode == ChatMode.NORMAL: if self.loop_mode == ChatMode.NORMAL:
self.energy_value -= 0.3 self.energy_value -= 0.3
self.energy_value = max(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): def print_cycle_info(self, cycle_timers):
# 记录循环信息和计时器结果 # 记录循环信息和计时器结果
@@ -200,9 +198,9 @@ class HeartFChatting:
async def _loopbody(self): async def _loopbody(self):
if self.loop_mode == ChatMode.FOCUS: if self.loop_mode == ChatMode.FOCUS:
if await self._observe(): if await self._observe():
self.energy_value -= 1 * global_config.chat.focus_value self.energy_value -= 1 / global_config.chat.focus_value
else: else:
self.energy_value -= 3 * global_config.chat.focus_value self.energy_value -= 3 / global_config.chat.focus_value
if self.energy_value <= 1: if self.energy_value <= 1:
self.energy_value = 1 self.energy_value = 1
self.loop_mode = ChatMode.NORMAL self.loop_mode = ChatMode.NORMAL
@@ -218,15 +216,17 @@ class HeartFChatting:
limit_mode="earliest", limit_mode="earliest",
filter_bot=True, 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: if self.energy_value >= 30:
self.loop_mode = ChatMode.FOCUS self.loop_mode = ChatMode.FOCUS
self.energy_value = 10 + (len(new_messages_data) / (3 * global_config.chat.focus_value)) * 10 return True
return True
if self.energy_value >= 30 * global_config.chat.focus_value:
self.loop_mode = ChatMode.FOCUS
return True
if new_messages_data: if new_messages_data:
earliest_messages_data = new_messages_data[0] earliest_messages_data = new_messages_data[0]
@@ -235,10 +235,10 @@ class HeartFChatting:
if_think = await self.normal_response(earliest_messages_data) if_think = await self.normal_response(earliest_messages_data)
if if_think: if if_think:
factor = max(global_config.chat.focus_value, 0.1) 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}") logger.info(f"{self.log_prefix} 进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}")
else: 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}")
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") person_name = await person_info_manager.get_value(person_id, "person_name")
return f"{person_name}:{message_data.get('processed_plain_text')}" return f"{person_name}:{message_data.get('processed_plain_text')}"
async def send_typing(self): async def _send_and_store_reply(
group_info = GroupInfo(platform="amaidesu_default", group_id="114514", group_name="内心") 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( # 存储reply action信息
platform="amaidesu_default", person_info_manager = get_person_info_manager()
user_info=None, person_id = person_info_manager.get_person_id(
group_info=group_info, 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): return loop_info, reply_text, cycle_timers
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
)
async def _observe(self, message_data: Optional[Dict[str, Any]] = None): async def _observe(self, message_data: Optional[Dict[str, Any]] = None):
# sourcery skip: hoist-statement-from-if, merge-comparisons, reintroduce-else # sourcery skip: hoist-statement-from-if, merge-comparisons, reintroduce-else
if not message_data: if not message_data:
message_data = {} message_data = {}
action_type = "no_action" 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() cycle_timers, thinking_id = self.start_cycle()
logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考[模式:{self.loop_mode}]") logger.info(f"{self.log_prefix} 开始第{self._cycle_counter}次思考[模式:{self.loop_mode}]")
if ENABLE_S4U: 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()): async with global_prompt_manager.async_message_scope(self.chat_stream.context.get_template_name()):
loop_start_time = time.time() loop_start_time = time.time()
@@ -310,95 +335,254 @@ class HeartFChatting:
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 动作修改失败: {e}") logger.error(f"{self.log_prefix} 动作修改失败: {e}")
# 如果normal开始一个回复生成进程先准备好回复其实是和planer同时进行的 # 检查是否在normal模式下没有可用动作除了reply相关动作
skip_planner = False
if self.loop_mode == ChatMode.NORMAL: if self.loop_mode == ChatMode.NORMAL:
reply_to_str = await self.build_reply_to_str(message_data) # 过滤掉reply相关的动作检查是否还有其他动作
gen_task = asyncio.create_task(self._generate_response(message_data, available_actions, reply_to_str)) 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): if not non_reply_actions:
plan_result, target_message = await self.action_planner.plan(mode=self.loop_mode) skip_planner = True
logger.info(f"{self.log_prefix} Normal模式下没有可用动作直接回复")
action_result: dict = plan_result.get("action_result", {}) # type: ignore # 直接设置为reply动作
action_type, action_data, reasoning, is_parallel = ( action_type = "reply"
action_result.get("action_type", "error"), reasoning = ""
action_result.get("action_data", {}), action_data = {"loop_start_time": loop_start_time}
action_result.get("reasoning", "未提供理由"), is_parallel = False
action_result.get("is_parallel", True),
)
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: # 如果normal模式且不跳过规划器开始一个回复生成进程先准备好回复其实是和planer同时进行的
if action_type == "no_action": if not skip_planner:
logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复") reply_to_str = await self.build_reply_to_str(message_data)
elif is_parallel: gen_task = asyncio.create_task(
logger.info( self._generate_response(
f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作" 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 if self.loop_mode == ChatMode.NORMAL:
try: # 只有在gen_task存在时才等待
response_set = await asyncio.wait_for(gen_task, timeout=gather_timeout) if not gen_task:
except asyncio.TimeoutError: reply_to_str = await self.build_reply_to_str(message_data)
response_set = None 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: gather_timeout = global_config.chat.thinking_timeout
content = " ".join([item[1] for item in response_set if item[0] == "text"]) 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: if not response_set:
logger.warning(f"[{self.log_prefix}] 模型未生成回复内容") logger.warning(f"{self.log_prefix}模型未生成回复内容")
return False return False
elif action_type not in ["no_action"] and not is_parallel: else:
logger.info( logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复 (focus模式)")
f"[{self.log_prefix}] {global_config.bot.nickname} 原本想要回复:{content},但选择执行{action_type},不发表回复"
)
return False
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) 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
)
if ENABLE_S4U:
await self.stop_typing()
await mai_thinking_manager.get_mai_think(self.stream_id).do_think_after_response(reply_text)
return True return True
else: 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:
# 动作执行计时 async def handle_reply_task() -> Tuple[Optional[Dict[str, Any]], str, Dict[str, float]]:
with Timer("动作执行", cycle_timers): # 等待预生成的回复任务完成
success, reply_text, command = await self._handle_action( gather_timeout = global_config.chat.thinking_timeout
action_type, reasoning, action_data, cycle_timers, thinking_id, action_message 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_task_result = results[1]
"action_result": plan_result.get("action_result", {}), if isinstance(action_task_result, BaseException):
}, logger.error(f"{self.log_prefix} 动作任务执行异常: {action_task_result}")
"loop_action_info": { else:
"action_taken": success, action_success, action_reply_text, action_command = action_task_result
"reply_text": reply_text, else:
"command": command, results = await asyncio.gather(background_action_task, return_exceptions=True)
"taken_time": time.time(), # 只有动作任务
}, 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} 麦麦决定停止专注聊天") if reply_loop_info:
return False # 如果有回复信息使用回复的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.end_cycle(loop_info, cycle_timers)
self.print_cycle_info(cycle_timers) self.print_cycle_info(cycle_timers)
@@ -406,8 +590,16 @@ class HeartFChatting:
if self.loop_mode == ChatMode.NORMAL: if self.loop_mode == ChatMode.NORMAL:
await self.willing_manager.after_generate_reply_handle(message_data.get("message_id", "")) 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": 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 return True
elif action_type == "no_action":
# 当执行回复动作时也重置no_reply计数器s
NoReplyAction.reset_consecutive_count()
logger.info(f"{self.log_prefix} 执行了回复动作重置no_reply计数器")
return True return True
@@ -435,7 +627,7 @@ class HeartFChatting:
action: str, action: str,
reasoning: str, reasoning: str,
action_data: dict, action_data: dict,
cycle_timers: dict, cycle_timers: Dict[str, float],
thinking_id: str, thinking_id: str,
action_message: dict, action_message: dict,
) -> tuple[bool, str, str]: ) -> 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) 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 += additional_config["maimcore_reply_probability_gain"]
reply_probability = min(max(reply_probability, 0), 1) # 确保概率在 0-1 之间 reply_probability = min(max(reply_probability, 0), 1) # 确保概率在 0-1 之间
talk_frequency = global_config.chat.get_current_talk_frequency(self.stream_id) talk_frequency = global_config.chat.get_current_talk_frequency(self.stream_id)
reply_probability = talk_frequency * reply_probability reply_probability = talk_frequency * reply_probability
# 处理表情包 # 处理表情包
if message_data.get("is_emoji") or message_data.get("is_picid"): if message_data.get("is_emoji") or message_data.get("is_picid"):
@@ -544,7 +736,11 @@ class HeartFChatting:
return False return False
async def _generate_response( 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]: ) -> Optional[list]:
"""生成普通回复""" """生成普通回复"""
try: try:
@@ -552,8 +748,8 @@ class HeartFChatting:
chat_stream=self.chat_stream, chat_stream=self.chat_stream,
reply_to=reply_to, reply_to=reply_to,
available_actions=available_actions, available_actions=available_actions,
enable_tool=global_config.tool.enable_in_normal_chat, enable_tool=global_config.tool.enable_tool,
request_type="chat.replyer.normal", request_type=request_type,
) )
if not success or not reply_set: if not success or not reply_set:
@@ -563,10 +759,10 @@ class HeartFChatting:
return reply_set return reply_set
except Exception as e: 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 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() current_time = time.time()
new_message_count = message_api.count_new_messages( new_message_count = message_api.count_new_messages(
chat_id=self.chat_stream.stream_id, start_time=thinking_start_time, end_time=current_time 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) need_reply = new_message_count >= random.randint(2, 4)
if need_reply: if need_reply:
logger.info( logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复")
f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,使用引用回复"
)
else: else:
logger.debug( logger.info(f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,不使用引用回复")
f"{self.log_prefix} 从思考到回复,共有{new_message_count}条新消息,不使用引用回复"
)
reply_text = "" reply_text = ""
first_replied = False first_replied = False

View File

@@ -1,10 +1,13 @@
import time import time
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from src.config.config import global_config from src.config.config import global_config
from src.common.message_repository import count_messages
from src.common.logger import get_logger 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__) 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) bot_reply_count = count_messages(bot_filter)
return {"bot_reply_count": bot_reply_count, "total_message_count": total_message_count} 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
)

View File

@@ -525,9 +525,9 @@ class EmojiManager:
如果文件已被删除,则执行对象的删除方法并从列表中移除 如果文件已被删除,则执行对象的删除方法并从列表中移除
""" """
try: try:
if not self.emoji_objects: # if not self.emoji_objects:
logger.warning("[检查] emoji_objects为空跳过完整性检查") # logger.warning("[检查] emoji_objects为空跳过完整性检查")
return # return
total_count = len(self.emoji_objects) total_count = len(self.emoji_objects)
self.emoji_num = total_count self.emoji_num = total_count
@@ -707,6 +707,38 @@ class EmojiManager:
return emoji return emoji
return None # 如果循环结束还没找到,则返回 None 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: async def delete_emoji(self, emoji_hash: str) -> bool:
"""根据哈希值删除表情包 """根据哈希值删除表情包

View File

@@ -51,7 +51,7 @@ def init_prompt() -> None:
"想说明某个具体的事实观点,但懒得明说,或者不便明说,或表达一种默契",使用"懂的都懂" "想说明某个具体的事实观点,但懒得明说,或者不便明说,或表达一种默契",使用"懂的都懂"
"当涉及游戏相关时,表示意外的夸赞,略带戏谑意味"时,使用"这么强!" "当涉及游戏相关时,表示意外的夸赞,略带戏谑意味"时,使用"这么强!"
注意不要总结你自己SELF的发言 注意不要总结你自己SELF的发言
现在请你概括 现在请你概括
""" """
Prompt(learn_style_prompt, "learn_style_prompt") Prompt(learn_style_prompt, "learn_style_prompt")
@@ -330,48 +330,8 @@ class ExpressionLearner:
""" """
current_time = time.time() current_time = time.time()
# 全局衰减所有已存储的表达方式 # 全局衰减所有已存储的表达方式(直接操作数据库)
for type in ["style", "grammar"]: self._apply_global_decay_to_database(current_time)
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
learnt_style: Optional[List[Tuple[str, str, str]]] = [] learnt_style: Optional[List[Tuple[str, str, str]]] = []
learnt_grammar: 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 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: def calculate_decay_factor(self, time_diff_days: float) -> float:
""" """
计算衰减值 计算衰减值
@@ -410,30 +406,6 @@ class ExpressionLearner:
return min(0.01, decay) 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]]: async def learn_and_store(self, type: str, num: int = 10) -> List[Tuple[str, str, str]]:
# sourcery skip: use-join # sourcery skip: use-join
""" """

View File

@@ -2,7 +2,7 @@ import json
import time import time
import random import random
from typing import List, Dict, Tuple, Optional from typing import List, Dict, Tuple, Optional, Any
from json_repair import repair_json from json_repair import repair_json
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
@@ -117,36 +117,42 @@ class ExpressionSelector:
def get_random_expressions( def get_random_expressions(
self, chat_id: str, total_num: int, style_percentage: float, grammar_percentage: float 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合并抽选 # 支持多chat_id合并抽选
related_chat_ids = self.get_related_chat_ids(chat_id) related_chat_ids = self.get_related_chat_ids(chat_id)
style_exprs = []
grammar_exprs = [] # 优化一次性查询所有相关chat_id的表达方式
for cid in related_chat_ids: style_query = Expression.select().where(
style_query = Expression.select().where((Expression.chat_id == cid) & (Expression.type == "style")) (Expression.chat_id.in_(related_chat_ids)) & (Expression.type == "style")
grammar_query = Expression.select().where((Expression.chat_id == cid) & (Expression.type == "grammar")) )
style_exprs.extend([ grammar_query = Expression.select().where(
{ (Expression.chat_id.in_(related_chat_ids)) & (Expression.type == "grammar")
"situation": expr.situation, )
"style": expr.style,
"count": expr.count, style_exprs = [
"last_active_time": expr.last_active_time, {
"source_id": cid, "situation": expr.situation,
"type": "style", "style": expr.style,
"create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, "count": expr.count,
} for expr in style_query "last_active_time": expr.last_active_time,
]) "source_id": expr.chat_id,
grammar_exprs.extend([ "type": "style",
{ "create_date": expr.create_date if expr.create_date is not None else expr.last_active_time,
"situation": expr.situation, } for expr in style_query
"style": expr.style, ]
"count": expr.count,
"last_active_time": expr.last_active_time, grammar_exprs = [
"source_id": cid, {
"type": "grammar", "situation": expr.situation,
"create_date": expr.create_date if expr.create_date is not None else expr.last_active_time, "style": expr.style,
} for expr in grammar_query "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) style_num = int(total_num * style_percentage)
grammar_num = int(total_num * grammar_percentage) grammar_num = int(total_num * grammar_percentage)
# 按权重抽样使用count作为权重 # 按权重抽样使用count作为权重
@@ -162,7 +168,7 @@ class ExpressionSelector:
selected_grammar = [] selected_grammar = []
return selected_style, 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分组后一次性写入数据库""" """对一批表达方式更新count值按chat_id+type分组后一次性写入数据库"""
if not expressions_to_update: if not expressions_to_update:
return return
@@ -203,7 +209,7 @@ class ExpressionSelector:
max_num: int = 10, max_num: int = 10,
min_num: int = 5, min_num: int = 5,
target_message: Optional[str] = None, target_message: Optional[str] = None,
) -> List[Dict[str, str]]: ) -> List[Dict[str, Any]]:
# sourcery skip: inline-variable, list-comprehension # sourcery skip: inline-variable, list-comprehension
"""使用LLM选择适合的表达方式""" """使用LLM选择适合的表达方式"""
@@ -273,6 +279,7 @@ class ExpressionSelector:
if not isinstance(result, dict) or "selected_situations" not in result: if not isinstance(result, dict) or "selected_situations" not in result:
logger.error("LLM返回格式错误") logger.error("LLM返回格式错误")
logger.info(f"LLM返回结果: \n{content}")
return [] return []
selected_indices = result["selected_situations"] selected_indices = result["selected_situations"]

View File

@@ -12,6 +12,7 @@ from src.chat.message_receive.storage import MessageStorage
from src.chat.heart_flow.heartflow import heartflow from src.chat.heart_flow.heartflow import heartflow
from src.chat.utils.utils import is_mentioned_bot_in_message from src.chat.utils.utils import is_mentioned_bot_in_message
from src.chat.utils.timer_calculator import Timer 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.common.logger import get_logger
from src.person_info.relationship_manager import get_relationship_manager from src.person_info.relationship_manager import get_relationship_manager
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
@@ -56,16 +57,41 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
with Timer("记忆激活"): with Timer("记忆激活"):
interested_rate = await hippocampus_manager.get_activate_from_text( interested_rate = await hippocampus_manager.get_activate_from_text(
message.processed_plain_text, message.processed_plain_text,
max_depth= 5,
fast_retrieval=False, fast_retrieval=False,
) )
logger.debug(f"记忆激活率: {interested_rate:.2f}") logger.debug(f"记忆激活率: {interested_rate:.2f}")
text_len = len(message.processed_plain_text) text_len = len(message.processed_plain_text)
# 根据文本长度调整兴趣度,长度越大兴趣度越高但增长率递减最低0.01最高0.05 # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算
# 采用对数函数实现递减增长 # 基于实际分布0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%)
base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) if text_len == 0:
base_interest = min(max(base_interest, 0.01), 0.05) 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 interested_rate += base_interest
@@ -124,7 +150,14 @@ class HeartFCMessageReceiver:
picid_pattern = r"\[picid:([^\]]+)\]" picid_pattern = r"\[picid:([^\]]+)\]"
processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text) processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text)
logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}") # type: ignore # 应用用户引用格式替换,将回复<aaa:bbb>和@<aaa:bbb>格式转换为可读格式
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}[兴趣度:{interested_rate:.2f}]") # type: ignore
logger.debug(f"[{mes_name}][当前时段回复频率: {current_talk_frequency}]") logger.debug(f"[{mes_name}][当前时段回复频率: {current_talk_frequency}]")

View File

@@ -224,10 +224,16 @@ class Hippocampus:
return hash((source, target)) return hash((source, target))
@staticmethod @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 # 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 = ( prompt = (
f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num_str}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,"
f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。"
f"如果确定找不出主题或者没有明显主题,返回<none>。" f"如果确定找不出主题或者没有明显主题,返回<none>。"
) )
@@ -300,6 +306,60 @@ class Hippocampus:
memories.sort(key=lambda x: x[2], reverse=True) memories.sort(key=lambda x: x[2], reverse=True)
return memories 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( async def get_memory_from_text(
self, self,
text: str, text: str,
@@ -325,39 +385,7 @@ class Hippocampus:
- memory_items: list, 该主题下的记忆项列表 - memory_items: list, 该主题下的记忆项列表
- similarity: float, 与文本的相似度 - similarity: float, 与文本的相似度
""" """
if not text: keywords = await self.get_keywords_from_text(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)}")
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
@@ -679,38 +707,7 @@ class Hippocampus:
Returns: Returns:
float: 激活节点数与总节点数的比值 float: 激活节点数与总节点数的比值
""" """
if not text: keywords = await self.get_keywords_from_text(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)}")
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] 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: for keyword in valid_keywords:
logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):")
# 初始化激活值 # 初始化激活值
activation_values = {keyword: 1.0} activation_values = {keyword: 1.5}
# 记录已访问的节点 # 记录已访问的节点
visited_nodes = {keyword} visited_nodes = {keyword}
# 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度)
@@ -1315,6 +1312,7 @@ class ParahippocampalGyrus:
return compressed_memory, similar_topics_dict return compressed_memory, similar_topics_dict
async def operation_build_memory(self): async def operation_build_memory(self):
# sourcery skip: merge-list-appends-into-extend
logger.info("------------------------------------开始构建记忆--------------------------------------") logger.info("------------------------------------开始构建记忆--------------------------------------")
start_time = time.time() start_time = time.time()
memory_samples = self.hippocampus.entorhinal_cortex.get_memory_sample() memory_samples = self.hippocampus.entorhinal_cortex.get_memory_sample()

View File

@@ -444,7 +444,7 @@ class MessageSending(MessageProcessBase):
is_emoji: bool = False, is_emoji: bool = False,
thinking_start_time: float = 0, thinking_start_time: float = 0,
apply_set_reply_logic: bool = False, apply_set_reply_logic: bool = False,
reply_to: str = None, # type: ignore reply_to: Optional[str] = None,
): ):
# 调用父类初始化 # 调用父类初始化
super().__init__( super().__init__(

View File

@@ -3,7 +3,7 @@ from src.plugin_system.base.base_action import BaseAction
from src.chat.message_receive.chat_stream import ChatStream from src.chat.message_receive.chat_stream import ChatStream
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.core.component_registry import component_registry 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") 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): def __init__(self):
"""初始化动作管理器""" """初始化动作管理器"""

View File

@@ -174,7 +174,7 @@ class ActionModifier:
continue # 总是激活,无需处理 continue # 总是激活,无需处理
elif activation_type == ActionActivationType.RANDOM: 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: if random.random() >= probability:
reason = f"RANDOM类型未触发概率{probability}" reason = f"RANDOM类型未触发概率{probability}"
deactivated_actions.append((action_name, reason)) deactivated_actions.append((action_name, reason))

View File

@@ -33,10 +33,11 @@ def init_prompt():
{time_block} {time_block}
{identity_block} {identity_block}
你现在需要根据聊天内容选择的合适的action来参与聊天。 你现在需要根据聊天内容选择的合适的action来参与聊天。
{chat_context_description},以下是具体的聊天内容 {chat_context_description},以下是具体的聊天内容
{chat_content_block} {chat_content_block}
{moderation_prompt} {moderation_prompt}
现在请你根据{by_what}选择合适的action和触发action的消息: 现在请你根据{by_what}选择合适的action和触发action的消息:
@@ -45,7 +46,7 @@ def init_prompt():
{no_action_block} {no_action_block}
{action_options_text} {action_options_text}
你必须从上面列出的可用action中选择一个并说明触发action的消息id原因。 你必须从上面列出的可用action中选择一个并说明触发action的消息id不是消息原文和选择该action的原因。
请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容: 请根据动作示例,以严格的 JSON 格式输出,且仅包含 JSON 内容:
""", """,
@@ -128,20 +129,6 @@ class ActionPlanner:
else: else:
logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到") 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 方法) --- # --- 构建提示词 (调用修改后的 PromptBuilder 方法) ---
prompt, message_id_list = await self.build_planner_prompt( prompt, message_id_list = await self.build_planner_prompt(
is_group_chat=is_group_chat, # <-- Pass HFC state is_group_chat=is_group_chat, # <-- Pass HFC state
@@ -224,7 +211,7 @@ class ActionPlanner:
reasoning = f"Planner 内部处理错误: {outer_e}" reasoning = f"Planner 内部处理错误: {outer_e}"
is_parallel = False 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 is_parallel = current_available_actions[action].parallel_action
action_result = { action_result = {
@@ -268,7 +255,7 @@ class ActionPlanner:
actions_before_now = get_actions_by_timestamp_with_chat( actions_before_now = get_actions_by_timestamp_with_chat(
chat_id=self.chat_id, chat_id=self.chat_id,
timestamp_start=time.time()-3600, timestamp_start=time.time() - 3600,
timestamp_end=time.time(), timestamp_end=time.time(),
limit=5, limit=5,
) )
@@ -288,7 +275,6 @@ class ActionPlanner:
if global_config.chat.at_bot_inevitable_reply: if global_config.chat.at_bot_inevitable_reply:
mentioned_bonus = "\n- 有人提到你或者at你" mentioned_bonus = "\n- 有人提到你或者at你"
by_what = "聊天内容" by_what = "聊天内容"
target_prompt = '\n "target_message_id":"触发action的消息id"' target_prompt = '\n "target_message_id":"触发action的消息id"'
no_action_block = f"""重要说明: no_action_block = f"""重要说明:
@@ -311,7 +297,7 @@ class ActionPlanner:
by_what = "聊天内容和用户的最新消息" by_what = "聊天内容和用户的最新消息"
target_prompt = "" target_prompt = ""
no_action_block = """重要说明: no_action_block = """重要说明:
- 'no_action' 表示只进行普通聊天回复,不执行任何额外动作 - 'reply' 表示只进行普通聊天回复,不执行任何额外动作
- 其他action表示在普通回复的基础上执行相应的额外动作""" - 其他action表示在普通回复的基础上执行相应的额外动作"""
chat_context_description = "你现在正在一个群聊中" chat_context_description = "你现在正在一个群聊中"

View File

@@ -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.timer_calculator import Timer # <--- Import Timer
from src.chat.utils.utils import get_chat_type_and_target_info 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.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.express.expression_selector import expression_selector
from src.chat.knowledge.knowledge_lib import qa_manager from src.chat.knowledge.knowledge_lib import qa_manager
from src.chat.memory_system.memory_activator import MemoryActivator 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.mood.mood_manager import mood_manager
from src.person_info.relationship_fetcher import relationship_fetcher_manager from src.person_info.relationship_fetcher import relationship_fetcher_manager
from src.person_info.person_info import get_person_info_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 from src.plugin_system.base.component_types import ActionInfo
logger = get_logger("replyer") logger = get_logger("replyer")
def init_prompt(): def init_prompt():
Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1") Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
Prompt("在群里聊天", "chat_target_group2") Prompt("在群里聊天", "chat_target_group2")
Prompt("{sender_name}聊天", "chat_target_private2") 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( Prompt(
""" """
@@ -109,7 +87,8 @@ def init_prompt():
{core_dialogue_prompt} {core_dialogue_prompt}
{reply_target_block} {reply_target_block}
对方最新发送的内容:{message_txt}
你现在的心情是:{mood_state} 你现在的心情是:{mood_state}
{config_expression_style} {config_expression_style}
注意不要复读你说过的话 注意不要复读你说过的话
@@ -159,6 +138,8 @@ class DefaultReplyer:
self.heart_fc_sender = HeartFCSender() self.heart_fc_sender = HeartFCSender()
self.memory_activator = MemoryActivator() self.memory_activator = MemoryActivator()
self.instant_memory = InstantMemory(chat_id=self.chat_stream.stream_id) 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) 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]: def _select_weighted_model_config(self) -> Dict[str, Any]:
@@ -171,67 +152,49 @@ class DefaultReplyer:
async def generate_reply_with_context( async def generate_reply_with_context(
self, self,
reply_data: Optional[Dict[str, Any]] = None,
reply_to: str = "", reply_to: str = "",
extra_info: str = "", extra_info: str = "",
available_actions: Optional[Dict[str, ActionInfo]] = None, available_actions: Optional[Dict[str, ActionInfo]] = None,
enable_tool: bool = True, enable_tool: bool = True,
enable_timeout: bool = False,
) -> Tuple[bool, Optional[str], Optional[str]]: ) -> Tuple[bool, Optional[str], Optional[str]]:
""" """
回复器 (Replier): 核心逻辑,负责生成回复文本。 回复器 (Replier): 负责生成回复文本的核心逻辑
(已整合原 HeartFCGenerator 的功能)
Args:
reply_to: 回复对象,格式为 "发送者:消息内容"
extra_info: 额外信息,用于补充上下文
available_actions: 可用的动作信息字典
enable_tool: 是否启用工具调用
Returns:
Tuple[bool, Optional[str], Optional[str]]: (是否成功, 生成的回复内容, 使用的prompt)
""" """
prompt = None prompt = None
if available_actions is None: if available_actions is None:
available_actions = {} available_actions = {}
try: 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 # 3. 构建 Prompt
with Timer("构建Prompt", {}): # 内部计时器,可选保留 with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_reply_context( 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, available_actions=available_actions,
enable_timeout=enable_timeout,
enable_tool=enable_tool, enable_tool=enable_tool,
) )
if not prompt:
logger.warning("构建prompt失败跳过回复生成")
return False, None, None
# 4. 调用 LLM 生成回复 # 4. 调用 LLM 生成回复
content = None content = None
reasoning_content = None # TODO: 复活这里
model_name = "unknown_model" # reasoning_content = None
# model_name = "unknown_model"
try: try:
with Timer("LLM生成", {}): # 内部计时器,可选保留 content = await self.llm_generate_content(prompt)
# 加权随机选择一个模型配置 logger.debug(f"replyer生成内容: {content}")
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}")
except Exception as llm_e: except Exception as llm_e:
# 精简报错信息 # 精简报错信息
@@ -247,73 +210,62 @@ class DefaultReplyer:
async def rewrite_reply_with_context( async def rewrite_reply_with_context(
self, self,
reply_data: Dict[str, Any],
raw_reply: str = "", raw_reply: str = "",
reason: str = "", reason: str = "",
reply_to: str = "", reply_to: str = "",
relation_info: str = "", return_prompt: bool = False,
) -> Tuple[bool, Optional[str]]: ) -> Tuple[bool, Optional[str], Optional[str]]:
""" """
表达器 (Expressor): 核心逻辑,负责生成回复文本。 表达器 (Expressor): 负责重写和优化回复文本。
Args:
raw_reply: 原始回复内容
reason: 回复原因
reply_to: 回复对象,格式为 "发送者:消息内容"
relation_info: 关系信息
Returns:
Tuple[bool, Optional[str]]: (是否成功, 重写后的回复内容)
""" """
try: try:
if not reply_data:
reply_data = {
"reply_to": reply_to,
"relation_info": relation_info,
}
with Timer("构建Prompt", {}): # 内部计时器,可选保留 with Timer("构建Prompt", {}): # 内部计时器,可选保留
prompt = await self.build_prompt_rewrite_context( prompt = await self.build_prompt_rewrite_context(
reply_data=reply_data, raw_reply=raw_reply,
reason=reason,
reply_to=reply_to,
) )
content = None content = None
reasoning_content = None # TODO: 复活这里
model_name = "unknown_model" # reasoning_content = None
# model_name = "unknown_model"
if not prompt: if not prompt:
logger.error("Prompt 构建失败,无法生成回复。") logger.error("Prompt 构建失败,无法生成回复。")
return False, None return False, None, None
try: try:
with Timer("LLM生成", {}): # 内部计时器,可选保留 content = await self.llm_generate_content(prompt)
# 加权随机选择一个模型配置 logger.info(f"想要表达:{raw_reply}||理由:{reason}||生成回复: {content}\n")
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")
except Exception as llm_e: except Exception as llm_e:
# 精简报错信息 # 精简报错信息
logger.error(f"LLM 生成失败: {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: except Exception as e:
logger.error(f"回复生成意外失败: {e}") logger.error(f"回复生成意外失败: {e}")
traceback.print_exc() 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: if not global_config.relationship.enable_relationship:
return "" return ""
relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_stream.stream_id) relationship_fetcher = relationship_fetcher_manager.get_fetcher(self.chat_stream.stream_id)
if not reply_data: if not reply_to:
return "" return ""
reply_to = reply_data.get("reply_to", "")
sender, text = self._parse_reply_target(reply_to) sender, text = self._parse_reply_target(reply_to)
if not sender or not text: if not sender or not text:
return "" return ""
@@ -327,7 +279,16 @@ class DefaultReplyer:
return await relationship_fetcher.build_relation_info(person_id, points_num=5) 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: if not global_config.expression.enable_expression:
return "" return ""
@@ -360,21 +321,31 @@ class DefaultReplyer:
expression_habits_block = "" expression_habits_block = ""
expression_habits_title = "" expression_habits_title = ""
if style_habits_str.strip(): if style_habits_str.strip():
expression_habits_title = "你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:" expression_habits_title = (
"你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:"
)
expression_habits_block += f"{style_habits_str}\n" expression_habits_block += f"{style_habits_str}\n"
if grammar_habits_str.strip(): if grammar_habits_str.strip():
expression_habits_title = "你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:" expression_habits_title = (
"你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:"
)
expression_habits_block += f"{grammar_habits_str}\n" expression_habits_block += f"{grammar_habits_str}\n"
if style_habits_str.strip() and grammar_habits_str.strip(): if style_habits_str.strip() and grammar_habits_str.strip():
expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中:" expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中:"
expression_habits_block = f"{expression_habits_title}\n{expression_habits_block}" return f"{expression_habits_title}\n{expression_habits_block}"
async def build_memory_block(self, chat_history: str, target: str) -> str:
"""构建记忆块
return expression_habits_block Args:
chat_history: 聊天历史记录
target: 目标消息内容
async def build_memory_block(self, chat_history, target): Returns:
str: 记忆信息字符串
"""
if not global_config.memory.enable_memory: if not global_config.memory.enable_memory:
return "" return ""
@@ -402,12 +373,13 @@ class DefaultReplyer:
return memory_str 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: Args:
reply_data: 回复数据,包含要回复的消息内容 chat_history: 聊天历史记录
chat_history: 聊天历史 reply_to: 回复对象,格式为 "发送者:消息内容"
enable_tool: 是否启用工具调用
Returns: Returns:
str: 工具信息字符串 str: 工具信息字符串
@@ -416,10 +388,9 @@ class DefaultReplyer:
if not enable_tool: if not enable_tool:
return "" return ""
if not reply_data: if not reply_to:
return "" return ""
reply_to = reply_data.get("reply_to", "")
sender, text = self._parse_reply_target(reply_to) sender, text = self._parse_reply_target(reply_to)
if not text: if not text:
@@ -452,7 +423,15 @@ class DefaultReplyer:
logger.error(f"工具信息获取失败: {e}") logger.error(f"工具信息获取失败: {e}")
return "" 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 = "" sender = ""
target = "" target = ""
# 添加None检查防止NoneType错误 # 添加None检查防止NoneType错误
@@ -466,7 +445,15 @@ class DefaultReplyer:
target = parts[1].strip() target = parts[1].strip()
return sender, target 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 = "" keywords_reaction_prompt = ""
try: try:
@@ -500,15 +487,25 @@ class DefaultReplyer:
return keywords_reaction_prompt 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() start_time = time.time()
result = await coroutine result = await coroutine
end_time = time.time() end_time = time.time()
duration = end_time - start_time duration = end_time - start_time
return name, result, duration 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 构建 s4u 风格的分离对话 prompt
@@ -517,7 +514,7 @@ class DefaultReplyer:
target_user_id: 目标用户ID当前对话对象 target_user_id: 目标用户ID当前对话对象
Returns: Returns:
tuple: (核心对话prompt, 背景对话prompt) Tuple[str, str]: (核心对话prompt, 背景对话prompt)
""" """
core_dialogue_list = [] core_dialogue_list = []
background_dialogue_list = [] background_dialogue_list = []
@@ -536,7 +533,7 @@ class DefaultReplyer:
# 其他用户的对话 # 其他用户的对话
background_dialogue_list.append(msg_dict) background_dialogue_list.append(msg_dict)
except Exception as e: except Exception as e:
logger.error(f"![1753364551656](image/default_generator/1753364551656.png)记录: {msg_dict}, 错误: {e}") logger.error(f"处理消息记录时出错: {msg_dict}, 错误: {e}")
# 构建背景对话 prompt # 构建背景对话 prompt
background_dialogue_prompt = "" background_dialogue_prompt = ""
@@ -581,8 +578,25 @@ class DefaultReplyer:
sender: str, sender: str,
target: str, target: str,
chat_info: str, chat_info: str,
): ) -> Any:
"""构建 mai_think 上下文信息""" """构建 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 = mai_thinking_manager.get_mai_think(chat_id)
mai_think.memory_block = memory_block mai_think.memory_block = memory_block
mai_think.relation_info_block = relation_info mai_think.relation_info_block = relation_info
@@ -598,21 +612,20 @@ class DefaultReplyer:
async def build_prompt_reply_context( async def build_prompt_reply_context(
self, self,
reply_data: Dict[str, Any], reply_to: str,
extra_info: str = "",
available_actions: Optional[Dict[str, ActionInfo]] = None, available_actions: Optional[Dict[str, ActionInfo]] = None,
enable_timeout: bool = False,
enable_tool: bool = True, enable_tool: bool = True,
) -> str: # sourcery skip: merge-else-if-into-elif, remove-redundant-if ) -> str: # sourcery skip: merge-else-if-into-elif, remove-redundant-if
""" """
构建回复器上下文 构建回复器上下文
Args: Args:
reply_data: 回复数据 reply_to: 回复对象,格式为 "发送者:消息内容"
replay_data 包含以下字段: extra_info: 额外信息,用于补充上下文
structured_info: 结构化信息,一般是工具调用获得的信息
reply_to: 回复对象
extra_info/extra_info_block: 额外信息
available_actions: 可用动作 available_actions: 可用动作
enable_timeout: 是否启用超时处理
enable_tool: 是否启用工具调用
Returns: Returns:
str: 构建好的上下文 str: 构建好的上下文
@@ -623,8 +636,6 @@ class DefaultReplyer:
chat_id = chat_stream.stream_id chat_id = chat_stream.stream_id
person_info_manager = get_person_info_manager() person_info_manager = get_person_info_manager()
is_group_chat = bool(chat_stream.group_info) 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: if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(chat_id) chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
@@ -633,6 +644,15 @@ class DefaultReplyer:
mood_prompt = "" mood_prompt = ""
sender, target = self._parse_reply_target(reply_to) 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描述 (如果启用planner)
action_descriptions = "" action_descriptions = ""
@@ -649,21 +669,6 @@ class DefaultReplyer:
limit=global_config.chat.max_context_size * 2, 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( message_list_before_short = get_raw_msg_before_timestamp_with_chat(
chat_id=chat_id, chat_id=chat_id,
timestamp=time.time(), timestamp=time.time(),
@@ -683,16 +688,12 @@ class DefaultReplyer:
self._time_and_run_task( self._time_and_run_task(
self.build_expression_habits(chat_talking_prompt_short, target), "expression_habits" self.build_expression_habits(chat_talking_prompt_short, target), "expression_habits"
), ),
self._time_and_run_task( self._time_and_run_task(self.build_relation_info(reply_to), "relation_info"),
self.build_relation_info(reply_data), "relation_info"
),
self._time_and_run_task(self.build_memory_block(chat_talking_prompt_short, target), "memory_block"), self._time_and_run_task(self.build_memory_block(chat_talking_prompt_short, target), "memory_block"),
self._time_and_run_task( self._time_and_run_task(
self.build_tool_info(chat_talking_prompt_short, reply_data, enable_tool=enable_tool), "tool_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"
), ),
self._time_and_run_task(get_prompt_info(target, threshold=0.38), "prompt_info"),
) )
# 任务名称中英文映射 # 任务名称中英文映射
@@ -701,7 +702,7 @@ class DefaultReplyer:
"relation_info": "感受关系", "relation_info": "感受关系",
"memory_block": "回忆", "memory_block": "回忆",
"tool_info": "使用工具", "tool_info": "使用工具",
"prompt_info": "获取知识" "prompt_info": "获取知识",
} }
# 处理结果 # 处理结果
@@ -723,8 +724,8 @@ class DefaultReplyer:
keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target) keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
if extra_info_block: if extra_info:
extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info_block}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策" extra_info_block = f"以下是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策\n{extra_info}\n以上是你在回复时需要参考的信息,现在请你阅读以下内容,进行决策"
else: else:
extra_info_block = "" extra_info_block = ""
@@ -779,116 +780,74 @@ class DefaultReplyer:
# 根据sender通过person_info_manager反向查找person_id再获取user_id # 根据sender通过person_info_manager反向查找person_id再获取user_id
person_id = person_info_manager.get_person_id_by_person_name(sender) person_id = person_info_manager.get_person_id_by_person_name(sender)
# 根据配置选择使用哪种 prompt 构建模式 # 使用 s4u 对话构建模式:分离当前对话对象和其他对话
if global_config.chat.use_s4u_prompt_mode and person_id: try:
# 使用 s4u 对话构建模式:分离当前对话对象和其他对话 user_id_value = await person_info_manager.get_value(person_id, "user_id")
try: if user_id_value:
user_id_value = await person_info_manager.get_value(person_id, "user_id") target_user_id = str(user_id_value)
if user_id_value: except Exception as e:
target_user_id = str(user_id_value) logger.warning(f"无法从person_id {person_id} 获取user_id: {e}")
except Exception as e: target_user_id = ""
logger.warning(f"无法从person_id {person_id} 获取user_id: {e}")
target_user_id = ""
# 构建分离的对话 prompt # 构建分离的对话 prompt
core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts( core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts(
message_list_before_now_long, target_user_id message_list_before_now_long, target_user_id
) )
self.build_mai_think_context( self.build_mai_think_context(
chat_id=chat_id, chat_id=chat_id,
memory_block=memory_block, memory_block=memory_block,
relation_info=relation_info, relation_info=relation_info,
time_block=time_block, time_block=time_block,
chat_target_1=chat_target_1, chat_target_1=chat_target_1,
chat_target_2=chat_target_2, chat_target_2=chat_target_2,
mood_prompt=mood_prompt, mood_prompt=mood_prompt,
identity_block=identity_block, identity_block=identity_block,
sender=sender, sender=sender,
target=target, target=target,
chat_info=f""" chat_info=f"""
{background_dialogue_prompt} {background_dialogue_prompt}
-------------------------------- --------------------------------
{time_block} {time_block}
这是你和{sender}的对话,你们正在交流中: 这是你和{sender}的对话,你们正在交流中:
{core_dialogue_prompt}""" {core_dialogue_prompt}""",
) )
# 使用 s4u 风格的模板
template_name = "s4u_style_prompt"
# 使用 s4u 风格的模板 return await global_prompt_manager.format_prompt(
template_name = "s4u_style_prompt" template_name,
expression_habits_block=expression_habits_block,
return await global_prompt_manager.format_prompt( tool_info_block=tool_info,
template_name, knowledge_prompt=prompt_info,
expression_habits_block=expression_habits_block, memory_block=memory_block,
tool_info_block=tool_info, relation_info_block=relation_info,
knowledge_prompt=prompt_info, extra_info_block=extra_info_block,
memory_block=memory_block, identity=identity_block,
relation_info_block=relation_info, action_descriptions=action_descriptions,
extra_info_block=extra_info_block, sender_name=sender,
identity=identity_block, mood_state=mood_prompt,
action_descriptions=action_descriptions, background_dialogue_prompt=background_dialogue_prompt,
sender_name=sender, time_block=time_block,
mood_state=mood_prompt, core_dialogue_prompt=core_dialogue_prompt,
background_dialogue_prompt=background_dialogue_prompt, reply_target_block=reply_target_block,
time_block=time_block, message_txt=target,
core_dialogue_prompt=core_dialogue_prompt, config_expression_style=global_config.expression.expression_style,
reply_target_block=reply_target_block, keywords_reaction_prompt=keywords_reaction_prompt,
message_txt=target, moderation_prompt=moderation_prompt_block,
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,
)
async def build_prompt_rewrite_context( async def build_prompt_rewrite_context(
self, self,
reply_data: Dict[str, Any], raw_reply: str,
reason: str,
reply_to: str,
) -> str: ) -> str:
chat_stream = self.chat_stream chat_stream = self.chat_stream
chat_id = chat_stream.stream_id chat_id = chat_stream.stream_id
is_group_chat = bool(chat_stream.group_info) 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) sender, target = self._parse_reply_target(reply_to)
# 添加情绪状态获取 # 添加情绪状态获取
@@ -915,7 +874,7 @@ class DefaultReplyer:
# 并行执行2个构建任务 # 并行执行2个构建任务
expression_habits_block, relation_info = await asyncio.gather( expression_habits_block, relation_info = await asyncio.gather(
self.build_expression_habits(chat_talking_prompt_half, target), 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) keywords_reaction_prompt = await self.build_keywords_reaction_prompt(target)
@@ -1018,6 +977,31 @@ class DefaultReplyer:
display_message=display_message, 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: def weighted_sample_no_replacement(items, weights, k) -> list:
""" """
@@ -1076,9 +1060,7 @@ async def get_prompt_info(message: str, threshold: float):
logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}") logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}")
logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
# 格式化知识信息 return f"你有以下这些**知识**\n{related_info}\n请你**记住上面的知识**,之后可能会用到。\n"
formatted_prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=related_info)
return formatted_prompt_info
else: else:
logger.debug("从LPMM知识库获取知识失败可能是从未导入过知识返回空知识...") logger.debug("从LPMM知识库获取知识失败可能是从未导入过知识返回空知识...")
return "" return ""

View File

@@ -2,7 +2,7 @@ import time # 导入 time 模块以获取当前时间
import random import random
import re 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 rich.traceback import install
from src.config.config import global_config 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 ActionRecords
from src.common.database.database_model import Images from src.common.database.database_model import Images
from src.person_info.person_info import PersonInfoManager, get_person_info_manager 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) 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:
"""
替换内容中的用户引用格式,包括回复<aaa:bbb>和@<aaa:bbb>格式
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
# 处理回复<aaa:bbb>格式
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)
# 处理@<aaa:bbb>格式
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:
"""
替换内容中的用户引用格式,包括回复<aaa:bbb>和@<aaa:bbb>格式
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
# 处理回复<aaa:bbb>格式
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)
# 处理@<aaa:bbb>格式
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( def get_raw_msg_by_timestamp(
timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest" timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest"
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@@ -374,33 +524,8 @@ def _build_readable_messages_internal(
else: else:
person_name = "某人" person_name = "某人"
# 检查是否有 回复<aaa:bbb> 字段 # 使用独立函数处理用户引用格式
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" content = replace_user_references_sync(content, platform, replace_bot_name=replace_bot_name)
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)
# 检查是否有 @<aaa:bbb> 字段 @<{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
target_str = "这是QQ的一个功能用于提及某人但没那么明显" target_str = "这是QQ的一个功能用于提及某人但没那么明显"
if target_str in content and random.random() < 0.6: 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 return formatted_string, details_list
def build_readable_messages_with_id( def build_readable_messages_with_id(
messages: List[Dict[str, Any]], messages: List[Dict[str, Any]],
replace_bot_name: bool = True, replace_bot_name: bool = True,
@@ -671,7 +797,7 @@ def build_readable_messages_with_id(
message_id_list = assign_message_ids(messages) message_id_list = assign_message_ids(messages)
formatted_string = build_readable_messages( formatted_string = build_readable_messages(
messages = messages, messages=messages,
replace_bot_name=replace_bot_name, replace_bot_name=replace_bot_name,
merge_messages=merge_messages, merge_messages=merge_messages,
timestamp_mode=timestamp_mode, timestamp_mode=timestamp_mode,
@@ -682,10 +808,7 @@ def build_readable_messages_with_id(
message_id_list=message_id_list, message_id_list=message_id_list,
) )
return formatted_string, message_id_list
return formatted_string , message_id_list
def build_readable_messages( def build_readable_messages(
@@ -770,7 +893,13 @@ def build_readable_messages(
if read_mark <= 0: if read_mark <= 0:
# 没有有效的 read_mark直接格式化所有消息 # 没有有效的 read_mark直接格式化所有消息
formatted_string, _, pic_id_mapping, _ = _build_readable_messages_internal( 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: for msg in messages:
try: try:
platform = msg.get("chat_info_platform") platform: str = msg.get("chat_info_platform") # type: ignore
user_id = msg.get("user_id") user_id = msg.get("user_id")
_timestamp = msg.get("time") _timestamp = msg.get("time")
content: str = "" 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) anon_name = get_anon_name(platform, user_id)
# print(f"anon_name:{anon_name}") # print(f"anon_name:{anon_name}")
# 处理 回复<aaa:bbb> # 使用独立函数处理用户引用格式,传入自定义的匿名名称解析器
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" def anon_name_resolver(platform: str, user_id: str) -> str:
match = re.search(reply_pattern, content)
if match:
# print(f"发现回复match:{match}")
bbb = match.group(2)
try: try:
anon_reply = get_anon_name(platform, bbb) return get_anon_name(platform, user_id)
# print(f"anon_reply:{anon_reply}")
except Exception: except Exception:
anon_reply = "?" return "?"
content = re.sub(reply_pattern, f"回复 {anon_reply}", content, count=1)
# 处理 @<aaa:bbb>无嵌套def content = replace_user_references_sync(content, platform, anon_name_resolver, replace_bot_name=False)
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
header = f"{anon_name}" header = f"{anon_name}"
output_lines.append(header) output_lines.append(header)

View File

@@ -37,7 +37,7 @@ class ImageManager:
self._ensure_image_dir() self._ensure_image_dir()
self._initialized = True 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: try:
db.connect(reuse_if_open=True) db.connect(reuse_if_open=True)
@@ -94,7 +94,7 @@ class ImageManager:
logger.error(f"保存描述到数据库失败 (Peewee): {str(e)}") logger.error(f"保存描述到数据库失败 (Peewee): {str(e)}")
async def get_emoji_description(self, image_base64: str) -> str: async def get_emoji_description(self, image_base64: str) -> str:
"""获取表情包描述,使用二步走识别并带缓存优化""" """获取表情包描述,优先使用Emoji表中的缓存数据"""
try: try:
# 计算图片哈希 # 计算图片哈希
# 确保base64字符串只包含ASCII字符 # 确保base64字符串只包含ASCII字符
@@ -104,9 +104,21 @@ class ImageManager:
image_hash = hashlib.md5(image_bytes).hexdigest() image_hash = hashlib.md5(image_bytes).hexdigest()
image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # type: ignore 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") cached_description = self._get_description_from_db(image_hash, "emoji")
if cached_description: if cached_description:
logger.info(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...")
return f"[表情包:{cached_description}]" return f"[表情包:{cached_description}]"
# === 二步走识别流程 === # === 二步走识别流程 ===
@@ -118,10 +130,10 @@ class ImageManager:
logger.warning("GIF转换失败无法获取描述") logger.warning("GIF转换失败无法获取描述")
return "[表情包(GIF处理失败)]" return "[表情包(GIF处理失败)]"
vlm_prompt = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,描述一下表情包表达的情感和内容,描述细节,从互联网梗,meme的角度去分析" 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: else:
vlm_prompt = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,描述细节,从互联网梗,meme的角度去分析" 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: if detailed_description is None:
logger.warning("VLM未能生成表情包详细描述") logger.warning("VLM未能生成表情包详细描述")
@@ -158,7 +170,7 @@ class ImageManager:
if len(emotions) > 1 and emotions[1] != emotions[0]: if len(emotions) > 1 and emotions[1] != emotions[0]:
final_emotion = f"{emotions[0]}{emotions[1]}" 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") cached_description = self._get_description_from_db(image_hash, "emoji")
@@ -204,10 +216,10 @@ class ImageManager:
except Exception as e: except Exception as e:
logger.error(f"获取表情包描述失败: {str(e)}") logger.error(f"获取表情包描述失败: {str(e)}")
return "[表情包]" return "[表情包(处理失败)]"
async def get_image_description(self, image_base64: str) -> str: async def get_image_description(self, image_base64: str) -> str:
"""获取普通图片描述,带查重和保存功能""" """获取普通图片描述,优先使用Images表中的缓存数据"""
try: try:
# 计算图片哈希 # 计算图片哈希
if isinstance(image_base64, str): if isinstance(image_base64, str):
@@ -215,7 +227,7 @@ class ImageManager:
image_bytes = base64.b64decode(image_base64) image_bytes = base64.b64decode(image_base64)
image_hash = hashlib.md5(image_bytes).hexdigest() image_hash = hashlib.md5(image_bytes).hexdigest()
# 检查图片是否已存在 # 优先检查Images表中是否已有完整的描述
existing_image = Images.get_or_none(Images.emoji_hash == image_hash) existing_image = Images.get_or_none(Images.emoji_hash == image_hash)
if existing_image: if existing_image:
# 更新计数 # 更新计数
@@ -227,18 +239,20 @@ class ImageManager:
# 如果已有描述,直接返回 # 如果已有描述,直接返回
if existing_image.description: if existing_image.description:
logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...")
return f"[图片:{existing_image.description}]" return f"[图片:{existing_image.description}]"
# 查询缓存描述 # 查询ImageDescriptions表的缓存描述
cached_description = self._get_description_from_db(image_hash, "image") cached_description = self._get_description_from_db(image_hash, "image")
if cached_description: if cached_description:
logger.debug(f"图片描述缓存中 {cached_description}") logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...")
return f"[图片:{cached_description}]" return f"[图片:{cached_description}]"
# 调用AI获取描述 # 调用AI获取描述
image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # type: ignore image_format = Image.open(io.BytesIO(image_bytes)).format.lower() # type: ignore
prompt = global_config.custom_prompt.image_prompt 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: if description is None:
logger.warning("AI未能生成图片描述") logger.warning("AI未能生成图片描述")
@@ -266,6 +280,7 @@ class ImageManager:
if not hasattr(existing_image, "vlm_processed") or existing_image.vlm_processed is None: if not hasattr(existing_image, "vlm_processed") or existing_image.vlm_processed is None:
existing_image.vlm_processed = True existing_image.vlm_processed = True
existing_image.save() existing_image.save()
logger.debug(f"[数据库] 更新已有图片记录: {image_hash[:8]}...")
else: else:
Images.create( Images.create(
image_id=str(uuid.uuid4()), image_id=str(uuid.uuid4()),
@@ -277,16 +292,18 @@ class ImageManager:
vlm_processed=True, vlm_processed=True,
count=1, count=1,
) )
logger.debug(f"[数据库] 创建新图片记录: {image_hash[:8]}...")
except Exception as e: except Exception as e:
logger.error(f"保存图片文件或元数据失败: {str(e)}") logger.error(f"保存图片文件或元数据失败: {str(e)}")
# 保存描述到ImageDescriptions表 # 保存描述到ImageDescriptions表作为备用缓存
self._save_description_to_db(image_hash, description, "image") self._save_description_to_db(image_hash, description, "image")
logger.info(f"[VLM完成] 图片描述生成: {description[:50]}...")
return f"[图片:{description}]" return f"[图片:{description}]"
except Exception as e: except Exception as e:
logger.error(f"获取图片描述失败: {str(e)}") logger.error(f"获取图片描述失败: {str(e)}")
return "[图片]" return "[图片(处理失败)]"
@staticmethod @staticmethod
def transform_gif(gif_base64: str, similarity_threshold: float = 1000.0, max_frames: int = 15) -> Optional[str]: 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_bytes = base64.b64decode(image_base64)
image_hash = hashlib.md5(image_bytes).hexdigest() 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") cached_description = self._get_description_from_db(image_hash, "image")
if cached_description: if cached_description:
logger.debug(f"VLM处理时发现缓存描述: {cached_description}") logger.debug(f"[缓存复用] 从ImageDescriptions表复用描述: {cached_description[:50]}...")
# 更新数据库
image = Images.get(Images.image_id == image_id)
image.description = cached_description image.description = cached_description
image.vlm_processed = True image.vlm_processed = True
image.save() image.save()
@@ -520,7 +553,8 @@ class ImageManager:
prompt = global_config.custom_prompt.image_prompt prompt = global_config.custom_prompt.image_prompt
# 获取VLM描述 # 获取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: if description is None:
logger.warning("VLM未能生成图片描述") logger.warning("VLM未能生成图片描述")
@@ -533,14 +567,15 @@ class ImageManager:
description = cached_description description = cached_description
# 更新数据库 # 更新数据库
image = Images.get(Images.image_id == image_id)
image.description = description image.description = description
image.vlm_processed = True image.vlm_processed = True
image.save() image.save()
# 保存描述到ImageDescriptions表 # 保存描述到ImageDescriptions表作为备用缓存
self._save_description_to_db(image_hash, description, "image") self._save_description_to_db(image_hash, description, "image")
logger.info(f"[VLM异步完成] 图片描述生成: {description[:50]}...")
except Exception as e: except Exception as e:
logger.error(f"VLM处理图片失败: {str(e)}") logger.error(f"VLM处理图片失败: {str(e)}")

View File

@@ -28,7 +28,7 @@ class ClassicalWillingManager(BaseWillingManager):
# print(f"[{chat_id}] 回复意愿: {current_willing}") # 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}") # print(f"[{chat_id}] 兴趣值: {interested_rate}")
@@ -36,20 +36,18 @@ class ClassicalWillingManager(BaseWillingManager):
current_willing += interested_rate - 0.2 current_willing += interested_rate - 0.2
if willing_info.is_mentioned_bot and global_config.chat.mentioned_bot_inevitable_reply and current_willing < 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) 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}") # print(f"[{chat_id}] 回复概率: {reply_probability}")
return reply_probability return reply_probability
async def before_generate_reply_handle(self, message_id): async def before_generate_reply_handle(self, message_id):
chat_id = self.ongoing_messages[message_id].chat_id pass
current_willing = self.chat_reply_willing.get(chat_id, 0)
self.chat_reply_willing[chat_id] = max(0.0, current_willing - 1.8)
async def after_generate_reply_handle(self, message_id): async def after_generate_reply_handle(self, message_id):
if message_id not in self.ongoing_messages: if message_id not in self.ongoing_messages:
@@ -58,7 +56,7 @@ class ClassicalWillingManager(BaseWillingManager):
chat_id = self.ongoing_messages[message_id].chat_id chat_id = self.ongoing_messages[message_id].chat_id
current_willing = self.chat_reply_willing.get(chat_id, 0) current_willing = self.chat_reply_willing.get(chat_id, 0)
if current_willing < 1: 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): async def not_reply_handle(self, message_id):
return await super().not_reply_handle(message_id) return await super().not_reply_handle(message_id)

View File

@@ -390,7 +390,7 @@ MODULE_COLORS = {
"tts_action": "\033[38;5;58m", # 深黄色 "tts_action": "\033[38;5;58m", # 深黄色
"doubao_pic_plugin": "\033[38;5;64m", # 深绿色 "doubao_pic_plugin": "\033[38;5;64m", # 深绿色
# Action组件 # Action组件
"no_reply_action": "\033[38;5;196m", # 亮色,显眼 "no_reply_action": "\033[38;5;214m", # 亮色,显眼但不像警告
"reply_action": "\033[38;5;46m", # 亮绿色 "reply_action": "\033[38;5;46m", # 亮绿色
"base_action": "\033[38;5;250m", # 浅灰色 "base_action": "\033[38;5;250m", # 浅灰色
# 数据库和消息 # 数据库和消息

View File

@@ -70,21 +70,20 @@ class ChatConfig(ConfigBase):
max_context_size: int = 18 max_context_size: int = 18
"""上下文长度""" """上下文长度"""
willing_amplifier: float = 1.0
replyer_random_probability: float = 0.5 replyer_random_probability: float = 0.5
""" """
发言时选择推理模型的概率0-1之间 发言时选择推理模型的概率0-1之间
选择普通模型的概率为 1 - reasoning_normal_model_probability 选择普通模型的概率为 1 - reasoning_normal_model_probability
""" """
thinking_timeout: int = 30 thinking_timeout: int = 40
"""麦麦最长思考规划时间超过这个时间的思考会放弃往往是api反应太慢""" """麦麦最长思考规划时间超过这个时间的思考会放弃往往是api反应太慢"""
talk_frequency: float = 1 talk_frequency: float = 1
"""回复频率阈值""" """回复频率阈值"""
use_s4u_prompt_mode: bool = False
"""是否使用 s4u 对话构建模式,该模式会分开处理当前对话对象和其他所有对话的内容进行 prompt 构建"""
mentioned_bot_inevitable_reply: bool = False mentioned_bot_inevitable_reply: bool = False
"""提及 bot 必然回复""" """提及 bot 必然回复"""
@@ -274,12 +273,6 @@ class NormalChatConfig(ConfigBase):
willing_mode: str = "classical" willing_mode: str = "classical"
"""意愿模式""" """意愿模式"""
response_interested_rate_amplifier: float = 1.0
"""回复兴趣度放大系数"""
@dataclass @dataclass
class ExpressionConfig(ConfigBase): class ExpressionConfig(ConfigBase):
"""表达配置类""" """表达配置类"""
@@ -307,11 +300,8 @@ class ExpressionConfig(ConfigBase):
class ToolConfig(ConfigBase): class ToolConfig(ConfigBase):
"""工具配置类""" """工具配置类"""
enable_in_normal_chat: bool = False enable_tool: bool = False
"""是否在普通聊天中启用工具""" """是否在聊天中启用工具"""
enable_in_focus_chat: bool = True
"""是否在专注聊天中启用工具"""
@dataclass @dataclass
class VoiceConfig(ConfigBase): class VoiceConfig(ConfigBase):

View File

@@ -273,15 +273,19 @@ class Individuality:
prompt=prompt, prompt=prompt,
) )
if response.strip(): if response and response.strip():
personality_parts.append(response.strip()) personality_parts.append(response.strip())
logger.info(f"精简人格侧面: {response.strip()}") logger.info(f"精简人格侧面: {response.strip()}")
else: else:
logger.error(f"使用LLM压缩人设时出错: {response}") logger.error(f"使用LLM压缩人设时出错: {response}")
# 压缩失败时使用原始内容
if personality_side:
personality_parts.append(personality_side)
if personality_parts: if personality_parts:
personality_result = "".join(personality_parts) personality_result = "".join(personality_parts)
else: else:
personality_result = personality_core personality_result = personality_core or "友好活泼"
else: else:
personality_result = personality_core personality_result = personality_core
if personality_side: if personality_side:
@@ -308,13 +312,14 @@ class Individuality:
prompt=prompt, prompt=prompt,
) )
if response.strip(): if response and response.strip():
identity_result = response.strip() identity_result = response.strip()
logger.info(f"精简身份: {identity_result}") logger.info(f"精简身份: {identity_result}")
else: else:
logger.error(f"使用LLM压缩身份时出错: {response}") logger.error(f"使用LLM压缩身份时出错: {response}")
identity_result = identity
else: else:
identity_result = "".join(identity) identity_result = identity
return identity_result return identity_result

View File

@@ -47,11 +47,35 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
logger.debug(f"记忆激活率: {interested_rate:.2f}") logger.debug(f"记忆激活率: {interested_rate:.2f}")
text_len = len(message.processed_plain_text) text_len = len(message.processed_plain_text)
# 根据文本长度调整兴趣度,长度越大兴趣度越高但增长率递减最低0.01最高0.05 # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算
# 采用对数函数实现递减增长 # 基于实际分布0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%)
base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) if text_len == 0:
base_interest = min(max(base_interest, 0.01), 0.05) 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 interested_rate += base_interest

View File

@@ -78,7 +78,7 @@ class ChatMood:
if interested_rate <= 0: if interested_rate <= 0:
interest_multiplier = 0 interest_multiplier = 0
else: else:
interest_multiplier = 3 * math.pow(interested_rate, 0.25) interest_multiplier = 2 * math.pow(interested_rate, 0.25)
logger.debug( logger.debug(
f"base_probability: {base_probability}, time_multiplier: {time_multiplier}, interest_multiplier: {interest_multiplier}" f"base_probability: {base_probability}, time_multiplier: {time_multiplier}, interest_multiplier: {interest_multiplier}"

View File

@@ -139,7 +139,7 @@ class RelationshipManager:
请用json格式输出引起了你的兴趣或者有什么需要你记忆的点。 请用json格式输出引起了你的兴趣或者有什么需要你记忆的点。
并为每个点赋予1-10的权重权重越高表示越重要。 并为每个点赋予1-10的权重权重越高表示越重要。
格式如下: 格式如下:
{{ [
{{ {{
"point": "{person_name}想让我记住他的生日我回答确认了他的生日是11月23日", "point": "{person_name}想让我记住他的生日我回答确认了他的生日是11月23日",
"weight": 10 "weight": 10
@@ -156,13 +156,10 @@ class RelationshipManager:
"point": "{person_name}喜欢吃辣具体来说没有辣的食物ta都不喜欢吃可能是因为ta是湖南人。", "point": "{person_name}喜欢吃辣具体来说没有辣的食物ta都不喜欢吃可能是因为ta是湖南人。",
"weight": 7 "weight": 7
}} }}
}} ]
如果没有就输出none,或points为空 如果没有就输出none,或返回空数组
{{ []
"point": "none",
"weight": 0
}}
""" """
# 调用LLM生成印象 # 调用LLM生成印象
@@ -184,17 +181,25 @@ class RelationshipManager:
try: try:
points = repair_json(points) points = repair_json(points)
points_data = json.loads(points) points_data = json.loads(points)
if points_data == "none" or not points_data or points_data.get("point") == "none":
points_list = []
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]
# 只处理正确的格式,错误格式直接跳过
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.warning(f"LLM返回了错误的JSON格式跳过解析: {type(points_data)}, 内容: {points_data}")
points_list = []
# 权重过滤逻辑
if points_list:
original_points_list = list(points_list) original_points_list = list(points_list)
points_list.clear() points_list.clear()
discarded_count = 0 discarded_count = 0

View File

@@ -9,6 +9,7 @@ from .base import (
BasePlugin, BasePlugin,
BaseAction, BaseAction,
BaseCommand, BaseCommand,
BaseTool,
ConfigField, ConfigField,
ComponentType, ComponentType,
ActionActivationType, ActionActivationType,
@@ -34,6 +35,7 @@ from .utils import (
from .apis import ( from .apis import (
chat_api, chat_api,
tool_api,
component_manage_api, component_manage_api,
config_api, config_api,
database_api, database_api,
@@ -44,17 +46,17 @@ from .apis import (
person_api, person_api,
plugin_manage_api, plugin_manage_api,
send_api, send_api,
utils_api,
register_plugin, register_plugin,
get_logger, get_logger,
) )
__version__ = "1.0.0" __version__ = "2.0.0"
__all__ = [ __all__ = [
# API 模块 # API 模块
"chat_api", "chat_api",
"tool_api",
"component_manage_api", "component_manage_api",
"config_api", "config_api",
"database_api", "database_api",
@@ -65,13 +67,13 @@ __all__ = [
"person_api", "person_api",
"plugin_manage_api", "plugin_manage_api",
"send_api", "send_api",
"utils_api",
"register_plugin", "register_plugin",
"get_logger", "get_logger",
# 基础类 # 基础类
"BasePlugin", "BasePlugin",
"BaseAction", "BaseAction",
"BaseCommand", "BaseCommand",
"BaseTool",
"BaseEventHandler", "BaseEventHandler",
# 类型定义 # 类型定义
"ComponentType", "ComponentType",

View File

@@ -17,7 +17,7 @@ from src.plugin_system.apis import (
person_api, person_api,
plugin_manage_api, plugin_manage_api,
send_api, send_api,
utils_api, tool_api,
) )
from .logging_api import get_logger from .logging_api import get_logger
from .plugin_register_api import register_plugin from .plugin_register_api import register_plugin
@@ -35,7 +35,7 @@ __all__ = [
"person_api", "person_api",
"plugin_manage_api", "plugin_manage_api",
"send_api", "send_api",
"utils_api",
"get_logger", "get_logger",
"register_plugin", "register_plugin",
"tool_api",
] ]

View File

@@ -32,6 +32,7 @@ class ChatManager:
@staticmethod @staticmethod
def get_all_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: def get_all_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
# sourcery skip: for-append-to-extend
"""获取所有聊天流 """获取所有聊天流
Args: Args:
@@ -57,6 +58,7 @@ class ChatManager:
@staticmethod @staticmethod
def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: def get_group_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
# sourcery skip: for-append-to-extend
"""获取所有群聊聊天流 """获取所有群聊聊天流
Args: Args:
@@ -79,6 +81,7 @@ class ChatManager:
@staticmethod @staticmethod
def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]: def get_private_streams(platform: Optional[str] | SpecialTypes = "qq") -> List[ChatStream]:
# sourcery skip: for-append-to-extend
"""获取所有私聊聊天流 """获取所有私聊聊天流
Args: Args:
@@ -105,7 +108,7 @@ class ChatManager:
@staticmethod @staticmethod
def get_group_stream_by_group_id( def get_group_stream_by_group_id(
group_id: str, platform: Optional[str] | SpecialTypes = "qq" group_id: str, platform: Optional[str] | SpecialTypes = "qq"
) -> Optional[ChatStream]: ) -> Optional[ChatStream]: # sourcery skip: remove-unnecessary-cast
"""根据群ID获取聊天流 """根据群ID获取聊天流
Args: Args:
@@ -142,7 +145,7 @@ class ChatManager:
@staticmethod @staticmethod
def get_private_stream_by_user_id( def get_private_stream_by_user_id(
user_id: str, platform: Optional[str] | SpecialTypes = "qq" user_id: str, platform: Optional[str] | SpecialTypes = "qq"
) -> Optional[ChatStream]: ) -> Optional[ChatStream]: # sourcery skip: remove-unnecessary-cast
"""根据用户ID获取私聊流 """根据用户ID获取私聊流
Args: Args:
@@ -207,7 +210,7 @@ class ChatManager:
chat_stream: 聊天流对象 chat_stream: 聊天流对象
Returns: Returns:
Dict[str, Any]: 聊天流信息字典 Dict ({str: Any}): 聊天流信息字典
Raises: Raises:
TypeError: 如果 chat_stream 不是 ChatStream 类型 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) 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) 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) 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获取聊天流的便捷函数""" """根据群ID获取聊天流的便捷函数"""
return ChatManager.get_group_stream_by_group_id(group_id, platform) 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获取私聊流的便捷函数""" """根据用户ID获取私聊流的便捷函数"""
return ChatManager.get_private_stream_by_user_id(user_id, platform) 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) 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) return ChatManager.get_stream_info(chat_stream)
def get_streams_summary(): def get_streams_summary() -> Dict[str, int]:
"""获取聊天流统计摘要的便捷函数""" """获取聊天流统计摘要的便捷函数"""
return ChatManager.get_streams_summary() return ChatManager.get_streams_summary()

View File

@@ -5,6 +5,7 @@ from src.plugin_system.base.component_types import (
EventHandlerInfo, EventHandlerInfo,
PluginInfo, PluginInfo,
ComponentType, 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) 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 特定查询方法 === # === EventHandler 特定查询方法 ===
def get_registered_event_handler_info( def get_registered_event_handler_info(
event_handler_name: str, 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) return global_announcement_manager.enable_specific_chat_action(stream_id, component_name)
case ComponentType.COMMAND: case ComponentType.COMMAND:
return global_announcement_manager.enable_specific_chat_command(stream_id, component_name) 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: case ComponentType.EVENT_HANDLER:
return global_announcement_manager.enable_specific_chat_event_handler(stream_id, component_name) return global_announcement_manager.enable_specific_chat_event_handler(stream_id, component_name)
case _: 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) return global_announcement_manager.disable_specific_chat_action(stream_id, component_name)
case ComponentType.COMMAND: case ComponentType.COMMAND:
return global_announcement_manager.disable_specific_chat_command(stream_id, component_name) 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: case ComponentType.EVENT_HANDLER:
return global_announcement_manager.disable_specific_chat_event_handler(stream_id, component_name) return global_announcement_manager.disable_specific_chat_event_handler(stream_id, component_name)
case _: case _:
raise ValueError(f"未知 component type: {component_type}") raise ValueError(f"未知 component type: {component_type}")
def get_locally_disabled_components(stream_id: str, component_type: ComponentType) -> list[str]: def get_locally_disabled_components(stream_id: str, component_type: ComponentType) -> list[str]:
""" """
获取指定消息流中禁用的组件列表。 获取指定消息流中禁用的组件列表。
@@ -239,6 +260,8 @@ def get_locally_disabled_components(stream_id: str, component_type: ComponentTyp
return global_announcement_manager.get_disabled_chat_actions(stream_id) return global_announcement_manager.get_disabled_chat_actions(stream_id)
case ComponentType.COMMAND: case ComponentType.COMMAND:
return global_announcement_manager.get_disabled_chat_commands(stream_id) 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: case ComponentType.EVENT_HANDLER:
return global_announcement_manager.get_disabled_chat_event_handlers(stream_id) return global_announcement_manager.get_disabled_chat_event_handlers(stream_id)
case _: case _:

View File

@@ -10,7 +10,6 @@
from typing import Any from typing import Any
from src.common.logger import get_logger from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
from src.person_info.person_info import get_person_info_manager
logger = get_logger("config_api") logger = get_logger("config_api")
@@ -26,7 +25,7 @@ def get_global_config(key: str, default: Any = None) -> Any:
插件应使用此方法读取全局配置,以保证只读和隔离性。 插件应使用此方法读取全局配置,以保证只读和隔离性。
Args: Args:
key: 命名空间式配置键名,支持嵌套访问,如 "section.subsection.key",大小写敏感 key: 命名空间式配置键名,使用嵌套访问,如 "section.subsection.key",大小写敏感
default: 如果配置不存在时返回的默认值 default: 如果配置不存在时返回的默认值
Returns: Returns:
@@ -76,50 +75,3 @@ def get_plugin_config(plugin_config: dict, key: str, default: Any = None) -> Any
except Exception as e: except Exception as e:
logger.warning(f"[ConfigAPI] 获取插件配置 {key} 失败: {e}") logger.warning(f"[ConfigAPI] 获取插件配置 {key} 失败: {e}")
return default 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

View File

@@ -152,10 +152,7 @@ async def db_query(
except DoesNotExist: except DoesNotExist:
# 记录不存在 # 记录不存在
if query_type == "get" and single_result: return None if query_type == "get" and single_result else []
return None
return []
except Exception as e: except Exception as e:
logger.error(f"[DatabaseAPI] 数据库操作出错: {e}") logger.error(f"[DatabaseAPI] 数据库操作出错: {e}")
traceback.print_exc() traceback.print_exc()
@@ -170,7 +167,8 @@ async def db_query(
async def db_save( async def db_save(
model_class: Type[Model], data: Dict[str, Any], key_field: Optional[str] = None, key_value: Optional[Any] = None 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会先尝试查找匹配的记录进行更新 如果提供了key_field和key_value会先尝试查找匹配的记录进行更新
@@ -203,10 +201,9 @@ async def db_save(
try: try:
# 如果提供了key_field和key_value尝试更新现有记录 # 如果提供了key_field和key_value尝试更新现有记录
if key_field and key_value is not None: if key_field and key_value is not None:
# 查找现有记录 if existing_records := list(
existing_records = list(model_class.select().where(getattr(model_class, key_field) == key_value).limit(1)) model_class.select().where(getattr(model_class, key_field) == key_value).limit(1)
):
if existing_records:
# 更新现有记录 # 更新现有记录
existing_record = existing_records[0] existing_record = existing_records[0]
for field, value in data.items(): for field, value in data.items():
@@ -244,8 +241,8 @@ async def db_get(
Args: Args:
model_class: Peewee模型类 model_class: Peewee模型类
filters: 过滤条件,字段名和值的字典 filters: 过滤条件,字段名和值的字典
order_by: 排序字段,前缀'-'表示降序,例如'-time'表示按时间字段即time字段降序
limit: 结果数量限制 limit: 结果数量限制
order_by: 排序字段,前缀'-'表示降序,例如'-time'表示按时间字段即time字段降序
single_result: 是否只返回单个结果如果为True则返回单个记录字典或None否则返回记录字典列表或空列表 single_result: 是否只返回单个结果如果为True则返回单个记录字典或None否则返回记录字典列表或空列表
Returns: Returns:
@@ -310,7 +307,7 @@ async def store_action_info(
thinking_id: str = "", thinking_id: str = "",
action_data: Optional[dict] = None, action_data: Optional[dict] = None,
action_name: str = "", action_name: str = "",
) -> Union[Dict[str, Any], None]: ) -> Optional[Dict[str, Any]]:
"""存储动作信息到数据库 """存储动作信息到数据库
将Action执行的相关信息保存到ActionRecords表中用于后续的记忆和上下文构建。 将Action执行的相关信息保存到ActionRecords表中用于后续的记忆和上下文构建。

View File

@@ -65,14 +65,14 @@ async def get_by_description(description: str) -> Optional[Tuple[str, str, str]]
return None 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: Args:
count: 要获取的表情包数量默认为1 count: 要获取的表情包数量默认为1
Returns: Returns:
Optional[List[Tuple[str, str, str]]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表,如果失败则为None List[Tuple[str, str, str]]: 包含(base64编码, 表情包描述, 随机情感标签)的元组列表,失败则返回空列表
Raises: Raises:
TypeError: 如果count不是整数类型 TypeError: 如果count不是整数类型
@@ -94,13 +94,13 @@ async def get_random(count: Optional[int] = 1) -> Optional[List[Tuple[str, str,
if not all_emojis: if not all_emojis:
logger.warning("[EmojiAPI] 没有可用的表情包") logger.warning("[EmojiAPI] 没有可用的表情包")
return None return []
# 过滤有效表情包 # 过滤有效表情包
valid_emojis = [emoji for emoji in all_emojis if not emoji.is_deleted] valid_emojis = [emoji for emoji in all_emojis if not emoji.is_deleted]
if not valid_emojis: if not valid_emojis:
logger.warning("[EmojiAPI] 没有有效的表情包") logger.warning("[EmojiAPI] 没有有效的表情包")
return None return []
if len(valid_emojis) < count: if len(valid_emojis) < count:
logger.warning( 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: if not results and count > 0:
logger.warning("[EmojiAPI] 随机获取表情包失败,没有一个可以成功处理") logger.warning("[EmojiAPI] 随机获取表情包失败,没有一个可以成功处理")
return None return []
logger.info(f"[EmojiAPI] 成功获取 {len(results)} 个随机表情包") logger.info(f"[EmojiAPI] 成功获取 {len(results)} 个随机表情包")
return results return results
except Exception as e: except Exception as e:
logger.error(f"[EmojiAPI] 获取随机表情包失败: {e}") logger.error(f"[EmojiAPI] 获取随机表情包失败: {e}")
return None return []
async def get_by_emotion(emotion: str) -> Optional[Tuple[str, str, str]]: 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 = [] matching_emojis = []
for emoji_obj in all_emojis: matching_emojis.extend(
if not emoji_obj.is_deleted and emotion.lower() in [e.lower() for e in emoji_obj.emotion]: emoji_obj
matching_emojis.append(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: if not matching_emojis:
logger.warning(f"[EmojiAPI] 未找到匹配情感 '{emotion}' 的表情包") logger.warning(f"[EmojiAPI] 未找到匹配情感 '{emotion}' 的表情包")
return None return None
@@ -256,10 +257,11 @@ def get_descriptions() -> List[str]:
emoji_manager = get_emoji_manager() emoji_manager = get_emoji_manager()
descriptions = [] descriptions = []
for emoji_obj in emoji_manager.emoji_objects: descriptions.extend(
if not emoji_obj.is_deleted and emoji_obj.description: emoji_obj.description
descriptions.append(emoji_obj.description) for emoji_obj in emoji_manager.emoji_objects
if not emoji_obj.is_deleted and emoji_obj.description
)
return descriptions return descriptions
except Exception as e: except Exception as e:
logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}") logger.error(f"[EmojiAPI] 获取表情包描述失败: {e}")

View File

@@ -84,18 +84,23 @@ async def generate_reply(
enable_chinese_typo: bool = True, enable_chinese_typo: bool = True,
return_prompt: bool = False, return_prompt: bool = False,
model_configs: Optional[List[Dict[str, Any]]] = None, model_configs: Optional[List[Dict[str, Any]]] = None,
request_type: str = "", request_type: str = "generator_api",
enable_timeout: bool = False,
) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]: ) -> Tuple[bool, List[Tuple[str, Any]], Optional[str]]:
"""生成回复 """生成回复
Args: Args:
chat_stream: 聊天流对象(优先) chat_stream: 聊天流对象(优先)
chat_id: 聊天ID备用 chat_id: 聊天ID备用
action_data: 动作数据 action_data: 动作数据向下兼容包含reply_to和extra_info
reply_to: 回复对象,格式为 "发送者:消息内容"
extra_info: 额外信息,用于补充上下文
available_actions: 可用动作
enable_tool: 是否启用工具调用
enable_splitter: 是否启用消息分割器 enable_splitter: 是否启用消息分割器
enable_chinese_typo: 是否启用错字生成器 enable_chinese_typo: 是否启用错字生成器
return_prompt: 是否返回提示词 return_prompt: 是否返回提示词
model_configs: 模型配置列表
request_type: 请求类型可选记录LLM使用
Returns: Returns:
Tuple[bool, List[Tuple[str, Any]], Optional[str]]: (是否成功, 回复集合, 提示词) Tuple[bool, List[Tuple[str, Any]], Optional[str]]: (是否成功, 回复集合, 提示词)
""" """
@@ -108,13 +113,16 @@ async def generate_reply(
logger.debug("[GeneratorAPI] 开始生成回复") 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( success, content, prompt = await replyer.generate_reply_with_context(
reply_data=action_data or {},
reply_to=reply_to, reply_to=reply_to,
extra_info=extra_info, extra_info=extra_info,
available_actions=available_actions, available_actions=available_actions,
enable_timeout=enable_timeout,
enable_tool=enable_tool, enable_tool=enable_tool,
) )
reply_set = [] reply_set = []
@@ -136,6 +144,7 @@ async def generate_reply(
except Exception as e: except Exception as e:
logger.error(f"[GeneratorAPI] 生成回复时出错: {e}") logger.error(f"[GeneratorAPI] 生成回复时出错: {e}")
logger.error(traceback.format_exc())
return False, [], None return False, [], None
@@ -146,15 +155,24 @@ async def rewrite_reply(
enable_splitter: bool = True, enable_splitter: bool = True,
enable_chinese_typo: bool = True, enable_chinese_typo: bool = True,
model_configs: Optional[List[Dict[str, Any]]] = None, 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: Args:
chat_stream: 聊天流对象(优先) chat_stream: 聊天流对象(优先)
reply_data: 回复数据 reply_data: 回复数据字典(向下兼容备用,当其他参数缺失时从此获取)
chat_id: 聊天ID备用 chat_id: 聊天ID备用
enable_splitter: 是否启用消息分割器 enable_splitter: 是否启用消息分割器
enable_chinese_typo: 是否启用错字生成器 enable_chinese_typo: 是否启用错字生成器
model_configs: 模型配置列表
raw_reply: 原始回复内容
reason: 回复原因
reply_to: 回复对象
return_prompt: 是否返回提示词
Returns: Returns:
Tuple[bool, List[Tuple[str, Any]]]: (是否成功, 回复集合) 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) replyer = get_replyer(chat_stream, chat_id, model_configs=model_configs)
if not replyer: if not replyer:
logger.error("[GeneratorAPI] 无法获取回复器") logger.error("[GeneratorAPI] 无法获取回复器")
return False, [] return False, [], None
logger.info("[GeneratorAPI] 开始重写回复") 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 = [] reply_set = []
if content: if content:
reply_set = await process_human_text(content, enable_splitter, enable_chinese_typo) reply_set = await process_human_text(content, enable_splitter, enable_chinese_typo)
@@ -179,14 +208,14 @@ async def rewrite_reply(
else: else:
logger.warning("[GeneratorAPI] 重写回复失败") logger.warning("[GeneratorAPI] 重写回复失败")
return success, reply_set return success, reply_set, prompt if return_prompt else None
except ValueError as ve: except ValueError as ve:
raise ve raise ve
except Exception as e: except Exception as e:
logger.error(f"[GeneratorAPI] 重写回复时出错: {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]]: 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: except Exception as e:
logger.error(f"[GeneratorAPI] 处理人形文本时出错: {e}") logger.error(f"[GeneratorAPI] 处理人形文本时出错: {e}")
return [] 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

View File

@@ -54,7 +54,7 @@ def get_available_models() -> Dict[str, Any]:
async def generate_with_model( async def generate_with_model(
prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs prompt: str, model_config: Dict[str, Any], request_type: str = "plugin.generate", **kwargs
) -> Tuple[bool, str, str, str]: ) -> Tuple[bool, str]:
"""使用指定模型生成内容 """使用指定模型生成内容
Args: Args:
@@ -73,10 +73,11 @@ async def generate_with_model(
llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs) llm_request = LLMRequest(model=model_config, request_type=request_type, **kwargs)
response, (reasoning, model_name) = await llm_request.generate_response_async(prompt) # TODO: 复活这个_
return True, response, reasoning, model_name response, _ = await llm_request.generate_response_async(prompt)
return True, response
except Exception as e: except Exception as e:
error_msg = f"生成内容时出错: {str(e)}" error_msg = f"生成内容时出错: {str(e)}"
logger.error(f"[LLMAPI] {error_msg}") logger.error(f"[LLMAPI] {error_msg}")
return False, error_msg, "", "" return False, error_msg

View File

@@ -207,7 +207,7 @@ def get_random_chat_messages(
def get_messages_by_time_for_users( 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]]: ) -> 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) 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) 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:
""" """
计算指定聊天中指定用户从开始时间到结束时间的新消息数量 计算指定聊天中指定用户从开始时间到结束时间的新消息数量

View File

@@ -1,10 +1,12 @@
from typing import Tuple, List from typing import Tuple, List
def list_loaded_plugins() -> List[str]: def list_loaded_plugins() -> List[str]:
""" """
列出所有当前加载的插件。 列出所有当前加载的插件。
Returns: Returns:
list: 当前加载的插件名称列表。 List[str]: 当前加载的插件名称列表。
""" """
from src.plugin_system.core.plugin_manager import plugin_manager from src.plugin_system.core.plugin_manager import plugin_manager
@@ -16,13 +18,34 @@ def list_registered_plugins() -> List[str]:
列出所有已注册的插件。 列出所有已注册的插件。
Returns: Returns:
list: 已注册的插件名称列表。 List[str]: 已注册的插件名称列表。
""" """
from src.plugin_system.core.plugin_manager import plugin_manager from src.plugin_system.core.plugin_manager import plugin_manager
return plugin_manager.list_registered_plugins() 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: async def remove_plugin(plugin_name: str) -> bool:
""" """
卸载指定的插件。 卸载指定的插件。
@@ -71,6 +94,7 @@ def load_plugin(plugin_name: str) -> Tuple[bool, int]:
return plugin_manager.load_registered_plugin_classes(plugin_name) return plugin_manager.load_registered_plugin_classes(plugin_name)
def add_plugin_directory(plugin_directory: str) -> bool: 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) return plugin_manager.add_plugin_directory(plugin_directory)
def rescan_plugin_directory() -> Tuple[int, int]: def rescan_plugin_directory() -> Tuple[int, int]:
""" """
重新扫描插件目录,加载新插件。 重新扫描插件目录,加载新插件。

View File

@@ -22,7 +22,6 @@
import traceback import traceback
import time import time
import difflib import difflib
import re
from typing import Optional, Union from typing import Optional, Union
from src.common.logger import get_logger 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.chat_stream import get_chat_manager
from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.message_receive.uni_message_sender import HeartFCSender
from src.chat.message_receive.message import MessageSending, MessageRecv 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 src.person_info.person_info import get_person_info_manager
from maim_message import Seg, UserInfo from maim_message import Seg, UserInfo
from src.config.config import global_config from src.config.config import global_config
@@ -50,7 +49,7 @@ async def _send_to_target(
display_message: str = "", display_message: str = "",
typing: bool = False, typing: bool = False,
reply_to: str = "", reply_to: str = "",
reply_to_platform_id: str = "", reply_to_platform_id: Optional[str] = None,
storage_message: bool = True, storage_message: bool = True,
show_log: bool = True, show_log: bool = True,
) -> bool: ) -> bool:
@@ -61,8 +60,11 @@ async def _send_to_target(
content: 消息内容 content: 消息内容
stream_id: 目标流ID stream_id: 目标流ID
display_message: 显示消息 display_message: 显示消息
typing: 是否显示正在输入 typing: 是否模拟打字等待。
reply_to: 回复消息格式,如"发送者:消息内容" reply_to: 回复消息格式"发送者:消息内容"
reply_to_platform_id: 回复消息,格式为"平台:用户ID",如果不提供则自动查找(插件开发者禁用!)
storage_message: 是否存储消息到数据库
show_log: 发送是否显示日志
Returns: Returns:
bool: 是否发送成功 bool: 是否发送成功
@@ -98,6 +100,10 @@ async def _send_to_target(
anchor_message = None anchor_message = None
if reply_to: if reply_to:
anchor_message = await _find_reply_message(target_stream, 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( bot_message = MessageSending(
@@ -183,32 +189,8 @@ async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageR
if person_name == sender: if person_name == sender:
translate_text = message["processed_plain_text"] translate_text = message["processed_plain_text"]
# 检查是否有 回复<aaa:bbb> 字段 # 使用独立函数处理用户引用格式
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" translate_text = await replace_user_references_async(translate_text, platform)
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)
# 检查是否有 @<aaa:bbb> 字段
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
similarity = difflib.SequenceMatcher(None, text, translate_text).ratio() similarity = difflib.SequenceMatcher(None, text, translate_text).ratio()
if similarity >= 0.9: if similarity >= 0.9:
@@ -287,12 +269,22 @@ async def text_to_stream(
stream_id: 聊天流ID stream_id: 聊天流ID
typing: 是否显示正在输入 typing: 是否显示正在输入
reply_to: 回复消息,格式为"发送者:消息内容" reply_to: 回复消息,格式为"发送者:消息内容"
reply_to_platform_id: 回复消息,格式为"平台:用户ID",如果不提供则自动查找(插件开发者禁用!)
storage_message: 是否存储消息到数据库 storage_message: 是否存储消息到数据库
Returns: Returns:
bool: 是否发送成功 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: 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, storage_message=storage_message,
show_log=show_log, 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
)

View File

@@ -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()]

View File

@@ -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())

View File

@@ -6,6 +6,7 @@
from .base_plugin import BasePlugin from .base_plugin import BasePlugin
from .base_action import BaseAction from .base_action import BaseAction
from .base_tool import BaseTool
from .base_command import BaseCommand from .base_command import BaseCommand
from .base_events_handler import BaseEventHandler from .base_events_handler import BaseEventHandler
from .component_types import ( from .component_types import (
@@ -15,6 +16,7 @@ from .component_types import (
ComponentInfo, ComponentInfo,
ActionInfo, ActionInfo,
CommandInfo, CommandInfo,
ToolInfo,
PluginInfo, PluginInfo,
PythonDependency, PythonDependency,
EventHandlerInfo, EventHandlerInfo,
@@ -27,12 +29,14 @@ __all__ = [
"BasePlugin", "BasePlugin",
"BaseAction", "BaseAction",
"BaseCommand", "BaseCommand",
"BaseTool",
"ComponentType", "ComponentType",
"ActionActivationType", "ActionActivationType",
"ChatMode", "ChatMode",
"ComponentInfo", "ComponentInfo",
"ActionInfo", "ActionInfo",
"CommandInfo", "CommandInfo",
"ToolInfo",
"PluginInfo", "PluginInfo",
"PythonDependency", "PythonDependency",
"ConfigField", "ConfigField",

View File

@@ -208,7 +208,7 @@ class BaseAction(ABC):
return False, f"等待新消息失败: {str(e)}" return False, f"等待新消息失败: {str(e)}"
async def send_text( 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: ) -> bool:
"""发送文本消息 """发送文本消息
@@ -227,7 +227,6 @@ class BaseAction(ABC):
text=content, text=content,
stream_id=self.chat_id, stream_id=self.chat_id,
reply_to=reply_to, reply_to=reply_to,
reply_to_platform_id=reply_to_platform_id,
typing=typing, typing=typing,
) )
@@ -384,7 +383,7 @@ class BaseAction(ABC):
keyword_case_sensitive=getattr(cls, "keyword_case_sensitive", False), keyword_case_sensitive=getattr(cls, "keyword_case_sensitive", False),
mode_enable=getattr(cls, "mode_enable", ChatMode.ALL), mode_enable=getattr(cls, "mode_enable", ChatMode.ALL),
parallel_action=getattr(cls, "parallel_action", True), 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", ""), llm_judge_prompt=getattr(cls, "llm_judge_prompt", ""),
# 使用正确的字段名 # 使用正确的字段名
action_parameters=getattr(cls, "action_parameters", {}).copy(), action_parameters=getattr(cls, "action_parameters", {}).copy(),

View File

@@ -60,10 +60,10 @@ class BaseCommand(ABC):
pass pass
def get_config(self, key: str, default=None): def get_config(self, key: str, default=None):
"""获取插件配置值,支持嵌套键访问 """获取插件配置值,使用嵌套键访问
Args: Args:
key: 配置键名,支持嵌套访问如 "section.subsection.key" key: 配置键名,使用嵌套访问如 "section.subsection.key"
default: 默认值 default: 默认值
Returns: Returns:

View File

@@ -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)

View File

@@ -10,6 +10,7 @@ class ComponentType(Enum):
ACTION = "action" # 动作组件 ACTION = "action" # 动作组件
COMMAND = "command" # 命令组件 COMMAND = "command" # 命令组件
TOOL = "tool" # 服务组件(预留)
SCHEDULER = "scheduler" # 定时任务组件(预留) SCHEDULER = "scheduler" # 定时任务组件(预留)
EVENT_HANDLER = "event_handler" # 事件处理组件(预留) EVENT_HANDLER = "event_handler" # 事件处理组件(预留)
@@ -145,6 +146,16 @@ class CommandInfo(ComponentInfo):
super().__post_init__() super().__post_init__()
self.component_type = ComponentType.COMMAND 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 @dataclass
class EventHandlerInfo(ComponentInfo): class EventHandlerInfo(ComponentInfo):

View File

@@ -6,14 +6,12 @@
from src.plugin_system.core.plugin_manager import plugin_manager 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.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.events_manager import events_manager
from src.plugin_system.core.global_announcement_manager import global_announcement_manager from src.plugin_system.core.global_announcement_manager import global_announcement_manager
__all__ = [ __all__ = [
"plugin_manager", "plugin_manager",
"component_registry", "component_registry",
"dependency_manager",
"events_manager", "events_manager",
"global_announcement_manager", "global_announcement_manager",
] ]

View File

@@ -6,6 +6,7 @@ from src.common.logger import get_logger
from src.plugin_system.base.component_types import ( from src.plugin_system.base.component_types import (
ComponentInfo, ComponentInfo,
ActionInfo, ActionInfo,
ToolInfo,
CommandInfo, CommandInfo,
EventHandlerInfo, EventHandlerInfo,
PluginInfo, 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_command import BaseCommand
from src.plugin_system.base.base_action import BaseAction 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 from src.plugin_system.base.base_events_handler import BaseEventHandler
logger = get_logger("component_registry") 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_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] = {} self._command_patterns: Dict[Pattern, str] = {}
"""编译后的正则 -> command名""" """编译后的正则 -> command名"""
# 工具特定注册表
self._tool_registry: Dict[str, Type[BaseTool]] = {} # 工具名 -> 工具类
self._llm_available_tools: Dict[str, Type[BaseTool]] = {} # llm可用的工具名 -> 工具类
# EventHandler特定注册表 # EventHandler特定注册表
self._event_handler_registry: Dict[str, Type[BaseEventHandler]] = {} self._event_handler_registry: Dict[str, Type[BaseEventHandler]] = {}
"""event_handler名 -> event_handler类""" """event_handler名 -> event_handler类"""
@@ -79,7 +85,9 @@ class ComponentRegistry:
return True return True
def register_component( 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: ) -> bool:
"""注册组件 """注册组件
@@ -125,6 +133,10 @@ class ComponentRegistry:
assert isinstance(component_info, CommandInfo) assert isinstance(component_info, CommandInfo)
assert issubclass(component_class, BaseCommand) assert issubclass(component_class, BaseCommand)
ret = self._register_command_component(component_info, component_class) 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: case ComponentType.EVENT_HANDLER:
assert isinstance(component_info, EventHandlerInfo) assert isinstance(component_info, EventHandlerInfo)
assert issubclass(component_class, BaseEventHandler) assert issubclass(component_class, BaseEventHandler)
@@ -180,6 +192,18 @@ class ComponentRegistry:
return True 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( def _register_event_handler_component(
self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler] self, handler_info: EventHandlerInfo, handler_class: Type[BaseEventHandler]
) -> bool: ) -> bool:
@@ -222,6 +246,9 @@ class ComponentRegistry:
keys_to_remove = [k for k, v in self._command_patterns.items() if v == component_name] keys_to_remove = [k for k, v in self._command_patterns.items() if v == component_name]
for key in keys_to_remove: for key in keys_to_remove:
self._command_patterns.pop(key) 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: case ComponentType.EVENT_HANDLER:
from .events_manager import events_manager # 延迟导入防止循环导入问题 from .events_manager import events_manager # 延迟导入防止循环导入问题
@@ -234,8 +261,8 @@ class ComponentRegistry:
self._components_classes.pop(namespaced_name) self._components_classes.pop(namespaced_name)
logger.info(f"组件 {component_name} 已移除") logger.info(f"组件 {component_name} 已移除")
return True return True
except KeyError: except KeyError as e:
logger.warning(f"移除组件时未找到组件: {component_name}") logger.warning(f"移除组件时未找到组件: {component_name}, 发生错误: {e}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"移除组件 {component_name} 时发生错误: {e}") logger.error(f"移除组件 {component_name} 时发生错误: {e}")
@@ -281,6 +308,10 @@ class ComponentRegistry:
assert isinstance(target_component_info, CommandInfo) assert isinstance(target_component_info, CommandInfo)
pattern = target_component_info.command_pattern pattern = target_component_info.command_pattern
self._command_patterns[re.compile(pattern)] = component_name 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: case ComponentType.EVENT_HANDLER:
assert isinstance(target_component_info, EventHandlerInfo) assert isinstance(target_component_info, EventHandlerInfo)
assert issubclass(target_component_class, BaseEventHandler) assert issubclass(target_component_class, BaseEventHandler)
@@ -308,20 +339,29 @@ class ComponentRegistry:
logger.warning(f"组件 {component_name} 未注册,无法禁用") logger.warning(f"组件 {component_name} 未注册,无法禁用")
return False return False
target_component_info.enabled = False target_component_info.enabled = False
match component_type: try:
case ComponentType.ACTION: match component_type:
self._default_actions.pop(component_name, None) case ComponentType.ACTION:
case ComponentType.COMMAND: self._default_actions.pop(component_name)
self._command_patterns = {k: v for k, v in self._command_patterns.items() if v != component_name} case ComponentType.COMMAND:
case ComponentType.EVENT_HANDLER: self._command_patterns = {k: v for k, v in self._command_patterns.items() if v != component_name}
self._enabled_event_handlers.pop(component_name, None) case ComponentType.TOOL:
from .events_manager import events_manager # 延迟导入防止循环导入问题 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) await events_manager.unregister_event_subscriber(component_name)
self._components[component_name].enabled = False self._components[component_name].enabled = False
self._components_by_type[component_type][component_name].enabled = False self._components_by_type[component_type][component_name].enabled = False
logger.info(f"组件 {component_name} 已禁用") logger.info(f"组件 {component_name} 已禁用")
return True 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( def get_component_info(
@@ -371,7 +411,7 @@ class ComponentRegistry:
self, self,
component_name: str, component_name: str,
component_type: Optional[ComponentType] = None, component_type: Optional[ComponentType] = None,
) -> Optional[Union[Type[BaseCommand], Type[BaseAction], Type[BaseEventHandler]]]: ) -> Optional[Union[Type[BaseCommand], Type[BaseAction], Type[BaseEventHandler], Type[BaseTool]]]:
"""获取组件类,支持自动命名空间解析 """获取组件类,支持自动命名空间解析
Args: Args:
@@ -476,6 +516,27 @@ class ComponentRegistry:
command_info, 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 特定查询方法 === # === EventHandler 特定查询方法 ===
def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]: def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]:
@@ -529,17 +590,21 @@ class ComponentRegistry:
"""获取注册中心统计信息""" """获取注册中心统计信息"""
action_components: int = 0 action_components: int = 0
command_components: int = 0 command_components: int = 0
tool_components: int = 0
events_handlers: int = 0 events_handlers: int = 0
for component in self._components.values(): for component in self._components.values():
if component.component_type == ComponentType.ACTION: if component.component_type == ComponentType.ACTION:
action_components += 1 action_components += 1
elif component.component_type == ComponentType.COMMAND: elif component.component_type == ComponentType.COMMAND:
command_components += 1 command_components += 1
elif component.component_type == ComponentType.TOOL:
tool_components += 1
elif component.component_type == ComponentType.EVENT_HANDLER: elif component.component_type == ComponentType.EVENT_HANDLER:
events_handlers += 1 events_handlers += 1
return { return {
"action_components": action_components, "action_components": action_components,
"command_components": command_components, "command_components": command_components,
"tool_components": tool_components,
"event_handlers": events_handlers, "event_handlers": events_handlers,
"total_components": len(self._components), "total_components": len(self._components),
"total_plugins": len(self._plugins), "total_plugins": len(self._plugins),

View File

@@ -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()

View File

@@ -13,6 +13,8 @@ class GlobalAnnouncementManager:
self._user_disabled_commands: Dict[str, List[str]] = {} self._user_disabled_commands: Dict[str, List[str]] = {}
# 用户禁用的事件处理器chat_id -> [handler_name] # 用户禁用的事件处理器chat_id -> [handler_name]
self._user_disabled_event_handlers: Dict[str, List[str]] = {} 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: def disable_specific_chat_action(self, chat_id: str, action_name: str) -> bool:
"""禁用特定聊天的某个动作""" """禁用特定聊天的某个动作"""
@@ -77,6 +79,27 @@ class GlobalAnnouncementManager:
return False return False
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]: def get_disabled_chat_actions(self, chat_id: str) -> List[str]:
"""获取特定聊天禁用的所有动作""" """获取特定聊天禁用的所有动作"""
return self._user_disabled_actions.get(chat_id, []).copy() return self._user_disabled_actions.get(chat_id, []).copy()
@@ -89,5 +112,9 @@ class GlobalAnnouncementManager:
"""获取特定聊天禁用的所有事件处理器""" """获取特定聊天禁用的所有事件处理器"""
return self._user_disabled_event_handlers.get(chat_id, []).copy() 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() global_announcement_manager = GlobalAnnouncementManager()

View File

@@ -8,10 +8,9 @@ from pathlib import Path
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.base.plugin_base import PluginBase 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 src.plugin_system.utils.manifest_utils import VersionComparator
from .component_registry import component_registry from .component_registry import component_registry
from .dependency_manager import dependency_manager
logger = get_logger("plugin_manager") logger = get_logger("plugin_manager")
@@ -207,104 +206,6 @@ class PluginManager:
""" """
return self.loaded_plugins.get(plugin_name) 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]: def list_loaded_plugins(self) -> List[str]:
""" """
@@ -324,6 +225,18 @@ class PluginManager:
""" """
return list(self.plugin_classes.keys()) 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)
# === 私有方法 === # === 私有方法 ===
# == 目录管理 == # == 目录管理 ==
def _ensure_plugin_directories(self) -> None: def _ensure_plugin_directories(self) -> None:
@@ -388,6 +301,7 @@ class PluginManager:
return False return False
module = module_from_spec(spec) module = module_from_spec(spec)
module.__package__ = module_name # 设置模块包名
spec.loader.exec_module(module) spec.loader.exec_module(module)
logger.debug(f"插件模块加载成功: {plugin_file}") logger.debug(f"插件模块加载成功: {plugin_file}")
@@ -444,6 +358,7 @@ class PluginManager:
stats = component_registry.get_registry_stats() stats = component_registry.get_registry_stats()
action_count = stats.get("action_components", 0) action_count = stats.get("action_components", 0)
command_count = stats.get("command_components", 0) command_count = stats.get("command_components", 0)
tool_count = stats.get("tool_components", 0)
event_handler_count = stats.get("event_handlers", 0) event_handler_count = stats.get("event_handlers", 0)
total_components = stats.get("total_components", 0) total_components = stats.get("total_components", 0)
@@ -451,7 +366,7 @@ class PluginManager:
if total_registered > 0: if total_registered > 0:
logger.info("🎉 插件系统加载完成!") logger.info("🎉 插件系统加载完成!")
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 = [ command_components = [
c for c in plugin_info.components if c.component_type == ComponentType.COMMAND 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 = [ event_handler_components = [
c for c in plugin_info.components if c.component_type == ComponentType.EVENT_HANDLER c for c in plugin_info.components if c.component_type == ComponentType.EVENT_HANDLER
] ]
@@ -497,7 +415,9 @@ class PluginManager:
if command_components: if command_components:
command_names = [c.name for c in command_components] command_names = [c.name for c in command_components]
logger.info(f" ⚡ Command组件: {', '.join(command_names)}") 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: if event_handler_components:
event_handler_names = [c.name for c in event_handler_components] event_handler_names = [c.name for c in event_handler_components]
logger.info(f" 📢 EventHandler组件: {', '.join(event_handler_names)}") logger.info(f" 📢 EventHandler组件: {', '.join(event_handler_names)}")

View File

@@ -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.llm_models.utils_model import LLMRequest
from src.config.config import global_config 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.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 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.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(): 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") Prompt(tool_executor_prompt, "tool_executor_prompt")
# 初始化提示词
init_tool_executor_prompt()
class ToolExecutor: class ToolExecutor:
"""独立的工具执行器组件 """独立的工具执行器组件
@@ -51,9 +57,6 @@ class ToolExecutor:
request_type="tool_executor", request_type="tool_executor",
) )
# 初始化工具实例
self.tool_instance = ToolUser()
# 缓存配置 # 缓存配置
self.enable_cache = enable_cache self.enable_cache = enable_cache
self.cache_ttl = cache_ttl self.cache_ttl = cache_ttl
@@ -73,7 +76,7 @@ class ToolExecutor:
return_details: 是否返回详细信息(使用的工具列表和提示词) return_details: 是否返回详细信息(使用的工具列表和提示词)
Returns: Returns:
如果return_details为False: List[Dict] - 工具执行结果列表 如果return_details为False: Tuple[List[Dict], List[str], str] - (工具执行结果列表, , )
如果return_details为True: 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): if cached_result := self._get_from_cache(cache_key):
logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行") logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行")
if not return_details: if not return_details:
return cached_result, [], "使用缓存结果" return cached_result, [], ""
# 从缓存结果中提取工具名称 # 从缓存结果中提取工具名称
used_tools = [result.get("tool_name", "unknown") for result in 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()) time_now = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
@@ -112,6 +115,7 @@ class ToolExecutor:
# 调用LLM进行工具决策 # 调用LLM进行工具决策
response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools) response, other_info = await self.llm_model.generate_response_async(prompt=prompt, tools=tools)
# TODO: 在APIADA加入后完全修复这里
# 解析LLM响应 # 解析LLM响应
if len(other_info) == 3: if len(other_info) == 3:
reasoning_content, model_name, tool_calls = other_info reasoning_content, model_name, tool_calls = other_info
@@ -134,6 +138,11 @@ class ToolExecutor:
else: else:
return tool_results, [], "" 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]]: 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}") 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: if result:
tool_info = { tool_info = {
@@ -205,6 +214,45 @@ class ToolExecutor:
return tool_results, used_tools 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: def _generate_cache_key(self, target_message: str, chat_history: str, sender: str) -> str:
"""生成缓存键 """生成缓存键
@@ -272,15 +320,6 @@ class ToolExecutor:
if expired_keys: if expired_keys:
logger.debug(f"{self.log_prefix}清理了{len(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( async def execute_specific_tool(
self, tool_name: str, tool_args: Dict, validate_args: bool = True self, tool_name: str, tool_args: Dict, validate_args: bool = True
) -> Optional[Dict]: ) -> Optional[Dict]:
@@ -299,7 +338,7 @@ class ToolExecutor:
logger.info(f"{self.log_prefix}直接执行工具: {tool_name}") 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: if result:
tool_info = { tool_info = {
@@ -366,12 +405,8 @@ class ToolExecutor:
logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}") logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}")
# 初始化提示词
init_tool_executor_prompt()
""" """
使用示例 ToolExecutor使用示例
# 1. 基础使用 - 从聊天消息执行工具启用缓存默认TTL=3 # 1. 基础使用 - 从聊天消息执行工具启用缓存默认TTL=3
executor = ToolExecutor(executor_id="my_executor") executor = ToolExecutor(executor_id="my_executor")
@@ -400,7 +435,6 @@ result = await executor.execute_specific_tool(
) )
# 6. 缓存管理 # 6. 缓存管理
available_tools = executor.get_available_tools()
cache_status = executor.get_cache_status() # 查看缓存状态 cache_status = executor.get_cache_status() # 查看缓存状态
executor.clear_cache() # 清空缓存 executor.clear_cache() # 清空缓存
executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置 executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置

View File

@@ -163,12 +163,11 @@ class VersionComparator:
version_normalized, max_normalized version_normalized, max_normalized
) )
if is_compatible: if not is_compatible:
logger.info(f"版本兼容性检查:{compat_msg}")
return True, compat_msg
else:
return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized},且无兼容性映射" return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized},且无兼容性映射"
logger.info(f"版本兼容性检查:{compat_msg}")
return True, compat_msg
return True, "" return True, ""
@staticmethod @staticmethod
@@ -358,14 +357,10 @@ class ManifestValidator:
if self.validation_errors: if self.validation_errors:
report.append("❌ 验证错误:") report.append("❌ 验证错误:")
for error in self.validation_errors: report.extend(f" - {error}" for error in self.validation_errors)
report.append(f" - {error}")
if self.validation_warnings: if self.validation_warnings:
report.append("⚠️ 验证警告:") report.append("⚠️ 验证警告:")
for warning in self.validation_warnings: report.extend(f" - {warning}" for warning in self.validation_warnings)
report.append(f" - {warning}")
if not self.validation_errors and not self.validation_warnings: if not self.validation_errors and not self.validation_warnings:
report.append("✅ Manifest文件验证通过") report.append("✅ Manifest文件验证通过")

View File

@@ -24,11 +24,6 @@
"is_built_in": true, "is_built_in": true,
"plugin_type": "action_provider", "plugin_type": "action_provider",
"components": [ "components": [
{
"type": "action",
"name": "reply",
"description": "参与聊天回复,发送文本进行表达"
},
{ {
"type": "action", "type": "action",
"name": "no_reply", "name": "no_reply",

View File

@@ -9,7 +9,8 @@ from src.common.logger import get_logger
# 导入API模块 - 标准Python包方式 # 导入API模块 - 标准Python包方式
from src.plugin_system.apis import emoji_api, llm_api, message_api 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 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 mode_enable = ChatMode.ALL
parallel_action = True parallel_action = True
random_activation_probability = 0.2 # 默认值,可通过配置覆盖
# 动作基本信息 # 动作基本信息
action_name = "emoji" action_name = "emoji"
@@ -115,7 +120,7 @@ class EmojiAction(BaseAction):
logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置无法调用LLM") logger.error(f"{self.log_prefix} 未找到'utils_small'模型配置无法调用LLM")
return False, "未找到'utils_small'模型配置" 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" prompt, model_config=chat_model_config, request_type="emoji"
) )
@@ -143,8 +148,8 @@ class EmojiAction(BaseAction):
logger.error(f"{self.log_prefix} 表情包发送失败") logger.error(f"{self.log_prefix} 表情包发送失败")
return False, "表情包发送失败" return False, "表情包发送失败"
# 重置NoReplyAction的连续计数器 # 注释:重置NoReplyAction的连续计数器现在由heartFC_chat.py统一管理
NoReplyAction.reset_consecutive_count() # NoReplyAction.reset_consecutive_count()
return True, f"发送表情包: {emoji_description}" return True, f"发送表情包: {emoji_description}"

View File

@@ -1,6 +1,7 @@
import random import random
import time import time
from typing import Tuple from typing import Tuple, List
from collections import deque
# 导入新插件系统 # 导入新插件系统
from src.plugin_system import BaseAction, ActionActivationType, ChatMode from src.plugin_system import BaseAction, ActionActivationType, ChatMode
@@ -17,11 +18,15 @@ logger = get_logger("no_reply_action")
class NoReplyAction(BaseAction): class NoReplyAction(BaseAction):
"""不回复动作,根据新消息的兴趣值或数量决定何时结束等待. """不回复动作,支持waiting和breaking两种形式.
新的等待逻辑: waiting形式:
1. 新消息累计兴趣值超过阈值 (默认10) 则结束等待 - 只要有新消息就结束动作
2. 累计新消息数量达到随机阈值 (默认5-10条) 则结束等待 - 记录新消息的兴趣度到列表(最多保留最近三项)
- 如果最近三次动作都是no_reply且最近新消息列表兴趣度之和小于阈值就进入breaking形式
breaking形式:
- 和原有逻辑一致,需要消息满足一定数量或累计一定兴趣值才结束动作
""" """
focus_activation_type = ActionActivationType.NEVER focus_activation_type = ActionActivationType.NEVER
@@ -36,111 +41,44 @@ class NoReplyAction(BaseAction):
# 连续no_reply计数器 # 连续no_reply计数器
_consecutive_count = 0 _consecutive_count = 0
# 新增:兴趣值退出阈值 # 最近三次no_reply的新消息兴趣度记录
_recent_interest_records: deque = deque(maxlen=3)
# 兴趣值退出阈值
_interest_exit_threshold = 3.0 _interest_exit_threshold = 3.0
# 新增:消息数量退出阈值 # 消息数量退出阈值
_min_exit_message_count = 5 _min_exit_message_count = 3
_max_exit_message_count = 10 _max_exit_message_count = 6
# 动作参数定义 # 动作参数定义
action_parameters = {} action_parameters = {}
# 动作使用场景 # 动作使用场景
action_require = ["你发送了消息,目前无人回复"] action_require = [""]
# 关联类型 # 关联类型
associated_types = [] associated_types = []
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行不回复动作""" """执行不回复动作"""
import asyncio
try: try:
# 增加连续计数
NoReplyAction._consecutive_count += 1
count = NoReplyAction._consecutive_count
reason = self.action_data.get("reason", "") reason = self.action_data.get("reason", "")
start_time = self.action_data.get("loop_start_time", time.time()) 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) form_type = self._determine_form_type()
logger.info(
f"{self.log_prefix} 本次no_reply需要 {exit_message_count_threshold} 条新消息或累计兴趣值超过 {self._interest_exit_threshold} 才能打断"
)
logger.info(f"{self.log_prefix} 选择不回复(第{count}次),开始摸鱼,原因: {reason}") logger.info(f"{self.log_prefix} 选择不回复(第{NoReplyAction._consecutive_count + 1}次),使用{form_type}形式,原因: {reason}")
# 进入等待状态 # 增加连续计数在确定要执行no_reply时才增加
while True: NoReplyAction._consecutive_count += 1
current_time = time.time()
elapsed_time = current_time - start_time
# 1. 检查新消息 if form_type == "waiting":
recent_messages_dict = message_api.get_messages_by_time_in_chat( return await self._execute_waiting_form(start_time, check_interval)
chat_id=self.chat_id, else:
start_time=start_time, return await self._execute_breaking_form(start_time, check_interval)
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)
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}") logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}")
@@ -153,8 +91,191 @@ class NoReplyAction(BaseAction):
) )
return False, f"不回复动作执行失败: {e}" 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 @classmethod
def reset_consecutive_count(cls): def reset_consecutive_count(cls):
"""重置连续计数器""" """重置连续计数器和兴趣度记录"""
cls._consecutive_count = 0 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

View File

@@ -8,9 +8,8 @@
from typing import List, Tuple, Type 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.plugin_system.base.config_types import ConfigField
from src.config.config import global_config
# 导入依赖的系统组件 # 导入依赖的系统组件
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -18,7 +17,6 @@ from src.common.logger import get_logger
# 导入API模块 - 标准Python包方式 # 导入API模块 - 标准Python包方式
from src.plugins.built_in.core_actions.no_reply import NoReplyAction 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.emoji import EmojiAction
from src.plugins.built_in.core_actions.reply import ReplyAction
logger = get_logger("core_actions") logger = get_logger("core_actions")
@@ -52,10 +50,9 @@ class CoreActionsPlugin(BasePlugin):
config_schema: dict = { config_schema: dict = {
"plugin": { "plugin": {
"enabled": ConfigField(type=bool, default=True, description="是否启用插件"), "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": { "components": {
"enable_reply": ConfigField(type=bool, default=True, description="是否启用回复动作"),
"enable_no_reply": ConfigField(type=bool, default=True, description="是否启用不回复动作"), "enable_no_reply": ConfigField(type=bool, default=True, description="是否启用不回复动作"),
"enable_emoji": 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]]: 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 = [] 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): if self.get_config("components.enable_no_reply", True):
components.append((NoReplyAction.get_action_info(), NoReplyAction)) components.append((NoReplyAction.get_action_info(), NoReplyAction))
if self.get_config("components.enable_emoji", True): if self.get_config("components.enable_emoji", True):
components.append((EmojiAction.get_action_info(), EmojiAction)) components.append((EmojiAction.get_action_info(), EmojiAction))
# components.append((DeepReplyAction.get_action_info(), DeepReplyAction))
return components return components

View File

@@ -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)}"

View File

@@ -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.chat.utils.utils import get_embedding
from src.common.database.database_model import Knowledges # Updated import from src.common.database.database_model import Knowledges # Updated import
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -77,7 +77,7 @@ class SearchKnowledgeTool(BaseTool):
Union[str, list]: 格式化的信息字符串或原始结果列表 Union[str, list]: 格式化的信息字符串或原始结果列表
""" """
if not query_embedding: if not query_embedding:
return "" if not return_raw else [] return [] if return_raw else ""
similar_items = [] similar_items = []
try: try:
@@ -115,10 +115,10 @@ class SearchKnowledgeTool(BaseTool):
except Exception as e: except Exception as e:
logger.error(f"从 Peewee 数据库获取知识信息失败: {str(e)}") logger.error(f"从 Peewee 数据库获取知识信息失败: {str(e)}")
return "" if not return_raw else [] return [] if return_raw else ""
if not results: if not results:
return "" if not return_raw else [] return [] if return_raw else ""
if return_raw: if return_raw:
# Peewee 模型实例不能直接序列化为 JSON如果需要原始模型调用者需要处理 # Peewee 模型实例不能直接序列化为 JSON如果需要原始模型调用者需要处理

View File

@@ -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.database import db
from src.common.logger import get_logger from src.common.logger import get_logger

View File

@@ -9,7 +9,7 @@
}, },
"license": "GPL-v3.0-or-later", "license": "GPL-v3.0-or-later",
"host_application": { "host_application": {
"min_version": "0.9.0" "min_version": "0.9.1"
}, },
"homepage_url": "https://github.com/MaiM-with-u/maibot", "homepage_url": "https://github.com/MaiM-with-u/maibot",
"repository_url": "https://github.com/MaiM-with-u/maibot", "repository_url": "https://github.com/MaiM-with-u/maibot",

View File

@@ -11,6 +11,7 @@ from src.plugin_system import (
component_manage_api, component_manage_api,
ComponentInfo, ComponentInfo,
ComponentType, ComponentType,
send_api,
) )
@@ -27,8 +28,15 @@ class ManagementCommand(BaseCommand):
or not self.message.message_info.user_info 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 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 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(" ") command_list = self.matched_groups["manage_command"].strip().split(" ")
if len(command_list) == 1: if len(command_list) == 1:
await self.show_help("all") await self.show_help("all")
@@ -42,7 +50,7 @@ class ManagementCommand(BaseCommand):
case "help": case "help":
await self.show_help("all") await self.show_help("all")
case _: case _:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if len(command_list) == 3: if len(command_list) == 3:
if command_list[1] == "plugin": if command_list[1] == "plugin":
@@ -56,7 +64,7 @@ class ManagementCommand(BaseCommand):
case "rescan": case "rescan":
await self._rescan_plugin_dirs() await self._rescan_plugin_dirs()
case _: case _:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
elif command_list[1] == "component": elif command_list[1] == "component":
if command_list[2] == "list": if command_list[2] == "list":
@@ -64,10 +72,10 @@ class ManagementCommand(BaseCommand):
elif command_list[2] == "help": elif command_list[2] == "help":
await self.show_help("component") await self.show_help("component")
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if len(command_list) == 4: if len(command_list) == 4:
if command_list[1] == "plugin": if command_list[1] == "plugin":
@@ -81,28 +89,28 @@ class ManagementCommand(BaseCommand):
case "add_dir": case "add_dir":
await self._add_dir(command_list[3]) await self._add_dir(command_list[3])
case _: case _:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
elif command_list[1] == "component": elif command_list[1] == "component":
if command_list[2] != "list": if command_list[2] != "list":
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if command_list[3] == "enabled": if command_list[3] == "enabled":
await self._list_enabled_components() await self._list_enabled_components()
elif command_list[3] == "disabled": elif command_list[3] == "disabled":
await self._list_disabled_components() await self._list_disabled_components()
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if len(command_list) == 5: if len(command_list) == 5:
if command_list[1] != "component": if command_list[1] != "component":
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if command_list[2] != "list": if command_list[2] != "list":
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if command_list[3] == "enabled": if command_list[3] == "enabled":
await self._list_enabled_components(target_type=command_list[4]) await self._list_enabled_components(target_type=command_list[4])
@@ -111,11 +119,11 @@ class ManagementCommand(BaseCommand):
elif command_list[3] == "type": elif command_list[3] == "type":
await self._list_registered_components_by_type(command_list[4]) await self._list_registered_components_by_type(command_list[4])
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if len(command_list) == 6: if len(command_list) == 6:
if command_list[1] != "component": if command_list[1] != "component":
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
if command_list[2] == "enable": if command_list[2] == "enable":
if command_list[3] == "global": if command_list[3] == "global":
@@ -123,7 +131,7 @@ class ManagementCommand(BaseCommand):
elif command_list[3] == "local": elif command_list[3] == "local":
await self._locally_enable_component(command_list[4], command_list[5]) await self._locally_enable_component(command_list[4], command_list[5])
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
elif command_list[2] == "disable": elif command_list[2] == "disable":
if command_list[3] == "global": if command_list[3] == "global":
@@ -131,10 +139,10 @@ class ManagementCommand(BaseCommand):
elif command_list[3] == "local": elif command_list[3] == "local":
await self._locally_disable_component(command_list[4], command_list[5]) await self._locally_disable_component(command_list[4], command_list[5])
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
else: else:
await self.send_text("插件管理命令不合法") await self._send_message("插件管理命令不合法")
return False, "命令不合法", True return False, "命令不合法", True
return True, "命令执行完成", True return True, "命令执行完成", True
@@ -180,51 +188,51 @@ class ManagementCommand(BaseCommand):
) )
case _: case _:
return return
await self.send_text(help_msg) await self._send_message(help_msg)
async def _list_loaded_plugins(self): async def _list_loaded_plugins(self):
plugins = plugin_manage_api.list_loaded_plugins() 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): async def _list_registered_plugins(self):
plugins = plugin_manage_api.list_registered_plugins() 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): async def _rescan_plugin_dirs(self):
plugin_manage_api.rescan_plugin_directory() plugin_manage_api.rescan_plugin_directory()
await self.send_text("插件目录重新扫描执行中") await self._send_message("插件目录重新扫描执行中")
async def _load_plugin(self, plugin_name: str): async def _load_plugin(self, plugin_name: str):
success, count = plugin_manage_api.load_plugin(plugin_name) success, count = plugin_manage_api.load_plugin(plugin_name)
if success: if success:
await self.send_text(f"插件加载成功: {plugin_name}") await self._send_message(f"插件加载成功: {plugin_name}")
else: else:
if count == 0: if count == 0:
await self.send_text(f"插件{plugin_name}为禁用状态") await self._send_message(f"插件{plugin_name}为禁用状态")
await self.send_text(f"插件加载失败: {plugin_name}") await self._send_message(f"插件加载失败: {plugin_name}")
async def _unload_plugin(self, plugin_name: str): async def _unload_plugin(self, plugin_name: str):
success = await plugin_manage_api.remove_plugin(plugin_name) success = await plugin_manage_api.remove_plugin(plugin_name)
if success: if success:
await self.send_text(f"插件卸载成功: {plugin_name}") await self._send_message(f"插件卸载成功: {plugin_name}")
else: else:
await self.send_text(f"插件卸载失败: {plugin_name}") await self._send_message(f"插件卸载失败: {plugin_name}")
async def _reload_plugin(self, plugin_name: str): async def _reload_plugin(self, plugin_name: str):
success = await plugin_manage_api.reload_plugin(plugin_name) success = await plugin_manage_api.reload_plugin(plugin_name)
if success: if success:
await self.send_text(f"插件重新加载成功: {plugin_name}") await self._send_message(f"插件重新加载成功: {plugin_name}")
else: else:
await self.send_text(f"插件重新加载失败: {plugin_name}") await self._send_message(f"插件重新加载失败: {plugin_name}")
async def _add_dir(self, dir_path: str): 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) success = plugin_manage_api.add_plugin_directory(dir_path)
await asyncio.sleep(0.5) # 防止乱序发送 await asyncio.sleep(0.5) # 防止乱序发送
if success: if success:
await self.send_text(f"插件目录添加成功: {dir_path}") await self._send_message(f"插件目录添加成功: {dir_path}")
else: else:
await self.send_text(f"插件目录添加失败: {dir_path}") await self._send_message(f"插件目录添加失败: {dir_path}")
def _fetch_all_registered_components(self) -> List[ComponentInfo]: def _fetch_all_registered_components(self) -> List[ComponentInfo]:
all_plugin_info = component_manage_api.get_all_plugin_info() all_plugin_info = component_manage_api.get_all_plugin_info()
@@ -255,29 +263,29 @@ class ManagementCommand(BaseCommand):
async def _list_all_registered_components(self): async def _list_all_registered_components(self):
components_info = self._fetch_all_registered_components() components_info = self._fetch_all_registered_components()
if not components_info: if not components_info:
await self.send_text("没有注册的组件") await self._send_message("没有注册的组件")
return return
all_components_str = ", ".join( all_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in components_info 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"): async def _list_enabled_components(self, target_type: str = "global"):
components_info = self._fetch_all_registered_components() components_info = self._fetch_all_registered_components()
if not components_info: if not components_info:
await self.send_text("没有注册的组件") await self._send_message("没有注册的组件")
return return
if target_type == "global": if target_type == "global":
enabled_components = [component for component in components_info if component.enabled] enabled_components = [component for component in components_info if component.enabled]
if not enabled_components: if not enabled_components:
await self.send_text("没有满足条件的已启用全局组件") await self._send_message("没有满足条件的已启用全局组件")
return return
enabled_components_str = ", ".join( enabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in enabled_components 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": elif target_type == "local":
locally_disabled_components = self._fetch_locally_disabled_components() locally_disabled_components = self._fetch_locally_disabled_components()
enabled_components = [ enabled_components = [
@@ -286,28 +294,28 @@ class ManagementCommand(BaseCommand):
if (component.name not in locally_disabled_components and component.enabled) if (component.name not in locally_disabled_components and component.enabled)
] ]
if not enabled_components: if not enabled_components:
await self.send_text("本聊天没有满足条件的已启用组件") await self._send_message("本聊天没有满足条件的已启用组件")
return return
enabled_components_str = ", ".join( enabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in enabled_components 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"): async def _list_disabled_components(self, target_type: str = "global"):
components_info = self._fetch_all_registered_components() components_info = self._fetch_all_registered_components()
if not components_info: if not components_info:
await self.send_text("没有注册的组件") await self._send_message("没有注册的组件")
return return
if target_type == "global": if target_type == "global":
disabled_components = [component for component in components_info if not component.enabled] disabled_components = [component for component in components_info if not component.enabled]
if not disabled_components: if not disabled_components:
await self.send_text("没有满足条件的已禁用全局组件") await self._send_message("没有满足条件的已禁用全局组件")
return return
disabled_components_str = ", ".join( disabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in disabled_components 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": elif target_type == "local":
locally_disabled_components = self._fetch_locally_disabled_components() locally_disabled_components = self._fetch_locally_disabled_components()
disabled_components = [ disabled_components = [
@@ -316,12 +324,12 @@ class ManagementCommand(BaseCommand):
if (component.name in locally_disabled_components or not component.enabled) if (component.name in locally_disabled_components or not component.enabled)
] ]
if not disabled_components: if not disabled_components:
await self.send_text("本聊天没有满足条件的已禁用组件") await self._send_message("本聊天没有满足条件的已禁用组件")
return return
disabled_components_str = ", ".join( disabled_components_str = ", ".join(
f"{component.name} ({component.component_type})" for component in disabled_components 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): async def _list_registered_components_by_type(self, target_type: str):
match target_type: match target_type:
@@ -332,18 +340,18 @@ class ManagementCommand(BaseCommand):
case "event_handler": case "event_handler":
component_type = ComponentType.EVENT_HANDLER component_type = ComponentType.EVENT_HANDLER
case _: case _:
await self.send_text(f"未知组件类型: {target_type}") await self._send_message(f"未知组件类型: {target_type}")
return return
components_info = component_manage_api.get_components_info_by_type(component_type) components_info = component_manage_api.get_components_info_by_type(component_type)
if not components_info: if not components_info:
await self.send_text(f"没有注册的 {target_type} 组件") await self._send_message(f"没有注册的 {target_type} 组件")
return return
components_str = ", ".join( components_str = ", ".join(
f"{name} ({component.component_type})" for name, component in components_info.items() 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): async def _globally_enable_component(self, component_name: str, component_type: str):
match component_type: match component_type:
@@ -354,12 +362,12 @@ class ManagementCommand(BaseCommand):
case "event_handler": case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER target_component_type = ComponentType.EVENT_HANDLER
case _: case _:
await self.send_text(f"未知组件类型: {component_type}") await self._send_message(f"未知组件类型: {component_type}")
return return
if component_manage_api.globally_enable_component(component_name, target_component_type): 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: 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): async def _globally_disable_component(self, component_name: str, component_type: str):
match component_type: match component_type:
@@ -370,13 +378,13 @@ class ManagementCommand(BaseCommand):
case "event_handler": case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER target_component_type = ComponentType.EVENT_HANDLER
case _: case _:
await self.send_text(f"未知组件类型: {component_type}") await self._send_message(f"未知组件类型: {component_type}")
return return
success = await component_manage_api.globally_disable_component(component_name, target_component_type) success = await component_manage_api.globally_disable_component(component_name, target_component_type)
if success: if success:
await self.send_text(f"全局禁用组件成功: {component_name}") await self._send_message(f"全局禁用组件成功: {component_name}")
else: 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): async def _locally_enable_component(self, component_name: str, component_type: str):
match component_type: match component_type:
@@ -387,16 +395,16 @@ class ManagementCommand(BaseCommand):
case "event_handler": case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER target_component_type = ComponentType.EVENT_HANDLER
case _: case _:
await self.send_text(f"未知组件类型: {component_type}") await self._send_message(f"未知组件类型: {component_type}")
return return
if component_manage_api.locally_enable_component( if component_manage_api.locally_enable_component(
component_name, component_name,
target_component_type, target_component_type,
self.message.chat_stream.stream_id, self.message.chat_stream.stream_id,
): ):
await self.send_text(f"本地启用组件成功: {component_name}") await self._send_message(f"本地启用组件成功: {component_name}")
else: 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): async def _locally_disable_component(self, component_name: str, component_type: str):
match component_type: match component_type:
@@ -407,34 +415,40 @@ class ManagementCommand(BaseCommand):
case "event_handler": case "event_handler":
target_component_type = ComponentType.EVENT_HANDLER target_component_type = ComponentType.EVENT_HANDLER
case _: case _:
await self.send_text(f"未知组件类型: {component_type}") await self._send_message(f"未知组件类型: {component_type}")
return return
if component_manage_api.locally_disable_component( if component_manage_api.locally_disable_component(
component_name, component_name,
target_component_type, target_component_type,
self.message.chat_stream.stream_id, self.message.chat_stream.stream_id,
): ):
await self.send_text(f"本地禁用组件成功: {component_name}") await self._send_message(f"本地禁用组件成功: {component_name}")
else: 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 @register_plugin
class PluginManagementPlugin(BasePlugin): class PluginManagementPlugin(BasePlugin):
plugin_name: str = "plugin_management_plugin" plugin_name: str = "plugin_management_plugin"
enable_plugin: bool = True enable_plugin: bool = False
dependencies: list[str] = [] dependencies: list[str] = []
python_dependencies: list[str] = [] python_dependencies: list[str] = []
config_file_name: str = "config.toml" config_file_name: str = "config.toml"
config_schema: dict = { config_schema: dict = {
"plugin": { "plugin": {
"enable": ConfigField(bool, default=True, description="是否启用插件"), "enabled": ConfigField(bool, default=False, description="是否启用插件"),
"permission": ConfigField(list, default=[], 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]]]: def get_plugin_components(self) -> List[Tuple[CommandInfo, Type[BaseCommand]]]:
components = [] components = []
if self.get_config("plugin.enable", True): if self.get_config("plugin.enabled", True):
components.append((ManagementCommand.get_command_info(), ManagementCommand)) components.append((ManagementCommand.get_command_info(), ManagementCommand))
return components return components

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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}

View File

@@ -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

View File

@@ -52,26 +52,26 @@ relation_frequency = 1 # 关系频率,麦麦构建关系的频率
[chat] #麦麦的聊天通用设置 [chat] #麦麦的聊天通用设置
focus_value = 1 focus_value = 1
# 麦麦的专注思考能力,越越容易专注,消耗token也越多 # 麦麦的专注思考能力,越越容易专注,可能消耗更多token
# 专注时能更好把握发言时机,能够进行持久的连续对话 # 专注时能更好把握发言时机,能够进行持久的连续对话
willing_amplifier = 1 # 麦麦回复意愿
max_context_size = 25 # 上下文长度 max_context_size = 25 # 上下文长度
thinking_timeout = 20 # 麦麦一次回复最长思考规划时间超过这个时间的思考会放弃往往是api反应太慢 thinking_timeout = 40 # 麦麦一次回复最长思考规划时间超过这个时间的思考会放弃往往是api反应太慢
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率 replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复 mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复
at_bot_inevitable_reply = true # @bot 或 提及bot 大概率回复 at_bot_inevitable_reply = true # @bot 或 提及bot 大概率回复
use_s4u_prompt_mode = true # 是否使用 s4u 对话构建模式,该模式会更好的把握当前对话对象的对话内容,但是对群聊整理理解能力较差(测试功能!!可能有未知问题!!)
talk_frequency = 1 # 麦麦回复频率,越高,麦麦回复越频繁 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 = ["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 值 # 注意:如果没有配置,则使用上面的默认 talk_frequency 值
@@ -105,11 +105,9 @@ ban_msgs_regex = [
[normal_chat] #普通聊天 [normal_chat] #普通聊天
willing_mode = "classical" # 回复意愿模式 —— 经典模式classicalmxp模式mxp自定义模式custom需要你自己实现 willing_mode = "classical" # 回复意愿模式 —— 经典模式classicalmxp模式mxp自定义模式custom需要你自己实现
response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数
[tool] [tool]
enable_in_normal_chat = false # 是否在普通聊天中启用工具 enable_tool = false # 是否在普通聊天中启用工具
enable_in_focus_chat = true # 是否在专注聊天中启用工具
[emoji] [emoji]
emoji_chance = 0.6 # 麦麦激活表情包动作的概率 emoji_chance = 0.6 # 麦麦激活表情包动作的概率