Merge branch 'dev' of https://github.com/Windpicker-owo/MaiBot into dev
This commit is contained in:
12
.github/workflows/docker-image.yml
vendored
12
.github/workflows/docker-image.yml
vendored
@@ -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
|
||||||
|
|||||||
12
.github/workflows/ruff.yml
vendored
12
.github/workflows/ruff.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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带来更自然、更智能的交互体验!
|
||||||
|
|||||||
@@ -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**。
|
||||||
|
|||||||
@@ -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`方法和自动化的配置生成机制已经为你准备好了一切!
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
|||||||
|
|
||||||

|

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