This commit is contained in:
Windpicker-owo
2025-07-26 18:37:55 +08:00
32 changed files with 1455 additions and 1249 deletions

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ class ExampleAction(BaseAction):
action_name = "example_action" # 动作的唯一标识符 action_name = "example_action" # 动作的唯一标识符
action_description = "这是一个示例动作" # 动作描述 action_description = "这是一个示例动作" # 动作描述
activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例 activation_type = ActionActivationType.ALWAYS # 这里以 ALWAYS 为例
mode_enable = ChatMode.ALL # 这里以 ALL 为例 mode_enable = ChatMode.ALL # 一般取ALL表示在所有聊天模式下都可用
associated_types = ["text", "emoji", ...] # 关联类型 associated_types = ["text", "emoji", ...] # 关联类型
parallel_action = False # 是否允许与其他Action并行执行 parallel_action = False # 是否允许与其他Action并行执行
action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...} action_parameters = {"param1": "参数1的说明", "param2": "参数2的说明", ...}
@@ -60,7 +60,7 @@ class ExampleAction(BaseAction):
**请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。** **请知悉,对于不同的处理器,其支持的消息类型可能会有所不同。在开发时请注意。**
#### action_parameters: 该Action的参数说明。 #### action_parameters: 该Action的参数说明。
这是一个字典键为参数名值为参数说明。这个字段可以帮助LLM理解如何使用这个Action并由LLM返回对应的参数最后传递到 Action 的 action_data 属性中。其格式与你定义的格式完全相同 **除非LLM哈气了返回了错误的内容**。 这是一个字典键为参数名值为参数说明。这个字段可以帮助LLM理解如何使用这个Action并由LLM返回对应的参数最后传递到 Action 的 **`action_data`** 属性中。其格式与你定义的格式完全相同 **除非LLM哈气了返回了错误的内容**。
--- ---
@@ -180,6 +180,8 @@ class GreetingAction(BaseAction):
return True, "发送了问候" return True, "发送了问候"
``` ```
一个完整的使用`ActionActivationType.KEYWORD`的例子请参考`plugins/hello_world_plugin`中的`ByeAction`
#### 第二层:使用决策 #### 第二层:使用决策
**在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action** **在Action被激活后使用条件决定麦麦什么时候会"选择"使用这个Action**

View File

@@ -6,34 +6,6 @@
> >
> 系统会根据你在代码中定义的 `config_schema` 自动生成配置文件。手动创建配置文件会破坏自动化流程,导致配置不一致、缺失注释和文档等问题。 > 系统会根据你在代码中定义的 `config_schema` 自动生成配置文件。手动创建配置文件会破坏自动化流程,导致配置不一致、缺失注释和文档等问题。
## 📖 目录
1. [配置架构变更说明](#配置架构变更说明)
2. [配置版本管理](#配置版本管理)
3. [配置定义Schema驱动的配置系统](#配置定义schema驱动的配置系统)
4. [配置访问在Action和Command中使用配置](#配置访问在action和command中使用配置)
5. [完整示例:从定义到使用](#完整示例从定义到使用)
6. [最佳实践与注意事项](#最佳实践与注意事项)
---
## 配置架构变更说明
- **`_manifest.json`** - 负责插件的**元数据信息**(静态)
- 插件名称、版本、描述
- 作者信息、许可证
- 仓库链接、关键词、分类
- 组件列表、兼容性信息
- **`config.toml`** - 负责插件的**运行时配置**(动态)
- `enabled` - 是否启用插件
- 功能参数配置
- 组件启用开关
- 用户可调整的行为参数
---
## 配置版本管理 ## 配置版本管理
### 🎯 版本管理概述 ### 🎯 版本管理概述
@@ -103,7 +75,7 @@ config_schema = {
2. **迁移配置值** - 将旧配置文件中的值迁移到新结构中 2. **迁移配置值** - 将旧配置文件中的值迁移到新结构中
3. **处理新增字段** - 新增的配置项使用默认值 3. **处理新增字段** - 新增的配置项使用默认值
4. **更新版本号** - `config_version` 字段自动更新为最新版本 4. **更新版本号** - `config_version` 字段自动更新为最新版本
5. **保存配置文件** - 迁移后的配置直接覆盖原文件(不保留备份) 5. **保存配置文件** - 迁移后的配置直接覆盖原文件**(不保留备份)**
### 🔧 实际使用示例 ### 🔧 实际使用示例
@@ -174,28 +146,13 @@ min_duration = 120
- 跳过版本检查和迁移 - 跳过版本检查和迁移
- 直接加载现有配置 - 直接加载现有配置
- 新增的配置项在代码中使用默认值访问 - 新增的配置项在代码中使用默认值访问
- 系统会详细记录配置迁移过程。
### 📝 配置迁移日志
系统会详细记录配置迁移过程:
```log
[MutePlugin] 检测到配置版本需要更新: 当前=v1.0.0, 期望=v1.1.0
[MutePlugin] 生成新配置结构...
[MutePlugin] 迁移配置值: plugin.enabled = true
[MutePlugin] 更新配置版本: plugin.config_version = 1.1.0 (旧值: 1.0.0)
[MutePlugin] 迁移配置值: mute.min_duration = 120
[MutePlugin] 迁移配置值: mute.max_duration = 3600
[MutePlugin] 新增节: permissions
[MutePlugin] 配置文件已从 v1.0.0 更新到 v1.1.0
```
### ⚠️ 重要注意事项 ### ⚠️ 重要注意事项
#### 1. 版本号管理 #### 1. 版本号管理
- 当你修改 `config_schema` 时,**必须同步更新** `config_version` - 当你修改 `config_schema` 时,**必须同步更新** `config_version`
- 建议使用语义化版本号 (例如:`1.0.0`, `1.1.0`, `2.0.0`) - 使用语义化版本号 (例如:`1.0.0`, `1.1.0`, `2.0.0`)
- 配置结构的重大变更应该增加主版本号
#### 2. 迁移策略 #### 2. 迁移策略
- **保留原值优先**: 迁移时优先保留用户的原有配置值 - **保留原值优先**: 迁移时优先保留用户的原有配置值
@@ -207,45 +164,7 @@ min_duration = 120
- **不保留备份**: 迁移后直接覆盖原配置文件,不保留备份 - **不保留备份**: 迁移后直接覆盖原配置文件,不保留备份
- **失败安全**: 如果迁移过程中出现错误,会回退到原配置 - **失败安全**: 如果迁移过程中出现错误,会回退到原配置
--- ## 配置定义
## 配置定义Schema驱动的配置系统
### 核心理念Schema驱动的配置
在新版插件系统中,我们引入了一套 **配置Schema模式驱动** 的机制。**你不需要也不应该手动创建和维护 `config.toml` 文件**,而是通过在插件代码中 **声明配置的结构**,系统将为你完成剩下的工作。
> **⚠️ 绝对不要手动创建 config.toml 文件!**
>
> - ❌ **错误做法**:手动在插件目录下创建 `config.toml` 文件
> - ✅ **正确做法**:在插件代码中定义 `config_schema`,让系统自动生成配置文件
**核心优势:**
- **自动化 (Automation)**: 如果配置文件不存在,系统会根据你的声明 **自动生成** 一份包含默认值和详细注释的 `config.toml` 文件。
- **规范化 (Standardization)**: 所有插件的配置都遵循统一的结构,提升了可维护性。
- **自带文档 (Self-documenting)**: 配置文件中的每一项都包含详细的注释、类型说明、可选值和示例,极大地降低了用户的使用门槛。
- **健壮性 (Robustness)**: 在代码中直接定义配置的类型和默认值,减少了因配置错误导致的运行时问题。
- **易于管理 (Easy Management)**: 生成的配置文件可以方便地加入 `.gitignore`避免将个人配置如API Key提交到版本库。
### 配置生成工作流程
```mermaid
graph TD
A[编写插件代码] --> B[定义 config_schema]
B --> C[首次加载插件]
C --> D{config.toml 是否存在?}
D -->|不存在| E[系统自动生成 config.toml]
D -->|存在| F[加载现有配置文件]
E --> G[配置完成,插件可用]
F --> G
style E fill:#90EE90
style B fill:#87CEEB
style G fill:#DDA0DD
```
### 如何定义配置
配置的定义在你的插件主类(继承自 `BasePlugin`)中完成,主要通过两个类属性: 配置的定义在你的插件主类(继承自 `BasePlugin`)中完成,主要通过两个类属性:
@@ -257,6 +176,7 @@ graph TD
每个配置项都通过一个 `ConfigField` 对象来定义。 每个配置项都通过一个 `ConfigField` 对象来定义。
```python ```python
from dataclasses import dataclass
from src.plugin_system.base.config_types import ConfigField from src.plugin_system.base.config_types import ConfigField
@dataclass @dataclass
@@ -270,28 +190,21 @@ class ConfigField:
choices: Optional[List[Any]] = None # 可选值列表 (可选) choices: Optional[List[Any]] = None # 可选值列表 (可选)
``` ```
### 配置定义示例 ### 配置示例
让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。 让我们以一个功能丰富的 `MutePlugin` 为例,看看如何定义它的配置。
```python ```python
# src/plugins/built_in/mute_plugin/plugin.py # src/plugins/built_in/mute_plugin/plugin.py
from src.plugin_system import BasePlugin, register_plugin from src.plugin_system import BasePlugin, register_plugin, ConfigField
from src.plugin_system.base.config_types import ConfigField
from typing import List, Tuple, Type from typing import List, Tuple, Type
@register_plugin @register_plugin
class MutePlugin(BasePlugin): class MutePlugin(BasePlugin):
"""禁言插件""" """禁言插件"""
# 插件基本信息 # 这里是插件基本信息,略去
plugin_name = "mute_plugin"
plugin_description = "群聊禁言管理插件,提供智能禁言功能"
plugin_version = "2.0.0"
plugin_author = "MaiBot开发团队"
enable_plugin = True
config_file_name = "config.toml"
# 步骤1: 定义配置节的描述 # 步骤1: 定义配置节的描述
config_section_descriptions = { config_section_descriptions = {
@@ -339,22 +252,9 @@ class MutePlugin(BasePlugin):
} }
} }
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: # 这里是插件方法,略去
# 在这里可以通过 self.get_config() 来获取配置值
enable_smart_mute = self.get_config("components.enable_smart_mute", True)
enable_mute_command = self.get_config("components.enable_mute_command", False)
components = []
if enable_smart_mute:
components.append((SmartMuteAction.get_action_info(), SmartMuteAction))
if enable_mute_command:
components.append((MuteCommand.get_command_info(), MuteCommand))
return components
``` ```
### 自动生成的配置文件
`mute_plugin` 首次加载且其目录中不存在 `config.toml` 时,系统会自动创建以下文件: `mute_plugin` 首次加载且其目录中不存在 `config.toml` 时,系统会自动创建以下文件:
```toml ```toml
@@ -413,317 +313,24 @@ prefix = "[MutePlugin]"
--- ---
## 配置访问在Action和Command中使用配置 ## 配置访问
### 问题描述 如果你想要在你的组件中访问配置,可以通过组件内置的 `get_config()` 方法访问配置。
在插件开发中,你可能遇到这样的问题 其参数为一个命名空间化的字符串。以上面的 `MutePlugin` 为例,你可以这样访问配置
- 想要在Action或Command中访问插件配置
### ✅ 解决方案
**直接使用 `self.get_config()` 方法!**
系统已经自动为你处理了配置传递,你只需要通过组件内置的 `get_config` 方法访问配置即可。
### 📖 快速示例
#### 在Action中访问配置
```python ```python
from src.plugin_system import BaseAction enable_smart_mute = self.get_config("components.enable_smart_mute", True)
class MyAction(BaseAction):
async def execute(self):
# 方法1: 获取配置值(带默认值)
api_key = self.get_config("api.key", "default_key")
timeout = self.get_config("api.timeout", 30)
# 方法2: 支持嵌套键访问
log_level = self.get_config("advanced.logging.level", "INFO")
# 方法3: 直接访问顶层配置
enable_feature = self.get_config("features.enable_smart", False)
# 使用配置值
if enable_feature:
await self.send_text(f"API密钥: {api_key}")
return True, "配置访问成功"
``` ```
#### 在Command中访问配置 如果尝试访问了一个不存在的配置项,系统会自动返回默认值(你传递的)或者 `None`
```python
from src.plugin_system import BaseCommand
class MyCommand(BaseCommand):
async def execute(self):
# 使用方式与Action完全相同
welcome_msg = self.get_config("messages.welcome", "欢迎!")
max_results = self.get_config("search.max_results", 10)
# 根据配置执行不同逻辑
if self.get_config("features.debug_mode", False):
await self.send_text(f"调试模式已启用,最大结果数: {max_results}")
await self.send_text(welcome_msg)
return True, "命令执行完成"
```
### 🔧 API方法详解
#### 1. `get_config(key, default=None)`
获取配置值,支持嵌套键访问:
```python
# 简单键
value = self.get_config("timeout", 30)
# 嵌套键(用点号分隔)
value = self.get_config("database.connection.host", "localhost")
value = self.get_config("features.ai.model", "gpt-3.5-turbo")
```
#### 2. 类型安全的配置访问
```python
# 确保正确的类型
max_retries = self.get_config("api.max_retries", 3)
if not isinstance(max_retries, int):
max_retries = 3 # 使用安全的默认值
# 布尔值配置
debug_mode = self.get_config("features.debug_mode", False)
if debug_mode:
# 调试功能逻辑
pass
```
#### 3. 配置驱动的组件行为
```python
class ConfigDrivenAction(BaseAction):
async def execute(self):
# 根据配置决定激活行为
activation_config = {
"use_keywords": self.get_config("activation.use_keywords", True),
"use_llm": self.get_config("activation.use_llm", False),
"keywords": self.get_config("activation.keywords", []),
}
# 根据配置调整功能
features = {
"enable_emoji": self.get_config("features.enable_emoji", True),
"enable_llm_reply": self.get_config("features.enable_llm_reply", False),
"max_length": self.get_config("output.max_length", 200),
}
# 使用配置执行逻辑
if features["enable_llm_reply"]:
# 使用LLM生成回复
pass
else:
# 使用模板回复
pass
return True, "配置驱动执行完成"
```
### 🔄 配置传递机制
系统自动处理配置传递,无需手动操作:
1. **插件初始化**`BasePlugin`加载`config.toml``self.config`
2. **组件注册** → 系统记录插件配置
3. **组件实例化** → 自动传递`plugin_config`参数给Action/Command
4. **配置访问** → 组件通过`self.get_config()`直接访问配置
---
## 完整示例:从定义到使用
### 插件定义
```python
from src.plugin_system.base.config_types import ConfigField
@register_plugin
class GreetingPlugin(BasePlugin):
"""问候插件完整示例"""
plugin_name = "greeting_plugin"
plugin_description = "智能问候插件,展示配置定义和访问的完整流程"
plugin_version = "1.0.0"
config_file_name = "config.toml"
# 配置节描述
config_section_descriptions = {
"plugin": "插件启用配置",
"greeting": "问候功能配置",
"features": "功能开关配置",
"messages": "消息模板配置"
}
# 配置Schema定义
config_schema = {
"plugin": {
"enabled": ConfigField(type=bool, default=True, description="是否启用插件")
},
"greeting": {
"template": ConfigField(
type=str,
default="你好,{username}!欢迎使用问候插件!",
description="问候消息模板"
),
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"),
"enable_llm": ConfigField(type=bool, default=False, description="是否使用LLM生成个性化问候")
},
"features": {
"smart_detection": ConfigField(type=bool, default=True, description="是否启用智能检测"),
"random_greeting": ConfigField(type=bool, default=False, description="是否使用随机问候语"),
"max_greetings_per_hour": ConfigField(type=int, default=5, description="每小时最大问候次数")
},
"messages": {
"custom_greetings": ConfigField(
type=list,
default=["你好!", "嗨!", "欢迎!"],
description="自定义问候语列表"
),
"error_message": ConfigField(
type=str,
default="问候功能暂时不可用",
description="错误时显示的消息"
)
}
}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""根据配置动态注册组件"""
components = []
# 根据配置决定是否注册组件
if self.get_config("plugin.enabled", True):
components.append((SmartGreetingAction.get_action_info(), SmartGreetingAction))
components.append((GreetingCommand.get_command_info(), GreetingCommand))
return components
```
### Action组件使用配置
```python
class SmartGreetingAction(BaseAction):
"""智能问候Action - 展示配置访问"""
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
activation_keywords = ["你好", "hello", "hi"]
async def execute(self) -> Tuple[bool, str]:
"""执行智能问候,大量使用配置"""
try:
# 检查插件是否启用
if not self.get_config("plugin.enabled", True):
return False, "插件已禁用"
# 获取问候配置
template = self.get_config("greeting.template", "你好,{username}")
enable_emoji = self.get_config("greeting.enable_emoji", True)
enable_llm = self.get_config("greeting.enable_llm", False)
# 获取功能配置
smart_detection = self.get_config("features.smart_detection", True)
random_greeting = self.get_config("features.random_greeting", False)
max_per_hour = self.get_config("features.max_greetings_per_hour", 5)
# 获取消息配置
custom_greetings = self.get_config("messages.custom_greetings", [])
error_message = self.get_config("messages.error_message", "问候功能不可用")
# 根据配置执行不同逻辑
username = self.action_data.get("username", "用户")
if random_greeting and custom_greetings:
# 使用随机自定义问候语
import random
greeting_msg = random.choice(custom_greetings)
elif enable_llm:
# 使用LLM生成个性化问候
greeting_msg = await self._generate_llm_greeting(username)
else:
# 使用模板问候
greeting_msg = template.format(username=username)
# 发送问候消息
await self.send_text(greeting_msg)
# 根据配置发送表情
if enable_emoji:
await self.send_emoji("😊")
return True, f"向{username}发送了问候"
except Exception as e:
# 使用配置的错误消息
await self.send_text(self.get_config("messages.error_message", "出错了"))
return False, f"问候失败: {str(e)}"
async def _generate_llm_greeting(self, username: str) -> str:
"""根据配置使用LLM生成问候语"""
# 这里可以进一步使用配置来定制LLM行为
llm_style = self.get_config("greeting.llm_style", "friendly")
# ... LLM调用逻辑
return f"你好 {username}!很高兴见到你!"
```
### Command组件使用配置
```python
class GreetingCommand(BaseCommand):
"""问候命令 - 展示配置访问"""
command_pattern = r"^/greet(?:\s+(?P<username>\w+))?$"
command_help = "发送问候消息"
command_examples = ["/greet", "/greet Alice"]
async def execute(self) -> Tuple[bool, Optional[str]]:
"""执行问候命令"""
# 检查功能是否启用
if not self.get_config("plugin.enabled", True):
await self.send_text("问候功能已禁用")
return False, "功能禁用"
# 获取用户名
username = self.matched_groups.get("username", "用户")
# 根据配置选择问候方式
if self.get_config("features.random_greeting", False):
custom_greetings = self.get_config("messages.custom_greetings", ["你好!"])
import random
greeting = random.choice(custom_greetings)
else:
template = self.get_config("greeting.template", "你好,{username}")
greeting = template.format(username=username)
# 发送问候
await self.send_text(greeting)
# 根据配置发送表情
if self.get_config("greeting.enable_emoji", True):
await self.send_text("😊")
return True, "问候发送成功"
```
--- ---
## 最佳实践与注意事项 ## 最佳实践与注意事项
### 配置定义最佳实践
> **🚨 核心原则:永远不要手动创建 config.toml 文件!** **🚨 核心原则:永远不要手动创建 config.toml 文件!**
1. **🔥 绝不手动创建配置文件**: **任何时候都不要手动创建 `config.toml` 文件**!必须通过在 `plugin.py` 中定义 `config_schema` 让系统自动生成。 1. **🔥 绝不手动创建配置文件**: **任何时候都不要手动创建 `config.toml` 文件**!必须通过在 `plugin.py` 中定义 `config_schema` 让系统自动生成。
-**禁止**`touch config.toml`、手动编写配置文件 -**禁止**`touch config.toml`、手动编写配置文件
@@ -737,76 +344,4 @@ class GreetingCommand(BaseCommand):
5. **gitignore**: 将 `plugins/*/config.toml``src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。 5. **gitignore**: 将 `plugins/*/config.toml``src/plugins/built_in/*/config.toml` 加入 `.gitignore`,以避免提交个人敏感信息。
6. **配置文件只供修改**: 自动生成的 `config.toml` 文件只应该被用户**修改**,而不是从零创建。 6. **配置文件只供修改**: 自动生成的 `config.toml` 文件只应该被用户**修改**,而不是从零创建。
### 配置访问最佳实践
#### 1. 总是提供默认值
```python
# ✅ 好的做法
timeout = self.get_config("api.timeout", 30)
# ❌ 避免这样做
timeout = self.get_config("api.timeout") # 可能返回None
```
#### 2. 验证配置类型
```python
# 获取配置后验证类型
max_items = self.get_config("list.max_items", 10)
if not isinstance(max_items, int) or max_items <= 0:
max_items = 10 # 使用安全的默认值
```
#### 3. 缓存复杂配置解析
```python
class MyAction(BaseAction):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# 在初始化时解析复杂配置,避免重复解析
self._api_config = self._parse_api_config()
def _parse_api_config(self):
return {
'key': self.get_config("api.key", ""),
'timeout': self.get_config("api.timeout", 30),
'retries': self.get_config("api.max_retries", 3)
}
```
#### 4. 配置驱动的组件注册
```python
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""根据配置动态注册组件"""
components = []
# 从配置获取组件启用状态
enable_action = self.get_config("components.enable_action", True)
enable_command = self.get_config("components.enable_command", True)
if enable_action:
components.append((MyAction.get_action_info(), MyAction))
if enable_command:
components.append((MyCommand.get_command_info(), MyCommand))
return components
```
### 🎉 总结
现在你掌握了插件配置的完整流程:
1. **定义配置**: 在插件中使用 `config_schema` 定义配置结构
2. **访问配置**: 在组件中使用 `self.get_config("key", default_value)` 访问配置
3. **自动生成**: 系统自动生成带注释的配置文件
4. **动态行为**: 根据配置动态调整插件行为
> **🚨 最后强调:任何时候都不要手动创建 config.toml 文件!**
>
> 让系统根据你的 `config_schema` 自动生成配置文件,这是插件系统的核心设计原则。
不需要继承`BasePlugin`,不需要复杂的配置传递,不需要手动创建配置文件,组件内置的`get_config`方法和自动化的配置生成机制已经为你准备好了一切!

View File

@@ -4,15 +4,34 @@
## 新手入门 ## 新手入门
- [📖 快速开始指南](quick-start.md) - 5分钟创建你的第一个插件 - [📖 快速开始指南](quick-start.md) - 快速创建你的第一个插件
## 组件功能详解 ## 组件功能详解
- [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件 - [🧱 Action组件详解](action-components.md) - 掌握最核心的Action组件
- [💻 Command组件详解](command-components.md) - 学习直接响应命令的组件 - [💻 Command组件详解](command-components.md) - 学习直接响应命令的组件
- [⚙️ 配置管理指南](configuration-guide.md) - 学会使用自动生成的插件配置文件 - [⚙️ 配置文件系统指南](configuration-guide.md) - 学会使用自动生成的插件配置文件
- [📄 Manifest系统指南](manifest-guide.md) - 了解插件元数据管理和配置架构 - [📄 Manifest系统指南](manifest-guide.md) - 了解插件元数据管理和配置架构
Command vs Action 选择指南
1. 使用Command的场景
- ✅ 用户需要明确调用特定功能
- ✅ 需要精确的参数控制
- ✅ 管理和配置操作
- ✅ 查询和信息显示
- ✅ 系统维护命令
2. 使用Action的场景
- ✅ 增强麦麦的智能行为
- ✅ 根据上下文自动触发
- ✅ 情绪和表情表达
- ✅ 智能建议和帮助
- ✅ 随机化的互动
## API浏览 ## API浏览
### 消息发送与处理API ### 消息发送与处理API

View File

@@ -1,20 +1,14 @@
# 🚀 快速开始指南 # 🚀 快速开始指南
本指南将带你用5分钟时间从零开始创建一个功能完整的MaiCore插件。 本指南将带你从零开始创建一个功能完整的MaiCore插件。
## 📖 概述 ## 📖 概述
这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件展示插件系统的基本概念。无需阅读其他文档,跟着本指南就能完成! 这个指南将带你快速创建你的第一个MaiCore插件。我们将创建一个简单的问候插件展示插件系统的基本概念。
## 🎯 学习目标 以下代码都在我们的`plugins/hello_world_plugin/`目录下。
- 理解插件的基本结构 ### 📂 准备工作
- 从最简单的插件开始,循序渐进
- 学会创建Action组件智能动作
- 学会创建Command组件命令响应
- 掌握配置Schema定义和配置文件自动生成可选
## 📂 准备工作
确保你已经: 确保你已经:
@@ -26,16 +20,29 @@
### 1. 创建插件目录 ### 1. 创建插件目录
在项目根目录的 `plugins/` 文件夹下创建你的插件目录,目录名与插件名保持一致: 在项目根目录的 `plugins/` 文件夹下创建你的插件目录
可以用以下命令快速创建: 这里我们创建一个名为 `hello_world_plugin` 的目录
```bash ### 2. 创建`_manifest.json`文件
mkdir plugins/hello_world_plugin
cd plugins/hello_world_plugin 在插件目录下面创建一个 `_manifest.json` 文件,内容如下:
```json
{
"manifest_version": 1,
"name": "Hello World 插件",
"version": "1.0.0",
"description": "一个简单的 Hello World 插件",
"author": {
"name": "你的名字"
}
}
``` ```
### 2. 创建最简单的插件 有关 `_manifest.json` 的详细说明,请参考 [Manifest文件指南](./manifest-guide.md)。
### 3. 创建最简单的插件
让我们从最基础的开始!创建 `plugin.py` 文件: 让我们从最基础的开始!创建 `plugin.py` 文件:
@@ -43,34 +50,33 @@ cd plugins/hello_world_plugin
from typing import List, Tuple, Type from typing import List, Tuple, Type
from src.plugin_system import BasePlugin, register_plugin, ComponentInfo from src.plugin_system import BasePlugin, register_plugin, ComponentInfo
# ===== 插件注册 ===== @register_plugin # 注册插件
@register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息(必须填写) # 以下是插件基本信息和方法(必须填写)
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True # 启用插件 enable_plugin = True # 启用插件
dependencies = [] # 插件依赖列表(目前为空)
python_dependencies = [] # Python依赖列表目前为空
config_file_name = "config.toml" # 配置文件名
config_schema = {} # 配置文件模式(目前为空)
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: # 获取插件组件
"""返回插件包含的组件列表(目前是空的)""" """返回插件包含的组件列表(目前是空的)"""
return [] return []
``` ```
🎉 **恭喜你刚刚创建了一个最简单但完整的MaiCore插件** 🎉 恭喜你刚刚创建了一个最简单但完整的MaiCore插件
**解释一下这些代码:** **解释一下这些代码:**
- 首先我们在plugin.py中定义了一个HelloWorldPulgin插件类继承自 `BasePlugin` ,提供基本功能。 - 首先,我们在`plugin.py`中定义了一个HelloWorldPlugin插件类继承自 `BasePlugin` ,提供基本功能。
- 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件" - 通过给类加上,`@register_plugin` 装饰器,我们告诉系统"这是一个插件"
- `plugin_name` 等是插件的基本信息,必须填写**此部分必须与目录名称相同,否则插件无法使用** - `plugin_name` 等是插件的基本信息,必须填写
- `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何action动作或者command(指令),是空的 - `get_plugin_components()` 返回插件的功能组件,现在我们没有定义任何 Action, Command 或者 EventHandler所以返回空列表。
### 3. 测试基础插件 ### 4. 测试基础插件
现在就可以测试这个插件了启动MaiCore 现在就可以测试这个插件了启动MaiCore
@@ -80,7 +86,7 @@ class HelloWorldPlugin(BasePlugin):
![1750326700269](image/quick-start/1750326700269.png) ![1750326700269](image/quick-start/1750326700269.png)
### 4. 添加第一个功能问候Action ### 5. 添加第一个功能问候Action
现在我们要给插件加入一个有用的功能我们从最好玩的Action做起 现在我们要给插件加入一个有用的功能我们从最好玩的Action做起
@@ -107,40 +113,34 @@ class HelloAction(BaseAction):
# === 基本信息(必须填写)=== # === 基本信息(必须填写)===
action_name = "hello_greeting" action_name = "hello_greeting"
action_description = "向用户发送问候消息" action_description = "向用户发送问候消息"
activation_type = ActionActivationType.ALWAYS # 始终激活
# === 功能描述(必须填写)=== # === 功能描述(必须填写)===
action_parameters = { action_parameters = {"greeting_message": "要发送的问候消息"}
"greeting_message": "要发送的问候消息" action_require = ["要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"]
}
action_require = [
"需要发送友好问候时使用",
"当有人向你问好时使用",
"当你遇见没有见过的人时使用"
]
associated_types = ["text"] associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, str]:
"""执行问候动作 - 这是核心功能""" """执行问候动作 - 这是核心功能"""
# 发送问候消息 # 发送问候消息
greeting_message = self.action_data.get("greeting_message","") greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = "嗨!很开心见到你!😊" + greeting_message message = base_message + greeting_message
await self.send_text(message) await self.send_text(message)
return True, "发送了问候消息" return True, "发送了问候消息"
# ===== 插件注册 =====
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息 # 插件基本信息
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件包含问候功能"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True enable_plugin = True
dependencies = []
python_dependencies = []
config_file_name = "config.toml"
config_schema = {}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
"""返回插件包含的组件列表""" """返回插件包含的组件列表"""
@@ -150,13 +150,17 @@ class HelloWorldPlugin(BasePlugin):
] ]
``` ```
**新增内容解释:** **解释一下这些代码**
- `HelloAction`一个Action组件MaiCore可能会选择使用它 - `HelloAction`我们定义的问候动作类,继承自 `BaseAction`,并实现了核心功能。
-`HelloWorldPlugin` 中,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `HelloAction` 注册为插件的一个组件。
- 这样一来当插件被加载时问候动作也会被一并加载并可以在MaiCore中使用。
- `execute()` 函数是Action的核心定义了当Action被MaiCore选择后具体要做什么 - `execute()` 函数是Action的核心定义了当Action被MaiCore选择后具体要做什么
- `self.send_text()` 是发送文本消息的便捷方法 - `self.send_text()` 是发送文本消息的便捷方法
### 5. 测试问候功能 Action 组件中有关`activation_type``action_parameters``action_require``associated_types` 等的详细说明请参考 [Action组件指南](./action-components.md)。
### 6. 测试问候Action
重启MaiCore然后在聊天中发送任意消息比如 重启MaiCore然后在聊天中发送任意消息比如
@@ -174,96 +178,17 @@ MaiCore可能会选择使用你的问候Action发送回复
> **💡 小提示**MaiCore会智能地决定什么时候使用它。如果没有立即看到效果多试几次不同的消息。 > **💡 小提示**MaiCore会智能地决定什么时候使用它。如果没有立即看到效果多试几次不同的消息。
🎉 **太棒了!你的插件已经有实际功能了!** 🎉 太棒了!你的插件已经有实际功能了!
### 5.5. 了解激活系统(重要概念) ### 7. 添加第二个功能时间查询Command
Action固然好用简单但是现在有个问题当用户加载了非常多的插件添加了很多自定义ActionLLM需要选择的Action也会变多
而不断增多的Action会加大LLM的消耗和负担降低Action使用的精准度。而且我们并不需要LLM在所有时候都考虑所有Action
例如,当群友只是在进行正常的聊天,就没有必要每次都考虑是否要选择“禁言”动作,这不仅影响决策速度,还会增加消耗。
那有什么办法能够让Action有选择的加入MaiCore的决策池呢
**什么是激活系统?**
激活系统决定了什么时候你的Action会被MaiCore"考虑"使用:
- **`ActionActivationType.ALWAYS`** - 总是可用(默认值)
- **`ActionActivationType.KEYWORD`** - 只有消息包含特定关键词时才可用
- **`ActionActivationType.PROBABILITY`** - 根据概率随机可用
- **`ActionActivationType.NEVER`** - 永不可用(用于调试)
> **💡 使用提示**
>
> - 推荐使用枚举类型(如 `ActionActivationType.ALWAYS`),有代码提示和类型检查
> - 也可以直接使用字符串(如 `"always"`),系统都支持
### 5.6. 进阶:尝试关键词激活(可选)
现在让我们尝试一个更精确的激活方式添加一个只在用户说特定关键词时才激活的Action
```python
# 在HelloAction后面添加这个新Action
class ByeAction(BaseAction):
"""告别Action - 只在用户说再见时激活"""
action_name = "bye_greeting"
action_description = "向用户发送告别消息"
# 使用关键词激活
focus_activation_type = ActionActivationType.KEYWORD
normal_activation_type = ActionActivationType.KEYWORD
# 关键词设置
activation_keywords = ["再见", "bye", "88", "拜拜"]
keyword_case_sensitive = False
action_parameters = {"bye_message": "要发送的告别消息"}
action_require = [
"用户要告别时使用",
"当有人要离开时使用",
"当有人和你说再见时使用",
]
associated_types = ["text"]
async def execute(self) -> Tuple[bool, str]:
bye_message = self.action_data.get("bye_message","")
message = "再见!期待下次聊天!👋" + bye_message
await self.send_text(message)
return True, "发送了告别消息"
```
然后在插件注册中添加这个Action
```python
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [
(HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction), # 添加告别Action
]
```
现在测试:发送"再见"应该会触发告别Action
**关键词激活的特点:**
- 更精确:只在包含特定关键词时才会被考虑
- 更可预测:用户知道说什么会触发什么功能
- 更适合:特定场景或命令式的功能
### 6. 添加第二个功能时间查询Command
现在让我们添加一个Command组件。Command和Action不同它是直接响应用户命令的 现在让我们添加一个Command组件。Command和Action不同它是直接响应用户命令的
Command是最简单最直接的不由LLM判断选择使用 Command是最简单最直接的不由LLM判断选择使用
```python ```python
# 在现有代码基础上添加Command组件 # 在现有代码基础上添加Command组件
import datetime
# ===== Command组件 =====
from src.plugin_system import BaseCommand from src.plugin_system import BaseCommand
#导入Command基类 #导入Command基类
@@ -275,53 +200,49 @@ class TimeCommand(BaseCommand):
# === 命令设置(必须填写)=== # === 命令设置(必须填写)===
command_pattern = r"^/time$" # 精确匹配 "/time" 命令 command_pattern = r"^/time$" # 精确匹配 "/time" 命令
command_help = "查询当前时间"
command_examples = ["/time"]
intercept_message = True # 拦截消息,不让其他组件处理
async def execute(self) -> Tuple[bool, str]: async def execute(self) -> Tuple[bool, Optional[str], bool]:
"""执行时间查询""" """执行时间查询"""
import datetime
# 获取当前时间 # 获取当前时间
time_format = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") time_format: str = "%Y-%m-%d %H:%M:%S"
now = datetime.datetime.now() now = datetime.datetime.now()
time_str = now.strftime(time_format) time_str = now.strftime(time_format)
# 发送时间信息 # 发送时间信息
message = f"⏰ 当前时间:{time_str}" message = f"⏰ 当前时间:{time_str}"
await self.send_text(message) await self.send_text(message)
return True, f"显示了当前时间: {time_str}"
# ===== 插件注册 ===== return True, f"显示了当前时间: {time_str}", True
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
# 插件基本信息
plugin_name = "hello_world_plugin" plugin_name = "hello_world_plugin"
plugin_description = "我的第一个MaiCore插件包含问候和时间查询功能"
plugin_version = "1.0.0"
plugin_author = "你的名字"
enable_plugin = True enable_plugin = True
dependencies = []
python_dependencies = []
config_file_name = "config.toml"
config_schema = {}
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [ return [
(HelloAction.get_action_info(), HelloAction), (HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction),
(TimeCommand.get_command_info(), TimeCommand), (TimeCommand.get_command_info(), TimeCommand),
] ]
``` ```
同样的,我们通过 `get_plugin_components()` 方法,通过调用`get_action_info()`这个内置方法将 `TimeCommand` 注册为插件的一个组件。
**Command组件解释** **Command组件解释**
- Command是直接响应用户命令的组件
- `command_pattern` 使用正则表达式匹配用户输入 - `command_pattern` 使用正则表达式匹配用户输入
- `^/time$` 表示精确匹配 "/time" - `^/time$` 表示精确匹配 "/time"
- `intercept_message = True` 表示处理完命令后不再让其他组件处理
### 7. 测试时间查询功能 有关 Command 组件的更多信息,请参考 [Command组件指南](./command-components.md)。
### 8. 测试时间查询Command
重启MaiCore发送命令 重启MaiCore发送命令
@@ -332,106 +253,147 @@ class HelloWorldPlugin(BasePlugin):
你应该会收到回复: 你应该会收到回复:
``` ```
⏰ 当前时间2024-01-01 12:30:45 ⏰ 当前时间2024-01-01 12:00:00
``` ```
🎉 **太棒了!现在你的插件有3个功能了** 🎉 太棒了!现在你已经了解了基本的 Action 和 Command 组件的使用方法。你可以根据自己的需求,继续扩展插件的功能,添加更多的 Action 和 Command 组件,让你的插件更加丰富和强大!
### 8. 添加配置文件(可选进阶) ---
如果你想让插件更加灵活,可以添加配置支持。 ## 进阶教程
如果你想让插件更加灵活和强大,可以参考接下来的进阶教程。
### 1. 添加配置文件
想要为插件添加配置文件吗?让我们一起来配置`config_schema`属性!
> **🚨 重要不要手动创建config.toml文件** > **🚨 重要不要手动创建config.toml文件**
> >
> 我们需要在插件代码中定义配置Schema让系统自动生成配置文件。 > 我们需要在插件代码中定义配置Schema让系统自动生成配置文件。
#### 📄 配置架构说明
在新的插件系统中,我们采用了**职责分离**的设计:
- **`_manifest.json`** - 插件元数据(名称、版本、描述、作者等)
- **`config.toml`** - 运行时配置(启用状态、功能参数等)
这样避免了信息重复,提高了维护性。
首先在插件类中定义配置Schema 首先在插件类中定义配置Schema
```python ```python
from src.plugin_system.base.config_types import ConfigField from src.plugin_system import ConfigField
@register_plugin @register_plugin
class HelloWorldPlugin(BasePlugin): class HelloWorldPlugin(BasePlugin):
"""Hello World插件 - 你的第一个MaiCore插件""" """Hello World插件 - 你的第一个MaiCore插件"""
plugin_name = "hello_world_plugin" # 插件基本信息
plugin_description = "我的第一个MaiCore插件包含问候和时间查询功能" plugin_name: str = "hello_world_plugin" # 内部标识符
plugin_version = "1.0.0" enable_plugin: bool = True
plugin_author = "你的名字" dependencies: List[str] = [] # 插件依赖列表
enable_plugin = True python_dependencies: List[str] = [] # Python包依赖列表
config_file_name = "config.toml" # 配置文件名 config_file_name: str = "config.toml" # 配置文件名
# 配置节描述
config_section_descriptions = {
"plugin": "插件启用配置",
"greeting": "问候功能配置",
"time": "时间查询配置"
}
# 配置Schema定义 # 配置Schema定义
config_schema = { config_schema: dict = {
"plugin": { "plugin": {
"enabled": ConfigField(type=bool, default=True, description="是否启用插件") "name": ConfigField(type=str, default="hello_world_plugin", description="插件名称"),
"version": ConfigField(type=str, default="1.0.0", description="插件版本"),
"enabled": ConfigField(type=bool, default=False, description="是否启用插件"),
}, },
"greeting": { "greeting": {
"message": ConfigField( "message": ConfigField(type=str, default="嗨!很开心见到你!😊", description="默认问候消息"),
type=str, "enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号"),
default="嗨!很开心见到你!😊",
description="默认问候消息"
),
"enable_emoji": ConfigField(type=bool, default=True, description="是否启用表情符号")
}, },
"time": { "time": {"format": ConfigField(type=str, default="%Y-%m-%d %H:%M:%S", description="时间显示格式")},
"format": ConfigField(
type=str,
default="%Y-%m-%d %H:%M:%S",
description="时间显示格式"
)
}
} }
def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]: def get_plugin_components(self) -> List[Tuple[ComponentInfo, Type]]:
return [ return [
(HelloAction.get_action_info(), HelloAction), (HelloAction.get_action_info(), HelloAction),
(ByeAction.get_action_info(), ByeAction),
(TimeCommand.get_command_info(), TimeCommand), (TimeCommand.get_command_info(), TimeCommand),
] ]
``` ```
然后修改Action和Command代码让它们读取配置 这会生成一个如下的 `config.toml` 文件
```toml
# hello_world_plugin - 自动生成的配置文件
# 我的第一个MaiCore插件包含问候功能和时间查询等基础示例
# 插件基本信息
[plugin]
# 插件名称
name = "hello_world_plugin"
# 插件版本
version = "1.0.0"
# 是否启用插件
enabled = false
# 问候功能配置
[greeting]
# 默认问候消息
message = "嗨!很开心见到你!😊"
# 是否启用表情符号
enable_emoji = true
# 时间查询配置
[time]
# 时间显示格式
format = "%Y-%m-%d %H:%M:%S"
```
然后修改Action和Command代码通过 `get_config()` 方法让它们读取配置(配置的键是命名空间式的):
```python ```python
# 在HelloAction的execute方法中 class HelloAction(BaseAction):
async def execute(self) -> Tuple[bool, str]: """问候Action - 简单的问候动作"""
# 从配置文件读取问候消息
greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = base_message + greeting_message
await self.send_text(message)
return True, "发送了问候消息"
# 在TimeCommand的execute方法中 # === 基本信息(必须填写)===
async def execute(self) -> Tuple[bool, str]: action_name = "hello_greeting"
import datetime action_description = "向用户发送问候消息"
activation_type = ActionActivationType.ALWAYS # 始终激活
# 从配置文件读取时间格式
time_format = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # === 功能描述(必须填写)===
now = datetime.datetime.now() action_parameters = {"greeting_message": "要发送的问候消息"}
time_str = now.strftime(time_format) action_require = ["需要发送友好问候时使用", "当有人向你问好时使用", "当你遇见没有见过的人时使用"]
associated_types = ["text"]
message = f"⏰ 当前时间:{time_str}"
await self.send_text(message) async def execute(self) -> Tuple[bool, str]:
return True, f"显示了当前时间: {time_str}" """执行问候动作 - 这是核心功能"""
# 发送问候消息
greeting_message = self.action_data.get("greeting_message", "")
base_message = self.get_config("greeting.message", "嗨!很开心见到你!😊")
message = base_message + greeting_message
await self.send_text(message)
return True, "发送了问候消息"
class TimeCommand(BaseCommand):
"""时间查询Command - 响应/time命令"""
command_name = "time"
command_description = "查询当前时间"
# === 命令设置(必须填写)===
command_pattern = r"^/time$" # 精确匹配 "/time" 命令
async def execute(self) -> Tuple[bool, str, bool]:
"""执行时间查询"""
import datetime
# 获取当前时间
time_format: str = self.get_config("time.format", "%Y-%m-%d %H:%M:%S") # type: ignore
now = datetime.datetime.now()
time_str = now.strftime(time_format)
# 发送时间信息
message = f"⏰ 当前时间:{time_str}"
await self.send_text(message)
return True, f"显示了当前时间: {time_str}", True
``` ```
**配置系统工作流程:** **配置系统工作流程:**
@@ -441,47 +403,20 @@ async def execute(self) -> Tuple[bool, str]:
3. **用户修改**: 用户可以修改生成的配置文件 3. **用户修改**: 用户可以修改生成的配置文件
4. **代码读取**: 使用 `self.get_config()` 读取配置值 4. **代码读取**: 使用 `self.get_config()` 读取配置值
**配置功能解释:** **绝对不要手动创建 `config.toml` 文件!**
- `self.get_config()` 可以读取配置文件中的值 更详细的配置系统介绍请参考 [配置指南](./configuration-guide.md)。
- 第一个参数是配置路径(用点分隔),第二个参数是默认值
- 配置文件会包含详细的注释和说明,用户可以轻松理解和修改
- **绝不要手动创建配置文件**,让系统自动生成
### 9. 创建说明文档(可选) ### 2. 创建说明文档
创建 `README.md` 文件来说明你的插件: 你可以创建一个 `README.md` 文件,描述插件的功能和使用方法。
```markdown ### 3. 发布到插件市场
# Hello World 插件
## 概述 如果你想让更多人使用你的插件可以将它发布到MaiCore的插件市场。
我的第一个MaiCore插件包含问候和时间查询功能。
## 功能 这部分请参考 [plugin-repo](https://github.com/Maim-with-u/plugin-repo) 的文档。
- **问候功能**: 当用户说"你好"、"hello"、"hi"时自动回复
- **时间查询**: 发送 `/time` 命令查询当前时间
## 使用方法 ---
### 问候功能
发送包含以下关键词的消息:
- "你好"
- "hello"
- "hi"
### 时间查询 🎉 恭喜你!你已经成功的创建了自己的插件了!
发送命令:`/time`
## 配置文件
插件会自动生成 `config.toml` 配置文件,用户可以修改:
- 问候消息内容
- 时间显示格式
- 插件启用状态
注意:配置文件是自动生成的,不要手动创建!
```
```
```

View File

@@ -0,0 +1,394 @@
import time
import sys
import os
import re
from typing import Dict, List, Tuple, Optional
from datetime import datetime
# Add project root to Python path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
from src.common.database.database_model import Messages, ChatStreams #noqa
def contains_emoji_or_image_tags(text: str) -> bool:
"""Check if text contains [表情包xxxxx] or [图片xxxxx] tags"""
if not text:
return False
# 检查是否包含 [表情包] 或 [图片] 标记
emoji_pattern = r'\[表情包[^\]]*\]'
image_pattern = r'\[图片[^\]]*\]'
return bool(re.search(emoji_pattern, text) or re.search(image_pattern, text))
def clean_reply_text(text: str) -> str:
"""Remove reply references like [回复 xxxx...] from text"""
if not text:
return text
# 匹配 [回复 xxxx...] 格式的内容
# 使用非贪婪匹配,匹配到第一个 ] 就停止
cleaned_text = re.sub(r'\[回复[^\]]*\]', '', text)
# 去除多余的空白字符
cleaned_text = cleaned_text.strip()
return cleaned_text
def get_chat_name(chat_id: str) -> str:
"""Get chat name from chat_id by querying ChatStreams table directly"""
try:
chat_stream = ChatStreams.get_or_none(ChatStreams.stream_id == chat_id)
if chat_stream is None:
return f"未知聊天 ({chat_id})"
if chat_stream.group_name:
return f"{chat_stream.group_name} ({chat_id})"
elif chat_stream.user_nickname:
return f"{chat_stream.user_nickname}的私聊 ({chat_id})"
else:
return f"未知聊天 ({chat_id})"
except Exception:
return f"查询失败 ({chat_id})"
def format_timestamp(timestamp: float) -> str:
"""Format timestamp to readable date string"""
try:
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, OSError):
return "未知时间"
def calculate_text_length_distribution(messages) -> Dict[str, int]:
"""Calculate distribution of processed_plain_text length"""
distribution = {
'0': 0, # 空文本
'1-5': 0, # 极短文本
'6-10': 0, # 很短文本
'11-20': 0, # 短文本
'21-30': 0, # 较短文本
'31-50': 0, # 中短文本
'51-70': 0, # 中等文本
'71-100': 0, # 较长文本
'101-150': 0, # 长文本
'151-200': 0, # 很长文本
'201-300': 0, # 超长文本
'301-500': 0, # 极长文本
'501-1000': 0, # 巨长文本
'1000+': 0 # 超巨长文本
}
for msg in messages:
if msg.processed_plain_text is None:
continue
# 排除包含表情包或图片标记的消息
if contains_emoji_or_image_tags(msg.processed_plain_text):
continue
# 清理文本中的回复引用
cleaned_text = clean_reply_text(msg.processed_plain_text)
length = len(cleaned_text)
if length == 0:
distribution['0'] += 1
elif length <= 5:
distribution['1-5'] += 1
elif length <= 10:
distribution['6-10'] += 1
elif length <= 20:
distribution['11-20'] += 1
elif length <= 30:
distribution['21-30'] += 1
elif length <= 50:
distribution['31-50'] += 1
elif length <= 70:
distribution['51-70'] += 1
elif length <= 100:
distribution['71-100'] += 1
elif length <= 150:
distribution['101-150'] += 1
elif length <= 200:
distribution['151-200'] += 1
elif length <= 300:
distribution['201-300'] += 1
elif length <= 500:
distribution['301-500'] += 1
elif length <= 1000:
distribution['501-1000'] += 1
else:
distribution['1000+'] += 1
return distribution
def get_text_length_stats(messages) -> Dict[str, float]:
"""Calculate basic statistics for processed_plain_text length"""
lengths = []
null_count = 0
excluded_count = 0 # 被排除的消息数量
for msg in messages:
if msg.processed_plain_text is None:
null_count += 1
elif contains_emoji_or_image_tags(msg.processed_plain_text):
# 排除包含表情包或图片标记的消息
excluded_count += 1
else:
# 清理文本中的回复引用
cleaned_text = clean_reply_text(msg.processed_plain_text)
lengths.append(len(cleaned_text))
if not lengths:
return {
'count': 0,
'null_count': null_count,
'excluded_count': excluded_count,
'min': 0,
'max': 0,
'avg': 0,
'median': 0
}
lengths.sort()
count = len(lengths)
return {
'count': count,
'null_count': null_count,
'excluded_count': excluded_count,
'min': min(lengths),
'max': max(lengths),
'avg': sum(lengths) / count,
'median': lengths[count // 2] if count % 2 == 1 else (lengths[count // 2 - 1] + lengths[count // 2]) / 2
}
def get_available_chats() -> List[Tuple[str, str, int]]:
"""Get all available chats with message counts"""
try:
# 获取所有有消息的chat_id排除特殊类型消息
chat_counts = {}
for msg in Messages.select(Messages.chat_id).distinct():
chat_id = msg.chat_id
count = Messages.select().where(
(Messages.chat_id == chat_id) &
(Messages.is_emoji != 1) &
(Messages.is_picid != 1) &
(Messages.is_command != 1)
).count()
if count > 0:
chat_counts[chat_id] = count
# 获取聊天名称
result = []
for chat_id, count in chat_counts.items():
chat_name = get_chat_name(chat_id)
result.append((chat_id, chat_name, count))
# 按消息数量排序
result.sort(key=lambda x: x[2], reverse=True)
return result
except Exception as e:
print(f"获取聊天列表失败: {e}")
return []
def get_time_range_input() -> Tuple[Optional[float], Optional[float]]:
"""Get time range input from user"""
print("\n时间范围选择:")
print("1. 最近1天")
print("2. 最近3天")
print("3. 最近7天")
print("4. 最近30天")
print("5. 自定义时间范围")
print("6. 不限制时间")
choice = input("请选择时间范围 (1-6): ").strip()
now = time.time()
if choice == "1":
return now - 24*3600, now
elif choice == "2":
return now - 3*24*3600, now
elif choice == "3":
return now - 7*24*3600, now
elif choice == "4":
return now - 30*24*3600, now
elif choice == "5":
print("请输入开始时间 (格式: YYYY-MM-DD HH:MM:SS):")
start_str = input().strip()
print("请输入结束时间 (格式: YYYY-MM-DD HH:MM:SS):")
end_str = input().strip()
try:
start_time = datetime.strptime(start_str, "%Y-%m-%d %H:%M:%S").timestamp()
end_time = datetime.strptime(end_str, "%Y-%m-%d %H:%M:%S").timestamp()
return start_time, end_time
except ValueError:
print("时间格式错误,将不限制时间范围")
return None, None
else:
return None, None
def get_top_longest_messages(messages, top_n: int = 10) -> List[Tuple[str, int, str, str]]:
"""Get top N longest messages"""
message_lengths = []
for msg in messages:
if msg.processed_plain_text is not None:
# 排除包含表情包或图片标记的消息
if contains_emoji_or_image_tags(msg.processed_plain_text):
continue
# 清理文本中的回复引用
cleaned_text = clean_reply_text(msg.processed_plain_text)
length = len(cleaned_text)
chat_name = get_chat_name(msg.chat_id)
time_str = format_timestamp(msg.time)
# 截取前100个字符作为预览
preview = cleaned_text[:100] + "..." if len(cleaned_text) > 100 else cleaned_text
message_lengths.append((chat_name, length, time_str, preview))
# 按长度排序取前N个
message_lengths.sort(key=lambda x: x[1], reverse=True)
return message_lengths[:top_n]
def analyze_text_lengths(chat_id: Optional[str] = None, start_time: Optional[float] = None, end_time: Optional[float] = None) -> None:
"""Analyze processed_plain_text lengths with optional filters"""
# 构建查询条件,排除特殊类型的消息
query = Messages.select().where(
(Messages.is_emoji != 1) &
(Messages.is_picid != 1) &
(Messages.is_command != 1)
)
if chat_id:
query = query.where(Messages.chat_id == chat_id)
if start_time:
query = query.where(Messages.time >= start_time)
if end_time:
query = query.where(Messages.time <= end_time)
messages = list(query)
if not messages:
print("没有找到符合条件的消息")
return
# 计算统计信息
distribution = calculate_text_length_distribution(messages)
stats = get_text_length_stats(messages)
top_longest = get_top_longest_messages(messages, 10)
# 显示结果
print("\n=== Processed Plain Text 长度分析结果 ===")
print("(已排除表情、图片ID、命令类型消息已排除[表情包]和[图片]标记消息,已清理回复引用)")
if chat_id:
print(f"聊天: {get_chat_name(chat_id)}")
else:
print("聊天: 全部聊天")
if start_time and end_time:
print(f"时间范围: {format_timestamp(start_time)}{format_timestamp(end_time)}")
elif start_time:
print(f"时间范围: {format_timestamp(start_time)} 之后")
elif end_time:
print(f"时间范围: {format_timestamp(end_time)} 之前")
else:
print("时间范围: 不限制")
print("\n基本统计:")
print(f"总消息数量: {len(messages)}")
print(f"有文本消息数量: {stats['count']}")
print(f"空文本消息数量: {stats['null_count']}")
print(f"被排除的消息数量: {stats['excluded_count']}")
if stats['count'] > 0:
print(f"最短长度: {stats['min']} 字符")
print(f"最长长度: {stats['max']} 字符")
print(f"平均长度: {stats['avg']:.2f} 字符")
print(f"中位数长度: {stats['median']:.2f} 字符")
print("\n文本长度分布:")
total = stats['count']
if total > 0:
for range_name, count in distribution.items():
if count > 0:
percentage = count / total * 100
print(f"{range_name} 字符: {count} ({percentage:.2f}%)")
# 显示最长的消息
if top_longest:
print(f"\n最长的 {len(top_longest)} 条消息:")
for i, (chat_name, length, time_str, preview) in enumerate(top_longest, 1):
print(f"{i}. [{chat_name}] {time_str}")
print(f" 长度: {length} 字符")
print(f" 预览: {preview}")
print()
def interactive_menu() -> None:
"""Interactive menu for text length analysis"""
while True:
print("\n" + "="*50)
print("Processed Plain Text 长度分析工具")
print("="*50)
print("1. 分析全部聊天")
print("2. 选择特定聊天分析")
print("q. 退出")
choice = input("\n请选择分析模式 (1-2, q): ").strip()
if choice.lower() == 'q':
print("再见!")
break
chat_id = None
if choice == "2":
# 显示可用的聊天列表
chats = get_available_chats()
if not chats:
print("没有找到聊天数据")
continue
print(f"\n可用的聊天 (共{len(chats)}个):")
for i, (_cid, name, count) in enumerate(chats, 1):
print(f"{i}. {name} ({count}条消息)")
try:
chat_choice = int(input(f"\n请选择聊天 (1-{len(chats)}): ").strip())
if 1 <= chat_choice <= len(chats):
chat_id = chats[chat_choice - 1][0]
else:
print("无效选择")
continue
except ValueError:
print("请输入有效数字")
continue
elif choice != "1":
print("无效选择")
continue
# 获取时间范围
start_time, end_time = get_time_range_input()
# 执行分析
analyze_text_lengths(chat_id, start_time, end_time)
input("\n按回车键继续...")
if __name__ == "__main__":
interactive_menu()

View File

@@ -88,11 +88,6 @@ class HeartFChatting:
self.loop_mode = ChatMode.NORMAL # 初始循环模式为普通模式 self.loop_mode = ChatMode.NORMAL # 初始循环模式为普通模式
# 新增:消息计数器和疲惫阈值
self._message_count = 0 # 发送的消息计数
self._message_threshold = max(10, int(30 * global_config.chat.focus_value))
self._fatigue_triggered = False # 是否已触发疲惫退出
self.action_manager = ActionManager() self.action_manager = ActionManager()
self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager) self.action_planner = ActionPlanner(chat_id=self.stream_id, action_manager=self.action_manager)
self.action_modifier = ActionModifier(action_manager=self.action_manager, chat_id=self.stream_id) self.action_modifier = ActionModifier(action_manager=self.action_manager, chat_id=self.stream_id)
@@ -112,7 +107,6 @@ class HeartFChatting:
self.last_read_time = time.time() - 1 self.last_read_time = time.time() - 1
self.willing_amplifier = 1
self.willing_manager = get_willing_manager() self.willing_manager = get_willing_manager()
logger.info(f"{self.log_prefix} HeartFChatting 初始化完成") logger.info(f"{self.log_prefix} HeartFChatting 初始化完成")
@@ -182,6 +176,9 @@ class HeartFChatting:
if self.loop_mode == ChatMode.NORMAL: if self.loop_mode == ChatMode.NORMAL:
self.energy_value -= 0.3 self.energy_value -= 0.3
self.energy_value = max(self.energy_value, 0.3) self.energy_value = max(self.energy_value, 0.3)
if self.loop_mode == ChatMode.FOCUS:
self.energy_value -= 0.6
self.energy_value = max(self.energy_value, 0.3)
def print_cycle_info(self, cycle_timers): def print_cycle_info(self, cycle_timers):
# 记录循环信息和计时器结果 # 记录循环信息和计时器结果
@@ -200,9 +197,9 @@ class HeartFChatting:
async def _loopbody(self): async def _loopbody(self):
if self.loop_mode == ChatMode.FOCUS: if self.loop_mode == ChatMode.FOCUS:
if await self._observe(): if await self._observe():
self.energy_value -= 1 * global_config.chat.focus_value self.energy_value -= 1 / global_config.chat.focus_value
else: else:
self.energy_value -= 3 * global_config.chat.focus_value self.energy_value -= 3 / global_config.chat.focus_value
if self.energy_value <= 1: if self.energy_value <= 1:
self.energy_value = 1 self.energy_value = 1
self.loop_mode = ChatMode.NORMAL self.loop_mode = ChatMode.NORMAL
@@ -218,15 +215,15 @@ class HeartFChatting:
limit_mode="earliest", limit_mode="earliest",
filter_bot=True, filter_bot=True,
) )
if global_config.chat.focus_value != 0:
if len(new_messages_data) > 3 / pow(global_config.chat.focus_value,0.5):
self.loop_mode = ChatMode.FOCUS
self.energy_value = 10 + (len(new_messages_data) / (3 / pow(global_config.chat.focus_value,0.5))) * 10
return True
if len(new_messages_data) > 3 * global_config.chat.focus_value: if self.energy_value >= 30:
self.loop_mode = ChatMode.FOCUS self.loop_mode = ChatMode.FOCUS
self.energy_value = 10 + (len(new_messages_data) / (3 * global_config.chat.focus_value)) * 10 return True
return True
if self.energy_value >= 30 * global_config.chat.focus_value:
self.loop_mode = ChatMode.FOCUS
return True
if new_messages_data: if new_messages_data:
earliest_messages_data = new_messages_data[0] earliest_messages_data = new_messages_data[0]
@@ -235,10 +232,10 @@ class HeartFChatting:
if_think = await self.normal_response(earliest_messages_data) if_think = await self.normal_response(earliest_messages_data)
if if_think: if if_think:
factor = max(global_config.chat.focus_value, 0.1) factor = max(global_config.chat.focus_value, 0.1)
self.energy_value *= 1.1 / factor self.energy_value *= 1.1 * factor
logger.info(f"{self.log_prefix} 进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}") logger.info(f"{self.log_prefix} 进行了思考,能量值按倍数增加,当前能量值:{self.energy_value:.1f}")
else: else:
self.energy_value += 0.1 / global_config.chat.focus_value self.energy_value += 0.1 * global_config.chat.focus_value
logger.debug(f"{self.log_prefix} 没有进行思考,能量值线性增加,当前能量值:{self.energy_value:.1f}") logger.debug(f"{self.log_prefix} 没有进行思考,能量值线性增加,当前能量值:{self.energy_value:.1f}")
logger.debug(f"{self.log_prefix} 当前能量值:{self.energy_value:.1f}") logger.debug(f"{self.log_prefix} 当前能量值:{self.energy_value:.1f}")
@@ -330,13 +327,13 @@ class HeartFChatting:
if self.loop_mode == ChatMode.NORMAL: if self.loop_mode == ChatMode.NORMAL:
if action_type == "no_action": 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: elif is_parallel:
logger.info( logger.info(
f"[{self.log_prefix}] {global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作" f"{self.log_prefix}{global_config.bot.nickname} 决定进行回复, 同时执行{action_type}动作"
) )
else: 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": if action_type == "no_action":
# 等待回复生成完毕 # 等待回复生成完毕
@@ -351,15 +348,15 @@ class HeartFChatting:
# 模型炸了,没有回复内容生成 # 模型炸了,没有回复内容生成
if not response_set: if not response_set:
logger.warning(f"[{self.log_prefix}] 模型未生成回复内容") logger.warning(f"{self.log_prefix}模型未生成回复内容")
return False return False
elif action_type not in ["no_action"] and not is_parallel: elif action_type not in ["no_action"] and not is_parallel:
logger.info( 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 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) # 发送回复 (不再需要传入 chat)
reply_text = await self._send_response(response_set, reply_to_str, loop_start_time,message_data) reply_text = await self._send_response(response_set, reply_to_str, loop_start_time,message_data)
@@ -406,8 +403,18 @@ class HeartFChatting:
if self.loop_mode == ChatMode.NORMAL: if self.loop_mode == ChatMode.NORMAL:
await self.willing_manager.after_generate_reply_handle(message_data.get("message_id", "")) await self.willing_manager.after_generate_reply_handle(message_data.get("message_id", ""))
# 管理no_reply计数器当执行了非no_reply动作时重置计数器
if action_type != "no_reply" and action_type != "no_action": if action_type != "no_reply" and action_type != "no_action":
# 导入NoReplyAction并重置计数器
from src.plugins.built_in.core_actions.no_reply import NoReplyAction
NoReplyAction.reset_consecutive_count()
logger.info(f"{self.log_prefix} 执行了{action_type}动作重置no_reply计数器")
return True return True
elif action_type == "no_action":
# 当执行回复动作时也重置no_reply计数器
from src.plugins.built_in.core_actions.no_reply import NoReplyAction
NoReplyAction.reset_consecutive_count()
logger.info(f"{self.log_prefix} 执行了回复动作重置no_reply计数器")
return True return True
@@ -501,7 +508,7 @@ class HeartFChatting:
"兴趣"模式下,判断是否回复并生成内容。 "兴趣"模式下,判断是否回复并生成内容。
""" """
interested_rate = (message_data.get("interest_value") or 0.0) * self.willing_amplifier interested_rate = (message_data.get("interest_value") or 0.0) * global_config.chat.willing_amplifier
self.willing_manager.setup(message_data, self.chat_stream) self.willing_manager.setup(message_data, self.chat_stream)
@@ -515,8 +522,8 @@ class HeartFChatting:
reply_probability += additional_config["maimcore_reply_probability_gain"] reply_probability += additional_config["maimcore_reply_probability_gain"]
reply_probability = min(max(reply_probability, 0), 1) # 确保概率在 0-1 之间 reply_probability = min(max(reply_probability, 0), 1) # 确保概率在 0-1 之间
talk_frequency = global_config.chat.get_current_talk_frequency(self.stream_id) talk_frequency = global_config.chat.get_current_talk_frequency(self.stream_id)
reply_probability = talk_frequency * reply_probability reply_probability = talk_frequency * reply_probability
# 处理表情包 # 处理表情包
if message_data.get("is_emoji") or message_data.get("is_picid"): if message_data.get("is_emoji") or message_data.get("is_picid"):
@@ -563,7 +570,7 @@ class HeartFChatting:
return reply_set return reply_set
except Exception as e: except Exception as e:
logger.error(f"[{self.log_prefix}] 回复生成出现错误:{str(e)} {traceback.format_exc()}") logger.error(f"{self.log_prefix}回复生成出现错误:{str(e)} {traceback.format_exc()}")
return None return None
async def _send_response(self, reply_set, reply_to, thinking_start_time, message_data): async def _send_response(self, reply_set, reply_to, thinking_start_time, message_data):

View File

@@ -525,9 +525,9 @@ class EmojiManager:
如果文件已被删除,则执行对象的删除方法并从列表中移除 如果文件已被删除,则执行对象的删除方法并从列表中移除
""" """
try: try:
if not self.emoji_objects: # if not self.emoji_objects:
logger.warning("[检查] emoji_objects为空跳过完整性检查") # logger.warning("[检查] emoji_objects为空跳过完整性检查")
return # return
total_count = len(self.emoji_objects) total_count = len(self.emoji_objects)
self.emoji_num = total_count self.emoji_num = total_count
@@ -707,6 +707,38 @@ class EmojiManager:
return emoji return emoji
return None # 如果循环结束还没找到,则返回 None return None # 如果循环结束还没找到,则返回 None
async def get_emoji_description_by_hash(self, emoji_hash: str) -> Optional[str]:
"""根据哈希值获取已注册表情包的描述
Args:
emoji_hash: 表情包的哈希值
Returns:
Optional[str]: 表情包描述如果未找到则返回None
"""
try:
# 先从内存中查找
emoji = await self.get_emoji_from_manager(emoji_hash)
if emoji and emoji.description:
logger.info(f"[缓存命中] 从内存获取表情包描述: {emoji.description[:50]}...")
return emoji.description
# 如果内存中没有,从数据库查找
self._ensure_db()
try:
emoji_record = Emoji.get_or_none(Emoji.emoji_hash == emoji_hash)
if emoji_record and emoji_record.description:
logger.info(f"[缓存命中] 从数据库获取表情包描述: {emoji_record.description[:50]}...")
return emoji_record.description
except Exception as e:
logger.error(f"从数据库查询表情包描述时出错: {e}")
return None
except Exception as e:
logger.error(f"获取表情包描述失败 (Hash: {emoji_hash}): {str(e)}")
return None
async def delete_emoji(self, emoji_hash: str) -> bool: async def delete_emoji(self, emoji_hash: str) -> bool:
"""根据哈希值删除表情包 """根据哈希值删除表情包

View File

@@ -330,48 +330,8 @@ class ExpressionLearner:
""" """
current_time = time.time() current_time = time.time()
# 全局衰减所有已存储的表达方式 # 全局衰减所有已存储的表达方式(直接操作数据库)
for type in ["style", "grammar"]: self._apply_global_decay_to_database(current_time)
base_dir = os.path.join("data", "expression", f"learnt_{type}")
if not os.path.exists(base_dir):
logger.debug(f"目录不存在,跳过衰减: {base_dir}")
continue
try:
chat_ids = os.listdir(base_dir)
logger.debug(f"{base_dir} 中找到 {len(chat_ids)} 个聊天ID目录进行衰减")
except Exception as e:
logger.error(f"读取目录失败 {base_dir}: {e}")
continue
for chat_id in chat_ids:
file_path = os.path.join(base_dir, chat_id, "expressions.json")
if not os.path.exists(file_path):
continue
try:
with open(file_path, "r", encoding="utf-8") as f:
expressions = json.load(f)
if not isinstance(expressions, list):
logger.warning(f"表达方式文件格式错误,跳过衰减: {file_path}")
continue
# 应用全局衰减
decayed_expressions = self.apply_decay_to_expressions(expressions, current_time)
# 保存衰减后的结果
with open(file_path, "w", encoding="utf-8") as f:
json.dump(decayed_expressions, f, ensure_ascii=False, indent=2)
logger.debug(f"已对 {file_path} 应用衰减,剩余 {len(decayed_expressions)} 个表达方式")
except json.JSONDecodeError as e:
logger.error(f"JSON解析失败跳过衰减 {file_path}: {e}")
except PermissionError as e:
logger.error(f"权限不足,无法更新 {file_path}: {e}")
except Exception as e:
logger.error(f"全局衰减{type}表达方式失败 {file_path}: {e}")
continue
learnt_style: Optional[List[Tuple[str, str, str]]] = [] learnt_style: Optional[List[Tuple[str, str, str]]] = []
learnt_grammar: Optional[List[Tuple[str, str, str]]] = [] learnt_grammar: Optional[List[Tuple[str, str, str]]] = []
@@ -388,6 +348,42 @@ class ExpressionLearner:
return learnt_style, learnt_grammar return learnt_style, learnt_grammar
def _apply_global_decay_to_database(self, current_time: float) -> None:
"""
对数据库中的所有表达方式应用全局衰减
"""
try:
# 获取所有表达方式
all_expressions = Expression.select()
updated_count = 0
deleted_count = 0
for expr in all_expressions:
# 计算时间差
last_active = expr.last_active_time
time_diff_days = (current_time - last_active) / (24 * 3600) # 转换为天
# 计算衰减值
decay_value = self.calculate_decay_factor(time_diff_days)
new_count = max(0.01, expr.count - decay_value)
if new_count <= 0.01:
# 如果count太小删除这个表达方式
expr.delete_instance()
deleted_count += 1
else:
# 更新count
expr.count = new_count
expr.save()
updated_count += 1
if updated_count > 0 or deleted_count > 0:
logger.info(f"全局衰减完成:更新了 {updated_count} 个表达方式,删除了 {deleted_count} 个表达方式")
except Exception as e:
logger.error(f"数据库全局衰减失败: {e}")
def calculate_decay_factor(self, time_diff_days: float) -> float: def calculate_decay_factor(self, time_diff_days: float) -> float:
""" """
计算衰减值 计算衰减值
@@ -410,30 +406,6 @@ class ExpressionLearner:
return min(0.01, decay) return min(0.01, decay)
def apply_decay_to_expressions(
self, expressions: List[Dict[str, Any]], current_time: float
) -> List[Dict[str, Any]]:
"""
对表达式列表应用衰减
返回衰减后的表达式列表移除count小于0的项
"""
result = []
for expr in expressions:
# 确保last_active_time存在如果不存在则使用current_time
if "last_active_time" not in expr:
expr["last_active_time"] = current_time
last_active = expr["last_active_time"]
time_diff_days = (current_time - last_active) / (24 * 3600) # 转换为天
decay_value = self.calculate_decay_factor(time_diff_days)
expr["count"] = max(0.01, expr.get("count", 1) - decay_value)
if expr["count"] > 0:
result.append(expr)
return result
async def learn_and_store(self, type: str, num: int = 10) -> List[Tuple[str, str, str]]: async def learn_and_store(self, type: str, num: int = 10) -> List[Tuple[str, str, str]]:
# sourcery skip: use-join # sourcery skip: use-join
""" """

View File

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

View File

@@ -12,6 +12,7 @@ from src.chat.message_receive.storage import MessageStorage
from src.chat.heart_flow.heartflow import heartflow from src.chat.heart_flow.heartflow import heartflow
from src.chat.utils.utils import is_mentioned_bot_in_message from src.chat.utils.utils import is_mentioned_bot_in_message
from src.chat.utils.timer_calculator import Timer from src.chat.utils.timer_calculator import Timer
from src.chat.utils.chat_message_builder import replace_user_references_sync
from src.common.logger import get_logger from src.common.logger import get_logger
from src.person_info.relationship_manager import get_relationship_manager from src.person_info.relationship_manager import get_relationship_manager
from src.mood.mood_manager import mood_manager from src.mood.mood_manager import mood_manager
@@ -56,16 +57,41 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
with Timer("记忆激活"): with Timer("记忆激活"):
interested_rate = await hippocampus_manager.get_activate_from_text( interested_rate = await hippocampus_manager.get_activate_from_text(
message.processed_plain_text, message.processed_plain_text,
max_depth= 5,
fast_retrieval=False, fast_retrieval=False,
) )
logger.debug(f"记忆激活率: {interested_rate:.2f}") logger.debug(f"记忆激活率: {interested_rate:.2f}")
text_len = len(message.processed_plain_text) text_len = len(message.processed_plain_text)
# 根据文本长度调整兴趣度,长度越大兴趣度越高但增长率递减最低0.01最高0.05 # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算
# 采用对数函数实现递减增长 # 基于实际分布0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%)
base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) if text_len == 0:
base_interest = min(max(base_interest, 0.01), 0.05) base_interest = 0.01 # 空消息最低兴趣度
elif text_len <= 5:
# 1-5字符线性增长 0.01 -> 0.03
base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4
elif text_len <= 10:
# 6-10字符线性增长 0.03 -> 0.06
base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5
elif text_len <= 20:
# 11-20字符线性增长 0.06 -> 0.12
base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10
elif text_len <= 30:
# 21-30字符线性增长 0.12 -> 0.18
base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10
elif text_len <= 50:
# 31-50字符线性增长 0.18 -> 0.22
base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20
elif text_len <= 100:
# 51-100字符线性增长 0.22 -> 0.26
base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50
else:
# 100+字符:对数增长 0.26 -> 0.3,增长率递减
base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901
# 确保在范围内
base_interest = min(max(base_interest, 0.01), 0.3)
interested_rate += base_interest interested_rate += base_interest
@@ -123,6 +149,13 @@ class HeartFCMessageReceiver:
# 如果消息中包含图片标识,则将 [picid:...] 替换为 [图片] # 如果消息中包含图片标识,则将 [picid:...] 替换为 [图片]
picid_pattern = r"\[picid:([^\]]+)\]" picid_pattern = r"\[picid:([^\]]+)\]"
processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text) processed_plain_text = re.sub(picid_pattern, "[图片]", message.processed_plain_text)
# 应用用户引用格式替换,将回复<aaa:bbb>和@<aaa:bbb>格式转换为可读格式
processed_plain_text = replace_user_references_sync(
processed_plain_text,
message.message_info.platform, # type: ignore
replace_bot_name=True
)
logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}]") # type: ignore logger.info(f"[{mes_name}]{userinfo.user_nickname}:{processed_plain_text}[兴趣度:{interested_rate:.2f}]") # type: ignore

View File

@@ -224,10 +224,16 @@ class Hippocampus:
return hash((source, target)) return hash((source, target))
@staticmethod @staticmethod
def find_topic_llm(text, topic_num): def find_topic_llm(text: str, topic_num: int | list[int]):
# sourcery skip: inline-immediately-returned-variable # sourcery skip: inline-immediately-returned-variable
topic_num_str = ""
if isinstance(topic_num, list):
topic_num_str = f"{topic_num[0]}-{topic_num[1]}"
else:
topic_num_str = topic_num
prompt = ( prompt = (
f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来," f"这是一段文字:\n{text}\n\n请你从这段话中总结出最多{topic_num_str}个关键的概念,可以是名词,动词,或者特定人物,帮我列出来,"
f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。" f"将主题用逗号隔开,并加上<>,例如<主题1>,<主题2>......尽可能精简。只需要列举最多{topic_num}个话题就好,不要有序号,不要告诉我其他内容。"
f"如果确定找不出主题或者没有明显主题,返回<none>。" f"如果确定找不出主题或者没有明显主题,返回<none>。"
) )
@@ -300,6 +306,60 @@ class Hippocampus:
memories.sort(key=lambda x: x[2], reverse=True) memories.sort(key=lambda x: x[2], reverse=True)
return memories return memories
async def get_keywords_from_text(self, text: str) -> list:
"""从文本中提取关键词。
Args:
text (str): 输入文本
fast_retrieval (bool, optional): 是否使用快速检索。默认为False。
如果为True使用jieba分词提取关键词速度更快但可能不够准确。
如果为False使用LLM提取关键词速度较慢但更准确。
"""
if not text:
return []
# 使用LLM提取关键词 - 根据详细文本长度分布优化topic_num计算
text_length = len(text)
topic_num: int | list[int] = 0
if text_length <= 5:
words = jieba.cut(text)
keywords = [word for word in words if len(word) > 1]
keywords = list(set(keywords))[:3] # 限制最多3个关键词
if keywords:
logger.info(f"提取关键词: {keywords}")
return keywords
elif text_length <= 10:
topic_num = [1, 3] # 6-10字符: 1个关键词 (27.18%的文本)
elif text_length <= 20:
topic_num = [2, 4] # 11-20字符: 2个关键词 (22.76%的文本)
elif text_length <= 30:
topic_num = [3, 5] # 21-30字符: 3个关键词 (10.33%的文本)
elif text_length <= 50:
topic_num = [4, 5] # 31-50字符: 4个关键词 (9.79%的文本)
else:
topic_num = 5 # 51+字符: 5个关键词 (其余长文本)
topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async(
self.find_topic_llm(text, topic_num)
)
# 提取关键词
keywords = re.findall(r"<([^>]+)>", topics_response)
if not keywords:
keywords = []
else:
keywords = [
keyword.strip()
for keyword in ",".join(keywords).replace("", ",").replace("", ",").replace(" ", ",").split(",")
if keyword.strip()
]
if keywords:
logger.info(f"提取关键词: {keywords}")
return keywords
async def get_memory_from_text( async def get_memory_from_text(
self, self,
text: str, text: str,
@@ -325,39 +385,7 @@ class Hippocampus:
- memory_items: list, 该主题下的记忆项列表 - memory_items: list, 该主题下的记忆项列表
- similarity: float, 与文本的相似度 - similarity: float, 与文本的相似度
""" """
if not text: keywords = await self.get_keywords_from_text(text)
return []
if fast_retrieval:
# 使用jieba分词提取关键词
words = jieba.cut(text)
# 过滤掉停用词和单字词
keywords = [word for word in words if len(word) > 1]
# 去重
keywords = list(set(keywords))
# 限制关键词数量
logger.debug(f"提取关键词: {keywords}")
else:
# 使用LLM提取关键词
topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量
# logger.info(f"提取关键词数量: {topic_num}")
topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async(
self.find_topic_llm(text, topic_num)
)
# 提取关键词
keywords = re.findall(r"<([^>]+)>", topics_response)
if not keywords:
keywords = []
else:
keywords = [
keyword.strip()
for keyword in ",".join(keywords).replace("", ",").replace("", ",").replace(" ", ",").split(",")
if keyword.strip()
]
# logger.info(f"提取的关键词: {', '.join(keywords)}")
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
@@ -679,38 +707,7 @@ class Hippocampus:
Returns: Returns:
float: 激活节点数与总节点数的比值 float: 激活节点数与总节点数的比值
""" """
if not text: keywords = await self.get_keywords_from_text(text)
return 0
if fast_retrieval:
# 使用jieba分词提取关键词
words = jieba.cut(text)
# 过滤掉停用词和单字词
keywords = [word for word in words if len(word) > 1]
# 去重
keywords = list(set(keywords))
# 限制关键词数量
keywords = keywords[:5]
else:
# 使用LLM提取关键词
topic_num = min(5, max(1, int(len(text) * 0.1))) # 根据文本长度动态调整关键词数量
# logger.info(f"提取关键词数量: {topic_num}")
topics_response, (reasoning_content, model_name) = await self.model_summary.generate_response_async(
self.find_topic_llm(text, topic_num)
)
# 提取关键词
keywords = re.findall(r"<([^>]+)>", topics_response)
if not keywords:
keywords = []
else:
keywords = [
keyword.strip()
for keyword in ",".join(keywords).replace("", ",").replace("", ",").replace(" ", ",").split(",")
if keyword.strip()
]
# logger.info(f"提取的关键词: {', '.join(keywords)}")
# 过滤掉不存在于记忆图中的关键词 # 过滤掉不存在于记忆图中的关键词
valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G] valid_keywords = [keyword for keyword in keywords if keyword in self.memory_graph.G]
@@ -727,7 +724,7 @@ class Hippocampus:
for keyword in valid_keywords: for keyword in valid_keywords:
logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):") logger.debug(f"开始以关键词 '{keyword}' 为中心进行扩散检索 (最大深度: {max_depth}):")
# 初始化激活值 # 初始化激活值
activation_values = {keyword: 1.0} activation_values = {keyword: 1.5}
# 记录已访问的节点 # 记录已访问的节点
visited_nodes = {keyword} visited_nodes = {keyword}
# 待处理的节点队列,每个元素是(节点, 激活值, 当前深度) # 待处理的节点队列,每个元素是(节点, 激活值, 当前深度)
@@ -1315,6 +1312,7 @@ class ParahippocampalGyrus:
return compressed_memory, similar_topics_dict return compressed_memory, similar_topics_dict
async def operation_build_memory(self): async def operation_build_memory(self):
# sourcery skip: merge-list-appends-into-extend
logger.info("------------------------------------开始构建记忆--------------------------------------") logger.info("------------------------------------开始构建记忆--------------------------------------")
start_time = time.time() start_time = time.time()
memory_samples = self.hippocampus.entorhinal_cortex.get_memory_sample() memory_samples = self.hippocampus.entorhinal_cortex.get_memory_sample()

View File

@@ -17,7 +17,11 @@ from src.chat.message_receive.uni_message_sender import HeartFCSender
from src.chat.utils.timer_calculator import Timer # <--- Import Timer from src.chat.utils.timer_calculator import Timer # <--- Import Timer
from src.chat.utils.utils import get_chat_type_and_target_info from src.chat.utils.utils import get_chat_type_and_target_info
from src.chat.utils.prompt_builder import Prompt, global_prompt_manager from src.chat.utils.prompt_builder import Prompt, global_prompt_manager
from src.chat.utils.chat_message_builder import build_readable_messages, get_raw_msg_before_timestamp_with_chat from src.chat.utils.chat_message_builder import (
build_readable_messages,
get_raw_msg_before_timestamp_with_chat,
replace_user_references_sync,
)
from src.chat.express.expression_selector import expression_selector from src.chat.express.expression_selector import expression_selector
from src.chat.knowledge.knowledge_lib import qa_manager from src.chat.knowledge.knowledge_lib import qa_manager
from src.chat.memory_system.memory_activator import MemoryActivator from src.chat.memory_system.memory_activator import MemoryActivator
@@ -30,6 +34,7 @@ from src.plugin_system.base.component_types import ActionInfo
logger = get_logger("replyer") logger = get_logger("replyer")
def init_prompt(): def init_prompt():
Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1") Prompt("你正在qq群里聊天下面是群里在聊的内容", "chat_target_group1")
Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1") Prompt("你正在和{sender_name}聊天,这是你们之前聊的内容:", "chat_target_private1")
@@ -356,17 +361,20 @@ class DefaultReplyer:
expression_habits_block = "" expression_habits_block = ""
expression_habits_title = "" expression_habits_title = ""
if style_habits_str.strip(): if style_habits_str.strip():
expression_habits_title = "你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:" expression_habits_title = (
"你可以参考以下的语言习惯,当情景合适就使用,但不要生硬使用,以合理的方式结合到你的回复中:"
)
expression_habits_block += f"{style_habits_str}\n" expression_habits_block += f"{style_habits_str}\n"
if grammar_habits_str.strip(): if grammar_habits_str.strip():
expression_habits_title = "你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:" expression_habits_title = (
"你可以选择下面的句法进行回复,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式使用:"
)
expression_habits_block += f"{grammar_habits_str}\n" expression_habits_block += f"{grammar_habits_str}\n"
if style_habits_str.strip() and grammar_habits_str.strip(): if style_habits_str.strip() and grammar_habits_str.strip():
expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中:" expression_habits_title = "你可以参考以下的语言习惯和句法,如果情景合适就使用,不要盲目使用,不要生硬使用,以合理的方式结合到你的回复中:"
expression_habits_block = f"{expression_habits_title}\n{expression_habits_block}" expression_habits_block = f"{expression_habits_title}\n{expression_habits_block}"
return expression_habits_block return expression_habits_block
@@ -375,27 +383,27 @@ class DefaultReplyer:
return "" return ""
instant_memory = None instant_memory = None
running_memories = await self.memory_activator.activate_memory_with_chat_history( running_memories = await self.memory_activator.activate_memory_with_chat_history(
target_message=target, chat_history_prompt=chat_history target_message=target, chat_history_prompt=chat_history
) )
if global_config.memory.enable_instant_memory: if global_config.memory.enable_instant_memory:
asyncio.create_task(self.instant_memory.create_and_store_memory(chat_history)) asyncio.create_task(self.instant_memory.create_and_store_memory(chat_history))
instant_memory = await self.instant_memory.get_memory(target) instant_memory = await self.instant_memory.get_memory(target)
logger.info(f"即时记忆:{instant_memory}") logger.info(f"即时记忆:{instant_memory}")
if not running_memories: if not running_memories:
return "" return ""
memory_str = "以下是当前在聊天中,你回忆起的记忆:\n" memory_str = "以下是当前在聊天中,你回忆起的记忆:\n"
for running_memory in running_memories: for running_memory in running_memories:
memory_str += f"- {running_memory['content']}\n" memory_str += f"- {running_memory['content']}\n"
if instant_memory: if instant_memory:
memory_str += f"- {instant_memory}\n" memory_str += f"- {instant_memory}\n"
return memory_str return memory_str
async def build_tool_info(self, chat_history, reply_data: Optional[Dict], enable_tool: bool = True): async def build_tool_info(self, chat_history, reply_data: Optional[Dict], enable_tool: bool = True):
@@ -438,7 +446,7 @@ class DefaultReplyer:
tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。" tool_info_str += "以上是你获取到的实时信息,请在回复时参考这些信息。"
logger.info(f"获取到 {len(tool_results)} 个工具结果") logger.info(f"获取到 {len(tool_results)} 个工具结果")
return tool_info_str return tool_info_str
else: else:
logger.debug("未获取到任何工具结果") logger.debug("未获取到任何工具结果")
@@ -469,7 +477,7 @@ class DefaultReplyer:
# 添加None检查防止NoneType错误 # 添加None检查防止NoneType错误
if target is None: if target is None:
return keywords_reaction_prompt return keywords_reaction_prompt
# 处理关键词规则 # 处理关键词规则
for rule in global_config.keyword_reaction.keyword_rules: for rule in global_config.keyword_reaction.keyword_rules:
if any(keyword in target for keyword in rule.keywords): if any(keyword in target for keyword in rule.keywords):
@@ -621,7 +629,7 @@ class DefaultReplyer:
is_group_chat = bool(chat_stream.group_info) is_group_chat = bool(chat_stream.group_info)
reply_to = reply_data.get("reply_to", "none") reply_to = reply_data.get("reply_to", "none")
extra_info_block = reply_data.get("extra_info", "") or reply_data.get("extra_info_block", "") extra_info_block = reply_data.get("extra_info", "") or reply_data.get("extra_info_block", "")
if global_config.mood.enable_mood: if global_config.mood.enable_mood:
chat_mood = mood_manager.get_mood_by_chat_id(chat_id) chat_mood = mood_manager.get_mood_by_chat_id(chat_id)
mood_prompt = chat_mood.mood_state mood_prompt = chat_mood.mood_state
@@ -630,6 +638,8 @@ class DefaultReplyer:
sender, target = self._parse_reply_target(reply_to) sender, target = self._parse_reply_target(reply_to)
target = replace_user_references_sync(target, chat_stream.platform, replace_bot_name=True)
# 构建action描述 (如果启用planner) # 构建action描述 (如果启用planner)
action_descriptions = "" action_descriptions = ""
if available_actions: if available_actions:
@@ -679,25 +689,21 @@ class DefaultReplyer:
self._time_and_run_task( self._time_and_run_task(
self.build_expression_habits(chat_talking_prompt_short, target), "expression_habits" self.build_expression_habits(chat_talking_prompt_short, target), "expression_habits"
), ),
self._time_and_run_task( self._time_and_run_task(self.build_relation_info(reply_data), "relation_info"),
self.build_relation_info(reply_data), "relation_info"
),
self._time_and_run_task(self.build_memory_block(chat_talking_prompt_short, target), "memory_block"), self._time_and_run_task(self.build_memory_block(chat_talking_prompt_short, target), "memory_block"),
self._time_and_run_task( self._time_and_run_task(
self.build_tool_info(chat_talking_prompt_short, reply_data, enable_tool=enable_tool), "tool_info" self.build_tool_info(chat_talking_prompt_short, reply_data, enable_tool=enable_tool), "tool_info"
), ),
self._time_and_run_task( self._time_and_run_task(get_prompt_info(target, threshold=0.38), "prompt_info"),
get_prompt_info(target, threshold=0.38), "prompt_info"
),
) )
# 任务名称中英文映射 # 任务名称中英文映射
task_name_mapping = { task_name_mapping = {
"expression_habits": "选取表达方式", "expression_habits": "选取表达方式",
"relation_info": "感受关系", "relation_info": "感受关系",
"memory_block": "回忆", "memory_block": "回忆",
"tool_info": "使用工具", "tool_info": "使用工具",
"prompt_info": "获取知识" "prompt_info": "获取知识",
} }
# 处理结果 # 处理结果
@@ -790,7 +796,7 @@ class DefaultReplyer:
core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts( core_dialogue_prompt, background_dialogue_prompt = self.build_s4u_chat_history_prompts(
message_list_before_now_long, target_user_id message_list_before_now_long, target_user_id
) )
self.build_mai_think_context( self.build_mai_think_context(
chat_id=chat_id, chat_id=chat_id,
memory_block=memory_block, memory_block=memory_block,
@@ -807,9 +813,8 @@ class DefaultReplyer:
-------------------------------- --------------------------------
{time_block} {time_block}
这是你和{sender}的对话,你们正在交流中: 这是你和{sender}的对话,你们正在交流中:
{core_dialogue_prompt}""" {core_dialogue_prompt}""",
) )
# 使用 s4u 风格的模板 # 使用 s4u 风格的模板
template_name = "s4u_style_prompt" template_name = "s4u_style_prompt"
@@ -847,9 +852,9 @@ class DefaultReplyer:
identity_block=identity_block, identity_block=identity_block,
sender=sender, sender=sender,
target=target, target=target,
chat_info=chat_talking_prompt chat_info=chat_talking_prompt,
) )
# 使用原有的模式 # 使用原有的模式
return await global_prompt_manager.format_prompt( return await global_prompt_manager.format_prompt(
template_name, template_name,
@@ -1071,9 +1076,11 @@ async def get_prompt_info(message: str, threshold: float):
related_info += found_knowledge_from_lpmm related_info += found_knowledge_from_lpmm
logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}") logger.debug(f"获取知识库内容耗时: {(end_time - start_time):.3f}")
logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}") logger.debug(f"获取知识库内容,相关信息:{related_info[:100]}...,信息长度: {len(related_info)}")
# 格式化知识信息 # 格式化知识信息
formatted_prompt_info = await global_prompt_manager.format_prompt("knowledge_prompt", prompt_info=related_info) formatted_prompt_info = await global_prompt_manager.format_prompt(
"knowledge_prompt", prompt_info=related_info
)
return formatted_prompt_info return formatted_prompt_info
else: else:
logger.debug("从LPMM知识库获取知识失败可能是从未导入过知识返回空知识...") logger.debug("从LPMM知识库获取知识失败可能是从未导入过知识返回空知识...")

View File

@@ -2,7 +2,7 @@ import time # 导入 time 模块以获取当前时间
import random import random
import re import re
from typing import List, Dict, Any, Tuple, Optional from typing import List, Dict, Any, Tuple, Optional, Callable
from rich.traceback import install from rich.traceback import install
from src.config.config import global_config from src.config.config import global_config
@@ -10,11 +10,161 @@ from src.common.message_repository import find_messages, count_messages
from src.common.database.database_model import ActionRecords from src.common.database.database_model import ActionRecords
from src.common.database.database_model import Images from src.common.database.database_model import Images
from src.person_info.person_info import PersonInfoManager, get_person_info_manager from src.person_info.person_info import PersonInfoManager, get_person_info_manager
from src.chat.utils.utils import translate_timestamp_to_human_readable,assign_message_ids from src.chat.utils.utils import translate_timestamp_to_human_readable, assign_message_ids
install(extra_lines=3) install(extra_lines=3)
def replace_user_references_sync(
content: str,
platform: str,
name_resolver: Optional[Callable[[str, str], str]] = None,
replace_bot_name: bool = True,
) -> str:
"""
替换内容中的用户引用格式,包括回复<aaa:bbb>和@<aaa:bbb>格式
Args:
content: 要处理的内容字符串
platform: 平台标识
name_resolver: 名称解析函数,接收(platform, user_id)参数,返回用户名称
如果为None则使用默认的person_info_manager
replace_bot_name: 是否将机器人的user_id替换为"机器人昵称(你)"
Returns:
str: 处理后的内容字符串
"""
if name_resolver is None:
person_info_manager = get_person_info_manager()
def default_resolver(platform: str, user_id: str) -> str:
# 检查是否是机器人自己
if replace_bot_name and user_id == global_config.bot.qq_account:
return f"{global_config.bot.nickname}(你)"
person_id = PersonInfoManager.get_person_id(platform, user_id)
return person_info_manager.get_value_sync(person_id, "person_name") or user_id # type: ignore
name_resolver = default_resolver
# 处理回复<aaa:bbb>格式
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
match = re.search(reply_pattern, content)
if match:
aaa = match[1]
bbb = match[2]
try:
# 检查是否是机器人自己
if replace_bot_name and bbb == global_config.bot.qq_account:
reply_person_name = f"{global_config.bot.nickname}(你)"
else:
reply_person_name = name_resolver(platform, bbb) or aaa
content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1)
except Exception:
# 如果解析失败,使用原始昵称
content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1)
# 处理@<aaa:bbb>格式
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
at_matches = list(re.finditer(at_pattern, content))
if at_matches:
new_content = ""
last_end = 0
for m in at_matches:
new_content += content[last_end : m.start()]
aaa = m.group(1)
bbb = m.group(2)
try:
# 检查是否是机器人自己
if replace_bot_name and bbb == global_config.bot.qq_account:
at_person_name = f"{global_config.bot.nickname}(你)"
else:
at_person_name = name_resolver(platform, bbb) or aaa
new_content += f"@{at_person_name}"
except Exception:
# 如果解析失败,使用原始昵称
new_content += f"@{aaa}"
last_end = m.end()
new_content += content[last_end:]
content = new_content
return content
async def replace_user_references_async(
content: str,
platform: str,
name_resolver: Optional[Callable[[str, str], Any]] = None,
replace_bot_name: bool = True,
) -> str:
"""
替换内容中的用户引用格式,包括回复<aaa:bbb>和@<aaa:bbb>格式
Args:
content: 要处理的内容字符串
platform: 平台标识
name_resolver: 名称解析函数,接收(platform, user_id)参数,返回用户名称
如果为None则使用默认的person_info_manager
replace_bot_name: 是否将机器人的user_id替换为"机器人昵称(你)"
Returns:
str: 处理后的内容字符串
"""
if name_resolver is None:
person_info_manager = get_person_info_manager()
async def default_resolver(platform: str, user_id: str) -> str:
# 检查是否是机器人自己
if replace_bot_name and user_id == global_config.bot.qq_account:
return f"{global_config.bot.nickname}(你)"
person_id = PersonInfoManager.get_person_id(platform, user_id)
return await person_info_manager.get_value(person_id, "person_name") or user_id # type: ignore
name_resolver = default_resolver
# 处理回复<aaa:bbb>格式
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>"
match = re.search(reply_pattern, content)
if match:
aaa = match.group(1)
bbb = match.group(2)
try:
# 检查是否是机器人自己
if replace_bot_name and bbb == global_config.bot.qq_account:
reply_person_name = f"{global_config.bot.nickname}(你)"
else:
reply_person_name = await name_resolver(platform, bbb) or aaa
content = re.sub(reply_pattern, f"回复 {reply_person_name}", content, count=1)
except Exception:
# 如果解析失败,使用原始昵称
content = re.sub(reply_pattern, f"回复 {aaa}", content, count=1)
# 处理@<aaa:bbb>格式
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
at_matches = list(re.finditer(at_pattern, content))
if at_matches:
new_content = ""
last_end = 0
for m in at_matches:
new_content += content[last_end : m.start()]
aaa = m.group(1)
bbb = m.group(2)
try:
# 检查是否是机器人自己
if replace_bot_name and bbb == global_config.bot.qq_account:
at_person_name = f"{global_config.bot.nickname}(你)"
else:
at_person_name = await name_resolver(platform, bbb) or aaa
new_content += f"@{at_person_name}"
except Exception:
# 如果解析失败,使用原始昵称
new_content += f"@{aaa}"
last_end = m.end()
new_content += content[last_end:]
content = new_content
return content
def get_raw_msg_by_timestamp( def get_raw_msg_by_timestamp(
timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest" timestamp_start: float, timestamp_end: float, limit: int = 0, limit_mode: str = "latest"
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
@@ -374,33 +524,8 @@ def _build_readable_messages_internal(
else: else:
person_name = "某人" person_name = "某人"
# 检查是否有 回复<aaa:bbb> 字段 # 使用独立函数处理用户引用格式
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" content = replace_user_references_sync(content, platform, replace_bot_name=replace_bot_name)
match = re.search(reply_pattern, content)
if match:
aaa: str = match[1]
bbb: str = match[2]
reply_person_id = PersonInfoManager.get_person_id(platform, bbb)
reply_person_name = person_info_manager.get_value_sync(reply_person_id, "person_name") or aaa
# 在内容前加上回复信息
content = re.sub(reply_pattern, lambda m, name=reply_person_name: f"回复 {name}", content, count=1)
# 检查是否有 @<aaa:bbb> 字段 @<{member_info.get('nickname')}:{member_info.get('user_id')}>
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
at_matches = list(re.finditer(at_pattern, content))
if at_matches:
new_content = ""
last_end = 0
for m in at_matches:
new_content += content[last_end : m.start()]
aaa = m.group(1)
bbb = m.group(2)
at_person_id = PersonInfoManager.get_person_id(platform, bbb)
at_person_name = person_info_manager.get_value_sync(at_person_id, "person_name") or aaa
new_content += f"@{at_person_name}"
last_end = m.end()
new_content += content[last_end:]
content = new_content
target_str = "这是QQ的一个功能用于提及某人但没那么明显" target_str = "这是QQ的一个功能用于提及某人但没那么明显"
if target_str in content and random.random() < 0.6: if target_str in content and random.random() < 0.6:
@@ -654,6 +779,7 @@ async def build_readable_messages_with_list(
return formatted_string, details_list return formatted_string, details_list
def build_readable_messages_with_id( def build_readable_messages_with_id(
messages: List[Dict[str, Any]], messages: List[Dict[str, Any]],
replace_bot_name: bool = True, replace_bot_name: bool = True,
@@ -669,9 +795,9 @@ def build_readable_messages_with_id(
允许通过参数控制格式化行为。 允许通过参数控制格式化行为。
""" """
message_id_list = assign_message_ids(messages) message_id_list = assign_message_ids(messages)
formatted_string = build_readable_messages( formatted_string = build_readable_messages(
messages = messages, messages=messages,
replace_bot_name=replace_bot_name, replace_bot_name=replace_bot_name,
merge_messages=merge_messages, merge_messages=merge_messages,
timestamp_mode=timestamp_mode, timestamp_mode=timestamp_mode,
@@ -682,10 +808,7 @@ def build_readable_messages_with_id(
message_id_list=message_id_list, message_id_list=message_id_list,
) )
return formatted_string, message_id_list
return formatted_string , message_id_list
def build_readable_messages( def build_readable_messages(
@@ -770,7 +893,13 @@ def build_readable_messages(
if read_mark <= 0: if read_mark <= 0:
# 没有有效的 read_mark直接格式化所有消息 # 没有有效的 read_mark直接格式化所有消息
formatted_string, _, pic_id_mapping, _ = _build_readable_messages_internal( formatted_string, _, pic_id_mapping, _ = _build_readable_messages_internal(
copy_messages, replace_bot_name, merge_messages, timestamp_mode, truncate, show_pic=show_pic, message_id_list=message_id_list copy_messages,
replace_bot_name,
merge_messages,
timestamp_mode,
truncate,
show_pic=show_pic,
message_id_list=message_id_list,
) )
# 生成图片映射信息并添加到最前面 # 生成图片映射信息并添加到最前面
@@ -893,7 +1022,7 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str:
for msg in messages: for msg in messages:
try: try:
platform = msg.get("chat_info_platform") platform: str = msg.get("chat_info_platform") # type: ignore
user_id = msg.get("user_id") user_id = msg.get("user_id")
_timestamp = msg.get("time") _timestamp = msg.get("time")
content: str = "" content: str = ""
@@ -916,38 +1045,14 @@ async def build_anonymous_messages(messages: List[Dict[str, Any]]) -> str:
anon_name = get_anon_name(platform, user_id) anon_name = get_anon_name(platform, user_id)
# print(f"anon_name:{anon_name}") # print(f"anon_name:{anon_name}")
# 处理 回复<aaa:bbb> # 使用独立函数处理用户引用格式,传入自定义的匿名名称解析器
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" def anon_name_resolver(platform: str, user_id: str) -> str:
match = re.search(reply_pattern, content)
if match:
# print(f"发现回复match:{match}")
bbb = match.group(2)
try: try:
anon_reply = get_anon_name(platform, bbb) return get_anon_name(platform, user_id)
# print(f"anon_reply:{anon_reply}")
except Exception: except Exception:
anon_reply = "?" return "?"
content = re.sub(reply_pattern, f"回复 {anon_reply}", content, count=1)
# 处理 @<aaa:bbb>无嵌套def content = replace_user_references_sync(content, platform, anon_name_resolver, replace_bot_name=False)
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
at_matches = list(re.finditer(at_pattern, content))
if at_matches:
# print(f"发现@match:{at_matches}")
new_content = ""
last_end = 0
for m in at_matches:
new_content += content[last_end : m.start()]
bbb = m.group(2)
try:
anon_at = get_anon_name(platform, bbb)
# print(f"anon_at:{anon_at}")
except Exception:
anon_at = "?"
new_content += f"@{anon_at}"
last_end = m.end()
new_content += content[last_end:]
content = new_content
header = f"{anon_name}" header = f"{anon_name}"
output_lines.append(header) output_lines.append(header)

View File

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

View File

@@ -28,7 +28,7 @@ class ClassicalWillingManager(BaseWillingManager):
# print(f"[{chat_id}] 回复意愿: {current_willing}") # print(f"[{chat_id}] 回复意愿: {current_willing}")
interested_rate = willing_info.interested_rate * global_config.normal_chat.response_interested_rate_amplifier interested_rate = willing_info.interested_rate
# print(f"[{chat_id}] 兴趣值: {interested_rate}") # print(f"[{chat_id}] 兴趣值: {interested_rate}")
@@ -36,20 +36,18 @@ class ClassicalWillingManager(BaseWillingManager):
current_willing += interested_rate - 0.2 current_willing += interested_rate - 0.2
if willing_info.is_mentioned_bot and global_config.chat.mentioned_bot_inevitable_reply and current_willing < 2: if willing_info.is_mentioned_bot and global_config.chat.mentioned_bot_inevitable_reply and current_willing < 2:
current_willing += 1 if current_willing < 1.0 else 0.05 current_willing += 1 if current_willing < 1.0 else 0.2
self.chat_reply_willing[chat_id] = min(current_willing, 1.0) self.chat_reply_willing[chat_id] = min(current_willing, 1.0)
reply_probability = min(max((current_willing - 0.5), 0.01) * 2, 1) reply_probability = min(max((current_willing - 0.5), 0.01) * 2, 1.5)
# print(f"[{chat_id}] 回复概率: {reply_probability}") # print(f"[{chat_id}] 回复概率: {reply_probability}")
return reply_probability return reply_probability
async def before_generate_reply_handle(self, message_id): async def before_generate_reply_handle(self, message_id):
chat_id = self.ongoing_messages[message_id].chat_id pass
current_willing = self.chat_reply_willing.get(chat_id, 0)
self.chat_reply_willing[chat_id] = max(0.0, current_willing - 1.8)
async def after_generate_reply_handle(self, message_id): async def after_generate_reply_handle(self, message_id):
if message_id not in self.ongoing_messages: if message_id not in self.ongoing_messages:
@@ -58,7 +56,7 @@ class ClassicalWillingManager(BaseWillingManager):
chat_id = self.ongoing_messages[message_id].chat_id chat_id = self.ongoing_messages[message_id].chat_id
current_willing = self.chat_reply_willing.get(chat_id, 0) current_willing = self.chat_reply_willing.get(chat_id, 0)
if current_willing < 1: if current_willing < 1:
self.chat_reply_willing[chat_id] = min(1.0, current_willing + 0.4) self.chat_reply_willing[chat_id] = min(1.0, current_willing + 0.3)
async def not_reply_handle(self, message_id): async def not_reply_handle(self, message_id):
return await super().not_reply_handle(message_id) return await super().not_reply_handle(message_id)

View File

@@ -36,7 +36,7 @@ def compare_dicts(new, old, path=None, new_comments=None, old_comments=None, log
continue continue
if key not in old: if key not in old:
comment = get_key_comment(new, key) comment = get_key_comment(new, key)
logs.append(f"新增: {'.'.join(path + [str(key)])} 注释: {comment if comment else ''}") logs.append(f"新增: {'.'.join(path + [str(key)])} 注释: {comment or ''}")
elif isinstance(new[key], (dict, Table)) and isinstance(old.get(key), (dict, Table)): elif isinstance(new[key], (dict, Table)) and isinstance(old.get(key), (dict, Table)):
compare_dicts(new[key], old[key], path + [str(key)], new_comments, old_comments, logs) compare_dicts(new[key], old[key], path + [str(key)], new_comments, old_comments, logs)
# 删减项 # 删减项
@@ -45,7 +45,7 @@ def compare_dicts(new, old, path=None, new_comments=None, old_comments=None, log
continue continue
if key not in new: if key not in new:
comment = get_key_comment(old, key) comment = get_key_comment(old, key)
logs.append(f"删减: {'.'.join(path + [str(key)])} 注释: {comment if comment else ''}") logs.append(f"删减: {'.'.join(path + [str(key)])} 注释: {comment or ''}")
return logs return logs

View File

@@ -68,6 +68,8 @@ class ChatConfig(ConfigBase):
max_context_size: int = 18 max_context_size: int = 18
"""上下文长度""" """上下文长度"""
willing_amplifier: float = 1.0
replyer_random_probability: float = 0.5 replyer_random_probability: float = 0.5
""" """
@@ -273,12 +275,6 @@ class NormalChatConfig(ConfigBase):
willing_mode: str = "classical" willing_mode: str = "classical"
"""意愿模式""" """意愿模式"""
response_interested_rate_amplifier: float = 1.0
"""回复兴趣度放大系数"""
@dataclass @dataclass
class ExpressionConfig(ConfigBase): class ExpressionConfig(ConfigBase):
"""表达配置类""" """表达配置类"""

View File

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

View File

@@ -47,11 +47,35 @@ async def _calculate_interest(message: MessageRecv) -> Tuple[float, bool]:
logger.debug(f"记忆激活率: {interested_rate:.2f}") logger.debug(f"记忆激活率: {interested_rate:.2f}")
text_len = len(message.processed_plain_text) text_len = len(message.processed_plain_text)
# 根据文本长度调整兴趣度,长度越大兴趣度越高但增长率递减最低0.01最高0.05 # 根据文本长度分布调整兴趣度,采用分段函数实现更精确的兴趣度计算
# 采用对数函数实现递减增长 # 基于实际分布0-5字符(26.57%), 6-10字符(27.18%), 11-20字符(22.76%), 21-30字符(10.33%), 31+字符(13.86%)
base_interest = 0.01 + (0.05 - 0.01) * (math.log10(text_len + 1) / math.log10(1000 + 1)) if text_len == 0:
base_interest = min(max(base_interest, 0.01), 0.05) base_interest = 0.01 # 空消息最低兴趣度
elif text_len <= 5:
# 1-5字符线性增长 0.01 -> 0.03
base_interest = 0.01 + (text_len - 1) * (0.03 - 0.01) / 4
elif text_len <= 10:
# 6-10字符线性增长 0.03 -> 0.06
base_interest = 0.03 + (text_len - 5) * (0.06 - 0.03) / 5
elif text_len <= 20:
# 11-20字符线性增长 0.06 -> 0.12
base_interest = 0.06 + (text_len - 10) * (0.12 - 0.06) / 10
elif text_len <= 30:
# 21-30字符线性增长 0.12 -> 0.18
base_interest = 0.12 + (text_len - 20) * (0.18 - 0.12) / 10
elif text_len <= 50:
# 31-50字符线性增长 0.18 -> 0.22
base_interest = 0.18 + (text_len - 30) * (0.22 - 0.18) / 20
elif text_len <= 100:
# 51-100字符线性增长 0.22 -> 0.26
base_interest = 0.22 + (text_len - 50) * (0.26 - 0.22) / 50
else:
# 100+字符:对数增长 0.26 -> 0.3,增长率递减
base_interest = 0.26 + (0.3 - 0.26) * (math.log10(text_len - 99) / math.log10(901)) # 1000-99=901
# 确保在范围内
base_interest = min(max(base_interest, 0.01), 0.3)
interested_rate += base_interest interested_rate += base_interest

View File

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

View File

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

View File

@@ -22,7 +22,6 @@
import traceback import traceback
import time import time
import difflib import difflib
import re
from typing import Optional, Union from typing import Optional, Union
from src.common.logger import get_logger from src.common.logger import get_logger
@@ -30,7 +29,7 @@ from src.common.logger import get_logger
from src.chat.message_receive.chat_stream import get_chat_manager from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.message_receive.uni_message_sender import HeartFCSender from src.chat.message_receive.uni_message_sender import HeartFCSender
from src.chat.message_receive.message import MessageSending, MessageRecv from src.chat.message_receive.message import MessageSending, MessageRecv
from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat from src.chat.utils.chat_message_builder import get_raw_msg_before_timestamp_with_chat, replace_user_references_async
from src.person_info.person_info import get_person_info_manager from src.person_info.person_info import get_person_info_manager
from maim_message import Seg, UserInfo from maim_message import Seg, UserInfo
from src.config.config import global_config from src.config.config import global_config
@@ -183,32 +182,8 @@ async def _find_reply_message(target_stream, reply_to: str) -> Optional[MessageR
if person_name == sender: if person_name == sender:
translate_text = message["processed_plain_text"] translate_text = message["processed_plain_text"]
# 检查是否有 回复<aaa:bbb> 字段 # 使用独立函数处理用户引用格式
reply_pattern = r"回复<([^:<>]+):([^:<>]+)>" translate_text = await replace_user_references_async(translate_text, platform)
if match := re.search(reply_pattern, translate_text):
aaa = match.group(1)
bbb = match.group(2)
reply_person_id = get_person_info_manager().get_person_id(platform, bbb)
reply_person_name = await get_person_info_manager().get_value(reply_person_id, "person_name") or aaa
# 在内容前加上回复信息
translate_text = re.sub(reply_pattern, f"回复 {reply_person_name}", translate_text, count=1)
# 检查是否有 @<aaa:bbb> 字段
at_pattern = r"@<([^:<>]+):([^:<>]+)>"
at_matches = list(re.finditer(at_pattern, translate_text))
if at_matches:
new_content = ""
last_end = 0
for m in at_matches:
new_content += translate_text[last_end : m.start()]
aaa = m.group(1)
bbb = m.group(2)
at_person_id = get_person_info_manager().get_person_id(platform, bbb)
at_person_name = await get_person_info_manager().get_value(at_person_id, "person_name") or aaa
new_content += f"@{at_person_name}"
last_end = m.end()
new_content += translate_text[last_end:]
translate_text = new_content
similarity = difflib.SequenceMatcher(None, text, translate_text).ratio() similarity = difflib.SequenceMatcher(None, text, translate_text).ratio()
if similarity >= 0.9: if similarity >= 0.9:

View File

@@ -9,7 +9,8 @@ from src.common.logger import get_logger
# 导入API模块 - 标准Python包方式 # 导入API模块 - 标准Python包方式
from src.plugin_system.apis import emoji_api, llm_api, message_api from src.plugin_system.apis import emoji_api, llm_api, message_api
from src.plugins.built_in.core_actions.no_reply import NoReplyAction # 注释不再需要导入NoReplyAction因为计数器管理已移至heartFC_chat.py
# from src.plugins.built_in.core_actions.no_reply import NoReplyAction
from src.config.config import global_config from src.config.config import global_config
@@ -143,8 +144,8 @@ class EmojiAction(BaseAction):
logger.error(f"{self.log_prefix} 表情包发送失败") logger.error(f"{self.log_prefix} 表情包发送失败")
return False, "表情包发送失败" return False, "表情包发送失败"
# 重置NoReplyAction的连续计数器 # 注释:重置NoReplyAction的连续计数器现在由heartFC_chat.py统一管理
NoReplyAction.reset_consecutive_count() # NoReplyAction.reset_consecutive_count()
return True, f"发送表情包: {emoji_description}" return True, f"发送表情包: {emoji_description}"

View File

@@ -1,6 +1,7 @@
import random import random
import time import time
from typing import Tuple from typing import Tuple, List
from collections import deque
# 导入新插件系统 # 导入新插件系统
from src.plugin_system import BaseAction, ActionActivationType, ChatMode from src.plugin_system import BaseAction, ActionActivationType, ChatMode
@@ -17,11 +18,15 @@ logger = get_logger("no_reply_action")
class NoReplyAction(BaseAction): class NoReplyAction(BaseAction):
"""不回复动作,根据新消息的兴趣值或数量决定何时结束等待. """不回复动作,支持waiting和breaking两种形式.
新的等待逻辑: waiting形式:
1. 新消息累计兴趣值超过阈值 (默认10) 则结束等待 - 只要有新消息就结束动作
2. 累计新消息数量达到随机阈值 (默认5-10条) 则结束等待 - 记录新消息的兴趣度到列表(最多保留最近三项)
- 如果最近三次动作都是no_reply且最近新消息列表兴趣度之和小于阈值就进入breaking形式
breaking形式:
- 和原有逻辑一致,需要消息满足一定数量或累计一定兴趣值才结束动作
""" """
focus_activation_type = ActionActivationType.NEVER focus_activation_type = ActionActivationType.NEVER
@@ -35,18 +40,21 @@ class NoReplyAction(BaseAction):
# 连续no_reply计数器 # 连续no_reply计数器
_consecutive_count = 0 _consecutive_count = 0
# 最近三次no_reply的新消息兴趣度记录
_recent_interest_records: deque = deque(maxlen=3)
# 新增:兴趣值退出阈值 # 兴趣值退出阈值
_interest_exit_threshold = 3.0 _interest_exit_threshold = 3.0
# 新增:消息数量退出阈值 # 消息数量退出阈值
_min_exit_message_count = 5 _min_exit_message_count = 3
_max_exit_message_count = 10 _max_exit_message_count = 6
# 动作参数定义 # 动作参数定义
action_parameters = {} action_parameters = {}
# 动作使用场景 # 动作使用场景
action_require = ["你发送了消息,目前无人回复"] action_require = [""]
# 关联类型 # 关联类型
associated_types = [] associated_types = []
@@ -56,91 +64,22 @@ class NoReplyAction(BaseAction):
import asyncio import asyncio
try: try:
# 增加连续计数
NoReplyAction._consecutive_count += 1
count = NoReplyAction._consecutive_count
reason = self.action_data.get("reason", "") reason = self.action_data.get("reason", "")
start_time = self.action_data.get("loop_start_time", time.time()) start_time = self.action_data.get("loop_start_time", time.time())
check_interval = 0.6 # 每秒检查一次 check_interval = 0.6
# 随机生成本次等待需要的新消息数量阈值 # 判断使用哪种形式
exit_message_count_threshold = random.randint(self._min_exit_message_count, self._max_exit_message_count) form_type = self._determine_form_type()
logger.info(
f"{self.log_prefix} 本次no_reply需要 {exit_message_count_threshold} 条新消息或累计兴趣值超过 {self._interest_exit_threshold} 才能打断" logger.info(f"{self.log_prefix} 选择不回复(第{NoReplyAction._consecutive_count + 1}次),使用{form_type}形式,原因: {reason}")
)
logger.info(f"{self.log_prefix} 选择不回复(第{count}次),开始摸鱼,原因: {reason}") # 增加连续计数在确定要执行no_reply时才增加
NoReplyAction._consecutive_count += 1
# 进入等待状态 if form_type == "waiting":
while True: return await self._execute_waiting_form(start_time, check_interval)
current_time = time.time() else:
elapsed_time = current_time - start_time return await self._execute_breaking_form(start_time, check_interval)
# 1. 检查新消息
recent_messages_dict = message_api.get_messages_by_time_in_chat(
chat_id=self.chat_id,
start_time=start_time,
end_time=current_time,
filter_mai=True,
filter_command=True,
)
new_message_count = len(recent_messages_dict)
# 2. 检查消息数量是否达到阈值
talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id)
if new_message_count >= exit_message_count_threshold / talk_frequency:
logger.info(
f"{self.log_prefix} 累计消息数量达到{new_message_count}条(>{exit_message_count_threshold / talk_frequency}),结束等待"
)
exit_reason = f"{global_config.bot.nickname}(你)看到了{new_message_count}条新消息,可以考虑一下是否要进行回复"
await self.store_action_info(
action_build_into_prompt=False,
action_prompt_display=exit_reason,
action_done=True,
)
return True, f"累计消息数量达到{new_message_count}条,结束等待 (等待时间: {elapsed_time:.1f}秒)"
# 3. 检查累计兴趣值
if new_message_count > 0:
accumulated_interest = 0.0
for msg_dict in recent_messages_dict:
text = msg_dict.get("processed_plain_text", "")
interest_value = msg_dict.get("interest_value", 0.0)
if text:
accumulated_interest += interest_value
talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id)
# 只在兴趣值变化时输出log
if not hasattr(self, "_last_accumulated_interest") or accumulated_interest != self._last_accumulated_interest:
logger.info(f"{self.log_prefix} 当前累计兴趣值: {accumulated_interest:.2f}, 当前聊天频率: {talk_frequency:.2f}")
self._last_accumulated_interest = accumulated_interest
if accumulated_interest >= self._interest_exit_threshold / talk_frequency:
logger.info(
f"{self.log_prefix} 累计兴趣值达到{accumulated_interest:.2f}(>{self._interest_exit_threshold / talk_frequency}),结束等待"
)
exit_reason = f"{global_config.bot.nickname}(你)感觉到了大家浓厚的兴趣(兴趣值{accumulated_interest:.1f}),决定重新加入讨论"
await self.store_action_info(
action_build_into_prompt=False,
action_prompt_display=exit_reason,
action_done=True,
)
return (
True,
f"累计兴趣值达到{accumulated_interest:.2f},结束等待 (等待时间: {elapsed_time:.1f}秒)",
)
# 每10秒输出一次等待状态
if int(elapsed_time) > 0 and int(elapsed_time) % 10 == 0:
logger.debug(
f"{self.log_prefix} 已等待{elapsed_time:.0f}秒,累计{new_message_count}条消息,继续等待..."
)
# 使用 asyncio.sleep(1) 来避免在同一秒内重复打印日志
await asyncio.sleep(1)
# 短暂等待后继续检查
await asyncio.sleep(check_interval)
except Exception as e: except Exception as e:
logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}") logger.error(f"{self.log_prefix} 不回复动作执行失败: {e}")
@@ -153,8 +92,191 @@ class NoReplyAction(BaseAction):
) )
return False, f"不回复动作执行失败: {e}" return False, f"不回复动作执行失败: {e}"
def _determine_form_type(self) -> str:
"""判断使用哪种形式的no_reply"""
# 如果连续no_reply次数少于3次使用waiting形式
if NoReplyAction._consecutive_count < 3:
return "waiting"
# 如果最近三次记录不足使用waiting形式
if len(NoReplyAction._recent_interest_records) < 3:
return "waiting"
# 计算最近三次记录的兴趣度总和
total_recent_interest = sum(NoReplyAction._recent_interest_records)
# 获取当前聊天频率和意愿系数
talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id)
willing_amplifier = global_config.chat.willing_amplifier
# 计算调整后的阈值
adjusted_threshold = self._interest_exit_threshold / talk_frequency / willing_amplifier
logger.info(f"{self.log_prefix} 最近三次兴趣度总和: {total_recent_interest:.2f}, 调整后阈值: {adjusted_threshold:.2f}")
# 如果兴趣度总和小于阈值进入breaking形式
if total_recent_interest < adjusted_threshold:
logger.info(f"{self.log_prefix} 兴趣度不足进入breaking形式")
return "breaking"
else:
logger.info(f"{self.log_prefix} 兴趣度充足继续使用waiting形式")
return "waiting"
async def _execute_waiting_form(self, start_time: float, check_interval: float) -> Tuple[bool, str]:
"""执行waiting形式的no_reply"""
import asyncio
logger.info(f"{self.log_prefix} 进入waiting形式等待任何新消息")
while True:
current_time = time.time()
elapsed_time = current_time - start_time
# 检查新消息
recent_messages_dict = message_api.get_messages_by_time_in_chat(
chat_id=self.chat_id,
start_time=start_time,
end_time=current_time,
filter_mai=True,
filter_command=True,
)
new_message_count = len(recent_messages_dict)
# waiting形式只要有新消息就结束
if new_message_count > 0:
# 计算新消息的总兴趣度
total_interest = 0.0
for msg_dict in recent_messages_dict:
interest_value = msg_dict.get("interest_value", 0.0)
if msg_dict.get("processed_plain_text", ""):
total_interest += interest_value * global_config.chat.willing_amplifier
# 记录到最近兴趣度列表
NoReplyAction._recent_interest_records.append(total_interest)
logger.info(
f"{self.log_prefix} waiting形式检测到{new_message_count}条新消息,总兴趣度: {total_interest:.2f},结束等待"
)
exit_reason = f"{global_config.bot.nickname}(你)看到了{new_message_count}条新消息,可以考虑一下是否要进行回复"
await self.store_action_info(
action_build_into_prompt=False,
action_prompt_display=exit_reason,
action_done=True,
)
return True, f"waiting形式检测到{new_message_count}条新消息,结束等待 (等待时间: {elapsed_time:.1f}秒)"
# 每10秒输出一次等待状态
if int(elapsed_time) > 0 and int(elapsed_time) % 10 == 0:
logger.debug(f"{self.log_prefix} waiting形式已等待{elapsed_time:.0f}秒,继续等待新消息...")
await asyncio.sleep(1)
# 短暂等待后继续检查
await asyncio.sleep(check_interval)
async def _execute_breaking_form(self, start_time: float, check_interval: float) -> Tuple[bool, str]:
"""执行breaking形式的no_reply原有逻辑"""
import asyncio
# 随机生成本次等待需要的新消息数量阈值
exit_message_count_threshold = random.randint(self._min_exit_message_count, self._max_exit_message_count)
logger.info(f"{self.log_prefix} 进入breaking形式需要{exit_message_count_threshold}条消息或足够兴趣度")
while True:
current_time = time.time()
elapsed_time = current_time - start_time
# 检查新消息
recent_messages_dict = message_api.get_messages_by_time_in_chat(
chat_id=self.chat_id,
start_time=start_time,
end_time=current_time,
filter_mai=True,
filter_command=True,
)
new_message_count = len(recent_messages_dict)
# 检查消息数量是否达到阈值
talk_frequency = global_config.chat.get_current_talk_frequency(self.chat_id)
modified_exit_count_threshold = (exit_message_count_threshold / talk_frequency) / global_config.chat.willing_amplifier
if new_message_count >= modified_exit_count_threshold:
# 记录兴趣度到列表
total_interest = 0.0
for msg_dict in recent_messages_dict:
interest_value = msg_dict.get("interest_value", 0.0)
if msg_dict.get("processed_plain_text", ""):
total_interest += interest_value * global_config.chat.willing_amplifier
NoReplyAction._recent_interest_records.append(total_interest)
logger.info(
f"{self.log_prefix} breaking形式累计消息数量达到{new_message_count}条(>{modified_exit_count_threshold}),结束等待"
)
exit_reason = f"{global_config.bot.nickname}(你)看到了{new_message_count}条新消息,可以考虑一下是否要进行回复"
await self.store_action_info(
action_build_into_prompt=False,
action_prompt_display=exit_reason,
action_done=True,
)
return True, f"breaking形式累计消息数量达到{new_message_count}条,结束等待 (等待时间: {elapsed_time:.1f}秒)"
# 检查累计兴趣值
if new_message_count > 0:
accumulated_interest = 0.0
for msg_dict in recent_messages_dict:
text = msg_dict.get("processed_plain_text", "")
interest_value = msg_dict.get("interest_value", 0.0)
if text:
accumulated_interest += interest_value * global_config.chat.willing_amplifier
# 只在兴趣值变化时输出log
if not hasattr(self, "_last_accumulated_interest") or accumulated_interest != self._last_accumulated_interest:
logger.info(f"{self.log_prefix} breaking形式当前累计兴趣值: {accumulated_interest:.2f}, 当前聊天频率: {talk_frequency:.2f}")
self._last_accumulated_interest = accumulated_interest
if accumulated_interest >= self._interest_exit_threshold / talk_frequency:
# 记录兴趣度到列表
NoReplyAction._recent_interest_records.append(accumulated_interest)
logger.info(
f"{self.log_prefix} breaking形式累计兴趣值达到{accumulated_interest:.2f}(>{self._interest_exit_threshold / talk_frequency}),结束等待"
)
exit_reason = f"{global_config.bot.nickname}(你)感觉到了大家浓厚的兴趣(兴趣值{accumulated_interest:.1f}),决定重新加入讨论"
await self.store_action_info(
action_build_into_prompt=False,
action_prompt_display=exit_reason,
action_done=True,
)
return (
True,
f"breaking形式累计兴趣值达到{accumulated_interest:.2f},结束等待 (等待时间: {elapsed_time:.1f}秒)",
)
# 每10秒输出一次等待状态
if int(elapsed_time) > 0 and int(elapsed_time) % 10 == 0:
logger.debug(
f"{self.log_prefix} breaking形式已等待{elapsed_time:.0f}秒,累计{new_message_count}条消息,继续等待..."
)
await asyncio.sleep(1)
# 短暂等待后继续检查
await asyncio.sleep(check_interval)
@classmethod @classmethod
def reset_consecutive_count(cls): def reset_consecutive_count(cls):
"""重置连续计数器""" """重置连续计数器和兴趣度记录"""
cls._consecutive_count = 0 cls._consecutive_count = 0
logger.debug("NoReplyAction连续计数器已重置") cls._recent_interest_records.clear()
logger.debug("NoReplyAction连续计数器和兴趣度记录已重置")
@classmethod
def get_recent_interest_records(cls) -> List[float]:
"""获取最近的兴趣度记录"""
return list(cls._recent_interest_records)
@classmethod
def get_consecutive_count(cls) -> int:
"""获取连续计数"""
return cls._consecutive_count

View File

@@ -66,13 +66,12 @@ class CoreActionsPlugin(BasePlugin):
if global_config.emoji.emoji_activate_type == "llm": if global_config.emoji.emoji_activate_type == "llm":
EmojiAction.random_activation_probability = 0.0 EmojiAction.random_activation_probability = 0.0
EmojiAction.focus_activation_type = ActionActivationType.LLM_JUDGE EmojiAction.activation_type = ActionActivationType.LLM_JUDGE
EmojiAction.normal_activation_type = ActionActivationType.LLM_JUDGE
elif global_config.emoji.emoji_activate_type == "random": elif global_config.emoji.emoji_activate_type == "random":
EmojiAction.random_activation_probability = global_config.emoji.emoji_chance EmojiAction.random_activation_probability = global_config.emoji.emoji_chance
EmojiAction.focus_activation_type = ActionActivationType.RANDOM EmojiAction.activation_type = ActionActivationType.RANDOM
EmojiAction.normal_activation_type = ActionActivationType.RANDOM
# --- 根据配置注册组件 --- # --- 根据配置注册组件 ---
components = [] components = []
if self.get_config("components.enable_reply", True): if self.get_config("components.enable_reply", True):

View File

@@ -13,7 +13,8 @@ from src.common.logger import get_logger
# 导入API模块 - 标准Python包方式 # 导入API模块 - 标准Python包方式
from src.plugin_system.apis import generator_api, message_api from src.plugin_system.apis import generator_api, message_api
from src.plugins.built_in.core_actions.no_reply import NoReplyAction # 注释不再需要导入NoReplyAction因为计数器管理已移至heartFC_chat.py
# from src.plugins.built_in.core_actions.no_reply import NoReplyAction
from src.person_info.person_info import get_person_info_manager from src.person_info.person_info import get_person_info_manager
from src.mais4u.mai_think import mai_thinking_manager from src.mais4u.mai_think import mai_thinking_manager
from src.mais4u.constant_s4u import ENABLE_S4U from src.mais4u.constant_s4u import ENABLE_S4U
@@ -138,8 +139,8 @@ class ReplyAction(BaseAction):
action_done=True, action_done=True,
) )
# 重置NoReplyAction的连续计数器 # 注释:重置NoReplyAction的连续计数器现在由heartFC_chat.py统一管理
NoReplyAction.reset_consecutive_count() # NoReplyAction.reset_consecutive_count()
return success, reply_text return success, reply_text

View File

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

View File

@@ -422,13 +422,14 @@ class ManagementCommand(BaseCommand):
@register_plugin @register_plugin
class PluginManagementPlugin(BasePlugin): class PluginManagementPlugin(BasePlugin):
plugin_name: str = "plugin_management_plugin" plugin_name: str = "plugin_management_plugin"
enable_plugin: bool = True enable_plugin: bool = False
dependencies: list[str] = [] dependencies: list[str] = []
python_dependencies: list[str] = [] python_dependencies: list[str] = []
config_file_name: str = "config.toml" config_file_name: str = "config.toml"
config_schema: dict = { config_schema: dict = {
"plugin": { "plugin": {
"enable": ConfigField(bool, default=True, description="是否启用插件"), "enable": ConfigField(bool, default=False, description="是否启用插件"),
"config_version": ConfigField(type=str, default="1.0.0", description="配置文件版本"),
"permission": ConfigField(list, default=[], description="有权限使用插件管理命令的用户列表"), "permission": ConfigField(list, default=[], description="有权限使用插件管理命令的用户列表"),
}, },
} }

View File

@@ -1,5 +1,5 @@
[inner] [inner]
version = "4.4.8" version = "4.4.9"
#----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读---- #----以下是给开发人员阅读的,如果你只是部署了麦麦,不需要阅读----
#如果你想要修改配置文件请在修改后将version的值进行变更 #如果你想要修改配置文件请在修改后将version的值进行变更
@@ -52,9 +52,11 @@ relation_frequency = 1 # 关系频率,麦麦构建关系的频率
[chat] #麦麦的聊天通用设置 [chat] #麦麦的聊天通用设置
focus_value = 1 focus_value = 1
# 麦麦的专注思考能力,越越容易专注,消耗token也越多 # 麦麦的专注思考能力,越越容易专注,可能消耗更多token
# 专注时能更好把握发言时机,能够进行持久的连续对话 # 专注时能更好把握发言时机,能够进行持久的连续对话
willing_amplifier = 1 # 麦麦回复意愿
max_context_size = 25 # 上下文长度 max_context_size = 25 # 上下文长度
thinking_timeout = 20 # 麦麦一次回复最长思考规划时间超过这个时间的思考会放弃往往是api反应太慢 thinking_timeout = 20 # 麦麦一次回复最长思考规划时间超过这个时间的思考会放弃往往是api反应太慢
replyer_random_probability = 0.5 # 首要replyer模型被选择的概率 replyer_random_probability = 0.5 # 首要replyer模型被选择的概率
@@ -67,11 +69,11 @@ use_s4u_prompt_mode = true # 是否使用 s4u 对话构建模式,该模式会
talk_frequency = 1 # 麦麦回复频率,越高,麦麦回复越频繁 talk_frequency = 1 # 麦麦回复频率,越高,麦麦回复越频繁
time_based_talk_frequency = ["8:00,1", "12:00,1.5", "18:00,2", "01:00,0.5"] time_based_talk_frequency = ["8:00,1", "12:00,1.2", "18:00,1.5", "01:00,0.6"]
# 基于时段的回复频率配置(可选) # 基于时段的回复频率配置(可选)
# 格式time_based_talk_frequency = ["HH:MM,frequency", ...] # 格式time_based_talk_frequency = ["HH:MM,frequency", ...]
# 示例: # 示例:
# time_based_talk_frequency = ["8:00,1", "12:00,2", "18:00,1.5", "00:00,0.5"] # time_based_talk_frequency = ["8:00,1", "12:00,1.2", "18:00,1.5", "00:00,0.6"]
# 说明:表示从该时间开始使用该频率,直到下一个时间点 # 说明:表示从该时间开始使用该频率,直到下一个时间点
# 注意:如果没有配置,则使用上面的默认 talk_frequency 值 # 注意:如果没有配置,则使用上面的默认 talk_frequency 值
@@ -105,7 +107,6 @@ ban_msgs_regex = [
[normal_chat] #普通聊天 [normal_chat] #普通聊天
willing_mode = "classical" # 回复意愿模式 —— 经典模式classicalmxp模式mxp自定义模式custom需要你自己实现 willing_mode = "classical" # 回复意愿模式 —— 经典模式classicalmxp模式mxp自定义模式custom需要你自己实现
response_interested_rate_amplifier = 1 # 麦麦回复兴趣度放大系数
[tool] [tool]
enable_in_normal_chat = false # 是否在普通聊天中启用工具 enable_in_normal_chat = false # 是否在普通聊天中启用工具