Skip to content

Commit c448118

Browse files
committed
feat:统一加载插件,区分内部插件和外部插件,提供示例命令发送插件
1 parent 450dad2 commit c448118

22 files changed

Lines changed: 2029 additions & 182 deletions

.gitignore

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,4 @@ src/plugins/test_plugin_pic/actions/pic_action_config.toml
309309
run_pet.bat
310310

311311
# 忽略 /src/plugins 但保留特定目录
312-
/src/plugins/*
313-
!/src/plugins/doubao_pic/
314-
!/src/plugins/mute_plugin/
315-
!/src/plugins/tts_plugin/
316-
!/src/plugins/vtb_action/
317-
!/src/plugins/__init__.py
318-
!/src/plugins/example_commands/
312+
/plugins/*

HEARFLOW_API_说明文档.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# HearflowAPI 使用说明
2+
3+
## 概述
4+
5+
HearflowAPI 是一个新增的插件API模块,提供了与心流和子心流相关的操作接口。通过这个API,插件开发者可以方便地获取和操作sub_hearflow实例。
6+
7+
## 主要功能
8+
9+
### 1. 获取子心流实例
10+
11+
#### `get_sub_hearflow_by_chat_id(chat_id: str) -> Optional[SubHeartflow]`
12+
根据chat_id获取指定的sub_hearflow实例(仅获取已存在的)。
13+
14+
**参数:**
15+
- `chat_id`: 聊天ID,与sub_hearflow的subheartflow_id相同
16+
17+
**返回值:**
18+
- `SubHeartflow`: sub_hearflow实例,如果不存在则返回None
19+
20+
**示例:**
21+
```python
22+
# 获取当前聊天的子心流实例
23+
current_subflow = await self.get_sub_hearflow_by_chat_id(self.observation.chat_id)
24+
if current_subflow:
25+
print(f"找到子心流: {current_subflow.chat_id}")
26+
else:
27+
print("子心流不存在")
28+
```
29+
30+
#### `get_or_create_sub_hearflow_by_chat_id(chat_id: str) -> Optional[SubHeartflow]`
31+
根据chat_id获取或创建sub_hearflow实例。
32+
33+
**参数:**
34+
- `chat_id`: 聊天ID
35+
36+
**返回值:**
37+
- `SubHeartflow`: sub_hearflow实例,创建失败时返回None
38+
39+
**示例:**
40+
```python
41+
# 获取或创建子心流实例
42+
subflow = await self.get_or_create_sub_hearflow_by_chat_id("some_chat_id")
43+
if subflow:
44+
print("成功获取或创建子心流")
45+
```
46+
47+
### 2. 获取子心流列表
48+
49+
#### `get_all_sub_hearflow_ids() -> List[str]`
50+
获取所有活跃子心流的ID列表。
51+
52+
**返回值:**
53+
- `List[str]`: 所有活跃子心流的ID列表
54+
55+
#### `get_all_sub_hearflows() -> List[SubHeartflow]`
56+
获取所有活跃的子心流实例。
57+
58+
**返回值:**
59+
- `List[SubHeartflow]`: 所有活跃的子心流实例列表
60+
61+
**示例:**
62+
```python
63+
# 获取所有活跃的子心流ID
64+
all_chat_ids = self.get_all_sub_hearflow_ids()
65+
print(f"共有 {len(all_chat_ids)} 个活跃的子心流")
66+
67+
# 获取所有活跃的子心流实例
68+
all_subflows = self.get_all_sub_hearflows()
69+
for subflow in all_subflows:
70+
print(f"子心流 {subflow.chat_id} 状态: {subflow.chat_state.chat_status.value}")
71+
```
72+
73+
### 3. 心流状态操作
74+
75+
#### `get_sub_hearflow_chat_state(chat_id: str) -> Optional[ChatState]`
76+
获取指定子心流的聊天状态。
77+
78+
**参数:**
79+
- `chat_id`: 聊天ID
80+
81+
**返回值:**
82+
- `ChatState`: 聊天状态,如果子心流不存在则返回None
83+
84+
#### `set_sub_hearflow_chat_state(chat_id: str, target_state: ChatState) -> bool`
85+
设置指定子心流的聊天状态。
86+
87+
**参数:**
88+
- `chat_id`: 聊天ID
89+
- `target_state`: 目标状态
90+
91+
**返回值:**
92+
- `bool`: 是否设置成功
93+
94+
**示例:**
95+
```python
96+
from src.chat.heart_flow.sub_heartflow import ChatState
97+
98+
# 获取当前状态
99+
current_state = await self.get_sub_hearflow_chat_state(self.observation.chat_id)
100+
print(f"当前状态: {current_state.value}")
101+
102+
# 设置状态
103+
success = await self.set_sub_hearflow_chat_state(self.observation.chat_id, ChatState.FOCUS)
104+
if success:
105+
print("状态设置成功")
106+
```
107+
108+
### 4. Replyer和Expressor操作
109+
110+
#### `get_sub_hearflow_replyer_and_expressor(chat_id: str) -> Tuple[Optional[Any], Optional[Any]]`
111+
根据chat_id获取指定子心流的replyer和expressor实例。
112+
113+
**参数:**
114+
- `chat_id`: 聊天ID
115+
116+
**返回值:**
117+
- `Tuple[Optional[Any], Optional[Any]]`: (replyer实例, expressor实例),如果子心流不存在或未处于FOCUSED状态,返回(None, None)
118+
119+
#### `get_sub_hearflow_replyer(chat_id: str) -> Optional[Any]`
120+
根据chat_id获取指定子心流的replyer实例。
121+
122+
**参数:**
123+
- `chat_id`: 聊天ID
124+
125+
**返回值:**
126+
- `Optional[Any]`: replyer实例,如果不存在则返回None
127+
128+
#### `get_sub_hearflow_expressor(chat_id: str) -> Optional[Any]`
129+
根据chat_id获取指定子心流的expressor实例。
130+
131+
**参数:**
132+
- `chat_id`: 聊天ID
133+
134+
**返回值:**
135+
- `Optional[Any]`: expressor实例,如果不存在则返回None
136+
137+
**示例:**
138+
```python
139+
# 获取replyer和expressor
140+
replyer, expressor = await self.get_sub_hearflow_replyer_and_expressor(self.observation.chat_id)
141+
if replyer and expressor:
142+
print(f"获取到replyer: {type(replyer).__name__}")
143+
print(f"获取到expressor: {type(expressor).__name__}")
144+
145+
# 检查属性
146+
print(f"Replyer聊天ID: {replyer.chat_id}")
147+
print(f"Expressor聊天ID: {expressor.chat_id}")
148+
print(f"是否群聊: {replyer.is_group_chat}")
149+
150+
# 单独获取replyer
151+
replyer = await self.get_sub_hearflow_replyer(self.observation.chat_id)
152+
if replyer:
153+
print("获取到replyer实例")
154+
155+
# 单独获取expressor
156+
expressor = await self.get_sub_hearflow_expressor(self.observation.chat_id)
157+
if expressor:
158+
print("获取到expressor实例")
159+
```
160+
161+
## 可用的聊天状态
162+
163+
```python
164+
from src.chat.heart_flow.sub_heartflow import ChatState
165+
166+
ChatState.FOCUS # 专注模式
167+
ChatState.NORMAL # 普通模式
168+
ChatState.ABSENT # 离开模式
169+
```
170+
171+
## 完整插件示例
172+
173+
```python
174+
from typing import Tuple
175+
from src.chat.actions.plugin_action import PluginAction, register_action
176+
from src.chat.heart_flow.sub_heartflow import ChatState
177+
178+
@register_action
179+
class MyHearflowPlugin(PluginAction):
180+
"""我的心流插件"""
181+
182+
activation_keywords = ["心流信息"]
183+
184+
async def process(self) -> Tuple[bool, str]:
185+
try:
186+
# 获取当前聊天的chat_id
187+
current_chat_id = self.observation.chat_id
188+
189+
# 获取子心流实例
190+
subflow = await self.get_sub_hearflow_by_chat_id(current_chat_id)
191+
if not subflow:
192+
return False, "未找到子心流实例"
193+
194+
# 获取状态信息
195+
current_state = await self.get_sub_hearflow_chat_state(current_chat_id)
196+
197+
# 构建回复
198+
response = f"心流信息:\n"
199+
response += f"聊天ID: {current_chat_id}\n"
200+
response += f"当前状态: {current_state.value}\n"
201+
response += f"是否群聊: {subflow.is_group_chat}\n"
202+
203+
return True, response
204+
205+
except Exception as e:
206+
return False, f"处理出错: {str(e)}"
207+
```
208+
209+
## 注意事项
210+
211+
1. **线程安全**: API内部已处理锁机制,确保线程安全。
212+
213+
2. **错误处理**: 所有API方法都包含异常处理,失败时会记录日志并返回安全的默认值。
214+
215+
3. **性能考虑**: `get_sub_hearflow_by_chat_id` 只获取已存在的实例,性能更好;`get_or_create_sub_hearflow_by_chat_id` 会在需要时创建新实例。
216+
217+
4. **状态管理**: 修改心流状态时请谨慎,确保不会影响系统的正常运行。
218+
219+
5. **日志记录**: 所有操作都会记录适当的日志,便于调试和监控。
220+
221+
6. **Replyer和Expressor可用性**:
222+
- 这些实例仅在子心流处于**FOCUSED状态**时可用
223+
- 如果子心流处于NORMAL或ABSENT状态,将返回None
224+
- 需要确保HeartFC实例存在且正常运行
225+
226+
7. **使用Replyer和Expressor时的注意事项**:
227+
- 直接调用这些实例的方法需要谨慎,可能影响系统正常运行
228+
- 建议主要用于监控、信息获取和状态检查
229+
- 不建议在插件中直接调用回复生成方法,这可能与系统的正常流程冲突
230+
231+
## 相关类型和模块
232+
233+
- `SubHeartflow`: 子心流实例类
234+
- `ChatState`: 聊天状态枚举
235+
- `DefaultReplyer`: 默认回复器类
236+
- `DefaultExpressor`: 默认表达器类
237+
- `HeartFChatting`: 专注聊天主类
238+
- `src.chat.heart_flow.heartflow`: 主心流模块
239+
- `src.chat.heart_flow.subheartflow_manager`: 子心流管理器
240+
- `src.chat.focus_chat.replyer.default_replyer`: 回复器模块
241+
- `src.chat.focus_chat.expressors.default_expressor`: 表达器模块

docs/plugin_loading_paths.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# 插件加载路径说明
2+
3+
## 概述
4+
5+
MaiBot-Core 现在支持从多个路径加载插件,为插件开发者提供更大的灵活性。
6+
7+
## 支持的插件路径
8+
9+
系统会按以下优先级顺序搜索和加载插件:
10+
11+
### 1. 项目根目录插件路径:`/plugins`
12+
- **路径**: 项目根目录下的 `plugins/` 文件夹
13+
- **优先级**: 最高
14+
- **用途**: 用户自定义插件、第三方插件
15+
- **特点**:
16+
- 与项目源码分离
17+
- 便于版本控制管理
18+
- 适合用户添加个人插件
19+
20+
### 2. 源码目录插件路径:`/src/plugins`
21+
- **路径**: src目录下的 `plugins/` 文件夹
22+
- **优先级**: 次高
23+
- **用途**: 系统内置插件、官方插件
24+
- **特点**:
25+
- 与项目源码集成
26+
- 适合系统级功能插件
27+
28+
## 插件结构支持
29+
30+
两个路径都支持相同的插件结构:
31+
32+
### 传统结构(推荐用于复杂插件)
33+
```
34+
plugins/my_plugin/
35+
├── __init__.py
36+
├── actions/
37+
│ ├── __init__.py
38+
│ └── my_action.py
39+
├── commands/
40+
│ ├── __init__.py
41+
│ └── my_command.py
42+
└── config.toml
43+
```
44+
45+
### 简化结构(推荐用于简单插件)
46+
```
47+
plugins/my_plugin/
48+
├── __init__.py
49+
├── my_action.py
50+
├── my_command.py
51+
└── config.toml
52+
```
53+
54+
## 文件命名约定
55+
56+
### 动作文件
57+
- `*_action.py`
58+
- `*_actions.py`
59+
- 包含 `action` 字样的文件名
60+
61+
### 命令文件
62+
- `*_command.py`
63+
- `*_commands.py`
64+
- 包含 `command` 字样的文件名
65+
66+
## 加载行为
67+
68+
1. **顺序加载**: 先加载 `/plugins`,再加载 `/src/plugins`
69+
2. **重名处理**: 如果两个路径中有同名插件,优先加载 `/plugins` 中的版本
70+
3. **错误隔离**: 单个插件加载失败不会影响其他插件的加载
71+
4. **详细日志**: 系统会记录每个插件的来源路径和加载状态
72+
73+
## 最佳实践
74+
75+
### 用户插件开发
76+
- 将自定义插件放在 `/plugins` 目录
77+
- 使用清晰的插件命名
78+
- 包含必要的 `__init__.py` 文件
79+
80+
### 系统插件开发
81+
- 将系统集成插件放在 `/src/plugins` 目录
82+
- 遵循项目代码规范
83+
- 完善的错误处理
84+
85+
### 版本控制
86+
-`/plugins` 目录添加到 `.gitignore`(如果是用户自定义插件)
87+
- 或者为插件创建独立的git仓库
88+
89+
## 示例插件
90+
91+
参考 `/plugins/example_root_plugin/` 中的示例插件,了解如何在根目录创建插件。
92+
93+
## 故障排除
94+
95+
### 常见问题
96+
97+
1. **插件未被加载**
98+
- 检查插件目录是否有 `__init__.py` 文件
99+
- 确认文件命名符合约定
100+
- 查看启动日志中的加载信息
101+
102+
2. **导入错误**
103+
- 确保插件依赖的模块已安装
104+
- 检查导入路径是否正确
105+
106+
3. **重复注册**
107+
- 检查是否有同名的动作或命令
108+
- 避免在不同路径放置相同功能的插件
109+
110+
### 调试日志
111+
112+
启动时查看日志输出:
113+
```
114+
[INFO] 正在从 plugins 加载插件...
115+
[INFO] 正在从 src/plugins 加载插件...
116+
[SUCCESS] 插件加载完成: 总计 X 个动作, Y 个命令
117+
[INFO] 插件加载详情:
118+
[INFO] example_plugin (来源: plugins): 1 动作, 1 命令
119+
```

src/chat/actions/plugin_action.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from src.chat.actions.plugin_api.config_api import ConfigAPI
1818
from src.chat.actions.plugin_api.utils_api import UtilsAPI
1919
from src.chat.actions.plugin_api.stream_api import StreamAPI
20+
from src.chat.actions.plugin_api.hearflow_api import HearflowAPI
2021

2122
# 以下为类型注解需要
2223
from src.chat.message_receive.chat_stream import ChatStream # noqa
@@ -27,7 +28,7 @@
2728
logger = get_logger("plugin_action")
2829

2930

30-
class PluginAction(BaseAction, MessageAPI, LLMAPI, DatabaseAPI, ConfigAPI, UtilsAPI, StreamAPI):
31+
class PluginAction(BaseAction, MessageAPI, LLMAPI, DatabaseAPI, ConfigAPI, UtilsAPI, StreamAPI, HearflowAPI):
3132
"""插件动作基类
3233
3334
封装了主程序内部依赖,提供简化的API接口给插件开发者

0 commit comments

Comments
 (0)