Merge origin/master into windpicker-adapter branch

This commit is contained in:
Windpicker-owo
2025-08-28 19:21:37 +08:00
65 changed files with 3922 additions and 1372 deletions

1
.gitignore vendored
View File

@@ -327,6 +327,7 @@ run_pet.bat
!/plugins/hello_world_plugin !/plugins/hello_world_plugin
!/plugins/take_picture_plugin !/plugins/take_picture_plugin
!/plugins/napcat_adapter_plugin !/plugins/napcat_adapter_plugin
!/plugins/echo_example
config.toml config.toml

View File

@@ -2,7 +2,7 @@
**版本V1.1** **版本V1.1**
**更新日期2025年8月26日** **更新日期2025年8月26日**
**生效日期2025年8月26日** **生效日期2025年8月26日**
**适用的MaiMbot-Pro-Max版本号:所有版本** **适用的MoFox_Bot版本号:所有版本**
**2025© MoFox_Bot项目团队** **2025© MoFox_Bot项目团队**

View File

@@ -11,8 +11,8 @@
[![Python](https://img.shields.io/badge/Python-3.10+-3776ab?logo=python&logoColor=white&style=for-the-badge)](https://www.python.org/) [![Python](https://img.shields.io/badge/Python-3.10+-3776ab?logo=python&logoColor=white&style=for-the-badge)](https://www.python.org/)
[![License](https://img.shields.io/badge/License-GPLv3-d73a49?logo=gnu&logoColor=white&style=for-the-badge)](https://github.com/MoFox-Studio/MoFox_Bot/blob/master/LICENSE) [![License](https://img.shields.io/badge/License-GPLv3-d73a49?logo=gnu&logoColor=white&style=for-the-badge)](https://github.com/MoFox-Studio/MoFox_Bot/blob/master/LICENSE)
[![Contributors](https://img.shields.io/badge/Contributors-Welcome-brightgreen?logo=github&logoColor=white&style=for-the-badge)](https://github.com/MoFox-Studio/MoFox_Bot/graphs/contributors) [![Contributors](https://img.shields.io/badge/Contributors-Welcome-brightgreen?logo=github&logoColor=white&style=for-the-badge)](https://github.com/MoFox-Studio/MoFox_Bot/graphs/contributors)
[![Stars](https://img.shields.io/github/stars/MaiBot-Plus/MaiMbot-Pro-Max?style=for-the-badge&logo=star&logoColor=white&color=yellow&label=Stars)](https://github.com/MoFox-Studio/MoFox_Bot/stargazers) [![Stars](https://img.shields.io/github/stars/MoFox-Studio/MoFox_Bot?style=for-the-badge&logo=star&logoColor=white&color=yellow&label=Stars)](https://github.com/MoFox-Studio/MoFox_Bot/stargazers)
[![Release](https://img.shields.io/github/v/release/MaiBot-Plus/MaiMbot-Pro-Max?style=for-the-badge&logo=github&logoColor=white&color=orange)](https://github.com/MaiBot-Plus/MaiMbot-Pro-Max/releases) [![Release](https://img.shields.io/github/v/release/MoFox-Studio/MoFox_Bot?style=for-the-badge&logo=github&logoColor=white&color=orange)](https://github.com/MoFox-Studio/MoFox_Bot/releases)
[![QQ](https://img.shields.io/badge/QQ-Bot-blue?style=for-the-badge&logo=tencentqq&logoColor=white)](https://github.com/NapNeko/NapCatQQ) [![QQ](https://img.shields.io/badge/QQ-Bot-blue?style=for-the-badge&logo=tencentqq&logoColor=white)](https://github.com/NapNeko/NapCatQQ)
</div> </div>
@@ -114,8 +114,8 @@
```bash ```bash
# 克隆项目 # 克隆项目
git clone https://github.com/MaiBot-Plus/MaiMbot-Pro-Max.git git clone https://github.com/MoFox-Studio/MoFox_Bot.git
cd MaiMbot-Pro-Max cd MoFox_Bot
# 安装依赖 # 安装依赖
pip install -r requirements.txt pip install -r requirements.txt

2
bot.py
View File

@@ -31,7 +31,6 @@ from src.manager.async_task_manager import async_task_manager # noqa
from src.config.config import global_config # noqa from src.config.config import global_config # noqa
from src.common.database.database import initialize_sql_database # noqa from src.common.database.database import initialize_sql_database # noqa
from src.common.database.sqlalchemy_models import initialize_database as init_db # noqa from src.common.database.sqlalchemy_models import initialize_database as init_db # noqa
logger = get_logger("main") logger = get_logger("main")
@@ -240,6 +239,7 @@ class MaiBotMain(BaseMain):
self.setup_timezone() self.setup_timezone()
self.check_and_confirm_eula() self.check_and_confirm_eula()
self.initialize_database() self.initialize_database()
return self.create_main_system() return self.create_main_system()

260
docs/PLUS_COMMAND_GUIDE.md Normal file
View File

@@ -0,0 +1,260 @@
# 增强命令系统使用指南
## 概述
增强命令系统是MaiBot插件系统的一个扩展让命令的定义和使用变得更加简单直观。你不再需要编写复杂的正则表达式只需要定义命令名、别名和参数处理逻辑即可。
## 核心特性
- **无需正则表达式**:只需定义命令名和别名
- **自动参数解析**:提供`CommandArgs`类处理参数
- **命令别名支持**:一个命令可以有多个别名
- **优先级控制**:支持命令优先级设置
- **聊天类型限制**:可限制命令在群聊或私聊中使用
- **消息拦截**:可选择是否拦截消息进行后续处理
## 快速开始
### 1. 创建基础命令
```python
from src.plugin_system import PlusCommand, CommandArgs, ChatType
from typing import Tuple, Optional
class EchoCommand(PlusCommand):
"""Echo命令示例"""
command_name = "echo"
command_description = "回显命令"
command_aliases = ["say", "repeat"] # 可选:命令别名
priority = 5 # 可选:优先级,数字越大优先级越高
chat_type_allow = ChatType.ALL # 可选ALL, GROUP, PRIVATE
intercept_message = True # 可选:是否拦截消息
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
"""执行命令"""
if args.is_empty():
await self.send_text("❓ 请提供要回显的内容\\n用法: /echo <内容>")
return True, "参数不足", True
content = args.get_raw()
await self.send_text(f"🔊 {content}")
return True, "Echo命令执行成功", True
```
### 2. 在插件中注册命令
```python
from src.plugin_system import BasePlugin, create_plus_command_adapter, register_plugin
@register_plugin
class MyPlugin(BasePlugin):
plugin_name = "my_plugin"
enable_plugin = True
dependencies = []
python_dependencies = []
config_file_name = "config.toml"
def get_plugin_components(self):
components = []
# 使用工厂函数创建适配器
echo_adapter = create_plus_command_adapter(EchoCommand)
components.append((EchoCommand.get_command_info(), echo_adapter))
return components
```
## CommandArgs 类详解
`CommandArgs`类提供了丰富的参数处理功能:
### 基础方法
```python
# 获取原始参数字符串
raw_text = args.get_raw()
# 获取解析后的参数列表(按空格分割,支持引号)
arg_list = args.get_args()
# 检查是否有参数
if args.is_empty():
# 没有参数的处理
# 获取参数数量
count = args.count()
```
### 获取特定参数
```python
# 获取第一个参数
first_arg = args.get_first("默认值")
# 获取指定索引的参数
second_arg = args.get_arg(1, "默认值")
# 获取从指定位置开始的剩余参数
remaining = args.get_remaining(1) # 从第2个参数开始
```
### 标志参数处理
```python
# 检查是否包含标志
if args.has_flag("--verbose"):
# 处理verbose模式
# 获取标志的值
output_file = args.get_flag_value("--output", "default.txt")
name = args.get_flag_value("--name", "Anonymous")
```
## 高级示例
### 1. 带子命令的复杂命令
```python
class TestCommand(PlusCommand):
command_name = "test"
command_description = "测试命令,展示参数解析功能"
command_aliases = ["t"]
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
if args.is_empty():
await self.send_text("用法: /test <子命令> [参数]")
return True, "显示帮助", True
subcommand = args.get_first().lower()
if subcommand == "args":
result = f"""
🔍 参数解析结果:
原始字符串: '{args.get_raw()}'
解析后参数: {args.get_args()}
参数数量: {args.count()}
第一个参数: '{args.get_first()}'
剩余参数: '{args.get_remaining()}'
"""
await self.send_text(result)
elif subcommand == "flags":
result = f"""
🏴 标志测试结果:
包含 --verbose: {args.has_flag('--verbose')}
包含 -v: {args.has_flag('-v')}
--output 的值: '{args.get_flag_value('--output', '未设置')}'
--name 的值: '{args.get_flag_value('--name', '未设置')}'
"""
await self.send_text(result)
else:
await self.send_text(f"❓ 未知的子命令: {subcommand}")
return True, "Test命令执行成功", True
```
### 2. 聊天类型限制示例
```python
class PrivateOnlyCommand(PlusCommand):
command_name = "private"
command_description = "仅私聊可用的命令"
chat_type_allow = ChatType.PRIVATE
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
await self.send_text("这是一个仅私聊可用的命令")
return True, "私聊命令执行", True
class GroupOnlyCommand(PlusCommand):
command_name = "group"
command_description = "仅群聊可用的命令"
chat_type_allow = ChatType.GROUP
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
await self.send_text("这是一个仅群聊可用的命令")
return True, "群聊命令执行", True
```
### 3. 配置驱动的命令
```python
class ConfigurableCommand(PlusCommand):
command_name = "config_cmd"
command_description = "可配置的命令"
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
# 从插件配置中获取设置
max_length = self.get_config("commands.max_length", 100)
enabled_features = self.get_config("commands.features", [])
if args.is_empty():
await self.send_text("请提供参数")
return True, "无参数", True
content = args.get_raw()
if len(content) > max_length:
await self.send_text(f"内容过长,最大允许 {max_length} 字符")
return True, "内容过长", True
# 根据配置决定功能
if "uppercase" in enabled_features:
content = content.upper()
await self.send_text(f"处理结果: {content}")
return True, "配置命令执行", True
```
## 支持的命令前缀
系统支持以下命令前缀(在`config/bot_config.toml`中配置):
- `/` - 斜杠(默认)
- `!` - 感叹号
- `.` - 点号
- `#` - 井号
例如对于echo命令以下调用都是有效的
- `/echo Hello`
- `!echo Hello`
- `.echo Hello`
- `#echo Hello`
## 返回值说明
`execute`方法需要返回一个三元组:
```python
return (执行成功标志, 可选消息, 是否拦截后续处理)
```
- **执行成功标志** (bool): True表示命令执行成功False表示失败
- **可选消息** (Optional[str]): 用于日志记录的消息
- **是否拦截后续处理** (bool): True表示拦截消息不进行后续处理
## 最佳实践
1. **命令命名**:使用简短、直观的命令名
2. **别名设置**:为常用命令提供简短别名
3. **参数验证**:总是检查参数的有效性
4. **错误处理**:提供清晰的错误提示和使用说明
5. **配置支持**:重要设置应该可配置
6. **聊天类型**:根据命令功能选择合适的聊天类型限制
## 完整示例
完整的插件示例请参考 `plugins/echo_example/plugin.py` 文件。
## 与传统BaseCommand的区别
| 特性 | PlusCommand | BaseCommand |
|------|-------------|-------------|
| 正则表达式 | 自动生成 | 手动编写 |
| 参数解析 | CommandArgs类 | 手动处理 |
| 别名支持 | 内置支持 | 需要在正则中处理 |
| 代码复杂度 | 简单 | 复杂 |
| 学习曲线 | 平缓 | 陡峭 |
增强命令系统让插件开发变得更加简单和高效,特别适合新手开发者快速上手。

View File

@@ -68,15 +68,15 @@ class ExampleAction(BaseAction):
Action采用**两层决策机制**来优化性能和决策质量: Action采用**两层决策机制**来优化性能和决策质量:
> 设计目的在加载许多插件的时候降低LLM决策压力避免让麦麦在过多的选项中纠结。 > 设计目的在加载许多插件的时候降低LLM决策压力避免让MoFox-Bot在过多的选项中纠结。
**第一层激活控制Activation Control** **第一层激活控制Activation Control**
激活决定麦麦是否 **“知道”** 这个Action的存在即这个Action是否进入决策候选池。不被激活的Action麦麦永远不会选择。 激活决定MoFox-Bot是否 **“知道”** 这个Action的存在即这个Action是否进入决策候选池。不被激活的ActionMoFox-Bot永远不会选择。
**第二层使用决策Usage Decision** **第二层使用决策Usage Decision**
在Action被激活后使用条件决定麦麦什么时候会 **“选择”** 使用这个Action。 在Action被激活后使用条件决定MoFox-Bot什么时候会 **“选择”** 使用这个Action。
### 决策参数详解 🔧 ### 决策参数详解 🔧
@@ -84,8 +84,8 @@ Action采用**两层决策机制**来优化性能和决策质量:
| 激活类型 | 说明 | 使用场景 | | 激活类型 | 说明 | 使用场景 |
| ----------- | ---------------------------------------- | ---------------------- | | ----------- | ---------------------------------------- | ---------------------- |
| [`NEVER`](#never-激活) | 从不激活Action对麦麦不可见 | 临时禁用某个Action | | [`NEVER`](#never-激活) | 从不激活Action对MoFox-Bot不可见 | 临时禁用某个Action |
| [`ALWAYS`](#always-激活) | 永远激活Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 | | [`ALWAYS`](#always-激活) | 永远激活Action总是在MoFox-Bot的候选池中 | 核心功能,如回复、不回复 |
| [`LLM_JUDGE`](#llm_judge-激活) | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 | | [`LLM_JUDGE`](#llm_judge-激活) | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 |
| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 | | `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 |
| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 | | `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 |
@@ -184,13 +184,13 @@ class GreetingAction(BaseAction):
#### 第二层:使用决策 #### 第二层:使用决策
**在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action** **在Action被激活后使用条件决定MoFox-Bot什么时候会"选择"使用这个Action**
这一层由以下因素综合决定: 这一层由以下因素综合决定:
- `action_require`使用场景描述帮助LLM判断何时选择 - `action_require`使用场景描述帮助LLM判断何时选择
- `action_parameters`所需参数影响Action的可执行性 - `action_parameters`所需参数影响Action的可执行性
- 当前聊天上下文和麦麦的决策逻辑 - 当前聊天上下文和MoFox-Bot的决策逻辑
--- ---
@@ -214,11 +214,11 @@ class EmojiAction(BaseAction):
1. **第一层激活判断** 1. **第一层激活判断**
- 使用随机数进行决策,当`random.random() < self.random_activation_probability`时,麦麦才"知道"可以使用这个Action - 使用随机数进行决策,当`random.random() < self.random_activation_probability`时,MoFox-Bot才"知道"可以使用这个Action
2. **第二层使用决策** 2. **第二层使用决策**
- 即使Action被激活麦麦还会根据 `action_require` 中的条件判断是否真正选择使用 - 即使Action被激活MoFox-Bot还会根据 `action_require` 中的条件判断是否真正选择使用
- 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求,麦麦可能不会选择这个Action - 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求,MoFox-Bot可能不会选择这个Action
--- ---

View File

@@ -0,0 +1,124 @@
# 自动化工具缓存系统使用指南
为了提升性能并减少不必要的重复计算或API调用MMC内置了一套强大且易于使用的自动化工具缓存系统。该系统同时支持传统的**精确缓存**和先进的**语义缓存**。工具开发者无需编写任何手动缓存逻辑,只需在工具类中设置几个属性,即可轻松启用和配置缓存行为。
## 核心概念
- **精确缓存 (KV Cache)**: 当一个工具被调用时,系统会根据工具名称和所有参数生成一个唯一的键。只有当**下一次调用的工具名和所有参数与之前完全一致**时,才会命中缓存。
- **语义缓存 (Vector Cache)**: 它不要求参数完全一致,而是理解参数的**语义和意图**。例如,`"查询深圳今天的天气"``"今天深圳天气怎么样"` 这两个不同的查询,在语义上是高度相似的。如果启用了语义缓存,第二个查询就能成功命中由第一个查询产生的缓存结果。
## 如何为你的工具启用缓存
为你的工具(必须继承自 `BaseTool`)启用缓存非常简单,只需在你的工具类定义中添加以下一个或多个属性即可:
### 1. `enable_cache: bool`
这是启用缓存的总开关。
- **类型**: `bool`
- **默认值**: `False`
- **作用**: 设置为 `True` 即可为该工具启用缓存功能。如果为 `False`,后续的所有缓存配置都将无效。
**示例**:
```python
class MyAwesomeTool(BaseTool):
# ... 其他定义 ...
enable_cache: bool = True
```
### 2. `cache_ttl: int`
设置缓存的生存时间Time-To-Live
- **类型**: `int`
- **单位**: 秒
- **默认值**: `3600` (1小时)
- **作用**: 定义缓存条目在被视为过期之前可以存活多长时间。
**示例**:
```python
class MyLongTermCacheTool(BaseTool):
# ... 其他定义 ...
enable_cache: bool = True
cache_ttl: int = 86400 # 缓存24小时
```
### 3. `semantic_cache_query_key: Optional[str]`
启用语义缓存的关键。
- **类型**: `Optional[str]`
- **默认值**: `None`
- **作用**:
- 将此属性的值设置为你工具的某个**参数的名称**(字符串)。
- 自动化缓存系统在工作时,会提取该参数的值,将其转换为向量,并进行语义相似度搜索。
- 如果该值为 `None`,则此工具**仅使用精确缓存**。
**示例**:
```python
class WebSurfingTool(BaseTool):
name: str = "web_search"
parameters = [
("query", ToolParamType.STRING, "要搜索的关键词或问题。", True, None),
# ... 其他参数 ...
]
# --- 缓存配置 ---
enable_cache: bool = True
cache_ttl: int = 7200 # 缓存2小时
semantic_cache_query_key: str = "query" # <-- 关键!
```
在上面的例子中,`web_search` 工具的 `"query"` 参数值(例如,用户输入的搜索词)将被用于语义缓存搜索。
## 完整示例
假设我们有一个调用外部API来获取股票价格的工具。由于股价在短时间内相对稳定且查询意图可能相似如 "苹果股价" vs "AAPL股价"),因此非常适合使用缓存。
```python
# in your_plugin/tools/stock_checker.py
from src.plugin_system import BaseTool, ToolParamType
class StockCheckerTool(BaseTool):
"""
一个用于查询股票价格的工具。
"""
name: str = "get_stock_price"
description: str = "获取指定公司或股票代码的最新价格。"
available_for_llm: bool = True
parameters = [
("symbol", ToolParamType.STRING, "公司名称或股票代码 (e.g., 'AAPL', '苹果')", True, None),
]
# --- 缓存配置 ---
# 1. 开启缓存
enable_cache: bool = True
# 2. 股价信息缓存10分钟
cache_ttl: int = 600
# 3. 使用 "symbol" 参数进行语义搜索
semantic_cache_query_key: str = "symbol"
# --------------------
async def execute(self, function_args: dict[str, Any]) -> dict[str, Any]:
symbol = function_args.get("symbol")
# ... 这里是你调用外部API获取股票价格的逻辑 ...
# price = await some_stock_api.get_price(symbol)
price = 123.45 # 示例价格
return {
"type": "stock_price_result",
"content": f"{symbol} 的当前价格是 ${price}"
}
```
通过以上简单的三行配置,`StockCheckerTool` 现在就拥有了强大的自动化缓存能力:
- 当用户查询 `"苹果"` 时,工具会执行并缓存结果。
- 在接下来的10分钟内如果再次查询 `"苹果"`,将直接从精确缓存返回结果。
- 更智能的是,如果另一个用户查询 `"AAPL"`,语义缓存系统会识别出 `"AAPL"``"苹果"` 在语义上高度相关大概率也会直接返回缓存的结果而无需再次调用API。
---
现在你可以专注于实现工具的核心逻辑把缓存的复杂性交给MMC的自动化系统来处理。

View File

@@ -0,0 +1,128 @@
# 统一向量数据库服务使用指南
本文档旨在说明如何在 `mmc` 项目中使用新集成的统一向量数据库服务。该服务提供了一个标准化的接口,用于与底层向量数据库(当前为 ChromaDB进行交互同时确保了代码的解耦和未来的可扩展性。
## 核心设计理念
1. **统一入口**: 所有对向量数据库的操作都应通过全局单例 `vector_db_service` 进行。
2. **抽象接口**: 服务遵循 `VectorDBBase` 抽象基类定义的接口,未来可以轻松替换为其他向量数据库(如 Milvus, FAISS而无需修改业务代码。
3. **单例模式**: 整个应用程序共享一个数据库客户端实例,避免了资源浪费和管理混乱。
4. **数据隔离**: 使用不同的 `collection` 名称来隔离不同业务模块(如语义缓存、瞬时记忆)的数据。在 `collection` 内部,使用 `metadata` 字段(如 `chat_id`)来隔离不同用户或会话的数据。
## 如何使用
### 1. 导入服务
在任何需要使用向量数据库的文件中,只需导入全局服务实例:
```python
from src.common.vector_db import vector_db_service
```
### 2. 主要操作
`vector_db_service` 对象提供了所有你需要的方法,这些方法都定义在 `VectorDBBase` 中。
#### a. 获取或创建集合 (Collection)
在操作数据之前,你需要先指定一个集合。如果集合不存在,它将被自动创建。
```python
# 为语义缓存创建一个集合
vector_db_service.get_or_create_collection(name="semantic_cache")
# 为瞬时记忆创建一个集合
vector_db_service.get_or_create_collection(
name="instant_memory",
metadata={"hnsw:space": "cosine"} # 可以传入特定于实现的参数
)
```
#### b. 添加数据
使用 `add` 方法向指定集合中添加向量、文档和元数据。
```python
collection_name = "instant_memory"
chat_id = "user_123"
message_id = "msg_abc"
embedding_vector = [0.1, 0.2, 0.3, ...] # 你的 embedding 向量
content = "你好,这是一个测试消息"
vector_db_service.add(
collection_name=collection_name,
embeddings=[embedding_vector],
documents=[content],
metadatas=[{
"chat_id": chat_id,
"timestamp": 1678886400.0,
"sender": "user"
}],
ids=[message_id]
)
```
#### c. 查询数据
使用 `query` 方法来查找相似的向量。你可以使用 `where` 子句来过滤元数据。
```python
query_vector = [0.11, 0.22, 0.33, ...] # 用于查询的向量
collection_name = "instant_memory"
chat_id_to_query = "user_123"
results = vector_db_service.query(
collection_name=collection_name,
query_embeddings=[query_vector],
n_results=5, # 返回最相似的5个结果
where={"chat_id": chat_id_to_query} # **重要**: 使用 where 来隔离不同聊天的数据
)
# results 的结构:
# {
# 'ids': [['msg_abc']],
# 'distances': [[0.0123]],
# 'metadatas': [[{'chat_id': 'user_123', ...}]],
# 'embeddings': None,
# 'documents': [['你好,这是一个测试消息']]
# }
print(results)
```
#### d. 删除数据
你可以根据 `id``where` 条件来删除数据。
```python
# 根据 ID 删除
vector_db_service.delete(
collection_name="instant_memory",
ids=["msg_abc"]
)
# 根据 where 条件删除 (例如,删除某个用户的所有记忆)
vector_db_service.delete(
collection_name="instant_memory",
where={"chat_id": "user_123"}
)
```
#### e. 获取集合数量
使用 `count` 方法获取一个集合中的条目总数。
```python
count = vector_db_service.count(collection_name="semantic_cache")
print(f"语义缓存集合中有 {count} 条数据。")
```
**注意**: `count` 方法目前返回整个集合的条目数,不会根据 `where` 条件进行过滤。
### 3. 代码位置
- **抽象基类**: [`mmc/src/common/vector_db/base.py`](mmc/src/common/vector_db/base.py)
- **ChromaDB 实现**: [`mmc/src/common/vector_db/chromadb_impl.py`](mmc/src/common/vector_db/chromadb_impl.py)
- **服务入口**: [`mmc/src/common/vector_db/__init__.py`](mmc/src/common/vector_db/__init__.py)
---
这份完整的文档应该能帮助您和团队的其他成员正确地使用新的向量数据库服务。如果您有任何其他问题,请随时提出。

View File

@@ -0,0 +1,53 @@
{
"manifest_version": 1,
"format_version": "1.0.0",
"name": "Echo 示例插件",
"description": "展示增强命令系统的Echo命令示例插件",
"version": "1.0.0",
"author": {
"name": "MoFox"
},
"license": "MIT",
"keywords": ["echo", "example", "command"],
"categories": ["utility", "example"],
"host_application": {
"name": "MaiBot",
"min_version": "0.10.0"
},
"entry_points": {
"main": "plugin.py"
},
"plugin_info": {
"is_built_in": false,
"plugin_type": "example",
"components": [
{
"type": "command",
"name": "echo",
"description": "回显命令,支持别名 say, repeat"
},
{
"type": "command",
"name": "hello",
"description": "问候命令,支持别名 hi, greet"
},
{
"type": "command",
"name": "info",
"description": "显示插件信息,支持别名 about"
},
{
"type": "command",
"name": "test",
"description": "测试命令,展示参数解析功能"
}
],
"features": [
"增强命令系统示例",
"无需正则表达式的命令定义",
"命令别名支持",
"参数解析功能",
"聊天类型限制"
]
}
}

View File

@@ -0,0 +1,203 @@
"""
Echo 示例插件
展示增强命令系统的使用方法
"""
from typing import List, Tuple, Type, Optional, Union
from src.plugin_system import (
BasePlugin,
PlusCommand,
CommandArgs,
PlusCommandInfo,
ConfigField,
ChatType,
register_plugin,
)
from src.plugin_system.base.component_types import PythonDependency
class EchoCommand(PlusCommand):
"""Echo命令示例"""
command_name = "echo"
command_description = "回显命令"
command_aliases = ["say", "repeat"]
priority = 5
chat_type_allow = ChatType.ALL
intercept_message = True
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
"""执行echo命令"""
if args.is_empty():
await self.send_text("❓ 请提供要回显的内容\n用法: /echo <内容>")
return True, "参数不足", True
content = args.get_raw()
# 检查内容长度限制
max_length = self.get_config("commands.max_content_length", 500)
if len(content) > max_length:
await self.send_text(f"❌ 内容过长,最大允许 {max_length} 字符")
return True, "内容过长", True
await self.send_text(f"🔊 {content}")
return True, "Echo命令执行成功", True
class HelloCommand(PlusCommand):
"""Hello命令示例"""
command_name = "hello"
command_description = "问候命令"
command_aliases = ["hi", "greet"]
priority = 3
chat_type_allow = ChatType.ALL
intercept_message = True
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
"""执行hello命令"""
if args.is_empty():
await self.send_text("👋 Hello! 很高兴见到你!")
else:
name = args.get_first()
await self.send_text(f"👋 Hello, {name}! 很高兴见到你!")
return True, "Hello命令执行成功", True
class InfoCommand(PlusCommand):
"""信息命令示例"""
command_name = "info"
command_description = "显示插件信息"
command_aliases = ["about"]
priority = 1
chat_type_allow = ChatType.ALL
intercept_message = True
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
"""执行info命令"""
info_text = (
"📋 Echo 示例插件信息\n"
"版本: 1.0.0\n"
"作者: MaiBot Team\n"
"描述: 展示增强命令系统的使用方法\n\n"
"🎯 可用命令:\n"
"• /echo|/say|/repeat <内容> - 回显内容\n"
"• /hello|/hi|/greet [名字] - 问候\n"
"• /info|/about - 显示此信息\n"
"• /test <子命令> [参数] - 测试各种功能"
)
await self.send_text(info_text)
return True, "Info命令执行成功", True
class TestCommand(PlusCommand):
"""测试命令示例,展示参数解析功能"""
command_name = "test"
command_description = "测试命令,展示参数解析功能"
command_aliases = ["t"]
priority = 2
chat_type_allow = ChatType.ALL
intercept_message = True
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
"""执行test命令"""
if args.is_empty():
help_text = (
"🧪 测试命令帮助\n"
"用法: /test <子命令> [参数]\n\n"
"可用子命令:\n"
"• args - 显示参数解析结果\n"
"• flags - 测试标志参数\n"
"• count - 计算参数数量\n"
"• join - 连接所有参数"
)
await self.send_text(help_text)
return True, "显示帮助", True
subcommand = args.get_first().lower()
if subcommand == "args":
result = (
f"🔍 参数解析结果:\n"
f"原始字符串: '{args.get_raw()}'\n"
f"解析后参数: {args.get_args()}\n"
f"参数数量: {args.count()}\n"
f"第一个参数: '{args.get_first()}'\n"
f"剩余参数: '{args.get_remaining()}'"
)
await self.send_text(result)
elif subcommand == "flags":
result = (
f"🏴 标志测试结果:\n"
f"包含 --verbose: {args.has_flag('--verbose')}\n"
f"包含 -v: {args.has_flag('-v')}\n"
f"--output 的值: '{args.get_flag_value('--output', '未设置')}'\n"
f"--name 的值: '{args.get_flag_value('--name', '未设置')}'"
)
await self.send_text(result)
elif subcommand == "count":
count = args.count() - 1 # 减去子命令本身
await self.send_text(f"📊 除子命令外的参数数量: {count}")
elif subcommand == "join":
remaining = args.get_remaining()
if remaining:
await self.send_text(f"🔗 连接结果: {remaining}")
else:
await self.send_text("❌ 没有可连接的参数")
else:
await self.send_text(f"❓ 未知的子命令: {subcommand}")
return True, "Test命令执行成功", True
@register_plugin
class EchoExamplePlugin(BasePlugin):
"""Echo 示例插件"""
plugin_name: str = "echo_example_plugin"
enable_plugin: bool = True
dependencies: List[str] = []
python_dependencies: List[Union[str, "PythonDependency"]] = []
config_file_name: str = "config.toml"
config_schema = {
"plugin": {
"enabled": ConfigField(bool, default=True, description="是否启用插件"),
"config_version": ConfigField(str, default="1.0.0", description="配置文件版本"),
},
"commands": {
"echo_enabled": ConfigField(bool, default=True, description="是否启用 Echo 命令"),
"cooldown": ConfigField(int, default=0, description="命令冷却时间(秒)"),
"max_content_length": ConfigField(int, default=500, description="最大回显内容长度"),
},
}
config_section_descriptions = {
"plugin": "插件基本配置",
"commands": "命令相关配置",
}
def get_plugin_components(self) -> List[Tuple[PlusCommandInfo, Type]]:
"""获取插件组件"""
components = []
if self.get_config("plugin.enabled", True):
# 添加所有命令直接使用PlusCommand类
if self.get_config("commands.echo_enabled", True):
components.append((EchoCommand.get_plus_command_info(), EchoCommand))
components.append((HelloCommand.get_plus_command_info(), HelloCommand))
components.append((InfoCommand.get_plus_command_info(), InfoCommand))
components.append((TestCommand.get_plus_command_info(), TestCommand))
return components

View File

@@ -100,7 +100,7 @@ class CycleProcessor:
from src.plugin_system.core.event_manager import event_manager from src.plugin_system.core.event_manager import event_manager
from src.plugin_system.base.component_types import EventType from src.plugin_system.base.component_types import EventType
# 触发 ON_PLAN 事件 # 触发 ON_PLAN 事件
result = await event_manager.trigger_event(EventType.ON_PLAN, stream_id=self.context.stream_id) result = await event_manager.trigger_event(EventType.ON_PLAN, plugin_name="SYSTEM", stream_id=self.context.stream_id)
if result and not result.all_continue_process(): if result and not result.all_continue_process():
return return
@@ -131,6 +131,11 @@ class CycleProcessor:
if ENABLE_S4U: if ENABLE_S4U:
await stop_typing() await stop_typing()
# 在一轮动作执行完毕后,增加睡眠压力
if self.context.energy_manager and global_config.wakeup_system.enable_insomnia_system:
if action_type not in ["no_reply", "no_action"]:
self.context.energy_manager.increase_sleep_pressure()
return True return True

View File

@@ -5,6 +5,7 @@ from src.common.logger import get_logger
from src.config.config import global_config from src.config.config import global_config
from src.plugin_system.base.component_types import ChatMode from src.plugin_system.base.component_types import ChatMode
from .hfc_context import HfcContext from .hfc_context import HfcContext
from src.schedule.schedule_manager import schedule_manager
logger = get_logger("hfc") logger = get_logger("hfc")
@@ -77,7 +78,7 @@ class EnergyManager:
async def _energy_loop(self): async def _energy_loop(self):
""" """
能量管理的主循环 能量与睡眠压力管理的主循环
功能说明: 功能说明:
- 每10秒执行一次能量更新 - 每10秒执行一次能量更新
@@ -92,24 +93,35 @@ class EnergyManager:
if not self.context.chat_stream: if not self.context.chat_stream:
continue continue
is_group_chat = self.context.chat_stream.group_info is not None # 判断当前是否为睡眠时间
if is_group_chat and global_config.chat.group_chat_mode != "auto": is_sleeping = schedule_manager.is_sleeping(self.context.wakeup_manager)
if global_config.chat.group_chat_mode == "focus":
self.context.loop_mode = ChatMode.FOCUS
self.context.energy_value = 35
elif global_config.chat.group_chat_mode == "normal":
self.context.loop_mode = ChatMode.NORMAL
self.context.energy_value = 15
continue
if self.context.loop_mode == ChatMode.NORMAL: if is_sleeping:
self.context.energy_value -= 0.3 # 睡眠中:减少睡眠压力
self.context.energy_value = max(self.context.energy_value, 0.3) decay_per_10s = global_config.wakeup_system.sleep_pressure_decay_rate / 6
if self.context.loop_mode == ChatMode.FOCUS: self.context.sleep_pressure -= decay_per_10s
self.context.energy_value -= 0.6 self.context.sleep_pressure = max(self.context.sleep_pressure, 0)
self.context.energy_value = max(self.context.energy_value, 0.3) self._log_sleep_pressure_change("睡眠压力释放")
else:
self._log_energy_change("能量衰减") # 清醒时:处理能量衰减
is_group_chat = self.context.chat_stream.group_info is not None
if is_group_chat and global_config.chat.group_chat_mode != "auto":
if global_config.chat.group_chat_mode == "focus":
self.context.loop_mode = ChatMode.FOCUS
self.context.energy_value = 35
elif global_config.chat.group_chat_mode == "normal":
self.context.loop_mode = ChatMode.NORMAL
self.context.energy_value = 15
continue
if self.context.loop_mode == ChatMode.NORMAL:
self.context.energy_value -= 0.3
self.context.energy_value = max(self.context.energy_value, 0.3)
if self.context.loop_mode == ChatMode.FOCUS:
self.context.energy_value -= 0.6
self.context.energy_value = max(self.context.energy_value, 0.3)
self._log_energy_change("能量值衰减")
def _should_log_energy(self) -> bool: def _should_log_energy(self) -> bool:
""" """
@@ -129,6 +141,15 @@ class EnergyManager:
return True return True
return False return False
def increase_sleep_pressure(self):
"""
在执行动作后增加睡眠压力
"""
increment = global_config.wakeup_system.sleep_pressure_increment
self.context.sleep_pressure += increment
self.context.sleep_pressure = min(self.context.sleep_pressure, 100.0) # 设置一个100的上限
self._log_sleep_pressure_change("执行动作,睡眠压力累积")
def _log_energy_change(self, action: str, reason: str = ""): def _log_energy_change(self, action: str, reason: str = ""):
""" """
记录能量变化日志 记录能量变化日志
@@ -151,4 +172,14 @@ class EnergyManager:
log_message = f"{self.context.log_prefix} {action},当前能量值:{self.context.energy_value:.1f}" log_message = f"{self.context.log_prefix} {action},当前能量值:{self.context.energy_value:.1f}"
if reason: if reason:
log_message = f"{self.context.log_prefix} {action}{reason},当前能量值:{self.context.energy_value:.1f}" log_message = f"{self.context.log_prefix} {action}{reason},当前能量值:{self.context.energy_value:.1f}"
logger.debug(log_message) logger.debug(log_message)
def _log_sleep_pressure_change(self, action: str):
"""
记录睡眠压力变化日志
"""
# 使用与能量日志相同的频率控制
if self._should_log_energy():
logger.info(f"{self.context.log_prefix} {action},当前睡眠压力:{self.context.sleep_pressure:.1f}")
else:
logger.debug(f"{self.context.log_prefix} {action},当前睡眠压力:{self.context.sleep_pressure:.1f}")

View File

@@ -8,8 +8,9 @@ from src.config.config import global_config
from src.person_info.relationship_builder_manager import relationship_builder_manager from src.person_info.relationship_builder_manager import relationship_builder_manager
from src.chat.express.expression_learner import expression_learner_manager from src.chat.express.expression_learner import expression_learner_manager
from src.plugin_system.base.component_types import ChatMode from src.plugin_system.base.component_types import ChatMode
from src.manager.schedule_manager import schedule_manager from src.schedule.schedule_manager import schedule_manager
from src.plugin_system.apis import message_api from src.plugin_system.apis import message_api
from src.mood.mood_manager import mood_manager
from .hfc_context import HfcContext from .hfc_context import HfcContext
from .energy_manager import EnergyManager from .energy_manager import EnergyManager
@@ -48,6 +49,7 @@ class HeartFChatting:
# 将唤醒度管理器设置到上下文中 # 将唤醒度管理器设置到上下文中
self.context.wakeup_manager = self.wakeup_manager self.context.wakeup_manager = self.wakeup_manager
self.context.energy_manager = self.energy_manager
self._loop_task: Optional[asyncio.Task] = None self._loop_task: Optional[asyncio.Task] = None
@@ -196,8 +198,28 @@ class HeartFChatting:
- NORMAL模式检查进入FOCUS模式的条件并通过normal_mode_handler处理消息 - NORMAL模式检查进入FOCUS模式的条件并通过normal_mode_handler处理消息
""" """
is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager) is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager)
# 核心修复:在睡眠模式下获取消息时,不过滤命令消息,以确保@消息能被接收 # --- 失眠状态管理 ---
if self.context.is_in_insomnia and time.time() > self.context.insomnia_end_time:
# 失眠状态结束
self.context.is_in_insomnia = False
await self.proactive_thinker.trigger_goodnight_thinking()
if is_sleeping and not self.context.was_sleeping:
# 刚刚进入睡眠状态,进行一次入睡检查
if self.wakeup_manager and self.wakeup_manager.check_for_insomnia():
# 触发失眠
self.context.is_in_insomnia = True
duration = global_config.wakeup_system.insomnia_duration_minutes * 60
self.context.insomnia_end_time = time.time() + duration
# 判断失眠原因并触发思考
reason = "random"
if self.context.sleep_pressure < global_config.wakeup_system.sleep_pressure_threshold:
reason = "low_pressure"
await self.proactive_thinker.trigger_insomnia_thinking(reason)
# 核心修复:在睡眠模式(包括失眠)下获取消息时,不过滤命令消息,以确保@消息能被接收
filter_command_flag = not is_sleeping filter_command_flag = not is_sleeping
recent_messages = message_api.get_messages_by_time_in_chat( recent_messages = message_api.get_messages_by_time_in_chat(
@@ -220,9 +242,16 @@ class HeartFChatting:
# 处理唤醒度逻辑 # 处理唤醒度逻辑
if is_sleeping: if is_sleeping:
self._handle_wakeup_messages(recent_messages) self._handle_wakeup_messages(recent_messages)
# 如果仍在睡眠状态,跳过正常处理但仍返回有新消息 # 再次检查睡眠状态,因为_handle_wakeup_messages可能会触发唤醒
if schedule_manager.is_sleeping(self.wakeup_manager): current_is_sleeping = schedule_manager.is_sleeping(self.wakeup_manager)
if not self.context.is_in_insomnia and current_is_sleeping:
# 仍然在睡眠,跳过本轮的消息处理
return has_new_messages return has_new_messages
else:
# 从睡眠中被唤醒,需要继续处理本轮消息
logger.info(f"{self.context.log_prefix} 从睡眠中被唤醒,将处理积压的消息。")
self.context.last_wakeup_time = time.time()
# 根据聊天模式处理新消息 # 根据聊天模式处理新消息
if self.context.loop_mode == ChatMode.FOCUS: if self.context.loop_mode == ChatMode.FOCUS:
@@ -239,6 +268,21 @@ class HeartFChatting:
self._check_focus_exit() self._check_focus_exit()
elif self.context.loop_mode == ChatMode.NORMAL: elif self.context.loop_mode == ChatMode.NORMAL:
self._check_focus_entry(0) # 传入0表示无新消息 self._check_focus_entry(0) # 传入0表示无新消息
# 更新上一帧的睡眠状态
self.context.was_sleeping = is_sleeping
# --- 重新入睡逻辑 ---
# 如果被吵醒了,并且在一定时间内没有新消息,则尝试重新入睡
if schedule_manager._is_woken_up and not has_new_messages:
re_sleep_delay = global_config.wakeup_system.re_sleep_delay_minutes * 60
# 使用 last_message_time 来判断空闲时间
if time.time() - self.context.last_message_time > re_sleep_delay:
logger.info(f"{self.context.log_prefix} 已被唤醒且超过 {re_sleep_delay / 60} 分钟无新消息,尝试重新入睡。")
schedule_manager.reset_wakeup_state()
# 保存HFC上下文状态
self.context.save_context_state()
return has_new_messages return has_new_messages

View File

@@ -1,6 +1,8 @@
from typing import List, Optional, TYPE_CHECKING from typing import List, Optional, TYPE_CHECKING
import time import time
from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager from src.chat.message_receive.chat_stream import ChatStream, get_chat_manager
from src.common.logger import get_logger
from src.manager.local_store_manager import local_storage
from src.person_info.relationship_builder_manager import RelationshipBuilder from src.person_info.relationship_builder_manager import RelationshipBuilder
from src.chat.express.expression_learner import ExpressionLearner from src.chat.express.expression_learner import ExpressionLearner
from src.plugin_system.base.component_types import ChatMode from src.plugin_system.base.component_types import ChatMode
@@ -9,6 +11,7 @@ from src.chat.chat_loop.hfc_utils import CycleDetail
if TYPE_CHECKING: if TYPE_CHECKING:
from .wakeup_manager import WakeUpManager from .wakeup_manager import WakeUpManager
from .energy_manager import EnergyManager
class HfcContext: class HfcContext:
def __init__(self, chat_id: str): def __init__(self, chat_id: str):
@@ -40,6 +43,13 @@ class HfcContext:
self.loop_mode = ChatMode.NORMAL self.loop_mode = ChatMode.NORMAL
self.energy_value = 5.0 self.energy_value = 5.0
self.sleep_pressure = 0.0
self.was_sleeping = False # 用于检测睡眠状态的切换
# 失眠状态
self.is_in_insomnia: bool = False
self.insomnia_end_time: float = 0.0
self.last_wakeup_time: float = 0.0 # 被吵醒的时间
self.last_message_time = time.time() self.last_message_time = time.time()
self.last_read_time = time.time() - 10 self.last_read_time = time.time() - 10
@@ -53,4 +63,38 @@ class HfcContext:
self.current_cycle_detail: Optional[CycleDetail] = None self.current_cycle_detail: Optional[CycleDetail] = None
# 唤醒度管理器 - 延迟初始化以避免循环导入 # 唤醒度管理器 - 延迟初始化以避免循环导入
self.wakeup_manager: Optional['WakeUpManager'] = None self.wakeup_manager: Optional['WakeUpManager'] = None
self.energy_manager: Optional['EnergyManager'] = None
self._load_context_state()
def _get_storage_key(self) -> str:
"""获取当前聊天流的本地存储键"""
return f"hfc_context_state_{self.stream_id}"
def _load_context_state(self):
"""从本地存储加载状态"""
state = local_storage[self._get_storage_key()]
if state and isinstance(state, dict):
self.energy_value = state.get("energy_value", 5.0)
self.sleep_pressure = state.get("sleep_pressure", 0.0)
self.is_in_insomnia = state.get("is_in_insomnia", False)
self.insomnia_end_time = state.get("insomnia_end_time", 0.0)
logger = get_logger("hfc_context")
logger.info(f"{self.log_prefix} 成功从本地存储加载HFC上下文状态: {state}")
else:
logger = get_logger("hfc_context")
logger.info(f"{self.log_prefix} 未找到本地HFC上下文状态将使用默认值初始化。")
def save_context_state(self):
"""将当前状态保存到本地存储"""
state = {
"energy_value": self.energy_value,
"sleep_pressure": self.sleep_pressure,
"is_in_insomnia": self.is_in_insomnia,
"insomnia_end_time": self.insomnia_end_time,
"last_wakeup_time": self.last_wakeup_time,
}
local_storage[self._get_storage_key()] = state
logger = get_logger("hfc_context")
logger.debug(f"{self.log_prefix} 已将HFC上下文状态保存到本地存储: {state}")

View File

@@ -279,3 +279,56 @@ class ProactiveThinker:
except Exception as e: except Exception as e:
logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}") logger.error(f"{self.context.log_prefix} 主动思考执行异常: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
async def trigger_insomnia_thinking(self, reason: str):
"""
由外部事件(如失眠)触发的一次性主动思考
Args:
reason: 触发的原因 (e.g., "low_pressure", "random")
"""
logger.info(f"{self.context.log_prefix} 因“{reason}”触发失眠,开始深夜思考...")
# 1. 根据原因修改情绪
try:
from src.mood.mood_manager import mood_manager
mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id)
if reason == "low_pressure":
mood_obj.mood_state = "精力过剩,毫无睡意"
elif reason == "random":
mood_obj.mood_state = "深夜emo胡思乱想"
mood_obj.last_change_time = time.time() # 更新时间戳以允许后续的情绪回归
logger.info(f"{self.context.log_prefix} 因失眠,情绪状态被强制更新为: {mood_obj.mood_state}")
except Exception as e:
logger.error(f"{self.context.log_prefix} 设置失眠情绪时出错: {e}")
# 2. 直接执行主动思考逻辑
try:
# 传入一个象征性的silence_duration因为它在这里不重要
await self._execute_proactive_thinking(silence_duration=1)
except Exception as e:
logger.error(f"{self.context.log_prefix} 失眠思考执行出错: {e}")
logger.error(traceback.format_exc())
async def trigger_goodnight_thinking(self):
"""
在失眠状态结束后,触发一次准备睡觉的主动思考
"""
logger.info(f"{self.context.log_prefix} 失眠状态结束,准备睡觉,触发告别思考...")
# 1. 设置一个准备睡觉的特定情绪
try:
from src.mood.mood_manager import mood_manager
mood_obj = mood_manager.get_mood_by_chat_id(self.context.stream_id)
mood_obj.mood_state = "有点困了,准备睡觉了"
mood_obj.last_change_time = time.time()
logger.info(f"{self.context.log_prefix} 情绪状态更新为: {mood_obj.mood_state}")
except Exception as e:
logger.error(f"{self.context.log_prefix} 设置睡前情绪时出错: {e}")
# 2. 直接执行主动思考逻辑
try:
await self._execute_proactive_thinking(silence_duration=1)
except Exception as e:
logger.error(f"{self.context.log_prefix} 睡前告别思考执行出错: {e}")
logger.error(traceback.format_exc())

View File

@@ -3,6 +3,7 @@ import time
from typing import Optional from typing import Optional
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.manager.local_store_manager import local_storage
from .hfc_context import HfcContext from .hfc_context import HfcContext
logger = get_logger("wakeup") logger = get_logger("wakeup")
@@ -39,6 +40,40 @@ class WakeUpManager:
self.angry_duration = wakeup_config.angry_duration self.angry_duration = wakeup_config.angry_duration
self.enabled = wakeup_config.enable self.enabled = wakeup_config.enable
self.angry_prompt = wakeup_config.angry_prompt self.angry_prompt = wakeup_config.angry_prompt
# 失眠系统参数
self.insomnia_enabled = wakeup_config.enable_insomnia_system
self.sleep_pressure_threshold = wakeup_config.sleep_pressure_threshold
self.deep_sleep_threshold = wakeup_config.deep_sleep_threshold
self.insomnia_chance_low_pressure = wakeup_config.insomnia_chance_low_pressure
self.insomnia_chance_normal_pressure = wakeup_config.insomnia_chance_normal_pressure
self._load_wakeup_state()
def _get_storage_key(self) -> str:
"""获取当前聊天流的本地存储键"""
return f"wakeup_manager_state_{self.context.stream_id}"
def _load_wakeup_state(self):
"""从本地存储加载状态"""
state = local_storage[self._get_storage_key()]
if state and isinstance(state, dict):
self.wakeup_value = state.get("wakeup_value", 0.0)
self.is_angry = state.get("is_angry", False)
self.angry_start_time = state.get("angry_start_time", 0.0)
logger.info(f"{self.context.log_prefix} 成功从本地存储加载唤醒状态: {state}")
else:
logger.info(f"{self.context.log_prefix} 未找到本地唤醒状态,将使用默认值初始化。")
def _save_wakeup_state(self):
"""将当前状态保存到本地存储"""
state = {
"wakeup_value": self.wakeup_value,
"is_angry": self.is_angry,
"angry_start_time": self.angry_start_time,
}
local_storage[self._get_storage_key()] = state
logger.debug(f"{self.context.log_prefix} 已将唤醒状态保存到本地存储: {state}")
async def start(self): async def start(self):
"""启动唤醒度管理器""" """启动唤醒度管理器"""
@@ -82,6 +117,7 @@ class WakeUpManager:
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
mood_manager.clear_angry_from_wakeup(self.context.stream_id) mood_manager.clear_angry_from_wakeup(self.context.stream_id)
logger.info(f"{self.context.log_prefix} 愤怒状态结束,恢复正常") logger.info(f"{self.context.log_prefix} 愤怒状态结束,恢复正常")
self._save_wakeup_state()
# 唤醒度自然衰减 # 唤醒度自然衰减
if self.wakeup_value > 0: if self.wakeup_value > 0:
@@ -89,6 +125,7 @@ class WakeUpManager:
self.wakeup_value = max(0, self.wakeup_value - self.decay_rate) self.wakeup_value = max(0, self.wakeup_value - self.decay_rate)
if old_value != self.wakeup_value: if old_value != self.wakeup_value:
logger.debug(f"{self.context.log_prefix} 唤醒度衰减: {old_value:.1f} -> {self.wakeup_value:.1f}") logger.debug(f"{self.context.log_prefix} 唤醒度衰减: {old_value:.1f} -> {self.wakeup_value:.1f}")
self._save_wakeup_state()
def add_wakeup_value(self, is_private_chat: bool, is_mentioned: bool = False) -> bool: def add_wakeup_value(self, is_private_chat: bool, is_mentioned: bool = False) -> bool:
""" """
@@ -105,10 +142,9 @@ class WakeUpManager:
if not self.enabled: if not self.enabled:
return False return False
from src.manager.schedule_manager import schedule_manager # 只有在休眠且非失眠状态下才累积唤醒度
from src.schedule.schedule_manager import schedule_manager
# 只有在休眠状态下才累积唤醒度 if not schedule_manager.is_sleeping() or self.context.is_in_insomnia:
if not schedule_manager.is_sleeping():
return False return False
old_value = self.wakeup_value old_value = self.wakeup_value
@@ -136,7 +172,8 @@ class WakeUpManager:
if self.wakeup_value >= self.wakeup_threshold: if self.wakeup_value >= self.wakeup_threshold:
self._trigger_wakeup() self._trigger_wakeup()
return True return True
self._save_wakeup_state()
return False return False
def _trigger_wakeup(self): def _trigger_wakeup(self):
@@ -145,10 +182,16 @@ class WakeUpManager:
self.angry_start_time = time.time() self.angry_start_time = time.time()
self.wakeup_value = 0.0 # 重置唤醒度 self.wakeup_value = 0.0 # 重置唤醒度
self._save_wakeup_state()
# 通知情绪管理系统进入愤怒状态 # 通知情绪管理系统进入愤怒状态
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
mood_manager.set_angry_from_wakeup(self.context.stream_id) mood_manager.set_angry_from_wakeup(self.context.stream_id)
# 通知日程管理器重置睡眠状态
from src.schedule.schedule_manager import schedule_manager
schedule_manager.reset_sleep_state_after_wakeup()
logger.info(f"{self.context.log_prefix} 唤醒度达到阈值({self.wakeup_threshold}),被吵醒进入愤怒状态!") logger.info(f"{self.context.log_prefix} 唤醒度达到阈值({self.wakeup_threshold}),被吵醒进入愤怒状态!")
def get_angry_prompt_addition(self) -> str: def get_angry_prompt_addition(self) -> str:
@@ -177,4 +220,39 @@ class WakeUpManager:
"wakeup_threshold": self.wakeup_threshold, "wakeup_threshold": self.wakeup_threshold,
"is_angry": self.is_angry, "is_angry": self.is_angry,
"angry_remaining_time": max(0, self.angry_duration - (time.time() - self.angry_start_time)) if self.is_angry else 0 "angry_remaining_time": max(0, self.angry_duration - (time.time() - self.angry_start_time)) if self.is_angry else 0
} }
def check_for_insomnia(self) -> bool:
"""
在尝试入睡时检查是否会失眠
Returns:
bool: 如果失眠则返回 True否则返回 False
"""
if not self.insomnia_enabled:
return False
import random
pressure = self.context.sleep_pressure
# 压力过高,深度睡眠,极难失眠
if pressure > self.deep_sleep_threshold:
return False
# 根据睡眠压力决定失眠概率
from src.schedule.schedule_manager import schedule_manager
if pressure < self.sleep_pressure_threshold:
# 压力不足型失眠
if schedule_manager._is_in_voluntary_delay:
logger.debug(f"{self.context.log_prefix} 处于主动延迟睡眠期间,跳过压力不足型失眠判断。")
elif random.random() < self.insomnia_chance_low_pressure:
logger.info(f"{self.context.log_prefix} 睡眠压力不足 ({pressure:.1f}),触发失眠!")
return True
else:
# 压力正常,随机失眠
if random.random() < self.insomnia_chance_normal_pressure:
logger.info(f"{self.context.log_prefix} 睡眠压力正常 ({pressure:.1f}),触发随机失眠!")
return True
return False

View File

@@ -326,7 +326,7 @@ async def clear_temp_emoji() -> None:
if os.path.exists(need_clear): if os.path.exists(need_clear):
files = os.listdir(need_clear) files = os.listdir(need_clear)
# 如果文件数超过100就全部删除 # 如果文件数超过100就全部删除
if len(files) > 100: if len(files) > 1000:
for filename in files: for filename in files:
file_path = os.path.join(need_clear, filename) file_path = os.path.join(need_clear, filename)
if os.path.isfile(file_path): if os.path.isfile(file_path):

View File

@@ -114,16 +114,27 @@ class ExpressionSelector:
return None return None
def get_related_chat_ids(self, chat_id: str) -> List[str]: def get_related_chat_ids(self, chat_id: str) -> List[str]:
"""根据expression_groups配置获取与当前chat_id相关的所有chat_id包括自身""" """根据expression.rules配置获取与当前chat_id相关的所有chat_id包括自身"""
groups = global_config.expression.expression_groups rules = global_config.expression.rules
for group in groups: current_group = None
group_chat_ids = []
for stream_config_str in group: # 找到当前chat_id所在的组
if chat_id_candidate := self._parse_stream_config_to_chat_id(stream_config_str): for rule in rules:
group_chat_ids.append(chat_id_candidate) if rule.chat_stream_id and self._parse_stream_config_to_chat_id(rule.chat_stream_id) == chat_id:
if chat_id in group_chat_ids: current_group = rule.group
return group_chat_ids break
return [chat_id]
if not current_group:
return [chat_id]
# 找出同一组的所有chat_id
related_chat_ids = []
for rule in rules:
if rule.group == current_group and rule.chat_stream_id:
if chat_id_candidate := self._parse_stream_config_to_chat_id(rule.chat_stream_id):
related_chat_ids.append(chat_id_candidate)
return related_chat_ids if related_chat_ids else [chat_id]
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

View File

@@ -108,7 +108,7 @@ class InstantMemory:
memory_id=memory_item.memory_id, memory_id=memory_item.memory_id,
chat_id=memory_item.chat_id, chat_id=memory_item.chat_id,
memory_text=memory_item.memory_text, memory_text=memory_item.memory_text,
keywords=json.dumps(memory_item.keywords, ensure_ascii=False), keywords=orjson.dumps(memory_item.keywords).decode('utf-8'),
create_time=memory_item.create_time, create_time=memory_item.create_time,
last_view_time=memory_item.last_view_time, last_view_time=memory_item.last_view_time,
) )
@@ -172,8 +172,8 @@ class InstantMemory:
# 对每条记忆 # 对每条记忆
mem_keywords_str = mem.keywords or "[]" mem_keywords_str = mem.keywords or "[]"
try: try:
mem_keywords = json.loads(mem_keywords_str) mem_keywords = orjson.loads(mem_keywords_str)
except json.JSONDecodeError: except orjson.JSONDecodeError:
mem_keywords = [] mem_keywords = []
# logger.info(f"mem_keywords: {mem_keywords}") # logger.info(f"mem_keywords: {mem_keywords}")
# logger.info(f"keywords_list: {keywords_list}") # logger.info(f"keywords_list: {keywords_list}")

View File

@@ -4,10 +4,9 @@ from typing import List, Dict, Any
from dataclasses import dataclass from dataclasses import dataclass
import threading import threading
import chromadb
from chromadb.config import Settings
from src.common.logger import get_logger from src.common.logger import get_logger
from src.chat.utils.utils import get_embedding from src.chat.utils.utils import get_embedding
from src.common.vector_db import vector_db_service
logger = get_logger("vector_instant_memory_v2") logger = get_logger("vector_instant_memory_v2")
@@ -45,10 +44,7 @@ class VectorInstantMemoryV2:
self.chat_id = chat_id self.chat_id = chat_id
self.retention_hours = retention_hours self.retention_hours = retention_hours
self.cleanup_interval = cleanup_interval self.cleanup_interval = cleanup_interval
self.collection_name = "instant_memory"
# ChromaDB相关
self.client = None
self.collection = None
# 清理任务相关 # 清理任务相关
self.cleanup_task = None self.cleanup_task = None
@@ -61,22 +57,16 @@ class VectorInstantMemoryV2:
logger.info(f"向量瞬时记忆系统V2初始化完成: {chat_id} (保留{retention_hours}小时)") logger.info(f"向量瞬时记忆系统V2初始化完成: {chat_id} (保留{retention_hours}小时)")
def _init_chroma(self): def _init_chroma(self):
"""初始化ChromaDB连接""" """使用全局服务初始化向量数据库集合"""
try: try:
db_path = f"./data/memory_vectors/{self.chat_id}" # 现在我们只获取集合,而不是创建新的客户端
self.client = chromadb.PersistentClient( vector_db_service.get_or_create_collection(
path=db_path, name=self.collection_name,
settings=Settings(anonymized_telemetry=False)
)
self.collection = self.client.get_or_create_collection(
name="chat_messages",
metadata={"hnsw:space": "cosine"} metadata={"hnsw:space": "cosine"}
) )
logger.info(f"向量记忆数据库初始化成功: {db_path}") logger.info(f"向量记忆集合 '{self.collection_name}' 已准备就绪")
except Exception as e: except Exception as e:
logger.error(f"ChromaDB初始化失败: {e}") logger.error(f"获取向量记忆集合失败: {e}")
self.client = None
self.collection = None
def _start_cleanup_task(self): def _start_cleanup_task(self):
"""启动定时清理任务""" """启动定时清理任务"""
@@ -95,36 +85,40 @@ class VectorInstantMemoryV2:
def _cleanup_expired_messages(self): def _cleanup_expired_messages(self):
"""清理过期的聊天记录""" """清理过期的聊天记录"""
if not self.collection:
return
try: try:
# 计算过期时间戳
expire_time = time.time() - (self.retention_hours * 3600) expire_time = time.time() - (self.retention_hours * 3600)
# 查询所有记录 # 采用 get -> filter -> delete 模式,避免复杂的 where 查询
all_results = self.collection.get( # 1. 获取当前 chat_id 的所有文档
results = vector_db_service.get(
collection_name=self.collection_name,
where={"chat_id": self.chat_id}, where={"chat_id": self.chat_id},
include=["metadatas"] include=["metadatas"]
) )
# 找出过期的记录ID if not results or not results.get('ids'):
logger.info(f"chat_id '{self.chat_id}' 没有找到任何记录,无需清理")
return
# 2. 在内存中过滤出过期的文档
expired_ids = [] expired_ids = []
metadatas = all_results.get("metadatas") or [] metadatas = results.get('metadatas', [])
ids = all_results.get("ids") or [] ids = results.get('ids', [])
for i, metadata in enumerate(metadatas): for i, metadata in enumerate(metadatas):
if metadata and isinstance(metadata, dict): if metadata and metadata.get('timestamp', float('inf')) < expire_time:
timestamp = metadata.get("timestamp", 0) expired_ids.append(ids[i])
if isinstance(timestamp, (int, float)) and timestamp < expire_time:
if i < len(ids): # 3. 如果有过期文档,根据 ID 进行删除
expired_ids.append(ids[i])
# 批量删除过期记录
if expired_ids: if expired_ids:
self.collection.delete(ids=expired_ids) vector_db_service.delete(
logger.info(f"清理了 {len(expired_ids)} 条过期聊天记录") collection_name=self.collection_name,
ids=expired_ids
)
logger.info(f"为 chat_id '{self.chat_id}' 清理了 {len(expired_ids)} 条过期记录")
else:
logger.info(f"chat_id '{self.chat_id}' 没有需要清理的过期记录")
except Exception as e: except Exception as e:
logger.error(f"清理过期记录失败: {e}") logger.error(f"清理过期记录失败: {e}")
@@ -139,7 +133,7 @@ class VectorInstantMemoryV2:
Returns: Returns:
bool: 是否存储成功 bool: 是否存储成功
""" """
if not self.collection or not content.strip(): if not content.strip():
return False return False
try: try:
@@ -149,10 +143,8 @@ class VectorInstantMemoryV2:
logger.warning(f"消息向量生成失败: {content[:50]}...") logger.warning(f"消息向量生成失败: {content[:50]}...")
return False return False
# 生成唯一消息ID
message_id = f"{self.chat_id}_{int(time.time() * 1000)}_{hash(content) % 10000}" message_id = f"{self.chat_id}_{int(time.time() * 1000)}_{hash(content) % 10000}"
# 创建消息对象
message = ChatMessage( message = ChatMessage(
message_id=message_id, message_id=message_id,
chat_id=self.chat_id, chat_id=self.chat_id,
@@ -161,8 +153,9 @@ class VectorInstantMemoryV2:
sender=sender sender=sender
) )
# 存储到ChromaDB # 使用新的服务存储
self.collection.add( vector_db_service.add(
collection_name=self.collection_name,
embeddings=[message_vector], embeddings=[message_vector],
documents=[content], documents=[content],
metadatas=[{ metadatas=[{
@@ -194,23 +187,23 @@ class VectorInstantMemoryV2:
Returns: Returns:
List[Dict]: 相似消息列表包含content、similarity、timestamp等信息 List[Dict]: 相似消息列表包含content、similarity、timestamp等信息
""" """
if not self.collection or not query.strip(): if not query.strip():
return [] return []
try: try:
# 生成查询向量
query_vector = await get_embedding(query) query_vector = await get_embedding(query)
if not query_vector: if not query_vector:
return [] return []
# 向量相似度搜索 # 使用新的服务进行查询
results = self.collection.query( results = vector_db_service.query(
collection_name=self.collection_name,
query_embeddings=[query_vector], query_embeddings=[query_vector],
n_results=top_k, n_results=top_k,
where={"chat_id": self.chat_id} where={"chat_id": self.chat_id}
) )
if not results['documents'] or not results['documents'][0]: if not results.get('documents') or not results['documents'][0]:
return [] return []
# 处理搜索结果 # 处理搜索结果
@@ -311,15 +304,18 @@ class VectorInstantMemoryV2:
"cleanup_interval": self.cleanup_interval, "cleanup_interval": self.cleanup_interval,
"system_status": "running" if self.is_running else "stopped", "system_status": "running" if self.is_running else "stopped",
"total_messages": 0, "total_messages": 0,
"db_status": "connected" if self.collection else "disconnected" "db_status": "connected"
} }
if self.collection: try:
try: # 注意count() 现在没有 chat_id 过滤,返回的是整个集合的数量
result = self.collection.count() # 若要精确计数,需要 get(where={"chat_id": ...}) 然后 len(results['ids'])
stats["total_messages"] = result # 这里为了简化,暂时显示集合总数
except Exception: result = vector_db_service.count(collection_name=self.collection_name)
stats["total_messages"] = "查询失败" stats["total_messages"] = result
except Exception:
stats["total_messages"] = "查询失败"
stats["db_status"] = "disconnected"
return stats return stats

View File

@@ -98,6 +98,118 @@ class ChatBot:
self._started = True self._started = True
async def _process_plus_commands(self, message: MessageRecv):
"""独立处理PlusCommand系统"""
try:
text = message.processed_plain_text
# 获取配置的命令前缀
from src.config.config import global_config
prefixes = global_config.command.command_prefixes
# 检查是否以任何前缀开头
matched_prefix = None
for prefix in prefixes:
if text.startswith(prefix):
matched_prefix = prefix
break
if not matched_prefix:
return False, None, True # 不是命令,继续处理
# 移除前缀
command_part = text[len(matched_prefix):].strip()
# 分离命令名和参数
parts = command_part.split(None, 1)
if not parts:
return False, None, True # 没有命令名,继续处理
command_word = parts[0].lower()
args_text = parts[1] if len(parts) > 1 else ""
# 查找匹配的PlusCommand
plus_command_registry = component_registry.get_plus_command_registry()
matching_commands = []
for plus_command_name, plus_command_class in plus_command_registry.items():
plus_command_info = component_registry.get_registered_plus_command_info(plus_command_name)
if not plus_command_info:
continue
# 检查命令名是否匹配(命令名和别名)
all_commands = [plus_command_name.lower()] + [alias.lower() for alias in plus_command_info.command_aliases]
if command_word in all_commands:
matching_commands.append((plus_command_class, plus_command_info, plus_command_name))
if not matching_commands:
return False, None, True # 没有找到匹配的PlusCommand继续处理
# 如果有多个匹配,按优先级排序
if len(matching_commands) > 1:
matching_commands.sort(key=lambda x: x[1].priority, reverse=True)
logger.warning(f"文本 '{text}' 匹配到多个PlusCommand: {[cmd[2] for cmd in matching_commands]},使用优先级最高的")
plus_command_class, plus_command_info, plus_command_name = matching_commands[0]
# 检查命令是否被禁用
if (
message.chat_stream
and message.chat_stream.stream_id
and plus_command_name
in global_announcement_manager.get_disabled_chat_commands(message.chat_stream.stream_id)
):
logger.info("用户禁用的PlusCommand跳过处理")
return False, None, True
message.is_command = True
# 获取插件配置
plugin_config = component_registry.get_plugin_config(plus_command_name)
# 创建PlusCommand实例
plus_command_instance = plus_command_class(message, plugin_config)
try:
# 检查聊天类型限制
if not plus_command_instance.is_chat_type_allowed():
is_group = hasattr(message, 'is_group_message') and message.is_group_message
logger.info(f"PlusCommand {plus_command_class.__name__} 不支持当前聊天类型: {'群聊' if is_group else '私聊'}")
return False, None, True # 跳过此命令,继续处理其他消息
# 设置参数
from src.plugin_system.base.command_args import CommandArgs
command_args = CommandArgs(args_text)
plus_command_instance.args = command_args
# 执行命令
success, response, intercept_message = await plus_command_instance.execute(command_args)
# 记录命令执行结果
if success:
logger.info(f"PlusCommand执行成功: {plus_command_class.__name__} (拦截: {intercept_message})")
else:
logger.warning(f"PlusCommand执行失败: {plus_command_class.__name__} - {response}")
# 根据命令的拦截设置决定是否继续处理消息
return True, response, not intercept_message # 找到命令根据intercept_message决定是否继续
except Exception as e:
logger.error(f"执行PlusCommand时出错: {plus_command_class.__name__} - {e}")
logger.error(traceback.format_exc())
try:
await plus_command_instance.send_text(f"命令执行出错: {str(e)}")
except Exception as send_error:
logger.error(f"发送错误消息失败: {send_error}")
# 命令出错时,根据命令的拦截设置决定是否继续处理消息
return True, str(e), False # 出错时继续处理消息
except Exception as e:
logger.error(f"处理PlusCommand时出错: {e}")
return False, None, True # 出错时继续处理消息
async def _process_commands_with_new_system(self, message: MessageRecv): async def _process_commands_with_new_system(self, message: MessageRecv):
# sourcery skip: use-named-expression # sourcery skip: use-named-expression
"""使用新插件系统处理命令""" """使用新插件系统处理命令"""
@@ -306,16 +418,26 @@ class ChatBot:
): ):
return return
# 命令处理 - 使用新插件系统检查并处理命令 # 命令处理 - 首先尝试PlusCommand独立处理
is_command, cmd_result, continue_process = await self._process_commands_with_new_system(message) is_plus_command, plus_cmd_result, plus_continue_process = await self._process_plus_commands(message)
# 如果是命令且不需要继续处理,则直接返回 # 如果是PlusCommand且不需要继续处理,则直接返回
if is_command and not continue_process: if is_plus_command and not plus_continue_process:
await MessageStorage.store_message(message, chat) await MessageStorage.store_message(message, chat)
logger.info(f"命令处理完成,跳过后续消息处理: {cmd_result}") logger.info(f"PlusCommand处理完成,跳过后续消息处理: {plus_cmd_result}")
return return
# 如果不是PlusCommand尝试传统的BaseCommand处理
if not is_plus_command:
is_command, cmd_result, continue_process = await self._process_commands_with_new_system(message)
# 如果是命令且不需要继续处理,则直接返回
if is_command and not continue_process:
await MessageStorage.store_message(message, chat)
logger.info(f"命令处理完成,跳过后续消息处理: {cmd_result}")
return
result = await event_manager.trigger_event(EventType.ON_MESSAGE,message=message) result = await event_manager.trigger_event(EventType.ON_MESSAGE,plugin_name="SYSTEM",message=message)
if not result.all_continue_process(): if not result.all_continue_process():
raise UserWarning(f"插件{result.get_summary().get('stopped_handlers','')}于消息到达时取消了消息处理") raise UserWarning(f"插件{result.get_summary().get('stopped_handlers','')}于消息到达时取消了消息处理")

View File

@@ -21,7 +21,7 @@ from src.chat.planner_actions.action_manager import ActionManager
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType from src.plugin_system.base.component_types import ActionInfo, ChatMode, ComponentType
from src.plugin_system.core.component_registry import component_registry from src.plugin_system.core.component_registry import component_registry
from src.manager.schedule_manager import schedule_manager from src.schedule.schedule_manager import schedule_manager
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
from src.chat.memory_system.Hippocampus import hippocampus_manager from src.chat.memory_system.Hippocampus import hippocampus_manager
logger = get_logger("planner") logger = get_logger("planner")

View File

@@ -32,7 +32,7 @@ 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.plugin_system.base.component_types import ActionInfo, EventType from src.plugin_system.base.component_types import ActionInfo, EventType
from src.plugin_system.apis import llm_api from src.plugin_system.apis import llm_api
from src.manager.schedule_manager import schedule_manager from src.schedule.schedule_manager import schedule_manager
logger = get_logger("replyer") logger = get_logger("replyer")
@@ -235,7 +235,7 @@ class DefaultReplyer:
from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor不然会循环依赖 from src.plugin_system.core.tool_use import ToolExecutor # 延迟导入ToolExecutor不然会循环依赖
self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id, enable_cache=False) self.tool_executor = ToolExecutor(chat_id=self.chat_stream.stream_id)
async def _build_cross_context_block(self, current_chat_id: str, target_user_info: Optional[Dict[str, Any]]) -> str: async def _build_cross_context_block(self, current_chat_id: str, target_user_info: Optional[Dict[str, Any]]) -> str:
"""构建跨群聊上下文""" """构建跨群聊上下文"""
@@ -370,7 +370,7 @@ class DefaultReplyer:
from src.plugin_system.core.event_manager import event_manager from src.plugin_system.core.event_manager import event_manager
if not from_plugin: if not from_plugin:
result = await event_manager.trigger_event(EventType.POST_LLM,prompt=prompt,stream_id=stream_id) result = await event_manager.trigger_event(EventType.POST_LLM,plugin_name="SYSTEM",prompt=prompt,stream_id=stream_id)
if not result.all_continue_process(): if not result.all_continue_process():
raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于请求前中断了内容生成") raise UserWarning(f"插件{result.get_summary().get('stopped_handlers', '')}于请求前中断了内容生成")
@@ -390,7 +390,7 @@ class DefaultReplyer:
} }
# 触发 AFTER_LLM 事件 # 触发 AFTER_LLM 事件
if not from_plugin: if not from_plugin:
result = await event_manager.trigger_event(EventType.AFTER_LLM,prompt=prompt,llm_response=llm_response,stream_id=stream_id) result = await event_manager.trigger_event(EventType.AFTER_LLM,plugin_name="SYSTEM",prompt=prompt,llm_response=llm_response,stream_id=stream_id)
if not result.all_continue_process(): if not result.all_continue_process():
raise UserWarning(f"插件{result.get_summary().get('stopped_handlers','')}于请求后取消了内容生成") raise UserWarning(f"插件{result.get_summary().get('stopped_handlers','')}于请求后取消了内容生成")
except UserWarning as e: except UserWarning as e:

View File

@@ -12,7 +12,6 @@ install(extra_lines=3)
logger = get_logger("prompt_build") logger = get_logger("prompt_build")
class PromptContext: class PromptContext:
def __init__(self): def __init__(self):
self._context_prompts: Dict[str, Dict[str, "Prompt"]] = {} self._context_prompts: Dict[str, Dict[str, "Prompt"]] = {}
@@ -28,7 +27,7 @@ class PromptContext:
@_current_context.setter @_current_context.setter
def _current_context(self, value: Optional[str]): def _current_context(self, value: Optional[str]):
"""设置当前协程的上下文ID""" """设置当前协程的上下文ID"""
self._current_context_var.set(value) self._current_context_var.set(value) # type: ignore
@asynccontextmanager @asynccontextmanager
async def async_scope(self, context_id: Optional[str] = None): async def async_scope(self, context_id: Optional[str] = None):
@@ -52,7 +51,7 @@ class PromptContext:
# 保存当前协程的上下文值,不影响其他协程 # 保存当前协程的上下文值,不影响其他协程
previous_context = self._current_context previous_context = self._current_context
# 设置当前协程的新上下文 # 设置当前协程的新上下文
token = self._current_context_var.set(context_id) if context_id else None token = self._current_context_var.set(context_id) if context_id else None # type: ignore
else: else:
# 如果没有提供新上下文,保持当前上下文不变 # 如果没有提供新上下文,保持当前上下文不变
previous_context = self._current_context previous_context = self._current_context
@@ -90,7 +89,8 @@ class PromptContext:
"""异步注册提示模板到指定作用域""" """异步注册提示模板到指定作用域"""
async with self._context_lock: async with self._context_lock:
if target_context := context_id or self._current_context: if target_context := context_id or self._current_context:
self._context_prompts.setdefault(target_context, {})[prompt.name] = prompt if prompt.name:
self._context_prompts.setdefault(target_context, {})[prompt.name] = prompt
class PromptManager: class PromptManager:
@@ -132,12 +132,16 @@ class PromptManager:
def add_prompt(self, name: str, fstr: str) -> "Prompt": def add_prompt(self, name: str, fstr: str) -> "Prompt":
prompt = Prompt(fstr, name=name) prompt = Prompt(fstr, name=name)
self._prompts[prompt.name] = prompt if prompt.name:
self._prompts[prompt.name] = prompt
return prompt return prompt
async def format_prompt(self, name: str, **kwargs) -> str: async def format_prompt(self, name: str, **kwargs) -> str:
# 获取当前提示词
prompt = await self.get_prompt_async(name) prompt = await self.get_prompt_async(name)
return prompt.format(**kwargs) # 获取基本格式化结果
result = prompt.format(**kwargs)
return result
# 全局单例 # 全局单例
@@ -145,6 +149,11 @@ global_prompt_manager = PromptManager()
class Prompt(str): class Prompt(str):
template: str
name: Optional[str]
args: List[str]
_args: List[Any]
_kwargs: Dict[str, Any]
# 临时标记,作为类常量 # 临时标记,作为类常量
_TEMP_LEFT_BRACE = "__ESCAPED_LEFT_BRACE__" _TEMP_LEFT_BRACE = "__ESCAPED_LEFT_BRACE__"
_TEMP_RIGHT_BRACE = "__ESCAPED_RIGHT_BRACE__" _TEMP_RIGHT_BRACE = "__ESCAPED_RIGHT_BRACE__"
@@ -165,7 +174,7 @@ class Prompt(str):
"""将临时标记还原为实际的花括号字符""" """将临时标记还原为实际的花括号字符"""
return template.replace(Prompt._TEMP_LEFT_BRACE, "{").replace(Prompt._TEMP_RIGHT_BRACE, "}") return template.replace(Prompt._TEMP_LEFT_BRACE, "{").replace(Prompt._TEMP_RIGHT_BRACE, "}")
def __new__(cls, fstr, name: Optional[str] = None, args: Union[List[Any], tuple[Any, ...]] = None, **kwargs): def __new__(cls, fstr, name: Optional[str] = None, args: Optional[Union[List[Any], tuple[Any, ...]]] = None, **kwargs):
# 如果传入的是元组,转换为列表 # 如果传入的是元组,转换为列表
if isinstance(args, tuple): if isinstance(args, tuple):
args = list(args) args = list(args)
@@ -201,7 +210,7 @@ class Prompt(str):
@classmethod @classmethod
async def create_async( async def create_async(
cls, fstr, name: Optional[str] = None, args: Union[List[Any], tuple[Any, ...]] = None, **kwargs cls, fstr, name: Optional[str] = None, args: Optional[Union[List[Any], tuple[Any, ...]]] = None, **kwargs
): ):
"""异步创建Prompt实例""" """异步创建Prompt实例"""
prompt = cls(fstr, name, args, **kwargs) prompt = cls(fstr, name, args, **kwargs)
@@ -210,7 +219,9 @@ class Prompt(str):
return prompt return prompt
@classmethod @classmethod
def _format_template(cls, template, args: List[Any] = None, kwargs: Dict[str, Any] = None) -> str: def _format_template(cls, template, args: Optional[List[Any]] = None, kwargs: Optional[Dict[str, Any]] = None) -> str:
if kwargs is None:
kwargs = {}
# 预处理模板中的转义花括号 # 预处理模板中的转义花括号
processed_template = cls._process_escaped_braces(template) processed_template = cls._process_escaped_braces(template)

View File

@@ -449,6 +449,8 @@ def find_similar_topics_simple(text: str, topics: list, top_k: int = 5) -> list:
def truncate_message(message: str, max_length=20) -> str: def truncate_message(message: str, max_length=20) -> str:
"""截断消息,使其不超过指定长度""" """截断消息,使其不超过指定长度"""
if message is None:
return ""
return f"{message[:max_length]}..." if len(message) > max_length else message return f"{message[:max_length]}..." if len(message) > max_length else message

View File

@@ -4,13 +4,13 @@ import hashlib
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np
import faiss import faiss
import chromadb from typing import Any, Dict, Optional, Union, List
from typing import Any, Dict, Optional, Union
from src.common.logger import get_logger from src.common.logger import get_logger
from src.llm_models.utils_model import LLMRequest from src.llm_models.utils_model import LLMRequest
from src.config.config import global_config, model_config from src.config.config import global_config, model_config
from src.common.database.sqlalchemy_models import CacheEntries from src.common.database.sqlalchemy_models import CacheEntries
from src.common.database.sqlalchemy_database_api import db_query, db_save from src.common.database.sqlalchemy_database_api import db_query, db_save
from src.common.vector_db import vector_db_service
logger = get_logger("cache_manager") logger = get_logger("cache_manager")
@@ -28,25 +28,23 @@ class CacheManager:
cls._instance = super(CacheManager, cls).__new__(cls) cls._instance = super(CacheManager, cls).__new__(cls)
return cls._instance return cls._instance
def __init__(self, default_ttl: int = 3600, chroma_path: str = "data/chroma_db"): def __init__(self, default_ttl: int = 3600):
""" """
初始化缓存管理器。 初始化缓存管理器。
""" """
if not hasattr(self, '_initialized'): if not hasattr(self, '_initialized'):
self.default_ttl = default_ttl self.default_ttl = default_ttl
self.semantic_cache_collection_name = "semantic_cache"
# L1 缓存 (内存) # L1 缓存 (内存)
self.l1_kv_cache: Dict[str, Dict[str, Any]] = {} self.l1_kv_cache: Dict[str, Dict[str, Any]] = {}
embedding_dim = global_config.lpmm_knowledge.embedding_dimension embedding_dim = global_config.lpmm_knowledge.embedding_dimension
self.l1_vector_index = faiss.IndexFlatIP(embedding_dim) self.l1_vector_index = faiss.IndexFlatIP(embedding_dim)
self.l1_vector_id_to_key: Dict[int, str] = {} self.l1_vector_id_to_key: Dict[int, str] = {}
# 语义缓存 (ChromaDB) # L2 向量缓存 (使用新的服务)
vector_db_service.get_or_create_collection(self.semantic_cache_collection_name)
self.chroma_client = chromadb.PersistentClient(path=chroma_path)
self.chroma_collection = self.chroma_client.get_or_create_collection(name="semantic_cache")
# 嵌入模型 # 嵌入模型
self.embedding_model = LLMRequest(model_config.model_task_config.embedding) self.embedding_model = LLMRequest(model_config.model_task_config.embedding)
@@ -143,7 +141,7 @@ class CacheManager:
# 步骤 2a: L1 语义缓存 (FAISS) # 步骤 2a: L1 语义缓存 (FAISS)
if query_embedding is not None and self.l1_vector_index.ntotal > 0: if query_embedding is not None and self.l1_vector_index.ntotal > 0:
faiss.normalize_L2(query_embedding) faiss.normalize_L2(query_embedding)
distances, indices = self.l1_vector_index.search(query_embedding, 1) distances, indices = self.l1_vector_index.search(query_embedding, 1) # type: ignore
if indices.size > 0 and distances[0][0] > 0.75: # IP 越大越相似 if indices.size > 0 and distances[0][0] > 0.75: # IP 越大越相似
hit_index = indices[0][0] hit_index = indices[0][0]
l1_hit_key = self.l1_vector_id_to_key.get(hit_index) l1_hit_key = self.l1_vector_id_to_key.get(hit_index)
@@ -152,18 +150,20 @@ class CacheManager:
return self.l1_kv_cache[l1_hit_key]["data"] return self.l1_kv_cache[l1_hit_key]["data"]
# 步骤 2b: L2 精确缓存 (数据库) # 步骤 2b: L2 精确缓存 (数据库)
cache_results = await db_query( cache_results_obj = await db_query(
model_class=CacheEntries, model_class=CacheEntries,
query_type="get", query_type="get",
filters={"cache_key": key}, filters={"cache_key": key},
single_result=True single_result=True
) )
if cache_results: if cache_results_obj:
expires_at = cache_results["expires_at"] # 使用 getattr 安全访问属性,避免 Pylance 类型检查错误
expires_at = getattr(cache_results_obj, "expires_at", 0)
if time.time() < expires_at: if time.time() < expires_at:
logger.info(f"命中L2键值缓存: {key}") logger.info(f"命中L2键值缓存: {key}")
data = orjson.loads(cache_results["cache_value"]) cache_value = getattr(cache_results_obj, "cache_value", "{}")
data = orjson.loads(cache_value)
# 更新访问统计 # 更新访问统计
await db_query( await db_query(
@@ -172,7 +172,7 @@ class CacheManager:
filters={"cache_key": key}, filters={"cache_key": key},
data={ data={
"last_accessed": time.time(), "last_accessed": time.time(),
"access_count": cache_results["access_count"] + 1 "access_count": getattr(cache_results_obj, "access_count", 0) + 1
} }
) )
@@ -187,29 +187,35 @@ class CacheManager:
filters={"cache_key": key} filters={"cache_key": key}
) )
# 步骤 2c: L2 语义缓存 (ChromaDB) # 步骤 2c: L2 语义缓存 (VectorDB Service)
if query_embedding is not None and self.chroma_collection: if query_embedding is not None:
try: try:
results = self.chroma_collection.query(query_embeddings=query_embedding.tolist(), n_results=1) results = vector_db_service.query(
if results and results['ids'] and results['ids'][0]: collection_name=self.semantic_cache_collection_name,
distance = results['distances'][0][0] if results['distances'] and results['distances'][0] else 'N/A' query_embeddings=query_embedding.tolist(),
n_results=1
)
if results and results.get('ids') and results['ids'][0]:
distance = results['distances'][0][0] if results.get('distances') and results['distances'][0] else 'N/A'
logger.debug(f"L2语义搜索找到最相似的结果: id={results['ids'][0]}, 距离={distance}") logger.debug(f"L2语义搜索找到最相似的结果: id={results['ids'][0]}, 距离={distance}")
if distance != 'N/A' and distance < 0.75: if distance != 'N/A' and distance < 0.75:
l2_hit_key = results['ids'][0][0] if isinstance(results['ids'][0], list) else results['ids'][0] l2_hit_key = results['ids'][0][0] if isinstance(results['ids'][0], list) else results['ids'][0]
logger.info(f"命中L2语义缓存: key='{l2_hit_key}', 距离={distance:.4f}") logger.info(f"命中L2语义缓存: key='{l2_hit_key}', 距离={distance:.4f}")
# 从数据库获取缓存数据 # 从数据库获取缓存数据
semantic_cache_results = await db_query( semantic_cache_results_obj = await db_query(
model_class=CacheEntries, model_class=CacheEntries,
query_type="get", query_type="get",
filters={"cache_key": l2_hit_key}, filters={"cache_key": l2_hit_key},
single_result=True single_result=True
) )
if semantic_cache_results: if semantic_cache_results_obj:
expires_at = semantic_cache_results["expires_at"] expires_at = getattr(semantic_cache_results_obj, "expires_at", 0)
if time.time() < expires_at: if time.time() < expires_at:
data = orjson.loads(semantic_cache_results["cache_value"]) cache_value = getattr(semantic_cache_results_obj, "cache_value", "{}")
data = orjson.loads(cache_value)
logger.debug(f"L2语义缓存返回的数据: {data}") logger.debug(f"L2语义缓存返回的数据: {data}")
# 回填 L1 # 回填 L1
@@ -218,13 +224,13 @@ class CacheManager:
try: try:
new_id = self.l1_vector_index.ntotal new_id = self.l1_vector_index.ntotal
faiss.normalize_L2(query_embedding) faiss.normalize_L2(query_embedding)
self.l1_vector_index.add(x=query_embedding) self.l1_vector_index.add(x=query_embedding) # type: ignore
self.l1_vector_id_to_key[new_id] = key self.l1_vector_id_to_key[new_id] = key
except Exception as e: except Exception as e:
logger.error(f"回填L1向量索引时发生错误: {e}") logger.error(f"回填L1向量索引时发生错误: {e}")
return data return data
except Exception as e: except Exception as e:
logger.warning(f"ChromaDB查询失败: {e}") logger.warning(f"VectorDB Service 查询失败: {e}")
logger.debug(f"缓存未命中: {key}") logger.debug(f"缓存未命中: {key}")
return None return None
@@ -261,22 +267,27 @@ class CacheManager:
) )
# 写入语义缓存 # 写入语义缓存
if semantic_query and self.embedding_model and self.chroma_collection: if semantic_query and self.embedding_model:
try: try:
embedding_result = await self.embedding_model.get_embedding(semantic_query) embedding_result = await self.embedding_model.get_embedding(semantic_query)
if embedding_result: if embedding_result:
# embedding_result是一个元组(embedding_vector, model_name),取第一个元素
embedding_vector = embedding_result[0] if isinstance(embedding_result, tuple) else embedding_result embedding_vector = embedding_result[0] if isinstance(embedding_result, tuple) else embedding_result
validated_embedding = self._validate_embedding(embedding_vector) validated_embedding = self._validate_embedding(embedding_vector)
if validated_embedding is not None: if validated_embedding is not None:
embedding = np.array([validated_embedding], dtype='float32') embedding = np.array([validated_embedding], dtype='float32')
# 写入 L1 Vector # 写入 L1 Vector
new_id = self.l1_vector_index.ntotal new_id = self.l1_vector_index.ntotal
faiss.normalize_L2(embedding) faiss.normalize_L2(embedding)
self.l1_vector_index.add(x=embedding) self.l1_vector_index.add(x=embedding) # type: ignore
self.l1_vector_id_to_key[new_id] = key self.l1_vector_id_to_key[new_id] = key
# 写入 L2 Vector
self.chroma_collection.add(embeddings=embedding.tolist(), ids=[key]) # 写入 L2 Vector (使用新的服务)
vector_db_service.add(
collection_name=self.semantic_cache_collection_name,
embeddings=embedding.tolist(),
ids=[key]
)
except Exception as e: except Exception as e:
logger.warning(f"语义缓存写入失败: {e}") logger.warning(f"语义缓存写入失败: {e}")
@@ -298,15 +309,14 @@ class CacheManager:
filters={} # 删除所有记录 filters={} # 删除所有记录
) )
# 清空ChromaDB # 清空 VectorDB
if self.chroma_collection: try:
try: vector_db_service.delete_collection(name=self.semantic_cache_collection_name)
self.chroma_client.delete_collection(name="semantic_cache") vector_db_service.get_or_create_collection(name=self.semantic_cache_collection_name)
self.chroma_collection = self.chroma_client.get_or_create_collection(name="semantic_cache") except Exception as e:
except Exception as e: logger.warning(f"清空 VectorDB 集合失败: {e}")
logger.warning(f"清空ChromaDB失败: {e}")
logger.info("L2 (数据库 & ChromaDB) 缓存已清空。") logger.info("L2 (数据库 & VectorDB) 缓存已清空。")
async def clear_all(self): async def clear_all(self):
"""清空所有缓存。""" """清空所有缓存。"""
@@ -338,4 +348,64 @@ class CacheManager:
logger.info(f"清理了 {len(expired_keys)} 个过期的L1缓存条目") logger.info(f"清理了 {len(expired_keys)} 个过期的L1缓存条目")
# 全局实例 # 全局实例
tool_cache = CacheManager() tool_cache = CacheManager()
import inspect
import time
def wrap_tool_executor():
"""
包装工具执行器以添加缓存功能
这个函数应该在系统启动时被调用一次
"""
from src.plugin_system.core.tool_use import ToolExecutor
from src.plugin_system.apis.tool_api import get_tool_instance
original_execute = ToolExecutor.execute_tool_call
async def wrapped_execute_tool_call(self, tool_call, tool_instance=None):
if not tool_instance:
tool_instance = get_tool_instance(tool_call.func_name)
if not tool_instance or not tool_instance.enable_cache:
return await original_execute(self, tool_call, tool_instance)
try:
tool_file_path = inspect.getfile(tool_instance.__class__)
semantic_query = None
if tool_instance.semantic_cache_query_key:
semantic_query = tool_call.args.get(tool_instance.semantic_cache_query_key)
cached_result = await tool_cache.get(
tool_name=tool_call.func_name,
function_args=tool_call.args,
tool_file_path=tool_file_path,
semantic_query=semantic_query
)
if cached_result:
logger.info(f"{getattr(self, 'log_prefix', '')}使用缓存结果,跳过工具 {tool_call.func_name} 执行")
return cached_result
except Exception as e:
logger.error(f"{getattr(self, 'log_prefix', '')}检查工具缓存时出错: {e}")
result = await original_execute(self, tool_call, tool_instance)
try:
tool_file_path = inspect.getfile(tool_instance.__class__)
semantic_query = None
if tool_instance.semantic_cache_query_key:
semantic_query = tool_call.args.get(tool_instance.semantic_cache_query_key)
await tool_cache.set(
tool_name=tool_call.func_name,
function_args=tool_call.args,
tool_file_path=tool_file_path,
data=result,
ttl=tool_instance.cache_ttl,
semantic_query=semantic_query
)
except Exception as e:
logger.error(f"{getattr(self, 'log_prefix', '')}设置工具缓存时出错: {e}")
return result
ToolExecutor.execute_tool_call = wrapped_execute_tool_call

View File

@@ -80,16 +80,54 @@ def mark_plans_completed(plan_ids: List[int]):
with get_db_session() as session: with get_db_session() as session:
try: try:
plans_to_mark = session.query(MonthlyPlan).filter(MonthlyPlan.id.in_(plan_ids)).all()
if not plans_to_mark:
logger.info("没有需要标记为完成的月度计划。")
return
plan_details = "\n".join([f" {i+1}. {plan.plan_text}" for i, plan in enumerate(plans_to_mark)])
logger.info(f"以下 {len(plans_to_mark)} 条月度计划将被标记为已完成:\n{plan_details}")
session.query(MonthlyPlan).filter( session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(plan_ids) MonthlyPlan.id.in_(plan_ids)
).update({"status": "completed"}, synchronize_session=False) ).update({"status": "completed"}, synchronize_session=False)
session.commit() session.commit()
logger.info(f"成功将 {len(plan_ids)} 条月度计划标记为已完成。")
except Exception as e: except Exception as e:
logger.error(f"标记月度计划为完成时发生错误: {e}") logger.error(f"标记月度计划为完成时发生错误: {e}")
session.rollback() session.rollback()
raise raise
def delete_plans_by_ids(plan_ids: List[int]):
"""
根据ID列表从数据库中物理删除月度计划。
:param plan_ids: 需要删除的计划ID列表。
"""
if not plan_ids:
return
with get_db_session() as session:
try:
# 先查询要删除的计划,用于日志记录
plans_to_delete = session.query(MonthlyPlan).filter(MonthlyPlan.id.in_(plan_ids)).all()
if not plans_to_delete:
logger.info("没有找到需要删除的月度计划。")
return
plan_details = "\n".join([f" {i+1}. {plan.plan_text}" for i, plan in enumerate(plans_to_delete)])
logger.info(f"检测到月度计划超额,将删除以下 {len(plans_to_delete)} 条计划:\n{plan_details}")
# 执行删除
session.query(MonthlyPlan).filter(
MonthlyPlan.id.in_(plan_ids)
).delete(synchronize_session=False)
session.commit()
except Exception as e:
logger.error(f"删除月度计划时发生错误: {e}")
session.rollback()
raise
def soft_delete_plans(plan_ids: List[int]): def soft_delete_plans(plan_ids: List[int]):
""" """
将指定ID的计划标记为软删除兼容旧接口 将指定ID的计划标记为软删除兼容旧接口

View File

@@ -5,12 +5,12 @@
from sqlalchemy import Column, String, Float, Integer, Boolean, Text, Index, create_engine, DateTime from sqlalchemy import Column, String, Float, Integer, Boolean, Text, Index, create_engine, DateTime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session from sqlalchemy.orm import sessionmaker, Session, Mapped, mapped_column
from sqlalchemy.pool import QueuePool from sqlalchemy.pool import QueuePool
import os import os
import datetime import datetime
import time import time
from typing import Iterator, Optional from typing import Iterator, Optional, Any, Dict
from src.common.logger import get_logger from src.common.logger import get_logger
from contextlib import contextmanager from contextlib import contextmanager
@@ -306,14 +306,14 @@ class Expression(Base):
"""表达风格模型""" """表达风格模型"""
__tablename__ = 'expression' __tablename__ = 'expression'
id = Column(Integer, primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
situation = Column(Text, nullable=False) situation: Mapped[str] = mapped_column(Text, nullable=False)
style = Column(Text, nullable=False) style: Mapped[str] = mapped_column(Text, nullable=False)
count = Column(Float, nullable=False) count: Mapped[float] = mapped_column(Float, nullable=False)
last_active_time = Column(Float, nullable=False) last_active_time: Mapped[float] = mapped_column(Float, nullable=False)
chat_id = Column(get_string_field(64), nullable=False, index=True) chat_id: Mapped[str] = mapped_column(get_string_field(64), nullable=False, index=True)
type = Column(Text, nullable=False) type: Mapped[str] = mapped_column(Text, nullable=False)
create_date = Column(Float, nullable=True) create_date: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
__table_args__ = ( __table_args__ = (
Index('idx_expression_chat_id', 'chat_id'), Index('idx_expression_chat_id', 'chat_id'),
@@ -589,7 +589,7 @@ def initialize_database():
config = global_config.database config = global_config.database
# 配置引擎参数 # 配置引擎参数
engine_kwargs = { engine_kwargs: Dict[str, Any] = {
'echo': False, # 生产环境关闭SQL日志 'echo': False, # 生产环境关闭SQL日志
'future': True, 'future': True,
} }
@@ -642,7 +642,9 @@ def get_db_session() -> Iterator[Session]:
"""数据库会话上下文管理器 - 推荐使用这个而不是get_session()""" """数据库会话上下文管理器 - 推荐使用这个而不是get_session()"""
session: Optional[Session] = None session: Optional[Session] = None
try: try:
_, SessionLocal = initialize_database() engine, SessionLocal = initialize_database()
if not SessionLocal:
raise RuntimeError("Database session not initialized")
session = SessionLocal() session = SessionLocal()
yield session yield session
#session.commit() #session.commit()

View File

@@ -0,0 +1,19 @@
from .base import VectorDBBase
from .chromadb_impl import ChromaDBImpl
def get_vector_db_service() -> VectorDBBase:
"""
工厂函数,初始化并返回向量数据库服务实例。
目前硬编码为 ChromaDB未来可以从配置中读取。
"""
# TODO: 从全局配置中读取数据库类型和路径
db_path = "data/chroma_db"
# ChromaDBImpl 是一个单例,所以这里每次调用都会返回同一个实例
return ChromaDBImpl(path=db_path)
# 全局向量数据库服务实例
vector_db_service: VectorDBBase = get_vector_db_service()
__all__ = ["vector_db_service", "VectorDBBase"]

View File

@@ -0,0 +1,145 @@
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
class VectorDBBase(ABC):
"""
向量数据库的抽象基类 (ABC),定义了所有向量数据库实现必须遵循的接口。
"""
@abstractmethod
def __init__(self, path: str, **kwargs: Any):
"""
初始化向量数据库客户端。
Args:
path (str): 数据库文件的存储路径。
**kwargs: 其他特定于实现的参数。
"""
pass
@abstractmethod
def get_or_create_collection(self, name: str, **kwargs: Any) -> Any:
"""
获取或创建一个集合 (Collection)。
Args:
name (str): 集合的名称。
**kwargs: 其他特定于实现的参数 (例如 metadata)。
Returns:
Any: 代表集合的对象。
"""
pass
@abstractmethod
def add(
self,
collection_name: str,
embeddings: List[List[float]],
documents: Optional[List[str]] = None,
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
) -> None:
"""
向指定集合中添加数据。
Args:
collection_name (str): 目标集合的名称。
embeddings (List[List[float]]): 向量列表。
documents (Optional[List[str]], optional): 文档列表。Defaults to None.
metadatas (Optional[List[Dict[str, Any]]], optional): 元数据列表。Defaults to None.
ids (Optional[List[str]], optional): ID 列表。Defaults to None.
"""
pass
@abstractmethod
def query(
self,
collection_name: str,
query_embeddings: List[List[float]],
n_results: int = 1,
where: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> Dict[str, List[Any]]:
"""
在指定集合中查询相似向量。
Args:
collection_name (str): 目标集合的名称。
query_embeddings (List[List[float]]): 用于查询的向量列表。
n_results (int, optional): 返回结果的数量。Defaults to 1.
where (Optional[Dict[str, Any]], optional): 元数据过滤条件。Defaults to None.
**kwargs: 其他特定于实现的参数。
Returns:
Dict[str, List[Any]]: 查询结果,通常包含 ids, distances, metadatas, documents。
"""
pass
@abstractmethod
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
where: Optional[Dict[str, Any]] = None,
) -> None:
"""
从指定集合中删除数据。
Args:
collection_name (str): 目标集合的名称。
ids (Optional[List[str]], optional): 要删除的条目的 ID 列表。Defaults to None.
where (Optional[Dict[str, Any]], optional): 基于元数据的过滤条件。Defaults to None.
"""
pass
@abstractmethod
def get(
self,
collection_name: str,
ids: Optional[List[str]] = None,
where: Optional[Dict[str, Any]] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
where_document: Optional[Dict[str, Any]] = None,
include: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
根据条件从集合中获取数据。
Args:
collection_name (str): 目标集合的名称。
ids (Optional[List[str]], optional): 要获取的条目的 ID 列表。Defaults to None.
where (Optional[Dict[str, Any]], optional): 基于元数据的过滤条件。Defaults to None.
limit (Optional[int], optional): 返回结果的数量限制。Defaults to None.
offset (Optional[int], optional): 返回结果的偏移量。Defaults to None.
where_document (Optional[Dict[str, Any]], optional): 基于文档内容的过滤条件。Defaults to None.
include (Optional[List[str]], optional): 指定返回的数据字段 (e.g., ["metadatas", "documents"])。Defaults to None.
Returns:
Dict[str, Any]: 获取到的数据。
"""
pass
@abstractmethod
def count(self, collection_name: str) -> int:
"""
获取指定集合中的条目总数。
Args:
collection_name (str): 目标集合的名称。
Returns:
int: 条目总数。
"""
pass
@abstractmethod
def delete_collection(self, name: str) -> None:
"""
删除一个集合。
Args:
name (str): 要删除的集合的名称。
"""
pass

View File

@@ -0,0 +1,163 @@
import threading
from typing import Any, Dict, List, Optional
import chromadb
from chromadb.config import Settings
from .base import VectorDBBase
from src.common.logger import get_logger
logger = get_logger("chromadb_impl")
class ChromaDBImpl(VectorDBBase):
"""
ChromaDB 的具体实现,遵循 VectorDBBase 接口。
采用单例模式,确保全局只有一个 ChromaDB 客户端实例。
"""
_instance = None
_lock = threading.Lock()
def __new__(cls, *args, **kwargs):
if not cls._instance:
with cls._lock:
if not cls._instance:
cls._instance = super(ChromaDBImpl, cls).__new__(cls)
return cls._instance
def __init__(self, path: str = "data/chroma_db", **kwargs: Any):
"""
初始化 ChromaDB 客户端。
由于是单例,这个初始化只会执行一次。
"""
if not hasattr(self, '_initialized'):
with self._lock:
if not hasattr(self, '_initialized'):
try:
self.client = chromadb.PersistentClient(
path=path,
settings=Settings(anonymized_telemetry=False)
)
self._collections: Dict[str, Any] = {}
self._initialized = True
logger.info(f"ChromaDB 客户端已初始化,数据库路径: {path}")
except Exception as e:
logger.error(f"ChromaDB 初始化失败: {e}")
self.client = None
self._initialized = False
def get_or_create_collection(self, name: str, **kwargs: Any) -> Any:
if not self.client:
raise ConnectionError("ChromaDB 客户端未初始化")
if name in self._collections:
return self._collections[name]
try:
collection = self.client.get_or_create_collection(name=name, **kwargs)
self._collections[name] = collection
logger.info(f"成功获取或创建集合: '{name}'")
return collection
except Exception as e:
logger.error(f"获取或创建集合 '{name}' 失败: {e}")
return None
def add(
self,
collection_name: str,
embeddings: List[List[float]],
documents: Optional[List[str]] = None,
metadatas: Optional[List[Dict[str, Any]]] = None,
ids: Optional[List[str]] = None,
) -> None:
collection = self.get_or_create_collection(collection_name)
if collection:
try:
collection.add(
embeddings=embeddings,
documents=documents,
metadatas=metadatas,
ids=ids,
)
except Exception as e:
logger.error(f"向集合 '{collection_name}' 添加数据失败: {e}")
def query(
self,
collection_name: str,
query_embeddings: List[List[float]],
n_results: int = 1,
where: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> Dict[str, List[Any]]:
collection = self.get_or_create_collection(collection_name)
if collection:
try:
return collection.query(
query_embeddings=query_embeddings,
n_results=n_results,
where=where or {},
**kwargs,
)
except Exception as e:
logger.error(f"查询集合 '{collection_name}' 失败: {e}")
return {}
def get(
self,
collection_name: str,
ids: Optional[List[str]] = None,
where: Optional[Dict[str, Any]] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
where_document: Optional[Dict[str, Any]] = None,
include: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""根据条件从集合中获取数据"""
collection = self.get_or_create_collection(collection_name)
if collection:
try:
return collection.get(
ids=ids,
where=where,
limit=limit,
offset=offset,
where_document=where_document,
include=include,
)
except Exception as e:
logger.error(f"从集合 '{collection_name}' 获取数据失败: {e}")
return {}
def delete(
self,
collection_name: str,
ids: Optional[List[str]] = None,
where: Optional[Dict[str, Any]] = None,
) -> None:
collection = self.get_or_create_collection(collection_name)
if collection:
try:
collection.delete(ids=ids, where=where)
except Exception as e:
logger.error(f"从集合 '{collection_name}' 删除数据失败: {e}")
def count(self, collection_name: str) -> int:
collection = self.get_or_create_collection(collection_name)
if collection:
try:
return collection.count()
except Exception as e:
logger.error(f"获取集合 '{collection_name}' 计数失败: {e}")
return 0
def delete_collection(self, name: str) -> None:
if not self.client:
raise ConnectionError("ChromaDB 客户端未初始化")
try:
self.client.delete_collection(name=name)
if name in self._collections:
del self._collections[name]
logger.info(f"集合 '{name}' 已被删除")
except Exception as e:
logger.error(f"删除集合 '{name}' 失败: {e}")

View File

@@ -45,6 +45,7 @@ from src.config.official_configs import (
MonthlyPlanSystemConfig, MonthlyPlanSystemConfig,
CrossContextConfig, CrossContextConfig,
PermissionConfig, PermissionConfig,
CommandConfig,
MaizoneIntercomConfig, MaizoneIntercomConfig,
) )
@@ -372,7 +373,7 @@ class Config(ValidatedConfigBase):
chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置") chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置")
response_post_process: ResponsePostProcessConfig = Field(..., description="响应后处理配置") response_post_process: ResponsePostProcessConfig = Field(..., description="响应后处理配置")
response_splitter: ResponseSplitterConfig = Field(..., description="响应分割配置") response_splitter: ResponseSplitterConfig = Field(..., description="响应分割配置")
experimental: ExperimentalConfig = Field(..., description="实验性功能配置") experimental: ExperimentalConfig = Field(default_factory=lambda: ExperimentalConfig(), description="实验性功能配置")
maim_message: MaimMessageConfig = Field(..., description="Maim消息配置") maim_message: MaimMessageConfig = Field(..., description="Maim消息配置")
lpmm_knowledge: LPMMKnowledgeConfig = Field(..., description="LPMM知识配置") lpmm_knowledge: LPMMKnowledgeConfig = Field(..., description="LPMM知识配置")
tool: ToolConfig = Field(..., description="工具配置") tool: ToolConfig = Field(..., description="工具配置")
@@ -381,6 +382,7 @@ class Config(ValidatedConfigBase):
voice: VoiceConfig = Field(..., description="语音配置") voice: VoiceConfig = Field(..., description="语音配置")
schedule: ScheduleConfig = Field(..., description="调度配置") schedule: ScheduleConfig = Field(..., description="调度配置")
permission: PermissionConfig = Field(..., description="权限配置") permission: PermissionConfig = Field(..., description="权限配置")
command: CommandConfig = Field(..., description="命令系统配置")
# 有默认值的字段放在后面 # 有默认值的字段放在后面
anti_prompt_injection: AntiPromptInjectionConfig = Field(default_factory=lambda: AntiPromptInjectionConfig(), description="反提示注入配置") anti_prompt_injection: AntiPromptInjectionConfig = Field(default_factory=lambda: AntiPromptInjectionConfig(), description="反提示注入配置")

View File

@@ -263,11 +263,20 @@ class NormalChatConfig(ValidatedConfigBase):
class ExpressionRule(ValidatedConfigBase):
"""表达学习规则"""
chat_stream_id: str = Field(..., description="聊天流ID空字符串表示全局")
use_expression: bool = Field(default=True, description="是否使用学到的表达")
learn_expression: bool = Field(default=True, description="是否学习表达")
learning_strength: float = Field(default=1.0, description="学习强度")
group: Optional[str] = Field(default=None, description="表达共享组")
class ExpressionConfig(ValidatedConfigBase): class ExpressionConfig(ValidatedConfigBase):
"""表达配置类""" """表达配置类"""
expression_learning: list[list] = Field(default_factory=lambda: [], description="表达学习") rules: List[ExpressionRule] = Field(default_factory=list, description="表达学习规则")
expression_groups: list[list[str]] = Field(default_factory=list, description="表达组")
def _parse_stream_config_to_chat_id(self, stream_config_str: str) -> Optional[str]: def _parse_stream_config_to_chat_id(self, stream_config_str: str) -> Optional[str]:
""" """
@@ -314,87 +323,39 @@ class ExpressionConfig(ValidatedConfigBase):
Returns: Returns:
tuple: (是否使用表达, 是否学习表达, 学习间隔) tuple: (是否使用表达, 是否学习表达, 学习间隔)
""" """
if not self.expression_learning: if not self.rules:
# 如果没有配置,使用默认值:启用表达,启用学习,300秒间隔 # 如果没有配置,使用默认值:启用表达,启用学习,强度1.0
return True, True, 300 return True, True, 1.0
# 优先检查聊天流特定的配置 # 优先检查聊天流特定的配置
if chat_stream_id: if chat_stream_id:
specific_config = self._get_stream_specific_config(chat_stream_id) for rule in self.rules:
if specific_config is not None: if rule.chat_stream_id and self._parse_stream_config_to_chat_id(rule.chat_stream_id) == chat_stream_id:
return specific_config return rule.use_expression, rule.learn_expression, rule.learning_strength
# 检查全局配置(第一个元素为空字符串的配置) # 检查全局配置(chat_stream_id为空字符串的配置)
global_config = self._get_global_config() for rule in self.rules:
if global_config is not None: if rule.chat_stream_id == "":
return global_config return rule.use_expression, rule.learn_expression, rule.learning_strength
# 如果都没有匹配,返回默认值 # 如果都没有匹配,返回默认值
return True, True, 300 return True, True, 1.0
def _get_stream_specific_config(self, chat_stream_id: str) -> Optional[tuple[bool, bool, float]]:
"""
获取特定聊天流的表达配置
Args: class ToolHistoryConfig(ValidatedConfigBase):
chat_stream_id: 聊天流ID哈希值 """工具历史记录配置类"""
Returns: enable_history: bool = True
tuple: (是否使用表达, 是否学习表达, 学习间隔),如果没有配置则返回 None """是否启用工具历史记录"""
"""
for config_item in self.expression_learning:
if not config_item or len(config_item) < 4:
continue
stream_config_str = config_item[0] # 例如 "qq:1026294844:group" enable_prompt_history: bool = True
"""是否在提示词中加入工具历史记录"""
# 如果是空字符串,跳过(这是全局配置) max_history: int = 5
if stream_config_str == "": """注入到提示词中的最大工具历史记录数量"""
continue
# 解析配置字符串并生成对应的 chat_id
config_chat_id = self._parse_stream_config_to_chat_id(stream_config_str)
if config_chat_id is None:
continue
# 比较生成的 chat_id
if config_chat_id != chat_stream_id:
continue
# 解析配置
try:
use_expression = config_item[1].lower() == "enable"
enable_learning = config_item[2].lower() == "enable"
learning_intensity = float(config_item[3])
return use_expression, enable_learning, learning_intensity
except (ValueError, IndexError):
continue
return None
def _get_global_config(self) -> Optional[tuple[bool, bool, float]]:
"""
获取全局表达配置
Returns:
tuple: (是否使用表达, 是否学习表达, 学习间隔),如果没有配置则返回 None
"""
for config_item in self.expression_learning:
if not config_item or len(config_item) < 4:
continue
# 检查是否为全局配置(第一个元素为空字符串)
if config_item[0] == "":
try:
use_expression = config_item[1].lower() == "enable"
enable_learning = config_item[2].lower() == "enable"
learning_intensity = float(config_item[3])
return use_expression, enable_learning, learning_intensity
except (ValueError, IndexError):
continue
return None
data_dir: str = "data/tool_history"
"""历史记录保存目录"""
class ToolConfig(ValidatedConfigBase): class ToolConfig(ValidatedConfigBase):
@@ -402,7 +363,8 @@ class ToolConfig(ValidatedConfigBase):
enable_tool: bool = Field(default=False, description="启用工具") enable_tool: bool = Field(default=False, description="启用工具")
history: ToolHistoryConfig = Field(default_factory=ToolHistoryConfig)
"""工具历史记录配置"""
class VoiceConfig(ValidatedConfigBase): class VoiceConfig(ValidatedConfigBase):
"""语音识别配置类""" """语音识别配置类"""
@@ -568,6 +530,14 @@ class ScheduleConfig(ValidatedConfigBase):
enable: bool = Field(default=True, description="启用") enable: bool = Field(default=True, description="启用")
guidelines: Optional[str] = Field(default=None, description="指导方针") guidelines: Optional[str] = Field(default=None, description="指导方针")
enable_is_sleep: bool = Field(default=True, description="让AI会根据日程表睡觉和苏醒") enable_is_sleep: bool = Field(default=True, description="让AI会根据日程表睡觉和苏醒")
enable_flexible_sleep: bool = Field(default=True, description="是否启用弹性睡眠")
flexible_sleep_pressure_threshold: float = Field(default=40.0, description="触发弹性睡眠的睡眠压力阈值,低于该值可能延迟入睡")
max_sleep_delay_minutes: int = Field(default=60, description="单日最大延迟入睡分钟数")
enable_pre_sleep_notification: bool = Field(default=True, description="是否启用睡前消息")
pre_sleep_notification_groups: List[str] = Field(default_factory=list, description="接收睡前消息的群号列表, 格式: [\"platform:group_id1\", \"platform:group_id2\"]")
pre_sleep_prompt: str = Field(default="我准备睡觉了,请生成一句简短自然的晚安问候。", description="用于生成睡前消息的提示")
@@ -641,7 +611,7 @@ class PluginsConfig(ValidatedConfigBase):
class WakeUpSystemConfig(ValidatedConfigBase): class WakeUpSystemConfig(ValidatedConfigBase):
"""唤醒度系统配置类""" """唤醒度与失眠系统配置类"""
enable: bool = Field(default=True, description="是否启用唤醒度系统") enable: bool = Field(default=True, description="是否启用唤醒度系统")
wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒") wakeup_threshold: float = Field(default=15.0, ge=1.0, description="唤醒阈值,达到此值时会被唤醒")
@@ -652,6 +622,16 @@ class WakeUpSystemConfig(ValidatedConfigBase):
angry_duration: float = Field(default=300.0, ge=10.0, description="愤怒状态持续时间(秒)") angry_duration: float = Field(default=300.0, ge=10.0, description="愤怒状态持续时间(秒)")
angry_prompt: str = Field(default="你被人吵醒了非常生气,说话带着怒气", description="被吵醒后的愤怒提示词") angry_prompt: str = Field(default="你被人吵醒了非常生气,说话带着怒气", description="被吵醒后的愤怒提示词")
# --- 失眠机制相关参数 ---
enable_insomnia_system: bool = Field(default=True, description="是否启用失眠系统")
insomnia_duration_minutes: int = Field(default=30, ge=1, description="单次失眠状态的持续时间(分钟)")
sleep_pressure_threshold: float = Field(default=30.0, description="触发“压力不足型失眠”的睡眠压力阈值")
deep_sleep_threshold: float = Field(default=80.0, description="进入“深度睡眠”的睡眠压力阈值")
insomnia_chance_low_pressure: float = Field(default=0.6, ge=0.0, le=1.0, description="压力不足时的失眠基础概率")
insomnia_chance_normal_pressure: float = Field(default=0.1, ge=0.0, le=1.0, description="压力正常时的失眠基础概率")
sleep_pressure_increment: float = Field(default=1.5, ge=0.0, description="每次AI执行动作后增加的睡眠压力值")
sleep_pressure_decay_rate: float = Field(default=1.5, ge=0.0, description="睡眠时,每分钟衰减的睡眠压力值")
class MonthlyPlanSystemConfig(ValidatedConfigBase): class MonthlyPlanSystemConfig(ValidatedConfigBase):
"""月度计划系统配置类""" """月度计划系统配置类"""
@@ -681,6 +661,12 @@ class MaizoneIntercomConfig(ValidatedConfigBase):
groups: List[ContextGroup] = Field(default_factory=list, description="Maizone互通组列表") groups: List[ContextGroup] = Field(default_factory=list, description="Maizone互通组列表")
class CommandConfig(ValidatedConfigBase):
"""命令系统配置类"""
command_prefixes: List[str] = Field(default_factory=lambda: ['/', '!', '.', '#'], description="支持的命令前缀列表")
class PermissionConfig(ValidatedConfigBase): class PermissionConfig(ValidatedConfigBase):
"""权限系统配置类""" """权限系统配置类"""

View File

@@ -328,10 +328,13 @@ class LLMRequest:
if not reasoning_content and content: if not reasoning_content and content:
content, extracted_reasoning = self._extract_reasoning(content) content, extracted_reasoning = self._extract_reasoning(content)
reasoning_content = extracted_reasoning reasoning_content = extracted_reasoning
# 检测是否为空回复或截断 is_empty_reply = False
is_empty_reply = not content or content.strip() == ""
is_truncated = False is_truncated = False
# 检测是否为空回复或截断
if not tool_calls:
is_empty_reply = not content or content.strip() == ""
is_truncated = False
if use_anti_truncation: if use_anti_truncation:
if content.endswith("[done]"): if content.endswith("[done]"):
@@ -370,7 +373,7 @@ class LLMRequest:
) )
# 处理空回复 # 处理空回复
if not content: if not content and not tool_calls:
if raise_when_empty: if raise_when_empty:
raise RuntimeError(f"经过 {empty_retry_count} 次重试后仍然生成空回复") raise RuntimeError(f"经过 {empty_retry_count} 次重试后仍然生成空回复")
content = "生成的响应为空,请检查模型配置或输入内容是否正确" content = "生成的响应为空,请检查模型配置或输入内容是否正确"

View File

@@ -17,8 +17,8 @@ from src.individuality.individuality import get_individuality, Individuality
from src.common.server import get_global_server, Server from src.common.server import get_global_server, Server
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
from rich.traceback import install from rich.traceback import install
from src.manager.schedule_manager import schedule_manager from src.schedule.schedule_manager import schedule_manager
from src.manager.monthly_plan_manager import monthly_plan_manager from src.schedule.monthly_plan_manager import monthly_plan_manager
from src.plugin_system.core.event_manager import event_manager from src.plugin_system.core.event_manager import event_manager
from src.plugin_system.base.component_types import EventType from src.plugin_system.base.component_types import EventType
# from src.api.main import start_api_server # from src.api.main import start_api_server
@@ -141,10 +141,10 @@ class MainSystem:
logger.info(f""" logger.info(f"""
全部系统初始化完成,{global_config.bot.nickname}已成功唤醒 全部系统初始化完成,{global_config.bot.nickname}已成功唤醒
========================================================= =========================================================
MaiMbot-Pro-Max(第三方修改版) MoFox_Bot(第三方修改版)
全部组件已成功启动! 全部组件已成功启动!
========================================================= =========================================================
🌐 项目地址: https://github.com/MaiBot-Plus/MaiMbot-Pro-Max 🌐 项目地址: https://github.com/MoFox-Studio/MoFox_Bot
🏠 官方项目: https://github.com/MaiM-with-u/MaiBot 🏠 官方项目: https://github.com/MaiM-with-u/MaiBot
========================================================= =========================================================
这是基于原版MMC的社区改版包含增强功能和优化(同时也有更多的'特性') 这是基于原版MMC的社区改版包含增强功能和优化(同时也有更多的'特性')
@@ -254,7 +254,7 @@ MaiMbot-Pro-Max(第三方修改版)
try: try:
await event_manager.trigger_event(EventType.ON_START) await event_manager.trigger_event(EventType.ON_START,plugin_name="SYSTEM")
init_time = int(1000 * (time.time() - init_start_time)) init_time = int(1000 * (time.time() - init_start_time))
logger.info(f"初始化完成,神经元放电{init_time}") logger.info(f"初始化完成,神经元放电{init_time}")
except Exception as e: except Exception as e:

View File

@@ -1,313 +0,0 @@
# mmc/src/manager/monthly_plan_manager.py
import asyncio
from datetime import datetime, timedelta
from typing import List, Optional
from src.common.database.monthly_plan_db import (
add_new_plans,
get_archived_plans_for_month,
archive_active_plans_for_month,
has_active_plans
)
from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger
from src.manager.async_task_manager import AsyncTask, async_task_manager
logger = get_logger("monthly_plan_manager")
# 默认的月度计划生成指导原则
DEFAULT_MONTHLY_PLAN_GUIDELINES = """
我希望你能为自己制定一些有意义的月度小目标和计划。
这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。
每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
"""
class MonthlyPlanManager:
"""月度计划管理器
负责月度计划的生成、管理和生命周期控制。
与 ScheduleManager 解耦,专注于月度层面的计划管理。
"""
def __init__(self):
self.llm = LLMRequest(
model_set=model_config.model_task_config.schedule_generator,
request_type="monthly_plan"
)
self.generation_running = False
self.monthly_task_started = False
async def start_monthly_plan_generation(self):
"""启动每月初自动生成新月度计划的任务,并在启动时检查一次"""
if not self.monthly_task_started:
logger.info("正在启动每月月度计划生成任务...")
task = MonthlyPlanGenerationTask(self)
await async_task_manager.add_task(task)
self.monthly_task_started = True
logger.info("每月月度计划生成任务已成功启动。")
# 启动时立即检查并按需生成
logger.info("执行启动时月度计划检查...")
await self.ensure_and_generate_plans_if_needed()
else:
logger.info("每月月度计划生成任务已在运行中。")
async def ensure_and_generate_plans_if_needed(self, target_month: Optional[str] = None) -> bool:
"""
确保指定月份有计划,如果没有则触发生成。
这是按需生成的主要入口点。
"""
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
if not has_active_plans(target_month):
logger.info(f"{target_month} 没有任何有效的月度计划,将立即生成。")
return await self.generate_monthly_plans(target_month)
else:
# logger.info(f"{target_month} 已存在有效的月度计划,跳过生成。")
return True # 已经有计划,也算成功
async def generate_monthly_plans(self, target_month: Optional[str] = None) -> bool:
"""
生成指定月份的月度计划
:param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None则为当前月份。
:return: 是否生成成功
"""
if self.generation_running:
logger.info("月度计划生成任务已在运行中,跳过重复启动")
return False
self.generation_running = True
try:
# 确定目标月份
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
logger.info(f"开始为 {target_month} 生成月度计划...")
# 检查是否启用月度计划系统
if not global_config.monthly_plan_system or not global_config.monthly_plan_system.enable:
logger.info("月度计划系统已禁用,跳过计划生成。")
return False
# 获取上个月的归档计划作为参考
last_month = self._get_previous_month(target_month)
archived_plans = get_archived_plans_for_month(last_month)
# 构建生成 Prompt
prompt = self._build_generation_prompt(target_month, archived_plans)
# 调用 LLM 生成计划
plans = await self._generate_plans_with_llm(prompt)
if plans:
# 保存到数据库
add_new_plans(plans, target_month)
logger.info(f"成功为 {target_month} 生成并保存了 {len(plans)} 条月度计划。")
return True
else:
logger.warning(f"未能为 {target_month} 生成有效的月度计划。")
return False
except Exception as e:
logger.error(f"生成 {target_month} 月度计划时发生错误: {e}")
return False
finally:
self.generation_running = False
def _get_previous_month(self, current_month: str) -> str:
"""获取上个月的月份字符串"""
try:
year, month = map(int, current_month.split('-'))
if month == 1:
return f"{year-1}-12"
else:
return f"{year}-{month-1:02d}"
except Exception:
# 如果解析失败,返回一个不存在的月份
return "1900-01"
def _build_generation_prompt(self, target_month: str, archived_plans: List) -> str:
"""构建月度计划生成的 Prompt"""
# 获取配置
guidelines = getattr(global_config.monthly_plan_system, 'guidelines', None) or DEFAULT_MONTHLY_PLAN_GUIDELINES
personality = global_config.personality.personality_core
personality_side = global_config.personality.personality_side
max_plans = global_config.monthly_plan_system.max_plans_per_month
# 构建上月未完成计划的参考信息
archived_plans_block = ""
if archived_plans:
archived_texts = [f"- {plan.plan_text}" for plan in archived_plans[:5]] # 最多显示5个
archived_plans_block = f"""
**上个月未完成的一些计划(可作为参考)**:
{chr(10).join(archived_texts)}
你可以考虑是否要在这个月继续推进这些计划,或者制定全新的计划。
"""
prompt = f"""
我,{global_config.bot.nickname},需要为自己制定 {target_month} 的月度计划。
**关于我**:
- **核心人设**: {personality}
- **具体习惯与兴趣**:
{personality_side}
{archived_plans_block}
**我的月度计划制定原则**:
{guidelines}
**重要要求**:
1. 请为我生成 {max_plans} 条左右的月度计划
2. 每条计划都应该是一句话,简洁明了,具体可行
3. 计划应该涵盖不同的生活方面(学习、娱乐、社交、个人成长等)
4. 返回格式必须是纯文本,每行一条计划,不要使用 JSON 或其他格式
5. 不要包含任何解释性文字,只返回计划列表
**示例格式**:
学习一门新的编程语言或技术
每周至少看两部有趣的电影
与朋友们组织一次户外活动
阅读3本感兴趣的书籍
尝试制作一道新的料理
请你扮演我,以我的身份和兴趣,为 {target_month} 制定合适的月度计划。
"""
return prompt
async def _generate_plans_with_llm(self, prompt: str) -> List[str]:
"""使用 LLM 生成月度计划列表"""
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
logger.info(f"正在生成月度计划 (第 {attempt} 次尝试)")
response, _ = await self.llm.generate_response_async(prompt)
# 解析响应
plans = self._parse_plans_response(response)
if plans:
logger.info(f"成功生成 {len(plans)} 条月度计划")
return plans
else:
logger.warning(f"{attempt} 次生成的计划为空,继续重试...")
except Exception as e:
logger.error(f"{attempt} 次生成月度计划失败: {e}")
# 添加短暂延迟,避免过于频繁的请求
if attempt < max_retries:
await asyncio.sleep(2)
logger.error("所有尝试都失败,无法生成月度计划")
return []
def _parse_plans_response(self, response: str) -> List[str]:
"""解析 LLM 响应,提取计划列表"""
try:
# 清理响应文本
response = response.strip()
# 按行分割
lines = [line.strip() for line in response.split('\n') if line.strip()]
# 过滤掉明显不是计划的行(比如包含特殊标记的行)
plans = []
for line in lines:
# 跳过包含特殊标记的行
if any(marker in line for marker in ['**', '##', '```', '---', '===', '###']):
continue
# 移除可能的序号前缀
line = line.lstrip('0123456789.- ')
# 确保计划不为空且有意义
if len(line) > 5 and not line.startswith(('', '以上', '总结', '注意')):
plans.append(line)
# 限制计划数量
max_plans = global_config.monthly_plan_system.max_plans_per_month
if len(plans) > max_plans:
plans = plans[:max_plans]
return plans
except Exception as e:
logger.error(f"解析月度计划响应时发生错误: {e}")
return []
async def archive_current_month_plans(self, target_month: Optional[str] = None):
"""
归档当前月份的活跃计划
:param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None则为当前月份。
"""
try:
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
logger.info(f"开始归档 {target_month} 的活跃月度计划...")
archived_count = archive_active_plans_for_month(target_month)
logger.info(f"成功归档了 {archived_count}{target_month} 的月度计划。")
except Exception as e:
logger.error(f"归档 {target_month} 月度计划时发生错误: {e}")
class MonthlyPlanGenerationTask(AsyncTask):
"""每月初自动生成新月度计划的任务"""
def __init__(self, monthly_plan_manager: MonthlyPlanManager):
super().__init__(task_name="MonthlyPlanGenerationTask")
self.monthly_plan_manager = monthly_plan_manager
async def run(self):
while True:
try:
# 计算到下个月1号凌晨的时间
now = datetime.now()
# 获取下个月的第一天
if now.month == 12:
next_month = datetime(now.year + 1, 1, 1)
else:
next_month = datetime(now.year, now.month + 1, 1)
sleep_seconds = (next_month - now).total_seconds()
logger.info(f"下一次月度计划生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {next_month.strftime('%Y-%m-%d %H:%M:%S')})")
# 等待直到下个月1号
await asyncio.sleep(sleep_seconds)
# 先归档上个月的计划
last_month = (next_month - timedelta(days=1)).strftime("%Y-%m")
await self.monthly_plan_manager.archive_current_month_plans(last_month)
# 生成新月份的计划
current_month = next_month.strftime("%Y-%m")
logger.info(f"到达月初,开始生成 {current_month} 的月度计划...")
await self.monthly_plan_manager.generate_monthly_plans(current_month)
except asyncio.CancelledError:
logger.info("每月月度计划生成任务被取消。")
break
except Exception as e:
logger.error(f"每月月度计划生成任务发生未知错误: {e}")
# 发生错误后等待1小时再重试避免频繁失败
await asyncio.sleep(3600)
# 全局实例
monthly_plan_manager = MonthlyPlanManager()

View File

@@ -66,6 +66,11 @@ class ChatMood:
self.last_change_time: float = 0 self.last_change_time: float = 0
async def update_mood_by_message(self, message: MessageRecv, interested_rate: float): async def update_mood_by_message(self, message: MessageRecv, interested_rate: float):
# 如果当前聊天处于失眠状态,则锁定情绪,不允许更新
if self.chat_id in mood_manager.insomnia_chats:
logger.debug(f"{self.log_prefix} 处于失眠状态,情绪已锁定,跳过更新。")
return
self.regression_count = 0 self.regression_count = 0
during_last_time = message.message_info.time - self.last_change_time # type: ignore during_last_time = message.message_info.time - self.last_change_time # type: ignore
@@ -216,6 +221,7 @@ class MoodManager:
self.mood_list: list[ChatMood] = [] self.mood_list: list[ChatMood] = []
"""当前情绪状态""" """当前情绪状态"""
self.task_started: bool = False self.task_started: bool = False
self.insomnia_chats: set[str] = set() # 正在失眠的聊天ID列表
async def start(self): async def start(self):
"""启动情绪回归后台任务""" """启动情绪回归后台任务"""
@@ -262,6 +268,16 @@ class MoodManager:
mood.mood_state = "感觉很平静" mood.mood_state = "感觉很平静"
logger.info(f"{mood.log_prefix} 清除被吵醒的愤怒状态") logger.info(f"{mood.log_prefix} 清除被吵醒的愤怒状态")
def start_insomnia(self, chat_id: str):
"""开始一个聊天的失眠状态,锁定情绪更新"""
logger.info(f"Chat [{chat_id}]进入失眠状态,情绪已锁定。")
self.insomnia_chats.add(chat_id)
def stop_insomnia(self, chat_id: str):
"""停止一个聊天的失眠状态,解锁情绪更新"""
logger.info(f"Chat [{chat_id}]失眠状态结束,情绪已解锁。")
self.insomnia_chats.discard(chat_id)
def get_angry_prompt_addition(self, chat_id: str) -> str: def get_angry_prompt_addition(self, chat_id: str) -> str:
"""获取愤怒状态下的提示词补充""" """获取愤怒状态下的提示词补充"""
mood = self.get_mood_by_chat_id(chat_id) mood = self.get_mood_by_chat_id(chat_id)

View File

@@ -17,6 +17,7 @@ from .base import (
ComponentInfo, ComponentInfo,
ActionInfo, ActionInfo,
CommandInfo, CommandInfo,
PlusCommandInfo,
PluginInfo, PluginInfo,
ToolInfo, ToolInfo,
PythonDependency, PythonDependency,
@@ -25,6 +26,12 @@ from .base import (
EventType, EventType,
MaiMessages, MaiMessages,
ToolParamType, ToolParamType,
# 新增的增强命令系统
PlusCommand,
CommandArgs,
PlusCommandAdapter,
create_plus_command_adapter,
ChatType,
) )
# 导入工具模块 # 导入工具模块
@@ -81,10 +88,17 @@ __all__ = [
"BaseCommand", "BaseCommand",
"BaseTool", "BaseTool",
"BaseEventHandler", "BaseEventHandler",
# 增强命令系统
"PlusCommand",
"CommandArgs",
"PlusCommandAdapter",
"create_plus_command_adapter",
"create_plus_command_adapter",
# 类型定义 # 类型定义
"ComponentType", "ComponentType",
"ActionActivationType", "ActionActivationType",
"ChatMode", "ChatMode",
"ChatType",
"ComponentInfo", "ComponentInfo",
"ActionInfo", "ActionInfo",
"CommandInfo", "CommandInfo",

View File

@@ -1,4 +1,4 @@
from typing import Optional, Type from typing import Any, Dict, List, Optional, Type
from src.plugin_system.base.base_tool import BaseTool from src.plugin_system.base.base_tool import BaseTool
from src.plugin_system.base.component_types import ComponentType from src.plugin_system.base.component_types import ComponentType
@@ -31,4 +31,4 @@ def get_llm_available_tool_definitions():
from src.plugin_system.core import component_registry from src.plugin_system.core import component_registry
llm_available_tools = component_registry.get_llm_available_tools() 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()] return [(name, tool_class.get_tool_definition()) for name, tool_class in llm_available_tools.items()]

View File

@@ -13,9 +13,11 @@ from .component_types import (
ComponentType, ComponentType,
ActionActivationType, ActionActivationType,
ChatMode, ChatMode,
ChatType,
ComponentInfo, ComponentInfo,
ActionInfo, ActionInfo,
CommandInfo, CommandInfo,
PlusCommandInfo,
ToolInfo, ToolInfo,
PluginInfo, PluginInfo,
PythonDependency, PythonDependency,
@@ -25,6 +27,8 @@ from .component_types import (
ToolParamType, ToolParamType,
) )
from .config_types import ConfigField from .config_types import ConfigField
from .plus_command import PlusCommand, PlusCommandAdapter, create_plus_command_adapter
from .command_args import CommandArgs
__all__ = [ __all__ = [
"BasePlugin", "BasePlugin",
@@ -34,9 +38,11 @@ __all__ = [
"ComponentType", "ComponentType",
"ActionActivationType", "ActionActivationType",
"ChatMode", "ChatMode",
"ChatType",
"ComponentInfo", "ComponentInfo",
"ActionInfo", "ActionInfo",
"CommandInfo", "CommandInfo",
"PlusCommandInfo",
"ToolInfo", "ToolInfo",
"PluginInfo", "PluginInfo",
"PythonDependency", "PythonDependency",
@@ -46,4 +52,9 @@ __all__ = [
"BaseEventHandler", "BaseEventHandler",
"MaiMessages", "MaiMessages",
"ToolParamType", "ToolParamType",
# 增强命令系统
"PlusCommand",
"CommandArgs",
"PlusCommandAdapter",
"create_plus_command_adapter",
] ]

View File

@@ -1,19 +1,20 @@
import asyncio
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from src.common.logger import get_logger from src.common.logger import get_logger
logger = get_logger("base_event") logger = get_logger("base_event")
class HandlerResult: class HandlerResult:
"""事件处理器执行结果 """事件处理器执行结果
所有事件处理器必须返回此类的实例 所有事件处理器必须返回此类的实例
""" """
def __init__(self, success: bool, continue_process: bool, message: str = "", handler_name: str = ""): def __init__(self, success: bool, continue_process: bool, message: Any = {}, handler_name: str = ""):
self.success = success self.success = success
self.continue_process = continue_process self.continue_process = continue_process
self.message = message self.message = message
self.handler_name = handler_name self.handler_name = handler_name
def __repr__(self): def __repr__(self):
return f"HandlerResult(success={self.success}, continue_process={self.continue_process}, message='{self.message}', handler_name='{self.handler_name}')" return f"HandlerResult(success={self.success}, continue_process={self.continue_process}, message='{self.message}', handler_name='{self.handler_name}')"
@@ -66,13 +67,22 @@ class HandlerResultsCollection:
} }
class BaseEvent: class BaseEvent:
def __init__(self, name: str): def __init__(
self,
name: str,
allowed_subscribers: List[str]=[],
allowed_triggers: List[str]=[]
):
self.name = name self.name = name
self.enabled = True self.enabled = True
self.allowed_subscribers = allowed_subscribers # 记录事件处理器名
self.allowed_triggers = allowed_triggers # 记录插件名
from src.plugin_system.base.base_events_handler import BaseEventHandler from src.plugin_system.base.base_events_handler import BaseEventHandler
self.subscribers: List["BaseEventHandler"] = [] # 订阅该事件的事件处理器列表 self.subscribers: List["BaseEventHandler"] = [] # 订阅该事件的事件处理器列表
self.event_handle_lock = asyncio.Lock()
def __name__(self): def __name__(self):
return self.name return self.name
@@ -88,22 +98,45 @@ class BaseEvent:
if not self.enabled: if not self.enabled:
return HandlerResultsCollection([]) return HandlerResultsCollection([])
# 按权重从高到低排序订阅者 # 使用锁确保同一个事件不能同时激活多次
# 使用直接属性访问,-1代表自动权重 async with self.event_handle_lock:
sorted_subscribers = sorted(self.subscribers, key=lambda h: h.weight if hasattr(h, 'weight') and h.weight != -1 else 0, reverse=True) # 按权重从高到低排序订阅者
# 使用直接属性访问,-1代表自动权重
results = [] sorted_subscribers = sorted(self.subscribers, key=lambda h: h.weight if hasattr(h, 'weight') and h.weight != -1 else 0, reverse=True)
for subscriber in sorted_subscribers:
try: # 并行执行所有订阅者
result = await subscriber.execute(params) tasks = []
if not result.handler_name: for subscriber in sorted_subscribers:
# 补充handler_name # 为每个订阅者创建执行任务
result.handler_name = subscriber.handler_name if hasattr(subscriber, 'handler_name') else subscriber.__class__.__name__ task = self._execute_subscriber(subscriber, params)
results.append(result) tasks.append(task)
except Exception as e:
# 处理执行异常 # 等待所有任务完成
results = await asyncio.gather(*tasks, return_exceptions=True)
# 处理执行结果
processed_results = []
for i, result in enumerate(results):
subscriber = sorted_subscribers[i]
handler_name = subscriber.handler_name if hasattr(subscriber, 'handler_name') else subscriber.__class__.__name__ handler_name = subscriber.handler_name if hasattr(subscriber, 'handler_name') else subscriber.__class__.__name__
logger.error(f"事件处理器 {handler_name} 执行失败: {e}")
results.append(HandlerResult(False, True, str(e), handler_name)) if isinstance(result, Exception):
# 处理执行异常
return HandlerResultsCollection(results) logger.error(f"事件处理器 {handler_name} 执行失败: {result}")
processed_results.append(HandlerResult(False, True, str(result), handler_name))
else:
# 正常执行结果
if not result.handler_name:
# 补充handler_name
result.handler_name = handler_name
processed_results.append(result)
return HandlerResultsCollection(processed_results)
async def _execute_subscriber(self, subscriber, params: dict) -> HandlerResult:
"""执行单个订阅者处理器"""
try:
return await subscriber.execute(params)
except Exception as e:
# 异常会在 gather 中捕获,这里直接抛出让 gather 处理
raise e

View File

@@ -1,13 +1,14 @@
from abc import abstractmethod from abc import abstractmethod
from typing import List, Type, Tuple, Union from typing import List, Type, Tuple, Union, TYPE_CHECKING
from .plugin_base import PluginBase from .plugin_base import PluginBase
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.base.component_types import ActionInfo, CommandInfo, EventHandlerInfo, ToolInfo from src.plugin_system.base.component_types import ActionInfo, CommandInfo, PlusCommandInfo, EventHandlerInfo, ToolInfo
from .base_action import BaseAction from .base_action import BaseAction
from .base_command import BaseCommand from .base_command import BaseCommand
from .base_events_handler import BaseEventHandler from .base_events_handler import BaseEventHandler
from .base_tool import BaseTool from .base_tool import BaseTool
from .plus_command import PlusCommand
logger = get_logger("base_plugin") logger = get_logger("base_plugin")
@@ -31,6 +32,7 @@ class BasePlugin(PluginBase):
Union[ Union[
Tuple[ActionInfo, Type[BaseAction]], Tuple[ActionInfo, Type[BaseAction]],
Tuple[CommandInfo, Type[BaseCommand]], Tuple[CommandInfo, Type[BaseCommand]],
Tuple[PlusCommandInfo, Type[PlusCommand]],
Tuple[EventHandlerInfo, Type[BaseEventHandler]], Tuple[EventHandlerInfo, Type[BaseEventHandler]],
Tuple[ToolInfo, Type[BaseTool]], Tuple[ToolInfo, Type[BaseTool]],
] ]

View File

@@ -28,6 +28,15 @@ class BaseTool(ABC):
""" """
available_for_llm: bool = False available_for_llm: bool = False
"""是否可供LLM使用""" """是否可供LLM使用"""
history_ttl: int = 5
"""工具调用历史记录的TTL值默认为5。设为0表示不记录历史"""
enable_cache: bool = False
"""是否为该工具启用缓存"""
cache_ttl: int = 3600
"""缓存的TTL值默认为3600秒1小时"""
semantic_cache_query_key: Optional[str] = None
"""用于语义缓存的查询参数键名。如果设置,将使用此参数的值进行语义相似度搜索"""
def __init__(self, plugin_config: Optional[dict] = None): def __init__(self, plugin_config: Optional[dict] = None):
self.plugin_config = plugin_config or {} # 直接存储插件配置字典 self.plugin_config = plugin_config or {} # 直接存储插件配置字典

View File

@@ -0,0 +1,158 @@
"""命令参数解析类
提供简单易用的命令参数解析功能
"""
from typing import List, Optional
import shlex
class CommandArgs:
"""命令参数解析类
提供方便的方法来处理命令参数
"""
def __init__(self, raw_args: str = ""):
"""初始化命令参数
Args:
raw_args: 原始参数字符串
"""
self._raw_args = raw_args.strip()
self._parsed_args: Optional[List[str]] = None
def get_raw(self) -> str:
"""获取完整的参数字符串
Returns:
str: 原始参数字符串
"""
return self._raw_args
def get_args(self) -> List[str]:
"""获取解析后的参数列表
将参数按空格分割,支持引号包围的参数
Returns:
List[str]: 参数列表
"""
if self._parsed_args is None:
if not self._raw_args:
self._parsed_args = []
else:
try:
# 使用shlex来正确处理引号和转义字符
self._parsed_args = shlex.split(self._raw_args)
except ValueError:
# 如果shlex解析失败fallback到简单的split
self._parsed_args = self._raw_args.split()
return self._parsed_args
@property
def is_empty(self) -> bool:
"""检查参数是否为空
Returns:
bool: 如果没有参数返回True
"""
return len(self.get_args()) == 0
def get_arg(self, index: int, default: str = "") -> str:
"""获取指定索引的参数
Args:
index: 参数索引从0开始
default: 默认值
Returns:
str: 参数值或默认值
"""
args = self.get_args()
if 0 <= index < len(args):
return args[index]
return default
@property
def get_first(self, default: str = "") -> str:
"""获取第一个参数
Args:
default: 默认值
Returns:
str: 第一个参数或默认值
"""
return self.get_arg(0, default)
def get_remaining(self, start_index: int = 0) -> str:
"""获取从指定索引开始的剩余参数字符串
Args:
start_index: 起始索引
Returns:
str: 剩余参数组成的字符串
"""
args = self.get_args()
if start_index < len(args):
return " ".join(args[start_index:])
return ""
def count(self) -> int:
"""获取参数数量
Returns:
int: 参数数量
"""
return len(self.get_args())
def has_flag(self, flag: str) -> bool:
"""检查是否包含指定的标志参数
Args:
flag: 标志名(如 "--verbose""-v"
Returns:
bool: 如果包含该标志返回True
"""
return flag in self.get_args()
def get_flag_value(self, flag: str, default: str = "") -> str:
"""获取标志参数的值
查找 --key=value 或 --key value 形式的参数
Args:
flag: 标志名(如 "--output"
default: 默认值
Returns:
str: 标志的值或默认值
"""
args = self.get_args()
# 查找 --key=value 形式
for arg in args:
if arg.startswith(f"{flag}="):
return arg[len(flag) + 1:]
# 查找 --key value 形式
try:
flag_index = args.index(flag)
if flag_index + 1 < len(args):
return args[flag_index + 1]
except ValueError:
pass
return default
def __str__(self) -> str:
"""字符串表示"""
return self._raw_args
def __repr__(self) -> str:
"""调试表示"""
return f"CommandArgs(raw='{self._raw_args}', parsed={self.get_args()})"

View File

@@ -12,6 +12,7 @@ class ComponentType(Enum):
ACTION = "action" # 动作组件 ACTION = "action" # 动作组件
COMMAND = "command" # 命令组件 COMMAND = "command" # 命令组件
PLUS_COMMAND = "plus_command" # 增强命令组件
TOOL = "tool" # 工具组件 TOOL = "tool" # 工具组件
SCHEDULER = "scheduler" # 定时任务组件(预留) SCHEDULER = "scheduler" # 定时任务组件(预留)
EVENT_HANDLER = "event_handler" # 事件处理组件 EVENT_HANDLER = "event_handler" # 事件处理组件
@@ -164,6 +165,22 @@ class CommandInfo(ComponentInfo):
self.component_type = ComponentType.COMMAND self.component_type = ComponentType.COMMAND
@dataclass
class PlusCommandInfo(ComponentInfo):
"""增强命令组件信息"""
command_aliases: List[str] = field(default_factory=list) # 命令别名列表
priority: int = 0 # 命令优先级
chat_type_allow: ChatType = ChatType.ALL # 允许的聊天类型
intercept_message: bool = False # 是否拦截消息
def __post_init__(self):
super().__post_init__()
if self.command_aliases is None:
self.command_aliases = []
self.component_type = ComponentType.PLUS_COMMAND
@dataclass @dataclass
class ToolInfo(ComponentInfo): class ToolInfo(ComponentInfo):
"""工具组件信息""" """工具组件信息"""

View File

@@ -0,0 +1,459 @@
"""增强版命令处理器
提供更简单易用的命令处理方式,无需手写正则表达式
"""
from abc import ABC, abstractmethod
from typing import Dict, Tuple, Optional, List
import re
from src.common.logger import get_logger
from src.plugin_system.base.component_types import CommandInfo, PlusCommandInfo, ComponentType, ChatType
from src.chat.message_receive.message import MessageRecv
from src.plugin_system.apis import send_api
from src.plugin_system.base.command_args import CommandArgs
from src.plugin_system.base.base_command import BaseCommand
from src.config.config import global_config
logger = get_logger("plus_command")
class PlusCommand(ABC):
"""增强版命令基类
提供更简单的命令定义方式,无需手写正则表达式
子类只需要定义:
- command_name: 命令名称
- command_description: 命令描述
- command_aliases: 命令别名列表(可选)
- priority: 优先级(可选,数字越大优先级越高)
- chat_type_allow: 允许的聊天类型(可选)
- intercept_message: 是否拦截消息(可选)
"""
# 子类需要定义的属性
command_name: str = ""
"""命令名称,如 'echo'"""
command_description: str = ""
"""命令描述"""
command_aliases: List[str] = []
"""命令别名列表,如 ['say', 'repeat']"""
priority: int = 0
"""命令优先级,数字越大优先级越高"""
chat_type_allow: ChatType = ChatType.ALL
"""允许的聊天类型"""
intercept_message: bool = False
"""是否拦截消息,不进行后续处理"""
def __init__(self, message: MessageRecv, plugin_config: Optional[dict] = None):
"""初始化命令组件
Args:
message: 接收到的消息对象
plugin_config: 插件配置字典
"""
self.message = message
self.plugin_config = plugin_config or {}
self.log_prefix = "[PlusCommand]"
# 解析命令参数
self._parse_command()
# 验证聊天类型限制
if not self._validate_chat_type():
is_group = hasattr(self.message, 'is_group_message') and self.message.is_group_message
logger.warning(
f"{self.log_prefix} 命令 '{self.command_name}' 不支持当前聊天类型: "
f"{'群聊' if is_group else '私聊'}, 允许类型: {self.chat_type_allow.value}"
)
def _parse_command(self) -> None:
"""解析命令和参数"""
if not hasattr(self.message, 'plain_text') or not self.message.plain_text:
self.args = CommandArgs("")
return
plain_text = self.message.plain_text.strip()
# 获取配置的命令前缀
prefixes = global_config.command.command_prefixes
# 检查是否以任何前缀开头
matched_prefix = None
for prefix in prefixes:
if plain_text.startswith(prefix):
matched_prefix = prefix
break
if not matched_prefix:
self.args = CommandArgs("")
return
# 移除前缀
command_part = plain_text[len(matched_prefix):].strip()
# 分离命令名和参数
parts = command_part.split(None, 1)
if not parts:
self.args = CommandArgs("")
return
command_word = parts[0].lower()
args_text = parts[1] if len(parts) > 1 else ""
# 检查命令名是否匹配
all_commands = [self.command_name.lower()] + [alias.lower() for alias in self.command_aliases]
if command_word not in all_commands:
self.args = CommandArgs("")
return
# 创建参数对象
self.args = CommandArgs(args_text)
def _validate_chat_type(self) -> bool:
"""验证当前聊天类型是否允许执行此命令
Returns:
bool: 如果允许执行返回True否则返回False
"""
if self.chat_type_allow == ChatType.ALL:
return True
# 检查是否为群聊消息
is_group = hasattr(self.message, 'is_group_message') and self.message.is_group_message
if self.chat_type_allow == ChatType.GROUP and is_group:
return True
elif self.chat_type_allow == ChatType.PRIVATE and not is_group:
return True
else:
return False
def is_chat_type_allowed(self) -> bool:
"""检查当前聊天类型是否允许执行此命令
Returns:
bool: 如果允许执行返回True否则返回False
"""
return self._validate_chat_type()
def is_command_match(self) -> bool:
"""检查当前消息是否匹配此命令
Returns:
bool: 如果匹配返回True
"""
return not self.args.is_empty() or self._is_exact_command_call()
def _is_exact_command_call(self) -> bool:
"""检查是否是精确的命令调用(无参数)"""
if not hasattr(self.message, 'plain_text') or not self.message.plain_text:
return False
plain_text = self.message.plain_text.strip()
# 获取配置的命令前缀
prefixes = global_config.command.command_prefixes
# 检查每个前缀
for prefix in prefixes:
if plain_text.startswith(prefix):
command_part = plain_text[len(prefix):].strip()
all_commands = [self.command_name.lower()] + [alias.lower() for alias in self.command_aliases]
if command_part.lower() in all_commands:
return True
return False
@abstractmethod
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
"""执行命令的抽象方法,子类必须实现
Args:
args: 解析后的命令参数
Returns:
Tuple[bool, Optional[str], bool]: (是否执行成功, 可选的回复消息, 是否拦截消息)
"""
pass
def get_config(self, key: str, default=None):
"""获取插件配置值,使用嵌套键访问
Args:
key: 配置键名,使用嵌套访问如 "section.subsection.key"
default: 默认值
Returns:
Any: 配置值或默认值
"""
if not self.plugin_config:
return default
# 支持嵌套键访问
keys = key.split(".")
current = self.plugin_config
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return current
async def send_text(self, content: str, reply_to: str = "") -> bool:
"""发送回复消息
Args:
content: 回复内容
reply_to: 回复消息,格式为"发送者:消息内容"
Returns:
bool: 是否发送成功
"""
# 获取聊天流信息
chat_stream = self.message.chat_stream
if not chat_stream or not hasattr(chat_stream, "stream_id"):
logger.error(f"{self.log_prefix} 缺少聊天流或stream_id")
return False
return await send_api.text_to_stream(text=content, stream_id=chat_stream.stream_id, reply_to=reply_to)
async def send_type(
self, message_type: str, content: str, display_message: str = "", typing: bool = False, reply_to: str = ""
) -> bool:
"""发送指定类型的回复消息到当前聊天环境
Args:
message_type: 消息类型,如"text""image""emoji"
content: 消息内容
display_message: 显示消息(可选)
typing: 是否显示正在输入
reply_to: 回复消息,格式为"发送者:消息内容"
Returns:
bool: 是否发送成功
"""
# 获取聊天流信息
chat_stream = self.message.chat_stream
if not chat_stream or not hasattr(chat_stream, "stream_id"):
logger.error(f"{self.log_prefix} 缺少聊天流或stream_id")
return False
return await send_api.custom_to_stream(
message_type=message_type,
content=content,
stream_id=chat_stream.stream_id,
display_message=display_message,
typing=typing,
reply_to=reply_to,
)
async def send_emoji(self, emoji_base64: str) -> bool:
"""发送表情包
Args:
emoji_base64: 表情包的base64编码
Returns:
bool: 是否发送成功
"""
chat_stream = self.message.chat_stream
if not chat_stream or not hasattr(chat_stream, "stream_id"):
logger.error(f"{self.log_prefix} 缺少聊天流或stream_id")
return False
return await send_api.emoji_to_stream(emoji_base64, chat_stream.stream_id)
async def send_image(self, image_base64: str) -> bool:
"""发送图片
Args:
image_base64: 图片的base64编码
Returns:
bool: 是否发送成功
"""
chat_stream = self.message.chat_stream
if not chat_stream or not hasattr(chat_stream, "stream_id"):
logger.error(f"{self.log_prefix} 缺少聊天流或stream_id")
return False
return await send_api.image_to_stream(image_base64, chat_stream.stream_id)
@classmethod
def get_command_info(cls) -> "CommandInfo":
"""从类属性生成CommandInfo
Returns:
CommandInfo: 生成的命令信息对象
"""
if "." in cls.command_name:
logger.error(f"命令名称 '{cls.command_name}' 包含非法字符 '.',请使用下划线替代")
raise ValueError(f"命令名称 '{cls.command_name}' 包含非法字符 '.',请使用下划线替代")
# 生成正则表达式模式来匹配命令
command_pattern = cls._generate_command_pattern()
return CommandInfo(
name=cls.command_name,
component_type=ComponentType.COMMAND,
description=cls.command_description,
command_pattern=command_pattern,
chat_type_allow=getattr(cls, "chat_type_allow", ChatType.ALL),
)
@classmethod
def get_plus_command_info(cls) -> "PlusCommandInfo":
"""从类属性生成PlusCommandInfo
Returns:
PlusCommandInfo: 生成的增强命令信息对象
"""
if "." in cls.command_name:
logger.error(f"命令名称 '{cls.command_name}' 包含非法字符 '.',请使用下划线替代")
raise ValueError(f"命令名称 '{cls.command_name}' 包含非法字符 '.',请使用下划线替代")
return PlusCommandInfo(
name=cls.command_name,
component_type=ComponentType.PLUS_COMMAND,
description=cls.command_description,
command_aliases=getattr(cls, "command_aliases", []),
priority=getattr(cls, "priority", 0),
chat_type_allow=getattr(cls, "chat_type_allow", ChatType.ALL),
intercept_message=getattr(cls, "intercept_message", False),
)
@classmethod
def _generate_command_pattern(cls) -> str:
"""生成命令匹配的正则表达式
Returns:
str: 正则表达式字符串
"""
# 获取所有可能的命令名(主命令名 + 别名)
all_commands = [cls.command_name] + getattr(cls, 'command_aliases', [])
# 转义特殊字符并创建选择组
escaped_commands = [re.escape(cmd) for cmd in all_commands]
commands_pattern = "|".join(escaped_commands)
# 获取默认前缀列表(这里先用硬编码,后续可以优化为动态获取)
default_prefixes = ["/", "!", ".", "#"]
escaped_prefixes = [re.escape(prefix) for prefix in default_prefixes]
prefixes_pattern = "|".join(escaped_prefixes)
# 生成完整的正则表达式
# 匹配: [前缀][命令名][可选空白][任意参数]
pattern = f"^(?P<prefix>{prefixes_pattern})(?P<command>{commands_pattern})(?P<args>\\s.*)?$"
return pattern
class PlusCommandAdapter(BaseCommand):
"""PlusCommand适配器
将PlusCommand适配到现有的插件系统继承BaseCommand
"""
def __init__(self, plus_command_class, message: MessageRecv, plugin_config: Optional[dict] = None):
"""初始化适配器
Args:
plus_command_class: PlusCommand子类
message: 消息对象
plugin_config: 插件配置
"""
# 先设置必要的类属性
self.command_name = plus_command_class.command_name
self.command_description = plus_command_class.command_description
self.command_pattern = plus_command_class._generate_command_pattern()
self.chat_type_allow = getattr(plus_command_class, "chat_type_allow", ChatType.ALL)
self.priority = getattr(plus_command_class, "priority", 0)
self.intercept_message = getattr(plus_command_class, "intercept_message", False)
# 调用父类初始化
super().__init__(message, plugin_config)
# 创建PlusCommand实例
self.plus_command = plus_command_class(message, plugin_config)
async def execute(self) -> Tuple[bool, Optional[str], bool]:
"""执行命令
Returns:
Tuple[bool, Optional[str], bool]: 执行结果
"""
# 检查命令是否匹配
if not self.plus_command.is_command_match():
return False, "命令不匹配", False
# 检查聊天类型权限
if not self.plus_command.is_chat_type_allowed():
return False, "不支持当前聊天类型", self.intercept_message
# 执行命令
try:
return await self.plus_command.execute(self.plus_command.args)
except Exception as e:
logger.error(f"执行命令时出错: {e}", exc_info=True)
return False, f"命令执行出错: {str(e)}", self.intercept_message
def create_plus_command_adapter(plus_command_class):
"""创建PlusCommand适配器的工厂函数
Args:
plus_command_class: PlusCommand子类
Returns:
适配器类
"""
class AdapterClass(BaseCommand):
command_name = plus_command_class.command_name
command_description = plus_command_class.command_description
command_pattern = plus_command_class._generate_command_pattern()
chat_type_allow = getattr(plus_command_class, "chat_type_allow", ChatType.ALL)
def __init__(self, message: MessageRecv, plugin_config: Optional[dict] = None):
super().__init__(message, plugin_config)
self.plus_command = plus_command_class(message, plugin_config)
self.priority = getattr(plus_command_class, "priority", 0)
self.intercept_message = getattr(plus_command_class, "intercept_message", False)
async def execute(self) -> Tuple[bool, Optional[str], bool]:
"""执行命令"""
# 从BaseCommand的正则匹配结果中提取参数
args_text = ""
if hasattr(self, 'matched_groups') and self.matched_groups:
# 从正则匹配组中获取参数部分
args_match = self.matched_groups.get('args', '')
if args_match:
args_text = args_match.strip()
# 创建CommandArgs对象
command_args = CommandArgs(args_text)
# 检查聊天类型权限
if not self.plus_command.is_chat_type_allowed():
return False, "不支持当前聊天类型", self.intercept_message
# 执行命令,传递正确解析的参数
try:
return await self.plus_command.execute(command_args)
except Exception as e:
logger.error(f"执行命令时出错: {e}", exc_info=True)
return False, f"命令执行出错: {str(e)}", self.intercept_message
return AdapterClass
# 兼容旧的命名
PlusCommandAdapter = create_plus_command_adapter

View File

View File

@@ -8,6 +8,7 @@ from src.plugin_system.base.component_types import (
ActionInfo, ActionInfo,
ToolInfo, ToolInfo,
CommandInfo, CommandInfo,
PlusCommandInfo,
EventHandlerInfo, EventHandlerInfo,
PluginInfo, PluginInfo,
ComponentType, ComponentType,
@@ -16,6 +17,7 @@ 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_tool import BaseTool
from src.plugin_system.base.base_events_handler import BaseEventHandler from src.plugin_system.base.base_events_handler import BaseEventHandler
from src.plugin_system.base.plus_command import PlusCommand
logger = get_logger("component_registry") logger = get_logger("component_registry")
@@ -32,7 +34,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, BaseTool, BaseEventHandler]]] = {} self._components_classes: Dict[str, Type[Union[BaseCommand, BaseAction, BaseTool, BaseEventHandler, PlusCommand]]] = {}
"""命名空间式组件名 -> 组件类""" """命名空间式组件名 -> 组件类"""
# 插件注册表 # 插件注册表
@@ -133,6 +135,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.PLUS_COMMAND:
assert isinstance(component_info, PlusCommandInfo)
assert issubclass(component_class, PlusCommand)
ret = self._register_plus_command_component(component_info, component_class)
case ComponentType.TOOL: case ComponentType.TOOL:
assert isinstance(component_info, ToolInfo) assert isinstance(component_info, ToolInfo)
assert issubclass(component_class, BaseTool) assert issubclass(component_class, BaseTool)
@@ -192,6 +198,26 @@ class ComponentRegistry:
return True return True
def _register_plus_command_component(self, plus_command_info: PlusCommandInfo, plus_command_class: Type[PlusCommand]) -> bool:
"""注册PlusCommand组件到特定注册表"""
plus_command_name = plus_command_info.name
if not plus_command_name:
logger.error(f"PlusCommand组件 {plus_command_class.__name__} 必须指定名称")
return False
if not isinstance(plus_command_info, PlusCommandInfo) or not issubclass(plus_command_class, PlusCommand):
logger.error(f"注册失败: {plus_command_name} 不是有效的PlusCommand")
return False
# 创建专门的PlusCommand注册表如果还没有
if not hasattr(self, '_plus_command_registry'):
self._plus_command_registry: Dict[str, Type[PlusCommand]] = {}
self._plus_command_registry[plus_command_name] = plus_command_class
logger.debug(f"已注册PlusCommand组件: {plus_command_name}")
return True
def _register_tool_component(self, tool_info: ToolInfo, tool_class: Type[BaseTool]) -> bool: def _register_tool_component(self, tool_info: ToolInfo, tool_class: Type[BaseTool]) -> bool:
"""注册Tool组件到Tool特定注册表""" """注册Tool组件到Tool特定注册表"""
tool_name = tool_info.name tool_name = tool_info.name
@@ -248,6 +274,12 @@ class ComponentRegistry:
self._command_patterns.pop(key, None) self._command_patterns.pop(key, None)
logger.debug(f"已移除Command组件: {component_name} (清理了 {len(keys_to_remove)} 个模式)") logger.debug(f"已移除Command组件: {component_name} (清理了 {len(keys_to_remove)} 个模式)")
case ComponentType.PLUS_COMMAND:
# 移除PlusCommand注册
if hasattr(self, '_plus_command_registry'):
self._plus_command_registry.pop(component_name, None)
logger.debug(f"已移除PlusCommand组件: {component_name}")
case ComponentType.TOOL: case ComponentType.TOOL:
# 移除Tool注册 # 移除Tool注册
self._tool_registry.pop(component_name, None) self._tool_registry.pop(component_name, None)
@@ -520,21 +552,23 @@ class ComponentRegistry:
text: 输入文本 text: 输入文本
Returns: Returns:
Tuple: (命令类, 匹配的命名组, 是否拦截消息, 插件名) 或 None Tuple: (命令类, 匹配的命名组, 命令信息) 或 None
""" """
# 只查找传统的BaseCommand
candidates = [pattern for pattern in self._command_patterns if pattern.match(text)] candidates = [pattern for pattern in self._command_patterns if pattern.match(text)]
if not candidates: if candidates:
return None if len(candidates) > 1:
if len(candidates) > 1: logger.warning(f"文本 '{text}' 匹配到多个命令模式: {candidates},使用第一个匹配")
logger.warning(f"文本 '{text}' 匹配到多个命令模式: {candidates},使用第一个匹配") command_name = self._command_patterns[candidates[0]]
command_name = self._command_patterns[candidates[0]] command_info: CommandInfo = self.get_registered_command_info(command_name) # type: ignore
command_info: CommandInfo = self.get_registered_command_info(command_name) # type: ignore return (
return ( self._command_registry[command_name],
self._command_registry[command_name], candidates[0].match(text).groupdict(), # type: ignore
candidates[0].match(text).groupdict(), # type: ignore command_info,
command_info, )
)
return None
# === Tool 特定查询方法 === # === Tool 特定查询方法 ===
def get_tool_registry(self) -> Dict[str, Type[BaseTool]]: def get_tool_registry(self) -> Dict[str, Type[BaseTool]]:
@@ -557,6 +591,25 @@ class ComponentRegistry:
info = self.get_component_info(tool_name, ComponentType.TOOL) info = self.get_component_info(tool_name, ComponentType.TOOL)
return info if isinstance(info, ToolInfo) else None return info if isinstance(info, ToolInfo) else None
# === PlusCommand 特定查询方法 ===
def get_plus_command_registry(self) -> Dict[str, Type[PlusCommand]]:
"""获取PlusCommand注册表"""
if not hasattr(self, '_plus_command_registry'):
self._plus_command_registry: Dict[str, Type[PlusCommand]] = {}
return self._plus_command_registry.copy()
def get_registered_plus_command_info(self, command_name: str) -> Optional[PlusCommandInfo]:
"""获取PlusCommand信息
Args:
command_name: 命令名称
Returns:
PlusCommandInfo: 命令信息对象,如果命令不存在则返回 None
"""
info = self.get_component_info(command_name, ComponentType.PLUS_COMMAND)
return info if isinstance(info, PlusCommandInfo) else None
# === EventHandler 特定查询方法 === # === EventHandler 特定查询方法 ===
def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]: def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]:
@@ -612,6 +665,7 @@ class ComponentRegistry:
command_components: int = 0 command_components: int = 0
tool_components: int = 0 tool_components: int = 0
events_handlers: int = 0 events_handlers: int = 0
plus_command_components: 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
@@ -621,11 +675,14 @@ class ComponentRegistry:
tool_components += 1 tool_components += 1
elif component.component_type == ComponentType.EVENT_HANDLER: elif component.component_type == ComponentType.EVENT_HANDLER:
events_handlers += 1 events_handlers += 1
elif component.component_type == ComponentType.PLUS_COMMAND:
plus_command_components += 1
return { return {
"action_components": action_components, "action_components": action_components,
"command_components": command_components, "command_components": command_components,
"tool_components": tool_components, "tool_components": tool_components,
"event_handlers": events_handlers, "event_handlers": events_handlers,
"plus_command_components": plus_command_components,
"total_components": len(self._components), "total_components": len(self._components),
"total_plugins": len(self._plugins), "total_plugins": len(self._plugins),
"components_by_type": { "components_by_type": {

View File

@@ -2,7 +2,6 @@
事件管理器 - 实现Event和EventHandler的单例管理 事件管理器 - 实现Event和EventHandler的单例管理
提供统一的事件注册、管理和触发接口 提供统一的事件注册、管理和触发接口
""" """
from typing import Dict, Type, List, Optional, Any, Union from typing import Dict, Type, List, Optional, Any, Union
from threading import Lock from threading import Lock
@@ -41,12 +40,18 @@ class EventManager:
self._initialized = True self._initialized = True
logger.info("EventManager 单例初始化完成") logger.info("EventManager 单例初始化完成")
def register_event(self, event_name: Union[EventType, str]) -> bool: def register_event(
self,
event_name: Union[EventType, str],
allowed_subscribers: List[str]=[],
allowed_triggers: List[str]=[]
) -> bool:
"""注册一个新的事件 """注册一个新的事件
Args: Args:
event_name Union[EventType, str]: 事件名称 event_name Union[EventType, str]: 事件名称
allowed_subscribers: List[str]: 事件订阅者白名单,
allowed_triggers: List[str]: 事件触发插件白名单
Returns: Returns:
bool: 注册成功返回True已存在返回False bool: 注册成功返回True已存在返回False
""" """
@@ -54,7 +59,7 @@ class EventManager:
logger.warning(f"事件 {event_name} 已存在,跳过注册") logger.warning(f"事件 {event_name} 已存在,跳过注册")
return False return False
event = BaseEvent(event_name) event = BaseEvent(event_name,allowed_subscribers,allowed_triggers)
self._events[event_name] = event self._events[event_name] = event
logger.info(f"事件 {event_name} 注册成功") logger.info(f"事件 {event_name} 注册成功")
@@ -211,7 +216,12 @@ class EventManager:
if handler_instance in event.subscribers: if handler_instance in event.subscribers:
logger.warning(f"事件处理器 {handler_name} 已经订阅了事件 {event_name},跳过重复订阅") logger.warning(f"事件处理器 {handler_name} 已经订阅了事件 {event_name},跳过重复订阅")
return True return True
# 白名单检查
if event.allowed_subscribers and handler_name not in event.allowed_subscribers:
logger.warning(f"事件处理器 {handler_name} 不在事件 {event_name} 的订阅者白名单中,无法订阅")
return False
event.subscribers.append(handler_instance) event.subscribers.append(handler_instance)
# 按权重从高到低排序订阅者 # 按权重从高到低排序订阅者
@@ -265,11 +275,12 @@ class EventManager:
return {handler.handler_name: handler for handler in event.subscribers} return {handler.handler_name: handler for handler in event.subscribers}
async def trigger_event(self, event_name: Union[EventType, str], **kwargs) -> Optional[HandlerResultsCollection]: async def trigger_event(self, event_name: Union[EventType, str], plugin_name: Optional[str]="", **kwargs) -> Optional[HandlerResultsCollection]:
"""触发指定事件 """触发指定事件
Args: Args:
event_name Union[EventType, str]: 事件名称 event_name Union[EventType, str]: 事件名称
plugin_name str: 触发事件的插件名
**kwargs: 传递给处理器的参数 **kwargs: 传递给处理器的参数
Returns: Returns:
@@ -281,7 +292,15 @@ class EventManager:
if event is None: if event is None:
logger.error(f"事件 {event_name} 不存在,无法触发") logger.error(f"事件 {event_name} 不存在,无法触发")
return None return None
# 插件白名单检查
if event.allowed_triggers and not plugin_name:
logger.warning(f"事件 {event_name} 存在触发者白名单缺少plugin_name无法验证权限已拒绝触发")
return None
elif event.allowed_triggers and plugin_name not in event.allowed_triggers:
logger.warning(f"插件 {plugin_name} 没有权限触发事件 {event_name},已拒绝触发!")
return None
return await event.activate(params) return await event.activate(params)
def init_default_events(self) -> None: def init_default_events(self) -> None:
@@ -294,12 +313,11 @@ class EventManager:
EventType.POST_LLM, EventType.POST_LLM,
EventType.AFTER_LLM, EventType.AFTER_LLM,
EventType.POST_SEND, EventType.POST_SEND,
EventType.AFTER_SEND, EventType.AFTER_SEND
EventType.UNKNOWN
] ]
for event_name in default_events: for event_name in default_events:
self.register_event(event_name) self.register_event(event_name,allowed_triggers=["SYSTEM"])
logger.info("默认事件初始化完成") logger.info("默认事件初始化完成")

View File

@@ -363,13 +363,14 @@ class PluginManager:
command_count = stats.get("command_components", 0) command_count = stats.get("command_components", 0)
tool_count = stats.get("tool_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)
plus_command_count = stats.get("plus_command_components", 0)
total_components = stats.get("total_components", 0) total_components = stats.get("total_components", 0)
# 📋 显示插件加载总览 # 📋 显示插件加载总览
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}, Tool: {tool_count}, EventHandler: {event_handler_count})" f"📊 总览: {total_registered}个插件, {total_components}个组件 (Action: {action_count}, Command: {command_count}, Tool: {tool_count}, PlusCommand: {plus_command_count}, EventHandler: {event_handler_count})"
) )
# 显示详细的插件列表 # 显示详细的插件列表
@@ -410,6 +411,9 @@ class PluginManager:
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
] ]
plus_command_components = [
c for c in plugin_info.components if c.component_type == ComponentType.PLUS_COMMAND
]
if action_components: if action_components:
action_names = [c.name for c in action_components] action_names = [c.name for c in action_components]
@@ -421,6 +425,9 @@ class PluginManager:
if tool_components: if tool_components:
tool_names = [c.name for c in tool_components] tool_names = [c.name for c in tool_components]
logger.info(f" 🛠️ Tool组件: {', '.join(tool_names)}") logger.info(f" 🛠️ Tool组件: {', '.join(tool_names)}")
if plus_command_components:
plus_command_names = [c.name for c in plus_command_components]
logger.info(f" ⚡ PlusCommand组件: {', '.join(plus_command_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

@@ -40,13 +40,12 @@ class ToolExecutor:
可以直接输入聊天消息内容,自动判断并执行相应的工具,返回结构化的工具执行结果。 可以直接输入聊天消息内容,自动判断并执行相应的工具,返回结构化的工具执行结果。
""" """
def __init__(self, chat_id: str, enable_cache: bool = False, cache_ttl: int = 3): def __init__(self, chat_id: str):
"""初始化工具执行器 """初始化工具执行器
Args: Args:
executor_id: 执行器标识符,用于日志记录 executor_id: 执行器标识符,用于日志记录
enable_cache: 是否启用缓存机制 chat_id: 聊天标识符,用于日志记录
cache_ttl: 缓存生存时间(周期数)
""" """
self.chat_id = chat_id self.chat_id = chat_id
self.chat_stream = get_chat_manager().get_stream(self.chat_id) self.chat_stream = get_chat_manager().get_stream(self.chat_id)
@@ -54,12 +53,7 @@ class ToolExecutor:
self.llm_model = LLMRequest(model_set=model_config.model_task_config.tool_use, request_type="tool_executor") self.llm_model = LLMRequest(model_set=model_config.model_task_config.tool_use, request_type="tool_executor")
# 缓存配置 logger.info(f"{self.log_prefix}工具执行器初始化完成")
self.enable_cache = enable_cache
self.cache_ttl = cache_ttl
self.tool_cache = {} # 格式: {cache_key: {"result": result, "ttl": ttl, "timestamp": timestamp}}
logger.info(f"{self.log_prefix}工具执行器初始化完成,缓存{'启用' if enable_cache else '禁用'}TTL={cache_ttl}")
async def execute_from_chat_message( async def execute_from_chat_message(
self, target_message: str, chat_history: str, sender: str, return_details: bool = False self, target_message: str, chat_history: str, sender: str, return_details: bool = False
@@ -77,18 +71,6 @@ class ToolExecutor:
如果return_details为True: Tuple[List[Dict], List[str], str] - (结果列表, 使用的工具, 提示词) 如果return_details为True: Tuple[List[Dict], List[str], str] - (结果列表, 使用的工具, 提示词)
""" """
# 首先检查缓存
cache_key = self._generate_cache_key(target_message, chat_history, sender)
if cached_result := self._get_from_cache(cache_key):
logger.info(f"{self.log_prefix}使用缓存结果,跳过工具执行")
if not return_details:
return cached_result, [], ""
# 从缓存结果中提取工具名称
used_tools = [result.get("tool_name", "unknown") for result in cached_result]
return cached_result, used_tools, ""
# 缓存未命中,执行工具调用
# 获取可用工具 # 获取可用工具
tools = self._get_tool_definitions() tools = self._get_tool_definitions()
@@ -117,10 +99,6 @@ class ToolExecutor:
# 执行工具调用 # 执行工具调用
tool_results, used_tools = await self.execute_tool_calls(tool_calls) tool_results, used_tools = await self.execute_tool_calls(tool_calls)
# 缓存结果
if tool_results:
self._set_cache(cache_key, tool_results)
if used_tools: if used_tools:
logger.info(f"{self.log_prefix}工具执行完成,共执行{len(used_tools)}个工具: {used_tools}") logger.info(f"{self.log_prefix}工具执行完成,共执行{len(used_tools)}个工具: {used_tools}")
@@ -151,9 +129,19 @@ class ToolExecutor:
return [], [] return [], []
# 提取tool_calls中的函数名称 # 提取tool_calls中的函数名称
func_names = [call.func_name for call in tool_calls if call.func_name] func_names = []
for call in tool_calls:
try:
if hasattr(call, 'func_name'):
func_names.append(call.func_name)
except Exception as e:
logger.error(f"{self.log_prefix}获取工具名称失败: {e}")
continue
logger.info(f"{self.log_prefix}开始执行工具调用: {func_names}") if func_names:
logger.info(f"{self.log_prefix}开始执行工具调用: {func_names}")
else:
logger.warning(f"{self.log_prefix}未找到有效的工具调用")
# 执行每个工具调用 # 执行每个工具调用
for tool_call in tool_calls: for tool_call in tool_calls:
@@ -208,6 +196,7 @@ class ToolExecutor:
try: try:
function_name = tool_call.func_name function_name = tool_call.func_name
function_args = tool_call.args or {} function_args = tool_call.args or {}
logger.info(f"🤖 {self.log_prefix} 正在执行工具: [bold green]{function_name}[/bold green] | 参数: {function_args}")
function_args["llm_called"] = True # 标记为LLM调用 function_args["llm_called"] = True # 标记为LLM调用
# 获取对应工具实例 # 获取对应工具实例
@@ -216,88 +205,24 @@ class ToolExecutor:
logger.warning(f"未知工具名称: {function_name}") logger.warning(f"未知工具名称: {function_name}")
return None return None
# 执行工具 # 执行工具并记录日志
logger.debug(f"{self.log_prefix}执行工具 {function_name},参数: {function_args}")
result = await tool_instance.execute(function_args) result = await tool_instance.execute(function_args)
if result: if result:
logger.debug(f"{self.log_prefix}工具 {function_name} 执行成功,结果: {result}")
return { return {
"tool_call_id": tool_call.call_id, "tool_call_id": tool_call.call_id,
"role": "tool", "role": "tool",
"name": function_name, "name": function_name,
"type": "function", "type": "function",
"content": result["content"], "content": result.get("content", "")
} }
logger.warning(f"{self.log_prefix}工具 {function_name} 返回空结果")
return None return None
except Exception as e: except Exception as e:
logger.error(f"执行工具调用时发生错误: {str(e)}") logger.error(f"执行工具调用时发生错误: {str(e)}")
raise e raise e
def _generate_cache_key(self, target_message: str, chat_history: str, sender: str) -> str:
"""生成缓存键
Args:
target_message: 目标消息内容
chat_history: 聊天历史
sender: 发送者
Returns:
str: 缓存键
"""
import hashlib
# 使用消息内容和群聊状态生成唯一缓存键
content = f"{target_message}_{chat_history}_{sender}"
return hashlib.md5(content.encode()).hexdigest()
def _get_from_cache(self, cache_key: str) -> Optional[List[Dict]]:
"""从缓存获取结果
Args:
cache_key: 缓存键
Returns:
Optional[List[Dict]]: 缓存的结果如果不存在或过期则返回None
"""
if not self.enable_cache or cache_key not in self.tool_cache:
return None
cache_item = self.tool_cache[cache_key]
if cache_item["ttl"] <= 0:
# 缓存过期,删除
del self.tool_cache[cache_key]
logger.debug(f"{self.log_prefix}缓存过期,删除缓存键: {cache_key}")
return None
# 减少TTL
cache_item["ttl"] -= 1
logger.debug(f"{self.log_prefix}使用缓存结果剩余TTL: {cache_item['ttl']}")
return cache_item["result"]
def _set_cache(self, cache_key: str, result: List[Dict]):
"""设置缓存
Args:
cache_key: 缓存键
result: 要缓存的结果
"""
if not self.enable_cache:
return
self.tool_cache[cache_key] = {"result": result, "ttl": self.cache_ttl, "timestamp": time.time()}
logger.debug(f"{self.log_prefix}设置缓存TTL: {self.cache_ttl}")
def _cleanup_expired_cache(self):
"""清理过期的缓存"""
if not self.enable_cache:
return
expired_keys = []
expired_keys.extend(cache_key for cache_key, cache_item in self.tool_cache.items() if cache_item["ttl"] <= 0)
for key in expired_keys:
del self.tool_cache[key]
if expired_keys:
logger.debug(f"{self.log_prefix}清理了{len(expired_keys)}个过期缓存")
async def execute_specific_tool_simple(self, tool_name: str, tool_args: Dict) -> Optional[Dict]: async def execute_specific_tool_simple(self, tool_name: str, tool_args: Dict) -> Optional[Dict]:
"""直接执行指定工具 """直接执行指定工具
@@ -336,86 +261,30 @@ class ToolExecutor:
return None return None
def clear_cache(self):
"""清空所有缓存"""
if self.enable_cache:
cache_count = len(self.tool_cache)
self.tool_cache.clear()
logger.info(f"{self.log_prefix}清空了{cache_count}个缓存项")
def get_cache_status(self) -> Dict:
"""获取缓存状态信息
Returns:
Dict: 包含缓存统计信息的字典
"""
if not self.enable_cache:
return {"enabled": False, "cache_count": 0}
# 清理过期缓存
self._cleanup_expired_cache()
total_count = len(self.tool_cache)
ttl_distribution = {}
for cache_item in self.tool_cache.values():
ttl = cache_item["ttl"]
ttl_distribution[ttl] = ttl_distribution.get(ttl, 0) + 1
return {
"enabled": True,
"cache_count": total_count,
"cache_ttl": self.cache_ttl,
"ttl_distribution": ttl_distribution,
}
def set_cache_config(self, enable_cache: Optional[bool] = None, cache_ttl: int = -1):
"""动态修改缓存配置
Args:
enable_cache: 是否启用缓存
cache_ttl: 缓存TTL
"""
if enable_cache is not None:
self.enable_cache = enable_cache
logger.info(f"{self.log_prefix}缓存状态修改为: {'启用' if enable_cache else '禁用'}")
if cache_ttl > 0:
self.cache_ttl = cache_ttl
logger.info(f"{self.log_prefix}缓存TTL修改为: {cache_ttl}")
""" """
ToolExecutor使用示例 ToolExecutor使用示例
# 1. 基础使用 - 从聊天消息执行工具启用缓存默认TTL=3 # 1. 基础使用 - 从聊天消息执行工具
executor = ToolExecutor(executor_id="my_executor") executor = ToolExecutor(chat_id=my_chat_id)
results, _, _ = await executor.execute_from_chat_message( results, _, _ = await executor.execute_from_chat_message(
talking_message_str="今天天气怎么样?现在几点了?", target_message="今天天气怎么样?现在几点了?",
is_group_chat=False chat_history="",
sender="用户"
) )
# 2. 禁用缓存的执行器 # 2. 获取详细信息
no_cache_executor = ToolExecutor(executor_id="no_cache", enable_cache=False)
# 3. 自定义缓存TTL
long_cache_executor = ToolExecutor(executor_id="long_cache", cache_ttl=10)
# 4. 获取详细信息
results, used_tools, prompt = await executor.execute_from_chat_message( results, used_tools, prompt = await executor.execute_from_chat_message(
talking_message_str="帮我查询Python相关知识", target_message="帮我查询Python相关知识",
is_group_chat=False, chat_history="",
sender="用户",
return_details=True return_details=True
) )
# 5. 直接执行特定工具 # 3. 直接执行特定工具
result = await executor.execute_specific_tool_simple( result = await executor.execute_specific_tool_simple(
tool_name="get_knowledge", tool_name="get_knowledge",
tool_args={"query": "机器学习"} tool_args={"query": "机器学习"}
) )
# 6. 缓存管理
cache_status = executor.get_cache_status() # 查看缓存状态
executor.clear_cache() # 清空缓存
executor.set_cache_config(cache_ttl=5) # 动态修改缓存配置
""" """

View File

@@ -9,9 +9,9 @@ from typing import Callable, Optional
from inspect import iscoroutinefunction from inspect import iscoroutinefunction
from src.plugin_system.apis.permission_api import permission_api from src.plugin_system.apis.permission_api import permission_api
from src.plugin_system.apis.send_api import send_message from src.plugin_system.apis.send_api import text_to_stream
from src.plugin_system.apis.logging_api import get_logger from src.plugin_system.apis.logging_api import get_logger
from src.common.message import ChatStream from src.chat.message_receive.chat_stream import ChatStream
logger = get_logger(__name__) logger = get_logger(__name__)
@@ -37,6 +37,8 @@ def require_permission(permission_node: str, deny_message: Optional[str] = None)
async def async_wrapper(*args, **kwargs): async def async_wrapper(*args, **kwargs):
# 尝试从参数中提取 ChatStream 对象 # 尝试从参数中提取 ChatStream 对象
chat_stream = None chat_stream = None
# 首先检查位置参数中的 ChatStream
for arg in args: for arg in args:
if isinstance(arg, ChatStream): if isinstance(arg, ChatStream):
chat_stream = arg chat_stream = arg
@@ -46,21 +48,31 @@ def require_permission(permission_node: str, deny_message: Optional[str] = None)
if chat_stream is None: if chat_stream is None:
chat_stream = kwargs.get('chat_stream') chat_stream = kwargs.get('chat_stream')
# 如果还没找到,检查是否是 PlusCommand 方法调用
if chat_stream is None and args:
# 检查第一个参数是否有 message.chat_stream 属性PlusCommand 实例)
instance = args[0]
if hasattr(instance, 'message') and hasattr(instance.message, 'chat_stream'):
chat_stream = instance.message.chat_stream
if chat_stream is None: if chat_stream is None:
logger.error(f"权限装饰器无法找到 ChatStream 对象,函数: {func.__name__}") logger.error(f"权限装饰器无法找到 ChatStream 对象,函数: {func.__name__}")
return return
# 检查权限 # 检查权限
has_permission = permission_api.check_permission( has_permission = permission_api.check_permission(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id, chat_stream.user_info.user_id,
permission_node permission_node
) )
if not has_permission: if not has_permission:
# 权限不足,发送拒绝消息 # 权限不足,发送拒绝消息
message = deny_message or f"❌ 你没有执行此操作的权限\n需要权限: {permission_node}" message = deny_message or f"❌ 你没有执行此操作的权限\n需要权限: {permission_node}"
await send_message(chat_stream, message) await text_to_stream(message, chat_stream.stream_id)
# 对于PlusCommand的execute方法需要返回适当的元组
if func.__name__ == 'execute' and hasattr(args[0], 'send_text'):
return False, "权限不足", True
return return
# 权限检查通过,执行原函数 # 权限检查通过,执行原函数
@@ -83,13 +95,13 @@ def require_permission(permission_node: str, deny_message: Optional[str] = None)
# 检查权限 # 检查权限
has_permission = permission_api.check_permission( has_permission = permission_api.check_permission(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id, chat_stream.user_info.user_id,
permission_node permission_node
) )
if not has_permission: if not has_permission:
logger.warning(f"用户 {chat_stream.user_platform}:{chat_stream.user_id} 没有权限 {permission_node}") logger.warning(f"用户 {chat_stream.platform}:{chat_stream.user_info.user_id} 没有权限 {permission_node}")
return return
# 权限检查通过,执行原函数 # 权限检查通过,执行原函数
@@ -124,6 +136,8 @@ def require_master(deny_message: Optional[str] = None):
async def async_wrapper(*args, **kwargs): async def async_wrapper(*args, **kwargs):
# 尝试从参数中提取 ChatStream 对象 # 尝试从参数中提取 ChatStream 对象
chat_stream = None chat_stream = None
# 首先检查位置参数中的 ChatStream
for arg in args: for arg in args:
if isinstance(arg, ChatStream): if isinstance(arg, ChatStream):
chat_stream = arg chat_stream = arg
@@ -133,20 +147,28 @@ def require_master(deny_message: Optional[str] = None):
if chat_stream is None: if chat_stream is None:
chat_stream = kwargs.get('chat_stream') chat_stream = kwargs.get('chat_stream')
# 如果还没找到,检查是否是 PlusCommand 方法调用
if chat_stream is None and args:
# 检查第一个参数是否有 message.chat_stream 属性PlusCommand 实例)
instance = args[0]
if hasattr(instance, 'message') and hasattr(instance.message, 'chat_stream'):
chat_stream = instance.message.chat_stream
if chat_stream is None: if chat_stream is None:
logger.error(f"Master权限装饰器无法找到 ChatStream 对象,函数: {func.__name__}") logger.error(f"Master权限装饰器无法找到 ChatStream 对象,函数: {func.__name__}")
return return
# 检查是否为Master用户 # 检查是否为Master用户
is_master = permission_api.is_master( is_master = permission_api.is_master(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id chat_stream.user_info.user_id
) )
if not is_master: if not is_master:
# 权限不足,发送拒绝消息
message = deny_message or "❌ 此操作仅限Master用户执行" message = deny_message or "❌ 此操作仅限Master用户执行"
await send_message(chat_stream, message) await text_to_stream(message, chat_stream.stream_id)
if func.__name__ == 'execute' and hasattr(args[0], 'send_text'):
return False, "需要Master权限", True
return return
# 权限检查通过,执行原函数 # 权限检查通过,执行原函数
@@ -169,12 +191,12 @@ def require_master(deny_message: Optional[str] = None):
# 检查是否为Master用户 # 检查是否为Master用户
is_master = permission_api.is_master( is_master = permission_api.is_master(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id chat_stream.user_info.user_id
) )
if not is_master: if not is_master:
logger.warning(f"用户 {chat_stream.user_platform}:{chat_stream.user_id} 不是Master用户") logger.warning(f"用户 {chat_stream.platform}:{chat_stream.user_info.user_id} 不是Master用户")
return return
# 权限检查通过,执行原函数 # 权限检查通过,执行原函数
@@ -209,8 +231,8 @@ class PermissionChecker:
bool: 是否拥有权限 bool: 是否拥有权限
""" """
return permission_api.check_permission( return permission_api.check_permission(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id, chat_stream.user_info.user_id,
permission_node permission_node
) )
@@ -226,8 +248,8 @@ class PermissionChecker:
bool: 是否为Master用户 bool: 是否为Master用户
""" """
return permission_api.is_master( return permission_api.is_master(
chat_stream.user_platform, chat_stream.platform,
chat_stream.user_id chat_stream.user_info.user_id
) )
@staticmethod @staticmethod
@@ -248,7 +270,7 @@ class PermissionChecker:
if not has_permission: if not has_permission:
message = deny_message or f"❌ 你没有执行此操作的权限\n需要权限: {permission_node}" message = deny_message or f"❌ 你没有执行此操作的权限\n需要权限: {permission_node}"
await send_message(chat_stream, message) await text_to_stream(message, chat_stream.stream_id)
return has_permission return has_permission
@@ -269,6 +291,6 @@ class PermissionChecker:
if not is_master: if not is_master:
message = deny_message or "❌ 此操作仅限Master用户执行" message = deny_message or "❌ 此操作仅限Master用户执行"
await send_message(chat_stream, message) await text_to_stream(message, chat_stream.stream_id)
return is_master return is_master

View File

@@ -5,52 +5,32 @@
from typing import Tuple from typing import Tuple
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system import BaseCommand from src.plugin_system.base.plus_command import PlusCommand
from src.plugin_system.base.command_args import CommandArgs
from src.plugin_system.utils.permission_decorators import require_permission
from ..services.manager import get_qzone_service, get_config_getter from ..services.manager import get_qzone_service, get_config_getter
logger = get_logger("MaiZone.SendFeedCommand") logger = get_logger("MaiZone.SendFeedCommand")
class SendFeedCommand(BaseCommand): class SendFeedCommand(PlusCommand):
""" """
响应用户通过 `/send_feed` 命令发送说说的请求。 响应用户通过 `/send_feed` 命令发送说说的请求。
""" """
command_name: str = "send_feed" command_name: str = "send_feed"
command_description: str = "发送一条QQ空间说说" command_description: str = "发送一条QQ空间说说"
command_pattern: str = r"^/send_feed(?:\s+(?P<topic>.*))?$" command_aliases = ["发空间"]
command_help: str = "使用 /send_feed [主题] 来发送一条说说"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def _check_permission(self) -> bool: @require_permission("plugin.send.permission")
"""检查当前用户是否有权限执行此命令""" async def execute(self, args: CommandArgs) -> Tuple[bool, str, bool]:
user_id = self.message.message_info.user_info.user_id
if not user_id:
return False
get_config = get_config_getter()
permission_list = get_config("send.permission", [])
permission_type = get_config("send.permission_type", "whitelist")
if not isinstance(permission_list, list):
return False
if permission_type == 'whitelist':
return user_id in permission_list
elif permission_type == 'blacklist':
return user_id not in permission_list
return False
async def execute(self) -> Tuple[bool, str, bool]:
""" """
执行命令的核心逻辑。 执行命令的核心逻辑。
""" """
if not self._check_permission():
await self.send_text("抱歉,你没有权限使用这个命令哦。")
return False, "权限不足", True
topic = self.matched_groups.get("topic", "") topic = args.get_remaining()
stream_id = self.message.chat_stream.stream_id stream_id = self.message.chat_stream.stream_id
await self.send_text(f"收到!正在为你生成关于“{topic or '随机'}”的说说,请稍候...") await self.send_text(f"收到!正在为你生成关于“{topic or '随机'}”的说说,请稍候...")

View File

@@ -13,6 +13,7 @@ from src.plugin_system import (
register_plugin register_plugin
) )
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.apis.permission_api import permission_api
from .actions.read_feed_action import ReadFeedAction from .actions.read_feed_action import ReadFeedAction
from .actions.send_feed_action import SendFeedAction from .actions.send_feed_action import SendFeedAction
@@ -42,6 +43,7 @@ class MaiZoneRefactoredPlugin(BasePlugin):
"plugin": {"enable": ConfigField(type=bool, default=True, description="是否启用插件")}, "plugin": {"enable": ConfigField(type=bool, default=True, description="是否启用插件")},
"models": { "models": {
"text_model": ConfigField(type=str, default="maizone", description="生成文本的模型名称"), "text_model": ConfigField(type=str, default="maizone", description="生成文本的模型名称"),
"vision_model": ConfigField(type=str, default="YISHAN-gemini-2.5-flash", description="识别图片的模型名称"),
"siliconflow_apikey": ConfigField(type=str, default="", description="硅基流动AI生图API密钥"), "siliconflow_apikey": ConfigField(type=str, default="", description="硅基流动AI生图API密钥"),
}, },
"send": { "send": {
@@ -81,7 +83,12 @@ class MaiZoneRefactoredPlugin(BasePlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
permission_api.register_permission_node(
"plugin.send.permission",
"是否可以使用机器人发送说说",
"maiZone",
False
)
content_service = ContentService(self.get_config) content_service = ContentService(self.get_config)
image_service = ImageService(self.get_config) image_service = ImageService(self.get_config)
cookie_service = CookieService(self.get_config) cookie_service = CookieService(self.get_config)
@@ -101,5 +108,5 @@ class MaiZoneRefactoredPlugin(BasePlugin):
return [ return [
(SendFeedAction.get_action_info(), SendFeedAction), (SendFeedAction.get_action_info(), SendFeedAction),
(ReadFeedAction.get_action_info(), ReadFeedAction), (ReadFeedAction.get_action_info(), ReadFeedAction),
(SendFeedCommand.get_command_info(), SendFeedCommand), (SendFeedCommand.get_plus_command_info(), SendFeedCommand),
] ]

View File

@@ -6,8 +6,19 @@
from typing import Callable, Optional from typing import Callable, Optional
import datetime import datetime
import base64
import aiohttp
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.apis import llm_api, config_api import base64
import aiohttp
import imghdr
import asyncio
from src.common.logger import get_logger
from src.plugin_system.apis import llm_api, config_api, generator_api, person_api
from src.chat.message_receive.chat_stream import get_chat_manager
from maim_message import UserInfo
from src.llm_models.utils_model import LLMRequest
from src.config.api_ada_configs import TaskConfig
# 导入旧的工具函数,我们稍后会考虑是否也需要重构它 # 导入旧的工具函数,我们稍后会考虑是否也需要重构它
from ..utils.history_utils import get_send_history from ..utils.history_utils import get_send_history
@@ -97,110 +108,181 @@ class ContentService:
logger.error(f"生成说说内容时发生异常: {e}") logger.error(f"生成说说内容时发生异常: {e}")
return "" return ""
async def generate_comment(self, content: str, target_name: str, rt_con: str = "") -> str: async def generate_comment(self, content: str, target_name: str, rt_con: str = "", images: list = []) -> str:
""" """
针对一条具体的说说内容生成评论。 针对一条具体的说说内容生成评论。
:param content: 好友的说说内容。
:param target_name: 好友的昵称。
:param rt_con: 如果是转发的说说,这里是原说说内容。
:return: 生成的评论内容,如果失败则返回空字符串。
""" """
try: for i in range(3): # 重试3次
# 获取模型配置 try:
models = llm_api.get_available_models() chat_manager = get_chat_manager()
text_model = str(self.get_config("models.text_model", "replyer_1")) bot_platform = config_api.get_global_config('bot.platform')
model_config = models.get(text_model) bot_qq = str(config_api.get_global_config('bot.qq_account'))
bot_nickname = config_api.get_global_config('bot.nickname')
bot_user_info = UserInfo(
platform=bot_platform,
user_id=bot_qq,
user_nickname=bot_nickname
)
if not model_config: chat_stream = await chat_manager.get_or_create_stream(
logger.error("未配置LLM模型") platform=bot_platform,
return "" user_info=bot_user_info
)
# 获取机器人信息 if not chat_stream:
bot_personality = config_api.get_global_config("personality.personality_core", "一个机器人") logger.error(f"无法为QQ号 {bot_qq} 创建聊天流")
bot_expression = config_api.get_global_config("expression.expression_style", "内容积极向上") return ""
# 构建提示词 image_descriptions = []
if not rt_con: if images:
prompt = f""" for image_url in images:
你是'{bot_personality}',你正在浏览你好友'{target_name}'的QQ空间 description = await self._describe_image(image_url)
你看到了你的好友'{target_name}'qq空间上内容是'{content}'的说说,你想要发表你的一条评论, if description:
{bot_expression},回复的平淡一些,简短一些,说中文, image_descriptions.append(description)
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出回复内容
""" extra_info = "正在评论QQ空间的好友说说。"
else: if image_descriptions:
prompt = f""" extra_info += "说说中包含的图片内容如下:\n" + "\n".join(image_descriptions)
你是'{bot_personality}',你正在浏览你好友'{target_name}'的QQ空间
你看到了你的好友'{target_name}'在qq空间上转发了一条内容为'{rt_con}'的说说,你的好友的评论为'{content}'
你想要发表你的一条评论,{bot_expression},回复的平淡一些,简短一些,说中文,
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出回复内容
"""
logger.info(f"正在为'{target_name}'的说说生成评论: {content[:20]}...") reply_to = f"{target_name}:{content}"
if rt_con:
reply_to += f"\n[转发内容]: {rt_con}"
# 调用LLM生成评论 success, reply_set, _ = await generator_api.generate_reply(
success, comment, _, _ = await llm_api.generate_with_model( chat_stream=chat_stream,
prompt=prompt, reply_to=reply_to,
model_config=model_config, extra_info=extra_info,
request_type="comment.generate", request_type="maizone.comment"
temperature=0.3, )
max_tokens=100
)
if success: if success and reply_set:
logger.info(f"成功生成评论内容:'{comment}'") comment = "".join([content for type, content in reply_set if type == 'text'])
return comment logger.info(f"成功生成评论内容:'{comment}'")
else: return comment
logger.error("生成评论内容失败") else:
return "" # 如果生成失败,则进行重试
if i < 2:
except Exception as e: logger.warning(f"生成评论失败将在5秒后重试 (尝试 {i+1}/3)")
logger.error(f"生成评论内容时发生异常: {e}") await asyncio.sleep(5)
return "" continue
else:
logger.error("使用 generator_api 生成评论失败")
return ""
except Exception as e:
if i < 2:
logger.warning(f"生成评论时发生异常将在5秒后重试 (尝试 {i+1}/3): {e}")
await asyncio.sleep(5)
continue
else:
logger.error(f"生成评论时发生异常: {e}")
return ""
return ""
async def generate_comment_reply(self, story_content: str, comment_content: str, commenter_name: str) -> str: async def generate_comment_reply(self, story_content: str, comment_content: str, commenter_name: str) -> str:
""" """
针对自己说说的评论,生成回复。 针对自己说说的评论,生成回复。
:param story_content: 原始说说内容。
:param comment_content: 好友的评论内容。
:param commenter_name: 评论者的昵称。
:return: 生成的回复内容。
""" """
try: for i in range(3): # 重试3次
models = llm_api.get_available_models() try:
text_model = str(self.get_config("models.text_model", "replyer_1")) chat_manager = get_chat_manager()
model_config = models.get(text_model) bot_platform = config_api.get_global_config('bot.platform')
if not model_config: bot_qq = str(config_api.get_global_config('bot.qq_account'))
return "" bot_nickname = config_api.get_global_config('bot.nickname')
bot_personality = config_api.get_global_config("personality.personality_core", "一个机器人") bot_user_info = UserInfo(
bot_expression = config_api.get_global_config("expression.expression_style", "内容积极向上") platform=bot_platform,
user_id=bot_qq,
user_nickname=bot_nickname
)
prompt = f""" chat_stream = await chat_manager.get_or_create_stream(
你是'{bot_personality}',你的好友'{commenter_name}'评论了你QQ空间上的一条内容为“{story_content}”说说, platform=bot_platform,
你的好友对该说说的评论为:“{comment_content}”,你想要对此评论进行回复 user_info=bot_user_info
{bot_expression},回复的平淡一些,简短一些,说中文, )
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号()表情包at或 @等 )。只输出回复内容
"""
success, reply, _, _ = await llm_api.generate_with_model(
prompt=prompt,
model_config=model_config,
request_type="comment.reply.generate",
temperature=0.3,
max_tokens=100
)
if success: if not chat_stream:
logger.info(f"成功为'{commenter_name}'的评论生成回复: '{reply}'") logger.error(f"无法为QQ号 {bot_qq} 创建聊天流")
return reply return ""
else:
logger.error("生成评论回复失败") reply_to = f"{commenter_name}:{comment_content}"
return "" extra_info = f"正在回复我的QQ空间说说“{story_content}”下的评论。"
except Exception as e:
logger.error(f"生成评论回复时发生异常: {e}") success, reply_set, _ = await generator_api.generate_reply(
return "" chat_stream=chat_stream,
reply_to=reply_to,
extra_info=extra_info,
request_type="maizone.comment_reply"
)
if success and reply_set:
reply = "".join([content for type, content in reply_set if type == 'text'])
logger.info(f"成功为'{commenter_name}'的评论生成回复: '{reply}'")
return reply
else:
if i < 2:
logger.warning(f"生成评论回复失败将在5秒后重试 (尝试 {i+1}/3)")
await asyncio.sleep(5)
continue
else:
logger.error("使用 generator_api 生成评论回复失败")
return ""
except Exception as e:
if i < 2:
logger.warning(f"生成评论回复时发生异常将在5秒后重试 (尝试 {i+1}/3): {e}")
await asyncio.sleep(5)
continue
else:
logger.error(f"生成评论回复时发生异常: {e}")
return ""
return ""
async def _describe_image(self, image_url: str) -> Optional[str]:
"""
使用LLM识别图片内容。
"""
for i in range(3): # 重试3次
try:
async with aiohttp.ClientSession() as session:
async with session.get(image_url, timeout=30) as resp:
if resp.status != 200:
logger.error(f"下载图片失败: {image_url}, status: {resp.status}")
await asyncio.sleep(2)
continue
image_bytes = await resp.read()
image_format = imghdr.what(None, image_bytes)
if not image_format:
logger.error(f"无法识别图片格式: {image_url}")
return None
image_base64 = base64.b64encode(image_bytes).decode("utf-8")
vision_model_name = self.get_config("models.vision_model", "vision")
if not vision_model_name:
logger.error("未在插件配置中指定视觉模型")
return None
vision_model_config = TaskConfig(
model_list=[vision_model_name],
temperature=0.3,
max_tokens=1500
)
llm_request = LLMRequest(model_set=vision_model_config, request_type="maizone.image_describe")
prompt = config_api.get_global_config("custom_prompt.image_prompt", "请描述这张图片")
description, _ = await llm_request.generate_response_for_image(
prompt=prompt,
image_base64=image_base64,
image_format=image_format,
)
return description
except Exception as e:
logger.error(f"识别图片时发生异常 (尝试 {i+1}/3): {e}")
await asyncio.sleep(2)
return None
async def generate_story_from_activity(self, activity: str) -> str: async def generate_story_from_activity(self, activity: str) -> str:
""" """

View File

@@ -17,7 +17,7 @@ import aiohttp
import bs4 import bs4
import json5 import json5
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system.apis import config_api, person_api,chat_api from src.plugin_system.apis import config_api, person_api
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.utils.chat_message_builder import ( from src.chat.utils.chat_message_builder import (
build_readable_messages_with_id, build_readable_messages_with_id,
@@ -151,24 +151,31 @@ class QZoneService:
return return
try: try:
feeds = await api_client["monitor_list_feeds"](20) # 监控时检查最近20条动态 # --- 第一步: 单独处理自己说说的评论 ---
if not feeds: if self.get_config("monitor.enable_auto_reply", False):
logger.info("监控完成:未发现新说说") try:
own_feeds = await api_client["list_feeds"](qq_account, 5) # 获取自己最近5条说说
if own_feeds:
logger.info(f"获取到自己 {len(own_feeds)} 条说说,检查评论...")
for feed in own_feeds:
await self._reply_to_own_feed_comments(feed, api_client)
await asyncio.sleep(random.uniform(3, 5))
except Exception as e:
logger.error(f"处理自己说说评论时发生异常: {e}", exc_info=True)
# --- 第二步: 处理好友的动态 ---
friend_feeds = await api_client["monitor_list_feeds"](20)
if not friend_feeds:
logger.info("监控完成:未发现好友新说说")
return return
logger.info(f"监控任务: 发现 {len(feeds)} 条新动态,准备处理...") logger.info(f"监控任务: 发现 {len(friend_feeds)}好友新动态,准备处理...")
for feed in feeds: for feed in friend_feeds:
target_qq = feed.get("target_qq") target_qq = feed.get("target_qq")
if not target_qq: if not target_qq or str(target_qq) == str(qq_account): # 确保不重复处理自己的
continue continue
# 区分是自己的说说还是他人的说说 await self._process_single_feed(feed, api_client, target_qq, target_qq)
if target_qq == qq_account:
if self.get_config("monitor.enable_auto_reply", False):
await self._reply_to_own_feed_comments(feed, api_client)
else:
await self._process_single_feed(feed, api_client, target_qq, target_qq)
await asyncio.sleep(random.uniform(5, 10)) await asyncio.sleep(random.uniform(5, 10))
except Exception as e: except Exception as e:
logger.error(f"监控好友动态时发生异常: {e}", exc_info=True) logger.error(f"监控好友动态时发生异常: {e}", exc_info=True)
@@ -244,12 +251,20 @@ class QZoneService:
if not comments: if not comments:
return return
# 筛选出未被自己回复过的评论 # 筛选出未被自己回复过的评论
my_comment_tids = { if not comments:
c["parent_tid"] for c in comments if c.get("parent_tid") and c.get("qq_account") == qq_account return
# 找到所有我已经回复过的评论的ID
replied_to_tids = {
c['parent_tid'] for c in comments
if c.get('parent_tid') and str(c.get('qq_account')) == str(qq_account)
} }
# 找出所有非我发出且我未回复过的评论
comments_to_reply = [ comments_to_reply = [
c for c in comments if not c.get("parent_tid") and c.get("comment_tid") not in my_comment_tids c for c in comments
if str(c.get('qq_account')) != str(qq_account) and c.get('comment_tid') not in replied_to_tids
] ]
if not comments_to_reply: if not comments_to_reply:
@@ -275,9 +290,10 @@ class QZoneService:
content = feed.get("content", "") content = feed.get("content", "")
fid = feed.get("tid", "") fid = feed.get("tid", "")
rt_con = feed.get("rt_con", "") rt_con = feed.get("rt_con", "")
images = feed.get("images", [])
if random.random() <= self.get_config("read.comment_possibility", 0.3): if random.random() <= self.get_config("read.comment_possibility", 0.3):
comment_text = await self.content_service.generate_comment(content, target_name, rt_con) comment_text = await self.content_service.generate_comment(content, target_name, rt_con, images)
if comment_text: if comment_text:
await api_client["comment"](target_qq, fid, comment_text) await api_client["comment"](target_qq, fid, comment_text)
@@ -655,6 +671,19 @@ class QZoneService:
c.get("name") == my_name for c in msg.get("commentlist", []) if isinstance(c, dict) c.get("name") == my_name for c in msg.get("commentlist", []) if isinstance(c, dict)
) )
if not is_commented: if not is_commented:
images = [pic['url1'] for pic in msg.get('pictotal', []) if 'url1' in pic]
comments = []
if 'commentlist' in msg:
for c in msg['commentlist']:
comments.append({
'qq_account': c.get('uin'),
'nickname': c.get('name'),
'content': c.get('content'),
'comment_tid': c.get('tid'),
'parent_tid': c.get('parent_tid') # API直接返回了父ID
})
feeds_list.append( feeds_list.append(
{ {
"tid": msg.get("tid", ""), "tid": msg.get("tid", ""),
@@ -665,6 +694,8 @@ class QZoneService:
"rt_con": msg.get("rt_con", {}).get("content", "") "rt_con": msg.get("rt_con", {}).get("content", "")
if isinstance(msg.get("rt_con"), dict) if isinstance(msg.get("rt_con"), dict)
else "", else "",
"images": images,
"comments": comments
} }
) )
return feeds_list return feeds_list
@@ -815,10 +846,61 @@ class QZoneService:
text_div = soup.find('div', class_='f-info') text_div = soup.find('div', class_='f-info')
text = text_div.get_text(strip=True) if text_div else "" text = text_div.get_text(strip=True) if text_div else ""
# --- 借鉴原版插件的精确图片提取逻辑 ---
image_urls = []
img_box = soup.find('div', class_='img-box')
if img_box:
for img in img_box.find_all('img'):
src = img.get('src')
# 排除QQ空间的小图标和表情
if src and 'qzonestyle.gtimg.cn' not in src:
image_urls.append(src)
# 视频封面也视为图片
video_thumb = soup.select_one('div.video-img img')
if video_thumb and 'src' in video_thumb.attrs:
image_urls.append(video_thumb['src'])
# 去重
images = list(set(image_urls))
comments = []
comment_divs = soup.find_all('div', class_='f-single-comment')
for comment_div in comment_divs:
# --- 处理主评论 ---
author_a = comment_div.find('a', class_='f-nick')
content_span = comment_div.find('span', class_='f-re-con')
if author_a and content_span:
comments.append({
'qq_account': str(comment_div.get('data-uin', '')),
'nickname': author_a.get_text(strip=True),
'content': content_span.get_text(strip=True),
'comment_tid': comment_div.get('data-tid', ''),
'parent_tid': None # 主评论没有父ID
})
# --- 处理这条主评论下的所有回复 ---
reply_divs = comment_div.find_all('div', class_='f-single-re')
for reply_div in reply_divs:
reply_author_a = reply_div.find('a', class_='f-nick')
reply_content_span = reply_div.find('span', class_='f-re-con')
if reply_author_a and reply_content_span:
comments.append({
'qq_account': str(reply_div.get('data-uin', '')),
'nickname': reply_author_a.get_text(strip=True),
'content': reply_content_span.get_text(strip=True).lstrip(': '), # 移除回复内容前多余的冒号和空格
'comment_tid': reply_div.get('data-tid', ''),
'parent_tid': reply_div.get('data-parent-tid', comment_div.get('data-tid', '')) # 如果没有父ID则将父ID设为主评论ID
})
feeds_list.append({ feeds_list.append({
'target_qq': target_qq, 'target_qq': target_qq,
'tid': tid, 'tid': tid,
'content': text, 'content': text,
'images': images,
'comments': comments
}) })
logger.info(f"监控任务发现 {len(feeds_list)} 条未处理的新说说。") logger.info(f"监控任务发现 {len(feeds_list)} 条未处理的新说说。")
return feeds_list return feeds_list

View File

@@ -10,7 +10,7 @@ import traceback
from typing import Callable from typing import Callable
from src.common.logger import get_logger from src.common.logger import get_logger
from src.manager.schedule_manager import schedule_manager from src.schedule.schedule_manager import schedule_manager
from src.common.database.sqlalchemy_database_api import get_db_session from src.common.database.sqlalchemy_database_api import get_db_session
from src.common.database.sqlalchemy_models import MaiZoneScheduleStatus from src.common.database.sqlalchemy_models import MaiZoneScheduleStatus

View File

@@ -2,6 +2,7 @@
权限管理插件 权限管理插件
提供权限系统的管理命令,包括权限授权、撤销、查询等功能。 提供权限系统的管理命令,包括权限授权、撤销、查询等功能。
使用新的PlusCommand系统重构。
""" """
import re import re
@@ -9,23 +10,26 @@ from typing import List, Optional, Tuple, Type
from src.plugin_system.apis.plugin_register_api import register_plugin from src.plugin_system.apis.plugin_register_api import register_plugin
from src.plugin_system.base.base_plugin import BasePlugin from src.plugin_system.base.base_plugin import BasePlugin
from src.plugin_system.base.base_command import BaseCommand from src.plugin_system.base.plus_command import PlusCommand
from src.plugin_system.base.command_args import CommandArgs
from src.plugin_system.apis.permission_api import permission_api from src.plugin_system.apis.permission_api import permission_api
from src.plugin_system.apis.logging_api import get_logger from src.plugin_system.apis.logging_api import get_logger
from src.plugin_system.base.component_types import CommandInfo from src.plugin_system.base.component_types import PlusCommandInfo, ChatType
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
from src.plugin_system.utils.permission_decorators import require_permission, require_master, PermissionChecker
logger = get_logger("Permission") logger = get_logger("Permission")
class PermissionCommand(BaseCommand): class PermissionCommand(PlusCommand):
"""权限管理命令""" """权限管理命令 - 使用PlusCommand系统"""
command_name = "permission" command_name = "permission"
command_description = "权限管理命令" command_description = "权限管理命令,支持授权、撤销、查询等功能"
command_pattern = r"^/permission(?:\s|$)" command_aliases = ["perm", "权限"]
command_help = "/permission <子命令> [参数...]" priority = 10
chat_type_allow = ChatType.ALL
intercept_message = True intercept_message = True
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@@ -43,70 +47,48 @@ class PermissionCommand(BaseCommand):
"permission_manager", "permission_manager",
True True
) )
def can_execute(self) -> bool:
"""检查命令是否可以执行"""
# 基本权限检查由权限系统处理
return True
async def execute(self, args: List[str]) -> None:
"""执行权限管理命令"""
if not args:
await self._show_help()
return
subcommand = args[0].lower()
remaining_args = args[1:]
chat_stream = self.message.chat_stream
# 检查基本查看权限
can_view = permission_api.check_permission(
chat_stream.platform,
chat_stream.user_info.user_id,
"plugin.permission.view"
) or permission_api.is_master(chat_stream.platform, chat_stream.user_info.user_id)
# 检查管理权限 async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
can_manage = permission_api.check_permission( """执行权限管理命令"""
chat_stream.platform, if args.is_empty:
chat_stream.user_info.user_id, await self._show_help()
"plugin.permission.manage" return True, "显示帮助信息", True
) or permission_api.is_master(chat_stream.platform, chat_stream.user_info.user_id)
subcommand = args.get_first.lower()
remaining_args = args.get_args()[1:] # 获取除第一个参数外的所有参数
chat_stream = self.message.chat_stream
if subcommand in ["grant", "授权", "give"]: if subcommand in ["grant", "授权", "give"]:
if not can_manage:
await self.send_text("❌ 你没有权限管理的权限")
return
await self._grant_permission(chat_stream, remaining_args) await self._grant_permission(chat_stream, remaining_args)
return True, "执行授权命令", True
elif subcommand in ["revoke", "撤销", "remove"]: elif subcommand in ["revoke", "撤销", "remove"]:
if not can_manage:
await self.send_text("❌ 你没有权限管理的权限")
return
await self._revoke_permission(chat_stream, remaining_args) await self._revoke_permission(chat_stream, remaining_args)
return True, "执行撤销命令", True
elif subcommand in ["list", "列表", "ls"]: elif subcommand in ["list", "列表", "ls"]:
if not can_view:
await self.send_text("❌ 你没有查看权限的权限")
return
await self._list_permissions(chat_stream, remaining_args) await self._list_permissions(chat_stream, remaining_args)
return True, "执行列表命令", True
elif subcommand in ["check", "检查"]: elif subcommand in ["check", "检查"]:
if not can_view:
await self.send_text("❌ 你没有查看权限的权限")
return
await self._check_permission(chat_stream, remaining_args) await self._check_permission(chat_stream, remaining_args)
return True, "执行检查命令", True
elif subcommand in ["nodes", "节点"]: elif subcommand in ["nodes", "节点"]:
if not can_view:
await self.send_text("❌ 你没有查看权限的权限")
return
await self._list_nodes(chat_stream, remaining_args) await self._list_nodes(chat_stream, remaining_args)
return True, "执行节点命令", True
elif subcommand in ["allnodes", "全部节点", "all"]:
await self._list_all_nodes_with_description(chat_stream)
return True, "执行全部节点命令", True
elif subcommand in ["help", "帮助"]: elif subcommand in ["help", "帮助"]:
await self._show_help(chat_stream) await self._show_help()
return True, "显示帮助信息", True
else: else:
await self.send_text(f"❌ 未知的子命令: {subcommand}\n使用 /permission help 查看帮助") await self.send_text(f"❌ 未知的子命令: {subcommand}\n使用 /permission help 查看帮助")
return True, "未知子命令", True
async def _show_help(self): async def _show_help(self):
"""显示帮助信息""" """显示帮助信息"""
@@ -120,6 +102,7 @@ class PermissionCommand(BaseCommand):
• /permission list [用户] - 查看用户权限列表 • /permission list [用户] - 查看用户权限列表
• /permission check <@用户|QQ号> <权限节点> - 检查用户是否拥有权限 • /permission check <@用户|QQ号> <权限节点> - 检查用户是否拥有权限
• /permission nodes [插件名] - 查看权限节点列表 • /permission nodes [插件名] - 查看权限节点列表
• /permission allnodes - 查看所有插件的权限节点详情
❓ 其他: ❓ 其他:
• /permission help - 显示此帮助 • /permission help - 显示此帮助
@@ -127,16 +110,26 @@ class PermissionCommand(BaseCommand):
📝 示例: 📝 示例:
• /permission grant @张三 plugin.example.command • /permission grant @张三 plugin.example.command
• /permission list 123456789 • /permission list 123456789
• /permission nodes example_plugin""" • /permission nodes example_plugin
• /permission allnodes
🔄 别名:可以使用 /perm 或 /权限 代替 /permission"""
await self.send_text(help_text) await self.send_text(help_text)
def _parse_user_mention(self, mention: str) -> Optional[str]: def _parse_user_mention(self, mention: str) -> Optional[str]:
"""解析用户提及提取QQ号""" """解析用户提及提取QQ号
# 匹配 @用户 格式提取QQ号
at_match = re.search(r'\[CQ:at,qq=(\d+)\]', mention) 支持的格式:
- @<用户名:QQ号> 格式
- [CQ:at,qq=QQ号] 格式
- 直接的QQ号
"""
# 匹配 @<用户名:QQ号> 格式提取QQ号
at_match = re.search(r'@<[^:]+:(\d+)>', mention)
if at_match: if at_match:
return at_match.group(1) return at_match.group(1)
# 直接是数字 # 直接是数字
if mention.isdigit(): if mention.isdigit():
@@ -144,62 +137,94 @@ class PermissionCommand(BaseCommand):
return None return None
async def _grant_permission(self, chat_stream , args: List[str]): @staticmethod
def parse_user_from_args(args: CommandArgs, index: int = 0) -> Optional[str]:
"""从CommandArgs中解析用户ID
Args:
args: 命令参数对象
index: 参数索引默认为0第一个参数
Returns:
Optional[str]: 解析出的用户ID如果解析失败返回None
"""
if index >= args.count():
return None
mention = args.get_arg(index)
# 匹配 @<用户名:QQ号> 格式提取QQ号
at_match = re.search(r'@<[^:]+:(\d+)>', mention)
if at_match:
return at_match.group(1)
# 匹配传统的 [CQ:at,qq=数字] 格式
cq_match = re.search(r'\[CQ:at,qq=(\d+)\]', mention)
if cq_match:
return cq_match.group(1)
# 直接是数字
if mention.isdigit():
return mention
return None
@require_permission("plugin.permission.manage", "❌ 你没有权限管理的权限")
async def _grant_permission(self, chat_stream, args: List[str]):
"""授权用户权限""" """授权用户权限"""
if len(args) < 2: if len(args) < 2:
await self.send_text("❌ 用法: /permission grant <@用户|QQ号> <权限节点>") await self.send_text("❌ 用法: /permission grant <@用户|QQ号> <权限节点>")
return return
user_mention = args[0] # 解析用户ID - 使用新的解析方法
permission_node = args[1] user_id = self._parse_user_mention(args[0])
# 解析用户ID
user_id = self._parse_user_mention(user_mention)
if not user_id: if not user_id:
await self.send_text("❌ 无效的用户格式,请使用 @用户 或直接输入QQ号") await self.send_text("❌ 无效的用户格式,请使用 @<用户名:QQ号> 或直接输入QQ号")
return return
permission_node = args[1]
# 执行授权 # 执行授权
success = permission_api.grant_permission(chat_stream.platform, user_id, permission_node) success = permission_api.grant_permission(chat_stream.platform, user_id, permission_node)
if success: if success:
await self.send_text(f"✅ 已授权用户 {user_id} 权限节点 {permission_node}") await self.send_text(f"✅ 已授权用户 {user_id} 权限节点 `{permission_node}`")
else: else:
await self.send_text("❌ 授权失败,请检查权限节点是否存在") await self.send_text("❌ 授权失败,请检查权限节点是否存在")
@require_permission("plugin.permission.manage", "❌ 你没有权限管理的权限")
async def _revoke_permission(self, chat_stream, args: List[str]): async def _revoke_permission(self, chat_stream, args: List[str]):
"""撤销用户权限""" """撤销用户权限"""
if len(args) < 2: if len(args) < 2:
await self.send_text("❌ 用法: /permission revoke <@用户|QQ号> <权限节点>") await self.send_text("❌ 用法: /permission revoke <@用户|QQ号> <权限节点>")
return return
user_mention = args[0] # 解析用户ID - 使用新的解析方法
permission_node = args[1] user_id = self._parse_user_mention(args[0])
# 解析用户ID
user_id = self._parse_user_mention(user_mention)
if not user_id: if not user_id:
await self.send_text("❌ 无效的用户格式,请使用 @用户 或直接输入QQ号") await self.send_text("❌ 无效的用户格式,请使用 @<用户名:QQ号> 或直接输入QQ号")
return return
permission_node = args[1]
# 执行撤销 # 执行撤销
success = permission_api.revoke_permission(chat_stream.platform, user_id, permission_node) success = permission_api.revoke_permission(chat_stream.platform, user_id, permission_node)
if success: if success:
await self.send_text(f"✅ 已撤销用户 {user_id} 权限节点 {permission_node}") await self.send_text(f"✅ 已撤销用户 {user_id} 权限节点 `{permission_node}`")
else: else:
await self.send_text("❌ 撤销失败,请检查权限节点是否存在") await self.send_text("❌ 撤销失败,请检查权限节点是否存在")
@require_permission("plugin.permission.view", "❌ 你没有查看权限的权限")
async def _list_permissions(self, chat_stream, args: List[str]): async def _list_permissions(self, chat_stream, args: List[str]):
"""列出用户权限""" """列出用户权限"""
target_user_id = None target_user_id = None
if args: if args:
# 指定了用户 # 指定了用户 - 使用新的解析方法
user_mention = args[0] target_user_id = self._parse_user_mention(args[0])
target_user_id = self._parse_user_mention(user_mention)
if not target_user_id: if not target_user_id:
await self.send_text("❌ 无效的用户格式,请使用 @用户 或直接输入QQ号") await self.send_text("❌ 无效的用户格式,请使用 @<用户名:QQ号> 或直接输入QQ号")
return return
else: else:
# 查看自己的权限 # 查看自己的权限
@@ -212,45 +237,46 @@ class PermissionCommand(BaseCommand):
permissions = permission_api.get_user_permissions(chat_stream.platform, target_user_id) permissions = permission_api.get_user_permissions(chat_stream.platform, target_user_id)
if is_master: if is_master:
response = f"👑 用户 {target_user_id} 是Master用户拥有所有权限" response = f"👑 用户 `{target_user_id}` 是Master用户拥有所有权限"
else: else:
if permissions: if permissions:
perm_list = "\n".join([f"{perm}" for perm in permissions]) perm_list = "\n".join([f"`{perm}`" for perm in permissions])
response = f"📋 用户 {target_user_id} 拥有的权限:\n{perm_list}" response = f"📋 用户 `{target_user_id}` 拥有的权限:\n{perm_list}"
else: else:
response = f"📋 用户 {target_user_id} 没有任何权限" response = f"📋 用户 `{target_user_id}` 没有任何权限"
await self.send_text(response) await self.send_text(response)
@require_permission("plugin.permission.view", "❌ 你没有查看权限的权限")
async def _check_permission(self, chat_stream, args: List[str]): async def _check_permission(self, chat_stream, args: List[str]):
"""检查用户权限""" """检查用户权限"""
if len(args) < 2: if len(args) < 2:
await self.send_text("❌ 用法: /permission check <@用户|QQ号> <权限节点>") await self.send_text("❌ 用法: /permission check <@用户|QQ号> <权限节点>")
return return
user_mention = args[0] # 解析用户ID - 使用新的解析方法
permission_node = args[1] user_id = self._parse_user_mention(args[0])
# 解析用户ID
user_id = self._parse_user_mention(user_mention)
if not user_id: if not user_id:
await self.send_text("❌ 无效的用户格式,请使用 @用户 或直接输入QQ号") await self.send_text("❌ 无效的用户格式,请使用 @<用户名:QQ号> 或直接输入QQ号")
return return
permission_node = args[1]
# 检查权限 # 检查权限
has_permission = permission_api.check_permission(chat_stream.platform, user_id, permission_node) has_permission = permission_api.check_permission(chat_stream.platform, user_id, permission_node)
is_master = permission_api.is_master(chat_stream.platform, user_id) is_master = permission_api.is_master(chat_stream.platform, user_id)
if has_permission: if has_permission:
if is_master: if is_master:
response = f"✅ 用户 {user_id} 拥有权限 {permission_node}Master用户" response = f"✅ 用户 `{user_id}` 拥有权限 `{permission_node}`Master用户"
else: else:
response = f"✅ 用户 {user_id} 拥有权限 {permission_node}" response = f"✅ 用户 `{user_id}` 拥有权限 `{permission_node}`"
else: else:
response = f"❌ 用户 {user_id} 没有权限 {permission_node}" response = f"❌ 用户 `{user_id}` 没有权限 `{permission_node}`"
await self.send_text(response) await self.send_text(response)
@require_permission("plugin.permission.view", "❌ 你没有查看权限的权限")
async def _list_nodes(self, chat_stream, args: List[str]): async def _list_nodes(self, chat_stream, args: List[str]):
"""列出权限节点""" """列出权限节点"""
plugin_name = args[0] if args else None plugin_name = args[0] if args else None
@@ -283,6 +309,74 @@ class PermissionCommand(BaseCommand):
await self.send_text(response) await self.send_text(response)
@require_permission("plugin.permission.view", "❌ 你没有查看权限的权限")
async def _list_all_nodes_with_description(self, chat_stream):
"""列出所有插件的权限节点(带详细描述)"""
# 获取所有权限节点
all_nodes = permission_api.get_all_permission_nodes()
if not all_nodes:
response = "📋 系统中没有任何权限节点"
await self.send_text(response)
return
# 按插件名分组节点
plugins_dict = {}
for node in all_nodes:
plugin_name = node["plugin_name"]
if plugin_name not in plugins_dict:
plugins_dict[plugin_name] = []
plugins_dict[plugin_name].append(node)
# 构建响应消息
response_parts = ["📋 所有插件权限节点详情:\n"]
for plugin_name in sorted(plugins_dict.keys()):
nodes = plugins_dict[plugin_name]
response_parts.append(f"🔌 **{plugin_name}** ({len(nodes)}个节点)")
for node in nodes:
default_text = "✅默认授权" if node["default_granted"] else "❌默认拒绝"
response_parts.append(f" • `{node['node_name']}` - {default_text}")
response_parts.append(f" 📄 {node['description']}")
response_parts.append("") # 插件间空行分隔
# 添加统计信息
total_nodes = len(all_nodes)
total_plugins = len(plugins_dict)
response_parts.append(f"📊 统计:共 {total_plugins} 个插件,{total_nodes} 个权限节点")
response = "\n".join(response_parts)
# 如果消息太长,分段发送
if len(response) > 4000: # 预留一些空间避免超出限制
await self._send_long_message(response)
else:
await self.send_text(response)
async def _send_long_message(self, message: str):
"""发送长消息,自动分段"""
lines = message.split('\n')
current_chunk = []
current_length = 0
for line in lines:
line_length = len(line) + 1 # +1 for newline
# 如果添加这一行会超出限制,先发送当前块
if current_length + line_length > 3500 and current_chunk:
await self.send_text('\n'.join(current_chunk))
current_chunk = []
current_length = 0
current_chunk.append(line)
current_length += line_length
# 发送最后一块
if current_chunk:
await self.send_text('\n'.join(current_chunk))
@register_plugin @register_plugin
class PermissionManagerPlugin(BasePlugin): class PermissionManagerPlugin(BasePlugin):
@@ -298,5 +392,6 @@ class PermissionManagerPlugin(BasePlugin):
} }
} }
def get_plugin_components(self) -> List[Tuple[CommandInfo, Type[BaseCommand]]]: def get_plugin_components(self) -> List[Tuple[PlusCommandInfo, Type[PlusCommand]]]:
return [(PermissionCommand.get_command_info(), PermissionCommand)] """返回插件的PlusCommand组件"""
return [(PermissionCommand.get_plus_command_info(), PermissionCommand)]

View File

@@ -12,8 +12,8 @@
"host_application": { "host_application": {
"min_version": "0.8.0" "min_version": "0.8.0"
}, },
"homepage_url": "https://github.com/MaiBot-Plus/MaiMbot-Pro-Max", "homepage_url": "https://github.com/MoFox-Studio/MoFox_Bot",
"repository_url": "https://github.com/MaiBot-Plus/MaiMbot-Pro-Max", "repository_url": "https://github.com/MoFox-Studio/MoFox_Bot",
"keywords": ["poke", "interaction", "fun", "social", "ai-reply", "auto-response"], "keywords": ["poke", "interaction", "fun", "social", "ai-reply", "auto-response"],
"categories": ["Social", "Interactive", "Fun"], "categories": ["Social", "Interactive", "Fun"],

View File

@@ -11,7 +11,6 @@ from bs4 import BeautifulSoup
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system import BaseTool, ToolParamType, llm_api from src.plugin_system import BaseTool, ToolParamType, llm_api
from src.plugin_system.apis import config_api from src.plugin_system.apis import config_api
from src.common.cache_manager import tool_cache
from ..utils.formatters import format_url_parse_results from ..utils.formatters import format_url_parse_results
from ..utils.url_utils import parse_urls_from_input, validate_urls from ..utils.url_utils import parse_urls_from_input, validate_urls
@@ -30,6 +29,12 @@ class URLParserTool(BaseTool):
parameters = [ parameters = [
("urls", ToolParamType.STRING, "要理解的网站", True, None), ("urls", ToolParamType.STRING, "要理解的网站", True, None),
] ]
# --- 新的缓存配置 ---
enable_cache: bool = True
cache_ttl: int = 86400 # 缓存24小时
semantic_cache_query_key: str = "urls"
# --------------------
def __init__(self, plugin_config=None): def __init__(self, plugin_config=None):
super().__init__(plugin_config) super().__init__(plugin_config)
@@ -42,10 +47,11 @@ class URLParserTool(BaseTool):
if exa_api_keys is None: if exa_api_keys is None:
# 从插件配置文件读取 # 从插件配置文件读取
exa_api_keys = self.get_config("exa.api_keys", []) exa_api_keys = self.get_config("exa.api_keys", [])
# 创建API密钥管理器 # 创建API密钥管理器
from typing import cast, List
self.api_manager = create_api_key_manager_from_config( self.api_manager = create_api_key_manager_from_config(
exa_api_keys, cast(List[str], exa_api_keys),
lambda key: Exa(api_key=key), lambda key: Exa(api_key=key),
"Exa URL Parser" "Exa URL Parser"
) )
@@ -135,16 +141,6 @@ class URLParserTool(BaseTool):
""" """
执行URL内容提取和总结。优先使用Exa失败后尝试本地解析。 执行URL内容提取和总结。优先使用Exa失败后尝试本地解析。
""" """
# 获取当前文件路径用于缓存键
import os
current_file_path = os.path.abspath(__file__)
# 检查缓存
cached_result = await tool_cache.get(self.name, function_args, current_file_path)
if cached_result:
logger.info(f"缓存命中: {self.name} -> {function_args}")
return cached_result
urls_input = function_args.get("urls") urls_input = function_args.get("urls")
if not urls_input: if not urls_input:
return {"error": "URL列表不能为空。"} return {"error": "URL列表不能为空。"}
@@ -235,8 +231,4 @@ class URLParserTool(BaseTool):
"errors": error_messages "errors": error_messages
} }
# 保存到缓存
if "error" not in result:
await tool_cache.set(self.name, function_args, current_file_path, result)
return result return result

View File

@@ -7,7 +7,6 @@ from typing import Any, Dict, List
from src.common.logger import get_logger from src.common.logger import get_logger
from src.plugin_system import BaseTool, ToolParamType from src.plugin_system import BaseTool, ToolParamType
from src.plugin_system.apis import config_api from src.plugin_system.apis import config_api
from src.common.cache_manager import tool_cache
from ..engines.exa_engine import ExaSearchEngine from ..engines.exa_engine import ExaSearchEngine
from ..engines.tavily_engine import TavilySearchEngine from ..engines.tavily_engine import TavilySearchEngine
@@ -31,6 +30,12 @@ class WebSurfingTool(BaseTool):
("time_range", ToolParamType.STRING, "指定搜索的时间范围,可以是 'any', 'week', 'month'。默认为 'any'", False, ["any", "week", "month"]) ("time_range", ToolParamType.STRING, "指定搜索的时间范围,可以是 'any', 'week', 'month'。默认为 'any'", False, ["any", "week", "month"])
] # type: ignore ] # type: ignore
# --- 新的缓存配置 ---
enable_cache: bool = True
cache_ttl: int = 7200 # 缓存2小时
semantic_cache_query_key: str = "query"
# --------------------
def __init__(self, plugin_config=None): def __init__(self, plugin_config=None):
super().__init__(plugin_config) super().__init__(plugin_config)
# 初始化搜索引擎 # 初始化搜索引擎
@@ -46,16 +51,6 @@ class WebSurfingTool(BaseTool):
if not query: if not query:
return {"error": "搜索查询不能为空。"} return {"error": "搜索查询不能为空。"}
# 获取当前文件路径用于缓存键
import os
current_file_path = os.path.abspath(__file__)
# 检查缓存
cached_result = await tool_cache.get(self.name, function_args, current_file_path, semantic_query=query)
if cached_result:
logger.info(f"缓存命中: {self.name} -> {function_args}")
return cached_result
# 读取搜索配置 # 读取搜索配置
enabled_engines = config_api.get_global_config("web_search.enabled_engines", ["ddg"]) enabled_engines = config_api.get_global_config("web_search.enabled_engines", ["ddg"])
search_strategy = config_api.get_global_config("web_search.search_strategy", "single") search_strategy = config_api.get_global_config("web_search.search_strategy", "single")
@@ -69,10 +64,6 @@ class WebSurfingTool(BaseTool):
result = await self._execute_fallback_search(function_args, enabled_engines) result = await self._execute_fallback_search(function_args, enabled_engines)
else: # single else: # single
result = await self._execute_single_search(function_args, enabled_engines) result = await self._execute_single_search(function_args, enabled_engines)
# 保存到缓存
if "error" not in result:
await tool_cache.set(self.name, function_args, current_file_path, result, semantic_query=query)
return result return result

View File

@@ -1,106 +1,331 @@
# mmc/src/schedule/monthly_plan_manager.py # mmc/src/manager/monthly_plan_manager.py
# 我要混提交
import datetime import asyncio
from src.config.config import global_config from datetime import datetime, timedelta
from src.common.database.monthly_plan_db import get_active_plans_for_month, add_new_plans from typing import List, Optional
from src.schedule.plan_generator import PlanGenerator
from src.common.database.monthly_plan_db import (
add_new_plans,
get_archived_plans_for_month,
archive_active_plans_for_month,
has_active_plans,
get_active_plans_for_month,
delete_plans_by_ids
)
from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger from src.common.logger import get_logger
from src.manager.async_task_manager import AsyncTask, async_task_manager
logger = get_logger("monthly_plan_manager") logger = get_logger("monthly_plan_manager")
# 默认的月度计划生成指导原则
DEFAULT_MONTHLY_PLAN_GUIDELINES = """
我希望你能为自己制定一些有意义的月度小目标和计划。
这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。
每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。
请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。
"""
class MonthlyPlanManager: class MonthlyPlanManager:
"""月度计划管理器
负责月度计划的生成、管理和生命周期控制。
与 ScheduleManager 解耦,专注于月度层面的计划管理。
""" """
管理月度计划的生成和填充。
""" def __init__(self):
self.llm = LLMRequest(
model_set=model_config.model_task_config.schedule_generator,
request_type="monthly_plan"
)
self.generation_running = False
self.monthly_task_started = False
@staticmethod async def start_monthly_plan_generation(self):
async def initialize_monthly_plans(): """启动每月初自动生成新月度计划的任务,并在启动时检查一次"""
""" if not self.monthly_task_started:
程序启动时调用,检查并按需填充当月的计划池。 logger.info(" 正在启动每月月度计划生成任务...")
""" task = MonthlyPlanGenerationTask(self)
config = global_config.monthly_plan_system await async_task_manager.add_task(task)
if not config or not config.enable: self.monthly_task_started = True
logger.info("月层计划系统未启用,跳过初始化") logger.info(" 每月月度计划生成任务已成功启动")
return
# 启动时立即检查并按需生成
logger.info(" 执行启动时月度计划检查...")
await self.ensure_and_generate_plans_if_needed()
else:
logger.info(" 每月月度计划生成任务已在运行中。")
now = datetime.datetime.now() async def ensure_and_generate_plans_if_needed(self, target_month: Optional[str] = None) -> bool:
current_month_str = now.strftime("%Y-%m") """
确保指定月份有计划,如果没有则触发生成。
这是按需生成的主要入口点。
"""
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
if not has_active_plans(target_month):
logger.info(f" {target_month} 没有任何有效的月度计划,将立即生成。")
return await self.generate_monthly_plans(target_month)
else:
logger.info(f"{target_month} 已存在有效的月度计划。")
plans = get_active_plans_for_month(target_month)
# 检查是否超出上限
max_plans = global_config.monthly_plan_system.max_plans_per_month
if len(plans) > max_plans:
logger.warning(f"当前月度计划数量 ({len(plans)}) 超出上限 ({max_plans}),将自动删除多余的计划。")
# 按创建时间升序排序(旧的在前),然后删除超出上限的部分(新的)
plans_to_delete = sorted(plans, key=lambda p: p.created_at)[max_plans:]
delete_ids = [p.id for p in plans_to_delete]
delete_plans_by_ids(delete_ids)
# 重新获取计划列表
plans = get_active_plans_for_month(target_month)
if plans:
plan_texts = "\n".join([f" {i+1}. {plan.plan_text}" for i, plan in enumerate(plans)])
logger.info(f"当前月度计划内容:\n{plan_texts}")
return True # 已经有计划,也算成功
async def generate_monthly_plans(self, target_month: Optional[str] = None) -> bool:
"""
生成指定月份的月度计划
:param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None则为当前月份。
:return: 是否生成成功
"""
if self.generation_running:
logger.info("月度计划生成任务已在运行中,跳过重复启动")
return False
self.generation_running = True
try: try:
# 1. 检查当月已有计划数量 # 确定目标月份
existing_plans = get_active_plans_for_month(current_month_str) if target_month is None:
plan_count = len(existing_plans) target_month = datetime.now().strftime("%Y-%m")
header = "📅 月度计划检查" logger.info(f"开始为 {target_month} 生成月度计划...")
# 2. 判断是否需要生成新计划 # 检查是否启用月度计划系统
if plan_count >= config.generation_threshold: if not global_config.monthly_plan_system or not global_config.monthly_plan_system.enable:
summary = f"计划数量充足 ({plan_count}/{config.generation_threshold}),无需生成。" logger.info(" 月度计划系统已禁用,跳过计划生成。")
log_message = ( return False
f"\n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n"
f"{header}\n"
f"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫\n"
f"┃ 月份: {current_month_str}\n"
f"┃ 状态: {summary}\n"
f"┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
)
logger.info(log_message)
return
# 3. 计算需要生成的计划数量并调用生成器
needed_plans = config.generation_threshold - plan_count
summary = f"计划不足 ({plan_count}/{config.generation_threshold}),需要生成 {needed_plans} 条。"
generation_info = f"即将生成 {config.plans_per_generation} 条新计划..."
log_message = (
f"\n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n"
f"{header}\n"
f"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫\n"
f"┃ 月份: {current_month_str}\n"
f"┃ 状态: {summary}\n"
f"┃ 操作: {generation_info}\n"
f"┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
)
logger.info(log_message)
generator = PlanGenerator() # 获取上个月的归档计划作为参考
new_plans = await generator.generate_plans( last_month = self._get_previous_month(target_month)
year=now.year, archived_plans = get_archived_plans_for_month(last_month)
month=now.month,
count=config.plans_per_generation # 每次生成固定数量以保证质量 # 构建生成 Prompt
) prompt = self._build_generation_prompt(target_month, archived_plans)
# 4. 将新计划存入数据库 # 调用 LLM 生成计划
if new_plans: plans = await self._generate_plans_with_llm(prompt)
add_new_plans(new_plans, current_month_str)
completion_header = "✅ 月度计划生成完毕" if plans:
completion_summary = f"成功添加 {len(new_plans)} 条新计划。" # 保存到数据库
add_new_plans(plans, target_month)
# 构建计划详情 logger.info(f"成功为 {target_month} 生成并保存了 {len(plans)} 条月度计划。")
plan_details = "\n".join([f"┃ - {plan}" for plan in new_plans]) return True
log_message = (
f"\n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n"
f"{completion_header}\n"
f"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫\n"
f"┃ 月份: {current_month_str}\n"
f"┃ 结果: {completion_summary}\n"
f"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫\n"
f"{plan_details}\n"
f"┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
)
logger.info(log_message)
else: else:
completion_header = "❌ 月度计划生成失败" logger.warning(f"未能为 {target_month} 生成有效的月度计划。")
completion_summary = "未能生成任何新的月度计划。" return False
log_message = (
f"\n┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓\n"
f"{completion_header}\n"
f"┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫\n"
f"┃ 月份: {current_month_str}\n"
f"┃ 结果: {completion_summary}\n"
f"┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛"
)
logger.warning(log_message)
except Exception as e: except Exception as e:
logger.error(f"初始化月度计划时发生严重错误: {e}", exc_info=True) logger.error(f" 生成 {target_month} 月度计划时发生错误: {e}")
return False
finally:
self.generation_running = False
def _get_previous_month(self, current_month: str) -> str:
"""获取上个月的月份字符串"""
try:
year, month = map(int, current_month.split('-'))
if month == 1:
return f"{year-1}-12"
else:
return f"{year}-{month-1:02d}"
except Exception:
# 如果解析失败,返回一个不存在的月份
return "1900-01"
def _build_generation_prompt(self, target_month: str, archived_plans: List) -> str:
"""构建月度计划生成的 Prompt"""
# 获取配置
guidelines = getattr(global_config.monthly_plan_system, 'guidelines', None) or DEFAULT_MONTHLY_PLAN_GUIDELINES
personality = global_config.personality.personality_core
personality_side = global_config.personality.personality_side
max_plans = global_config.monthly_plan_system.max_plans_per_month
# 构建上月未完成计划的参考信息
archived_plans_block = ""
if archived_plans:
archived_texts = [f"- {plan.plan_text}" for plan in archived_plans[:5]] # 最多显示5个
archived_plans_block = f"""
**上个月未完成的一些计划(可作为参考)**:
{chr(10).join(archived_texts)}
你可以考虑是否要在这个月继续推进这些计划,或者制定全新的计划。
"""
prompt = f"""
我,{global_config.bot.nickname},需要为自己制定 {target_month} 的月度计划。
**关于我**:
- **核心人设**: {personality}
- **具体习惯与兴趣**:
{personality_side}
{archived_plans_block}
**我的月度计划制定原则**:
{guidelines}
**重要要求**:
1. 请为我生成 {max_plans} 条左右的月度计划
2. 每条计划都应该是一句话,简洁明了,具体可行
3. 计划应该涵盖不同的生活方面(学习、娱乐、社交、个人成长等)
4. 返回格式必须是纯文本,每行一条计划,不要使用 JSON 或其他格式
5. 不要包含任何解释性文字,只返回计划列表
**示例格式**:
学习一门新的编程语言或技术
每周至少看两部有趣的电影
与朋友们组织一次户外活动
阅读3本感兴趣的书籍
尝试制作一道新的料理
请你扮演我,以我的身份和兴趣,为 {target_month} 制定合适的月度计划。
"""
return prompt
async def _generate_plans_with_llm(self, prompt: str) -> List[str]:
"""使用 LLM 生成月度计划列表"""
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
logger.info(f" 正在生成月度计划 (第 {attempt} 次尝试)")
response, _ = await self.llm.generate_response_async(prompt)
# 解析响应
plans = self._parse_plans_response(response)
if plans:
logger.info(f"成功生成 {len(plans)} 条月度计划")
return plans
else:
logger.warning(f"{attempt} 次生成的计划为空,继续重试...")
except Exception as e:
logger.error(f"{attempt} 次生成月度计划失败: {e}")
# 添加短暂延迟,避免过于频繁的请求
if attempt < max_retries:
await asyncio.sleep(2)
logger.error(" 所有尝试都失败,无法生成月度计划")
return []
def _parse_plans_response(self, response: str) -> List[str]:
"""解析 LLM 响应,提取计划列表"""
try:
# 清理响应文本
response = response.strip()
# 按行分割
lines = [line.strip() for line in response.split('\n') if line.strip()]
# 过滤掉明显不是计划的行(比如包含特殊标记的行)
plans = []
for line in lines:
# 跳过包含特殊标记的行
if any(marker in line for marker in ['**', '##', '```', '---', '===', '###']):
continue
# 移除可能的序号前缀
line = line.lstrip('0123456789.- ')
# 确保计划不为空且有意义
if len(line) > 5 and not line.startswith(('', '以上', '总结', '注意')):
plans.append(line)
# 限制计划数量
max_plans = global_config.monthly_plan_system.max_plans_per_month
if len(plans) > max_plans:
plans = plans[:max_plans]
return plans
except Exception as e:
logger.error(f"解析月度计划响应时发生错误: {e}")
return []
async def archive_current_month_plans(self, target_month: Optional[str] = None):
"""
归档当前月份的活跃计划
:param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None则为当前月份。
"""
try:
if target_month is None:
target_month = datetime.now().strftime("%Y-%m")
logger.info(f" 开始归档 {target_month} 的活跃月度计划...")
archived_count = archive_active_plans_for_month(target_month)
logger.info(f" 成功归档了 {archived_count}{target_month} 的月度计划。")
except Exception as e:
logger.error(f" 归档 {target_month} 月度计划时发生错误: {e}")
class MonthlyPlanGenerationTask(AsyncTask):
"""每月初自动生成新月度计划的任务"""
def __init__(self, monthly_plan_manager: MonthlyPlanManager):
super().__init__(task_name="MonthlyPlanGenerationTask")
self.monthly_plan_manager = monthly_plan_manager
async def run(self):
while True:
try:
# 计算到下个月1号凌晨的时间
now = datetime.now()
# 获取下个月的第一天
if now.month == 12:
next_month = datetime(now.year + 1, 1, 1)
else:
next_month = datetime(now.year, now.month + 1, 1)
sleep_seconds = (next_month - now).total_seconds()
logger.info(f" 下一次月度计划生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {next_month.strftime('%Y-%m-%d %H:%M:%S')})")
# 等待直到下个月1号
await asyncio.sleep(sleep_seconds)
# 先归档上个月的计划
last_month = (next_month - timedelta(days=1)).strftime("%Y-%m")
await self.monthly_plan_manager.archive_current_month_plans(last_month)
# 生成新月份的计划
current_month = next_month.strftime("%Y-%m")
logger.info(f" 到达月初,开始生成 {current_month} 的月度计划...")
await self.monthly_plan_manager.generate_monthly_plans(current_month)
except asyncio.CancelledError:
logger.info(" 每月月度计划生成任务被取消。")
break
except Exception as e:
logger.error(f" 每月月度计划生成任务发生未知错误: {e}")
# 发生错误后等待1小时再重试避免频繁失败
await asyncio.sleep(3600)
# 全局实例
monthly_plan_manager = MonthlyPlanManager()

View File

@@ -1,116 +0,0 @@
# mmc/src/schedule/plan_generator.py
import orjson
from typing import List
from pydantic import BaseModel, ValidationError
from json_repair import repair_json
from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger
logger = get_logger("plan_generator")
class PlanResponse(BaseModel):
"""
用于验证月度计划LLM响应的Pydantic模型。
"""
plans: List[str]
class PlanGenerator:
"""
负责生成月度计划。
"""
def __init__(self):
self.bot_personality = self._get_bot_personality()
task_config = model_config.model_task_config.get_task("monthly_plan_generator")
self.llm_request = LLMRequest(model_set=task_config, request_type="monthly_plan_generator")
def _get_bot_personality(self) -> str:
"""
从全局配置中获取Bot的人设描述。
"""
core = global_config.personality.personality_core or ""
side = global_config.personality.personality_side or ""
identity = global_config.personality.identity or ""
return f"核心人设: {core}\n侧面人设: {side}\n身份设定: {identity}"
def _build_prompt(self, year: int, month: int, count: int) -> str:
"""
构建用于生成月度计划的Prompt。
"""
prompt_template = f"""
你是一个富有想象力的助手,你的任务是为一位虚拟角色生成月度计划。
**角色设定:**
---
{self.bot_personality}
---
请为即将到来的 **{year}{month}月** 设计 **{count}** 个符合该角色身份的、独立的、积极向上的月度计划或小目标。
**要求:**
1. 每个计划都应简短、清晰,用一两句话描述。
2. 语言风格必须自然、口语化,严格符合角色的性格设定。
3. 计划内容要具有创造性,避免陈词滥调。
4. 请以严格的JSON格式返回格式为{{"plans": ["计划一", "计划二", ...]}}
5. 除了JSON对象不要包含任何额外的解释、注释或前后导语。
"""
return prompt_template.strip()
async def generate_plans(self, year: int, month: int, count: int) -> List[str]:
"""
调用LLM生成指定月份的计划。
:param year: 年份
:param month: 月份
:param count: 需要生成的计划数量
:return: 生成的计划文本列表
"""
try:
# 1. 构建Prompt
prompt = self._build_prompt(year, month, count)
logger.info(f"正在为 {year}-{month} 生成 {count} 个月度计划...")
# 2. 调用LLM
llm_content, (reasoning, model_name, _) = await self.llm_request.generate_response_async(prompt=prompt)
logger.info(f"使用模型 '{model_name}' 生成完成。")
if reasoning:
logger.debug(f"模型推理过程: {reasoning}")
if not llm_content:
logger.error("LLM未能返回有效的计划内容。")
return []
# 3. 解析并验证LLM返回的JSON
try:
# 移除可能的Markdown代码块标记
clean_content = llm_content.strip()
if clean_content.startswith("```json"):
clean_content = clean_content[7:]
if clean_content.endswith("```"):
clean_content = clean_content[:-3]
# 修复并解析JSON
repaired_json_str = repair_json(clean_content)
data = orjson.loads(repaired_json_str)
# 使用Pydantic进行验证
validated_response = PlanResponse.model_validate(data)
plans = validated_response.plans
logger.info(f"成功生成并验证了 {len(plans)} 个月度计划。")
return plans
except orjson.JSONDecodeError:
logger.error(f"修复后仍然无法解析LLM返回的JSON: {llm_content}")
return []
except ValidationError as e:
logger.error(f"LLM返回的JSON格式不符合预期: {e}\n原始响应: {llm_content}")
return []
except Exception as e:
logger.error(f"调用LLM生成月度计划时发生未知错误: {e}", exc_info=True)
return []

View File

@@ -1,5 +1,6 @@
import orjson import orjson
import asyncio import asyncio
import random
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from lunar_python import Lunar from lunar_python import Lunar
@@ -15,6 +16,8 @@ from src.llm_models.utils_model import LLMRequest
from src.common.logger import get_logger from src.common.logger import get_logger
from json_repair import repair_json from json_repair import repair_json
from src.manager.async_task_manager import AsyncTask, async_task_manager from src.manager.async_task_manager import AsyncTask, async_task_manager
from src.manager.local_store_manager import local_storage
from src.plugin_system.apis import send_api, generator_api
logger = get_logger("schedule_manager") logger = get_logger("schedule_manager")
@@ -128,6 +131,17 @@ class ScheduleManager:
self.sleep_log_interval = 35 # 日志记录间隔,单位秒 self.sleep_log_interval = 35 # 日志记录间隔,单位秒
self.schedule_generation_running = False # 防止重复生成任务 self.schedule_generation_running = False # 防止重复生成任务
# 弹性睡眠相关状态
self._is_preparing_sleep: bool = False
self._sleep_buffer_end_time: Optional[datetime] = None
self._total_delayed_minutes_today: int = 0
self._last_sleep_check_date: Optional[datetime.date] = None
self._last_fully_slept_log_time: float = 0
self._is_in_voluntary_delay: bool = False # 新增:标记是否处于主动延迟睡眠状态
self._is_woken_up: bool = False # 新增:标记是否被吵醒
self._load_sleep_state()
async def start_daily_schedule_generation(self): async def start_daily_schedule_generation(self):
"""启动每日零点自动生成新日程的任务""" """启动每日零点自动生成新日程的任务"""
if not self.daily_task_started: if not self.daily_task_started:
@@ -226,7 +240,7 @@ class ScheduleManager:
# 如果计划耗尽,则触发补充生成 # 如果计划耗尽,则触发补充生成
if not sampled_plans: if not sampled_plans:
logger.info("可用的月度计划已耗尽或不足,尝试进行补充生成...") logger.info("可用的月度计划已耗尽或不足,尝试进行补充生成...")
from src.manager.monthly_plan_manager import monthly_plan_manager from mmc.src.schedule.monthly_plan_manager import monthly_plan_manager
success = await monthly_plan_manager.generate_monthly_plans(current_month_str) success = await monthly_plan_manager.generate_monthly_plans(current_month_str)
if success: if success:
logger.info("补充生成完成,重新抽取月度计划...") logger.info("补充生成完成,重新抽取月度计划...")
@@ -392,27 +406,118 @@ class ScheduleManager:
continue continue
return None return None
def is_sleeping(self, wakeup_manager=None) -> bool: def is_sleeping(self, wakeup_manager: Optional["WakeUpManager"] = None) -> bool:
""" """
通过关键词匹配检查当前是否处于休眠时间 通过关键词匹配唤醒度睡眠压力等综合判断是否处于休眠时间
新增弹性睡眠机制允许在压力低时延迟入睡并在入睡前发送通知
Args:
wakeup_manager: 可选的唤醒度管理器用于检查是否被唤醒
Returns:
bool: 是否处于休眠状态
""" """
from src.chat.chat_loop.wakeup_manager import WakeUpManager
# --- 基础检查 ---
if not global_config.schedule.enable_is_sleep: if not global_config.schedule.enable_is_sleep:
return False return False
if not self.today_schedule: if not self.today_schedule:
return False return False
# 从配置获取关键词,如果配置中没有则使用默认列表 now = datetime.now()
sleep_keywords = ["休眠", "睡觉", "梦乡",] today = now.date()
now = datetime.now().time()
# 遍历当天的所有日程 # --- 每日状态重置 ---
if self._last_sleep_check_date != today:
logger.info(f"新的一天 ({today}),重置弹性睡眠状态。")
self._total_delayed_minutes_today = 0
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._last_sleep_check_date = today
self._is_in_voluntary_delay = False
self._save_sleep_state()
# --- 检查是否在“准备入睡”的缓冲期 ---
if self._is_preparing_sleep and self._sleep_buffer_end_time:
if now >= self._sleep_buffer_end_time:
current_timestamp = now.timestamp()
if current_timestamp - self._last_fully_slept_log_time > 45:
logger.info("睡眠缓冲期结束,正式进入休眠状态。")
self._last_fully_slept_log_time = current_timestamp
return True
else:
remaining_seconds = (self._sleep_buffer_end_time - now).total_seconds()
logger.debug(f"处于入睡缓冲期,剩余 {remaining_seconds:.1f} 秒。")
return False
# --- 判断当前是否为理论上的睡眠时间 ---
is_in_theoretical_sleep, activity = self._is_in_theoretical_sleep_time(now.time())
if not is_in_theoretical_sleep:
# 如果不在理论睡眠时间,确保重置准备状态
if self._is_preparing_sleep:
logger.info("已离开理论休眠时间,取消“准备入睡”状态。")
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._is_in_voluntary_delay = False
self._is_woken_up = False # 离开睡眠时间,重置唤醒状态
self._save_sleep_state()
return False
# --- 处理唤醒状态 ---
if self._is_woken_up:
current_timestamp = now.timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒,保持清醒状态。")
self.last_sleep_log_time = current_timestamp
return False
# --- 核心:弹性睡眠逻辑 ---
if global_config.schedule.enable_flexible_sleep and not self._is_preparing_sleep:
# 首次进入理论睡眠时间,触发弹性判断
logger.info(f"进入理论休眠时间 '{activity}',开始弹性睡眠判断...")
# 1. 获取睡眠压力
sleep_pressure = wakeup_manager.context.sleep_pressure if wakeup_manager else 999
pressure_threshold = global_config.schedule.flexible_sleep_pressure_threshold
# 2. 判断是否延迟
if sleep_pressure < pressure_threshold and self._total_delayed_minutes_today < global_config.schedule.max_sleep_delay_minutes:
delay_minutes = 15 # 每次延迟15分钟
self._total_delayed_minutes_today += delay_minutes
self._sleep_buffer_end_time = now + timedelta(minutes=delay_minutes)
self._is_in_voluntary_delay = True # 标记进入主动延迟
logger.info(f"睡眠压力 ({sleep_pressure:.1f}) 低于阈值 ({pressure_threshold}),延迟入睡 {delay_minutes} 分钟。今日已累计延迟 {self._total_delayed_minutes_today} 分钟。")
else:
# 3. 计算5-10分钟的入睡缓冲
self._is_in_voluntary_delay = False # 非主动延迟
buffer_seconds = random.randint(5 * 60, 10 * 60)
self._sleep_buffer_end_time = now + timedelta(seconds=buffer_seconds)
logger.info(f"睡眠压力正常或已达今日最大延迟,将在 {buffer_seconds / 60:.1f} 分钟内入睡。")
# 4. 发送睡前通知
if global_config.schedule.enable_pre_sleep_notification:
asyncio.create_task(self._send_pre_sleep_notification())
self._is_preparing_sleep = True
self._save_sleep_state()
return False # 进入准备阶段,但尚未正式入睡
# --- 经典模式或已在弹性睡眠流程中 ---
current_timestamp = now.timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"当前处于休眠活动 '{activity}' 中 (经典模式)。")
self.last_sleep_log_time = current_timestamp
return True
def reset_sleep_state_after_wakeup(self):
"""被唤醒后重置睡眠状态"""
if self._is_preparing_sleep or self.is_sleeping():
logger.info("被唤醒,重置所有睡眠准备状态,恢复清醒!")
self._is_preparing_sleep = False
self._sleep_buffer_end_time = None
self._is_in_voluntary_delay = False
self._is_woken_up = True # 标记为已被唤醒
self._save_sleep_state()
def _is_in_theoretical_sleep_time(self, now_time: time) -> (bool, Optional[str]):
"""检查当前时间是否落在日程表的任何一个睡眠活动中"""
sleep_keywords = ["休眠", "睡觉", "梦乡"]
for event in self.today_schedule: for event in self.today_schedule:
try: try:
activity = event.get("activity", "").strip() activity = event.get("activity", "").strip()
@@ -421,47 +526,130 @@ class ScheduleManager:
if not activity or not time_range: if not activity or not time_range:
continue continue
# 1. 检查活动内容是否包含任一休眠关键词
if any(keyword in activity for keyword in sleep_keywords): if any(keyword in activity for keyword in sleep_keywords):
# 2. 如果包含,再检查当前时间是否在该时间段内
start_str, end_str = time_range.split('-') start_str, end_str = time_range.split('-')
start_time = datetime.strptime(start_str.strip(), "%H:%M").time() start_time = datetime.strptime(start_str.strip(), "%H:%M").time()
end_time = datetime.strptime(end_str.strip(), "%H:%M").time() end_time = datetime.strptime(end_str.strip(), "%H:%M").time()
is_in_time_range = False
if start_time <= end_time: # 同一天 if start_time <= end_time: # 同一天
if start_time <= now < end_time: if start_time <= now_time < end_time:
is_in_time_range = True return True, activity
else: # 跨天 else: # 跨天
if now >= start_time or now < end_time: if now_time >= start_time or now_time < end_time:
is_in_time_range = True return True, activity
# 如果时间匹配,则进入最终判断
if is_in_time_range:
# 检查是否被唤醒
if wakeup_manager and wakeup_manager.is_in_angry_state():
current_timestamp = datetime.now().timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"在休眠活动 '{activity}' 期间,但已被唤醒。")
self.last_sleep_log_time = current_timestamp
else:
logger.debug(f"在休眠活动 '{activity}' 期间,但已被唤醒。")
return False
current_timestamp = datetime.now().timestamp()
if current_timestamp - self.last_sleep_log_time > self.sleep_log_interval:
logger.info(f"当前处于休眠活动 '{activity}' 中。")
self.last_sleep_log_time = current_timestamp
else:
logger.debug(f"当前处于休眠活动 '{activity}' 中。")
return True # 找到匹配的休眠活动直接返回True
except (ValueError, KeyError, AttributeError) as e: except (ValueError, KeyError, AttributeError) as e:
logger.warning(f"解析日程事件时出错: {event}, 错误: {e}") logger.warning(f"解析日程事件时出错: {event}, 错误: {e}")
continue continue
return False, None
async def _send_pre_sleep_notification(self):
"""异步生成并发送睡前通知"""
try:
groups = global_config.schedule.pre_sleep_notification_groups
prompt = global_config.schedule.pre_sleep_prompt
if not groups:
logger.info("未配置睡前通知的群组,跳过发送。")
return
if not prompt:
logger.warning("睡前通知的prompt为空跳过发送。")
return
# 为防止消息风暴,稍微延迟一下
await asyncio.sleep(random.uniform(5, 15))
for group_id_str in groups:
try:
# 格式 "platform:group_id"
parts = group_id_str.split(":")
if len(parts) != 2:
logger.warning(f"无效的群组ID格式: {group_id_str}")
continue
platform, group_id = parts
# 使用与 ChatStream.get_stream_id 相同的逻辑生成 stream_id
import hashlib
key = "_".join([platform, group_id])
stream_id = hashlib.md5(key.encode()).hexdigest()
logger.info(f"正在为群组 {group_id_str} (Stream ID: {stream_id}) 生成睡前消息...")
# 调用 generator_api 生成回复
success, reply_set, _ = await generator_api.generate_reply(
chat_id=stream_id,
extra_info=prompt,
request_type="schedule.pre_sleep_notification"
)
if success and reply_set:
# 提取文本内容并发送
reply_text = "".join([content for msg_type, content in reply_set if msg_type == "text"])
if reply_text:
logger.info(f"向群组 {group_id_str} 发送睡前消息: {reply_text}")
await send_api.text_to_stream(text=reply_text, stream_id=stream_id)
else:
logger.warning(f"为群组 {group_id_str} 生成的回复内容为空。")
else:
logger.error(f"为群组 {group_id_str} 生成睡前消息失败。")
await asyncio.sleep(random.uniform(2, 5)) # 避免发送过快
except Exception as e:
logger.error(f"向群组 {group_id_str} 发送睡前消息失败: {e}")
except Exception as e:
logger.error(f"发送睡前通知任务失败: {e}")
def _save_sleep_state(self):
"""将当前弹性睡眠状态保存到本地存储"""
try:
state = {
"is_preparing_sleep": self._is_preparing_sleep,
"sleep_buffer_end_time_ts": self._sleep_buffer_end_time.timestamp() if self._sleep_buffer_end_time else None,
"total_delayed_minutes_today": self._total_delayed_minutes_today,
"last_sleep_check_date_str": self._last_sleep_check_date.isoformat() if self._last_sleep_check_date else None,
"is_in_voluntary_delay": self._is_in_voluntary_delay,
"is_woken_up": self._is_woken_up,
}
local_storage["schedule_sleep_state"] = state
logger.debug(f"已保存睡眠状态: {state}")
except Exception as e:
logger.error(f"保存睡眠状态失败: {e}")
def _load_sleep_state(self):
"""从本地存储加载弹性睡眠状态"""
try:
state = local_storage["schedule_sleep_state"]
if state and isinstance(state, dict):
self._is_preparing_sleep = state.get("is_preparing_sleep", False)
# 遍历完所有日程都未找到匹配的休眠活动 end_time_ts = state.get("sleep_buffer_end_time_ts")
return False if end_time_ts:
self._sleep_buffer_end_time = datetime.fromtimestamp(end_time_ts)
self._total_delayed_minutes_today = state.get("total_delayed_minutes_today", 0)
self._is_in_voluntary_delay = state.get("is_in_voluntary_delay", False)
self._is_woken_up = state.get("is_woken_up", False)
date_str = state.get("last_sleep_check_date_str")
if date_str:
self._last_sleep_check_date = datetime.fromisoformat(date_str).date()
logger.info(f"成功从本地存储加载睡眠状态: {state}")
except Exception as e:
logger.warning(f"加载睡眠状态失败,将使用默认值: {e}")
def reset_wakeup_state(self):
"""重置被唤醒的状态,允许重新尝试入睡"""
if self._is_woken_up:
logger.info("重置唤醒状态,将重新尝试入睡。")
self._is_woken_up = False
self._is_preparing_sleep = False # 允许重新进入弹性睡眠判断
self._sleep_buffer_end_time = None
self._save_sleep_state()
def _validate_schedule_with_pydantic(self, schedule_data) -> bool: def _validate_schedule_with_pydantic(self, schedule_data) -> bool:
"""使用Pydantic验证日程数据格式和完整性""" """使用Pydantic验证日程数据格式和完整性"""

View File

@@ -1,7 +1,7 @@
[inner] [inner]
version = "6.5.2" version = "6.5.7"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
#如果你想要修改配置文件请递增version的值 #如果你想要修改配置文件请递增version的值
#如果新增项目请阅读src/config/official_configs.py中的说明 #如果新增项目请阅读src/config/official_configs.py中的说明
# #
@@ -9,7 +9,7 @@ version = "6.5.2"
# 主版本号MMC版本更新 # 主版本号MMC版本更新
# 次版本号:配置文件内容大更新 # 次版本号:配置文件内容大更新
# 修订号:配置文件内容小更新 # 修订号:配置文件内容小更新
#----以上是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以上是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
[database]# 数据库配置 [database]# 数据库配置
database_type = "sqlite" # 数据库类型,支持 "sqlite" 或 "mysql" database_type = "sqlite" # 数据库类型,支持 "sqlite" 或 "mysql"
@@ -44,15 +44,16 @@ connection_timeout = 10 # 连接超时时间(秒)
# Master用户配置拥有最高权限无视所有权限节点 # Master用户配置拥有最高权限无视所有权限节点
# 格式:[[platform, user_id], ...] # 格式:[[platform, user_id], ...]
# 示例:[["qq", "123456"], ["telegram", "user789"]] # 示例:[["qq", "123456"], ["telegram", "user789"]]
master_users = [ master_users = []# ["qq", "123456789"], # 示例QQ平台的Master用户
# ["qq", "123456789"], # 示例QQ平台的Master用户
]
[bot] [bot]
platform = "qq" platform = "qq"
qq_account = 1145141919810 # 麦麦的QQ账号 qq_account = 1145141919810 # MoFox-Bot的QQ账号
nickname = "麦麦" # 麦麦的昵称 nickname = "MoFox-Bot" # MoFox-Bot的昵称
alias_names = ["麦叠", "牢麦"] # 麦麦的别名 alias_names = ["麦叠", "牢麦"] # MoFox-Bot的别名
[command]
command_prefixes = ['/', '!', '.', '#']
[personality] [personality]
# 建议50字以内描述人格的核心特质 # 建议50字以内描述人格的核心特质
@@ -63,7 +64,7 @@ personality_side = "用一句话或几句话描述人格的侧面特质"
# 可以描述外貌,性别,身高,职业,属性等等描述 # 可以描述外貌,性别,身高,职业,属性等等描述
identity = "年龄为19岁,是女孩子,身高为160cm,有黑色的短发" identity = "年龄为19岁,是女孩子,身高为160cm,有黑色的短发"
# 描述麦麦说话的表达风格,表达习惯,如要修改,可以酌情新增内容 # 描述MoFox-Bot说话的表达风格,表达习惯,如要修改,可以酌情新增内容
reply_style = "回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。" reply_style = "回复可以简短一些。可以参考贴吧,知乎和微博的回复风格,回复不要浮夸,不要用夸张修辞,平淡一些。"
#回复的Prompt模式选择s4u为原有s4u样式normal为0.9之前的模式 #回复的Prompt模式选择s4u为原有s4u样式normal为0.9之前的模式
@@ -74,32 +75,46 @@ compress_identity = true # 是否压缩身份,压缩后会精简身份信息
[expression] [expression]
# 表达学习配置 # 表达学习配置
expression_learning = [ # 表达学习配置列表,支持按聊天流配置 # rules是一个列表每个元素都是一个学习规则
["", "enable", "enable", 1.0], # 全局配置使用表达启用学习学习强度1.0 # chat_stream_id: 聊天流ID格式为 "platform:id:type",例如 "qq:123456:private"。空字符串""表示全局配置
["qq:1919810:group", "enable", "enable", 1.5], # 特定群聊配置使用表达启用学习学习强度1.5 # use_expression: 是否使用学到的表达 (true/false)
["qq:114514:private", "enable", "disable", 0.5], # 特定私聊配置使用表达禁用学习学习强度0.5 # learn_expression: 是否学习新的表达 (true/false)
# 格式说明: # learning_strength: 学习强度(浮点数),影响学习频率
# 第一位: chat_stream_id空字符串表示全局配置 # group: 表达共享组的名称(字符串),相同组的聊天会共享学习到的表达方式
# 第二位: 是否使用学到的表达 ("enable"/"disable") [[expression.rules]]
# 第三位: 是否学习表达 ("enable"/"disable") chat_stream_id = ""
# 第四位: 学习强度(浮点数),影响学习频率,最短学习时间间隔 = 300/学习强度(秒) use_expression = true
# 学习强度越高,学习越频繁;学习强度越低,学习越少 learn_expression = true
] learning_strength = 1.0
expression_groups = [ [[expression.rules]]
["qq:1919810:private","qq:114514:private","qq:1111111:group"], # 在这里设置互通组相同组的chat_id会共享学习到的表达方式 chat_stream_id = "qq:1919810:group"
# 格式:["qq:123456:private","qq:654321:group"] use_expression = true
# 注意如果为群聊则需要设置为group如果设置为私聊则需要设置为private learn_expression = true
] learning_strength = 1.5
[[expression.rules]]
chat_stream_id = "qq:114514:private"
group = "group_A"
use_expression = true
learn_expression = false
learning_strength = 0.5
[[expression.rules]]
chat_stream_id = "qq:1919810:private"
group = "group_A"
use_expression = true
learn_expression = true
learning_strength = 1.0
[chat] #麦麦的聊天通用设置 [chat] #MoFox-Bot的聊天通用设置
focus_value = 1 focus_value = 1
# 麦麦的专注思考能力越高越容易专注可能消耗更多token # MoFox-Bot的专注思考能力越高越容易专注可能消耗更多token
# 专注时能更好把握发言时机,能够进行持久的连续对话 # 专注时能更好把握发言时机,能够进行持久的连续对话
talk_frequency = 1 # 麦麦活跃度,越高,麦麦回复越频繁 talk_frequency = 1 # MoFox-Bot活跃度越高MoFox-Bot回复越频繁
# 强制私聊专注模式 # 强制私聊专注模式
force_focus_private = false # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态 force_focus_private = false # 是否强制私聊进入专注模式,开启后私聊将始终保持专注状态
@@ -109,7 +124,7 @@ group_chat_mode = "auto" # 群聊聊天模式auto-自动切换normal-强
max_context_size = 25 # 上下文长度 max_context_size = 25 # 上下文长度
thinking_timeout = 40 # 麦麦一次回复最长思考规划时间超过这个时间的思考会放弃往往是api反应太慢 thinking_timeout = 40 # MoFox-Bot一次回复最长思考规划时间超过这个时间的思考会放弃往往是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 必然回复
@@ -166,7 +181,7 @@ delta_sigma = 120 # 正态分布的标准差,控制时间间隔的随机程度
[relationship] [relationship]
enable_relationship = true # 是否启用关系系统 enable_relationship = true # 是否启用关系系统
relation_frequency = 1 # 关系频率,麦麦构建关系的频率 relation_frequency = 1 # 关系频率,MoFox-Bot构建关系的频率
[message_receive] [message_receive]
@@ -213,35 +228,40 @@ willing_mode = "classical" # 回复意愿模式 —— 经典模式classical
[tool] [tool]
enable_tool = true # 是否在普通聊天中启用工具 enable_tool = true # 是否在普通聊天中启用工具
[tool.history]
enable_history = true # 是否启用工具调用历史记录
enable_prompt_history = true # 是否在提示词中加入工具历史记录
max_history = 5 # 每个会话最多保留的历史记录数
[mood] [mood]
enable_mood = true # 是否启用情绪系统 enable_mood = true # 是否启用情绪系统
mood_update_threshold = 1 # 情绪更新阈值,越高,更新越慢 mood_update_threshold = 1 # 情绪更新阈值,越高,更新越慢
[emoji] [emoji]
emoji_chance = 0.6 # 麦麦激活表情包动作的概率 emoji_chance = 0.6 # MoFox-Bot激活表情包动作的概率
emoji_activate_type = "llm" # 表情包激活类型可选randomllm ; random下表情包动作随机启用llm下表情包动作根据llm判断是否启用 emoji_activate_type = "llm" # 表情包激活类型可选randomllm ; random下表情包动作随机启用llm下表情包动作根据llm判断是否启用
max_reg_num = 60 # 表情包最大注册数量 max_reg_num = 60 # 表情包最大注册数量
do_replace = true # 开启则在达到最大数量时删除(替换)表情包,关闭则达到最大数量时不会继续收集表情包 do_replace = true # 开启则在达到最大数量时删除(替换)表情包,关闭则达到最大数量时不会继续收集表情包
check_interval = 10 # 检查表情包(注册,破损,删除)的时间间隔(分钟) check_interval = 10 # 检查表情包(注册,破损,删除)的时间间隔(分钟)
steal_emoji = true # 是否偷取表情包,让麦麦可以将一些表情包据为己有 steal_emoji = true # 是否偷取表情包,让MoFox-Bot可以将一些表情包据为己有
content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存 content_filtration = false # 是否启用表情包过滤,只有符合该要求的表情包才会被保存
filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存 filtration_prompt = "符合公序良俗" # 表情包过滤要求,只有符合该要求的表情包才会被保存
enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中 enable_emotion_analysis = false # 是否启用表情包感情关键词二次识别,启用后表情包在第一次识别完毕后将送入第二次大模型识别来总结感情关键词,并构建进回复和决策器的上下文消息中
[memory] [memory]
enable_memory = true # 是否启用记忆系统 enable_memory = true # 是否启用记忆系统
memory_build_interval = 600 # 记忆构建间隔 单位秒 间隔越低,麦麦学习越多,但是冗余信息也会增多 memory_build_interval = 600 # 记忆构建间隔 单位秒 间隔越低,MoFox-Bot学习越多,但是冗余信息也会增多
memory_build_distribution = [6.0, 3.0, 0.6, 32.0, 12.0, 0.4] # 记忆构建分布参数分布1均值标准差权重分布2均值标准差权重 memory_build_distribution = [6.0, 3.0, 0.6, 32.0, 12.0, 0.4] # 记忆构建分布参数分布1均值标准差权重分布2均值标准差权重
memory_build_sample_num = 8 # 采样数量,数值越高记忆采样次数越多 memory_build_sample_num = 8 # 采样数量,数值越高记忆采样次数越多
memory_build_sample_length = 30 # 采样长度,数值越高一段记忆内容越丰富 memory_build_sample_length = 30 # 采样长度,数值越高一段记忆内容越丰富
memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多 memory_compress_rate = 0.1 # 记忆压缩率 控制记忆精简程度 建议保持默认,调高可以获得更多信息,但是冗余信息也会增多
forget_memory_interval = 3000 # 记忆遗忘间隔 单位秒 间隔越低,麦麦遗忘越频繁,记忆更精简,但更难学习 forget_memory_interval = 3000 # 记忆遗忘间隔 单位秒 间隔越低,MoFox-Bot遗忘越频繁,记忆更精简,但更难学习
memory_forget_time = 48 #多长时间后的记忆会被遗忘 单位小时 memory_forget_time = 48 #多长时间后的记忆会被遗忘 单位小时
memory_forget_percentage = 0.008 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认 memory_forget_percentage = 0.008 # 记忆遗忘比例 控制记忆遗忘程度 越大遗忘越多 建议保持默认
consolidate_memory_interval = 1000 # 记忆整合间隔 单位秒 间隔越低,麦麦整合越频繁,记忆更精简 consolidate_memory_interval = 1000 # 记忆整合间隔 单位秒 间隔越低,MoFox-Bot整合越频繁,记忆更精简
consolidation_similarity_threshold = 0.7 # 相似度阈值 consolidation_similarity_threshold = 0.7 # 相似度阈值
consolidation_check_percentage = 0.05 # 检查节点比例 consolidation_check_percentage = 0.05 # 检查节点比例
@@ -253,7 +273,7 @@ enable_vector_instant_memory = true # 是否启用基于向量的瞬时记忆
memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ] memory_ban_words = [ "表情包", "图片", "回复", "聊天记录" ]
[voice] [voice]
enable_asr = false # 是否启用语音识别,启用后麦麦可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s enable_asr = false # 是否启用语音识别,启用后MoFox-Bot可以识别语音消息,启用该功能需要配置语音识别模型[model.voice]s
[lpmm_knowledge] # lpmm知识库配置 [lpmm_knowledge] # lpmm知识库配置
enable = false # 是否启用lpmm知识库 enable = false # 是否启用lpmm知识库
@@ -358,6 +378,21 @@ guidelines = """
""" """
enable_is_sleep = false enable_is_sleep = false
# --- 弹性睡眠与睡前消息 ---
# 是否启用弹性睡眠。启用后AI不会到点立刻入睡而是会根据睡眠压力增加5-10分钟的缓冲并可能因为压力不足而推迟睡眠。
enable_flexible_sleep = true
# 触发弹性睡眠的睡眠压力阈值。当AI的睡眠压力低于此值时可能会推迟入睡。
flexible_sleep_pressure_threshold = 40.0
# 每日最大可推迟入睡的总分钟数。
max_sleep_delay_minutes = 60
# 是否在进入“准备入睡”状态时发送一条消息通知。
enable_pre_sleep_notification = true
# 接收睡前消息的群组列表。格式为: ["platform:group_id1", "platform:group_id2"],例如 ["qq:12345678"]
pre_sleep_notification_groups = []
# 用于生成睡前消息的提示。AI会根据这个提示生成一句晚安问候。
pre_sleep_prompt = "我准备睡觉了,请生成一句简短自然的晚安问候。"
[video_analysis] # 视频分析配置 [video_analysis] # 视频分析配置
enable = true # 是否启用视频分析功能 enable = true # 是否启用视频分析功能
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择) analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择)
@@ -422,7 +457,7 @@ guidelines = """
""" """
[wakeup_system] [wakeup_system]
enable = true #"是否启用唤醒度系统" enable = false #"是否启用唤醒度系统"
wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒"
private_message_increment = 3.0 #"私聊消息增加的唤醒度" private_message_increment = 3.0 #"私聊消息增加的唤醒度"
group_mention_increment = 2.0 #"群聊艾特增加的唤醒度" group_mention_increment = 2.0 #"群聊艾特增加的唤醒度"
@@ -431,6 +466,22 @@ decay_interval = 30.0 #"唤醒度衰减间隔(秒)"
angry_duration = 300.0 #"愤怒状态持续时间(秒)" angry_duration = 300.0 #"愤怒状态持续时间(秒)"
angry_prompt = "你被人吵醒了非常生气,说话带着怒气" # "被吵醒后的愤怒提示词" angry_prompt = "你被人吵醒了非常生气,说话带着怒气" # "被吵醒后的愤怒提示词"
# --- 失眠机制相关参数 ---
enable_insomnia_system = false # 是否启用失眠系统
# 触发“压力不足型失眠”的睡眠压力阈值
sleep_pressure_threshold = 30.0
# 进入“深度睡眠”的睡眠压力阈值
deep_sleep_threshold = 80.0
# 压力不足时的失眠基础概率 (0.0 to 1.0)
insomnia_chance_low_pressure = 0.6
# 压力正常时的失眠基础概率 (0.0 to 1.0)
insomnia_chance_normal_pressure = 0.1
# 每次AI执行动作后增加的睡眠压力值
sleep_pressure_increment = 1.5
# 睡眠时,每分钟衰减的睡眠压力值
sleep_pressure_decay_rate = 1.5
insomnia_duration_minutes = 30 # 单次失眠状态的持续时间(分钟)
[cross_context] # 跨群聊上下文共享配置 [cross_context] # 跨群聊上下文共享配置
# 这是总开关,用于一键启用或禁用此功能 # 这是总开关,用于一键启用或禁用此功能
enable = false enable = false