Merge branch 'master' of https://github.com/MoFox-Studio/MoFox_Bot
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -325,7 +325,7 @@ run_pet.bat
|
||||
!/plugins/set_emoji_like
|
||||
!/plugins/permission_example
|
||||
!/plugins/hello_world_plugin
|
||||
!/plugins/take_picture_plugin
|
||||
!/plugins/echo_example
|
||||
|
||||
config.toml
|
||||
|
||||
|
||||
260
docs/PLUS_COMMAND_GUIDE.md
Normal file
260
docs/PLUS_COMMAND_GUIDE.md
Normal 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类 | 手动处理 |
|
||||
| 别名支持 | 内置支持 | 需要在正则中处理 |
|
||||
| 代码复杂度 | 简单 | 复杂 |
|
||||
| 学习曲线 | 平缓 | 陡峭 |
|
||||
|
||||
增强命令系统让插件开发变得更加简单和高效,特别适合新手开发者快速上手。
|
||||
53
plugins/echo_example/_manifest.json
Normal file
53
plugins/echo_example/_manifest.json
Normal 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": [
|
||||
"增强命令系统示例",
|
||||
"无需正则表达式的命令定义",
|
||||
"命令别名支持",
|
||||
"参数解析功能",
|
||||
"聊天类型限制"
|
||||
]
|
||||
}
|
||||
}
|
||||
203
plugins/echo_example/plugin.py
Normal file
203
plugins/echo_example/plugin.py
Normal 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
|
||||
@@ -242,10 +242,16 @@ class HeartFChatting:
|
||||
# 处理唤醒度逻辑
|
||||
if is_sleeping:
|
||||
self._handle_wakeup_messages(recent_messages)
|
||||
# 如果处于失眠状态,则无视睡眠时间,继续处理消息
|
||||
# 否则,如果仍然在睡眠(没被吵醒),则跳过本轮处理
|
||||
if not self.context.is_in_insomnia and schedule_manager.is_sleeping(self.wakeup_manager):
|
||||
# 再次检查睡眠状态,因为_handle_wakeup_messages可能会触发唤醒
|
||||
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
|
||||
else:
|
||||
# 从睡眠中被唤醒,需要继续处理本轮消息
|
||||
logger.info(f"{self.context.log_prefix} 从睡眠中被唤醒,将处理积压的消息。")
|
||||
self.context.last_wakeup_time = time.time()
|
||||
|
||||
# 根据聊天模式处理新消息
|
||||
if self.context.loop_mode == ChatMode.FOCUS:
|
||||
@@ -265,6 +271,18 @@ class HeartFChatting:
|
||||
|
||||
# 更新上一帧的睡眠状态
|
||||
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
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
import time
|
||||
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.chat.express.expression_learner import ExpressionLearner
|
||||
from src.plugin_system.base.component_types import ChatMode
|
||||
@@ -47,6 +49,7 @@ class HfcContext:
|
||||
# 失眠状态
|
||||
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_read_time = time.time() - 10
|
||||
@@ -61,4 +64,37 @@ class HfcContext:
|
||||
|
||||
# 唤醒度管理器 - 延迟初始化以避免循环导入
|
||||
self.wakeup_manager: Optional['WakeUpManager'] = None
|
||||
self.energy_manager: Optional['EnergyManager'] = 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}")
|
||||
@@ -3,6 +3,7 @@ import time
|
||||
from typing import Optional
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.manager.local_store_manager import local_storage
|
||||
from .hfc_context import HfcContext
|
||||
|
||||
logger = get_logger("wakeup")
|
||||
@@ -46,6 +47,33 @@ class WakeUpManager:
|
||||
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):
|
||||
"""启动唤醒度管理器"""
|
||||
@@ -89,6 +117,7 @@ class WakeUpManager:
|
||||
from src.mood.mood_manager import mood_manager
|
||||
mood_manager.clear_angry_from_wakeup(self.context.stream_id)
|
||||
logger.info(f"{self.context.log_prefix} 愤怒状态结束,恢复正常")
|
||||
self._save_wakeup_state()
|
||||
|
||||
# 唤醒度自然衰减
|
||||
if self.wakeup_value > 0:
|
||||
@@ -96,6 +125,7 @@ class WakeUpManager:
|
||||
self.wakeup_value = max(0, self.wakeup_value - self.decay_rate)
|
||||
if old_value != self.wakeup_value:
|
||||
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:
|
||||
"""
|
||||
@@ -112,10 +142,9 @@ class WakeUpManager:
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
# 只有在休眠且非失眠状态下才累积唤醒度
|
||||
from src.schedule.schedule_manager import schedule_manager
|
||||
|
||||
# 只有在休眠状态下才累积唤醒度
|
||||
if not schedule_manager.is_sleeping():
|
||||
if not schedule_manager.is_sleeping() or self.context.is_in_insomnia:
|
||||
return False
|
||||
|
||||
old_value = self.wakeup_value
|
||||
@@ -143,7 +172,8 @@ class WakeUpManager:
|
||||
if self.wakeup_value >= self.wakeup_threshold:
|
||||
self._trigger_wakeup()
|
||||
return True
|
||||
|
||||
|
||||
self._save_wakeup_state()
|
||||
return False
|
||||
|
||||
def _trigger_wakeup(self):
|
||||
@@ -152,10 +182,16 @@ class WakeUpManager:
|
||||
self.angry_start_time = time.time()
|
||||
self.wakeup_value = 0.0 # 重置唤醒度
|
||||
|
||||
self._save_wakeup_state()
|
||||
|
||||
# 通知情绪管理系统进入愤怒状态
|
||||
from src.mood.mood_manager import mood_manager
|
||||
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}),被吵醒进入愤怒状态!")
|
||||
|
||||
def get_angry_prompt_addition(self) -> str:
|
||||
@@ -205,9 +241,12 @@ class WakeUpManager:
|
||||
return False
|
||||
|
||||
# 根据睡眠压力决定失眠概率
|
||||
from src.schedule.schedule_manager import schedule_manager
|
||||
if pressure < self.sleep_pressure_threshold:
|
||||
# 压力不足型失眠
|
||||
if random.random() < self.insomnia_chance_low_pressure:
|
||||
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:
|
||||
|
||||
@@ -98,6 +98,118 @@ class ChatBot:
|
||||
|
||||
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):
|
||||
# sourcery skip: use-named-expression
|
||||
"""使用新插件系统处理命令"""
|
||||
@@ -306,14 +418,24 @@ class ChatBot:
|
||||
):
|
||||
return
|
||||
|
||||
# 命令处理 - 使用新插件系统检查并处理命令
|
||||
is_command, cmd_result, continue_process = await self._process_commands_with_new_system(message)
|
||||
|
||||
# 如果是命令且不需要继续处理,则直接返回
|
||||
if is_command and not continue_process:
|
||||
# 命令处理 - 首先尝试PlusCommand独立处理
|
||||
is_plus_command, plus_cmd_result, plus_continue_process = await self._process_plus_commands(message)
|
||||
|
||||
# 如果是PlusCommand且不需要继续处理,则直接返回
|
||||
if is_plus_command and not plus_continue_process:
|
||||
await MessageStorage.store_message(message, chat)
|
||||
logger.info(f"命令处理完成,跳过后续消息处理: {cmd_result}")
|
||||
logger.info(f"PlusCommand处理完成,跳过后续消息处理: {plus_cmd_result}")
|
||||
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)
|
||||
if not result.all_continue_process():
|
||||
|
||||
@@ -80,16 +80,54 @@ def mark_plans_completed(plan_ids: List[int]):
|
||||
|
||||
with get_db_session() as session:
|
||||
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(
|
||||
MonthlyPlan.id.in_(plan_ids)
|
||||
).update({"status": "completed"}, synchronize_session=False)
|
||||
session.commit()
|
||||
logger.info(f"成功将 {len(plan_ids)} 条月度计划标记为已完成。")
|
||||
except Exception as e:
|
||||
logger.error(f"标记月度计划为完成时发生错误: {e}")
|
||||
session.rollback()
|
||||
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]):
|
||||
"""
|
||||
将指定ID的计划标记为软删除(兼容旧接口)。
|
||||
|
||||
@@ -45,6 +45,7 @@ from src.config.official_configs import (
|
||||
MonthlyPlanSystemConfig,
|
||||
CrossContextConfig,
|
||||
PermissionConfig,
|
||||
CommandConfig,
|
||||
MaizoneIntercomConfig,
|
||||
)
|
||||
|
||||
@@ -372,7 +373,7 @@ class Config(ValidatedConfigBase):
|
||||
chinese_typo: ChineseTypoConfig = Field(..., description="中文错别字配置")
|
||||
response_post_process: ResponsePostProcessConfig = 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消息配置")
|
||||
lpmm_knowledge: LPMMKnowledgeConfig = Field(..., description="LPMM知识配置")
|
||||
tool: ToolConfig = Field(..., description="工具配置")
|
||||
@@ -381,6 +382,7 @@ class Config(ValidatedConfigBase):
|
||||
voice: VoiceConfig = Field(..., description="语音配置")
|
||||
schedule: ScheduleConfig = Field(..., description="调度配置")
|
||||
permission: PermissionConfig = Field(..., description="权限配置")
|
||||
command: CommandConfig = Field(..., description="命令系统配置")
|
||||
|
||||
# 有默认值的字段放在后面
|
||||
anti_prompt_injection: AntiPromptInjectionConfig = Field(default_factory=lambda: AntiPromptInjectionConfig(), description="反提示注入配置")
|
||||
|
||||
@@ -530,6 +530,14 @@ class ScheduleConfig(ValidatedConfigBase):
|
||||
enable: bool = Field(default=True, description="启用")
|
||||
guidelines: Optional[str] = Field(default=None, description="指导方针")
|
||||
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="用于生成睡前消息的提示")
|
||||
|
||||
|
||||
|
||||
@@ -653,6 +661,12 @@ class MaizoneIntercomConfig(ValidatedConfigBase):
|
||||
groups: List[ContextGroup] = Field(default_factory=list, description="Maizone互通组列表")
|
||||
|
||||
|
||||
class CommandConfig(ValidatedConfigBase):
|
||||
"""命令系统配置类"""
|
||||
|
||||
command_prefixes: List[str] = Field(default_factory=lambda: ['/', '!', '.', '#'], description="支持的命令前缀列表")
|
||||
|
||||
|
||||
class PermissionConfig(ValidatedConfigBase):
|
||||
"""权限系统配置类"""
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from .base import (
|
||||
ComponentInfo,
|
||||
ActionInfo,
|
||||
CommandInfo,
|
||||
PlusCommandInfo,
|
||||
PluginInfo,
|
||||
ToolInfo,
|
||||
PythonDependency,
|
||||
@@ -25,6 +26,12 @@ from .base import (
|
||||
EventType,
|
||||
MaiMessages,
|
||||
ToolParamType,
|
||||
# 新增的增强命令系统
|
||||
PlusCommand,
|
||||
CommandArgs,
|
||||
PlusCommandAdapter,
|
||||
create_plus_command_adapter,
|
||||
ChatType,
|
||||
)
|
||||
|
||||
# 导入工具模块
|
||||
@@ -81,10 +88,17 @@ __all__ = [
|
||||
"BaseCommand",
|
||||
"BaseTool",
|
||||
"BaseEventHandler",
|
||||
# 增强命令系统
|
||||
"PlusCommand",
|
||||
"CommandArgs",
|
||||
"PlusCommandAdapter",
|
||||
"create_plus_command_adapter",
|
||||
"create_plus_command_adapter",
|
||||
# 类型定义
|
||||
"ComponentType",
|
||||
"ActionActivationType",
|
||||
"ChatMode",
|
||||
"ChatType",
|
||||
"ComponentInfo",
|
||||
"ActionInfo",
|
||||
"CommandInfo",
|
||||
|
||||
@@ -13,9 +13,11 @@ from .component_types import (
|
||||
ComponentType,
|
||||
ActionActivationType,
|
||||
ChatMode,
|
||||
ChatType,
|
||||
ComponentInfo,
|
||||
ActionInfo,
|
||||
CommandInfo,
|
||||
PlusCommandInfo,
|
||||
ToolInfo,
|
||||
PluginInfo,
|
||||
PythonDependency,
|
||||
@@ -25,6 +27,8 @@ from .component_types import (
|
||||
ToolParamType,
|
||||
)
|
||||
from .config_types import ConfigField
|
||||
from .plus_command import PlusCommand, PlusCommandAdapter, create_plus_command_adapter
|
||||
from .command_args import CommandArgs
|
||||
|
||||
__all__ = [
|
||||
"BasePlugin",
|
||||
@@ -34,9 +38,11 @@ __all__ = [
|
||||
"ComponentType",
|
||||
"ActionActivationType",
|
||||
"ChatMode",
|
||||
"ChatType",
|
||||
"ComponentInfo",
|
||||
"ActionInfo",
|
||||
"CommandInfo",
|
||||
"PlusCommandInfo",
|
||||
"ToolInfo",
|
||||
"PluginInfo",
|
||||
"PythonDependency",
|
||||
@@ -46,4 +52,9 @@ __all__ = [
|
||||
"BaseEventHandler",
|
||||
"MaiMessages",
|
||||
"ToolParamType",
|
||||
# 增强命令系统
|
||||
"PlusCommand",
|
||||
"CommandArgs",
|
||||
"PlusCommandAdapter",
|
||||
"create_plus_command_adapter",
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional
|
||||
from src.common.logger import get_logger
|
||||
|
||||
@@ -73,6 +74,8 @@ class BaseEvent:
|
||||
from src.plugin_system.base.base_events_handler import BaseEventHandler
|
||||
self.subscribers: List["BaseEventHandler"] = [] # 订阅该事件的事件处理器列表
|
||||
|
||||
self.event_handle_lock = asyncio.Lock()
|
||||
|
||||
def __name__(self):
|
||||
return self.name
|
||||
|
||||
@@ -88,22 +91,45 @@ class BaseEvent:
|
||||
if not self.enabled:
|
||||
return HandlerResultsCollection([])
|
||||
|
||||
# 按权重从高到低排序订阅者
|
||||
# 使用直接属性访问,-1代表自动权重
|
||||
sorted_subscribers = sorted(self.subscribers, key=lambda h: h.weight if hasattr(h, 'weight') and h.weight != -1 else 0, reverse=True)
|
||||
|
||||
results = []
|
||||
for subscriber in sorted_subscribers:
|
||||
try:
|
||||
result = await subscriber.execute(params)
|
||||
if not result.handler_name:
|
||||
# 补充handler_name
|
||||
result.handler_name = subscriber.handler_name if hasattr(subscriber, 'handler_name') else subscriber.__class__.__name__
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
# 处理执行异常
|
||||
# 使用锁确保同一个事件不能同时激活多次
|
||||
async with self.event_handle_lock:
|
||||
# 按权重从高到低排序订阅者
|
||||
# 使用直接属性访问,-1代表自动权重
|
||||
sorted_subscribers = sorted(self.subscribers, key=lambda h: h.weight if hasattr(h, 'weight') and h.weight != -1 else 0, reverse=True)
|
||||
|
||||
# 并行执行所有订阅者
|
||||
tasks = []
|
||||
for subscriber in sorted_subscribers:
|
||||
# 为每个订阅者创建执行任务
|
||||
task = self._execute_subscriber(subscriber, params)
|
||||
tasks.append(task)
|
||||
|
||||
# 等待所有任务完成
|
||||
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__
|
||||
logger.error(f"事件处理器 {handler_name} 执行失败: {e}")
|
||||
results.append(HandlerResult(False, True, str(e), handler_name))
|
||||
|
||||
return HandlerResultsCollection(results)
|
||||
|
||||
if isinstance(result, Exception):
|
||||
# 处理执行异常
|
||||
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
|
||||
@@ -1,13 +1,14 @@
|
||||
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 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_command import BaseCommand
|
||||
from .base_events_handler import BaseEventHandler
|
||||
from .base_tool import BaseTool
|
||||
from .plus_command import PlusCommand
|
||||
|
||||
logger = get_logger("base_plugin")
|
||||
|
||||
@@ -31,6 +32,7 @@ class BasePlugin(PluginBase):
|
||||
Union[
|
||||
Tuple[ActionInfo, Type[BaseAction]],
|
||||
Tuple[CommandInfo, Type[BaseCommand]],
|
||||
Tuple[PlusCommandInfo, Type[PlusCommand]],
|
||||
Tuple[EventHandlerInfo, Type[BaseEventHandler]],
|
||||
Tuple[ToolInfo, Type[BaseTool]],
|
||||
]
|
||||
|
||||
156
src/plugin_system/base/command_args.py
Normal file
156
src/plugin_system/base/command_args.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""命令参数解析类
|
||||
|
||||
提供简单易用的命令参数解析功能
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def get_first(self, default: str = "") -> str:
|
||||
"""获取第一个参数
|
||||
|
||||
Args:
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
str: 第一个参数或默认值
|
||||
"""
|
||||
return self.get_arg(0, default)
|
||||
|
||||
def get_remaining(self, start_index: int = 1) -> 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()})"
|
||||
@@ -12,6 +12,7 @@ class ComponentType(Enum):
|
||||
|
||||
ACTION = "action" # 动作组件
|
||||
COMMAND = "command" # 命令组件
|
||||
PLUS_COMMAND = "plus_command" # 增强命令组件
|
||||
TOOL = "tool" # 工具组件
|
||||
SCHEDULER = "scheduler" # 定时任务组件(预留)
|
||||
EVENT_HANDLER = "event_handler" # 事件处理组件
|
||||
@@ -164,6 +165,22 @@ class CommandInfo(ComponentInfo):
|
||||
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
|
||||
class ToolInfo(ComponentInfo):
|
||||
"""工具组件信息"""
|
||||
|
||||
459
src/plugin_system/base/plus_command.py
Normal file
459
src/plugin_system/base/plus_command.py
Normal 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
|
||||
0
src/plugin_system/base/plus_plugin.py
Normal file
0
src/plugin_system/base/plus_plugin.py
Normal file
@@ -8,6 +8,7 @@ from src.plugin_system.base.component_types import (
|
||||
ActionInfo,
|
||||
ToolInfo,
|
||||
CommandInfo,
|
||||
PlusCommandInfo,
|
||||
EventHandlerInfo,
|
||||
PluginInfo,
|
||||
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_tool import BaseTool
|
||||
from src.plugin_system.base.base_events_handler import BaseEventHandler
|
||||
from src.plugin_system.base.plus_command import PlusCommand
|
||||
|
||||
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_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 issubclass(component_class, BaseCommand)
|
||||
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:
|
||||
assert isinstance(component_info, ToolInfo)
|
||||
assert issubclass(component_class, BaseTool)
|
||||
@@ -192,6 +198,26 @@ class ComponentRegistry:
|
||||
|
||||
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:
|
||||
"""注册Tool组件到Tool特定注册表"""
|
||||
tool_name = tool_info.name
|
||||
@@ -248,6 +274,12 @@ class ComponentRegistry:
|
||||
self._command_patterns.pop(key, None)
|
||||
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:
|
||||
# 移除Tool注册
|
||||
self._tool_registry.pop(component_name, None)
|
||||
@@ -520,21 +552,23 @@ class ComponentRegistry:
|
||||
text: 输入文本
|
||||
|
||||
Returns:
|
||||
Tuple: (命令类, 匹配的命名组, 是否拦截消息, 插件名) 或 None
|
||||
Tuple: (命令类, 匹配的命名组, 命令信息) 或 None
|
||||
"""
|
||||
|
||||
# 只查找传统的BaseCommand
|
||||
candidates = [pattern for pattern in self._command_patterns if pattern.match(text)]
|
||||
if not candidates:
|
||||
return None
|
||||
if len(candidates) > 1:
|
||||
logger.warning(f"文本 '{text}' 匹配到多个命令模式: {candidates},使用第一个匹配")
|
||||
command_name = self._command_patterns[candidates[0]]
|
||||
command_info: CommandInfo = self.get_registered_command_info(command_name) # type: ignore
|
||||
return (
|
||||
self._command_registry[command_name],
|
||||
candidates[0].match(text).groupdict(), # type: ignore
|
||||
command_info,
|
||||
)
|
||||
if candidates:
|
||||
if len(candidates) > 1:
|
||||
logger.warning(f"文本 '{text}' 匹配到多个命令模式: {candidates},使用第一个匹配")
|
||||
command_name = self._command_patterns[candidates[0]]
|
||||
command_info: CommandInfo = self.get_registered_command_info(command_name) # type: ignore
|
||||
return (
|
||||
self._command_registry[command_name],
|
||||
candidates[0].match(text).groupdict(), # type: ignore
|
||||
command_info,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
# === Tool 特定查询方法 ===
|
||||
def get_tool_registry(self) -> Dict[str, Type[BaseTool]]:
|
||||
@@ -557,6 +591,25 @@ class ComponentRegistry:
|
||||
info = self.get_component_info(tool_name, ComponentType.TOOL)
|
||||
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 特定查询方法 ===
|
||||
|
||||
def get_event_handler_registry(self) -> Dict[str, Type[BaseEventHandler]]:
|
||||
@@ -612,6 +665,7 @@ class ComponentRegistry:
|
||||
command_components: int = 0
|
||||
tool_components: int = 0
|
||||
events_handlers: int = 0
|
||||
plus_command_components: int = 0
|
||||
for component in self._components.values():
|
||||
if component.component_type == ComponentType.ACTION:
|
||||
action_components += 1
|
||||
@@ -621,11 +675,14 @@ class ComponentRegistry:
|
||||
tool_components += 1
|
||||
elif component.component_type == ComponentType.EVENT_HANDLER:
|
||||
events_handlers += 1
|
||||
elif component.component_type == ComponentType.PLUS_COMMAND:
|
||||
plus_command_components += 1
|
||||
return {
|
||||
"action_components": action_components,
|
||||
"command_components": command_components,
|
||||
"tool_components": tool_components,
|
||||
"event_handlers": events_handlers,
|
||||
"plus_command_components": plus_command_components,
|
||||
"total_components": len(self._components),
|
||||
"total_plugins": len(self._plugins),
|
||||
"components_by_type": {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
事件管理器 - 实现Event和EventHandler的单例管理
|
||||
提供统一的事件注册、管理和触发接口
|
||||
"""
|
||||
|
||||
from typing import Dict, Type, List, Optional, Any, Union
|
||||
from threading import Lock
|
||||
|
||||
@@ -281,7 +280,7 @@ class EventManager:
|
||||
if event is None:
|
||||
logger.error(f"事件 {event_name} 不存在,无法触发")
|
||||
return None
|
||||
|
||||
|
||||
return await event.activate(params)
|
||||
|
||||
def init_default_events(self) -> None:
|
||||
@@ -294,8 +293,7 @@ class EventManager:
|
||||
EventType.POST_LLM,
|
||||
EventType.AFTER_LLM,
|
||||
EventType.POST_SEND,
|
||||
EventType.AFTER_SEND,
|
||||
EventType.UNKNOWN
|
||||
EventType.AFTER_SEND
|
||||
]
|
||||
|
||||
for event_name in default_events:
|
||||
|
||||
@@ -363,13 +363,14 @@ class PluginManager:
|
||||
command_count = stats.get("command_components", 0)
|
||||
tool_count = stats.get("tool_components", 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)
|
||||
|
||||
# 📋 显示插件加载总览
|
||||
if total_registered > 0:
|
||||
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 = [
|
||||
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:
|
||||
action_names = [c.name for c in action_components]
|
||||
@@ -421,6 +425,9 @@ class PluginManager:
|
||||
if tool_components:
|
||||
tool_names = [c.name for c in tool_components]
|
||||
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:
|
||||
event_handler_names = [c.name for c in event_handler_components]
|
||||
logger.info(f" 📢 EventHandler组件: {', '.join(event_handler_names)}")
|
||||
|
||||
@@ -42,6 +42,7 @@ class MaiZoneRefactoredPlugin(BasePlugin):
|
||||
"plugin": {"enable": ConfigField(type=bool, default=True, description="是否启用插件")},
|
||||
"models": {
|
||||
"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密钥"),
|
||||
},
|
||||
"send": {
|
||||
|
||||
@@ -6,8 +6,19 @@
|
||||
from typing import Callable, Optional
|
||||
import datetime
|
||||
|
||||
import base64
|
||||
import aiohttp
|
||||
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
|
||||
@@ -97,110 +108,181 @@ class ContentService:
|
||||
logger.error(f"生成说说内容时发生异常: {e}")
|
||||
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:
|
||||
# 获取模型配置
|
||||
models = llm_api.get_available_models()
|
||||
text_model = str(self.get_config("models.text_model", "replyer_1"))
|
||||
model_config = models.get(text_model)
|
||||
for i in range(3): # 重试3次
|
||||
try:
|
||||
chat_manager = get_chat_manager()
|
||||
bot_platform = config_api.get_global_config('bot.platform')
|
||||
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:
|
||||
logger.error("未配置LLM模型")
|
||||
return ""
|
||||
chat_stream = await chat_manager.get_or_create_stream(
|
||||
platform=bot_platform,
|
||||
user_info=bot_user_info
|
||||
)
|
||||
|
||||
# 获取机器人信息
|
||||
bot_personality = config_api.get_global_config("personality.personality_core", "一个机器人")
|
||||
bot_expression = config_api.get_global_config("expression.expression_style", "内容积极向上")
|
||||
if not chat_stream:
|
||||
logger.error(f"无法为QQ号 {bot_qq} 创建聊天流")
|
||||
return ""
|
||||
|
||||
# 构建提示词
|
||||
if not rt_con:
|
||||
prompt = f"""
|
||||
你是'{bot_personality}',你正在浏览你好友'{target_name}'的QQ空间,
|
||||
你看到了你的好友'{target_name}'qq空间上内容是'{content}'的说说,你想要发表你的一条评论,
|
||||
{bot_expression},回复的平淡一些,简短一些,说中文,
|
||||
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容
|
||||
"""
|
||||
else:
|
||||
prompt = f"""
|
||||
你是'{bot_personality}',你正在浏览你好友'{target_name}'的QQ空间,
|
||||
你看到了你的好友'{target_name}'在qq空间上转发了一条内容为'{rt_con}'的说说,你的好友的评论为'{content}'
|
||||
你想要发表你的一条评论,{bot_expression},回复的平淡一些,简短一些,说中文,
|
||||
不要刻意突出自身学科背景,不要浮夸,不要夸张修辞,不要输出多余内容(包括前后缀,冒号和引号,括号(),表情包,at或 @等 )。只输出回复内容
|
||||
"""
|
||||
image_descriptions = []
|
||||
if images:
|
||||
for image_url in images:
|
||||
description = await self._describe_image(image_url)
|
||||
if description:
|
||||
image_descriptions.append(description)
|
||||
|
||||
extra_info = "正在评论QQ空间的好友说说。"
|
||||
if image_descriptions:
|
||||
extra_info += "说说中包含的图片内容如下:\n" + "\n".join(image_descriptions)
|
||||
|
||||
logger.info(f"正在为'{target_name}'的说说生成评论: {content[:20]}...")
|
||||
reply_to = f"{target_name}:{content}"
|
||||
if rt_con:
|
||||
reply_to += f"\n[转发内容]: {rt_con}"
|
||||
|
||||
# 调用LLM生成评论
|
||||
success, comment, _, _ = await llm_api.generate_with_model(
|
||||
prompt=prompt,
|
||||
model_config=model_config,
|
||||
request_type="comment.generate",
|
||||
temperature=0.3,
|
||||
max_tokens=100
|
||||
)
|
||||
success, reply_set, _ = await generator_api.generate_reply(
|
||||
chat_stream=chat_stream,
|
||||
reply_to=reply_to,
|
||||
extra_info=extra_info,
|
||||
request_type="maizone.comment"
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"成功生成评论内容:'{comment}'")
|
||||
return comment
|
||||
else:
|
||||
logger.error("生成评论内容失败")
|
||||
return ""
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成评论内容时发生异常: {e}")
|
||||
return ""
|
||||
if success and reply_set:
|
||||
comment = "".join([content for type, content in reply_set if type == 'text'])
|
||||
logger.info(f"成功生成评论内容:'{comment}'")
|
||||
return comment
|
||||
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 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:
|
||||
models = llm_api.get_available_models()
|
||||
text_model = str(self.get_config("models.text_model", "replyer_1"))
|
||||
model_config = models.get(text_model)
|
||||
if not model_config:
|
||||
return ""
|
||||
for i in range(3): # 重试3次
|
||||
try:
|
||||
chat_manager = get_chat_manager()
|
||||
bot_platform = config_api.get_global_config('bot.platform')
|
||||
bot_qq = str(config_api.get_global_config('bot.qq_account'))
|
||||
bot_nickname = config_api.get_global_config('bot.nickname')
|
||||
|
||||
bot_personality = config_api.get_global_config("personality.personality_core", "一个机器人")
|
||||
bot_expression = config_api.get_global_config("expression.expression_style", "内容积极向上")
|
||||
bot_user_info = UserInfo(
|
||||
platform=bot_platform,
|
||||
user_id=bot_qq,
|
||||
user_nickname=bot_nickname
|
||||
)
|
||||
|
||||
prompt = f"""
|
||||
你是'{bot_personality}',你的好友'{commenter_name}'评论了你QQ空间上的一条内容为“{story_content}”说说,
|
||||
你的好友对该说说的评论为:“{comment_content}”,你想要对此评论进行回复
|
||||
{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
|
||||
)
|
||||
chat_stream = await chat_manager.get_or_create_stream(
|
||||
platform=bot_platform,
|
||||
user_info=bot_user_info
|
||||
)
|
||||
|
||||
if success:
|
||||
logger.info(f"成功为'{commenter_name}'的评论生成回复: '{reply}'")
|
||||
return reply
|
||||
else:
|
||||
logger.error("生成评论回复失败")
|
||||
return ""
|
||||
except Exception as e:
|
||||
logger.error(f"生成评论回复时发生异常: {e}")
|
||||
return ""
|
||||
if not chat_stream:
|
||||
logger.error(f"无法为QQ号 {bot_qq} 创建聊天流")
|
||||
return ""
|
||||
|
||||
reply_to = f"{commenter_name}:{comment_content}"
|
||||
extra_info = f"正在回复我的QQ空间说说“{story_content}”下的评论。"
|
||||
|
||||
success, reply_set, _ = await generator_api.generate_reply(
|
||||
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:
|
||||
"""
|
||||
|
||||
@@ -151,24 +151,31 @@ class QZoneService:
|
||||
return
|
||||
|
||||
try:
|
||||
feeds = await api_client["monitor_list_feeds"](20) # 监控时检查最近20条动态
|
||||
if not feeds:
|
||||
logger.info("监控完成:未发现新说说")
|
||||
# --- 第一步: 单独处理自己说说的评论 ---
|
||||
if self.get_config("monitor.enable_auto_reply", False):
|
||||
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
|
||||
|
||||
logger.info(f"监控任务: 发现 {len(feeds)} 条新动态,准备处理...")
|
||||
for feed in feeds:
|
||||
logger.info(f"监控任务: 发现 {len(friend_feeds)} 条好友新动态,准备处理...")
|
||||
for feed in friend_feeds:
|
||||
target_qq = feed.get("target_qq")
|
||||
if not target_qq:
|
||||
if not target_qq or str(target_qq) == str(qq_account): # 确保不重复处理自己的
|
||||
continue
|
||||
|
||||
# 区分是自己的说说还是他人的说说
|
||||
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 self._process_single_feed(feed, api_client, target_qq, target_qq)
|
||||
await asyncio.sleep(random.uniform(5, 10))
|
||||
except Exception as e:
|
||||
logger.error(f"监控好友动态时发生异常: {e}", exc_info=True)
|
||||
@@ -244,12 +251,20 @@ class QZoneService:
|
||||
if not comments:
|
||||
return
|
||||
|
||||
# 筛选出未被自己回复过的主评论
|
||||
my_comment_tids = {
|
||||
c["parent_tid"] for c in comments if c.get("parent_tid") and c.get("qq_account") == qq_account
|
||||
# 筛选出未被自己回复过的评论
|
||||
if not comments:
|
||||
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 = [
|
||||
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:
|
||||
@@ -275,9 +290,10 @@ class QZoneService:
|
||||
content = feed.get("content", "")
|
||||
fid = feed.get("tid", "")
|
||||
rt_con = feed.get("rt_con", "")
|
||||
images = feed.get("images", [])
|
||||
|
||||
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:
|
||||
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)
|
||||
)
|
||||
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(
|
||||
{
|
||||
"tid": msg.get("tid", ""),
|
||||
@@ -665,6 +694,8 @@ class QZoneService:
|
||||
"rt_con": msg.get("rt_con", {}).get("content", "")
|
||||
if isinstance(msg.get("rt_con"), dict)
|
||||
else "",
|
||||
"images": images,
|
||||
"comments": comments
|
||||
}
|
||||
)
|
||||
return feeds_list
|
||||
@@ -815,10 +846,61 @@ class QZoneService:
|
||||
text_div = soup.find('div', class_='f-info')
|
||||
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({
|
||||
'target_qq': target_qq,
|
||||
'tid': tid,
|
||||
'content': text,
|
||||
'images': images,
|
||||
'comments': comments
|
||||
})
|
||||
logger.info(f"监控任务发现 {len(feeds_list)} 条未处理的新说说。")
|
||||
return feeds_list
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
权限管理插件
|
||||
|
||||
提供权限系统的管理命令,包括权限授权、撤销、查询等功能。
|
||||
使用新的PlusCommand系统重构。
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -9,23 +10,25 @@ from typing import List, Optional, Tuple, Type
|
||||
|
||||
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_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.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
|
||||
|
||||
|
||||
logger = get_logger("Permission")
|
||||
|
||||
|
||||
class PermissionCommand(BaseCommand):
|
||||
"""权限管理命令"""
|
||||
class PermissionCommand(PlusCommand):
|
||||
"""权限管理命令 - 使用PlusCommand系统"""
|
||||
|
||||
command_name = "permission"
|
||||
command_description = "权限管理命令"
|
||||
command_pattern = r"^/permission(?:\s|$)"
|
||||
command_help = "/permission <子命令> [参数...]"
|
||||
command_description = "权限管理命令,支持授权、撤销、查询等功能"
|
||||
command_aliases = ["perm", "权限"]
|
||||
priority = 10
|
||||
chat_type_allow = ChatType.ALL
|
||||
intercept_message = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -44,26 +47,16 @@ class PermissionCommand(BaseCommand):
|
||||
True
|
||||
)
|
||||
|
||||
def can_execute(self) -> bool:
|
||||
"""检查命令是否可以执行"""
|
||||
# 基本权限检查由权限系统处理
|
||||
return True
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str], bool]:
|
||||
async def execute(self, args: CommandArgs) -> Tuple[bool, Optional[str], bool]:
|
||||
"""执行权限管理命令"""
|
||||
# 从消息中解析命令和参数
|
||||
message_text = self.message.processed_plain_text.strip()
|
||||
# 移除 /permission 前缀,获取后续参数
|
||||
args_text = message_text[11:].strip() # "/permission" 是11个字符
|
||||
|
||||
if not args_text:
|
||||
if args.is_empty():
|
||||
await self._show_help()
|
||||
return True, "显示帮助信息", True
|
||||
|
||||
args = args_text.split()
|
||||
subcommand = args[0].lower()
|
||||
remaining_args = args[1:]
|
||||
subcommand = args.get_first().lower()
|
||||
remaining_args = args.get_args()[1:] # 获取除第一个参数外的所有参数
|
||||
chat_stream = self.message.chat_stream
|
||||
|
||||
# 检查基本查看权限
|
||||
can_view = permission_api.check_permission(
|
||||
chat_stream.platform,
|
||||
@@ -149,14 +142,16 @@ class PermissionCommand(BaseCommand):
|
||||
• /permission grant @张三 plugin.example.command
|
||||
• /permission list 123456789
|
||||
• /permission nodes example_plugin
|
||||
• /permission allnodes"""
|
||||
• /permission allnodes
|
||||
|
||||
🔄 别名:可以使用 /perm 或 /权限 代替 /permission"""
|
||||
|
||||
await self.send_text(help_text)
|
||||
|
||||
def _parse_user_mention(self, mention: str) -> Optional[str]:
|
||||
"""解析用户提及,提取QQ号"""
|
||||
# 匹配 @用户 格式,提取QQ号
|
||||
at_match = re.search(r'\[CQ:at,qq=(\d+)\]', mention)
|
||||
# 匹配 @<用户名:QQ号> 格式,提取QQ号
|
||||
at_match = re.search(r'@<[^:]+:(\d+)>', mention)
|
||||
if at_match:
|
||||
return at_match.group(1)
|
||||
|
||||
@@ -166,7 +161,7 @@ class PermissionCommand(BaseCommand):
|
||||
|
||||
return None
|
||||
|
||||
async def _grant_permission(self, chat_stream , args: List[str]):
|
||||
async def _grant_permission(self, chat_stream, args: List[str]):
|
||||
"""授权用户权限"""
|
||||
if len(args) < 2:
|
||||
await self.send_text("❌ 用法: /permission grant <@用户|QQ号> <权限节点>")
|
||||
@@ -387,5 +382,6 @@ class PermissionManagerPlugin(BasePlugin):
|
||||
}
|
||||
}
|
||||
|
||||
def get_plugin_components(self) -> List[Tuple[CommandInfo, Type[BaseCommand]]]:
|
||||
return [(PermissionCommand.get_command_info(), PermissionCommand)]
|
||||
def get_plugin_components(self) -> List[Tuple[PlusCommandInfo, Type[PlusCommand]]]:
|
||||
"""返回插件的PlusCommand组件"""
|
||||
return [(PermissionCommand.get_plus_command_info(), PermissionCommand)]
|
||||
@@ -8,7 +8,9 @@ from src.common.database.monthly_plan_db import (
|
||||
add_new_plans,
|
||||
get_archived_plans_for_month,
|
||||
archive_active_plans_for_month,
|
||||
has_active_plans
|
||||
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
|
||||
@@ -67,7 +69,23 @@ class MonthlyPlanManager:
|
||||
logger.info(f" {target_month} 没有任何有效的月度计划,将立即生成。")
|
||||
return await self.generate_monthly_plans(target_month)
|
||||
else:
|
||||
# logger.info(f"{target_month} 已存在有效的月度计划,跳过生成。")
|
||||
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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import orjson
|
||||
import asyncio
|
||||
import random
|
||||
from datetime import datetime, time, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
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 json_repair import repair_json
|
||||
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")
|
||||
@@ -128,6 +131,17 @@ class ScheduleManager:
|
||||
self.sleep_log_interval = 35 # 日志记录间隔,单位秒
|
||||
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):
|
||||
"""启动每日零点自动生成新日程的任务"""
|
||||
if not self.daily_task_started:
|
||||
@@ -392,27 +406,118 @@ class ScheduleManager:
|
||||
continue
|
||||
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:
|
||||
return False
|
||||
if not self.today_schedule:
|
||||
return False
|
||||
|
||||
# 从配置获取关键词,如果配置中没有则使用默认列表
|
||||
sleep_keywords = ["休眠", "睡觉", "梦乡",]
|
||||
|
||||
now = datetime.now().time()
|
||||
now = datetime.now()
|
||||
today = now.date()
|
||||
|
||||
# 遍历当天的所有日程
|
||||
# --- 每日状态重置 ---
|
||||
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:
|
||||
try:
|
||||
activity = event.get("activity", "").strip()
|
||||
@@ -421,47 +526,130 @@ class ScheduleManager:
|
||||
if not activity or not time_range:
|
||||
continue
|
||||
|
||||
# 1. 检查活动内容是否包含任一休眠关键词
|
||||
if any(keyword in activity for keyword in sleep_keywords):
|
||||
# 2. 如果包含,再检查当前时间是否在该时间段内
|
||||
start_str, end_str = time_range.split('-')
|
||||
start_time = datetime.strptime(start_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 <= now < end_time:
|
||||
is_in_time_range = True
|
||||
if start_time <= now_time < end_time:
|
||||
return True, activity
|
||||
else: # 跨天
|
||||
if now >= start_time or now < end_time:
|
||||
is_in_time_range = True
|
||||
|
||||
# 如果时间匹配,则进入最终判断
|
||||
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
|
||||
|
||||
if now_time >= start_time or now_time < end_time:
|
||||
return True, activity
|
||||
except (ValueError, KeyError, AttributeError) as e:
|
||||
logger.warning(f"解析日程事件时出错: {event}, 错误: {e}")
|
||||
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)
|
||||
|
||||
# 遍历完所有日程都未找到匹配的休眠活动
|
||||
return False
|
||||
end_time_ts = state.get("sleep_buffer_end_time_ts")
|
||||
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:
|
||||
"""使用Pydantic验证日程数据格式和完整性"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[inner]
|
||||
version = "6.5.4"
|
||||
version = "6.5.7"
|
||||
|
||||
#----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读----
|
||||
#如果你想要修改配置文件,请递增version的值
|
||||
@@ -52,6 +52,9 @@ qq_account = 1145141919810 # MoFox-Bot的QQ账号
|
||||
nickname = "MoFox-Bot" # MoFox-Bot的昵称
|
||||
alias_names = ["麦叠", "牢麦"] # MoFox-Bot的别名
|
||||
|
||||
[command]
|
||||
command_prefixes = ['/', '!', '.', '#']
|
||||
|
||||
[personality]
|
||||
# 建议50字以内,描述人格的核心特质
|
||||
personality_core = "是一个积极向上的女大学生"
|
||||
@@ -375,6 +378,21 @@ guidelines = """
|
||||
"""
|
||||
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] # 视频分析配置
|
||||
enable = true # 是否启用视频分析功能
|
||||
analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢 "建议frames大于8时不要使用这个" ...但是详细)、"batch_frames"(批量分析,快但可能略简单 -其实效果也差不多)或 "auto"(自动选择)
|
||||
|
||||
Reference in New Issue
Block a user