12
.github/workflows/ruff.yml
vendored
12
.github/workflows/ruff.yml
vendored
@@ -1,12 +1,12 @@
|
||||
name: Ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev-refactor # 例如:匹配所有以 feature/ 开头的分支
|
||||
# 添加你希望触发此 workflow 的其他分支
|
||||
# push:
|
||||
# branches:
|
||||
# - main
|
||||
# - dev
|
||||
# - dev-refactor # 例如:匹配所有以 feature/ 开头的分支
|
||||
# # 添加你希望触发此 workflow 的其他分支
|
||||
workflow_dispatch: # 允许手动触发工作流
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
## 🔥 更新和安装
|
||||
|
||||
**最新版本: v0.9.0** ([更新日志](changelogs/changelog.md))
|
||||
**最新版本: v0.9.1** ([更新日志](changelogs/changelog.md))
|
||||
|
||||
可前往 [Release](https://github.com/MaiM-with-u/MaiBot/releases/) 页面下载最新版本
|
||||
可前往 [启动器发布页面](https://github.com/MaiM-with-u/mailauncher/releases/)下载最新启动器
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [0.9.0] - 2025-7-25
|
||||
## [0.9.1] - 2025-7-25
|
||||
|
||||
- 修复reply导致的planner异常空跳
|
||||
- 修复表达方式迁移空目录问题
|
||||
- 修复reply_to空字段问题
|
||||
- 将metioned bot 和 at应用到focus prompt中
|
||||
- 更好的兴趣度计算
|
||||
- 修复部分模型由于enable_thinking导致的400问题
|
||||
- 优化关键词提取
|
||||
|
||||
## [0.9.0] - 2025-7-24
|
||||
|
||||
### 摘要
|
||||
MaiBot 0.9.0 重磅升级!本版本带来两大核心突破:**全面重构的插件系统**提供更强大的扩展能力和管理功能;**normal和focus模式统一化处理**大幅简化架构并提升性能。同时新增s4u prompt模式优化、语音消息支持、全新情绪系统和mais4u直播互动功能,为MaiBot带来更自然、更智能的交互体验!
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
- `database_api.py`中的`db_query`方法调整了参数顺序以增强参数限制的同时,保证了typing正确;`db_get`方法增加了`single_result`参数,与`db_query`保持一致。
|
||||
5. 增加了`logging_api`,可以用`get_logger`来获取日志记录器。
|
||||
6. 增加了插件和组件管理的API。
|
||||
7. `BaseCommand`的`execute`方法现在返回一个三元组,包含是否执行成功、可选的回复消息和是否拦截消息。
|
||||
- 这意味着你终于可以动态控制是否继续后续消息的处理了。
|
||||
|
||||
# 插件系统修改
|
||||
1. 现在所有的匹配模式不再是关键字了,而是枚举类。**(可能有遗漏)**
|
||||
|
||||
@@ -4,42 +4,183 @@
|
||||
|
||||
Action是给麦麦在回复之外提供额外功能的智能组件,**由麦麦的决策系统自主选择是否使用**,具有随机性和拟人化的调用特点。Action不是直接响应用户命令,而是让麦麦根据聊天情境智能地选择合适的动作,使其行为更加自然和真实。
|
||||
|
||||
### 🎯 Action的特点
|
||||
### Action的特点
|
||||
|
||||
- 🧠 **智能激活**:麦麦根据多种条件智能判断是否使用
|
||||
- 🎲 **随机性**:增加行为的不可预测性,更接近真人交流
|
||||
- 🎲 **可随机性**:可以使用随机数激活,增加行为的不可预测性,更接近真人交流
|
||||
- 🤖 **拟人化**:让麦麦的回应更自然、更有个性
|
||||
- 🔄 **情境感知**:基于聊天上下文做出合适的反应
|
||||
|
||||
## 🎯 两层决策机制
|
||||
---
|
||||
|
||||
## 🎯 Action组件的基本结构
|
||||
首先,所有的Action都应该继承`BaseAction`类。
|
||||
|
||||
其次,每个Action组件都应该实现以下基本信息:
|
||||
```python
|
||||
class ExampleAction(BaseAction):
|
||||
action_name = "example_action" # 动作的唯一标识符
|
||||
action_description = "这是一个示例动作" # 动作描述
|
||||
activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例
|
||||
mode_enable = ChatMode.ALL # 这里以 ALL 为例
|
||||
associated_types = ["text", "emoji", ...] # 关联类型
|
||||
parallel_action = False # 是否允许与其他Action并行执行
|
||||
action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...}
|
||||
# Action使用场景描述 - 帮助LLM判断何时"选择"使用
|
||||
action_require = ["使用场景描述1", "使用场景描述2", ...]
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
执行Action的主要逻辑
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否成功, 执行结果描述)
|
||||
"""
|
||||
# ---- 执行动作的逻辑 ----
|
||||
return True, "执行成功"
|
||||
```
|
||||
#### associated_types: 该Action会发送的消息类型,例如文本、表情等。
|
||||
|
||||
这部分由Adapter传递给处理器。
|
||||
|
||||
以 MaiBot-Napcat-Adapter 为例,可选项目如下:
|
||||
| 类型 | 说明 | 格式 |
|
||||
| --- | --- | --- |
|
||||
| text | 文本消息 | str |
|
||||
| emoji | 表情消息 | str: 表情包的无头base64|
|
||||
| image | 图片消息 | str: 图片的无头base64 |
|
||||
| reply | 回复消息 | str: 回复的消息ID |
|
||||
| voice | 语音消息 | str: wav格式语音的无头base64 |
|
||||
| command | 命令消息 | 参见Adapter文档 |
|
||||
| voiceurl | 语音URL消息 | str: wav格式语音的URL |
|
||||
| music | 音乐消息 | str: 这首歌在网易云音乐的音乐id |
|
||||
| videourl | 视频URL消息 | str: 视频的URL |
|
||||
| file | 文件消息 | str: 文件的路径 |
|
||||
|
||||
**请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。**
|
||||
|
||||
#### action_parameters: 该Action的参数说明。
|
||||
这是一个字典,键为参数名,值为参数说明。这个字段可以帮助LLM理解如何使用这个Action,并由LLM返回对应的参数,最后传递到 Action 的 action_data 属性中。其格式与你定义的格式完全相同 **(除非LLM哈气了,返回了错误的内容)**。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Action 调用的决策机制
|
||||
|
||||
Action采用**两层决策机制**来优化性能和决策质量:
|
||||
|
||||
### 第一层:激活控制(Activation Control)
|
||||
> 设计目的:在加载许多插件的时候降低LLM决策压力,避免让麦麦在过多的选项中纠结。
|
||||
|
||||
**激活决定麦麦是否"知道"这个Action的存在**,即这个Action是否进入决策候选池。**不被激活的Action麦麦永远不会选择**。
|
||||
**第一层:激活控制(Activation Control)**
|
||||
|
||||
> 🎯 **设计目的**:在加载许多插件的时候降低LLM决策压力,避免让麦麦在过多的选项中纠结。
|
||||
激活决定麦麦是否 **“知道”** 这个Action的存在,即这个Action是否进入决策候选池。不被激活的Action麦麦永远不会选择。
|
||||
|
||||
#### 激活类型说明
|
||||
**第二层:使用决策(Usage Decision)**
|
||||
|
||||
| 激活类型 | 说明 | 使用场景 |
|
||||
| ------------- | ------------------------------------------- | ------------------------ |
|
||||
| `NEVER` | 从不激活,Action对麦麦不可见 | 临时禁用某个Action |
|
||||
| `ALWAYS` | 永远激活,Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 |
|
||||
| `LLM_JUDGE` | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 |
|
||||
| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 |
|
||||
| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 |
|
||||
在Action被激活后,使用条件决定麦麦什么时候会 **“选择”** 使用这个Action。
|
||||
|
||||
#### 聊天模式控制
|
||||
### 决策参数详解 🔧
|
||||
|
||||
| 模式 | 说明 |
|
||||
| ------------------- | ------------------------ |
|
||||
| `ChatMode.FOCUS` | 仅在专注聊天模式下可激活 |
|
||||
| `ChatMode.NORMAL` | 仅在普通聊天模式下可激活 |
|
||||
| `ChatMode.ALL` | 所有模式下都可激活 |
|
||||
#### 第一层:ActivationType 激活类型说明
|
||||
|
||||
### 第二层:使用决策(Usage Decision)
|
||||
| 激活类型 | 说明 | 使用场景 |
|
||||
| ----------- | ---------------------------------------- | ---------------------- |
|
||||
| [`NEVER`](#never-激活) | 从不激活,Action对麦麦不可见 | 临时禁用某个Action |
|
||||
| [`ALWAYS`](#always-激活) | 永远激活,Action总是在麦麦的候选池中 | 核心功能,如回复、不回复 |
|
||||
| [`LLM_JUDGE`](#llm_judge-激活) | 通过LLM智能判断当前情境是否需要激活此Action | 需要智能判断的复杂场景 |
|
||||
| `RANDOM` | 基于随机概率决定是否激活 | 增加行为随机性的功能 |
|
||||
| `KEYWORD` | 当检测到特定关键词时激活 | 明确触发条件的功能 |
|
||||
|
||||
#### `NEVER` 激活
|
||||
|
||||
`ActionActivationType.NEVER` 会使得 Action 永远不会被激活
|
||||
|
||||
```python
|
||||
class DisabledAction(BaseAction):
|
||||
activation_type = ActionActivationType.NEVER # 永远不激活
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 这个Action永远不会被执行
|
||||
return False, "这个Action被禁用"
|
||||
```
|
||||
|
||||
#### `ALWAYS` 激活
|
||||
|
||||
`ActionActivationType.ALWAYS` 会使得 Action 永远会被激活,即一直在 Action 候选池中
|
||||
|
||||
这种激活方式常用于核心功能,如回复或不回复。
|
||||
|
||||
```python
|
||||
class AlwaysActivatedAction(BaseAction):
|
||||
activation_type = ActionActivationType.ALWAYS # 永远激活
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 执行核心功能
|
||||
return True, "执行了核心功能"
|
||||
```
|
||||
|
||||
#### `LLM_JUDGE` 激活
|
||||
|
||||
`ActionActivationType.LLM_JUDGE`会使得这个 Action 根据 LLM 的判断来决定是否加入候选池。
|
||||
|
||||
而 LLM 的判断是基于代码中预设的`llm_judge_prompt`和自动提供的聊天上下文进行的。
|
||||
|
||||
因此使用此种方法需要实现`llm_judge_prompt`属性。
|
||||
|
||||
```python
|
||||
class LLMJudgedAction(BaseAction):
|
||||
activation_type = ActionActivationType.LLM_JUDGE # 通过LLM判断激活
|
||||
# LLM判断提示词
|
||||
llm_judge_prompt = (
|
||||
"判定是否需要使用这个动作的条件:\n"
|
||||
"1. 用户希望调用XXX这个动作\n"
|
||||
"...\n"
|
||||
"请回答\"是\"或\"否\"。\n"
|
||||
)
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 根据LLM判断是否执行
|
||||
return True, "执行了LLM判断功能"
|
||||
```
|
||||
|
||||
#### `RANDOM` 激活
|
||||
|
||||
`ActionActivationType.RANDOM`会使得这个 Action 根据随机概率决定是否加入候选池。
|
||||
|
||||
概率则由代码中的`random_activation_probability`控制。在内部实现中我们使用了`random.random()`来生成一个0到1之间的随机数,并与这个概率进行比较。
|
||||
|
||||
因此使用这个方法需要实现`random_activation_probability`属性。
|
||||
|
||||
```python
|
||||
class SurpriseAction(BaseAction):
|
||||
activation_type = ActionActivationType.RANDOM # 基于随机概率激活
|
||||
# 随机激活概率
|
||||
random_activation_probability = 0.1 # 10%概率激活
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 执行惊喜动作
|
||||
return True, "发送了惊喜内容"
|
||||
```
|
||||
|
||||
#### `KEYWORD` 激活
|
||||
|
||||
`ActionActivationType.KEYWORD`会使得这个 Action 在检测到特定关键词时激活。
|
||||
|
||||
关键词由代码中的`activation_keywords`定义,而`keyword_case_sensitive`则控制关键词匹配时是否区分大小写。在内部实现中,我们使用了`in`操作符来检查消息内容是否包含这些关键词。
|
||||
|
||||
因此,使用此种方法需要实现`activation_keywords`和`keyword_case_sensitive`属性。
|
||||
|
||||
```python
|
||||
class GreetingAction(BaseAction):
|
||||
activation_type = ActionActivationType.KEYWORD # 关键词激活
|
||||
activation_keywords = ["你好", "hello", "hi", "嗨"] # 关键词配置
|
||||
keyword_case_sensitive = False # 不区分大小写
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 执行问候逻辑
|
||||
return True, "发送了问候"
|
||||
```
|
||||
|
||||
#### 第二层:使用决策
|
||||
|
||||
**在Action被激活后,使用条件决定麦麦什么时候会"选择"使用这个Action**。
|
||||
|
||||
@@ -49,17 +190,16 @@ Action采用**两层决策机制**来优化性能和决策质量:
|
||||
- `action_parameters`:所需参数,影响Action的可执行性
|
||||
- 当前聊天上下文和麦麦的决策逻辑
|
||||
|
||||
### 🎬 决策流程示例
|
||||
---
|
||||
|
||||
假设有一个"发送表情"Action:
|
||||
### 决策流程示例
|
||||
|
||||
```python
|
||||
class EmojiAction(BaseAction):
|
||||
# 第一层:激活控制
|
||||
focus_activation_type = ActionActivationType.RANDOM # 专注模式下随机激活
|
||||
normal_activation_type = ActionActivationType.KEYWORD # 普通模式下关键词激活
|
||||
activation_keywords = ["表情", "emoji", "😊"]
|
||||
|
||||
activation_type = ActionActivationType.RANDOM # 随机激活
|
||||
random_activation_probability = 0.1 # 10%概率激活
|
||||
|
||||
# 第二层:使用决策
|
||||
action_require = [
|
||||
"表达情绪时可以选择使用",
|
||||
@@ -72,311 +212,84 @@ class EmojiAction(BaseAction):
|
||||
|
||||
1. **第一层激活判断**:
|
||||
|
||||
- 普通模式:只有当用户消息包含"表情"、"emoji"或"😊"时,麦麦才"知道"可以使用这个Action
|
||||
- 专注模式:随机激活,有概率让麦麦"看到"这个Action
|
||||
- 使用随机数进行决策,当`random.random() < self.random_activation_probability`时,麦麦才"知道"可以使用这个Action
|
||||
2. **第二层使用决策**:
|
||||
|
||||
- 即使Action被激活,麦麦还会根据 `action_require`中的条件判断是否真正选择使用
|
||||
- 即使Action被激活,麦麦还会根据 `action_require` 中的条件判断是否真正选择使用
|
||||
- 例如:如果刚刚已经发过表情,根据"不要连续发送多个表情"的要求,麦麦可能不会选择这个Action
|
||||
|
||||
## 📋 Action必须项清单
|
||||
|
||||
每个Action类都**必须**包含以下属性:
|
||||
|
||||
### 1. 激活控制必须项
|
||||
---
|
||||
|
||||
## Action 内置属性说明
|
||||
```python
|
||||
# 专注模式下的激活类型
|
||||
focus_activation_type = ActionActivationType.LLM_JUDGE
|
||||
|
||||
# 普通模式下的激活类型
|
||||
normal_activation_type = ActionActivationType.KEYWORD
|
||||
|
||||
# 启用的聊天模式
|
||||
mode_enable = ChatMode.ALL
|
||||
|
||||
# 是否允许与其他Action并行执行
|
||||
parallel_action = False
|
||||
```
|
||||
|
||||
### 2. 基本信息必须项
|
||||
|
||||
```python
|
||||
# Action的唯一标识名称
|
||||
action_name = "my_action"
|
||||
|
||||
# Action的功能描述
|
||||
action_description = "描述这个Action的具体功能和用途"
|
||||
```
|
||||
|
||||
### 3. 功能定义必须项
|
||||
|
||||
```python
|
||||
# Action参数定义 - 告诉LLM执行时需要什么参数
|
||||
action_parameters = {
|
||||
"param1": "参数1的说明",
|
||||
"param2": "参数2的说明"
|
||||
}
|
||||
|
||||
# Action使用场景描述 - 帮助LLM判断何时"选择"使用
|
||||
action_require = [
|
||||
"使用场景描述1",
|
||||
"使用场景描述2"
|
||||
]
|
||||
|
||||
# 关联的消息类型 - 说明Action能处理什么类型的内容
|
||||
associated_types = ["text", "emoji", "image"]
|
||||
```
|
||||
|
||||
### 4. 执行方法必须项
|
||||
|
||||
```python
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
"""
|
||||
执行Action的主要逻辑
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str]: (是否成功, 执行结果描述)
|
||||
"""
|
||||
# 执行动作的代码
|
||||
success = True
|
||||
message = "动作执行成功"
|
||||
|
||||
return success, message
|
||||
```
|
||||
|
||||
## 🔧 激活类型详解
|
||||
|
||||
### KEYWORD激活
|
||||
|
||||
当检测到特定关键词时激活Action:
|
||||
|
||||
```python
|
||||
class GreetingAction(BaseAction):
|
||||
focus_activation_type = ActionActivationType.KEYWORD
|
||||
normal_activation_type = ActionActivationType.KEYWORD
|
||||
|
||||
# 关键词配置
|
||||
activation_keywords = ["你好", "hello", "hi", "嗨"]
|
||||
keyword_case_sensitive = False # 不区分大小写
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 执行问候逻辑
|
||||
return True, "发送了问候"
|
||||
```
|
||||
|
||||
### LLM_JUDGE激活
|
||||
|
||||
通过LLM智能判断是否激活:
|
||||
|
||||
```python
|
||||
class HelpAction(BaseAction):
|
||||
focus_activation_type = ActionActivationType.LLM_JUDGE
|
||||
normal_activation_type = ActionActivationType.LLM_JUDGE
|
||||
|
||||
# LLM判断提示词
|
||||
llm_judge_prompt = """
|
||||
判定是否需要使用帮助动作的条件:
|
||||
1. 用户表达了困惑或需要帮助
|
||||
2. 用户提出了问题但没有得到满意答案
|
||||
3. 对话中出现了技术术语或复杂概念
|
||||
|
||||
请回答"是"或"否"。
|
||||
"""
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 执行帮助逻辑
|
||||
return True, "提供了帮助"
|
||||
```
|
||||
|
||||
### RANDOM激活
|
||||
|
||||
基于随机概率激活:
|
||||
|
||||
```python
|
||||
class SurpriseAction(BaseAction):
|
||||
focus_activation_type = ActionActivationType.RANDOM
|
||||
normal_activation_type = ActionActivationType.RANDOM
|
||||
|
||||
# 随机激活概率
|
||||
random_activation_probability = 0.1 # 10%概率激活
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 执行惊喜动作
|
||||
return True, "发送了惊喜内容"
|
||||
```
|
||||
|
||||
### ALWAYS激活
|
||||
|
||||
永远激活,常用于核心功能:
|
||||
|
||||
```python
|
||||
class CoreAction(BaseAction):
|
||||
focus_activation_type = ActionActivationType.ALWAYS
|
||||
normal_activation_type = ActionActivationType.ALWAYS
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 执行核心功能
|
||||
return True, "执行了核心功能"
|
||||
```
|
||||
|
||||
### NEVER激活
|
||||
|
||||
从不激活,用于临时禁用:
|
||||
|
||||
```python
|
||||
class DisabledAction(BaseAction):
|
||||
focus_activation_type = ActionActivationType.NEVER
|
||||
normal_activation_type = ActionActivationType.NEVER
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
# 这个方法不会被调用
|
||||
return False, "已禁用"
|
||||
```
|
||||
|
||||
## 📚 BaseAction内置属性和方法
|
||||
|
||||
### 内置属性
|
||||
|
||||
```python
|
||||
class MyAction(BaseAction):
|
||||
class BaseAction:
|
||||
def __init__(self):
|
||||
# 消息相关属性
|
||||
self.message # 当前消息对象
|
||||
self.chat_stream # 聊天流对象
|
||||
self.user_id # 用户ID
|
||||
self.user_nickname # 用户昵称
|
||||
self.platform # 平台类型 (qq, telegram等)
|
||||
self.chat_id # 聊天ID
|
||||
self.is_group # 是否群聊
|
||||
|
||||
self.log_prefix: str # 日志前缀
|
||||
self.group_id: str # 群组ID
|
||||
self.group_name: str # 群组名称
|
||||
self.user_id: str # 用户ID
|
||||
self.user_nickname: str # 用户昵称
|
||||
self.platform: str # 平台类型 (qq, telegram等)
|
||||
self.chat_id: str # 聊天ID
|
||||
self.chat_stream: ChatStream # 聊天流对象
|
||||
self.is_group: bool # 是否群聊
|
||||
|
||||
# 消息体
|
||||
self.action_message: dict # 消息数据
|
||||
|
||||
# Action相关属性
|
||||
self.action_data # Action执行时的数据
|
||||
self.thinking_id # 思考ID
|
||||
self.matched_groups # 匹配到的组(如果有正则匹配)
|
||||
self.action_data: dict # Action执行时的数据
|
||||
self.thinking_id: str # 思考ID
|
||||
```
|
||||
|
||||
### 内置方法
|
||||
action_message为一个字典,包含的键值对如下(省略了不必要的键值对)
|
||||
|
||||
```python
|
||||
class MyAction(BaseAction):
|
||||
# 配置相关
|
||||
{
|
||||
"message_id": "1234567890", # 消息id,str
|
||||
"time": 1627545600.0, # 时间戳,float
|
||||
"chat_id": "abcdef123456", # 聊天ID,str
|
||||
"reply_to": None, # 回复消息id,str或None
|
||||
"interest_value": 0.85, # 兴趣值,float
|
||||
"is_mentioned": True, # 是否被提及,bool
|
||||
"chat_info_last_active_time": 1627548600.0, # 最后活跃时间,float
|
||||
"processed_plain_text": None, # 处理后的文本,str或None
|
||||
"additional_config": None, # Adapter传来的additional_config,dict或None
|
||||
"is_emoji": False, # 是否为表情,bool
|
||||
"is_picid": False, # 是否为图片ID,bool
|
||||
"is_command": False # 是否为命令,bool
|
||||
}
|
||||
```
|
||||
|
||||
部分值的格式请自行查询数据库。
|
||||
|
||||
---
|
||||
|
||||
## Action 内置方法说明
|
||||
```python
|
||||
class BaseAction:
|
||||
def get_config(self, key: str, default=None):
|
||||
"""获取配置值"""
|
||||
pass
|
||||
"""获取插件配置值,使用嵌套键访问"""
|
||||
|
||||
# 消息发送相关
|
||||
async def send_text(self, text: str):
|
||||
async def wait_for_new_message(self, timeout: int = 1200) -> Tuple[bool, str]:
|
||||
"""等待新消息或超时"""
|
||||
|
||||
async def send_text(self, content: str, reply_to: str = "", reply_to_platform_id: str = "", typing: bool = False) -> bool:
|
||||
"""发送文本消息"""
|
||||
pass
|
||||
|
||||
async def send_emoji(self, emoji_base64: str):
|
||||
|
||||
async def send_emoji(self, emoji_base64: str) -> bool:
|
||||
"""发送表情包"""
|
||||
pass
|
||||
|
||||
async def send_image(self, image_base64: str):
|
||||
|
||||
async def send_image(self, image_base64: str) -> bool:
|
||||
"""发送图片"""
|
||||
pass
|
||||
|
||||
# 动作记录相关
|
||||
async def store_action_info(self, **kwargs):
|
||||
"""记录动作信息"""
|
||||
pass
|
||||
|
||||
async def send_custom(self, message_type: str, content: str, typing: bool = False, reply_to: str = "") -> bool:
|
||||
"""发送自定义类型消息"""
|
||||
|
||||
async def store_action_info(self, action_build_into_prompt: bool = False, action_prompt_display: str = "", action_done: bool = True) -> None:
|
||||
"""存储动作信息到数据库"""
|
||||
|
||||
async def send_command(self, command_name: str, args: Optional[dict] = None, display_message: str = "", storage_message: bool = True) -> bool:
|
||||
"""发送命令消息"""
|
||||
```
|
||||
|
||||
## 🎯 完整Action示例
|
||||
|
||||
```python
|
||||
from src.plugin_system import BaseAction, ActionActivationType, ChatMode
|
||||
from typing import Tuple
|
||||
|
||||
class ExampleAction(BaseAction):
|
||||
"""示例Action - 展示完整的Action结构"""
|
||||
|
||||
# === 激活控制 ===
|
||||
focus_activation_type = ActionActivationType.LLM_JUDGE
|
||||
normal_activation_type = ActionActivationType.KEYWORD
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = False
|
||||
|
||||
# 关键词激活配置
|
||||
activation_keywords = ["示例", "测试", "example"]
|
||||
keyword_case_sensitive = False
|
||||
|
||||
# LLM判断提示词
|
||||
llm_judge_prompt = "当用户需要示例或测试功能时激活"
|
||||
|
||||
# 随机激活概率(如果使用RANDOM类型)
|
||||
random_activation_probability = 0.2
|
||||
|
||||
# === 基本信息 ===
|
||||
action_name = "example_action"
|
||||
action_description = "这是一个示例Action,用于演示Action的完整结构"
|
||||
|
||||
# === 功能定义 ===
|
||||
action_parameters = {
|
||||
"content": "要处理的内容",
|
||||
"type": "处理类型",
|
||||
"options": "可选配置"
|
||||
}
|
||||
|
||||
action_require = [
|
||||
"用户需要示例功能时使用",
|
||||
"适合用于测试和演示",
|
||||
"不要在正式对话中频繁使用"
|
||||
]
|
||||
|
||||
associated_types = ["text", "emoji"]
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
"""执行示例Action"""
|
||||
try:
|
||||
# 获取Action参数
|
||||
content = self.action_data.get("content", "默认内容")
|
||||
action_type = self.action_data.get("type", "default")
|
||||
|
||||
# 获取配置
|
||||
enable_feature = self.get_config("example.enable_advanced", False)
|
||||
max_length = self.get_config("example.max_length", 100)
|
||||
|
||||
# 执行具体逻辑
|
||||
if action_type == "greeting":
|
||||
await self.send_text(f"你好!这是示例内容:{content}")
|
||||
elif action_type == "info":
|
||||
await self.send_text(f"信息:{content[:max_length]}")
|
||||
else:
|
||||
await self.send_text("执行了示例Action")
|
||||
|
||||
# 记录动作信息
|
||||
await self.store_action_info(
|
||||
action_build_into_prompt=True,
|
||||
action_prompt_display=f"执行了示例动作:{action_type}",
|
||||
action_done=True
|
||||
)
|
||||
|
||||
return True, f"示例Action执行成功,类型:{action_type}"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"执行失败:{str(e)}"
|
||||
```
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 1. Action设计原则
|
||||
|
||||
- **单一职责**:每个Action只负责一个明确的功能
|
||||
- **智能激活**:合理选择激活类型,避免过度激活
|
||||
- **清晰描述**:提供准确的`action_require`帮助LLM决策
|
||||
- **错误处理**:妥善处理执行过程中的异常情况
|
||||
|
||||
### 2. 性能优化
|
||||
|
||||
- **激活控制**:使用合适的激活类型减少不必要的LLM调用
|
||||
- **并行执行**:谨慎设置`parallel_action`,避免冲突
|
||||
- **资源管理**:及时释放占用的资源
|
||||
|
||||
### 3. 调试技巧
|
||||
|
||||
- **日志记录**:在关键位置添加日志
|
||||
- **参数验证**:检查`action_data`的有效性
|
||||
- **配置测试**:测试不同配置下的行为
|
||||
具体参数与用法参见`BaseAction`基类的定义。
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## 📖 什么是Command
|
||||
|
||||
Command是直接响应用户明确指令的组件,与Action不同,Command是**被动触发**的,当用户输入特定格式的命令时立即执行。Command通过正则表达式匹配用户输入,提供确定性的功能服务。
|
||||
Command是直接响应用户明确指令的组件,与Action不同,Command是**被动触发**的,当用户输入特定格式的命令时立即执行。
|
||||
|
||||
Command通过正则表达式匹配用户输入,提供确定性的功能服务。
|
||||
|
||||
### 🎯 Command的特点
|
||||
|
||||
@@ -12,501 +14,76 @@ Command是直接响应用户明确指令的组件,与Action不同,Command是
|
||||
- 🛑 **拦截控制**:可以控制是否阻止消息继续处理
|
||||
- 📝 **参数解析**:支持从用户输入中提取参数
|
||||
|
||||
## 🆚 Action vs Command 核心区别
|
||||
---
|
||||
|
||||
| 特征 | Action | Command |
|
||||
| ------------------ | --------------------- | ---------------- |
|
||||
| **触发方式** | 麦麦主动决策使用 | 用户主动触发 |
|
||||
| **决策机制** | 两层决策(激活+使用) | 直接匹配执行 |
|
||||
| **随机性** | 有随机性和智能性 | 确定性执行 |
|
||||
| **用途** | 增强麦麦行为拟人化 | 提供具体功能服务 |
|
||||
| **性能影响** | 需要LLM决策 | 正则匹配,性能好 |
|
||||
## 🛠️ Command组件的基本结构
|
||||
|
||||
## 🏗️ Command基本结构
|
||||
|
||||
### 必须属性
|
||||
首先,Command组件需要继承自`BaseCommand`类,并实现必要的方法。
|
||||
|
||||
```python
|
||||
from src.plugin_system import BaseCommand
|
||||
class ExampleCommand(BaseCommand):
|
||||
command_name = "example" # 命令名称,作为唯一标识符
|
||||
command_description = "这是一个示例命令" # 命令描述
|
||||
command_pattern = r"" # 命令匹配的正则表达式
|
||||
|
||||
class MyCommand(BaseCommand):
|
||||
# 正则表达式匹配模式
|
||||
command_pattern = r"^/help\s+(?P<topic>\w+)$"
|
||||
|
||||
# 命令帮助说明
|
||||
command_help = "显示指定主题的帮助信息"
|
||||
|
||||
# 使用示例
|
||||
command_examples = ["/help action", "/help command"]
|
||||
|
||||
# 是否拦截后续处理
|
||||
intercept_message = True
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
"""执行命令逻辑"""
|
||||
# 命令执行逻辑
|
||||
return True, "执行成功"
|
||||
async def execute(self) -> Tuple[bool, Optional[str], bool]:
|
||||
"""
|
||||
执行Command的主要逻辑
|
||||
|
||||
Returns:
|
||||
Tuple[bool, str, bool]:
|
||||
- 第一个bool表示是否成功执行
|
||||
- 第二个str是执行结果消息
|
||||
- 第三个bool表示是否需要阻止消息继续处理
|
||||
"""
|
||||
# ---- 执行命令的逻辑 ----
|
||||
return True, "执行成功", False
|
||||
```
|
||||
**`command_pattern`**: 该Command匹配的正则表达式,用于精确匹配用户输入。
|
||||
|
||||
### 属性说明
|
||||
请注意:如果希望能获取到命令中的参数,请在正则表达式中使用有命名的捕获组,例如`(?P<param_name>pattern)`。
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
| --------------------- | --------- | -------------------- |
|
||||
| `command_pattern` | str | 正则表达式匹配模式 |
|
||||
| `command_help` | str | 命令帮助说明 |
|
||||
| `command_examples` | List[str] | 使用示例列表 |
|
||||
| `intercept_message` | bool | 是否拦截消息继续处理 |
|
||||
这样在匹配时,内部实现可以使用`re.match.groupdict()`方法获取到所有捕获组的参数,并以字典的形式存储在`self.matched_groups`中。
|
||||
|
||||
## 🔍 正则表达式匹配
|
||||
|
||||
### 基础匹配
|
||||
### 匹配样例
|
||||
假设我们有一个命令`/example param1=value1 param2=value2`,对应的正则表达式可以是:
|
||||
|
||||
```python
|
||||
class SimpleCommand(BaseCommand):
|
||||
# 匹配 /ping
|
||||
command_pattern = r"^/ping$"
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
await self.send_text("Pong!")
|
||||
return True, "发送了Pong回复"
|
||||
class ExampleCommand(BaseCommand):
|
||||
command_name = "example"
|
||||
command_description = "这是一个示例命令"
|
||||
command_pattern = r"/example (?P<param1>\w+) (?P<param2>\w+)"
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str], bool]:
|
||||
# 获取匹配的参数
|
||||
param1 = self.matched_groups.get("param1")
|
||||
param2 = self.matched_groups.get("param2")
|
||||
|
||||
# 执行逻辑
|
||||
return True, f"参数1: {param1}, 参数2: {param2}", False
|
||||
```
|
||||
|
||||
### 参数捕获
|
||||
|
||||
使用命名组 `(?P<n>pattern)` 捕获参数:
|
||||
---
|
||||
|
||||
## Command 内置方法说明
|
||||
```python
|
||||
class UserCommand(BaseCommand):
|
||||
# 匹配 /user add 张三 或 /user del 李四
|
||||
command_pattern = r"^/user\s+(?P<action>add|del|info)\s+(?P<username>\w+)$"
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
# 通过 self.matched_groups 获取捕获的参数
|
||||
action = self.matched_groups.get("action")
|
||||
username = self.matched_groups.get("username")
|
||||
|
||||
if action == "add":
|
||||
await self.send_text(f"添加用户:{username}")
|
||||
elif action == "del":
|
||||
await self.send_text(f"删除用户:{username}")
|
||||
elif action == "info":
|
||||
await self.send_text(f"用户信息:{username}")
|
||||
|
||||
return True, f"执行了{action}操作"
|
||||
class BaseCommand:
|
||||
def get_config(self, key: str, default=None):
|
||||
"""获取插件配置值,使用嵌套键访问"""
|
||||
|
||||
async def send_text(self, content: str, reply_to: str = "") -> bool:
|
||||
"""发送回复消息"""
|
||||
|
||||
async def send_type(self, message_type: str, content: str, display_message: str = "", typing: bool = False, reply_to: str = "") -> bool:
|
||||
"""发送指定类型的回复消息到当前聊天环境"""
|
||||
|
||||
async def send_command(self, command_name: str, args: Optional[dict] = None, display_message: str = "", storage_message: bool = True) -> bool:
|
||||
"""发送命令消息"""
|
||||
|
||||
async def send_emoji(self, emoji_base64: str) -> bool:
|
||||
"""发送表情包"""
|
||||
|
||||
async def send_image(self, image_base64: str) -> bool:
|
||||
"""发送图片"""
|
||||
```
|
||||
|
||||
### 可选参数
|
||||
|
||||
```python
|
||||
class HelpCommand(BaseCommand):
|
||||
# 匹配 /help 或 /help topic
|
||||
command_pattern = r"^/help(?:\s+(?P<topic>\w+))?$"
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
topic = self.matched_groups.get("topic")
|
||||
|
||||
if topic:
|
||||
await self.send_text(f"显示{topic}的帮助")
|
||||
else:
|
||||
await self.send_text("显示总体帮助")
|
||||
|
||||
return True, "显示了帮助信息"
|
||||
```
|
||||
|
||||
## 🛑 拦截控制详解
|
||||
|
||||
### 拦截消息 (intercept_message = True)
|
||||
|
||||
```python
|
||||
class AdminCommand(BaseCommand):
|
||||
command_pattern = r"^/admin\s+.+"
|
||||
command_help = "管理员命令"
|
||||
intercept_message = True # 拦截,不继续处理
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
# 执行管理操作
|
||||
await self.send_text("执行管理命令")
|
||||
# 消息不会继续传递给其他组件
|
||||
return True, "管理命令执行完成"
|
||||
```
|
||||
|
||||
### 不拦截消息 (intercept_message = False)
|
||||
|
||||
```python
|
||||
class LogCommand(BaseCommand):
|
||||
command_pattern = r"^/log\s+.+"
|
||||
command_help = "记录日志"
|
||||
intercept_message = False # 不拦截,继续处理
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
# 记录日志但不阻止后续处理
|
||||
await self.send_text("已记录到日志")
|
||||
# 消息会继续传递,可能触发Action等其他组件
|
||||
return True, "日志记录完成"
|
||||
```
|
||||
|
||||
### 拦截控制的用途
|
||||
|
||||
| 场景 | intercept_message | 说明 |
|
||||
| -------- | ----------------- | -------------------------- |
|
||||
| 系统命令 | True | 防止命令被当作普通消息处理 |
|
||||
| 查询命令 | True | 直接返回结果,无需后续处理 |
|
||||
| 日志命令 | False | 记录但允许消息继续流转 |
|
||||
| 监控命令 | False | 监控但不影响正常聊天 |
|
||||
|
||||
## 🎨 完整Command示例
|
||||
|
||||
### 用户管理Command
|
||||
|
||||
```python
|
||||
from src.plugin_system import BaseCommand
|
||||
from typing import Tuple, Optional
|
||||
|
||||
class UserManagementCommand(BaseCommand):
|
||||
"""用户管理Command - 展示复杂参数处理"""
|
||||
|
||||
command_pattern = r"^/user\s+(?P<action>add|del|list|info)\s*(?P<username>\w+)?(?:\s+--(?P<options>.+))?$"
|
||||
command_help = "用户管理命令,支持添加、删除、列表、信息查询"
|
||||
command_examples = [
|
||||
"/user add 张三",
|
||||
"/user del 李四",
|
||||
"/user list",
|
||||
"/user info 王五",
|
||||
"/user add 赵六 --role=admin"
|
||||
]
|
||||
intercept_message = True
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
"""执行用户管理命令"""
|
||||
try:
|
||||
action = self.matched_groups.get("action")
|
||||
username = self.matched_groups.get("username")
|
||||
options = self.matched_groups.get("options")
|
||||
|
||||
# 解析选项
|
||||
parsed_options = self._parse_options(options) if options else {}
|
||||
|
||||
if action == "add":
|
||||
return await self._add_user(username, parsed_options)
|
||||
elif action == "del":
|
||||
return await self._delete_user(username)
|
||||
elif action == "list":
|
||||
return await self._list_users()
|
||||
elif action == "info":
|
||||
return await self._show_user_info(username)
|
||||
else:
|
||||
await self.send_text("❌ 不支持的操作")
|
||||
return False, f"不支持的操作: {action}"
|
||||
|
||||
except Exception as e:
|
||||
await self.send_text(f"❌ 命令执行失败: {str(e)}")
|
||||
return False, f"执行失败: {e}"
|
||||
|
||||
def _parse_options(self, options_str: str) -> dict:
|
||||
"""解析命令选项"""
|
||||
options = {}
|
||||
if options_str:
|
||||
for opt in options_str.split():
|
||||
if "=" in opt:
|
||||
key, value = opt.split("=", 1)
|
||||
options[key] = value
|
||||
return options
|
||||
|
||||
async def _add_user(self, username: str, options: dict) -> Tuple[bool, str]:
|
||||
"""添加用户"""
|
||||
if not username:
|
||||
await self.send_text("❌ 请指定用户名")
|
||||
return False, "缺少用户名参数"
|
||||
|
||||
# 检查用户是否已存在
|
||||
existing_users = await self._get_user_list()
|
||||
if username in existing_users:
|
||||
await self.send_text(f"❌ 用户 {username} 已存在")
|
||||
return False, f"用户已存在: {username}"
|
||||
|
||||
# 添加用户逻辑
|
||||
role = options.get("role", "user")
|
||||
await self.send_text(f"✅ 成功添加用户 {username},角色: {role}")
|
||||
return True, f"添加用户成功: {username}"
|
||||
|
||||
async def _delete_user(self, username: str) -> Tuple[bool, str]:
|
||||
"""删除用户"""
|
||||
if not username:
|
||||
await self.send_text("❌ 请指定用户名")
|
||||
return False, "缺少用户名参数"
|
||||
|
||||
await self.send_text(f"✅ 用户 {username} 已删除")
|
||||
return True, f"删除用户成功: {username}"
|
||||
|
||||
async def _list_users(self) -> Tuple[bool, str]:
|
||||
"""列出所有用户"""
|
||||
users = await self._get_user_list()
|
||||
if users:
|
||||
user_list = "\n".join([f"• {user}" for user in users])
|
||||
await self.send_text(f"📋 用户列表:\n{user_list}")
|
||||
else:
|
||||
await self.send_text("📋 暂无用户")
|
||||
return True, "显示用户列表"
|
||||
|
||||
async def _show_user_info(self, username: str) -> Tuple[bool, str]:
|
||||
"""显示用户信息"""
|
||||
if not username:
|
||||
await self.send_text("❌ 请指定用户名")
|
||||
return False, "缺少用户名参数"
|
||||
|
||||
# 模拟用户信息
|
||||
user_info = f"""
|
||||
👤 用户信息: {username}
|
||||
📧 邮箱: {username}@example.com
|
||||
🕒 注册时间: 2024-01-01
|
||||
🎯 角色: 普通用户
|
||||
""".strip()
|
||||
|
||||
await self.send_text(user_info)
|
||||
return True, f"显示用户信息: {username}"
|
||||
|
||||
async def _get_user_list(self) -> list:
|
||||
"""获取用户列表(示例)"""
|
||||
return ["张三", "李四", "王五"]
|
||||
```
|
||||
|
||||
### 系统信息Command
|
||||
|
||||
```python
|
||||
class SystemInfoCommand(BaseCommand):
|
||||
"""系统信息Command - 展示系统查询功能"""
|
||||
|
||||
command_pattern = r"^/(?:status|info)(?:\s+(?P<type>system|memory|plugins|all))?$"
|
||||
command_help = "查询系统状态信息"
|
||||
command_examples = [
|
||||
"/status",
|
||||
"/info system",
|
||||
"/status memory",
|
||||
"/info plugins"
|
||||
]
|
||||
intercept_message = True
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
"""执行系统信息查询"""
|
||||
info_type = self.matched_groups.get("type", "all")
|
||||
|
||||
try:
|
||||
if info_type in ["system", "all"]:
|
||||
await self._show_system_info()
|
||||
|
||||
if info_type in ["memory", "all"]:
|
||||
await self._show_memory_info()
|
||||
|
||||
if info_type in ["plugins", "all"]:
|
||||
await self._show_plugin_info()
|
||||
|
||||
return True, f"显示了{info_type}类型的系统信息"
|
||||
|
||||
except Exception as e:
|
||||
await self.send_text(f"❌ 获取系统信息失败: {str(e)}")
|
||||
return False, f"查询失败: {e}"
|
||||
|
||||
async def _show_system_info(self):
|
||||
"""显示系统信息"""
|
||||
import platform
|
||||
import datetime
|
||||
|
||||
system_info = f"""
|
||||
🖥️ **系统信息**
|
||||
📱 平台: {platform.system()} {platform.release()}
|
||||
🐍 Python: {platform.python_version()}
|
||||
⏰ 运行时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
""".strip()
|
||||
|
||||
await self.send_text(system_info)
|
||||
|
||||
async def _show_memory_info(self):
|
||||
"""显示内存信息"""
|
||||
import psutil
|
||||
|
||||
memory = psutil.virtual_memory()
|
||||
memory_info = f"""
|
||||
💾 **内存信息**
|
||||
📊 总内存: {memory.total // (1024**3)} GB
|
||||
🟢 可用内存: {memory.available // (1024**3)} GB
|
||||
📈 使用率: {memory.percent}%
|
||||
""".strip()
|
||||
|
||||
await self.send_text(memory_info)
|
||||
|
||||
async def _show_plugin_info(self):
|
||||
"""显示插件信息"""
|
||||
# 通过配置获取插件信息
|
||||
plugins = await self._get_loaded_plugins()
|
||||
|
||||
plugin_info = f"""
|
||||
🔌 **插件信息**
|
||||
📦 已加载插件: {len(plugins)}
|
||||
🔧 活跃插件: {len([p for p in plugins if p.get('active', False)])}
|
||||
""".strip()
|
||||
|
||||
await self.send_text(plugin_info)
|
||||
|
||||
async def _get_loaded_plugins(self) -> list:
|
||||
"""获取已加载的插件列表"""
|
||||
# 这里可以通过配置或API获取实际的插件信息
|
||||
return [
|
||||
{"name": "core_actions", "active": True},
|
||||
{"name": "example_plugin", "active": True},
|
||||
]
|
||||
```
|
||||
|
||||
### 自定义前缀Command
|
||||
|
||||
```python
|
||||
class CustomPrefixCommand(BaseCommand):
|
||||
"""自定义前缀Command - 展示非/前缀的命令"""
|
||||
|
||||
# 使用!前缀而不是/前缀
|
||||
command_pattern = r"^[!!](?P<command>roll|dice)\s*(?P<count>\d+)?$"
|
||||
command_help = "骰子命令,使用!前缀"
|
||||
command_examples = ["!roll", "!dice 6", "!roll 20"]
|
||||
intercept_message = True
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
"""执行骰子命令"""
|
||||
import random
|
||||
|
||||
command = self.matched_groups.get("command")
|
||||
count = int(self.matched_groups.get("count", "6"))
|
||||
|
||||
# 限制骰子面数
|
||||
if count > 100:
|
||||
await self.send_text("❌ 骰子面数不能超过100")
|
||||
return False, "骰子面数超限"
|
||||
|
||||
result = random.randint(1, count)
|
||||
await self.send_text(f"🎲 投掷{count}面骰子,结果: {result}")
|
||||
|
||||
return True, f"投掷了{count}面骰子,结果{result}"
|
||||
```
|
||||
|
||||
## 📊 性能优化建议
|
||||
|
||||
### 1. 正则表达式优化
|
||||
|
||||
```python
|
||||
# ✅ 好的做法 - 简单直接
|
||||
command_pattern = r"^/ping$"
|
||||
|
||||
# ❌ 避免 - 过于复杂
|
||||
command_pattern = r"^/(?:ping|pong|test|check|status|info|help|...)"
|
||||
|
||||
# ✅ 好的做法 - 分离复杂逻辑
|
||||
```
|
||||
|
||||
### 2. 参数验证
|
||||
|
||||
```python
|
||||
# ✅ 好的做法 - 早期验证
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
username = self.matched_groups.get("username")
|
||||
if not username:
|
||||
await self.send_text("❌ 请提供用户名")
|
||||
return False, "缺少参数"
|
||||
|
||||
# 继续处理...
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```python
|
||||
# ✅ 好的做法 - 完整错误处理
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
try:
|
||||
# 主要逻辑
|
||||
result = await self._process_command()
|
||||
return True, "执行成功"
|
||||
except ValueError as e:
|
||||
await self.send_text(f"❌ 参数错误: {e}")
|
||||
return False, f"参数错误: {e}"
|
||||
except Exception as e:
|
||||
await self.send_text(f"❌ 执行失败: {e}")
|
||||
return False, f"执行失败: {e}"
|
||||
```
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 1. 命令设计原则
|
||||
|
||||
```python
|
||||
# ✅ 好的命令设计
|
||||
"/user add 张三" # 动作 + 对象 + 参数
|
||||
"/config set key=value" # 动作 + 子动作 + 参数
|
||||
"/help command" # 动作 + 可选参数
|
||||
|
||||
# ❌ 避免的设计
|
||||
"/add_user_with_name_张三" # 过于冗长
|
||||
"/u a 张三" # 过于简写
|
||||
```
|
||||
|
||||
### 2. 帮助信息
|
||||
|
||||
```python
|
||||
class WellDocumentedCommand(BaseCommand):
|
||||
command_pattern = r"^/example\s+(?P<param>\w+)$"
|
||||
command_help = "示例命令:处理指定参数并返回结果"
|
||||
command_examples = [
|
||||
"/example test",
|
||||
"/example debug",
|
||||
"/example production"
|
||||
]
|
||||
```
|
||||
|
||||
### 3. 错误处理
|
||||
|
||||
```python
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
param = self.matched_groups.get("param")
|
||||
|
||||
# 参数验证
|
||||
if param not in ["test", "debug", "production"]:
|
||||
await self.send_text("❌ 无效的参数,支持: test, debug, production")
|
||||
return False, "无效参数"
|
||||
|
||||
# 执行逻辑
|
||||
try:
|
||||
result = await self._process_param(param)
|
||||
await self.send_text(f"✅ 处理完成: {result}")
|
||||
return True, f"处理{param}成功"
|
||||
except Exception as e:
|
||||
await self.send_text("❌ 处理失败,请稍后重试")
|
||||
return False, f"处理失败: {e}"
|
||||
```
|
||||
|
||||
### 4. 配置集成
|
||||
|
||||
```python
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
# 从配置读取设置
|
||||
max_items = self.get_config("command.max_items", 10)
|
||||
timeout = self.get_config("command.timeout", 30)
|
||||
|
||||
# 使用配置进行处理
|
||||
...
|
||||
```
|
||||
|
||||
## 📝 Command vs Action 选择指南
|
||||
|
||||
### 使用Command的场景
|
||||
|
||||
- ✅ 用户需要明确调用特定功能
|
||||
- ✅ 需要精确的参数控制
|
||||
- ✅ 管理和配置操作
|
||||
- ✅ 查询和信息显示
|
||||
- ✅ 系统维护命令
|
||||
|
||||
### 使用Action的场景
|
||||
|
||||
- ✅ 增强麦麦的智能行为
|
||||
- ✅ 根据上下文自动触发
|
||||
- ✅ 情绪和表情表达
|
||||
- ✅ 智能建议和帮助
|
||||
- ✅ 随机化的互动
|
||||
|
||||
|
||||
具体参数与用法参见`BaseCommand`基类的定义。
|
||||
@@ -147,7 +147,7 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin
|
||||
## 📋 字段说明
|
||||
|
||||
### 基本信息
|
||||
- `manifest_version`: manifest格式版本,当前为3
|
||||
- `manifest_version`: manifest格式版本,当前为1
|
||||
- `name`: 插件显示名称(必需)
|
||||
- `version`: 插件版本号(必需)
|
||||
- `description`: 插件功能描述(必需)
|
||||
@@ -165,10 +165,12 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin
|
||||
- `categories`: 分类数组(可选,建议填写)
|
||||
|
||||
### 兼容性
|
||||
- `host_application`: 主机应用兼容性(可选)
|
||||
- `host_application`: 主机应用兼容性(可选,建议填写)
|
||||
- `min_version`: 最低兼容版本
|
||||
- `max_version`: 最高兼容版本
|
||||
|
||||
⚠️ 在不填写的情况下,插件将默认支持所有版本。**(由于我们在不同版本对插件系统进行了大量的重构,这种情况几乎不可能。)**
|
||||
|
||||
### 国际化
|
||||
- `default_locale`: 默认语言(可选)
|
||||
- `locales_path`: 语言文件目录(可选)
|
||||
@@ -185,24 +187,13 @@ python scripts/manifest_tool.py validate src/plugins/my_plugin
|
||||
2. **编码格式**:manifest文件必须使用UTF-8编码
|
||||
3. **JSON格式**:文件必须是有效的JSON格式
|
||||
4. **必需字段**:`manifest_version`、`name`、`version`、`description`、`author.name`是必需的
|
||||
5. **版本兼容**:当前只支持manifest_version = 3
|
||||
5. **版本兼容**:当前只支持`manifest_version = 1`
|
||||
|
||||
## 🔍 常见问题
|
||||
|
||||
### Q: 为什么要强制要求manifest文件?
|
||||
A: Manifest文件提供了插件的标准化元数据,使得插件管理、依赖检查、版本兼容性验证等功能成为可能。
|
||||
|
||||
### Q: 可以不填写可选字段吗?
|
||||
A: 可以。所有标记为"可选"的字段都可以不填写,但建议至少填写`license`和`keywords`。
|
||||
|
||||
### Q: 如何快速为所有插件创建manifest?
|
||||
A: 可以编写脚本批量处理:
|
||||
```bash
|
||||
# 扫描并为每个缺少manifest的插件创建最小化manifest
|
||||
python scripts/manifest_tool.py scan src/plugins
|
||||
# 然后手动为每个插件运行create-minimal命令
|
||||
```
|
||||
|
||||
### Q: manifest验证失败怎么办?
|
||||
A: 根据验证器的错误提示修复相应问题。错误会导致插件加载失败,警告不会。
|
||||
|
||||
@@ -210,5 +201,5 @@ A: 根据验证器的错误提示修复相应问题。错误会导致插件加
|
||||
|
||||
查看内置插件的manifest文件作为参考:
|
||||
- `src/plugins/built_in/core_actions/_manifest.json`
|
||||
- `src/plugins/built_in/doubao_pic_plugin/_manifest.json`
|
||||
- `src/plugins/built_in/tts_plugin/_manifest.json`
|
||||
- `src/plugins/hello_world_plugin/_manifest.json`
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
## 📖 什么是工具系统
|
||||
|
||||
工具系统是MaiBot的信息获取能力扩展组件,**专门用于在Focus模式下扩宽麦麦能够获得的信息量**。如果说Action组件功能五花八门,可以拓展麦麦能做的事情,那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。
|
||||
工具系统是MaiBot的信息获取能力扩展组件。如果说Action组件功能五花八门,可以拓展麦麦能做的事情,那么Tool就是在某个过程中拓宽了麦麦能够获得的信息量。
|
||||
|
||||
### 🎯 工具系统的特点
|
||||
|
||||
- 🔍 **信息获取增强**:扩展麦麦获取外部信息的能力
|
||||
- 🎯 **Focus模式专用**:仅在专注聊天模式下工作,必须开启工具处理器
|
||||
- 📊 **数据丰富**:帮助麦麦获得更多背景信息和实时数据
|
||||
- 🔌 **插件式架构**:支持独立开发和注册新工具
|
||||
- ⚡ **自动发现**:工具会被系统自动识别和注册
|
||||
@@ -17,7 +16,6 @@
|
||||
| 特征 | Action | Command | Tool |
|
||||
|-----|-------|---------|------|
|
||||
| **主要用途** | 扩展麦麦行为能力 | 响应用户指令 | 扩展麦麦信息获取 |
|
||||
| **适用模式** | 所有模式 | 所有模式 | 仅Focus模式 |
|
||||
| **触发方式** | 麦麦智能决策 | 用户主动触发 | LLM根据需要调用 |
|
||||
| **目标** | 让麦麦做更多事情 | 提供具体功能 | 让麦麦知道更多信息 |
|
||||
| **使用场景** | 增强交互体验 | 功能服务 | 信息查询和分析 |
|
||||
@@ -54,7 +52,7 @@ class MyTool(BaseTool):
|
||||
"required": ["query"]
|
||||
}
|
||||
|
||||
async def execute(self, function_args, message_txt=""):
|
||||
async def execute(self, function_args: Dict[str, Any]):
|
||||
"""执行工具逻辑"""
|
||||
# 实现工具功能
|
||||
result = f"查询结果: {function_args.get('query')}"
|
||||
@@ -63,9 +61,6 @@ class MyTool(BaseTool):
|
||||
"name": self.name,
|
||||
"content": result
|
||||
}
|
||||
|
||||
# 注册工具
|
||||
register_tool(MyTool)
|
||||
```
|
||||
|
||||
### 属性说明
|
||||
@@ -80,7 +75,7 @@ register_tool(MyTool)
|
||||
|
||||
| 方法 | 参数 | 返回值 | 说明 |
|
||||
|-----|------|--------|------|
|
||||
| `execute` | `function_args`, `message_txt` | `dict` | 执行工具核心逻辑 |
|
||||
| `execute` | `function_args` | `dict` | 执行工具核心逻辑 |
|
||||
|
||||
## 🔄 自动注册机制
|
||||
|
||||
@@ -88,28 +83,14 @@ register_tool(MyTool)
|
||||
|
||||
1. **文件扫描**:系统自动遍历 `tool_can_use` 目录中的所有Python文件
|
||||
2. **类识别**:寻找继承自 `BaseTool` 的工具类
|
||||
3. **自动注册**:调用 `register_tool()` 的工具会被注册到系统中
|
||||
3. **自动注册**:只需要实现对应的类并把文件放在正确文件夹中就可自动注册
|
||||
4. **即用即加载**:工具在需要时被实例化和调用
|
||||
|
||||
### 注册流程
|
||||
|
||||
```python
|
||||
# 1. 创建工具类
|
||||
class WeatherTool(BaseTool):
|
||||
name = "weather_query"
|
||||
description = "查询指定城市的天气信息"
|
||||
# ...
|
||||
|
||||
# 2. 注册工具(在文件末尾)
|
||||
register_tool(WeatherTool)
|
||||
|
||||
# 3. 系统自动发现(无需手动操作)
|
||||
# discover_tools() 函数会自动完成注册
|
||||
```
|
||||
---
|
||||
|
||||
## 🎨 完整工具示例
|
||||
|
||||
### 天气查询工具
|
||||
完成一个天气查询工具
|
||||
|
||||
```python
|
||||
from src.tools.tool_can_use.base_tool import BaseTool, register_tool
|
||||
@@ -192,102 +173,9 @@ class WeatherTool(BaseTool):
|
||||
💧 湿度: {humidity}%
|
||||
━━━━━━━━━━━━━━━━━━
|
||||
""".strip()
|
||||
|
||||
# 注册工具
|
||||
register_tool(WeatherTool)
|
||||
```
|
||||
|
||||
### 知识查询工具
|
||||
|
||||
```python
|
||||
from src.tools.tool_can_use.base_tool import BaseTool, register_tool
|
||||
|
||||
class KnowledgeSearchTool(BaseTool):
|
||||
"""知识搜索工具 - 查询百科知识和专业信息"""
|
||||
|
||||
name = "knowledge_search"
|
||||
description = "搜索百科知识、专业术语解释、历史事件等信息"
|
||||
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "要搜索的知识关键词或问题"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "知识分类:science(科学)、history(历史)、technology(技术)、general(通用)等",
|
||||
"enum": ["science", "history", "technology", "general"]
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "结果语言:zh(中文)、en(英文)",
|
||||
"enum": ["zh", "en"]
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
|
||||
async def execute(self, function_args, message_txt=""):
|
||||
"""执行知识搜索"""
|
||||
try:
|
||||
query = function_args.get("query")
|
||||
category = function_args.get("category", "general")
|
||||
language = function_args.get("language", "zh")
|
||||
|
||||
# 执行搜索逻辑
|
||||
search_results = await self._search_knowledge(query, category, language)
|
||||
|
||||
# 格式化结果
|
||||
result = self._format_search_results(query, search_results)
|
||||
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": result
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"name": self.name,
|
||||
"content": f"知识搜索失败: {str(e)}"
|
||||
}
|
||||
|
||||
async def _search_knowledge(self, query: str, category: str, language: str) -> list:
|
||||
"""执行知识搜索"""
|
||||
# 这里实现实际的搜索逻辑
|
||||
# 可以对接维基百科API、百度百科API等
|
||||
|
||||
# 示例返回数据
|
||||
return [
|
||||
{
|
||||
"title": f"{query}的定义",
|
||||
"summary": f"关于{query}的详细解释...",
|
||||
"source": "Wikipedia"
|
||||
}
|
||||
]
|
||||
|
||||
def _format_search_results(self, query: str, results: list) -> str:
|
||||
"""格式化搜索结果"""
|
||||
if not results:
|
||||
return f"未找到关于 '{query}' 的相关信息"
|
||||
|
||||
formatted_text = f"📚 关于 '{query}' 的搜索结果:\n\n"
|
||||
|
||||
for i, result in enumerate(results[:3], 1): # 限制显示前3条
|
||||
title = result.get("title", "无标题")
|
||||
summary = result.get("summary", "无摘要")
|
||||
source = result.get("source", "未知来源")
|
||||
|
||||
formatted_text += f"{i}. **{title}**\n"
|
||||
formatted_text += f" {summary}\n"
|
||||
formatted_text += f" 📖 来源: {source}\n\n"
|
||||
|
||||
return formatted_text.strip()
|
||||
|
||||
# 注册工具
|
||||
register_tool(KnowledgeSearchTool)
|
||||
```
|
||||
---
|
||||
|
||||
## 📊 工具开发步骤
|
||||
|
||||
@@ -323,86 +211,21 @@ class MyNewTool(BaseTool):
|
||||
"name": self.name,
|
||||
"content": "执行结果"
|
||||
}
|
||||
|
||||
register_tool(MyNewTool)
|
||||
```
|
||||
|
||||
### 3. 测试工具
|
||||
|
||||
创建测试文件验证工具功能:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from my_new_tool import MyNewTool
|
||||
|
||||
async def test_tool():
|
||||
tool = MyNewTool()
|
||||
result = await tool.execute({"param": "value"})
|
||||
print(result)
|
||||
|
||||
asyncio.run(test_tool())
|
||||
```
|
||||
|
||||
### 4. 系统集成
|
||||
### 3. 系统集成
|
||||
|
||||
工具创建完成后,系统会自动发现和注册,无需额外配置。
|
||||
|
||||
## ⚙️ 工具处理器配置
|
||||
|
||||
### 启用工具处理器
|
||||
|
||||
工具系统仅在Focus模式下工作,需要确保工具处理器已启用:
|
||||
|
||||
```python
|
||||
# 在Focus模式配置中
|
||||
focus_config = {
|
||||
"enable_tool_processor": True, # 必须启用
|
||||
"tool_timeout": 30, # 工具执行超时时间(秒)
|
||||
"max_tools_per_message": 3 # 单次消息最大工具调用数
|
||||
}
|
||||
```
|
||||
|
||||
### 工具使用流程
|
||||
|
||||
1. **用户发送消息**:在Focus模式下发送需要信息查询的消息
|
||||
2. **LLM判断需求**:麦麦分析消息,判断是否需要使用工具获取信息
|
||||
3. **选择工具**:根据需求选择合适的工具
|
||||
4. **调用工具**:执行工具获取信息
|
||||
5. **整合回复**:将工具获取的信息整合到回复中
|
||||
|
||||
### 使用示例
|
||||
|
||||
```python
|
||||
# 用户消息示例
|
||||
"今天北京的天气怎么样?"
|
||||
|
||||
# 系统处理流程:
|
||||
# 1. 麦麦识别这是天气查询需求
|
||||
# 2. 调用 weather_query 工具
|
||||
# 3. 获取北京天气信息
|
||||
# 4. 整合信息生成回复
|
||||
|
||||
# 最终回复:
|
||||
"根据最新天气数据,北京今天晴天,温度22°C,湿度45%,适合外出活动。"
|
||||
```
|
||||
---
|
||||
|
||||
## 🚨 注意事项和限制
|
||||
|
||||
### 当前限制
|
||||
|
||||
1. **模式限制**:仅在Focus模式下可用
|
||||
2. **独立开发**:需要单独编写,暂未完全融入插件系统
|
||||
3. **适用范围**:主要适用于信息获取场景
|
||||
4. **配置要求**:必须开启工具处理器
|
||||
|
||||
### 未来改进
|
||||
|
||||
工具系统在之后可能会面临以下修改:
|
||||
|
||||
1. **插件系统融合**:更好地集成到插件系统中
|
||||
2. **模式扩展**:可能扩展到其他聊天模式
|
||||
3. **配置简化**:简化配置和部署流程
|
||||
4. **性能优化**:提升工具调用效率
|
||||
1. **独立开发**:需要单独编写,暂未完全融入插件系统
|
||||
2. **适用范围**:主要适用于信息获取场景
|
||||
3. **配置要求**:必须开启工具处理器
|
||||
|
||||
### 开发建议
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ class HelloAction(BaseAction):
|
||||
# === 基本信息(必须填写)===
|
||||
action_name = "hello_greeting"
|
||||
action_description = "向用户发送问候消息"
|
||||
activation_type = ActionActivationType.ALWAYS # 始终激活
|
||||
|
||||
# === 功能描述(必须填写)===
|
||||
action_parameters = {"greeting_message": "要发送的问候消息"}
|
||||
@@ -44,8 +45,7 @@ class ByeAction(BaseAction):
|
||||
action_description = "向用户发送告别消息"
|
||||
|
||||
# 使用关键词激活
|
||||
focus_activation_type = ActionActivationType.KEYWORD
|
||||
normal_activation_type = ActionActivationType.KEYWORD
|
||||
activation_type = ActionActivationType.KEYWORD
|
||||
|
||||
# 关键词设置
|
||||
activation_keywords = ["再见", "bye", "88", "拜拜"]
|
||||
@@ -75,11 +75,8 @@ class TimeCommand(BaseCommand):
|
||||
|
||||
# === 命令设置(必须填写)===
|
||||
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
|
||||
command_help = "查询当前时间"
|
||||
command_examples = ["/time"]
|
||||
intercept_message = True # 拦截消息,不让其他组件处理
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
async def execute(self) -> Tuple[bool, str, bool]:
|
||||
"""执行时间查询"""
|
||||
import datetime
|
||||
|
||||
@@ -92,7 +89,7 @@ class TimeCommand(BaseCommand):
|
||||
message = f"⏰ 当前时间:{time_str}"
|
||||
await self.send_text(message)
|
||||
|
||||
return True, f"显示了当前时间: {time_str}"
|
||||
return True, f"显示了当前时间: {time_str}", True
|
||||
|
||||
|
||||
class PrintMessage(BaseEventHandler):
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"manifest_version": 1,
|
||||
"name": "AI拍照插件 (Take Picture Plugin)",
|
||||
"version": "1.0.0",
|
||||
"description": "基于AI图像生成的拍照插件,可以生成逼真的自拍照片,支持照片存储和展示功能。",
|
||||
"author": {
|
||||
"name": "SengokuCola",
|
||||
"url": "https://github.com/SengokuCola"
|
||||
},
|
||||
"license": "GPL-v3.0-or-later",
|
||||
|
||||
"host_application": {
|
||||
"min_version": "0.9.0"
|
||||
},
|
||||
"homepage_url": "https://github.com/MaiM-with-u/maibot",
|
||||
"repository_url": "https://github.com/MaiM-with-u/maibot",
|
||||
"keywords": ["camera", "photo", "selfie", "ai", "image", "generation"],
|
||||
"categories": ["AI Tools", "Image Processing", "Entertainment"],
|
||||
|
||||
"default_locale": "zh-CN",
|
||||
"locales_path": "_locales",
|
||||
|
||||
"plugin_info": {
|
||||
"is_built_in": false,
|
||||
"plugin_type": "image_generator",
|
||||
"api_dependencies": ["volcengine"],
|
||||
"components": [
|
||||
{
|
||||
"type": "action",
|
||||
"name": "take_picture",
|
||||
"description": "生成一张用手机拍摄的照片,比如自拍或者近照",
|
||||
"activation_modes": ["keyword"],
|
||||
"keywords": ["拍张照", "自拍", "发张照片", "看看你", "你的照片"]
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"name": "show_recent_pictures",
|
||||
"description": "展示最近生成的5张照片",
|
||||
"pattern": "/show_pics"
|
||||
}
|
||||
],
|
||||
"features": [
|
||||
"AI驱动的自拍照生成",
|
||||
"个性化照片风格",
|
||||
"照片历史记录",
|
||||
"缓存机制优化",
|
||||
"火山引擎API集成"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,517 +0,0 @@
|
||||
"""
|
||||
拍照插件
|
||||
|
||||
功能特性:
|
||||
- Action: 生成一张自拍照,prompt由人设和模板生成
|
||||
- Command: 展示最近生成的照片
|
||||
|
||||
#此插件并不完善
|
||||
#此插件并不完善
|
||||
|
||||
#此插件并不完善
|
||||
|
||||
#此插件并不完善
|
||||
|
||||
#此插件并不完善
|
||||
|
||||
#此插件并不完善
|
||||
|
||||
#此插件并不完善
|
||||
|
||||
|
||||
|
||||
包含组件:
|
||||
- 拍照Action - 生成自拍照
|
||||
- 展示照片Command - 展示最近生成的照片
|
||||
"""
|
||||
|
||||
from typing import List, Tuple, Type, Optional
|
||||
import random
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import base64
|
||||
import traceback
|
||||
|
||||
from src.plugin_system.base.base_plugin import BasePlugin
|
||||
from src.plugin_system.base.base_action import BaseAction
|
||||
from src.plugin_system.base.base_command import BaseCommand
|
||||
from src.plugin_system.base.component_types import ComponentInfo, ActionActivationType, ChatMode
|
||||
from src.plugin_system.base.config_types import ConfigField
|
||||
from src.plugin_system import register_plugin
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("take_picture_plugin")
|
||||
|
||||
# 定义数据目录常量
|
||||
DATA_DIR = os.path.join("data", "take_picture_data")
|
||||
# 确保数据目录存在
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
# 创建全局锁
|
||||
file_lock = asyncio.Lock()
|
||||
|
||||
|
||||
class TakePictureAction(BaseAction):
|
||||
"""生成一张自拍照"""
|
||||
|
||||
focus_activation_type = ActionActivationType.KEYWORD
|
||||
normal_activation_type = ActionActivationType.KEYWORD
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = False
|
||||
|
||||
action_name = "take_picture"
|
||||
action_description = "生成一张用手机拍摄,比如自拍或者近照"
|
||||
activation_keywords = ["拍张照", "自拍", "发张照片", "看看你", "你的照片"]
|
||||
keyword_case_sensitive = False
|
||||
|
||||
action_parameters = {}
|
||||
|
||||
action_require = ["当用户想看你的照片时使用", "当用户让你发自拍时使用当想随手拍眼前的场景时使用"]
|
||||
|
||||
associated_types = ["text", "image"]
|
||||
|
||||
# 内置的Prompt模板,如果配置文件中没有定义,将使用这些模板
|
||||
DEFAULT_PROMPT_TEMPLATES = [
|
||||
"极其频繁无奇的iPhone自拍照,没有明确的主体或构图感,就是随手一拍的快照照片略带运动模糊,阳光或室内打光不均匀导致的轻微曝光过度,整体呈现出一种刻意的平庸感,就像是从口袋里拿手机时不小心拍到的一张自拍。主角是{name},{personality}"
|
||||
]
|
||||
|
||||
# 简单的请求缓存,避免短时间内重复请求
|
||||
_request_cache = {}
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
logger.info(f"{self.log_prefix} 执行拍照动作")
|
||||
|
||||
try:
|
||||
# 配置验证
|
||||
http_base_url = self.api.get_config("api.base_url")
|
||||
http_api_key = self.api.get_config("api.volcano_generate_api_key")
|
||||
|
||||
if not (http_base_url and http_api_key):
|
||||
error_msg = "抱歉,照片生成功能所需的API配置(如API地址或密钥)不完整,无法提供服务。"
|
||||
await self.send_text(error_msg)
|
||||
logger.error(f"{self.log_prefix} HTTP调用配置缺失: base_url 或 volcano_generate_api_key.")
|
||||
return False, "API配置不完整"
|
||||
|
||||
# API密钥验证
|
||||
if http_api_key == "YOUR_DOUBAO_API_KEY_HERE":
|
||||
error_msg = "照片生成功能尚未配置,请设置正确的API密钥。"
|
||||
await self.send_text(error_msg)
|
||||
logger.error(f"{self.log_prefix} API密钥未配置")
|
||||
return False, "API密钥未配置"
|
||||
|
||||
# 获取全局配置信息
|
||||
bot_nickname = self.api.get_global_config("bot.nickname", "麦麦")
|
||||
bot_personality = self.api.get_global_config("personality.personality_core", "")
|
||||
|
||||
personality_side = self.api.get_global_config("personality.personality_side", [])
|
||||
if personality_side:
|
||||
bot_personality += random.choice(personality_side)
|
||||
|
||||
# 准备模板变量
|
||||
template_vars = {"name": bot_nickname, "personality": bot_personality}
|
||||
|
||||
logger.info(f"{self.log_prefix} 使用的全局配置: name={bot_nickname}, personality={bot_personality}")
|
||||
|
||||
# 尝试从配置文件获取模板,如果没有则使用默认模板
|
||||
templates = self.api.get_config("picture.prompt_templates", self.DEFAULT_PROMPT_TEMPLATES)
|
||||
if not templates:
|
||||
logger.warning(f"{self.log_prefix} 未找到有效的提示词模板,使用默认模板")
|
||||
templates = self.DEFAULT_PROMPT_TEMPLATES
|
||||
|
||||
prompt_template = random.choice(templates)
|
||||
|
||||
# 填充模板
|
||||
final_prompt = prompt_template.format(**template_vars)
|
||||
|
||||
logger.info(f"{self.log_prefix} 生成的最终Prompt: {final_prompt}")
|
||||
|
||||
# 从配置获取参数
|
||||
model = self.api.get_config("picture.default_model", "doubao-seedream-3-0-t2i-250415")
|
||||
size = self.api.get_config("picture.default_size", "1024x1024")
|
||||
watermark = self.api.get_config("picture.default_watermark", True)
|
||||
guidance_scale = self.api.get_config("picture.default_guidance_scale", 2.5)
|
||||
seed = self.api.get_config("picture.default_seed", 42)
|
||||
|
||||
# 检查缓存
|
||||
enable_cache = self.api.get_config("storage.enable_cache", True)
|
||||
if enable_cache:
|
||||
cache_key = self._get_cache_key(final_prompt, model, size)
|
||||
if cache_key in self._request_cache:
|
||||
cached_result = self._request_cache[cache_key]
|
||||
logger.info(f"{self.log_prefix} 使用缓存的图片结果")
|
||||
await self.send_text("我之前拍过类似的照片,用之前的结果~")
|
||||
|
||||
# 直接发送缓存的结果
|
||||
send_success = await self._send_image(cached_result)
|
||||
if send_success:
|
||||
await self.send_text("这是我的照片,好看吗?")
|
||||
return True, "照片已发送(缓存)"
|
||||
else:
|
||||
# 缓存失败,清除这个缓存项并继续正常流程
|
||||
del self._request_cache[cache_key]
|
||||
|
||||
await self.send_text("正在为你拍照,请稍候...")
|
||||
|
||||
try:
|
||||
seed = random.randint(1, 1000000)
|
||||
success, result = await asyncio.to_thread(
|
||||
self._make_http_image_request,
|
||||
prompt=final_prompt,
|
||||
model=model,
|
||||
size=size,
|
||||
seed=seed,
|
||||
guidance_scale=guidance_scale,
|
||||
watermark=watermark,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} (HTTP) 异步请求执行失败: {e!r}", exc_info=True)
|
||||
traceback.print_exc()
|
||||
success = False
|
||||
result = f"照片生成服务遇到意外问题: {str(e)[:100]}"
|
||||
|
||||
if success:
|
||||
image_url = result
|
||||
logger.info(f"{self.log_prefix} 图片URL获取成功: {image_url[:70]}... 下载并编码.")
|
||||
|
||||
try:
|
||||
encode_success, encode_result = await asyncio.to_thread(self._download_and_encode_base64, image_url)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} (B64) 异步下载/编码失败: {e!r}", exc_info=True)
|
||||
traceback.print_exc()
|
||||
encode_success = False
|
||||
encode_result = f"图片下载或编码时发生内部错误: {str(e)[:100]}"
|
||||
|
||||
if encode_success:
|
||||
base64_image_string = encode_result
|
||||
# 更新缓存
|
||||
if enable_cache:
|
||||
self._update_cache(final_prompt, model, size, base64_image_string)
|
||||
|
||||
# 发送图片
|
||||
send_success = await self._send_image(base64_image_string)
|
||||
if send_success:
|
||||
# 存储到文件
|
||||
await self._store_picture_info(final_prompt, image_url)
|
||||
logger.info(f"{self.log_prefix} 成功生成并存储照片: {image_url}")
|
||||
await self.send_text("当当当当~这是我刚拍的照片,好看吗?")
|
||||
return True, f"成功生成照片: {image_url}"
|
||||
else:
|
||||
await self.send_text("照片生成了,但发送失败了,可能是格式问题...")
|
||||
return False, "照片发送失败"
|
||||
else:
|
||||
await self.send_text(f"照片下载失败: {encode_result}")
|
||||
return False, encode_result
|
||||
else:
|
||||
await self.send_text(f"哎呀,拍照失败了: {result}")
|
||||
return False, result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 执行拍照动作失败: {e}", exc_info=True)
|
||||
traceback.print_exc()
|
||||
await self.send_text("呜呜,拍照的时候出了一点小问题...")
|
||||
return False, str(e)
|
||||
|
||||
async def _store_picture_info(self, prompt: str, image_url: str):
|
||||
"""将照片信息存入日志文件"""
|
||||
log_file = self.api.get_config("storage.log_file", "picture_log.json")
|
||||
log_path = os.path.join(DATA_DIR, log_file)
|
||||
max_photos = self.api.get_config("storage.max_photos", 50)
|
||||
|
||||
async with file_lock:
|
||||
try:
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path, "r", encoding="utf-8") as f:
|
||||
log_data = json.load(f)
|
||||
else:
|
||||
log_data = []
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
log_data = []
|
||||
|
||||
# 添加新照片
|
||||
log_data.append(
|
||||
{"prompt": prompt, "image_url": image_url, "timestamp": datetime.datetime.now().isoformat()}
|
||||
)
|
||||
|
||||
# 如果超过最大数量,删除最旧的
|
||||
if len(log_data) > max_photos:
|
||||
log_data = sorted(log_data, key=lambda x: x.get("timestamp", ""), reverse=True)[:max_photos]
|
||||
|
||||
try:
|
||||
with open(log_path, "w", encoding="utf-8") as f:
|
||||
json.dump(log_data, f, ensure_ascii=False, indent=4)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 写入照片日志文件失败: {e}", exc_info=True)
|
||||
|
||||
def _make_http_image_request(
|
||||
self, prompt: str, model: str, size: str, seed: int, guidance_scale: float, watermark: bool
|
||||
) -> Tuple[bool, str]:
|
||||
"""发送HTTP请求到火山引擎豆包API生成图片"""
|
||||
try:
|
||||
base_url = self.api.get_config("api.base_url")
|
||||
api_key = self.api.get_config("api.volcano_generate_api_key")
|
||||
|
||||
# 构建请求URL和头部
|
||||
endpoint = f"{base_url.rstrip('/')}/images/generations"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
|
||||
# 构建请求体
|
||||
request_body = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"response_format": "url",
|
||||
"size": size,
|
||||
"seed": seed,
|
||||
"guidance_scale": guidance_scale,
|
||||
"watermark": watermark,
|
||||
"api-key": api_key,
|
||||
}
|
||||
|
||||
# 创建请求对象
|
||||
req = urllib.request.Request(
|
||||
endpoint,
|
||||
data=json.dumps(request_body).encode("utf-8"),
|
||||
headers=headers,
|
||||
method="POST",
|
||||
)
|
||||
|
||||
# 发送请求并获取响应
|
||||
with urllib.request.urlopen(req, timeout=60) as response:
|
||||
response_data = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
# 解析响应
|
||||
image_url = None
|
||||
if (
|
||||
isinstance(response_data.get("data"), list)
|
||||
and response_data["data"]
|
||||
and isinstance(response_data["data"][0], dict)
|
||||
):
|
||||
image_url = response_data["data"][0].get("url")
|
||||
elif response_data.get("url"):
|
||||
image_url = response_data.get("url")
|
||||
|
||||
if image_url:
|
||||
return True, image_url
|
||||
else:
|
||||
error_msg = response_data.get("error", {}).get("message", "未知错误")
|
||||
logger.error(f"API返回错误: {error_msg}")
|
||||
return False, f"API错误: {error_msg}"
|
||||
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode("utf-8")
|
||||
logger.error(f"HTTP错误 {e.code}: {error_body}")
|
||||
return False, f"HTTP错误 {e.code}: {error_body[:100]}..."
|
||||
except Exception as e:
|
||||
logger.error(f"请求异常: {e}", exc_info=True)
|
||||
return False, f"请求异常: {str(e)}"
|
||||
|
||||
def _download_and_encode_base64(self, image_url: str) -> Tuple[bool, str]:
|
||||
"""下载图片并转换为Base64编码"""
|
||||
try:
|
||||
with urllib.request.urlopen(image_url) as response:
|
||||
image_data = response.read()
|
||||
|
||||
base64_encoded = base64.b64encode(image_data).decode("utf-8")
|
||||
return True, base64_encoded
|
||||
except Exception as e:
|
||||
logger.error(f"图片下载编码失败: {e}", exc_info=True)
|
||||
return False, str(e)
|
||||
|
||||
async def _send_image(self, base64_image: str) -> bool:
|
||||
"""发送图片"""
|
||||
try:
|
||||
# 使用聊天流信息确定发送目标
|
||||
chat_stream = self.api.get_service("chat_stream")
|
||||
if not chat_stream:
|
||||
logger.error(f"{self.log_prefix} 没有可用的聊天流发送图片")
|
||||
return False
|
||||
|
||||
if chat_stream.group_info:
|
||||
# 群聊
|
||||
return await self.api.send_message_to_target(
|
||||
message_type="image",
|
||||
content=base64_image,
|
||||
platform=chat_stream.platform,
|
||||
target_id=str(chat_stream.group_info.group_id),
|
||||
is_group=True,
|
||||
display_message="发送生成的照片",
|
||||
)
|
||||
else:
|
||||
# 私聊
|
||||
return await self.api.send_message_to_target(
|
||||
message_type="image",
|
||||
content=base64_image,
|
||||
platform=chat_stream.platform,
|
||||
target_id=str(chat_stream.user_info.user_id),
|
||||
is_group=False,
|
||||
display_message="发送生成的照片",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 发送图片时出错: {e}")
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _get_cache_key(cls, description: str, model: str, size: str) -> str:
|
||||
"""生成缓存键"""
|
||||
return f"{description}|{model}|{size}"
|
||||
|
||||
def _update_cache(self, description: str, model: str, size: str, base64_image: str):
|
||||
"""更新缓存"""
|
||||
max_cache_size = self.api.get_config("storage.max_cache_size", 10)
|
||||
cache_key = self._get_cache_key(description, model, size)
|
||||
|
||||
# 添加到缓存
|
||||
self._request_cache[cache_key] = base64_image
|
||||
|
||||
# 如果缓存超过最大大小,删除最旧的项
|
||||
if len(self._request_cache) > max_cache_size:
|
||||
oldest_key = next(iter(self._request_cache))
|
||||
del self._request_cache[oldest_key]
|
||||
|
||||
|
||||
class ShowRecentPicturesCommand(BaseCommand):
|
||||
"""展示最近生成的照片"""
|
||||
|
||||
command_name = "show_recent_pictures"
|
||||
command_description = "展示最近生成的5张照片"
|
||||
command_pattern = r"^/show_pics$"
|
||||
command_help = "用法: /show_pics"
|
||||
command_examples = ["/show_pics"]
|
||||
intercept_message = True
|
||||
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
logger.info(f"{self.log_prefix} 执行展示最近照片命令")
|
||||
log_file = self.api.get_config("storage.log_file", "picture_log.json")
|
||||
log_path = os.path.join(DATA_DIR, log_file)
|
||||
|
||||
async with file_lock:
|
||||
try:
|
||||
if not os.path.exists(log_path):
|
||||
await self.send_text("最近还没有拍过照片哦,快让我自拍一张吧!")
|
||||
return True, "没有照片日志文件"
|
||||
|
||||
with open(log_path, "r", encoding="utf-8") as f:
|
||||
log_data = json.load(f)
|
||||
|
||||
if not log_data:
|
||||
await self.send_text("最近还没有拍过照片哦,快让我自拍一张吧!")
|
||||
return True, "没有照片"
|
||||
|
||||
# 获取最新的5张照片
|
||||
recent_pics = sorted(log_data, key=lambda x: x["timestamp"], reverse=True)[:5]
|
||||
|
||||
# 先发送文本消息
|
||||
await self.send_text("这是我最近拍的几张照片~")
|
||||
|
||||
# 逐个发送图片
|
||||
for pic in recent_pics:
|
||||
# 尝试获取图片URL
|
||||
image_url = pic.get("image_url")
|
||||
if image_url:
|
||||
try:
|
||||
# 下载图片并转换为Base64
|
||||
with urllib.request.urlopen(image_url) as response:
|
||||
image_data = response.read()
|
||||
base64_encoded = base64.b64encode(image_data).decode("utf-8")
|
||||
|
||||
# 发送图片
|
||||
await self.send_type(
|
||||
message_type="image", content=base64_encoded, display_message="发送最近的照片"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 下载或发送照片失败: {e}", exc_info=True)
|
||||
|
||||
return True, "成功展示最近的照片"
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await self.send_text("照片记录文件好像损坏了...")
|
||||
return False, "JSON解码错误"
|
||||
except Exception as e:
|
||||
logger.error(f"{self.log_prefix} 展示照片失败: {e}", exc_info=True)
|
||||
await self.send_text("哎呀,查找照片的时候出错了。")
|
||||
return False, str(e)
|
||||
|
||||
|
||||
@register_plugin
|
||||
class TakePicturePlugin(BasePlugin):
|
||||
"""拍照插件"""
|
||||
|
||||
plugin_name = "take_picture_plugin" # 内部标识符
|
||||
enable_plugin = False
|
||||
dependencies = [] # 插件依赖列表
|
||||
python_dependencies = [] # Python包依赖列表
|
||||
config_file_name = "config.toml"
|
||||
|
||||
# 配置节描述
|
||||
config_section_descriptions = {
|
||||
"plugin": "插件基本信息配置",
|
||||
"api": "API相关配置,包含火山引擎API的访问信息",
|
||||
"components": "组件启用控制",
|
||||
"picture": "拍照功能核心配置",
|
||||
"storage": "照片存储相关配置",
|
||||
}
|
||||
|
||||
# 配置Schema定义
|
||||
config_schema = {
|
||||
"plugin": {
|
||||
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
|
||||
},
|
||||
"api": {
|
||||
"base_url": ConfigField(
|
||||
type=str,
|
||||
default="https://ark.cn-beijing.volces.com/api/v3",
|
||||
description="API基础URL",
|
||||
example="https://api.example.com/v1",
|
||||
),
|
||||
"volcano_generate_api_key": ConfigField(
|
||||
type=str, default="YOUR_DOUBAO_API_KEY_HERE", description="火山引擎豆包API密钥", required=True
|
||||
),
|
||||
},
|
||||
"components": {
|
||||
"enable_take_picture_action": ConfigField(type=bool, default=True, description="是否启用拍照Action"),
|
||||
"enable_show_pics_command": ConfigField(type=bool, default=True, description="是否启用展示照片Command"),
|
||||
},
|
||||
"picture": {
|
||||
"default_model": ConfigField(
|
||||
type=str,
|
||||
default="doubao-seedream-3-0-t2i-250415",
|
||||
description="默认使用的文生图模型",
|
||||
choices=["doubao-seedream-3-0-t2i-250415", "doubao-seedream-2-0-t2i"],
|
||||
),
|
||||
"default_size": ConfigField(
|
||||
type=str,
|
||||
default="1024x1024",
|
||||
description="默认图片尺寸",
|
||||
example="1024x1024",
|
||||
choices=["1024x1024", "1024x1280", "1280x1024", "1024x1536", "1536x1024"],
|
||||
),
|
||||
"default_watermark": ConfigField(type=bool, default=True, description="是否默认添加水印"),
|
||||
"default_guidance_scale": ConfigField(
|
||||
type=float, default=2.5, description="模型指导强度,影响图片与提示的关联性", example="2.0"
|
||||
),
|
||||
"default_seed": ConfigField(type=int, default=42, description="随机种子,用于复现图片"),
|
||||
"prompt_templates": ConfigField(
|
||||
type=list, default=TakePictureAction.DEFAULT_PROMPT_TEMPLATES, description="用于生成自拍照的prompt模板"
|
||||
),
|
||||
},
|
||||
"storage": {
|
||||
"max_photos": ConfigField(type=int, default=50, description="最大保存的照片数量"),
|
||||
"log_file": ConfigField(type=str, default="picture_log.json", description="照片日志文件名"),
|
||||
"enable_cache": ConfigField(type=bool, default=True, description="是否启用请求缓存"),
|
||||
"max_cache_size": ConfigField(type=int, default=10, description="最大缓存数量"),
|
||||
},
|
||||
}
|
||||
|
||||
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
|
||||
"""返回插件包含的组件列表"""
|
||||
components = []
|
||||
if self.get_config("components.enable_take_picture_action", True):
|
||||
components.append((TakePictureAction.get_action_info(), TakePictureAction))
|
||||
if self.get_config("components.enable_show_pics_command", True):
|
||||
components.append((ShowRecentPicturesCommand.get_command_info(), ShowRecentPicturesCommand))
|
||||
return components
|
||||
@@ -3,10 +3,10 @@ import sys
|
||||
import os
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from datetime import datetime
|
||||
from src.common.database.database_model import Messages, ChatStreams
|
||||
# Add project root to Python path
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, project_root)
|
||||
from src.common.database.database_model import Messages, ChatStreams #noqa
|
||||
|
||||
|
||||
|
||||
|
||||
394
scripts/text_length_analysis.py
Normal file
394
scripts/text_length_analysis.py
Normal file
@@ -0,0 +1,394 @@
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
from datetime import datetime
|
||||
# Add project root to Python path
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, project_root)
|
||||
from src.common.database.database_model import Messages, ChatStreams #noqa
|
||||
|
||||
|
||||
def contains_emoji_or_image_tags(text: str) -> bool:
|
||||
"""Check if text contains [表情包xxxxx] or [图片xxxxx] tags"""
|
||||
if not text:
|
||||
return False
|
||||
|
||||
# 检查是否包含 [表情包] 或 [图片] 标记
|
||||
emoji_pattern = r'\[表情包[^\]]*\]'
|
||||
image_pattern = r'\[图片[^\]]*\]'
|
||||
|
||||
return bool(re.search(emoji_pattern, text) or re.search(image_pattern, text))
|
||||
|
||||
|
||||
def clean_reply_text(text: str) -> str:
|
||||
"""Remove reply references like [回复 xxxx...] from text"""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
# 匹配 [回复 xxxx...] 格式的内容
|
||||
# 使用非贪婪匹配,匹配到第一个 ] 就停止
|
||||
cleaned_text = re.sub(r'\[回复[^\]]*\]', '', text)
|
||||
|
||||
# 去除多余的空白字符
|
||||
cleaned_text = cleaned_text.strip()
|
||||
|
||||
return cleaned_text
|
||||
|
||||
|
||||
def get_chat_name(chat_id: str) -> str:
|
||||
"""Get chat name from chat_id by querying ChatStreams table directly"""
|
||||
try:
|
||||
chat_stream = ChatStreams.get_or_none(ChatStreams.stream_id == chat_id)
|
||||
if chat_stream is None:
|
||||
return f"未知聊天 ({chat_id})"
|
||||
|
||||
if chat_stream.group_name:
|
||||
return f"{chat_stream.group_name} ({chat_id})"
|
||||
elif chat_stream.user_nickname:
|
||||
return f"{chat_stream.user_nickname}的私聊 ({chat_id})"
|
||||
else:
|
||||
return f"未知聊天 ({chat_id})"
|
||||
except Exception:
|
||||
return f"查询失败 ({chat_id})"
|
||||
|
||||
|
||||
def format_timestamp(timestamp: float) -> str:
|
||||
"""Format timestamp to readable date string"""
|
||||
try:
|
||||
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, OSError):
|
||||
return "未知时间"
|
||||
|
||||
|
||||
def calculate_text_length_distribution(messages) -> Dict[str, int]:
|
||||
"""Calculate distribution of processed_plain_text length"""
|
||||
distribution = {
|
||||
'0': 0, # 空文本
|
||||
'1-5': 0, # 极短文本
|
||||
'6-10': 0, # 很短文本
|
||||
'11-20': 0, # 短文本
|
||||
'21-30': 0, # 较短文本
|
||||
'31-50': 0, # 中短文本
|
||||
'51-70': 0, # 中等文本
|
||||
'71-100': 0, # 较长文本
|
||||
'101-150': 0, # 长文本
|
||||
'151-200': 0, # 很长文本
|
||||
'201-300': 0, # 超长文本
|
||||
'301-500': 0, # 极长文本
|
||||
'501-1000': 0, # 巨长文本
|
||||
'1000+': 0 # 超巨长文本
|
||||
}
|
||||
|
||||
for msg in messages:
|
||||
if msg.processed_plain_text is None:
|
||||
continue
|
||||
|
||||
# 排除包含表情包或图片标记的消息
|
||||
if contains_emoji_or_image_tags(msg.processed_plain_text):
|
||||
continue
|
||||
|
||||
# 清理文本中的回复引用
|
||||
cleaned_text = clean_reply_text(msg.processed_plain_text)
|
||||
length = len(cleaned_text)
|
||||
|
||||
if length == 0:
|
||||
distribution['0'] += 1
|
||||
elif length <= 5:
|
||||
distribution['1-5'] += 1
|
||||
elif length <= 10:
|
||||
distribution['6-10'] += 1
|
||||
elif length <= 20:
|
||||
distribution['11-20'] += 1
|
||||
elif length <= 30:
|
||||
distribution['21-30'] += 1
|
||||
elif length <= 50:
|
||||
distribution['31-50'] += 1
|
||||
elif length <= 70:
|
||||
distribution['51-70'] += 1
|
||||
elif length <= 100:
|
||||
distribution['71-100'] += 1
|
||||
elif length <= 150:
|
||||
distribution['101-150'] += 1
|
||||
elif length <= 200:
|
||||
distribution['151-200'] += 1
|
||||
elif length <= 300:
|
||||
distribution['201-300'] += 1
|
||||
elif length <= 500:
|
||||
distribution['301-500'] += 1
|
||||
elif length <= 1000:
|
||||
distribution['501-1000'] += 1
|
||||
else:
|
||||
distribution['1000+'] += 1
|
||||
|
||||
return distribution
|
||||
|
||||
|
||||
def get_text_length_stats(messages) -> Dict[str, float]:
|
||||
"""Calculate basic statistics for processed_plain_text length"""
|
||||
lengths = []
|
||||
null_count = 0
|
||||
excluded_count = 0 # 被排除的消息数量
|
||||
|
||||
for msg in messages:
|
||||
if msg.processed_plain_text is None:
|
||||
null_count += 1
|
||||
elif contains_emoji_or_image_tags(msg.processed_plain_text):
|
||||
# 排除包含表情包或图片标记的消息
|
||||
excluded_count += 1
|
||||
else:
|
||||
# 清理文本中的回复引用
|
||||
cleaned_text = clean_reply_text(msg.processed_plain_text)
|
||||
lengths.append(len(cleaned_text))
|
||||
|
||||
if not lengths:
|
||||
return {
|
||||
'count': 0,
|
||||
'null_count': null_count,
|
||||
'excluded_count': excluded_count,
|
||||
'min': 0,
|
||||
'max': 0,
|
||||
'avg': 0,
|
||||
'median': 0
|
||||
}
|
||||
|
||||
lengths.sort()
|
||||
count = len(lengths)
|
||||
|
||||
return {
|
||||
'count': count,
|
||||
'null_count': null_count,
|
||||
'excluded_count': excluded_count,
|
||||
'min': min(lengths),
|
||||
'max': max(lengths),
|
||||
'avg': sum(lengths) / count,
|
||||
'median': lengths[count // 2] if count % 2 == 1 else (lengths[count // 2 - 1] + lengths[count // 2]) / 2
|
||||
}
|
||||
|
||||
|
||||
def get_available_chats() -> List[Tuple[str, str, int]]:
|
||||
"""Get all available chats with message counts"""
|
||||
try:
|
||||
# 获取所有有消息的chat_id,排除特殊类型消息
|
||||
chat_counts = {}
|
||||
for msg in Messages.select(Messages.chat_id).distinct():
|
||||
chat_id = msg.chat_id
|
||||
count = Messages.select().where(
|
||||
(Messages.chat_id == chat_id) &
|
||||
(Messages.is_emoji != 1) &
|
||||
(Messages.is_picid != 1) &
|
||||
(Messages.is_command != 1)
|
||||
).count()
|
||||
if count > 0:
|
||||
chat_counts[chat_id] = count
|
||||
|
||||
# 获取聊天名称
|
||||
result = []
|
||||
for chat_id, count in chat_counts.items():
|
||||
chat_name = get_chat_name(chat_id)
|
||||
result.append((chat_id, chat_name, count))
|
||||
|
||||
# 按消息数量排序
|
||||
result.sort(key=lambda x: x[2], reverse=True)
|
||||
return result
|
||||
except Exception as e:
|
||||
print(f"获取聊天列表失败: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_time_range_input() -> Tuple[Optional[float], Optional[float]]:
|
||||
"""Get time range input from user"""
|
||||
print("\n时间范围选择:")
|
||||
print("1. 最近1天")
|
||||
print("2. 最近3天")
|
||||
print("3. 最近7天")
|
||||
print("4. 最近30天")
|
||||
print("5. 自定义时间范围")
|
||||
print("6. 不限制时间")
|
||||
|
||||
choice = input("请选择时间范围 (1-6): ").strip()
|
||||
|
||||
now = time.time()
|
||||
|
||||
if choice == "1":
|
||||
return now - 24*3600, now
|
||||
elif choice == "2":
|
||||
return now - 3*24*3600, now
|
||||
elif choice == "3":
|
||||
return now - 7*24*3600, now
|
||||
elif choice == "4":
|
||||
return now - 30*24*3600, now
|
||||
elif choice == "5":
|
||||
print("请输入开始时间 (格式: YYYY-MM-DD HH:MM:SS):")
|
||||
start_str = input().strip()
|
||||
print("请输入结束时间 (格式: YYYY-MM-DD HH:MM:SS):")
|
||||
end_str = input().strip()
|
||||
|
||||
try:
|
||||
start_time = datetime.strptime(start_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
||||
end_time = datetime.strptime(end_str, "%Y-%m-%d %H:%M:%S").timestamp()
|
||||
return start_time, end_time
|
||||
except ValueError:
|
||||
print("时间格式错误,将不限制时间范围")
|
||||
return None, None
|
||||
else:
|
||||
return None, None
|
||||
|
||||
|
||||
def get_top_longest_messages(messages, top_n: int = 10) -> List[Tuple[str, int, str, str]]:
|
||||
"""Get top N longest messages"""
|
||||
message_lengths = []
|
||||
|
||||
for msg in messages:
|
||||
if msg.processed_plain_text is not None:
|
||||
# 排除包含表情包或图片标记的消息
|
||||
if contains_emoji_or_image_tags(msg.processed_plain_text):
|
||||
continue
|
||||
|
||||
# 清理文本中的回复引用
|
||||
cleaned_text = clean_reply_text(msg.processed_plain_text)
|
||||
length = len(cleaned_text)
|
||||
chat_name = get_chat_name(msg.chat_id)
|
||||
time_str = format_timestamp(msg.time)
|
||||
# 截取前100个字符作为预览
|
||||
preview = cleaned_text[:100] + "..." if len(cleaned_text) > 100 else cleaned_text
|
||||
message_lengths.append((chat_name, length, time_str, preview))
|
||||
|
||||
# 按长度排序,取前N个
|
||||
message_lengths.sort(key=lambda x: x[1], reverse=True)
|
||||
return message_lengths[:top_n]
|
||||
|
||||
|
||||
def analyze_text_lengths(chat_id: Optional[str] = None, start_time: Optional[float] = None, end_time: Optional[float] = None) -> None:
|
||||
"""Analyze processed_plain_text lengths with optional filters"""
|
||||
|
||||
# 构建查询条件,排除特殊类型的消息
|
||||
query = Messages.select().where(
|
||||
(Messages.is_emoji != 1) &
|
||||
(Messages.is_picid != 1) &
|
||||
(Messages.is_command != 1)
|
||||
)
|
||||
|
||||
if chat_id:
|
||||
query = query.where(Messages.chat_id == chat_id)
|
||||
|
||||
if start_time:
|
||||
query = query.where(Messages.time >= start_time)
|
||||
|
||||
if end_time:
|
||||
query = query.where(Messages.time <= end_time)
|
||||
|
||||
messages = list(query)
|
||||
|
||||
if not messages:
|
||||
print("没有找到符合条件的消息")
|
||||
return
|
||||
|
||||
# 计算统计信息
|
||||
distribution = calculate_text_length_distribution(messages)
|
||||
stats = get_text_length_stats(messages)
|
||||
top_longest = get_top_longest_messages(messages, 10)
|
||||
|
||||
# 显示结果
|
||||
print("\n=== Processed Plain Text 长度分析结果 ===")
|
||||
print("(已排除表情、图片ID、命令类型消息,已排除[表情包]和[图片]标记消息,已清理回复引用)")
|
||||
if chat_id:
|
||||
print(f"聊天: {get_chat_name(chat_id)}")
|
||||
else:
|
||||
print("聊天: 全部聊天")
|
||||
|
||||
if start_time and end_time:
|
||||
print(f"时间范围: {format_timestamp(start_time)} 到 {format_timestamp(end_time)}")
|
||||
elif start_time:
|
||||
print(f"时间范围: {format_timestamp(start_time)} 之后")
|
||||
elif end_time:
|
||||
print(f"时间范围: {format_timestamp(end_time)} 之前")
|
||||
else:
|
||||
print("时间范围: 不限制")
|
||||
|
||||
print("\n基本统计:")
|
||||
print(f"总消息数量: {len(messages)}")
|
||||
print(f"有文本消息数量: {stats['count']}")
|
||||
print(f"空文本消息数量: {stats['null_count']}")
|
||||
print(f"被排除的消息数量: {stats['excluded_count']}")
|
||||
if stats['count'] > 0:
|
||||
print(f"最短长度: {stats['min']} 字符")
|
||||
print(f"最长长度: {stats['max']} 字符")
|
||||
print(f"平均长度: {stats['avg']:.2f} 字符")
|
||||
print(f"中位数长度: {stats['median']:.2f} 字符")
|
||||
|
||||
print("\n文本长度分布:")
|
||||
total = stats['count']
|
||||
if total > 0:
|
||||
for range_name, count in distribution.items():
|
||||
if count > 0:
|
||||
percentage = count / total * 100
|
||||
print(f"{range_name} 字符: {count} ({percentage:.2f}%)")
|
||||
|
||||
# 显示最长的消息
|
||||
if top_longest:
|
||||
print(f"\n最长的 {len(top_longest)} 条消息:")
|
||||
for i, (chat_name, length, time_str, preview) in enumerate(top_longest, 1):
|
||||
print(f"{i}. [{chat_name}] {time_str}")
|
||||
print(f" 长度: {length} 字符")
|
||||
print(f" 预览: {preview}")
|
||||
print()
|
||||
|
||||
|
||||
def interactive_menu() -> None:
|
||||
"""Interactive menu for text length analysis"""
|
||||
|
||||
while True:
|
||||
print("\n" + "="*50)
|
||||
print("Processed Plain Text 长度分析工具")
|
||||
print("="*50)
|
||||
print("1. 分析全部聊天")
|
||||
print("2. 选择特定聊天分析")
|
||||
print("q. 退出")
|
||||
|
||||
choice = input("\n请选择分析模式 (1-2, q): ").strip()
|
||||
|
||||
if choice.lower() == 'q':
|
||||
print("再见!")
|
||||
break
|
||||
|
||||
chat_id = None
|
||||
|
||||
if choice == "2":
|
||||
# 显示可用的聊天列表
|
||||
chats = get_available_chats()
|
||||
if not chats:
|
||||
print("没有找到聊天数据")
|
||||
continue
|
||||
|
||||
print(f"\n可用的聊天 (共{len(chats)}个):")
|
||||
for i, (_cid, name, count) in enumerate(chats, 1):
|
||||
print(f"{i}. {name} ({count}条消息)")
|
||||
|
||||
try:
|
||||
chat_choice = int(input(f"\n请选择聊天 (1-{len(chats)}): ").strip())
|
||||
if 1 <= chat_choice <= len(chats):
|
||||
chat_id = chats[chat_choice - 1][0]
|
||||
else:
|
||||
print("无效选择")
|
||||
continue
|
||||
except ValueError:
|
||||
print("请输入有效数字")
|
||||
continue
|
||||
|
||||
elif choice != "1":
|
||||
print("无效选择")
|
||||
continue
|
||||
|
||||
# 获取时间范围
|
||||
start_time, end_time = get_time_range_input()
|
||||
|
||||
# 执行分析
|
||||
analyze_text_lengths(chat_id, start_time, end_time)
|
||||
|
||||
input("\n按回车键继续...")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
interactive_menu()
|
||||
@@ -236,10 +236,10 @@ class HeartFChatting:
|
||||
if if_think:
|
||||
factor = max(global_config.chat.focus_value, 0.1)
|
||||
self.energy_value *= 1.1 / factor
|
||||
logger.info(f"{self.log_prefix} 麦麦进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}")
|
||||
logger.info(f"{self.log_prefix} 进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}")
|
||||
else:
|
||||
self.energy_value += 0.1 / global_config.chat.focus_value
|
||||
logger.info(f"{self.log_prefix} 麦麦没有进行思考,能量值线性增加,当前能量值:{self.energy_value:.1f}")
|
||||
logger.debug(f"{self.log_prefix} 没有进行思考,能量值线性增加,当前能量值:{self.energy_value:.1f}")
|
||||
|
||||
logger.debug(f"{self.log_prefix} 当前能量值:{self.energy_value:.1f}")
|
||||
return True
|
||||
@@ -330,13 +330,13 @@ class HeartFChatting:
|
||||
|
||||
if self.loop_mode == ChatMode.NORMAL:
|
||||
if action_type == "no_action":
|
||||
logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复")
|
||||
logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复")
|
||||
elif is_parallel:
|
||||
logger.info(
|
||||
f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作"
|
||||
f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作"
|
||||
)
|
||||
else:
|
||||
logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定执行{action_type}动作")
|
||||
logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定执行{action_type}动作")
|
||||
|
||||
if action_type == "no_action":
|
||||
# 等待回复生成完毕
|
||||
@@ -351,15 +351,15 @@ class HeartFChatting:
|
||||
|
||||
# 模型炸了,没有回复内容生成
|
||||
if not response_set:
|
||||
logger.warning(f"[{self.log_prefix}] 模型未生成回复内容")
|
||||
logger.warning(f"{self.log_prefix}模型未生成回复内容")
|
||||
return False
|
||||
elif action_type not in ["no_action"] and not is_parallel:
|
||||
logger.info(
|
||||
f"[{self.log_prefix}] {global_config.bot.nickname} 原本想要回复:{content},但选择执行{action_type},不发表回复"
|
||||
f"{self.log_prefix}{global_config.bot.nickname} 原本想要回复:{content},但选择执行{action_type},不发表回复"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(f"[{self.log_prefix}] {global_config.bot.nickname} 决定的回复内容: {content}")
|
||||
logger.info(f"{self.log_prefix}{global_config.bot.nickname} 决定的回复内容: {content}")
|
||||
|
||||
# 发送回复 (不再需要传入 chat)
|
||||
reply_text = await self._send_response(response_set, reply_to_str, loop_start_time,message_data)
|
||||
@@ -563,7 +563,7 @@ class HeartFChatting:
|
||||
return reply_set
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[{self.log_prefix}] 回复生成出现错误:{str(e)} {traceback.format_exc()}")
|
||||
logger.error(f"{self.log_prefix}回复生成出现错误:{str(e)} {traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
async def _send_response(self, reply_set, reply_to, thinking_start_time, message_data):
|
||||
|
||||
@@ -87,36 +87,90 @@ class ExpressionLearner:
|
||||
request_type="expressor.learner",
|
||||
)
|
||||
self.llm_model = None
|
||||
self._ensure_expression_directories()
|
||||
self._auto_migrate_json_to_db()
|
||||
self._migrate_old_data_create_date()
|
||||
|
||||
def _ensure_expression_directories(self):
|
||||
"""
|
||||
确保表达方式相关的目录结构存在
|
||||
"""
|
||||
base_dir = os.path.join("data", "expression")
|
||||
directories_to_create = [
|
||||
base_dir,
|
||||
os.path.join(base_dir, "learnt_style"),
|
||||
os.path.join(base_dir, "learnt_grammar"),
|
||||
]
|
||||
|
||||
for directory in directories_to_create:
|
||||
try:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
logger.debug(f"确保目录存在: {directory}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建目录失败 {directory}: {e}")
|
||||
|
||||
def _auto_migrate_json_to_db(self):
|
||||
"""
|
||||
自动将/data/expression/learnt_style 和 learnt_grammar 下所有expressions.json迁移到数据库。
|
||||
迁移完成后在/data/expression/done.done写入标记文件,存在则跳过。
|
||||
"""
|
||||
done_flag = os.path.join("data", "expression", "done.done")
|
||||
base_dir = os.path.join("data", "expression")
|
||||
done_flag = os.path.join(base_dir, "done.done")
|
||||
|
||||
# 确保基础目录存在
|
||||
try:
|
||||
os.makedirs(base_dir, exist_ok=True)
|
||||
logger.debug(f"确保目录存在: {base_dir}")
|
||||
except Exception as e:
|
||||
logger.error(f"创建表达方式目录失败: {e}")
|
||||
return
|
||||
|
||||
if os.path.exists(done_flag):
|
||||
logger.info("表达方式JSON已迁移,无需重复迁移。")
|
||||
return
|
||||
base_dir = os.path.join("data", "expression")
|
||||
|
||||
logger.info("开始迁移表达方式JSON到数据库...")
|
||||
migrated_count = 0
|
||||
|
||||
for type in ["learnt_style", "learnt_grammar"]:
|
||||
type_str = "style" if type == "learnt_style" else "grammar"
|
||||
type_dir = os.path.join(base_dir, type)
|
||||
if not os.path.exists(type_dir):
|
||||
logger.debug(f"目录不存在,跳过: {type_dir}")
|
||||
continue
|
||||
for chat_id in os.listdir(type_dir):
|
||||
|
||||
try:
|
||||
chat_ids = os.listdir(type_dir)
|
||||
logger.debug(f"在 {type_dir} 中找到 {len(chat_ids)} 个聊天ID目录")
|
||||
except Exception as e:
|
||||
logger.error(f"读取目录失败 {type_dir}: {e}")
|
||||
continue
|
||||
|
||||
for chat_id in chat_ids:
|
||||
expr_file = os.path.join(type_dir, chat_id, "expressions.json")
|
||||
if not os.path.exists(expr_file):
|
||||
continue
|
||||
try:
|
||||
with open(expr_file, "r", encoding="utf-8") as f:
|
||||
expressions = json.load(f)
|
||||
|
||||
if not isinstance(expressions, list):
|
||||
logger.warning(f"表达方式文件格式错误,跳过: {expr_file}")
|
||||
continue
|
||||
|
||||
for expr in expressions:
|
||||
if not isinstance(expr, dict):
|
||||
continue
|
||||
|
||||
situation = expr.get("situation")
|
||||
style_val = expr.get("style")
|
||||
count = expr.get("count", 1)
|
||||
last_active_time = expr.get("last_active_time", time.time())
|
||||
|
||||
if not situation or not style_val:
|
||||
logger.warning(f"表达方式缺少必要字段,跳过: {expr}")
|
||||
continue
|
||||
|
||||
# 查重:同chat_id+type+situation+style
|
||||
from src.common.database.database_model import Expression
|
||||
|
||||
@@ -141,14 +195,28 @@ class ExpressionLearner:
|
||||
type=type_str,
|
||||
create_date=last_active_time, # 迁移时使用last_active_time作为创建时间
|
||||
)
|
||||
logger.info(f"已迁移 {expr_file} 到数据库")
|
||||
migrated_count += 1
|
||||
logger.info(f"已迁移 {expr_file} 到数据库,包含 {len(expressions)} 个表达方式")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析失败 {expr_file}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"迁移表达方式 {expr_file} 失败: {e}")
|
||||
|
||||
# 标记迁移完成
|
||||
try:
|
||||
# 确保done.done文件的父目录存在
|
||||
done_parent_dir = os.path.dirname(done_flag)
|
||||
if not os.path.exists(done_parent_dir):
|
||||
os.makedirs(done_parent_dir, exist_ok=True)
|
||||
logger.debug(f"为done.done创建父目录: {done_parent_dir}")
|
||||
|
||||
with open(done_flag, "w", encoding="utf-8") as f:
|
||||
f.write("done\n")
|
||||
logger.info("表达方式JSON迁移已完成,已写入done.done标记文件")
|
||||
logger.info(f"表达方式JSON迁移已完成,共迁移 {migrated_count} 个表达方式,已写入done.done标记文件")
|
||||
except PermissionError as e:
|
||||
logger.error(f"权限不足,无法写入done.done标记文件: {e}")
|
||||
except OSError as e:
|
||||
logger.error(f"文件系统错误,无法写入done.done标记文件: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"写入done.done标记文件失败: {e}")
|
||||
|
||||
@@ -266,9 +334,17 @@ class ExpressionLearner:
|
||||
for type in ["style", "grammar"]:
|
||||
base_dir = os.path.join("data", "expression", f"learnt_{type}")
|
||||
if not os.path.exists(base_dir):
|
||||
logger.debug(f"目录不存在,跳过衰减: {base_dir}")
|
||||
continue
|
||||
|
||||
for chat_id in os.listdir(base_dir):
|
||||
try:
|
||||
chat_ids = os.listdir(base_dir)
|
||||
logger.debug(f"在 {base_dir} 中找到 {len(chat_ids)} 个聊天ID目录进行衰减")
|
||||
except Exception as e:
|
||||
logger.error(f"读取目录失败 {base_dir}: {e}")
|
||||
continue
|
||||
|
||||
for chat_id in chat_ids:
|
||||
file_path = os.path.join(base_dir, chat_id, "expressions.json")
|
||||
if not os.path.exists(file_path):
|
||||
continue
|
||||
@@ -277,14 +353,24 @@ class ExpressionLearner:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
expressions = json.load(f)
|
||||
|
||||
if not isinstance(expressions, list):
|
||||
logger.warning(f"表达方式文件格式错误,跳过衰减: {file_path}")
|
||||
continue
|
||||
|
||||
# 应用全局衰减
|
||||
decayed_expressions = self.apply_decay_to_expressions(expressions, current_time)
|
||||
|
||||
# 保存衰减后的结果
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(decayed_expressions, f, ensure_ascii=False, indent=2)
|
||||
|
||||
logger.debug(f"已对 {file_path} 应用衰减,剩余 {len(decayed_expressions)} 个表达方式")
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON解析失败,跳过衰减 {file_path}: {e}")
|
||||
except PermissionError as e:
|
||||
logger.error(f"权限不足,无法更新 {file_path}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"全局衰减{type}表达方式失败: {e}")
|
||||
logger.error(f"全局衰减{type}表达方式失败 {file_path}: {e}")
|
||||
continue
|
||||
|
||||
learnt_style: Optional[List[Tuple[str, str, str]]] = []
|
||||
|
||||
@@ -12,6 +12,7 @@ from src.chat.message_receive.storage import MessageStorage
|
||||
from src.chat.heart_flow.heartflow import heartflow
|
||||
from src.chat.utils.utils import is_mentioned_bot_in_message
|
||||
from src.chat.utils.timer_calculator import Timer
|
||||
from src.chat.utils.chat_message_builder import replace_user_references_in_content
|
||||
from src.common.logger import get_logger
|
||||
from src.person_info.relationship_manager import get_relationship_manager
|
||||
from src.mood.mood_manager import mood_manager
|
||||
@@ -56,16 +57,41 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
|
||||
with Timer("记忆激活"):
|
||||
interested_rate = await hippocampus_manager.get_activate_from_text(
|
||||
message.processed_plain_text,
|
||||
max_depth= 5,
|
||||
fast_retrieval=False,
|
||||
)
|
||||
logger.debug(f"记忆激活率: {interested_rate:.2f}")
|
||||
|
||||
text_len = len(message.processed_plain_text)
|
||||
# 根据文本长度调整兴趣度,长度越大兴趣度越高,但增长率递减,最低0.01,最高0.05
|
||||
# 采用对数函数实现递减增长
|
||||
|
||||
base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1))
|
||||
base_interest = min(max(base_interest, 0.01), 0.05)
|
||||
# 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算
|
||||
# 基于实际分布:0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%)
|
||||
|
||||
if text_len == 0:
|
||||
base_interest = 0.01 # 空消息最低兴趣度
|
||||
elif text_len <= 5:
|
||||
# 1-5字符:线性增长 0.01 -> 0.03
|
||||
base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4
|
||||
elif text_len <= 10:
|
||||
# 6-10字符:线性增长 0.03 -> 0.06
|
||||
base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5
|
||||
elif text_len <= 20:
|
||||
# 11-20字符:线性增长 0.06 -> 0.12
|
||||
base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10
|
||||
elif text_len <= 30:
|
||||
# 21-30字符:线性增长 0.12 -> 0.18
|
||||
base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10
|
||||
elif text_len <= 50:
|
||||
# 31-50字符:线性增长 0.18 -> 0.22
|
||||
base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20
|
||||
elif text_len <= 100:
|
||||
# 51-100字符:线性增长 0.22 -> 0.26
|
||||
base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50
|
||||
else:
|
||||
# 100+字符:对数增长 0.26 -> 0.3,增长率递减
|
||||
base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901
|
||||
|
||||
# 确保在范围内
|
||||
base_interest = min(max(base_interest, 0.01), 0.3)
|
||||
|
||||
interested_rate += base_interest
|
||||
|
||||
@@ -111,9 +137,9 @@ class HeartFCMessageReceiver:
|
||||
subheartflow: SubHeartflow = await heartflow.get_or_create_subheartflow(chat.stream_id) # type: ignore
|
||||
|
||||
# subheartflow.add_message_to_normal_chat_cache(message, interested_rate, is_mentioned)
|
||||
|
||||
chat_mood = mood_manager.get_mood_by_chat_id(subheartflow.chat_id)
|
||||
asyncio.create_task(chat_mood.update_mood_by_message(message, interested_rate))
|
||||
if global_config.mood.enable_mood:
|
||||
chat_mood = mood_manager.get_mood_by_chat_id(subheartflow.chat_id)
|
||||
asyncio.create_task(chat_mood.update_mood_by_message(message, interested_rate))
|
||||
|
||||
# 3. 日志记录
|
||||
mes_name = chat.group_info.group_name if chat.group_info else "私聊"
|
||||
@@ -123,8 +149,16 @@ class HeartFCMessageReceiver:
|
||||
# 如果消息中包含图片标识,则将 [picid:...] 替换为 [图片]
|
||||
picid_pattern = r"\[picid:([^\]]+)\]"
|
||||
processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text)
|
||||
|
||||
# 应用用户引用格式替换,将回复<aaa:bbb>和@<aaa:bbb>格式转换为可读格式
|
||||
processed_plain_text = replace_user_references_in_content(
|
||||
processed_plain_text,
|
||||
message.message_info.platform,
|
||||
is_async=False,
|
||||
replace_bot_name=True
|
||||
)
|
||||
|
||||
logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}") # type: ignore
|
||||
logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}]") # type: ignore
|
||||
|
||||
logger.debug(f"[{mes_name}][当前时段回复频率: {current_talk_frequency}]")
|
||||
|
||||
|
||||
@@ -224,10 +224,15 @@ class Hippocampus:
|
||||
return hash((source, target))
|
||||
|
||||
@staticmethod
|
||||
def find_topic_llm(text, topic_num):
|
||||
# sourcery skip: inline-immediately-returned-variable
|
||||
def find_topic_llm(text:str, topic_num:int|list[int]):
|
||||
topic_num_str = ""
|
||||
if isinstance(topic_num, list):
|
||||
topic_num_str = f"{topic_num[0]}-{topic_num[1]}"
|
||||
else:
|
||||
topic_num_str = topic_num
|
||||
|
||||
prompt = (
|
||||
f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,"
|
||||
f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num_str}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,"
|
||||
f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。"
|
||||
f"如果确定找不出主题或者没有明显主题,返回<none>。"
|
||||
)
|
||||
@@ -299,6 +304,59 @@ class Hippocampus:
|
||||
# 按相似度降序排序
|
||||
memories.sort(key=lambda x: x[2], reverse=True)
|
||||
return memories
|
||||
|
||||
async def get_keywords_from_text(self, text: str) -> list:
|
||||
"""从文本中提取关键词。
|
||||
|
||||
Args:
|
||||
text (str): 输入文本
|
||||
fast_retrieval (bool, optional): 是否使用快速检索。默认为False。
|
||||
如果为True,使用jieba分词提取关键词,速度更快但可能不够准确。
|
||||
如果为False,使用LLM提取关键词,速度较慢但更准确。
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
# 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算
|
||||
text_length = len(text)
|
||||
topic_num:str|list[int] = None
|
||||
if text_length <= 5:
|
||||
words = jieba.cut(text)
|
||||
keywords = [word for word in words if len(word) > 1]
|
||||
keywords = list(set(keywords))[:3] # 限制最多3个关键词
|
||||
logger.info(f"提取关键词: {keywords}")
|
||||
return keywords
|
||||
elif text_length <= 10:
|
||||
topic_num = [1,3] # 6-10字符: 1个关键词 (27.18%的文本)
|
||||
elif text_length <= 20:
|
||||
topic_num = [2,4] # 11-20字符: 2个关键词 (22.76%的文本)
|
||||
elif text_length <= 30:
|
||||
topic_num = [3,5] # 21-30字符: 3个关键词 (10.33%的文本)
|
||||
elif text_length <= 50:
|
||||
topic_num = [4,5] # 31-50字符: 4个关键词 (9.79%的文本)
|
||||
else:
|
||||
topic_num = 5 # 51+字符: 5个关键词 (其余长文本)
|
||||
|
||||
|
||||
topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async(
|
||||
self.find_topic_llm(text, topic_num)
|
||||
)
|
||||
|
||||
# 提取关键词
|
||||
keywords = re.findall(r"<([^>]+)>", topics_response)
|
||||
if not keywords:
|
||||
keywords = []
|
||||
else:
|
||||
keywords = [
|
||||
keyword.strip()
|
||||
for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",")
|
||||
if keyword.strip()
|
||||
]
|
||||
|
||||
logger.info(f"提取关键词: {keywords}")
|
||||
|
||||
return keywords
|
||||
|
||||
|
||||
async def get_memory_from_text(
|
||||
self,
|
||||
@@ -325,39 +383,7 @@ class Hippocampus:
|
||||
- memory_items: list, 该主题下的记忆项列表
|
||||
- similarity: float, 与文本的相似度
|
||||
"""
|
||||
if not text:
|
||||
return []
|
||||
|
||||
if fast_retrieval:
|
||||
# 使用jieba分词提取关键词
|
||||
words = jieba.cut(text)
|
||||
# 过滤掉停用词和单字词
|
||||
keywords = [word for word in words if len(word) > 1]
|
||||
# 去重
|
||||
keywords = list(set(keywords))
|
||||
# 限制关键词数量
|
||||
logger.debug(f"提取关键词: {keywords}")
|
||||
|
||||
else:
|
||||
# 使用LLM提取关键词
|
||||
topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量
|
||||
# logger.info(f"提取关键词数量: {topic_num}")
|
||||
topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async(
|
||||
self.find_topic_llm(text, topic_num)
|
||||
)
|
||||
|
||||
# 提取关键词
|
||||
keywords = re.findall(r"<([^>]+)>", topics_response)
|
||||
if not keywords:
|
||||
keywords = []
|
||||
else:
|
||||
keywords = [
|
||||
keyword.strip()
|
||||
for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",")
|
||||
if keyword.strip()
|
||||
]
|
||||
|
||||
# logger.info(f"提取的关键词: {', '.join(keywords)}")
|
||||
keywords = await self.get_keywords_from_text(text)
|
||||
|
||||
# 过滤掉不存在于记忆图中的关键词
|
||||
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
|
||||
@@ -679,38 +705,7 @@ class Hippocampus:
|
||||
Returns:
|
||||
float: 激活节点数与总节点数的比值
|
||||
"""
|
||||
if not text:
|
||||
return 0
|
||||
|
||||
if fast_retrieval:
|
||||
# 使用jieba分词提取关键词
|
||||
words = jieba.cut(text)
|
||||
# 过滤掉停用词和单字词
|
||||
keywords = [word for word in words if len(word) > 1]
|
||||
# 去重
|
||||
keywords = list(set(keywords))
|
||||
# 限制关键词数量
|
||||
keywords = keywords[:5]
|
||||
else:
|
||||
# 使用LLM提取关键词
|
||||
topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量
|
||||
# logger.info(f"提取关键词数量: {topic_num}")
|
||||
topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async(
|
||||
self.find_topic_llm(text, topic_num)
|
||||
)
|
||||
|
||||
# 提取关键词
|
||||
keywords = re.findall(r"<([^>]+)>", topics_response)
|
||||
if not keywords:
|
||||
keywords = []
|
||||
else:
|
||||
keywords = [
|
||||
keyword.strip()
|
||||
for keyword in ",".join(keywords).replace(",", ",").replace("、", ",").replace(" ", ",").split(",")
|
||||
if keyword.strip()
|
||||
]
|
||||
|
||||
# logger.info(f"提取的关键词: {', '.join(keywords)}")
|
||||
keywords = await self.get_keywords_from_text(text)
|
||||
|
||||
# 过滤掉不存在于记忆图中的关键词
|
||||
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
|
||||
@@ -727,7 +722,7 @@ class Hippocampus:
|
||||
for keyword in valid_keywords:
|
||||
logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):")
|
||||
# 初始化激活值
|
||||
activation_values = {keyword: 1.0}
|
||||
activation_values = {keyword: 1.5}
|
||||
# 记录已访问的节点
|
||||
visited_nodes = {keyword}
|
||||
# 待处理的节点队列,每个元素是(节点, 激活值, 当前深度)
|
||||
|
||||
@@ -92,7 +92,6 @@ class ChatBot:
|
||||
command_result = component_registry.find_command_by_text(text)
|
||||
if command_result:
|
||||
command_class, matched_groups, command_info = command_result
|
||||
intercept_message = command_info.intercept_message
|
||||
plugin_name = command_info.plugin_name
|
||||
command_name = command_info.name
|
||||
if (
|
||||
@@ -115,7 +114,7 @@ class ChatBot:
|
||||
|
||||
try:
|
||||
# 执行命令
|
||||
success, response = await command_instance.execute()
|
||||
success, response, intercept_message = await command_instance.execute()
|
||||
|
||||
# 记录命令执行结果
|
||||
if success:
|
||||
@@ -128,8 +127,6 @@ class ChatBot:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行命令时出错: {command_class.__name__} - {e}")
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
try:
|
||||
@@ -138,7 +135,7 @@ class ChatBot:
|
||||
logger.error(f"发送错误消息失败: {send_error}")
|
||||
|
||||
# 命令出错时,根据命令的拦截设置决定是否继续处理消息
|
||||
return True, str(e), not intercept_message
|
||||
return True, str(e), False # 出错时继续处理消息
|
||||
|
||||
# 没有找到命令,继续处理消息
|
||||
return False, None, True
|
||||
|
||||
@@ -438,94 +438,4 @@ class ActionModifier:
|
||||
return True
|
||||
else:
|
||||
logger.debug(f"{self.log_prefix}动作 {action_name} 未匹配到任何关键词: {activation_keywords}")
|
||||
return False
|
||||
|
||||
# async def analyze_loop_actions(self, history_loop: List[CycleDetail]) -> List[tuple[str, str]]:
|
||||
# """分析最近的循环内容并决定动作的移除
|
||||
|
||||
# Returns:
|
||||
# List[Tuple[str, str]]: 包含要删除的动作及原因的元组列表
|
||||
# [("action3", "some reason")]
|
||||
# """
|
||||
# removals = []
|
||||
|
||||
# # 获取最近10次循环
|
||||
# recent_cycles = history_loop[-10:] if len(history_loop) > 10 else history_loop
|
||||
# if not recent_cycles:
|
||||
# return removals
|
||||
|
||||
# reply_sequence = [] # 记录最近的动作序列
|
||||
|
||||
# for cycle in recent_cycles:
|
||||
# action_result = cycle.loop_plan_info.get("action_result", {})
|
||||
# action_type = action_result.get("action_type", "unknown")
|
||||
# reply_sequence.append(action_type == "reply")
|
||||
|
||||
# # 计算连续回复的相关阈值
|
||||
|
||||
# max_reply_num = int(global_config.focus_chat.consecutive_replies * 3.2)
|
||||
# sec_thres_reply_num = int(global_config.focus_chat.consecutive_replies * 2)
|
||||
# one_thres_reply_num = int(global_config.focus_chat.consecutive_replies * 1.5)
|
||||
|
||||
# # 获取最近max_reply_num次的reply状态
|
||||
# if len(reply_sequence) >= max_reply_num:
|
||||
# last_max_reply_num = reply_sequence[-max_reply_num:]
|
||||
# else:
|
||||
# last_max_reply_num = reply_sequence[:]
|
||||
|
||||
# # 详细打印阈值和序列信息,便于调试
|
||||
# logger.info(
|
||||
# f"连续回复阈值: max={max_reply_num}, sec={sec_thres_reply_num}, one={one_thres_reply_num},"
|
||||
# f"最近reply序列: {last_max_reply_num}"
|
||||
# )
|
||||
# # print(f"consecutive_replies: {consecutive_replies}")
|
||||
|
||||
# # 根据最近的reply情况决定是否移除reply动作
|
||||
# if len(last_max_reply_num) >= max_reply_num and all(last_max_reply_num):
|
||||
# # 如果最近max_reply_num次都是reply,直接移除
|
||||
# reason = f"连续回复过多(最近{len(last_max_reply_num)}次全是reply,超过阈值{max_reply_num})"
|
||||
# removals.append(("reply", reason))
|
||||
# # reply_count = len(last_max_reply_num) - no_reply_count
|
||||
# elif len(last_max_reply_num) >= sec_thres_reply_num and all(last_max_reply_num[-sec_thres_reply_num:]):
|
||||
# # 如果最近sec_thres_reply_num次都是reply,40%概率移除
|
||||
# removal_probability = 0.4 / global_config.focus_chat.consecutive_replies
|
||||
# if random.random() < removal_probability:
|
||||
# reason = (
|
||||
# f"连续回复较多(最近{sec_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)"
|
||||
# )
|
||||
# removals.append(("reply", reason))
|
||||
# elif len(last_max_reply_num) >= one_thres_reply_num and all(last_max_reply_num[-one_thres_reply_num:]):
|
||||
# # 如果最近one_thres_reply_num次都是reply,20%概率移除
|
||||
# removal_probability = 0.2 / global_config.focus_chat.consecutive_replies
|
||||
# if random.random() < removal_probability:
|
||||
# reason = (
|
||||
# f"连续回复检测(最近{one_thres_reply_num}次全是reply,{removal_probability:.2f}概率移除,触发移除)"
|
||||
# )
|
||||
# removals.append(("reply", reason))
|
||||
# else:
|
||||
# logger.debug(f"{self.log_prefix}连续回复检测:无需移除reply动作,最近回复模式正常")
|
||||
|
||||
# return removals
|
||||
|
||||
# def get_available_actions_count(self, mode: str = "focus") -> int:
|
||||
# """获取当前可用动作数量(排除默认的no_action)"""
|
||||
# current_actions = self.action_manager.get_using_actions_for_mode(mode)
|
||||
# # 排除no_action(如果存在)
|
||||
# filtered_actions = {k: v for k, v in current_actions.items() if k != "no_action"}
|
||||
# return len(filtered_actions)
|
||||
|
||||
# def should_skip_planning_for_no_reply(self) -> bool:
|
||||
# """判断是否应该跳过规划过程"""
|
||||
# current_actions = self.action_manager.get_using_actions_for_mode("focus")
|
||||
# # 排除no_action(如果存在)
|
||||
# if len(current_actions) == 1 and "no_reply" in current_actions:
|
||||
# return True
|
||||
# return False
|
||||
|
||||
# def should_skip_planning_for_no_action(self) -> bool:
|
||||
# """判断是否应该跳过规划过程"""
|
||||
# available_count = self.action_manager.get_using_actions_for_mode("normal")
|
||||
# if available_count == 0:
|
||||
# logger.debug(f"{self.log_prefix} 没有可用动作,跳过规划")
|
||||
# return True
|
||||
# return False
|
||||
return False
|
||||
@@ -40,7 +40,6 @@ def init_prompt():
|
||||
{moderation_prompt}
|
||||
|
||||
现在请你根据{by_what}选择合适的action和触发action的消息:
|
||||
你刚刚选择并执行过的action是:
|
||||
{actions_before_now_block}
|
||||
|
||||
{no_action_block}
|
||||
@@ -130,17 +129,18 @@ class ActionPlanner:
|
||||
logger.warning(f"{self.log_prefix}使用中的动作 {action_name} 未在已注册动作中找到")
|
||||
|
||||
# 如果没有可用动作或只有no_reply动作,直接返回no_reply
|
||||
if not current_available_actions:
|
||||
action = "no_reply" if mode == ChatMode.FOCUS else "no_action"
|
||||
reasoning = "没有可用的动作"
|
||||
logger.info(f"{self.log_prefix}{reasoning}")
|
||||
return {
|
||||
"action_result": {
|
||||
"action_type": action,
|
||||
"action_data": action_data,
|
||||
"reasoning": reasoning,
|
||||
},
|
||||
}, None
|
||||
# 因为现在reply是永远激活,所以不需要空跳判定
|
||||
# if not current_available_actions:
|
||||
# action = "no_reply" if mode == ChatMode.FOCUS else "no_action"
|
||||
# reasoning = "没有可用的动作"
|
||||
# logger.info(f"{self.log_prefix}{reasoning}")
|
||||
# return {
|
||||
# "action_result": {
|
||||
# "action_type": action,
|
||||
# "action_data": action_data,
|
||||
# "reasoning": reasoning,
|
||||
# },
|
||||
# }, None
|
||||
|
||||
# --- 构建提示词 (调用修改后的 PromptBuilder 方法) ---
|
||||
prompt, message_id_list = await self.build_planner_prompt(
|
||||
@@ -268,6 +268,7 @@ class ActionPlanner:
|
||||
|
||||
actions_before_now = get_actions_by_timestamp_with_chat(
|
||||
chat_id=self.chat_id,
|
||||
timestamp_start=time.time()-3600,
|
||||
timestamp_end=time.time(),
|
||||
limit=5,
|
||||
)
|
||||
@@ -275,27 +276,35 @@ class ActionPlanner:
|
||||
actions_before_now_block = build_readable_actions(
|
||||
actions=actions_before_now,
|
||||
)
|
||||
|
||||
actions_before_now_block = f"你刚刚选择并执行过的action是:\n{actions_before_now_block}"
|
||||
|
||||
self.last_obs_time_mark = time.time()
|
||||
|
||||
if mode == ChatMode.FOCUS:
|
||||
mentioned_bonus = ""
|
||||
if global_config.chat.mentioned_bot_inevitable_reply:
|
||||
mentioned_bonus = "\n- 有人提到你"
|
||||
if global_config.chat.at_bot_inevitable_reply:
|
||||
mentioned_bonus = "\n- 有人提到你,或者at你"
|
||||
|
||||
|
||||
by_what = "聊天内容"
|
||||
target_prompt = '\n "target_message_id":"触发action的消息id"'
|
||||
no_action_block = """重要说明1:
|
||||
no_action_block = f"""重要说明:
|
||||
- 'no_reply' 表示只进行不进行回复,等待合适的回复时机
|
||||
- 当你刚刚发送了消息,没有人回复时,选择no_reply
|
||||
- 当你一次发送了太多消息,为了避免打扰聊天节奏,选择no_reply
|
||||
|
||||
动作:reply
|
||||
动作描述:参与聊天回复,发送文本进行表达
|
||||
- 你想要闲聊或者随便附和
|
||||
- 有人提到你
|
||||
- 你想要闲聊或者随便附和{mentioned_bonus}
|
||||
- 如果你刚刚进行了回复,不要对同一个话题重复回应
|
||||
{
|
||||
{{
|
||||
"action": "reply",
|
||||
"target_message_id":"触发action的消息id",
|
||||
"reason":"回复的原因"
|
||||
}
|
||||
}}
|
||||
|
||||
"""
|
||||
else:
|
||||
|
||||
@@ -17,7 +17,7 @@ from src.chat.message_receive.uni_message_sender import HeartFCSender
|
||||
from src.chat.utils.timer_calculator import Timer # <--- Import Timer
|
||||
from src.chat.utils.utils import get_chat_type_and_target_info
|
||||
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat
|
||||
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat, replace_user_references_in_content
|
||||
from src.chat.express.expression_selector import expression_selector
|
||||
from src.chat.knowledge.knowledge_lib import qa_manager
|
||||
from src.chat.memory_system.memory_activator import MemoryActivator
|
||||
@@ -74,6 +74,7 @@ def init_prompt():
|
||||
|
||||
你正在{chat_target_2},{reply_target_block}
|
||||
对这句话,你想表达,原句:{raw_reply},原因是:{reason}。你现在要思考怎么组织回复
|
||||
你现在的心情是:{mood_state}
|
||||
你需要使用合适的语法和句法,参考聊天内容,组织一条日常且口语化的回复。请你修改你想表达的原句,符合你的表达风格和语言习惯
|
||||
{config_expression_style},你可以完全重组回复,保留最基本的表达含义就好,但重组后保持语意通顺。
|
||||
{keywords_reaction_prompt}
|
||||
@@ -450,6 +451,9 @@ class DefaultReplyer:
|
||||
def _parse_reply_target(self, target_message: str) -> tuple:
|
||||
sender = ""
|
||||
target = ""
|
||||
# 添加None检查,防止NoneType错误
|
||||
if target_message is None:
|
||||
return sender, target
|
||||
if ":" in target_message or ":" in target_message:
|
||||
# 使用正则表达式匹配中文或英文冒号
|
||||
parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1)
|
||||
@@ -462,6 +466,10 @@ class DefaultReplyer:
|
||||
# 关键词检测与反应
|
||||
keywords_reaction_prompt = ""
|
||||
try:
|
||||
# 添加None检查,防止NoneType错误
|
||||
if target is None:
|
||||
return keywords_reaction_prompt
|
||||
|
||||
# 处理关键词规则
|
||||
for rule in global_config.keyword_reaction.keyword_rules:
|
||||
if any(keyword in target for keyword in rule.keywords):
|
||||
@@ -524,7 +532,7 @@ class DefaultReplyer:
|
||||
# 其他用户的对话
|
||||
background_dialogue_list.append(msg_dict)
|
||||
except Exception as e:
|
||||
logger.error(f"无法处理历史消息记录: {msg_dict}, 错误: {e}")
|
||||
logger.error(f"记录: {msg_dict}, 错误: {e}")
|
||||
|
||||
# 构建背景对话 prompt
|
||||
background_dialogue_prompt = ""
|
||||
@@ -613,11 +621,22 @@ class DefaultReplyer:
|
||||
is_group_chat = bool(chat_stream.group_info)
|
||||
reply_to = reply_data.get("reply_to", "none")
|
||||
extra_info_block = reply_data.get("extra_info", "") or reply_data.get("extra_info_block", "")
|
||||
|
||||
chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
|
||||
mood_prompt = chat_mood.mood_state
|
||||
|
||||
if global_config.mood.enable_mood:
|
||||
chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
|
||||
mood_prompt = chat_mood.mood_state
|
||||
else:
|
||||
mood_prompt = ""
|
||||
|
||||
sender, target = self._parse_reply_target(reply_to)
|
||||
|
||||
target = replace_user_references_in_content(
|
||||
target,
|
||||
chat_stream.platform,
|
||||
is_async=False,
|
||||
replace_bot_name=True
|
||||
)
|
||||
|
||||
|
||||
# 构建action描述 (如果启用planner)
|
||||
action_descriptions = ""
|
||||
@@ -876,6 +895,13 @@ class DefaultReplyer:
|
||||
reason = reply_data.get("reason", "")
|
||||
sender, target = self._parse_reply_target(reply_to)
|
||||
|
||||
# 添加情绪状态获取
|
||||
if global_config.mood.enable_mood:
|
||||
chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
|
||||
mood_prompt = chat_mood.mood_state
|
||||
else:
|
||||
mood_prompt = ""
|
||||
|
||||
message_list_before_now_half = get_raw_msg_before_timestamp_with_chat(
|
||||
chat_id=chat_id,
|
||||
timestamp=time.time(),
|
||||
@@ -956,6 +982,7 @@ class DefaultReplyer:
|
||||
reply_target_block=reply_target_block,
|
||||
raw_reply=raw_reply,
|
||||
reason=reason,
|
||||
mood_state=mood_prompt, # 添加情绪状态参数
|
||||
config_expression_style=global_config.expression.expression_style,
|
||||
keywords_reaction_prompt=keywords_reaction_prompt,
|
||||
moderation_prompt=moderation_prompt_block,
|
||||
|
||||
@@ -2,7 +2,7 @@ import time # 导入 time 模块以获取当前时间
|
||||
import random
|
||||
import re
|
||||
|
||||
from typing import List, Dict, Any, Tuple, Optional
|
||||
from typing import List, Dict, Any, Tuple, Optional, Union, Callable
|
||||
from rich.traceback import install
|
||||
|
||||
from src.config.config import global_config
|
||||
@@ -15,6 +15,155 @@ from src.chat.utils.utils import translate_timestamp_to_human_readable,assign_me
|
||||
install(extra_lines=3)
|
||||
|
||||
|
||||
def replace_user_references_in_content(
|
||||
content: str,
|
||||
platform: str,
|
||||
name_resolver: Union[Callable[[str, str], str], Callable[[str, str], Any]] = None,
|
||||
is_async: bool = False,
|
||||
replace_bot_name: bool = True
|
||||
) -> Union[str, Any]:
|
||||
"""
|
||||
替换内容中的用户引用格式,包括回复<aaa:bbb>和@<aaa:bbb>格式
|
||||
|
||||
Args:
|
||||
content: 要处理的内容字符串
|
||||
platform: 平台标识
|
||||
name_resolver: 名称解析函数,接收(platform, user_id)参数,返回用户名称
|
||||
如果为None,则使用默认的person_info_manager
|
||||
is_async: 是否为异步模式
|
||||
replace_bot_name: 是否将机器人的user_id替换为"机器人昵称(你)"
|
||||
|
||||
Returns:
|
||||
处理后的内容字符串(同步模式)或awaitable对象(异步模式)
|
||||
"""
|
||||
if is_async:
|
||||
return _replace_user_references_async(content, platform, name_resolver, replace_bot_name)
|
||||
else:
|
||||
return _replace_user_references_sync(content, platform, name_resolver, replace_bot_name)
|
||||
|
||||
|
||||
def _replace_user_references_sync(
|
||||
content: str,
|
||||
platform: str,
|
||||
name_resolver: Optional[Callable[[str, str], str]] = None,
|
||||
replace_bot_name: bool = True
|
||||
) -> str:
|
||||
"""同步版本的用户引用替换"""
|
||||
if name_resolver is None:
|
||||
person_info_manager = get_person_info_manager()
|
||||
def default_resolver(platform: str, user_id: str) -> str:
|
||||
# 检查是否是机器人自己
|
||||
if replace_bot_name and user_id == global_config.bot.qq_account:
|
||||
return f"{global_config.bot.nickname}(你)"
|
||||
person_id = PersonInfoManager.get_person_id(platform, user_id)
|
||||
return person_info_manager.get_value_sync(person_id, "person_name") or user_id
|
||||
name_resolver = default_resolver
|
||||
|
||||
# 处理回复<aaa:bbb>格式
|
||||
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
|
||||
match = re.search(reply_pattern, content)
|
||||
if match:
|
||||
aaa = match.group(1)
|
||||
bbb = match.group(2)
|
||||
try:
|
||||
# 检查是否是机器人自己
|
||||
if replace_bot_name and bbb == global_config.bot.qq_account:
|
||||
reply_person_name = f"{global_config.bot.nickname}(你)"
|
||||
else:
|
||||
reply_person_name = name_resolver(platform, bbb) or aaa
|
||||
content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1)
|
||||
except Exception:
|
||||
# 如果解析失败,使用原始昵称
|
||||
content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1)
|
||||
|
||||
# 处理@<aaa:bbb>格式
|
||||
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
|
||||
at_matches = list(re.finditer(at_pattern, content))
|
||||
if at_matches:
|
||||
new_content = ""
|
||||
last_end = 0
|
||||
for m in at_matches:
|
||||
new_content += content[last_end:m.start()]
|
||||
aaa = m.group(1)
|
||||
bbb = m.group(2)
|
||||
try:
|
||||
# 检查是否是机器人自己
|
||||
if replace_bot_name and bbb == global_config.bot.qq_account:
|
||||
at_person_name = f"{global_config.bot.nickname}(你)"
|
||||
else:
|
||||
at_person_name = name_resolver(platform, bbb) or aaa
|
||||
new_content += f"@{at_person_name}"
|
||||
except Exception:
|
||||
# 如果解析失败,使用原始昵称
|
||||
new_content += f"@{aaa}"
|
||||
last_end = m.end()
|
||||
new_content += content[last_end:]
|
||||
content = new_content
|
||||
|
||||
return content
|
||||
|
||||
|
||||
async def _replace_user_references_async(
|
||||
content: str,
|
||||
platform: str,
|
||||
name_resolver: Optional[Callable[[str, str], Any]] = None,
|
||||
replace_bot_name: bool = True
|
||||
) -> str:
|
||||
"""异步版本的用户引用替换"""
|
||||
if name_resolver is None:
|
||||
person_info_manager = get_person_info_manager()
|
||||
async def default_resolver(platform: str, user_id: str) -> str:
|
||||
# 检查是否是机器人自己
|
||||
if replace_bot_name and user_id == global_config.bot.qq_account:
|
||||
return f"{global_config.bot.nickname}(你)"
|
||||
person_id = PersonInfoManager.get_person_id(platform, user_id)
|
||||
return await person_info_manager.get_value(person_id, "person_name") or user_id
|
||||
name_resolver = default_resolver
|
||||
|
||||
# 处理回复<aaa:bbb>格式
|
||||
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
|
||||
match = re.search(reply_pattern, content)
|
||||
if match:
|
||||
aaa = match.group(1)
|
||||
bbb = match.group(2)
|
||||
try:
|
||||
# 检查是否是机器人自己
|
||||
if replace_bot_name and bbb == global_config.bot.qq_account:
|
||||
reply_person_name = f"{global_config.bot.nickname}(你)"
|
||||
else:
|
||||
reply_person_name = await name_resolver(platform, bbb) or aaa
|
||||
content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1)
|
||||
except Exception:
|
||||
# 如果解析失败,使用原始昵称
|
||||
content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1)
|
||||
|
||||
# 处理@<aaa:bbb>格式
|
||||
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
|
||||
at_matches = list(re.finditer(at_pattern, content))
|
||||
if at_matches:
|
||||
new_content = ""
|
||||
last_end = 0
|
||||
for m in at_matches:
|
||||
new_content += content[last_end:m.start()]
|
||||
aaa = m.group(1)
|
||||
bbb = m.group(2)
|
||||
try:
|
||||
# 检查是否是机器人自己
|
||||
if replace_bot_name and bbb == global_config.bot.qq_account:
|
||||
at_person_name = f"{global_config.bot.nickname}(你)"
|
||||
else:
|
||||
at_person_name = await name_resolver(platform, bbb) or aaa
|
||||
new_content += f"@{at_person_name}"
|
||||
except Exception:
|
||||
# 如果解析失败,使用原始昵称
|
||||
new_content += f"@{aaa}"
|
||||
last_end = m.end()
|
||||
new_content += content[last_end:]
|
||||
content = new_content
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def get_raw_msg_by_timestamp(
|
||||
timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest"
|
||||
) -> List[Dict[str, Any]]:
|
||||
@@ -374,33 +523,8 @@ def _build_readable_messages_internal(
|
||||
else:
|
||||
person_name = "某人"
|
||||
|
||||
# 检查是否有 回复<aaa:bbb> 字段
|
||||
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
|
||||
match = re.search(reply_pattern, content)
|
||||
if match:
|
||||
aaa: str = match[1]
|
||||
bbb: str = match[2]
|
||||
reply_person_id = PersonInfoManager.get_person_id(platform, bbb)
|
||||
reply_person_name = person_info_manager.get_value_sync(reply_person_id, "person_name") or aaa
|
||||
# 在内容前加上回复信息
|
||||
content = re.sub(reply_pattern, lambda m, name=reply_person_name: f"回复 {name}", content, count=1)
|
||||
|
||||
# 检查是否有 @<aaa:bbb> 字段 @<{member_info.get('nickname')}:{member_info.get('user_id')}>
|
||||
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
|
||||
at_matches = list(re.finditer(at_pattern, content))
|
||||
if at_matches:
|
||||
new_content = ""
|
||||
last_end = 0
|
||||
for m in at_matches:
|
||||
new_content += content[last_end : m.start()]
|
||||
aaa = m.group(1)
|
||||
bbb = m.group(2)
|
||||
at_person_id = PersonInfoManager.get_person_id(platform, bbb)
|
||||
at_person_name = person_info_manager.get_value_sync(at_person_id, "person_name") or aaa
|
||||
new_content += f"@{at_person_name}"
|
||||
last_end = m.end()
|
||||
new_content += content[last_end:]
|
||||
content = new_content
|
||||
# 使用独立函数处理用户引用格式
|
||||
content = replace_user_references_in_content(content, platform, is_async=False, replace_bot_name=replace_bot_name)
|
||||
|
||||
target_str = "这是QQ的一个功能,用于提及某人,但没那么明显"
|
||||
if target_str in content and random.random() < 0.6:
|
||||
@@ -916,38 +1040,14 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str:
|
||||
anon_name = get_anon_name(platform, user_id)
|
||||
# print(f"anon_name:{anon_name}")
|
||||
|
||||
# 处理 回复<aaa:bbb>
|
||||
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
|
||||
match = re.search(reply_pattern, content)
|
||||
if match:
|
||||
# print(f"发现回复match:{match}")
|
||||
bbb = match.group(2)
|
||||
# 使用独立函数处理用户引用格式,传入自定义的匿名名称解析器
|
||||
def anon_name_resolver(platform: str, user_id: str) -> str:
|
||||
try:
|
||||
anon_reply = get_anon_name(platform, bbb)
|
||||
# print(f"anon_reply:{anon_reply}")
|
||||
return get_anon_name(platform, user_id)
|
||||
except Exception:
|
||||
anon_reply = "?"
|
||||
content = re.sub(reply_pattern, f"回复 {anon_reply}", content, count=1)
|
||||
|
||||
# 处理 @<aaa:bbb>,无嵌套def
|
||||
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
|
||||
at_matches = list(re.finditer(at_pattern, content))
|
||||
if at_matches:
|
||||
# print(f"发现@match:{at_matches}")
|
||||
new_content = ""
|
||||
last_end = 0
|
||||
for m in at_matches:
|
||||
new_content += content[last_end : m.start()]
|
||||
bbb = m.group(2)
|
||||
try:
|
||||
anon_at = get_anon_name(platform, bbb)
|
||||
# print(f"anon_at:{anon_at}")
|
||||
except Exception:
|
||||
anon_at = "?"
|
||||
new_content += f"@{anon_at}"
|
||||
last_end = m.end()
|
||||
new_content += content[last_end:]
|
||||
content = new_content
|
||||
return "?"
|
||||
|
||||
content = replace_user_references_in_content(content, platform, anon_name_resolver, is_async=False, replace_bot_name=False)
|
||||
|
||||
header = f"{anon_name}说 "
|
||||
output_lines.append(header)
|
||||
|
||||
@@ -78,7 +78,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
|
||||
# print(f"is_mentioned: {is_mentioned}")
|
||||
# print(f"is_at: {is_at}")
|
||||
|
||||
if is_at and global_config.normal_chat.at_bot_inevitable_reply:
|
||||
if is_at and global_config.chat.at_bot_inevitable_reply:
|
||||
reply_probability = 1.0
|
||||
logger.debug("被@,回复概率设置为100%")
|
||||
else:
|
||||
@@ -103,7 +103,7 @@ def is_mentioned_bot_in_message(message: MessageRecv) -> tuple[bool, float]:
|
||||
for nickname in nicknames:
|
||||
if nickname in message_content:
|
||||
is_mentioned = True
|
||||
if is_mentioned and global_config.normal_chat.mentioned_bot_inevitable_reply:
|
||||
if is_mentioned and global_config.chat.mentioned_bot_inevitable_reply:
|
||||
reply_probability = 1.0
|
||||
logger.debug("被提及,回复概率设置为100%")
|
||||
return is_mentioned, reply_probability
|
||||
|
||||
@@ -35,7 +35,7 @@ class ClassicalWillingManager(BaseWillingManager):
|
||||
if interested_rate > 0.2:
|
||||
current_willing += interested_rate - 0.2
|
||||
|
||||
if willing_info.is_mentioned_bot and global_config.normal_chat.mentioned_bot_inevitable_reply and current_willing < 2:
|
||||
if willing_info.is_mentioned_bot and global_config.chat.mentioned_bot_inevitable_reply and current_willing < 2:
|
||||
current_willing += 1 if current_willing < 1.0 else 0.05
|
||||
|
||||
self.chat_reply_willing[chat_id] = min(current_willing, 1.0)
|
||||
|
||||
@@ -390,7 +390,7 @@ MODULE_COLORS = {
|
||||
"tts_action": "\033[38;5;58m", # 深黄色
|
||||
"doubao_pic_plugin": "\033[38;5;64m", # 深绿色
|
||||
# Action组件
|
||||
"no_reply_action": "\033[38;5;196m", # 亮红色,更显眼
|
||||
"no_reply_action": "\033[38;5;214m", # 亮橙色,显眼但不像警告
|
||||
"reply_action": "\033[38;5;46m", # 亮绿色
|
||||
"base_action": "\033[38;5;250m", # 浅灰色
|
||||
# 数据库和消息
|
||||
|
||||
@@ -49,7 +49,7 @@ TEMPLATE_DIR = os.path.join(PROJECT_ROOT, "template")
|
||||
|
||||
# 考虑到,实际上配置文件中的mai_version是不会自动更新的,所以采用硬编码
|
||||
# 对该字段的更新,请严格参照语义化版本规范:https://semver.org/lang/zh-CN/
|
||||
MMC_VERSION = "0.9.0"
|
||||
MMC_VERSION = "0.9.1"
|
||||
|
||||
|
||||
def get_key_comment(toml_table, key):
|
||||
|
||||
@@ -84,6 +84,12 @@ class ChatConfig(ConfigBase):
|
||||
use_s4u_prompt_mode: bool = False
|
||||
"""是否使用 s4u 对话构建模式,该模式会分开处理当前对话对象和其他所有对话的内容进行 prompt 构建"""
|
||||
|
||||
mentioned_bot_inevitable_reply: bool = False
|
||||
"""提及 bot 必然回复"""
|
||||
|
||||
at_bot_inevitable_reply: bool = False
|
||||
"""@bot 必然回复"""
|
||||
|
||||
# 修改:基于时段的回复频率配置,改为数组格式
|
||||
time_based_talk_frequency: list[str] = field(default_factory=lambda: [])
|
||||
"""
|
||||
@@ -270,11 +276,7 @@ class NormalChatConfig(ConfigBase):
|
||||
response_interested_rate_amplifier: float = 1.0
|
||||
"""回复兴趣度放大系数"""
|
||||
|
||||
mentioned_bot_inevitable_reply: bool = False
|
||||
"""提及 bot 必然回复"""
|
||||
|
||||
at_bot_inevitable_reply: bool = False
|
||||
"""@bot 必然回复"""
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -406,15 +408,9 @@ class MoodConfig(ConfigBase):
|
||||
|
||||
enable_mood: bool = False
|
||||
"""是否启用情绪系统"""
|
||||
|
||||
mood_update_interval: int = 1
|
||||
"""情绪更新间隔(秒)"""
|
||||
|
||||
mood_decay_rate: float = 0.95
|
||||
"""情绪衰减率"""
|
||||
|
||||
mood_intensity_factor: float = 0.7
|
||||
"""情绪强度因子"""
|
||||
|
||||
mood_update_threshold: float = 1.0
|
||||
"""情绪更新阈值,越高,更新越慢"""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -10,6 +10,7 @@ import base64
|
||||
from PIL import Image
|
||||
import io
|
||||
import os
|
||||
import copy # 添加copy模块用于深拷贝
|
||||
from src.common.database.database import db # 确保 db 被导入用于 create_tables
|
||||
from src.common.database.database_model import LLMUsage # 导入 LLMUsage 模型
|
||||
from src.config.config import global_config
|
||||
@@ -69,23 +70,28 @@ error_code_mapping = {
|
||||
|
||||
|
||||
async def _safely_record(request_content: Dict[str, Any], payload: Dict[str, Any]):
|
||||
"""安全地记录请求体,用于调试日志,不会修改原始payload对象"""
|
||||
# 创建payload的深拷贝,避免修改原始对象
|
||||
safe_payload = copy.deepcopy(payload)
|
||||
|
||||
image_base64: str = request_content.get("image_base64")
|
||||
image_format: str = request_content.get("image_format")
|
||||
if (
|
||||
image_base64
|
||||
and payload
|
||||
and isinstance(payload, dict)
|
||||
and "messages" in payload
|
||||
and len(payload["messages"]) > 0
|
||||
and safe_payload
|
||||
and isinstance(safe_payload, dict)
|
||||
and "messages" in safe_payload
|
||||
and len(safe_payload["messages"]) > 0
|
||||
):
|
||||
if isinstance(payload["messages"][0], dict) and "content" in payload["messages"][0]:
|
||||
content = payload["messages"][0]["content"]
|
||||
if isinstance(safe_payload["messages"][0], dict) and "content" in safe_payload["messages"][0]:
|
||||
content = safe_payload["messages"][0]["content"]
|
||||
if isinstance(content, list) and len(content) > 1 and "image_url" in content[1]:
|
||||
payload["messages"][0]["content"][1]["image_url"]["url"] = (
|
||||
# 只修改拷贝的对象,用于安全的日志记录
|
||||
safe_payload["messages"][0]["content"][1]["image_url"]["url"] = (
|
||||
f"data:image/{image_format.lower() if image_format else 'jpeg'};base64,"
|
||||
f"{image_base64[:10]}...{image_base64[-10:]}"
|
||||
)
|
||||
return payload
|
||||
return safe_payload
|
||||
|
||||
|
||||
class LLMRequest:
|
||||
@@ -109,10 +115,15 @@ class LLMRequest:
|
||||
|
||||
def __init__(self, model: dict, **kwargs):
|
||||
# 将大写的配置键转换为小写并从config中获取实际值
|
||||
logger.debug(f"🔍 [模型初始化] 开始初始化模型: {model.get('name', 'Unknown')}")
|
||||
logger.debug(f"🔍 [模型初始化] 模型配置: {model}")
|
||||
logger.debug(f"🔍 [模型初始化] 额外参数: {kwargs}")
|
||||
|
||||
try:
|
||||
# print(f"model['provider']: {model['provider']}")
|
||||
self.api_key = os.environ[f"{model['provider']}_KEY"]
|
||||
self.base_url = os.environ[f"{model['provider']}_BASE_URL"]
|
||||
logger.debug(f"🔍 [模型初始化] 成功获取环境变量: {model['provider']}_KEY 和 {model['provider']}_BASE_URL")
|
||||
except AttributeError as e:
|
||||
logger.error(f"原始 model dict 信息:{model}")
|
||||
logger.error(f"配置错误:找不到对应的配置项 - {str(e)}")
|
||||
@@ -124,6 +135,10 @@ class LLMRequest:
|
||||
self.model_name: str = model["name"]
|
||||
self.params = kwargs
|
||||
|
||||
# 记录配置文件中声明了哪些参数(不管值是什么)
|
||||
self.has_enable_thinking = "enable_thinking" in model
|
||||
self.has_thinking_budget = "thinking_budget" in model
|
||||
|
||||
self.enable_thinking = model.get("enable_thinking", False)
|
||||
self.temp = model.get("temp", 0.7)
|
||||
self.thinking_budget = model.get("thinking_budget", 4096)
|
||||
@@ -132,12 +147,24 @@ class LLMRequest:
|
||||
self.pri_out = model.get("pri_out", 0)
|
||||
self.max_tokens = model.get("max_tokens", global_config.model.model_max_output_length)
|
||||
# print(f"max_tokens: {self.max_tokens}")
|
||||
|
||||
logger.debug("🔍 [模型初始化] 模型参数设置完成:")
|
||||
logger.debug(f" - model_name: {self.model_name}")
|
||||
logger.debug(f" - has_enable_thinking: {self.has_enable_thinking}")
|
||||
logger.debug(f" - enable_thinking: {self.enable_thinking}")
|
||||
logger.debug(f" - has_thinking_budget: {self.has_thinking_budget}")
|
||||
logger.debug(f" - thinking_budget: {self.thinking_budget}")
|
||||
logger.debug(f" - temp: {self.temp}")
|
||||
logger.debug(f" - stream: {self.stream}")
|
||||
logger.debug(f" - max_tokens: {self.max_tokens}")
|
||||
logger.debug(f" - base_url: {self.base_url}")
|
||||
|
||||
# 获取数据库实例
|
||||
self._init_database()
|
||||
|
||||
# 从 kwargs 中提取 request_type,如果没有提供则默认为 "default"
|
||||
self.request_type = kwargs.pop("request_type", "default")
|
||||
logger.debug(f"🔍 [模型初始化] 初始化完成,request_type: {self.request_type}")
|
||||
|
||||
@staticmethod
|
||||
def _init_database():
|
||||
@@ -262,11 +289,12 @@ class LLMRequest:
|
||||
if self.temp != 0.7:
|
||||
payload["temperature"] = self.temp
|
||||
|
||||
# 添加enable_thinking参数(如果不是默认值False)
|
||||
if not self.enable_thinking:
|
||||
payload["enable_thinking"] = False
|
||||
# 添加enable_thinking参数(只有配置文件中声明了才添加,不管值是true还是false)
|
||||
if self.has_enable_thinking:
|
||||
payload["enable_thinking"] = self.enable_thinking
|
||||
|
||||
if self.thinking_budget != 4096:
|
||||
# 添加thinking_budget参数(只有配置文件中声明了才添加)
|
||||
if self.has_thinking_budget:
|
||||
payload["thinking_budget"] = self.thinking_budget
|
||||
|
||||
if self.max_tokens:
|
||||
@@ -334,6 +362,19 @@ class LLMRequest:
|
||||
# 似乎是openai流式必须要的东西,不过阿里云的qwq-plus加了这个没有影响
|
||||
if request_content["stream_mode"]:
|
||||
headers["Accept"] = "text/event-stream"
|
||||
|
||||
# 添加请求发送前的调试信息
|
||||
logger.debug(f"🔍 [请求调试] 模型 {self.model_name} 准备发送请求")
|
||||
logger.debug(f"🔍 [请求调试] API URL: {request_content['api_url']}")
|
||||
logger.debug(f"🔍 [请求调试] 请求头: {await self._build_headers(no_key=True, is_formdata=file_bytes is not None)}")
|
||||
|
||||
if not file_bytes:
|
||||
# 安全地记录请求体(隐藏敏感信息)
|
||||
safe_payload = await _safely_record(request_content, request_content["payload"])
|
||||
logger.debug(f"🔍 [请求调试] 请求体: {json.dumps(safe_payload, indent=2, ensure_ascii=False)}")
|
||||
else:
|
||||
logger.debug(f"🔍 [请求调试] 文件上传请求,文件格式: {request_content['file_format']}")
|
||||
|
||||
async with aiohttp.ClientSession(connector=await get_tcp_connector()) as session:
|
||||
post_kwargs = {"headers": headers}
|
||||
# form-data数据上传方式不同
|
||||
@@ -491,7 +532,36 @@ class LLMRequest:
|
||||
logger.warning(f"模型 {self.model_name} 请求限制(429),等待{wait_time}秒后重试...")
|
||||
raise RuntimeError("请求限制(429)")
|
||||
elif response.status in policy["abort_codes"]:
|
||||
if response.status != 403:
|
||||
# 特别处理400错误,添加详细调试信息
|
||||
if response.status == 400:
|
||||
logger.error(f"🔍 [调试信息] 模型 {self.model_name} 参数错误 (400) - 开始详细诊断")
|
||||
logger.error(f"🔍 [调试信息] 模型名称: {self.model_name}")
|
||||
logger.error(f"🔍 [调试信息] API地址: {self.base_url}")
|
||||
logger.error("🔍 [调试信息] 模型配置参数:")
|
||||
logger.error(f" - enable_thinking: {self.enable_thinking}")
|
||||
logger.error(f" - temp: {self.temp}")
|
||||
logger.error(f" - thinking_budget: {self.thinking_budget}")
|
||||
logger.error(f" - stream: {self.stream}")
|
||||
logger.error(f" - max_tokens: {self.max_tokens}")
|
||||
logger.error(f" - pri_in: {self.pri_in}")
|
||||
logger.error(f" - pri_out: {self.pri_out}")
|
||||
logger.error(f"🔍 [调试信息] 原始params: {self.params}")
|
||||
|
||||
# 尝试获取服务器返回的详细错误信息
|
||||
try:
|
||||
error_text = await response.text()
|
||||
logger.error(f"🔍 [调试信息] 服务器返回的原始错误内容: {error_text}")
|
||||
|
||||
try:
|
||||
error_json = json.loads(error_text)
|
||||
logger.error(f"🔍 [调试信息] 解析后的错误JSON: {json.dumps(error_json, indent=2, ensure_ascii=False)}")
|
||||
except json.JSONDecodeError:
|
||||
logger.error("🔍 [调试信息] 错误响应不是有效的JSON格式")
|
||||
except Exception as e:
|
||||
logger.error(f"🔍 [调试信息] 无法读取错误响应内容: {str(e)}")
|
||||
|
||||
raise RequestAbortException("参数错误,请检查调试信息", response)
|
||||
elif response.status != 403:
|
||||
raise RequestAbortException("请求出现错误,中断处理", response)
|
||||
else:
|
||||
raise PermissionDeniedException("模型禁止访问")
|
||||
@@ -510,6 +580,19 @@ class LLMRequest:
|
||||
logger.error(
|
||||
f"模型 {self.model_name} 错误码: {response.status} - {error_code_mapping.get(response.status)}"
|
||||
)
|
||||
|
||||
# 如果是400错误,额外输出请求体信息用于调试
|
||||
if response.status == 400:
|
||||
logger.error("🔍 [异常调试] 400错误 - 请求体调试信息:")
|
||||
try:
|
||||
safe_payload = await _safely_record(request_content, payload)
|
||||
logger.error(f"🔍 [异常调试] 发送的请求体: {json.dumps(safe_payload, indent=2, ensure_ascii=False)}")
|
||||
except Exception as debug_error:
|
||||
logger.error(f"🔍 [异常调试] 无法安全记录请求体: {str(debug_error)}")
|
||||
logger.error(f"🔍 [异常调试] 原始payload类型: {type(payload)}")
|
||||
if isinstance(payload, dict):
|
||||
logger.error(f"🔍 [异常调试] 原始payload键: {list(payload.keys())}")
|
||||
|
||||
# print(request_content)
|
||||
# print(response)
|
||||
# 尝试获取并记录服务器返回的详细错误信息
|
||||
@@ -654,14 +737,27 @@ class LLMRequest:
|
||||
"""
|
||||
# 复制一份参数,避免直接修改原始数据
|
||||
new_params = dict(params)
|
||||
|
||||
logger.debug(f"🔍 [参数转换] 模型 {self.model_name} 开始参数转换")
|
||||
logger.debug(f"🔍 [参数转换] 是否为CoT模型: {self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION}")
|
||||
logger.debug(f"🔍 [参数转换] CoT模型列表: {self.MODELS_NEEDING_TRANSFORMATION}")
|
||||
|
||||
if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION:
|
||||
logger.debug("🔍 [参数转换] 检测到CoT模型,开始参数转换")
|
||||
# 删除 'temperature' 参数(如果存在),但避免删除我们在_build_payload中添加的自定义温度
|
||||
if "temperature" in new_params and new_params["temperature"] == 0.7:
|
||||
new_params.pop("temperature")
|
||||
removed_temp = new_params.pop("temperature")
|
||||
logger.debug(f"🔍 [参数转换] 移除默认temperature参数: {removed_temp}")
|
||||
# 如果存在 'max_tokens',则重命名为 'max_completion_tokens'
|
||||
if "max_tokens" in new_params:
|
||||
old_value = new_params["max_tokens"]
|
||||
new_params["max_completion_tokens"] = new_params.pop("max_tokens")
|
||||
logger.debug(f"🔍 [参数转换] 参数重命名: max_tokens({old_value}) -> max_completion_tokens({new_params['max_completion_tokens']})")
|
||||
else:
|
||||
logger.debug("🔍 [参数转换] 非CoT模型,无需参数转换")
|
||||
|
||||
logger.debug(f"🔍 [参数转换] 转换前参数: {params}")
|
||||
logger.debug(f"🔍 [参数转换] 转换后参数: {new_params}")
|
||||
return new_params
|
||||
|
||||
async def _build_formdata_payload(self, file_bytes: bytes, file_format: str) -> aiohttp.FormData:
|
||||
@@ -693,7 +789,12 @@ class LLMRequest:
|
||||
async def _build_payload(self, prompt: str, image_base64: str = None, image_format: str = None) -> dict:
|
||||
"""构建请求体"""
|
||||
# 复制一份参数,避免直接修改 self.params
|
||||
logger.debug(f"🔍 [参数构建] 模型 {self.model_name} 开始构建请求体")
|
||||
logger.debug(f"🔍 [参数构建] 原始self.params: {self.params}")
|
||||
|
||||
params_copy = await self._transform_parameters(self.params)
|
||||
logger.debug(f"🔍 [参数构建] 转换后的params_copy: {params_copy}")
|
||||
|
||||
if image_base64:
|
||||
messages = [
|
||||
{
|
||||
@@ -715,26 +816,37 @@ class LLMRequest:
|
||||
"messages": messages,
|
||||
**params_copy,
|
||||
}
|
||||
|
||||
logger.debug(f"🔍 [参数构建] 基础payload构建完成: {list(payload.keys())}")
|
||||
|
||||
# 添加temp参数(如果不是默认值0.7)
|
||||
if self.temp != 0.7:
|
||||
payload["temperature"] = self.temp
|
||||
logger.debug(f"🔍 [参数构建] 添加temperature参数: {self.temp}")
|
||||
|
||||
# 添加enable_thinking参数(如果不是默认值False)
|
||||
if not self.enable_thinking:
|
||||
payload["enable_thinking"] = False
|
||||
# 添加enable_thinking参数(只有配置文件中声明了才添加,不管值是true还是false)
|
||||
if self.has_enable_thinking:
|
||||
payload["enable_thinking"] = self.enable_thinking
|
||||
logger.debug(f"🔍 [参数构建] 添加enable_thinking参数: {self.enable_thinking}")
|
||||
|
||||
if self.thinking_budget != 4096:
|
||||
# 添加thinking_budget参数(只有配置文件中声明了才添加)
|
||||
if self.has_thinking_budget:
|
||||
payload["thinking_budget"] = self.thinking_budget
|
||||
logger.debug(f"🔍 [参数构建] 添加thinking_budget参数: {self.thinking_budget}")
|
||||
|
||||
if self.max_tokens:
|
||||
payload["max_tokens"] = self.max_tokens
|
||||
logger.debug(f"🔍 [参数构建] 添加max_tokens参数: {self.max_tokens}")
|
||||
|
||||
# if "max_tokens" not in payload and "max_completion_tokens" not in payload:
|
||||
# payload["max_tokens"] = global_config.model.model_max_output_length
|
||||
# 如果 payload 中依然存在 max_tokens 且需要转换,在这里进行再次检查
|
||||
if self.model_name.lower() in self.MODELS_NEEDING_TRANSFORMATION and "max_tokens" in payload:
|
||||
old_value = payload["max_tokens"]
|
||||
payload["max_completion_tokens"] = payload.pop("max_tokens")
|
||||
logger.debug(f"🔍 [参数构建] CoT模型参数转换: max_tokens({old_value}) -> max_completion_tokens({payload['max_completion_tokens']})")
|
||||
|
||||
logger.debug(f"🔍 [参数构建] 最终payload键列表: {list(payload.keys())}")
|
||||
return payload
|
||||
|
||||
def _default_response_handler(
|
||||
|
||||
@@ -47,11 +47,35 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
|
||||
logger.debug(f"记忆激活率: {interested_rate:.2f}")
|
||||
|
||||
text_len = len(message.processed_plain_text)
|
||||
# 根据文本长度调整兴趣度,长度越大兴趣度越高,但增长率递减,最低0.01,最高0.05
|
||||
# 采用对数函数实现递减增长
|
||||
|
||||
base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1))
|
||||
base_interest = min(max(base_interest, 0.01), 0.05)
|
||||
# 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算
|
||||
# 基于实际分布:0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%)
|
||||
|
||||
if text_len == 0:
|
||||
base_interest = 0.01 # 空消息最低兴趣度
|
||||
elif text_len <= 5:
|
||||
# 1-5字符:线性增长 0.01 -> 0.03
|
||||
base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4
|
||||
elif text_len <= 10:
|
||||
# 6-10字符:线性增长 0.03 -> 0.06
|
||||
base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5
|
||||
elif text_len <= 20:
|
||||
# 11-20字符:线性增长 0.06 -> 0.12
|
||||
base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10
|
||||
elif text_len <= 30:
|
||||
# 21-30字符:线性增长 0.12 -> 0.18
|
||||
base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10
|
||||
elif text_len <= 50:
|
||||
# 31-50字符:线性增长 0.18 -> 0.22
|
||||
base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20
|
||||
elif text_len <= 100:
|
||||
# 51-100字符:线性增长 0.22 -> 0.26
|
||||
base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50
|
||||
else:
|
||||
# 100+字符:对数增长 0.26 -> 0.3,增长率递减
|
||||
base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901
|
||||
|
||||
# 确保在范围内
|
||||
base_interest = min(max(base_interest, 0.01), 0.3)
|
||||
|
||||
interested_rate += base_interest
|
||||
|
||||
|
||||
@@ -78,12 +78,12 @@ class ChatMood:
|
||||
if interested_rate <= 0:
|
||||
interest_multiplier = 0
|
||||
else:
|
||||
interest_multiplier = 3 * math.pow(interested_rate, 0.25)
|
||||
interest_multiplier = 2 * math.pow(interested_rate, 0.25)
|
||||
|
||||
logger.debug(
|
||||
f"base_probability: {base_probability}, time_multiplier: {time_multiplier}, interest_multiplier: {interest_multiplier}"
|
||||
)
|
||||
update_probability = min(1.0, base_probability * time_multiplier * interest_multiplier)
|
||||
update_probability = global_config.mood.mood_update_threshold * min(1.0, base_probability * time_multiplier * interest_multiplier)
|
||||
|
||||
if random.random() > update_probability:
|
||||
return
|
||||
|
||||
@@ -419,7 +419,7 @@ class RelationshipBuilder:
|
||||
async def update_impression_on_segments(self, person_id: str, chat_id: str, segments: List[Dict[str, Any]]):
|
||||
"""基于消息段更新用户印象"""
|
||||
original_segment_count = len(segments)
|
||||
logger.info(f"开始为 {person_id} 基于 {original_segment_count} 个消息段更新印象")
|
||||
logger.debug(f"开始为 {person_id} 基于 {original_segment_count} 个消息段更新印象")
|
||||
try:
|
||||
# 筛选要处理的消息段,每个消息段有10%的概率被丢弃
|
||||
segments_to_process = [s for s in segments if random.random() >= 0.1]
|
||||
@@ -432,7 +432,7 @@ class RelationshipBuilder:
|
||||
|
||||
dropped_count = original_segment_count - len(segments_to_process)
|
||||
if dropped_count > 0:
|
||||
logger.info(f"为 {person_id} 随机丢弃了 {dropped_count} / {original_segment_count} 个消息段")
|
||||
logger.debug(f"为 {person_id} 随机丢弃了 {dropped_count} / {original_segment_count} 个消息段")
|
||||
|
||||
processed_messages = []
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
await send_api.custom_message("video", video_data, "123456", True)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import traceback
|
||||
import time
|
||||
import difflib
|
||||
@@ -30,7 +31,7 @@ from src.common.logger import get_logger
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.chat.message_receive.uni_message_sender import HeartFCSender
|
||||
from src.chat.message_receive.message import MessageSending, MessageRecv
|
||||
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat
|
||||
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, replace_user_references_in_content
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
from maim_message import Seg, UserInfo
|
||||
from src.config.config import global_config
|
||||
@@ -183,32 +184,8 @@ async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageR
|
||||
if person_name == sender:
|
||||
translate_text = message["processed_plain_text"]
|
||||
|
||||
# 检查是否有 回复<aaa:bbb> 字段
|
||||
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
|
||||
if match := re.search(reply_pattern, translate_text):
|
||||
aaa = match.group(1)
|
||||
bbb = match.group(2)
|
||||
reply_person_id = get_person_info_manager().get_person_id(platform, bbb)
|
||||
reply_person_name = await get_person_info_manager().get_value(reply_person_id, "person_name") or aaa
|
||||
# 在内容前加上回复信息
|
||||
translate_text = re.sub(reply_pattern, f"回复 {reply_person_name}", translate_text, count=1)
|
||||
|
||||
# 检查是否有 @<aaa:bbb> 字段
|
||||
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
|
||||
at_matches = list(re.finditer(at_pattern, translate_text))
|
||||
if at_matches:
|
||||
new_content = ""
|
||||
last_end = 0
|
||||
for m in at_matches:
|
||||
new_content += translate_text[last_end : m.start()]
|
||||
aaa = m.group(1)
|
||||
bbb = m.group(2)
|
||||
at_person_id = get_person_info_manager().get_person_id(platform, bbb)
|
||||
at_person_name = await get_person_info_manager().get_value(at_person_id, "person_name") or aaa
|
||||
new_content += f"@{at_person_name}"
|
||||
last_end = m.end()
|
||||
new_content += translate_text[last_end:]
|
||||
translate_text = new_content
|
||||
# 使用独立函数处理用户引用格式
|
||||
translate_text = await replace_user_references_in_content(translate_text, platform, is_async=True)
|
||||
|
||||
similarity = difflib.SequenceMatcher(None, text, translate_text).ratio()
|
||||
if similarity >= 0.9:
|
||||
|
||||
@@ -49,12 +49,10 @@ class BaseAction(ABC):
|
||||
reasoning: 执行该动作的理由
|
||||
cycle_timers: 计时器字典
|
||||
thinking_id: 思考ID
|
||||
expressor: 表达器对象
|
||||
replyer: 回复器对象
|
||||
chat_stream: 聊天流对象
|
||||
log_prefix: 日志前缀
|
||||
shutting_down: 是否正在关闭
|
||||
plugin_config: 插件配置字典
|
||||
action_message: 消息数据
|
||||
**kwargs: 其他参数
|
||||
"""
|
||||
if plugin_config is None:
|
||||
@@ -414,23 +412,11 @@ class BaseAction(ABC):
|
||||
"""
|
||||
return await self.execute()
|
||||
|
||||
# def get_action_context(self, key: str, default=None):
|
||||
# """获取action上下文信息
|
||||
|
||||
# Args:
|
||||
# key: 上下文键名
|
||||
# default: 默认值
|
||||
|
||||
# Returns:
|
||||
# Any: 上下文值或默认值
|
||||
# """
|
||||
# return self.api.get_action_context(key, default)
|
||||
|
||||
def get_config(self, key: str, default=None):
|
||||
"""获取插件配置值,支持嵌套键访问
|
||||
"""获取插件配置值,使用嵌套键访问
|
||||
|
||||
Args:
|
||||
key: 配置键名,支持嵌套访问如 "section.subsection.key"
|
||||
key: 配置键名,使用嵌套访问如 "section.subsection.key"
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Tuple, Optional, List
|
||||
from typing import Dict, Tuple, Optional
|
||||
from src.common.logger import get_logger
|
||||
from src.plugin_system.base.component_types import CommandInfo, ComponentType
|
||||
from src.chat.message_receive.message import MessageRecv
|
||||
@@ -17,7 +17,6 @@ class BaseCommand(ABC):
|
||||
- command_pattern: 命令匹配的正则表达式
|
||||
- command_help: 命令帮助信息
|
||||
- command_examples: 命令使用示例列表
|
||||
- intercept_message: 是否拦截消息处理(默认True拦截,False继续传递)
|
||||
"""
|
||||
|
||||
command_name: str = ""
|
||||
@@ -27,11 +26,6 @@ class BaseCommand(ABC):
|
||||
# 默认命令设置
|
||||
command_pattern: str = r""
|
||||
"""命令匹配的正则表达式"""
|
||||
command_help: str = ""
|
||||
"""命令帮助信息"""
|
||||
command_examples: List[str] = []
|
||||
intercept_message: bool = True
|
||||
"""是否拦截信息,默认拦截,不进行后续处理"""
|
||||
|
||||
def __init__(self, message: MessageRecv, plugin_config: Optional[dict] = None):
|
||||
"""初始化Command组件
|
||||
@@ -57,19 +51,19 @@ class BaseCommand(ABC):
|
||||
self.matched_groups = groups
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self) -> Tuple[bool, Optional[str]]:
|
||||
async def execute(self) -> Tuple[bool, Optional[str], bool]:
|
||||
"""执行Command的抽象方法,子类必须实现
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (是否执行成功, 可选的回复消息)
|
||||
Tuple[bool, Optional[str], bool]: (是否执行成功, 可选的回复消息, 是否拦截消息 不进行 后续处理)
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_config(self, key: str, default=None):
|
||||
"""获取插件配置值,支持嵌套键访问
|
||||
"""获取插件配置值,使用嵌套键访问
|
||||
|
||||
Args:
|
||||
key: 配置键名,支持嵌套访问如 "section.subsection.key"
|
||||
key: 配置键名,使用嵌套访问如 "section.subsection.key"
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
@@ -231,7 +225,4 @@ class BaseCommand(ABC):
|
||||
component_type=ComponentType.COMMAND,
|
||||
description=cls.command_description,
|
||||
command_pattern=cls.command_pattern,
|
||||
command_help=cls.command_help,
|
||||
command_examples=cls.command_examples.copy() if cls.command_examples else [],
|
||||
intercept_message=cls.intercept_message,
|
||||
)
|
||||
|
||||
@@ -140,14 +140,9 @@ class CommandInfo(ComponentInfo):
|
||||
"""命令组件信息"""
|
||||
|
||||
command_pattern: str = "" # 命令匹配模式(正则表达式)
|
||||
command_help: str = "" # 命令帮助信息
|
||||
command_examples: List[str] = field(default_factory=list) # 命令使用示例
|
||||
intercept_message: bool = True # 是否拦截消息处理(默认拦截)
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.command_examples is None:
|
||||
self.command_examples = []
|
||||
self.component_type = ComponentType.COMMAND
|
||||
|
||||
|
||||
|
||||
@@ -182,17 +182,17 @@ class EventsManager:
|
||||
|
||||
async def cancel_handler_tasks(self, handler_name: str) -> None:
|
||||
tasks_to_be_cancelled = self._handler_tasks.get(handler_name, [])
|
||||
remaining_tasks = [task for task in tasks_to_be_cancelled if not task.done()]
|
||||
for task in remaining_tasks:
|
||||
task.cancel()
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5)
|
||||
logger.info(f"已取消事件处理器 {handler_name} 的所有任务")
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消")
|
||||
except Exception as e:
|
||||
logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}")
|
||||
finally:
|
||||
if remaining_tasks := [task for task in tasks_to_be_cancelled if not task.done()]:
|
||||
for task in remaining_tasks:
|
||||
task.cancel()
|
||||
try:
|
||||
await asyncio.wait_for(asyncio.gather(*remaining_tasks, return_exceptions=True), timeout=5)
|
||||
logger.info(f"已取消事件处理器 {handler_name} 的所有任务")
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"取消事件处理器 {handler_name} 的任务超时,开始强制取消")
|
||||
except Exception as e:
|
||||
logger.error(f"取消事件处理器 {handler_name} 的任务时发生异常: {e}")
|
||||
if handler_name in self._handler_tasks:
|
||||
del self._handler_tasks[handler_name]
|
||||
|
||||
async def unregister_event_subscriber(self, handler_name: str) -> bool:
|
||||
|
||||
@@ -163,12 +163,11 @@ class VersionComparator:
|
||||
version_normalized, max_normalized
|
||||
)
|
||||
|
||||
if is_compatible:
|
||||
logger.info(f"版本兼容性检查:{compat_msg}")
|
||||
return True, compat_msg
|
||||
else:
|
||||
if not is_compatible:
|
||||
return False, f"版本 {version_normalized} 高于最大支持版本 {max_normalized},且无兼容性映射"
|
||||
|
||||
logger.info(f"版本兼容性检查:{compat_msg}")
|
||||
return True, compat_msg
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
@@ -358,14 +357,10 @@ class ManifestValidator:
|
||||
|
||||
if self.validation_errors:
|
||||
report.append("❌ 验证错误:")
|
||||
for error in self.validation_errors:
|
||||
report.append(f" - {error}")
|
||||
|
||||
report.extend(f" - {error}" for error in self.validation_errors)
|
||||
if self.validation_warnings:
|
||||
report.append("⚠️ 验证警告:")
|
||||
for warning in self.validation_warnings:
|
||||
report.append(f" - {warning}")
|
||||
|
||||
report.extend(f" - {warning}" for warning in self.validation_warnings)
|
||||
if not self.validation_errors and not self.validation_warnings:
|
||||
report.append("✅ Manifest文件验证通过")
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ class EmojiAction(BaseAction):
|
||||
"""表情动作 - 发送表情包"""
|
||||
|
||||
# 激活设置
|
||||
focus_activation_type = ActionActivationType.RANDOM
|
||||
normal_activation_type = ActionActivationType.RANDOM
|
||||
activation_type = ActionActivationType.RANDOM
|
||||
mode_enable = ChatMode.ALL
|
||||
parallel_action = True
|
||||
random_activation_probability = 0.2 # 默认值,可通过配置覆盖
|
||||
|
||||
@@ -66,13 +66,12 @@ class CoreActionsPlugin(BasePlugin):
|
||||
|
||||
if global_config.emoji.emoji_activate_type == "llm":
|
||||
EmojiAction.random_activation_probability = 0.0
|
||||
EmojiAction.focus_activation_type = ActionActivationType.LLM_JUDGE
|
||||
EmojiAction.normal_activation_type = ActionActivationType.LLM_JUDGE
|
||||
EmojiAction.activation_type = ActionActivationType.LLM_JUDGE
|
||||
|
||||
elif global_config.emoji.emoji_activate_type == "random":
|
||||
EmojiAction.random_activation_probability = global_config.emoji.emoji_chance
|
||||
EmojiAction.focus_activation_type = ActionActivationType.RANDOM
|
||||
EmojiAction.normal_activation_type = ActionActivationType.RANDOM
|
||||
EmojiAction.activation_type = ActionActivationType.RANDOM
|
||||
|
||||
# --- 根据配置注册组件 ---
|
||||
components = []
|
||||
if self.get_config("components.enable_reply", True):
|
||||
|
||||
@@ -32,13 +32,13 @@ class ReplyAction(BaseAction):
|
||||
|
||||
# 动作基本信息
|
||||
action_name = "reply"
|
||||
action_description = "参与聊天回复,发送文本进行表达"
|
||||
action_description = ""
|
||||
|
||||
# 动作参数定义
|
||||
action_parameters = {}
|
||||
|
||||
# 动作使用场景
|
||||
action_require = ["你想要闲聊或者随便附和", "有人提到你", "如果你刚刚进行了回复,不要对同一个话题重复回应"]
|
||||
action_require = [""]
|
||||
|
||||
# 关联类型
|
||||
associated_types = ["text"]
|
||||
@@ -46,6 +46,9 @@ class ReplyAction(BaseAction):
|
||||
def _parse_reply_target(self, target_message: str) -> tuple:
|
||||
sender = ""
|
||||
target = ""
|
||||
# 添加None检查,防止NoneType错误
|
||||
if target_message is None:
|
||||
return sender, target
|
||||
if ":" in target_message or ":" in target_message:
|
||||
# 使用正则表达式匹配中文或英文冒号
|
||||
parts = re.split(pattern=r"[::]", string=target_message, maxsplit=1)
|
||||
|
||||
@@ -19,7 +19,7 @@ class ManagementCommand(BaseCommand):
|
||||
description: str = "管理命令"
|
||||
command_pattern: str = r"(?P<manage_command>^/pm(\s[a-zA-Z0-9_]+)*\s*$)"
|
||||
|
||||
async def execute(self) -> Tuple[bool, str]:
|
||||
async def execute(self) -> Tuple[bool, str, bool]:
|
||||
# sourcery skip: merge-duplicate-blocks
|
||||
if (
|
||||
not self.message
|
||||
@@ -28,11 +28,11 @@ class ManagementCommand(BaseCommand):
|
||||
or str(self.message.message_info.user_info.user_id) not in self.get_config("plugin.permission", []) # type: ignore
|
||||
):
|
||||
await self.send_text("你没有权限使用插件管理命令")
|
||||
return False, "没有权限"
|
||||
return False, "没有权限", True
|
||||
command_list = self.matched_groups["manage_command"].strip().split(" ")
|
||||
if len(command_list) == 1:
|
||||
await self.show_help("all")
|
||||
return True, "帮助已发送"
|
||||
return True, "帮助已发送", True
|
||||
if len(command_list) == 2:
|
||||
match command_list[1]:
|
||||
case "plugin":
|
||||
@@ -43,7 +43,7 @@ class ManagementCommand(BaseCommand):
|
||||
await self.show_help("all")
|
||||
case _:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if len(command_list) == 3:
|
||||
if command_list[1] == "plugin":
|
||||
match command_list[2]:
|
||||
@@ -57,7 +57,7 @@ class ManagementCommand(BaseCommand):
|
||||
await self._rescan_plugin_dirs()
|
||||
case _:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
elif command_list[1] == "component":
|
||||
if command_list[2] == "list":
|
||||
await self._list_all_registered_components()
|
||||
@@ -65,10 +65,10 @@ class ManagementCommand(BaseCommand):
|
||||
await self.show_help("component")
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if len(command_list) == 4:
|
||||
if command_list[1] == "plugin":
|
||||
match command_list[2]:
|
||||
@@ -82,28 +82,28 @@ class ManagementCommand(BaseCommand):
|
||||
await self._add_dir(command_list[3])
|
||||
case _:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
elif command_list[1] == "component":
|
||||
if command_list[2] != "list":
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if command_list[3] == "enabled":
|
||||
await self._list_enabled_components()
|
||||
elif command_list[3] == "disabled":
|
||||
await self._list_disabled_components()
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if len(command_list) == 5:
|
||||
if command_list[1] != "component":
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if command_list[2] != "list":
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if command_list[3] == "enabled":
|
||||
await self._list_enabled_components(target_type=command_list[4])
|
||||
elif command_list[3] == "disabled":
|
||||
@@ -112,11 +112,11 @@ class ManagementCommand(BaseCommand):
|
||||
await self._list_registered_components_by_type(command_list[4])
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if len(command_list) == 6:
|
||||
if command_list[1] != "component":
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
if command_list[2] == "enable":
|
||||
if command_list[3] == "global":
|
||||
await self._globally_enable_component(command_list[4], command_list[5])
|
||||
@@ -124,7 +124,7 @@ class ManagementCommand(BaseCommand):
|
||||
await self._locally_enable_component(command_list[4], command_list[5])
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
elif command_list[2] == "disable":
|
||||
if command_list[3] == "global":
|
||||
await self._globally_disable_component(command_list[4], command_list[5])
|
||||
@@ -132,12 +132,12 @@ class ManagementCommand(BaseCommand):
|
||||
await self._locally_disable_component(command_list[4], command_list[5])
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
else:
|
||||
await self.send_text("插件管理命令不合法")
|
||||
return False, "命令不合法"
|
||||
return False, "命令不合法", True
|
||||
|
||||
return True, "命令执行完成"
|
||||
return True, "命令执行完成", True
|
||||
|
||||
async def show_help(self, target: str):
|
||||
help_msg = ""
|
||||
|
||||
@@ -39,11 +39,7 @@ class CompareNumbersTool(BaseTool):
|
||||
else:
|
||||
result = f"{num1} 等于 {num2}"
|
||||
|
||||
return {"type": "comparison_result", "id": f"{num1}_vs_{num2}", "content": result}
|
||||
return {"name": self.name, "content": result}
|
||||
except Exception as e:
|
||||
logger.error(f"比较数字失败: {str(e)}")
|
||||
return {"type": "info", "id": f"{num1}_vs_{num2}", "content": f"比较数字失败,炸了: {str(e)}"}
|
||||
|
||||
|
||||
# 注册工具
|
||||
# register_tool(CompareNumbersTool)
|
||||
return {"name": self.name, "content": f"比较数字失败,炸了: {str(e)}"}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from src.tools.tool_can_use.base_tool import BaseTool
|
||||
from src.person_info.person_info import get_person_info_manager
|
||||
from src.common.logger import get_logger
|
||||
import time
|
||||
|
||||
|
||||
logger = get_logger("rename_person_tool")
|
||||
@@ -24,7 +23,7 @@ class RenamePersonTool(BaseTool):
|
||||
"required": ["person_name"],
|
||||
}
|
||||
|
||||
async def execute(self, function_args: dict, message_txt=""):
|
||||
async def execute(self, function_args: dict):
|
||||
"""
|
||||
执行取名工具逻辑
|
||||
|
||||
@@ -82,7 +81,7 @@ class RenamePersonTool(BaseTool):
|
||||
|
||||
content = f"已成功将用户 {person_name_to_find} 的备注名更新为 {new_name}"
|
||||
logger.info(content)
|
||||
return {"type": "info", "id": f"rename_success_{time.time()}", "content": content}
|
||||
return {"name": self.name, "content": content}
|
||||
else:
|
||||
logger.warning(f"为用户 {person_id} 调用 qv_person_name 后未能成功获取新昵称。")
|
||||
# 尝试从内存中获取可能已经更新的名字
|
||||
@@ -101,4 +100,4 @@ class RenamePersonTool(BaseTool):
|
||||
except Exception as e:
|
||||
error_msg = f"重命名失败: {str(e)}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
return {"type": "info_error", "id": f"rename_error_{time.time()}", "content": error_msg}
|
||||
return {"name": self.name, "content": error_msg}
|
||||
|
||||
@@ -172,7 +172,7 @@ class ToolExecutor:
|
||||
logger.debug(f"{self.log_prefix}执行工具: {tool_name}")
|
||||
|
||||
# 执行工具
|
||||
result = await self.tool_instance._execute_tool_call(tool_call)
|
||||
result = await self.tool_instance.execute_tool_call(tool_call)
|
||||
|
||||
if result:
|
||||
tool_info = {
|
||||
@@ -299,7 +299,7 @@ class ToolExecutor:
|
||||
|
||||
logger.info(f"{self.log_prefix}直接执行工具: {tool_name}")
|
||||
|
||||
result = await self.tool_instance._execute_tool_call(tool_call)
|
||||
result = await self.tool_instance.execute_tool_call(tool_call)
|
||||
|
||||
if result:
|
||||
tool_info = {
|
||||
|
||||
@@ -16,7 +16,8 @@ class ToolUser:
|
||||
return get_all_tool_definitions()
|
||||
|
||||
@staticmethod
|
||||
async def _execute_tool_call(tool_call):
|
||||
async def execute_tool_call(tool_call):
|
||||
# sourcery skip: use-assigned-variable
|
||||
"""执行特定的工具调用
|
||||
|
||||
Args:
|
||||
|
||||
@@ -59,6 +59,9 @@ max_context_size = 25 # 上下文长度
|
||||
thinking_timeout = 20 # 麦麦一次回复最长思考规划时间,超过这个时间的思考会放弃(往往是api反应太慢)
|
||||
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
|
||||
|
||||
mentioned_bot_inevitable_reply = true # 提及 bot 大概率回复
|
||||
at_bot_inevitable_reply = true # @bot 或 提及bot 大概率回复
|
||||
|
||||
use_s4u_prompt_mode = true # 是否使用 s4u 对话构建模式,该模式会更好的把握当前对话对象的对话内容,但是对群聊整理理解能力较差(测试功能!!可能有未知问题!!)
|
||||
|
||||
|
||||
@@ -101,11 +104,8 @@ ban_msgs_regex = [
|
||||
]
|
||||
|
||||
[normal_chat] #普通聊天
|
||||
#一般回复参数
|
||||
willing_mode = "classical" # 回复意愿模式 —— 经典模式:classical,mxp模式:mxp,自定义模式:custom(需要你自己实现)
|
||||
response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数
|
||||
mentioned_bot_inevitable_reply = true # 提及 bot 必然回复
|
||||
at_bot_inevitable_reply = true # @bot 必然回复(包含提及)
|
||||
|
||||
[tool]
|
||||
enable_in_normal_chat = false # 是否在普通聊天中启用工具
|
||||
@@ -148,9 +148,7 @@ enable_asr = false # 是否启用语音识别,启用后麦麦可以识别语
|
||||
|
||||
[mood]
|
||||
enable_mood = true # 是否启用情绪系统
|
||||
mood_update_interval = 1.0 # 情绪更新间隔 单位秒
|
||||
mood_decay_rate = 0.95 # 情绪衰减率
|
||||
mood_intensity_factor = 1.0 # 情绪强度因子
|
||||
mood_update_threshold = 1 # 情绪更新阈值,越高,更新越慢
|
||||
|
||||
[lpmm_knowledge] # lpmm知识库配置
|
||||
enable = false # 是否启用lpmm知识库
|
||||
|
||||
Reference in New Issue
Block a user