Merge branch 'dev' into HFC-para

This commit is contained in:
SengokuCola
2025-05-13 12:44:21 +08:00
30 changed files with 1948 additions and 1010 deletions

View File

@@ -6,20 +6,21 @@ on:
- main - main
- classical - classical
- dev - dev
- new_knowledge
tags: tags:
- 'v*' - "v*.*.*"
workflow_dispatch: - "v*"
jobs: jobs:
build-and-push: build-amd64:
name: Build AMD64 Image
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }}
DATE_TAG: $(date -u +'%Y-%m-%dT%H-%M-%S')
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Clone maim_message - name: Clone maim_message
run: git clone https://github.com/MaiM-with-u/maim_message maim_message run: git clone https://github.com/MaiM-with-u/maim_message maim_message
@@ -29,6 +30,8 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -36,31 +39,131 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine Image Tags - name: Docker meta
id: tags id: meta
run: | uses: docker/metadata-action@v5
if [[ "${{ github.ref }}" == refs/tags/* ]]; then with:
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:${{ github.ref_name }},${{ secrets.DOCKERHUB_USERNAME }}/maimbot:latest" >> $GITHUB_OUTPUT images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
elif [ "${{ github.ref }}" == "refs/heads/main" ]; then tags: |
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:main-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT type=ref,event=branch
elif [ "${{ github.ref }}" == "refs/heads/classical" ]; then type=ref,event=tag
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:classical,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:classical-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT type=semver,pattern={{version}}
elif [ "${{ github.ref }}" == "refs/heads/dev" ]; then type=semver,pattern={{major}}.{{minor}}
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:dev,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:dev-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT type=semver,pattern={{major}}
elif [ "${{ github.ref }}" == "refs/heads/new_knowledge" ]; then type=sha
echo "tags=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:knowledge,${{ secrets.DOCKERHUB_USERNAME }}/maimbot:knowledge-$(date -u +'%Y%m%d%H%M%S')" >> $GITHUB_OUTPUT
fi
- name: Build and Push Docker Image - name: Build and Push AMD64 Docker Image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64
tags: ${{ steps.tags.outputs.tags }} tags: ${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-${{ github.sha }}
push: true push: true
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maimbot:buildcache,mode=max cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-buildcache,mode=max
labels: | labels: ${{ steps.meta.outputs.labels }}
org.opencontainers.image.created=${{ steps.tags.outputs.date_tag }} provenance: true
org.opencontainers.image.revision=${{ github.sha }} sbom: true
build-args: |
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=${{ github.sha }}
outputs: type=image,push=true
build-arm64:
name: Build ARM64 Image
runs-on: ubuntu-latest
env:
DOCKERHUB_USER: ${{ secrets.DOCKERHUB_USERNAME }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Clone maim_message
run: git clone https://github.com/MaiM-with-u/maim_message maim_message
- name: Clone lpmm
run: git clone https://github.com/MaiM-with-u/MaiMBot-LPMM.git MaiMBot-LPMM
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: --debug
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Build and Push ARM64 Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/arm64
tags: ${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-${{ github.sha }}
push: true
cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-buildcache
cache-to: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-buildcache,mode=max
labels: ${{ steps.meta.outputs.labels }}
provenance: true
sbom: true
build-args: |
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=${{ github.sha }}
outputs: type=image,push=true
create-manifest:
name: Create Multi-Arch Manifest
runs-on: ubuntu-latest
needs:
- build-amd64
- build-arm64
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKERHUB_USERNAME }}/maibot
tags: |
type=ref,event=branch
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
- name: Create and Push Manifest
run: |
# 为每个标签创建多架构镜像
for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ' '); do
echo "Creating manifest for $tag"
docker buildx imagetools create -t $tag \
${{ secrets.DOCKERHUB_USERNAME }}/maibot:amd64-${{ github.sha }} \
${{ secrets.DOCKERHUB_USERNAME }}/maibot:arm64-${{ github.sha }}
done

3
.gitignore vendored
View File

@@ -36,7 +36,6 @@ config/bot_config.toml
config/bot_config.toml.bak config/bot_config.toml.bak
config/lpmm_config.toml config/lpmm_config.toml
config/lpmm_config.toml.bak config/lpmm_config.toml.bak
src/plugins/remote/client_uuid.json
(测试版)麦麦生成人格.bat (测试版)麦麦生成人格.bat
(临时版)麦麦开始学习.bat (临时版)麦麦开始学习.bat
src/plugins/utils/statistic.py src/plugins/utils/statistic.py
@@ -44,7 +43,7 @@ src/plugins/utils/statistic.py
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
llm_statistics.txt maibot_statistics.html
mongodb mongodb
napcat napcat
run_dev.bat run_dev.bat

View File

@@ -2,7 +2,7 @@
**版本V1.0** **版本V1.0**
**更新日期2025年5月9日** **更新日期2025年5月9日**
**生效日期2025年3月18日** **生效日期2025年3月18日**
**适用的MaiMBot版本号所有版本** **适用的MaiBot版本号所有版本**
**2025© MaiBot项目团队** **2025© MaiBot项目团队**
@@ -10,7 +10,7 @@
## 一、一般条款 ## 一、一般条款
**1.1** MaiMBot项目包括MaiMBot的源代码、可执行文件、文档以及其它在本协议中所列出的文件以下简称“本项目”是由开发者及贡献者以下简称“项目团队”共同维护为用户提供自动回复功能的机器人代码项目。以下最终用户许可协议EULA以下简称“本协议”是用户以下简称“您”与项目团队之间关于使用本项目所订立的合同条件。 **1.1** MaiBot项目包括MaiBot的源代码、可执行文件、文档以及其它在本协议中所列出的文件以下简称“本项目”是由开发者及贡献者以下简称“项目团队”共同维护为用户提供自动回复功能的机器人代码项目。以下最终用户许可协议EULA以下简称“本协议”是用户以下简称“您”与项目团队之间关于使用本项目所订立的合同条件。
**1.2** 在运行或使用本项目之前,您**必须阅读并同意本协议的所有条款**。未成年人或其它无/不完全民事行为能力责任人请**在监护人的陪同下**阅读并同意本协议。如果您不同意,则不得运行或使用本项目。在这种情况下,您应立即从您的设备上卸载或删除本项目及其所有副本。 **1.2** 在运行或使用本项目之前,您**必须阅读并同意本协议的所有条款**。未成年人或其它无/不完全民事行为能力责任人请**在监护人的陪同下**阅读并同意本协议。如果您不同意,则不得运行或使用本项目。在这种情况下,您应立即从您的设备上卸载或删除本项目及其所有副本。

View File

@@ -6,7 +6,7 @@
**2025© MaiBot项目团队** **2025© MaiBot项目团队**
MaiMBot项目团队以下简称项目团队**尊重并保护**用户以下简称您的隐私。若您选择使用MaiMBot项目以下简称本项目则您需同意本项目按照以下隐私条款处理您的输入和输出内容 MaiBot项目团队以下简称项目团队**尊重并保护**用户以下简称您的隐私。若您选择使用MaiBot项目以下简称本项目则您需同意本项目按照以下隐私条款处理您的输入和输出内容
**1.1** 本项目**会**收集您的输入和输出内容并发送到第三方API用于生成新的输出内容。因此您的输入和输出内容**会**同时受到本项目和第三方API的隐私政策约束。 **1.1** 本项目**会**收集您的输入和输出内容并发送到第三方API用于生成新的输出内容。因此您的输入和输出内容**会**同时受到本项目和第三方API的隐私政策约束。

View File

@@ -36,7 +36,7 @@
</p> </p>
</p> </p>
## 新版0.6.x部署前先阅读https://docs.mai-mai.org/manual/usage/mmc_q_a ## 新版0.6.x部署前先阅读https://docs.mai-mai.org/faq/maibot/backup_update.html
## 📝 项目简介 ## 📝 项目简介
@@ -85,7 +85,7 @@
### ⚠️ 重要提示 ### ⚠️ 重要提示
- 升级到v0.6.x版本前请务必阅读[升级指南](https://docs.mai-mai.org/manual/usage/mmc_q_a) - 升级到v0.6.x版本前请务必阅读[升级指南](https://docs.mai-mai.org/faq/maibot/backup_update.html)
- 本版本基于MaiCore重构通过nonebot插件与QQ平台交互 - 本版本基于MaiCore重构通过nonebot插件与QQ平台交互
- 项目处于活跃开发阶段功能和API可能随时调整 - 项目处于活跃开发阶段功能和API可能随时调整
@@ -94,7 +94,7 @@
- [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722 - [二群](https://qm.qq.com/q/RzmCiRtHEW) 571780722
- [五群](https://qm.qq.com/q/JxvHZnxyec) 1022489779 - [五群](https://qm.qq.com/q/JxvHZnxyec) 1022489779
- [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475【已满】 - [三群](https://qm.qq.com/q/wlH5eT8OmQ) 1035228475【已满】
- [四群](https://qm.qq.com/q/wlH5eT8OmQ) 729957033【已满】 - [四群](https://qm.qq.com/q/wGePTl1UyY) 729957033【已满】

43
bot.py
View File

@@ -1,7 +1,6 @@
import asyncio import asyncio
import hashlib import hashlib
import os import os
import shutil
import sys import sys
from pathlib import Path from pathlib import Path
import time import time
@@ -15,6 +14,8 @@ from src.common.crash_logger import install_crash_handler
from src.main import MainSystem from src.main import MainSystem
from rich.traceback import install from rich.traceback import install
from src.manager.async_task_manager import async_task_manager
install(extra_lines=3) install(extra_lines=3)
# 设置工作目录为脚本所在目录 # 设置工作目录为脚本所在目录
@@ -64,38 +65,6 @@ def easter_egg():
print(rainbow_text) print(rainbow_text)
def init_config():
# 初次启动检测
if not os.path.exists("config/bot_config.toml"):
logger.warning("检测到bot_config.toml不存在正在从模板复制")
# 检查config目录是否存在
if not os.path.exists("config"):
os.makedirs("config")
logger.info("创建config目录")
shutil.copy("template/bot_config_template.toml", "config/bot_config.toml")
logger.info("复制完成请修改config/bot_config.toml和.env中的配置后重新启动")
if not os.path.exists("config/lpmm_config.toml"):
logger.warning("检测到lpmm_config.toml不存在正在从模板复制")
# 检查config目录是否存在
if not os.path.exists("config"):
os.makedirs("config")
logger.info("创建config目录")
shutil.copy("template/lpmm_config_template.toml", "config/lpmm_config.toml")
logger.info("复制完成请修改config/lpmm_config.toml和.env中的配置后重新启动")
def init_env():
# 检测.env文件是否存在
if not os.path.exists(".env"):
logger.error("检测到.env文件不存在")
shutil.copy("template/template.env", "./.env")
logger.info("已从template/template.env复制创建.env请修改配置后重新启动")
def load_env(): def load_env():
# 直接加载生产环境变量配置 # 直接加载生产环境变量配置
if os.path.exists(".env"): if os.path.exists(".env"):
@@ -140,6 +109,10 @@ def scan_provider(env_config: dict):
async def graceful_shutdown(): async def graceful_shutdown():
try: try:
logger.info("正在优雅关闭麦麦...") logger.info("正在优雅关闭麦麦...")
# 停止所有异步任务
await async_task_manager.stop_and_wait_all_tasks()
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks: for task in tasks:
task.cancel() task.cancel()
@@ -235,9 +208,9 @@ def raw_main():
check_eula() check_eula()
print("检查EULA和隐私条款完成") print("检查EULA和隐私条款完成")
easter_egg() easter_egg()
init_config()
init_env()
load_env() load_env()
env_config = {key: os.getenv(key) for key in os.environ} env_config = {key: os.getenv(key) for key in os.environ}

View File

@@ -16,8 +16,11 @@ services:
- maim_bot - maim_bot
core: core:
container_name: maim-bot-core container_name: maim-bot-core
image: sengokucola/maimbot:main image: sengokucola/maibot:latest
# image: infinitycat/maimbot:main # image: infinitycat/maibot:latest
# dev
# image: sengokucola/maibot:dev
# image: infinitycat/maibot:dev
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
# - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA # - EULA_AGREE=35362b6ea30f12891d46ef545122e84a # 同意EULA

Binary file not shown.

169
src/api/basic_info_api.py Normal file
View File

@@ -0,0 +1,169 @@
import platform
import psutil
import sys
import os
def get_system_info():
"""获取操作系统信息"""
return {
"system": platform.system(),
"release": platform.release(),
"version": platform.version(),
"machine": platform.machine(),
"processor": platform.processor(),
}
def get_python_version():
"""获取 Python 版本信息"""
return sys.version
def get_cpu_usage():
"""获取系统总CPU使用率"""
return psutil.cpu_percent(interval=1)
def get_process_cpu_usage():
"""获取当前进程CPU使用率"""
process = psutil.Process(os.getpid())
return process.cpu_percent(interval=1)
def get_memory_usage():
"""获取系统内存使用情况 (单位 MB)"""
mem = psutil.virtual_memory()
bytes_to_mb = lambda x: round(x / (1024 * 1024), 2) # noqa
return {
"total_mb": bytes_to_mb(mem.total),
"available_mb": bytes_to_mb(mem.available),
"percent": mem.percent,
"used_mb": bytes_to_mb(mem.used),
"free_mb": bytes_to_mb(mem.free),
}
def get_process_memory_usage():
"""获取当前进程内存使用情况 (单位 MB)"""
process = psutil.Process(os.getpid())
mem_info = process.memory_info()
bytes_to_mb = lambda x: round(x / (1024 * 1024), 2) # noqa
return {
"rss_mb": bytes_to_mb(mem_info.rss), # Resident Set Size: 实际使用物理内存
"vms_mb": bytes_to_mb(mem_info.vms), # Virtual Memory Size: 虚拟内存大小
"percent": process.memory_percent(), # 进程内存使用百分比
}
def get_disk_usage(path="/"):
"""获取指定路径磁盘使用情况 (单位 GB)"""
disk = psutil.disk_usage(path)
bytes_to_gb = lambda x: round(x / (1024 * 1024 * 1024), 2) # noqa
return {
"total_gb": bytes_to_gb(disk.total),
"used_gb": bytes_to_gb(disk.used),
"free_gb": bytes_to_gb(disk.free),
"percent": disk.percent,
}
def get_all_basic_info():
"""获取所有基本信息并封装返回"""
# 对于进程CPU使用率需要先初始化
process = psutil.Process(os.getpid())
process.cpu_percent(interval=None) # 初始化调用
process_cpu = process.cpu_percent(interval=0.1) # 短暂间隔获取
return {
"system_info": get_system_info(),
"python_version": get_python_version(),
"cpu_usage_percent": get_cpu_usage(),
"process_cpu_usage_percent": process_cpu,
"memory_usage": get_memory_usage(),
"process_memory_usage": get_process_memory_usage(),
"disk_usage_root": get_disk_usage("/"),
}
def get_all_basic_info_string() -> str:
"""获取所有基本信息并以带解释的字符串形式返回"""
info = get_all_basic_info()
sys_info = info["system_info"]
mem_usage = info["memory_usage"]
proc_mem_usage = info["process_memory_usage"]
disk_usage = info["disk_usage_root"]
# 对进程内存使用百分比进行格式化,保留两位小数
proc_mem_percent = round(proc_mem_usage["percent"], 2)
output_string = f"""[系统信息]
- 操作系统: {sys_info["system"]} (例如: Windows, Linux)
- 发行版本: {sys_info["release"]} (例如: 11, Ubuntu 20.04)
- 详细版本: {sys_info["version"]}
- 硬件架构: {sys_info["machine"]} (例如: AMD64)
- 处理器信息: {sys_info["processor"]}
[Python 环境]
- Python 版本: {info["python_version"]}
[CPU 状态]
- 系统总 CPU 使用率: {info["cpu_usage_percent"]}%
- 当前进程 CPU 使用率: {info["process_cpu_usage_percent"]}%
[系统内存使用情况]
- 总物理内存: {mem_usage["total_mb"]} MB
- 可用物理内存: {mem_usage["available_mb"]} MB
- 物理内存使用率: {mem_usage["percent"]}%
- 已用物理内存: {mem_usage["used_mb"]} MB
- 空闲物理内存: {mem_usage["free_mb"]} MB
[当前进程内存使用情况]
- 实际使用物理内存 (RSS): {proc_mem_usage["rss_mb"]} MB
- 占用虚拟内存 (VMS): {proc_mem_usage["vms_mb"]} MB
- 进程内存使用率: {proc_mem_percent}%
[磁盘使用情况 (根目录)]
- 总空间: {disk_usage["total_gb"]} GB
- 已用空间: {disk_usage["used_gb"]} GB
- 可用空间: {disk_usage["free_gb"]} GB
- 磁盘使用率: {disk_usage["percent"]}%
"""
return output_string
if __name__ == "__main__":
print(f"System Info: {get_system_info()}")
print(f"Python Version: {get_python_version()}")
print(f"CPU Usage: {get_cpu_usage()}%")
# 第一次调用 process.cpu_percent() 会返回0.0或一个无意义的值,需要间隔一段时间再调用
# 或者在初始化Process对象后先调用一次cpu_percent(interval=None)然后再调用cpu_percent(interval=1)
current_process = psutil.Process(os.getpid())
current_process.cpu_percent(interval=None) # 初始化
print(f"Process CPU Usage: {current_process.cpu_percent(interval=1)}%") # 实际获取
memory_usage_info = get_memory_usage()
print(
f"Memory Usage: Total={memory_usage_info['total_mb']}MB, Used={memory_usage_info['used_mb']}MB, Percent={memory_usage_info['percent']}%"
)
process_memory_info = get_process_memory_usage()
print(
f"Process Memory Usage: RSS={process_memory_info['rss_mb']}MB, VMS={process_memory_info['vms_mb']}MB, Percent={process_memory_info['percent']}%"
)
disk_usage_info = get_disk_usage("/")
print(
f"Disk Usage (Root): Total={disk_usage_info['total_gb']}GB, Used={disk_usage_info['used_gb']}GB, Percent={disk_usage_info['percent']}%"
)
print("\n--- All Basic Info (JSON) ---")
all_info = get_all_basic_info()
import json
print(json.dumps(all_info, indent=4, ensure_ascii=False))
print("\n--- All Basic Info (String with Explanations) ---")
info_string = get_all_basic_info_string()
print(info_string)

View File

@@ -1,6 +1,7 @@
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
import strawberry import strawberry
from packaging.version import Version
# from packaging.version import Version
import os import os
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
@@ -10,7 +11,7 @@ ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
class APIBotConfig: class APIBotConfig:
"""机器人配置类""" """机器人配置类"""
INNER_VERSION: Version # 配置文件内部版本号 INNER_VERSION: str # 配置文件内部版本号toml为字符串
MAI_VERSION: str # 硬编码的版本信息 MAI_VERSION: str # 硬编码的版本信息
# bot # bot
@@ -34,28 +35,28 @@ class APIBotConfig:
appearance: str # 外貌特征描述 appearance: str # 外貌特征描述
# schedule # schedule
ENABLE_SCHEDULE_GEN: bool # 是否启用日程生成 enable_schedule_gen: bool # 是否启用日程
ENABLE_SCHEDULE_INTERACTION: bool # 是否启用日程交互 enable_schedule_interaction: bool # 日程表是否影响回复模式
PROMPT_SCHEDULE_GEN: str # 日程生成提示词 prompt_schedule_gen: str # 日程生成提示词
SCHEDULE_DOING_UPDATE_INTERVAL: int # 日程进行中更新间隔 schedule_doing_update_interval: int # 日程更新间隔(秒)
SCHEDULE_TEMPERATURE: float # 日程生成温度 schedule_temperature: float # 日程温度
TIME_ZONE: str # 时区 time_zone: str # 时区
# platforms # platforms
platforms: Dict[str, str] # 平台信息 platforms: Dict[str, str] # 平台信息
# chat # chat
allow_focus_mode: bool # 是否允许专注模式 allow_focus_mode: bool # 是否允许专注聊天状态
base_normal_chat_num: int # 基础普通聊天次数 base_normal_chat_num: int # 最多允许多少个群进行普通聊天
base_focused_chat_num: int # 基础专注聊天次数 base_focused_chat_num: int # 最多允许多少个群进行专注聊天
observation_context_size: int # 观察上下文大小 observation_context_size: int # 观察到的最长上下文大小
message_buffer: bool # 是否启用消息缓冲 message_buffer: bool # 是否启用消息缓冲
ban_words: List[str] # 禁止词列表 ban_words: List[str] # 禁止词列表
ban_msgs_regex: List[str] # 禁止消息的正则表达式列表 ban_msgs_regex: List[str] # 禁止消息的正则表达式列表
# normal_chat # normal_chat
MODEL_R1_PROBABILITY: float # 模型推理概率 model_reasoning_probability: float # 推理模型概率
MODEL_V3_PROBABILITY: float # 模型普通概率 model_normal_probability: float # 普通模型概率
emoji_chance: float # 表情符号出现概率 emoji_chance: float # 表情符号出现概率
thinking_timeout: int # 思考超时时间 thinking_timeout: int # 思考超时时间
willing_mode: str # 意愿模式 willing_mode: str # 意愿模式
@@ -63,8 +64,8 @@ class APIBotConfig:
response_interested_rate_amplifier: float # 回复兴趣率放大器 response_interested_rate_amplifier: float # 回复兴趣率放大器
down_frequency_rate: float # 降低频率率 down_frequency_rate: float # 降低频率率
emoji_response_penalty: float # 表情回复惩罚 emoji_response_penalty: float # 表情回复惩罚
mentioned_bot_inevitable_reply: bool # 提到机器人时是否必定回复 mentioned_bot_inevitable_reply: bool # 提及 bot 必然回复
at_bot_inevitable_reply: bool # @机器人时是否必定回复 at_bot_inevitable_reply: bool # @bot 必然回复
# focus_chat # focus_chat
reply_trigger_threshold: float # 回复触发阈值 reply_trigger_threshold: float # 回复触发阈值
@@ -78,24 +79,25 @@ class APIBotConfig:
# emoji # emoji
max_emoji_num: int # 最大表情符号数量 max_emoji_num: int # 最大表情符号数量
max_reach_deletion: bool # 达到最大数量时是否删除 max_reach_deletion: bool # 达到最大数量时是否删除
EMOJI_CHECK_INTERVAL: int # 表情检查间隔 check_interval: int # 检查表情包的时间间隔(分钟)
EMOJI_REGISTER_INTERVAL: Optional[int] # 表情注册间隔(兼容性保留) save_pic: bool # 是否保存图片
EMOJI_SAVE: bool # 是否保存表情 save_emoji: bool # 是否保存表情
EMOJI_CHECK: bool # 是否检查表情 steal_emoji: bool # 是否偷取表情
EMOJI_CHECK_PROMPT: str # 表情检查提示词 enable_check: bool # 是否启用表情包过滤
check_prompt: str # 表情包过滤要求
# memory # memory
build_memory_interval: int # 构建记忆间隔 build_memory_interval: int # 记忆构建间隔
memory_build_distribution: List[float] # 记忆构建分布 build_memory_distribution: List[float] # 记忆构建分布
build_memory_sample_num: int # 构建记忆样本数量 build_memory_sample_num: int # 采样数量
build_memory_sample_length: int # 构建记忆样本长度 build_memory_sample_length: int # 采样长度
memory_compress_rate: float # 记忆压缩率 memory_compress_rate: float # 记忆压缩率
forget_memory_interval: int # 忘记记忆间隔 forget_memory_interval: int # 记忆遗忘间隔
memory_forget_time: int # 记忆忘记时间 memory_forget_time: int # 记忆遗忘时间(小时)
memory_forget_percentage: float # 记忆忘记百分比 memory_forget_percentage: float # 记忆遗忘比例
consolidate_memory_interval: int # 巩固记忆间隔 consolidate_memory_interval: int # 记忆整合间隔
consolidation_similarity_threshold: float # 巩固相似度阈值 consolidation_similarity_threshold: float # 相似度阈值
consolidation_check_percentage: float # 巩固检查百分比 consolidation_check_percentage: float # 检查节点比例
memory_ban_words: List[str] # 记忆禁止词列表 memory_ban_words: List[str] # 记忆禁止词列表
# mood # mood
@@ -128,21 +130,19 @@ class APIBotConfig:
# experimental # experimental
enable_friend_chat: bool # 是否启用好友聊天 enable_friend_chat: bool # 是否启用好友聊天
talk_allowed_private: List[int] # 允许私聊的QQ号列表 talk_allowed_private: List[int] # 允许私聊的QQ号列表
enable_pfc_chatting: bool # 是否启用PFC聊天 pfc_chatting: bool # 是否启用PFC聊天
# 模型配置 # 模型配置
llm_reasoning: Dict[str, Any] # 推理模型配置 llm_reasoning: Dict[str, Any] # 推理模型配置
llm_normal: Dict[str, Any] # 普通模型配置 llm_normal: Dict[str, Any] # 普通模型配置
llm_topic_judge: Dict[str, Any] # 主题判断模型配置 llm_topic_judge: Dict[str, Any] # 主题判断模型配置
llm_summary: Dict[str, Any] # 总结模型配置 llm_summary: Dict[str, Any] # 总结模型配置
llm_emotion_judge: Optional[Dict[str, Any]] # 情绪判断模型配置(兼容性保留)
embedding: Dict[str, Any] # 嵌入模型配置
vlm: Dict[str, Any] # VLM模型配置 vlm: Dict[str, Any] # VLM模型配置
moderation: Optional[Dict[str, Any]] # 审核模型配置(兼容性保留) llm_heartflow: Dict[str, Any] # 心流模型配置
llm_observation: Dict[str, Any] # 观察模型配置 llm_observation: Dict[str, Any] # 观察模型配置
llm_sub_heartflow: Dict[str, Any] # 子心流模型配置 llm_sub_heartflow: Dict[str, Any] # 子心流模型配置
llm_heartflow: Dict[str, Any] # 心流模型配置
llm_plan: Optional[Dict[str, Any]] # 计划模型配置 llm_plan: Optional[Dict[str, Any]] # 计划模型配置
embedding: Dict[str, Any] # 嵌入模型配置
llm_PFC_action_planner: Optional[Dict[str, Any]] # PFC行动计划模型配置 llm_PFC_action_planner: Optional[Dict[str, Any]] # PFC行动计划模型配置
llm_PFC_chat: Optional[Dict[str, Any]] # PFC聊天模型配置 llm_PFC_chat: Optional[Dict[str, Any]] # PFC聊天模型配置
llm_PFC_reply_checker: Optional[Dict[str, Any]] # PFC回复检查模型配置 llm_PFC_reply_checker: Optional[Dict[str, Any]] # PFC回复检查模型配置
@@ -150,6 +150,84 @@ class APIBotConfig:
api_urls: Optional[Dict[str, str]] # API地址配置 api_urls: Optional[Dict[str, str]] # API地址配置
@staticmethod
def validate_config(config: dict):
"""
校验传入的 toml 配置字典是否合法。
:param config: toml库load后的配置字典
:raises: ValueError, KeyError, TypeError
"""
# 检查主层级
required_sections = [
"inner",
"bot",
"groups",
"personality",
"identity",
"schedule",
"platforms",
"chat",
"normal_chat",
"focus_chat",
"emoji",
"memory",
"mood",
"keywords_reaction",
"chinese_typo",
"response_splitter",
"remote",
"experimental",
"model",
]
for section in required_sections:
if section not in config:
raise KeyError(f"缺少配置段: [{section}]")
# 检查部分关键字段
if "version" not in config["inner"]:
raise KeyError("缺少 inner.version 字段")
if not isinstance(config["inner"]["version"], str):
raise TypeError("inner.version 必须为字符串")
if "qq" not in config["bot"]:
raise KeyError("缺少 bot.qq 字段")
if not isinstance(config["bot"]["qq"], int):
raise TypeError("bot.qq 必须为整数")
if "personality_core" not in config["personality"]:
raise KeyError("缺少 personality.personality_core 字段")
if not isinstance(config["personality"]["personality_core"], str):
raise TypeError("personality.personality_core 必须为字符串")
if "identity_detail" not in config["identity"]:
raise KeyError("缺少 identity.identity_detail 字段")
if not isinstance(config["identity"]["identity_detail"], list):
raise TypeError("identity.identity_detail 必须为列表")
# 可继续添加更多字段的类型和值检查
# ...
# 检查模型配置
model_keys = [
"llm_reasoning",
"llm_normal",
"llm_topic_judge",
"llm_summary",
"vlm",
"llm_heartflow",
"llm_observation",
"llm_sub_heartflow",
"embedding",
]
if "model" not in config:
raise KeyError("缺少 [model] 配置段")
for key in model_keys:
if key not in config["model"]:
raise KeyError(f"缺少 model.{key} 配置")
# 检查通过
return True
@strawberry.type @strawberry.type
class APIEnvConfig: class APIEnvConfig:
@@ -182,6 +260,71 @@ class APIEnvConfig:
def get_env(self) -> str: def get_env(self) -> str:
return "env" return "env"
@staticmethod
def validate_config(config: dict):
"""
校验环境变量配置字典是否合法。
:param config: 环境变量配置字典
:raises: KeyError, TypeError
"""
required_fields = [
"HOST",
"PORT",
"PLUGINS",
"MONGODB_HOST",
"MONGODB_PORT",
"DATABASE_NAME",
"CHAT_ANY_WHERE_BASE_URL",
"SILICONFLOW_BASE_URL",
"DEEP_SEEK_BASE_URL",
]
for field in required_fields:
if field not in config:
raise KeyError(f"缺少环境变量配置字段: {field}")
if not isinstance(config["HOST"], str):
raise TypeError("HOST 必须为字符串")
if not isinstance(config["PORT"], int):
raise TypeError("PORT 必须为整数")
if not isinstance(config["PLUGINS"], list):
raise TypeError("PLUGINS 必须为列表")
if not isinstance(config["MONGODB_HOST"], str):
raise TypeError("MONGODB_HOST 必须为字符串")
if not isinstance(config["MONGODB_PORT"], int):
raise TypeError("MONGODB_PORT 必须为整数")
if not isinstance(config["DATABASE_NAME"], str):
raise TypeError("DATABASE_NAME 必须为字符串")
if not isinstance(config["CHAT_ANY_WHERE_BASE_URL"], str):
raise TypeError("CHAT_ANY_WHERE_BASE_URL 必须为字符串")
if not isinstance(config["SILICONFLOW_BASE_URL"], str):
raise TypeError("SILICONFLOW_BASE_URL 必须为字符串")
if not isinstance(config["DEEP_SEEK_BASE_URL"], str):
raise TypeError("DEEP_SEEK_BASE_URL 必须为字符串")
# 可选字段类型检查
optional_str_fields = [
"DEEP_SEEK_KEY",
"CHAT_ANY_WHERE_KEY",
"SILICONFLOW_KEY",
"CONSOLE_LOG_LEVEL",
"FILE_LOG_LEVEL",
"DEFAULT_CONSOLE_LOG_LEVEL",
"DEFAULT_FILE_LOG_LEVEL",
]
for field in optional_str_fields:
if field in config and config[field] is not None and not isinstance(config[field], str):
raise TypeError(f"{field} 必须为字符串或None")
if (
"SIMPLE_OUTPUT" in config
and config["SIMPLE_OUTPUT"] is not None
and not isinstance(config["SIMPLE_OUTPUT"], bool)
):
raise TypeError("SIMPLE_OUTPUT 必须为布尔值或None")
# 检查通过
return True
print("当前路径:") print("当前路径:")
print(ROOT_PATH) print(ROOT_PATH)

View File

@@ -16,6 +16,7 @@ from src.api.apiforgui import (
get_all_states, get_all_states,
) )
from src.heart_flow.sub_heartflow import ChatState from src.heart_flow.sub_heartflow import ChatState
from src.api.basic_info_api import get_all_basic_info # 新增导入
# import uvicorn # import uvicorn
# import os # import os
@@ -97,6 +98,18 @@ async def get_all_states_api():
return {"status": "failed", "reason": "failed to get all states"} return {"status": "failed", "reason": "failed to get all states"}
@router.get("/info")
async def get_system_basic_info():
"""获取系统基本信息"""
logger.info("请求系统基本信息")
try:
info = get_all_basic_info()
return {"status": "success", "data": info}
except Exception as e:
logger.error(f"获取系统基本信息失败: {e}")
return {"status": "failed", "reason": str(e)}
def start_api_server(): def start_api_server():
"""启动API服务器""" """启动API服务器"""
global_server.register_router(router, prefix="/api/v1") global_server.register_router(router, prefix="/api/v1")

View File

@@ -1,4 +1,5 @@
from fastapi import FastAPI, APIRouter from fastapi import FastAPI, APIRouter
from fastapi.middleware.cors import CORSMiddleware # 新增导入
from typing import Optional from typing import Optional
from uvicorn import Config, Server as UvicornServer from uvicorn import Config, Server as UvicornServer
import os import os
@@ -15,6 +16,21 @@ class Server:
self._server: Optional[UvicornServer] = None self._server: Optional[UvicornServer] = None
self.set_address(host, port) self.set_address(host, port)
# 配置 CORS
origins = [
"http://localhost:3000", # 允许的前端源
"http://127.0.0.1:3000",
# 在生产环境中,您应该添加实际的前端域名
]
self.app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True, # 是否支持 cookie
allow_methods=["*"], # 允许所有 HTTP 方法
allow_headers=["*"], # 允许所有 HTTP 请求头
)
def register_router(self, router: APIRouter, prefix: str = ""): def register_router(self, router: APIRouter, prefix: str = ""):
"""注册路由 """注册路由

View File

@@ -1,4 +1,4 @@
from src.plugins.moods.moods import MoodManager from src.manager.mood_manager import mood_manager
import enum import enum
@@ -13,5 +13,5 @@ class ChatStateInfo:
self.chat_status: ChatState = ChatState.CHAT self.chat_status: ChatState = ChatState.CHAT
self.current_state_time = 120 self.current_state_time = 120
self.mood_manager = MoodManager() self.mood_manager = mood_manager
self.mood = self.mood_manager.get_prompt() self.mood = self.mood_manager.get_mood_prompt()

View File

@@ -3,7 +3,7 @@ import time
import random import random
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager from src.manager.mood_manager import mood_manager
from src.config.config import global_config from src.config.config import global_config
logger = get_logger("mai_state") logger = get_logger("mai_state")
@@ -88,7 +88,7 @@ class MaiStateInfo:
self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间 self.last_min_check_time: float = time.time() # 上次1分钟规则检查时间
# Mood management is now part of MaiStateInfo # Mood management is now part of MaiStateInfo
self.mood_manager = MoodManager.get_instance() # Use singleton instance self.mood_manager = mood_manager # Use singleton instance
def update_mai_status(self, new_status: MaiState) -> bool: def update_mai_status(self, new_status: MaiState) -> bool:
""" """
@@ -124,7 +124,7 @@ class MaiStateInfo:
def get_mood_prompt(self) -> str: def get_mood_prompt(self) -> str:
"""获取当前的心情提示词""" """获取当前的心情提示词"""
# Delegate to the internal mood manager # Delegate to the internal mood manager
return self.mood_manager.get_prompt() return self.mood_manager.get_mood_prompt()
def get_current_state(self) -> MaiState: def get_current_state(self) -> MaiState:
"""获取当前的 MaiState""" """获取当前的 MaiState"""

View File

@@ -1,7 +1,12 @@
import asyncio import asyncio
import time import time
from .plugins.utils.statistic import LLMStatistics
from .plugins.moods.moods import MoodManager from maim_message import MessageServer
from .plugins.remote.remote import TelemetryHeartBeatTask
from .manager.async_task_manager import async_task_manager
from .plugins.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
from .manager.mood_manager import MoodPrintTask, MoodUpdateTask
from .plugins.schedule.schedule_generator import bot_schedule from .plugins.schedule.schedule_generator import bot_schedule
from .plugins.emoji_system.emoji_manager import emoji_manager from .plugins.emoji_system.emoji_manager import emoji_manager
from .plugins.person_info.person_info import person_info_manager from .plugins.person_info.person_info import person_info_manager
@@ -14,9 +19,8 @@ from .plugins.storage.storage import MessageStorage
from .config.config import global_config from .config.config import global_config
from .plugins.chat.bot import chat_bot from .plugins.chat.bot import chat_bot
from .common.logger_manager import get_logger from .common.logger_manager import get_logger
from .plugins.remote import heartbeat_thread # noqa: F401
from .individuality.individuality import Individuality from .individuality.individuality import Individuality
from .common.server import global_server from .common.server import global_server, Server
from rich.traceback import install from rich.traceback import install
from .plugins.heartFC_chat.expressors.exprssion_learner import expression_learner from .plugins.heartFC_chat.expressors.exprssion_learner import expression_learner
from .api.main import start_api_server from .api.main import start_api_server
@@ -28,17 +32,14 @@ logger = get_logger("main")
class MainSystem: class MainSystem:
def __init__(self): def __init__(self):
self.llm_stats = LLMStatistics("llm_statistics.txt") self.hippocampus_manager: HippocampusManager = HippocampusManager.get_instance()
self.mood_manager = MoodManager.get_instance() self.individuality: Individuality = Individuality.get_instance()
self.hippocampus_manager = HippocampusManager.get_instance()
self._message_manager_started = False
self.individuality = Individuality.get_instance()
# 使用消息API替代直接的FastAPI实例 # 使用消息API替代直接的FastAPI实例
from .plugins.message import global_api from .plugins.message import global_api
self.app = global_api self.app: MessageServer = global_api
self.server = global_server self.server: Server = global_server
async def initialize(self): async def initialize(self):
"""初始化系统组件""" """初始化系统组件"""
@@ -52,9 +53,15 @@ class MainSystem:
async def _init_components(self): async def _init_components(self):
"""初始化其他组件""" """初始化其他组件"""
init_start_time = time.time() init_start_time = time.time()
# 启动LLM统计
self.llm_stats.start() # 添加在线时间统计任务
logger.success("LLM统计功能启动成功") await async_task_manager.add_task(OnlineTimeRecordTask())
# 添加统计信息输出任务
await async_task_manager.add_task(StatisticOutputTask())
# 添加遥测心跳任务
await async_task_manager.add_task(TelemetryHeartBeatTask())
# 启动API服务器 # 启动API服务器
start_api_server() start_api_server()
@@ -63,9 +70,10 @@ class MainSystem:
emoji_manager.initialize() emoji_manager.initialize()
logger.success("表情包管理器初始化成功") logger.success("表情包管理器初始化成功")
# 启动情绪管理器 # 添加情绪衰减任务
self.mood_manager.start_mood_update(update_interval=global_config.mood_update_interval) await async_task_manager.add_task(MoodUpdateTask())
logger.success("情绪管理器启动成功") # 添加情绪打印任务
await async_task_manager.add_task(MoodPrintTask())
# 检查并清除person_info冗余字段启动个人习惯推断 # 检查并清除person_info冗余字段启动个人习惯推断
await person_info_manager.del_all_undefined_field() await person_info_manager.del_all_undefined_field()

View File

@@ -0,0 +1,150 @@
from abc import abstractmethod
import asyncio
from asyncio import Task, Event, Lock
from typing import Callable, Dict
from src.common.logger_manager import get_logger
logger = get_logger("async_task_manager")
class AsyncTask:
"""异步任务基类"""
def __init__(self, task_name: str | None = None, wait_before_start: int = 0, run_interval: int = 0):
self.task_name: str = task_name or self.__class__.__name__
"""任务名称"""
self.wait_before_start: int = wait_before_start
"""运行任务前是否进行等待单位设为0则不等待"""
self.run_interval: int = run_interval
"""多次运行的时间间隔单位设为0则仅运行一次"""
@abstractmethod
async def run(self):
"""
任务的执行过程
"""
pass
async def start_task(self, abort_flag: asyncio.Event):
if self.wait_before_start > 0:
# 等待指定时间后开始任务
await asyncio.sleep(self.wait_before_start)
while not abort_flag.is_set():
await self.run()
if self.run_interval > 0:
await asyncio.sleep(self.run_interval)
else:
break
class AsyncTaskManager:
"""异步任务管理器"""
def __init__(self):
self.tasks: Dict[str, Task] = {}
"""任务列表"""
self.abort_flag: Event = Event()
"""是否中止任务标志"""
self._lock: Lock = Lock()
"""异步锁当可能出现await时需要加锁"""
def _remove_task_call_back(self, task: Task):
"""
call_back: 任务完成后移除任务
"""
task_name = task.get_name()
if task_name in self.tasks:
# 任务完成后移除任务
del self.tasks[task_name]
logger.debug(f"已移除任务 '{task_name}'")
else:
logger.warning(f"尝试移除不存在的任务 '{task_name}'")
@staticmethod
def _default_finish_call_back(task: Task):
"""
call_back: 默认的任务完成回调函数
"""
try:
task.result()
logger.debug(f"任务 '{task.get_name()}' 完成")
except asyncio.CancelledError:
logger.debug(f"任务 '{task.get_name()}' 被取消")
except Exception as e:
logger.error(f"任务 '{task.get_name()}' 执行时发生异常: {e}", exc_info=True)
async def add_task(self, task: AsyncTask, call_back: Callable[[asyncio.Task], None] | None = None):
"""
添加任务
"""
if not issubclass(task.__class__, AsyncTask):
raise TypeError(f"task '{task.__class__.__name__}' 必须是继承 AsyncTask 的子类")
async with self._lock: # 由于可能需要await等待任务完成所以需要加异步锁
if task.task_name in self.tasks:
logger.warning(f"已存在名称为 '{task.task_name}' 的任务,正在尝试取消并替换")
self.tasks[task.task_name].cancel() # 取消已存在的任务
await self.tasks[task.task_name] # 等待任务完成
logger.info(f"成功结束任务 '{task.task_name}'")
# 创建新任务
task_inst = asyncio.create_task(task.start_task(self.abort_flag))
task_inst.set_name(task.task_name)
task_inst.add_done_callback(self._remove_task_call_back) # 添加完成回调函数-完成任务后自动移除任务
task_inst.add_done_callback(
call_back or self._default_finish_call_back
) # 添加完成回调函数-用户自定义或默认的FallBack
self.tasks[task.task_name] = task_inst # 将任务添加到任务列表
logger.info(f"已启动任务 '{task.task_name}'")
def get_tasks_status(self) -> Dict[str, Dict[str, str]]:
"""
获取所有任务的状态
"""
tasks_status = {}
for task_name, task in self.tasks.items():
tasks_status[task_name] = {
"status": "running" if not task.done() else "done",
}
return tasks_status
async def stop_and_wait_all_tasks(self):
"""
终止所有任务并等待它们完成该方法会阻塞其它尝试add_task()的操作)
"""
async with self._lock: # 由于可能需要await等待任务完成所以需要加异步锁
# 设置中止标志
self.abort_flag.set()
# 取消所有任务
for name, inst in self.tasks.items():
try:
inst.cancel()
except asyncio.CancelledError:
logger.info(f"已取消任务 '{name}'")
# 等待所有任务完成
for task_name, task_inst in self.tasks.items():
if not task_inst.done():
try:
await task_inst
except asyncio.CancelledError: # 此处再次捕获取消异常防止stop_all_tasks()时延迟抛出异常
logger.info(f"任务 {task_name} 已取消")
except Exception as e:
logger.error(f"任务 {task_name} 执行时发生异常: {e}", ext_info=True)
# 清空任务列表
self.tasks.clear()
self.abort_flag.clear()
logger.info("所有异步任务已停止")
async_task_manager = AsyncTaskManager()
"""全局异步任务管理器实例"""

View File

@@ -0,0 +1,67 @@
import json
import os
from src.common.logger_manager import get_logger
LOCAL_STORE_FILE_PATH = "data/local_store.json"
logger = get_logger("local_storage")
class LocalStoreManager:
file_path: str
"""本地存储路径"""
store: dict[str, str | list | dict | int | float | bool]
"""本地存储数据"""
def __init__(self, local_store_path: str | None = None):
self.file_path = local_store_path or LOCAL_STORE_FILE_PATH
self.store = {}
self.load_local_store()
def __getitem__(self, item: str) -> str | list | dict | int | float | bool | None:
"""获取本地存储数据"""
return self.store.get(item, None)
def __setitem__(self, key: str, value: str | list | dict | int | float | bool):
"""设置本地存储数据"""
self.store[key] = value
self.save_local_store()
def __contains__(self, item: str) -> bool:
"""检查本地存储数据是否存在"""
return item in self.store
def load_local_store(self):
"""加载本地存储数据"""
if os.path.exists(self.file_path):
# 存在本地存储文件,加载数据
logger.info("正在阅读记事本......我在看,我真的在看!")
logger.debug(f"加载本地存储数据: {self.file_path}")
try:
with open(self.file_path, "r", encoding="utf-8") as f:
self.store = json.load(f)
logger.success("全都记起来了!")
except json.JSONDecodeError:
logger.warning("啊咧?记事本被弄脏了,正在重建记事本......")
self.store = {}
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
logger.success("记事本重建成功!")
else:
# 不存在本地存储文件,创建新的目录和文件
logger.warning("啊咧?记事本不存在,正在创建新的记事本......")
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False, indent=4)
logger.success("记事本创建成功!")
def save_local_store(self):
"""保存本地存储数据"""
logger.debug(f"保存本地存储数据: {self.file_path}")
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(self.store, f, ensure_ascii=False, indent=4)
local_storage = LocalStoreManager("data/local_store.json") # 全局单例化

296
src/manager/mood_manager.py Normal file
View File

@@ -0,0 +1,296 @@
import asyncio
import math
import time
from dataclasses import dataclass
from typing import Dict, Tuple
from ..config.config import global_config
from ..common.logger_manager import get_logger
from ..manager.async_task_manager import AsyncTask
from ..individuality.individuality import Individuality
logger = get_logger("mood")
@dataclass
class MoodState:
valence: float
"""愉悦度 (-1.0 到 1.0)-1表示极度负面1表示极度正面"""
arousal: float
"""唤醒度 (-1.0 到 1.0)-1表示抑制1表示兴奋"""
text: str
"""心情的文本描述"""
@dataclass
class MoodChangeHistory:
valence_direction_factor: int
"""愉悦度变化的系数(正为增益,负为抑制)"""
arousal_direction_factor: int
"""唤醒度变化的系数(正为增益,负为抑制)"""
class MoodUpdateTask(AsyncTask):
def __init__(self):
super().__init__(
task_name="Mood Update Task",
wait_before_start=global_config.mood_update_interval,
run_interval=global_config.mood_update_interval,
)
# 从配置文件获取衰减率
self.decay_rate_valence: float = 1 - global_config.mood_decay_rate
"""愉悦度衰减率"""
self.decay_rate_arousal: float = 1 - global_config.mood_decay_rate
"""唤醒度衰减率"""
self.last_update = time.time()
"""上次更新时间"""
async def run(self):
current_time = time.time()
time_diff = current_time - self.last_update
agreeableness_factor = 1 # 宜人性系数
agreeableness_bias = 0 # 宜人性偏置
neuroticism_factor = 0.5 # 神经质系数
# 获取人格特质
personality = Individuality.get_instance().personality
if personality:
# 神经质:影响情绪变化速度
neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
# 宜人性:影响情绪基准线
if personality.agreeableness < 0.2:
agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
elif personality.agreeableness > 0.8:
agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
else:
agreeableness_bias = 0
# 分别计算正向和负向的衰减率
if mood_manager.current_mood.valence >= 0:
# 正向情绪衰减
decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
valence_target = 0 + agreeableness_bias
new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
-decay_rate_positive * time_diff * neuroticism_factor
)
else:
# 负向情绪衰减
decay_rate_negative = self.decay_rate_valence * agreeableness_factor
valence_target = 0 + agreeableness_bias
new_valence = valence_target + (mood_manager.current_mood.valence - valence_target) * math.exp(
-decay_rate_negative * time_diff * neuroticism_factor
)
# Arousal 向中性0回归
arousal_target = 0
new_arousal = arousal_target + (mood_manager.current_mood.arousal - arousal_target) * math.exp(
-self.decay_rate_arousal * time_diff * neuroticism_factor
)
mood_manager.set_current_mood(new_valence, new_arousal)
self.last_update = current_time
class MoodPrintTask(AsyncTask):
def __init__(self):
super().__init__(
task_name="Mood Print Task",
wait_before_start=60,
run_interval=60,
)
async def run(self):
# 打印当前心情
logger.info(
f"愉悦度: {mood_manager.current_mood.valence:.2f}, "
f"唤醒度: {mood_manager.current_mood.arousal:.2f}, "
f"心情: {mood_manager.current_mood.text}"
)
class MoodManager:
# TODO: 改进,使用具有实验支持的新情绪模型
EMOTION_FACTOR_MAP: Dict[str, Tuple[float, float]] = {
"开心": (0.21, 0.6),
"害羞": (0.15, 0.2),
"愤怒": (-0.24, 0.8),
"恐惧": (-0.21, 0.7),
"悲伤": (-0.21, 0.3),
"厌恶": (-0.12, 0.4),
"惊讶": (0.06, 0.7),
"困惑": (0.0, 0.6),
"平静": (0.03, 0.5),
}
"""
情绪词映射表 {mood: (valence, arousal)}
将情绪描述词映射到愉悦度和唤醒度的元组
"""
EMOTION_POINT_MAP: Dict[Tuple[float, float], str] = {
# 第一象限:高唤醒,正愉悦
(0.5, 0.4): "兴奋",
(0.3, 0.6): "快乐",
(0.2, 0.3): "满足",
# 第二象限:高唤醒,负愉悦
(-0.5, 0.4): "愤怒",
(-0.3, 0.6): "焦虑",
(-0.2, 0.3): "烦躁",
# 第三象限:低唤醒,负愉悦
(-0.5, -0.4): "悲伤",
(-0.3, -0.3): "疲倦",
(-0.4, -0.7): "疲倦",
# 第四象限:低唤醒,正愉悦
(0.2, -0.1): "平静",
(0.3, -0.2): "安宁",
(0.5, -0.4): "放松",
}
"""
情绪文本映射表 {(valence, arousal): mood}
将量化的情绪状态元组映射到文本描述
"""
def __init__(self):
self.current_mood = MoodState(
valence=0.0,
arousal=0.0,
text="平静",
)
"""当前情绪状态"""
self.mood_change_history: MoodChangeHistory = MoodChangeHistory(
valence_direction_factor=0,
arousal_direction_factor=0,
)
"""情绪变化历史"""
self._lock = asyncio.Lock()
"""异步锁,用于保护线程安全"""
def set_current_mood(self, new_valence: float, new_arousal: float):
"""
设置当前情绪状态
:param new_valence: 新的愉悦度
:param new_arousal: 新的唤醒度
"""
# 限制范围
self.current_mood.valence = max(-1.0, min(new_valence, 1.0))
self.current_mood.arousal = max(-1.0, min(new_arousal, 1.0))
closest_mood = None
min_distance = float("inf")
for (v, a), text in self.EMOTION_POINT_MAP.items():
# 计算当前情绪状态与每个情绪文本的欧氏距离
distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
if distance < min_distance:
min_distance = distance
closest_mood = text
if closest_mood:
self.current_mood.text = closest_mood
def update_current_mood(self, valence_delta: float, arousal_delta: float):
"""
根据愉悦度和唤醒度变化量更新当前情绪状态
:param valence_delta: 愉悦度变化量
:param arousal_delta: 唤醒度变化量
"""
# 计算连续增益/抑制
# 规则:多次相同方向的变化会有更大的影响系数,反方向的变化会清零影响系数(系数的正负号由变化方向决定)
if valence_delta * self.mood_change_history.valence_direction_factor > 0:
# 如果方向相同,则根据变化方向改变系数
if valence_delta > 0:
self.mood_change_history.valence_direction_factor += 1 # 若为正向,则增加
else:
self.mood_change_history.valence_direction_factor -= 1 # 若为负向,则减少
else:
# 如果方向不同,则重置计数
self.mood_change_history.valence_direction_factor = 0
if arousal_delta * self.mood_change_history.arousal_direction_factor > 0:
# 如果方向相同,则根据变化方向改变系数
if arousal_delta > 0:
self.mood_change_history.arousal_direction_factor += 1 # 若为正向,则增加计数
else:
self.mood_change_history.arousal_direction_factor -= 1 # 若为负向,则减少计数
else:
# 如果方向不同,则重置计数
self.mood_change_history.arousal_direction_factor = 0
# 计算增益/抑制的结果
# 规则:如果当前情绪状态与变化方向相同,则增益;否则抑制
if self.current_mood.valence * self.mood_change_history.valence_direction_factor > 0:
valence_delta = valence_delta * (1.01 ** abs(self.mood_change_history.valence_direction_factor))
else:
valence_delta = valence_delta * (0.99 ** abs(self.mood_change_history.valence_direction_factor))
if self.current_mood.arousal * self.mood_change_history.arousal_direction_factor > 0:
arousal_delta = arousal_delta * (1.01 ** abs(self.mood_change_history.arousal_direction_factor))
else:
arousal_delta = arousal_delta * (0.99 ** abs(self.mood_change_history.arousal_direction_factor))
self.set_current_mood(
new_valence=self.current_mood.valence + valence_delta,
new_arousal=self.current_mood.arousal + arousal_delta,
)
def get_mood_prompt(self) -> str:
"""
根据当前情绪状态生成提示词
"""
base_prompt = f"当前心情:{self.current_mood.text}"
# 根据情绪状态添加额外的提示信息
if self.current_mood.valence > 0.5:
base_prompt += "你现在心情很好,"
elif self.current_mood.valence < -0.5:
base_prompt += "你现在心情不太好,"
if self.current_mood.arousal > 0.4:
base_prompt += "情绪比较激动。"
elif self.current_mood.arousal < -0.4:
base_prompt += "情绪比较平静。"
return base_prompt
def get_arousal_multiplier(self) -> float:
"""
根据当前情绪状态返回唤醒度乘数
"""
if self.current_mood.arousal > 0.4:
multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
return multiplier
elif self.current_mood.arousal < -0.4:
multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
return multiplier
return 1.0
def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
"""
根据情绪词更新心情状态
:param emotion: 情绪词(如'开心', '悲伤'等位于self.EMOTION_FACTOR_MAP中的键
:param intensity: 情绪强度0.0-1.0
"""
if emotion not in self.EMOTION_FACTOR_MAP:
logger.error(f"[情绪更新] 未知情绪词: {emotion}")
return
valence_change, arousal_change = self.EMOTION_FACTOR_MAP[emotion]
old_valence = self.current_mood.valence
old_arousal = self.current_mood.arousal
old_mood = self.current_mood.text
self.update_current_mood(valence_change, arousal_change) # 更新当前情绪状态
logger.info(
f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
)
mood_manager = MoodManager()
"""全局情绪管理器"""

View File

@@ -6,7 +6,6 @@ MaiMBot插件系统
from .chat.chat_stream import chat_manager from .chat.chat_stream import chat_manager
from .emoji_system.emoji_manager import emoji_manager from .emoji_system.emoji_manager import emoji_manager
from .person_info.relationship_manager import relationship_manager from .person_info.relationship_manager import relationship_manager
from .moods.moods import MoodManager
from .willing.willing_manager import willing_manager from .willing.willing_manager import willing_manager
from .schedule.schedule_generator import bot_schedule from .schedule.schedule_generator import bot_schedule
@@ -15,7 +14,6 @@ __all__ = [
"chat_manager", "chat_manager",
"emoji_manager", "emoji_manager",
"relationship_manager", "relationship_manager",
"MoodManager",
"willing_manager", "willing_manager",
"bot_schedule", "bot_schedule",
] ]

View File

@@ -1,16 +1,15 @@
import traceback
from typing import Dict, Any from typing import Dict, Any
from ..moods.moods import MoodManager # 导入情绪管理器
from ...config.config import global_config
from .message import MessageRecv
from ..PFC.pfc_manager import PFCManager
from .chat_stream import chat_manager
from .only_message_process import MessageProcessor
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.manager.mood_manager import mood_manager # 导入情绪管理器
from .chat_stream import chat_manager
from .message import MessageRecv
from .only_message_process import MessageProcessor
from ..PFC.pfc_manager import PFCManager
from ..heartFC_chat.heartflow_processor import HeartFCProcessor from ..heartFC_chat.heartflow_processor import HeartFCProcessor
from ..utils.prompt_builder import Prompt, global_prompt_manager from ..utils.prompt_builder import Prompt, global_prompt_manager
import traceback from ...config.config import global_config
# 定义日志配置 # 定义日志配置
@@ -23,7 +22,7 @@ class ChatBot:
def __init__(self): def __init__(self):
self.bot = None # bot 实例引用 self.bot = None # bot 实例引用
self._started = False self._started = False
self.mood_manager = MoodManager.get_instance() # 获取情绪管理器单例 self.mood_manager = mood_manager # 获取情绪管理器单例
self.heartflow_processor = HeartFCProcessor() # 新增 self.heartflow_processor = HeartFCProcessor() # 新增
# 创建初始化PFC管理器的任务会在_ensure_started时执行 # 创建初始化PFC管理器的任务会在_ensure_started时执行

View File

@@ -1,21 +1,20 @@
import random import random
import time
import re import re
import time
from collections import Counter from collections import Counter
import jieba import jieba
import numpy as np import numpy as np
from src.common.logger import get_module_logger from maim_message import UserInfo
from pymongo.errors import PyMongoError from pymongo.errors import PyMongoError
from src.common.logger import get_module_logger
from src.manager.mood_manager import mood_manager
from .message import MessageRecv
from ..models.utils_model import LLMRequest from ..models.utils_model import LLMRequest
from ..utils.typo_generator import ChineseTypoGenerator from ..utils.typo_generator import ChineseTypoGenerator
from ...config.config import global_config
from .message import MessageRecv
from maim_message import UserInfo
from ..moods.moods import MoodManager
from ...common.database import db from ...common.database import db
from ...config.config import global_config
logger = get_module_logger("chat_utils") logger = get_module_logger("chat_utils")
@@ -405,7 +404,6 @@ def calculate_typing_time(
- 在所有输入结束后额外加上回车时间0.3秒 - 在所有输入结束后额外加上回车时间0.3秒
- 如果is_emoji为True将使用固定1秒的输入时间 - 如果is_emoji为True将使用固定1秒的输入时间
""" """
mood_manager = MoodManager.get_instance()
# 将0-1的唤醒度映射到-1到1 # 将0-1的唤醒度映射到-1到1
mood_arousal = mood_manager.current_mood.arousal mood_arousal = mood_manager.current_mood.arousal
# 映射到0.5到2倍的速度系数 # 映射到0.5到2倍的速度系数

View File

@@ -1,37 +1,35 @@
import asyncio import asyncio
import contextlib
import json # <--- 确保导入 json
import random # <--- 添加导入
import time import time
import traceback import traceback
import random # <--- 添加导入
import json # <--- 确保导入 json
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from collections import deque from collections import deque
from typing import List, Optional, Dict, Any, Deque, Callable, Coroutine
from rich.traceback import install
from src.common.logger_manager import get_logger
from src.config.config import global_config
from src.heart_flow.observation import Observation
from src.heart_flow.sub_mind import SubMind
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
from src.plugins.chat.chat_stream import ChatStream from src.plugins.chat.chat_stream import ChatStream
from src.plugins.chat.chat_stream import chat_manager from src.plugins.chat.chat_stream import chat_manager
from src.common.logger_manager import get_logger from src.plugins.chat.message import MessageRecv, BaseMessageInfo, MessageThinking, MessageSending
from src.plugins.chat.message import Seg # Local import needed after move
from src.plugins.chat.message import UserInfo
from src.plugins.chat.utils import process_llm_response
from src.plugins.chat.utils_image import image_path_to_base64 # Local import needed after move
from src.plugins.emoji_system.emoji_manager import emoji_manager
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleInfo
from src.plugins.heartFC_chat.heartflow_prompt_builder import global_prompt_manager, prompt_builder
from src.plugins.models.utils_model import LLMRequest from src.plugins.models.utils_model import LLMRequest
from src.config.config import global_config from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.timer_calculator import Timer from src.plugins.utils.chat_message_builder import num_new_messages_since
from src.heart_flow.observation.observation import Observation from src.plugins.utils.timer_calculator import Timer # <--- Import Timer
from src.plugins.heartFC_chat.heartflow_prompt_builder import prompt_builder from .heartFC_sender import HeartFCSender
import contextlib
from src.plugins.heartFC_chat.heartFC_Cycleinfo import CycleDetail
from src.heart_flow.observation.chatting_observation import ChattingObservation
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from rich.traceback import install
from src.heart_flow.info.info_base import InfoBase
from src.heart_flow.info.obs_info import ObsInfo
from src.heart_flow.info.cycle_info import CycleInfo
from src.heart_flow.info.mind_info import MindInfo
from src.heart_flow.info.structured_info import StructuredInfo
from src.plugins.heartFC_chat.info_processors.chattinginfo_processor import ChattingInfoProcessor
from src.plugins.heartFC_chat.info_processors.mind_processor import MindProcessor
from src.heart_flow.observation.memory_observation import MemoryObservation
from src.heart_flow.observation.hfcloop_observation import HFCloopObservation
from src.heart_flow.observation.working_observation import WorkingObservation
from src.plugins.heartFC_chat.info_processors.tool_processor import ToolProcessor
from src.plugins.heartFC_chat.expressors.default_expressor import DefaultExpressor
from src.plugins.heartFC_chat.hfc_utils import _create_empty_anchor_message
from src.plugins.heartFC_chat.memory_activator import MemoryActivator
install(extra_lines=3) install(extra_lines=3)

View File

@@ -10,7 +10,7 @@ import time
from typing import Union, Optional, Dict, Any from typing import Union, Optional, Dict, Any
from ...common.database import db from ...common.database import db
from ..chat.utils import get_recent_group_speaker from ..chat.utils import get_recent_group_speaker
from ..moods.moods import MoodManager from src.manager.mood_manager import mood_manager
from ..memory_system.Hippocampus import HippocampusManager from ..memory_system.Hippocampus import HippocampusManager
from ..schedule.schedule_generator import bot_schedule from ..schedule.schedule_generator import bot_schedule
from ..knowledge.knowledge_lib import qa_manager from ..knowledge.knowledge_lib import qa_manager
@@ -383,8 +383,7 @@ class PromptBuilder:
else: else:
logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}") logger.warning(f"Invalid person tuple encountered for relationship prompt: {person}")
mood_manager = MoodManager.get_instance() mood_prompt = mood_manager.get_mood_prompt()
mood_prompt = mood_manager.get_prompt()
reply_styles1 = [ reply_styles1 = [
("然后给出日常且口语化的回复,平淡一些", 0.4), ("然后给出日常且口语化的回复,平淡一些", 0.4),
("给出非常简短的回复", 0.4), ("给出非常简短的回复", 0.4),

View File

@@ -1,26 +1,26 @@
import time
import asyncio import asyncio
import traceback
import statistics # 导入 statistics 模块 import statistics # 导入 statistics 模块
import time
import traceback
from random import random from random import random
from typing import List, Optional # 导入 Optional from typing import List, Optional # 导入 Optional
from ..moods.moods import MoodManager
from ...config.config import global_config
from ..emoji_system.emoji_manager import emoji_manager
from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..willing.willing_manager import willing_manager
from maim_message import UserInfo, Seg from maim_message import UserInfo, Seg
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.heart_flow.utils_chat import get_chat_type_and_target_info
from src.manager.mood_manager import mood_manager
from src.plugins.chat.chat_stream import ChatStream, chat_manager from src.plugins.chat.chat_stream import ChatStream, chat_manager
from src.plugins.person_info.relationship_manager import relationship_manager from src.plugins.person_info.relationship_manager import relationship_manager
from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager from src.plugins.respon_info_catcher.info_catcher import info_catcher_manager
from src.plugins.utils.timer_calculator import Timer from src.plugins.utils.timer_calculator import Timer
from src.heart_flow.utils_chat import get_chat_type_and_target_info from .normal_chat_generator import NormalChatGenerator
from ..chat.message import MessageSending, MessageRecv, MessageThinking, MessageSet
from ..chat.message_sender import message_manager
from ..chat.utils_image import image_path_to_base64
from ..emoji_system.emoji_manager import emoji_manager
from ..willing.willing_manager import willing_manager
from ...config.config import global_config
logger = get_logger("chat") logger = get_logger("chat")
@@ -45,7 +45,7 @@ class NormalChat:
# Other sync initializations # Other sync initializations
self.gpt = NormalChatGenerator() self.gpt = NormalChatGenerator()
self.mood_manager = MoodManager.get_instance() self.mood_manager = mood_manager
self.start_time = time.time() self.start_time = time.time()
self.last_speak_time = 0 self.last_speak_time = 0
self._chat_task: Optional[asyncio.Task] = None self._chat_task: Optional[asyncio.Task] = None

View File

@@ -1,293 +0,0 @@
import math
import threading
import time
from dataclasses import dataclass
from ...config.config import global_config
from src.common.logger_manager import get_logger
from ..person_info.relationship_manager import relationship_manager
from src.individuality.individuality import Individuality
logger = get_logger("mood")
@dataclass
class MoodState:
valence: float # 愉悦度 (-1.0 到 1.0)-1表示极度负面1表示极度正面
arousal: float # 唤醒度 (-1.0 到 1.0)-1表示抑制1表示兴奋
text: str # 心情文本描述
class MoodManager:
_instance = None
_lock = threading.Lock()
def __new__(cls):
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
# 确保初始化代码只运行一次
if self._initialized:
return
self._initialized = True
# 初始化心情状态
self.current_mood = MoodState(valence=0.0, arousal=0.0, text="平静")
# 从配置文件获取衰减率
self.decay_rate_valence = 1 - global_config.mood_decay_rate # 愉悦度衰减率
self.decay_rate_arousal = 1 - global_config.mood_decay_rate # 唤醒度衰减率
# 上次更新时间
self.last_update = time.time()
# 线程控制
self._running = False
self._update_thread = None
# 情绪词映射表 (valence, arousal)
self.emotion_map = {
"开心": (0.21, 0.6),
"害羞": (0.15, 0.2),
"愤怒": (-0.24, 0.8),
"恐惧": (-0.21, 0.7),
"悲伤": (-0.21, 0.3),
"厌恶": (-0.12, 0.4),
"惊讶": (0.06, 0.7),
"困惑": (0.0, 0.6),
"平静": (0.03, 0.5),
}
# 情绪文本映射表
self.mood_text_map = {
# 第一象限:高唤醒,正愉悦
(0.5, 0.4): "兴奋",
(0.3, 0.6): "快乐",
(0.2, 0.3): "满足",
# 第二象限:高唤醒,负愉悦
(-0.5, 0.4): "愤怒",
(-0.3, 0.6): "焦虑",
(-0.2, 0.3): "烦躁",
# 第三象限:低唤醒,负愉悦
(-0.5, -0.4): "悲伤",
(-0.3, -0.3): "疲倦",
(-0.4, -0.7): "疲倦",
# 第四象限:低唤醒,正愉悦
(0.2, -0.1): "平静",
(0.3, -0.2): "安宁",
(0.5, -0.4): "放松",
}
@classmethod
def get_instance(cls) -> "MoodManager":
"""获取MoodManager的单例实例"""
if cls._instance is None:
cls._instance = MoodManager()
return cls._instance
def start_mood_update(self, update_interval: float = 5.0) -> None:
"""
启动情绪更新线程
:param update_interval: 更新间隔(秒)
"""
if self._running:
return
self._running = True
self._update_thread = threading.Thread(
target=self._continuous_mood_update, args=(update_interval,), daemon=True
)
self._update_thread.start()
def stop_mood_update(self) -> None:
"""停止情绪更新线程"""
self._running = False
if self._update_thread and self._update_thread.is_alive():
self._update_thread.join()
def _continuous_mood_update(self, update_interval: float) -> None:
"""
持续更新情绪状态的线程函数
:param update_interval: 更新间隔(秒)
"""
while self._running:
self._apply_decay()
self._update_mood_text()
time.sleep(update_interval)
def _apply_decay(self) -> None:
"""应用情绪衰减,正向和负向情绪分开计算"""
current_time = time.time()
time_diff = current_time - self.last_update
agreeableness_factor = 1
agreeableness_bias = 0
neuroticism_factor = 0.5
# 获取人格特质
personality = Individuality.get_instance().personality
if personality:
# 神经质:影响情绪变化速度
neuroticism_factor = 1 + (personality.neuroticism - 0.5) * 0.4
agreeableness_factor = 1 + (personality.agreeableness - 0.5) * 0.4
# 宜人性:影响情绪基准线
if personality.agreeableness < 0.2:
agreeableness_bias = (personality.agreeableness - 0.2) * 0.5
elif personality.agreeableness > 0.8:
agreeableness_bias = (personality.agreeableness - 0.8) * 0.5
else:
agreeableness_bias = 0
# 分别计算正向和负向的衰减率
if self.current_mood.valence >= 0:
# 正向情绪衰减
decay_rate_positive = self.decay_rate_valence * (1 / agreeableness_factor)
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_positive * time_diff * neuroticism_factor
)
else:
# 负向情绪衰减
decay_rate_negative = self.decay_rate_valence * agreeableness_factor
valence_target = 0 + agreeableness_bias
self.current_mood.valence = valence_target + (self.current_mood.valence - valence_target) * math.exp(
-decay_rate_negative * time_diff * neuroticism_factor
)
# Arousal 向中性0回归
arousal_target = 0
self.current_mood.arousal = arousal_target + (self.current_mood.arousal - arousal_target) * math.exp(
-self.decay_rate_arousal * time_diff * neuroticism_factor
)
# 确保值在合理范围内
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self.last_update = current_time
def update_mood_from_text(self, text: str, valence_change: float, arousal_change: float) -> None:
"""根据输入文本更新情绪状态"""
self.current_mood.valence += valence_change
self.current_mood.arousal += arousal_change
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
def set_mood_text(self, text: str) -> None:
"""直接设置心情文本"""
self.current_mood.text = text
def _update_mood_text(self) -> None:
"""根据当前情绪状态更新文本描述"""
closest_mood = None
min_distance = float("inf")
for (v, a), text in self.mood_text_map.items():
distance = math.sqrt((self.current_mood.valence - v) ** 2 + (self.current_mood.arousal - a) ** 2)
if distance < min_distance:
min_distance = distance
closest_mood = text
if closest_mood:
self.current_mood.text = closest_mood
def update_mood_by_user(self, user_id: str, valence_change: float, arousal_change: float) -> None:
"""根据用户ID更新情绪状态"""
# 这里可以根据用户ID添加特定的权重或规则
weight = 1.0 # 默认权重
self.current_mood.valence += valence_change * weight
self.current_mood.arousal += arousal_change * weight
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
def get_prompt(self) -> str:
"""根据当前情绪状态生成提示词"""
base_prompt = f"当前心情:{self.current_mood.text}"
# 根据情绪状态添加额外的提示信息
if self.current_mood.valence > 0.5:
base_prompt += "你现在心情很好,"
elif self.current_mood.valence < -0.5:
base_prompt += "你现在心情不太好,"
if self.current_mood.arousal > 0.4:
base_prompt += "情绪比较激动。"
elif self.current_mood.arousal < -0.4:
base_prompt += "情绪比较平静。"
return base_prompt
def get_arousal_multiplier(self) -> float:
"""根据当前情绪状态返回唤醒度乘数"""
if self.current_mood.arousal > 0.4:
multiplier = 1 + min(0.15, (self.current_mood.arousal - 0.4) / 3)
return multiplier
elif self.current_mood.arousal < -0.4:
multiplier = 1 - min(0.15, ((0 - self.current_mood.arousal) - 0.4) / 3)
return multiplier
return 1.0
def get_current_mood(self) -> MoodState:
"""获取当前情绪状态"""
return self.current_mood
def print_mood_status(self) -> None:
"""打印当前情绪状态"""
logger.info(
f"愉悦度: {self.current_mood.valence:.2f}, "
f"唤醒度: {self.current_mood.arousal:.2f}, "
f"心情: {self.current_mood.text}"
)
def update_mood_from_emotion(self, emotion: str, intensity: float = 1.0) -> None:
"""
根据情绪词更新心情状态
:param emotion: 情绪词(如'happy', 'sad'等)
:param intensity: 情绪强度0.0-1.0
"""
if emotion not in self.emotion_map:
logger.debug(f"[情绪更新] 未知情绪词: {emotion}")
return
valence_change, arousal_change = self.emotion_map[emotion]
old_valence = self.current_mood.valence
old_arousal = self.current_mood.arousal
old_mood = self.current_mood.text
valence_change = relationship_manager.feedback_to_mood(valence_change)
# 应用情绪强度
valence_change *= intensity
arousal_change *= intensity
# 更新当前情绪状态
self.current_mood.valence += valence_change
self.current_mood.arousal += arousal_change
# 限制范围
self.current_mood.valence = max(-1.0, min(1.0, self.current_mood.valence))
self.current_mood.arousal = max(-1.0, min(1.0, self.current_mood.arousal))
self._update_mood_text()
logger.info(
f"[情绪变化] {emotion}(强度:{intensity:.2f}) | 愉悦度:{old_valence:.2f}->{self.current_mood.valence:.2f}, 唤醒度:{old_arousal:.2f}->{self.current_mood.arousal:.2f} | 心情:{old_mood}->{self.current_mood.text}"
)

View File

@@ -6,6 +6,9 @@ from .person_info import person_info_manager
import time import time
import random import random
from maim_message import UserInfo from maim_message import UserInfo
from ...manager.mood_manager import mood_manager
# import re # import re
# import traceback # import traceback
@@ -22,9 +25,7 @@ class RelationshipManager:
@property @property
def mood_manager(self): def mood_manager(self):
if self._mood_manager is None: if self._mood_manager is None:
from ..moods.moods import MoodManager # 延迟导入 self._mood_manager = mood_manager
self._mood_manager = MoodManager.get_instance()
return self._mood_manager return self._mood_manager
def positive_feedback_sys(self, label: str, stance: str): def positive_feedback_sys(self, label: str, stance: str):
@@ -60,9 +61,7 @@ class RelationshipManager:
def mood_feedback(self, value): def mood_feedback(self, value):
"""情绪反馈""" """情绪反馈"""
mood_manager = self.mood_manager mood_manager = self.mood_manager
mood_gain = mood_manager.get_current_mood().valence ** 2 * math.copysign( mood_gain = mood_manager.current_mood.valence**2 * math.copysign(1, value * mood_manager.current_mood.valence)
1, value * mood_manager.get_current_mood().valence
)
value += value * mood_gain value += value * mood_gain
logger.info(f"当前relationship增益系数{mood_gain:.3f}") logger.info(f"当前relationship增益系数{mood_gain:.3f}")
return value return value

View File

@@ -1,4 +0,0 @@
from .remote import main
# 启动心跳线程
heartbeat_thread = main()

View File

@@ -1,248 +1,142 @@
import asyncio
import requests import requests
import time
import uuid
import platform import platform
import os
import json
import threading
import subprocess
# from loguru import logger # from loguru import logger
from src.common.logger_manager import get_logger from src.common.logger_manager import get_logger
from src.config.config import global_config from src.config.config import global_config
from src.manager.async_task_manager import AsyncTask
from src.manager.local_store_manager import local_storage
logger = get_logger("remote") logger = get_logger("remote")
# --- 使用向上导航的方式定义路径 --- TELEMETRY_SERVER_URL = "http://localhost:8080"
"""遥测服务地址"""
# 1. 获取当前文件 (remote.py) 所在的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 2. 从当前目录向上导航三级找到项目根目录
# (src/plugins/remote/ -> src/plugins/ -> src/ -> project_root)
root_dir = os.path.abspath(os.path.join(current_dir, "..", "..", ".."))
# 3. 定义 data 目录的路径 (位于项目根目录下)
data_dir = os.path.join(root_dir, "data")
# 4. 定义 UUID 文件在 data 目录下的完整路径
UUID_FILE = os.path.join(data_dir, "client_uuid.json")
# --- 路径定义结束 ---
# 生成或获取客户端唯一ID class TelemetryHeartBeatTask(AsyncTask):
def get_unique_id(): HEARTBEAT_INTERVAL = 300
# --- 在尝试读写 UUID_FILE 之前确保 data 目录存在 ---
# 将目录检查和创建逻辑移到这里,在首次需要写入前执行
try:
# exist_ok=True 意味着如果目录已存在也不会报错
os.makedirs(data_dir, exist_ok=True)
except OSError as e:
# 处理可能的权限错误等
logger.error(f"无法创建数据目录 {data_dir}: {e}")
# 根据你的错误处理逻辑,可能需要在这里返回错误或抛出异常
# 暂且返回 None 或抛出,避免继续执行导致问题
raise RuntimeError(f"无法创建必要的数据目录 {data_dir}") from e
# --- 目录检查结束 ---
# 检查是否已经有保存的UUID def __init__(self):
if os.path.exists(UUID_FILE): super().__init__(task_name="Telemetry Heart Beat Task", run_interval=self.HEARTBEAT_INTERVAL)
try: self.server_url = TELEMETRY_SERVER_URL
with open(UUID_FILE, "r", encoding="utf-8") as f: # 指定 encoding """遥测服务地址"""
data = json.load(f)
if "client_id" in data:
logger.debug(f"从本地文件读取客户端ID: {UUID_FILE}")
return data["client_id"]
except (json.JSONDecodeError, IOError) as e:
logger.warning(f"读取UUID文件 {UUID_FILE} 出错: {e}将生成新的UUID")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"读取UUID文件 {UUID_FILE} 时发生未知错误: {e}")
# 如果没有保存的UUID或读取出错则生成新的 self.client_uuid = local_storage["mmc_uuid"] if "mmc_uuid" in local_storage else None
client_id = generate_unique_id() """客户端UUID"""
logger.info(f"生成新的客户端ID: {client_id}")
# 保存UUID到文件 self.info_dict = self._get_sys_info()
try: """系统信息字典"""
# 再次确认目录存在 (虽然理论上前面已创建,但更保险)
os.makedirs(data_dir, exist_ok=True)
with open(UUID_FILE, "w", encoding="utf-8") as f: # 指定 encoding
json.dump({"client_id": client_id}, f, indent=4) # 添加 indent 使json可读
logger.info(f"已保存新生成的客户端ID到本地文件: {UUID_FILE}")
except IOError as e:
logger.error(f"保存UUID时出错: {UUID_FILE} - {e}")
except Exception as e: # 捕捉其他可能的异常
logger.error(f"保存UUID文件 {UUID_FILE} 时发生未知错误: {e}")
return client_id @staticmethod
def _get_sys_info() -> dict[str, str]:
"""获取系统信息"""
info_dict = {
"os_type": "Unknown",
"py_version": platform.python_version(),
"mmc_version": global_config.MAI_VERSION,
}
match platform.system():
case "Windows":
info_dict["os_type"] = "Windows"
case "Linux":
info_dict["os_type"] = "Linux"
case "Darwin":
info_dict["os_type"] = "macOS"
case _:
info_dict["os_type"] = "Unknown"
# 生成客户端唯一ID return info_dict
def generate_unique_id():
# 基于机器码生成唯一ID同一台机器上生成的UUID是固定的只要机器码不变
import hashlib
system_info = platform.system() async def _req_uuid(self) -> bool:
machine_code = None """
向服务端请求UUID不应在已存在UUID的情况下调用会覆盖原有的UUID
"""
try: if "deploy_time" not in local_storage:
if system_info == "Windows": logger.error("本地存储中缺少部署时间无法请求UUID")
# 使用wmic命令获取主机UUID更稳定
result = subprocess.check_output(
"wmic csproduct get uuid", shell=True, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL
)
lines = result.decode(errors="ignore").splitlines()
# 过滤掉空行和表头只取有效UUID
uuids = [line.strip() for line in lines if line.strip() and line.strip().lower() != "uuid"]
if uuids:
uuid_val = uuids[0]
# logger.debug(f"主机UUID: {uuid_val}")
# 增加无效值判断
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
machine_code = uuid_val
elif system_info == "Linux":
# 优先读取 /etc/machine-id其次 /var/lib/dbus/machine-id取第一个非空且内容有效的
for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
if os.path.exists(path):
with open(path, "r") as f:
code = f.read().strip()
# 只要内容非空且不是全0
if code and set(code) != {"0"}:
machine_code = code
break
elif system_info == "Darwin":
# macOS: 使用IOPlatformUUID
result = subprocess.check_output(
"ioreg -rd1 -c IOPlatformExpertDevice | awk '/IOPlatformUUID/'", shell=True
)
uuid_line = result.decode(errors="ignore")
# 解析出 "IOPlatformUUID" = "xxxx-xxxx-xxxx-xxxx"
import re
m = re.search(r'"IOPlatformUUID"\s*=\s*"([^"]+)"', uuid_line)
if m:
uuid_val = m.group(1)
logger.debug(f"IOPlatformUUID: {uuid_val}")
if uuid_val and uuid_val.lower() not in ["to be filled by o.e.m.", "none", "", "standard"]:
machine_code = uuid_val
except Exception as e:
logger.debug(f"获取机器码失败: {e}")
# 如果主板序列号无效尝试用MAC地址
if not machine_code:
try:
mac = uuid.getnode()
if (mac >> 40) % 2 == 0: # 不是本地伪造MAC
machine_code = str(mac)
except Exception as e:
logger.debug(f"获取MAC地址失败: {e}")
def md5_to_uuid(md5hex):
# 将32位md5字符串格式化为8-4-4-4-12的UUID格式
return f"{md5hex[0:8]}-{md5hex[8:12]}-{md5hex[12:16]}-{md5hex[16:20]}-{md5hex[20:32]}"
if machine_code:
# print(f"machine_code={machine_code!r}") # 可用于调试
md5 = hashlib.md5(machine_code.encode("utf-8")).hexdigest()
uuid_str = md5_to_uuid(md5)
else:
uuid_str = str(uuid.uuid4())
unique_id = f"{system_info}-{uuid_str}"
return unique_id
def send_heartbeat(server_url, client_id):
"""向服务器发送心跳"""
sys = platform.system()
try:
headers = {"Client-ID": client_id, "User-Agent": f"HeartbeatClient/{client_id[:8]}"}
data = json.dumps(
{"system": sys, "Version": global_config.MAI_VERSION},
)
logger.debug(f"正在发送心跳到服务器: {server_url}")
logger.debug(f"心跳数据: {data}")
response = requests.post(f"{server_url}/api/clients", headers=headers, data=data)
if response.status_code == 201:
data = response.json()
logger.debug(f"心跳发送成功。服务器响应: {data}")
return True
else:
logger.debug(f"心跳发送失败。状态码: {response.status_code}, 响应内容: {response.text}")
return False return False
try_count: int = 0
while True:
# 如果不存在则向服务端请求一个新的UUID注册客户端
logger.info("正在向遥测服务端请求UUID...")
try:
response = requests.post(
f"{TELEMETRY_SERVER_URL}/stat/reg_client",
json={"deploy_time": local_storage["deploy_time"]},
)
if response.status_code == 200:
data = response.json()
client_id = data.get("mmc_uuid")
if client_id:
# 将UUID存储到本地
local_storage["mmc_uuid"] = client_id
self.client_uuid = client_id
logger.info(f"成功获取UUID: {self.client_uuid}")
return True # 成功获取UUID返回True
else:
logger.error("无效的服务端响应")
else:
logger.error(f"请求UUID失败状态码: {response.status_code}, 响应内容: {response.text}")
except requests.RequestException as e:
logger.error(f"请求UUID时出错: {e}") # 可能是网络问题
# 请求失败,重试次数+1
try_count += 1
if try_count > 3:
# 如果超过3次仍然失败则退出
logger.error("获取UUID失败请检查网络连接或服务端状态")
return False
else:
# 如果可以重试,等待后继续(指数退避)
await asyncio.sleep(4**try_count)
async def _send_heartbeat(self):
"""向服务器发送心跳"""
try:
headers = {
"Client-UUID": self.client_uuid,
"User-Agent": f"HeartbeatClient/{self.client_uuid[:8]}",
}
logger.debug(f"正在发送心跳到服务器: {self.server_url}")
response = requests.post(
f"{self.server_url}/stat/client_heartbeat",
headers=headers,
json=self.info_dict,
)
# 处理响应
if 200 <= response.status_code < 300:
# 成功
logger.debug(f"心跳发送成功,状态码: {response.status_code}")
elif response.status_code == 403:
# 403 Forbidden
logger.error(
"心跳发送失败403 Forbidden: 可能是UUID无效或未注册。"
"处理措施重置UUID下次发送心跳时将尝试重新注册。"
)
self.client_uuid = None
del local_storage["mmc_uuid"] # 删除本地存储的UUID
else:
# 其他错误
logger.error(f"心跳发送失败,状态码: {response.status_code}, 响应内容: {response.text}")
except requests.RequestException as e: except requests.RequestException as e:
# 如果请求异常,可能是网络问题,不记录错误 logger.error(f"心跳发送失败: {e}")
logger.debug(f"发送心跳时出错: {e}")
return False
async def run(self):
class HeartbeatThread(threading.Thread):
"""心跳线程类"""
def __init__(self, server_url, interval):
super().__init__(daemon=True) # 设置为守护线程,主程序结束时自动结束
self.server_url = server_url
self.interval = interval
self.client_id = get_unique_id()
self.running = True
self.stop_event = threading.Event() # 添加事件对象用于可中断的等待
self.last_heartbeat_time = 0 # 记录上次发送心跳的时间
def run(self):
"""线程运行函数"""
logger.debug(f"心跳线程已启动客户端ID: {self.client_id}")
while self.running:
# 发送心跳 # 发送心跳
if send_heartbeat(self.server_url, self.client_id):
logger.info(f"{self.interval}秒后发送下一次心跳...")
else:
logger.info(f"{self.interval}秒后重试...")
self.last_heartbeat_time = time.time()
# 使用可中断的等待代替 sleep
# 每秒检查一次是否应该停止或发送心跳
remaining_wait = self.interval
while remaining_wait > 0 and self.running:
# 每次最多等待1秒便于及时响应停止请求
wait_time = min(1, remaining_wait)
if self.stop_event.wait(wait_time):
break # 如果事件被设置,立即退出等待
remaining_wait -= wait_time
# 检查是否由于外部原因导致间隔异常延长
if time.time() - self.last_heartbeat_time >= self.interval * 1.5:
logger.warning("检测到心跳间隔异常延长,立即发送心跳")
break
def stop(self):
"""停止线程"""
self.running = False
self.stop_event.set() # 设置事件,中断等待
logger.debug("心跳线程已收到停止信号")
def main():
if global_config.remote_enable: if global_config.remote_enable:
"""主函数,启动心跳线程""" if self.client_uuid is None:
# 配置 if not await self._req_uuid():
server_url = "http://hyybuth.xyz:10058" logger.error("获取UUID失败跳过此次心跳")
# server_url = "http://localhost:10058" return
heartbeat_interval = 300 # 5分钟
# 创建并启动心跳线程 await self._send_heartbeat()
heartbeat_thread = HeartbeatThread(server_url, heartbeat_interval)
heartbeat_thread.start()
return heartbeat_thread # 返回线程对象,便于外部控制
return None
# --- 测试用例 ---
if __name__ == "__main__":
print("测试唯一ID生成")
print("唯一ID:", get_unique_id())

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
from src.tools.tool_can_use.base_tool import BaseTool
from src.config.config import global_config
from src.common.logger_manager import get_logger
from src.plugins.moods.moods import MoodManager
from typing import Any from typing import Any
from src.common.logger_manager import get_logger
from src.config.config import global_config
from src.tools.tool_can_use.base_tool import BaseTool
from src.manager.mood_manager import mood_manager
logger = get_logger("change_mood_tool") logger = get_logger("change_mood_tool")
@@ -36,7 +36,6 @@ class ChangeMoodTool(BaseTool):
response_set = function_args.get("response_set") response_set = function_args.get("response_set")
_message_processed_plain_text = function_args.get("text") _message_processed_plain_text = function_args.get("text")
mood_manager = MoodManager.get_instance()
# gpt = ResponseGenerator() # gpt = ResponseGenerator()
if response_set is None: if response_set is None: