diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
new file mode 100644
index 000000000..0d1e50c5a
--- /dev/null
+++ b/.github/workflows/ruff.yml
@@ -0,0 +1,8 @@
+name: Ruff
+on: [ push, pull_request ]
+jobs:
+ ruff:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: astral-sh/ruff-action@v3
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 3579444dc..b4c7154de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -190,7 +190,6 @@ cython_debug/
# PyPI configuration file
.pypirc
-.env
# jieba
jieba.cache
@@ -199,4 +198,9 @@ jieba.cache
!.vscode/settings.json
# direnv
-/.direnv
\ No newline at end of file
+/.direnv
+
+# JetBrains
+.idea
+*.iml
+*.ipr
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..8a04e2d84
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,10 @@
+repos:
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ # Ruff version.
+ rev: v0.9.10
+ hooks:
+ # Run the linter.
+ - id: ruff
+ args: [ --fix ]
+ # Run the formatter.
+ - id: ruff-format
diff --git a/README.md b/README.md
index c14ac646e..a7394c7cf 100644
--- a/README.md
+++ b/README.md
@@ -61,6 +61,7 @@
- 📦 **Windows 一键傻瓜式部署**:请运行项目根目录中的 `run.bat`,部署完成后请参照后续配置指南进行配置
+- 📦 Linux 自动部署(实验) :请下载并运行项目根目录中的`run.sh`并按照提示安装,部署完成后请参照后续配置指南进行配置
- [📦 Windows 手动部署指南 ](docs/manual_deploy_windows.md)
diff --git a/bot.py b/bot.py
index 48517fe24..a3a844a15 100644
--- a/bot.py
+++ b/bot.py
@@ -17,19 +17,6 @@ env_mask = {key: os.getenv(key) for key in os.environ}
uvicorn_server = None
-# 配置日志
-log_path = os.path.join(os.getcwd(), "logs")
-if not os.path.exists(log_path):
- os.makedirs(log_path)
-
-# 添加文件日志,启用rotation和retention
-logger.add(
- os.path.join(log_path, "maimbot_{time:YYYY-MM-DD}.log"),
- rotation="00:00", # 每天0点创建新文件
- retention="30 days", # 保留30天的日志
- level="INFO",
- encoding="utf-8"
-)
def easter_egg():
# 彩蛋
@@ -76,7 +63,7 @@ def init_env():
# 首先加载基础环境变量.env
if os.path.exists(".env"):
- load_dotenv(".env",override=True)
+ load_dotenv(".env", override=True)
logger.success("成功加载基础环境变量配置")
@@ -90,10 +77,7 @@ def load_env():
logger.success("加载开发环境变量配置")
load_dotenv(".env.dev", override=True) # override=True 允许覆盖已存在的环境变量
- fn_map = {
- "prod": prod,
- "dev": dev
- }
+ fn_map = {"prod": prod, "dev": dev}
env = os.getenv("ENVIRONMENT")
logger.info(f"[load_env] 当前的 ENVIRONMENT 变量值:{env}")
@@ -109,28 +93,45 @@ def load_env():
logger.error(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在")
RuntimeError(f"ENVIRONMENT 配置错误,请检查 .env 文件中的 ENVIRONMENT 变量及对应 .env.{env} 是否存在")
-def load_logger():
- logger.remove() # 移除默认配置
- if os.getenv("ENVIRONMENT") == "dev":
- logger.add(
- sys.stderr,
- format="{time:YYYY-MM-DD HH:mm:ss.SSS} |> {level: <7} |> {name:.<8}:{function:.<8}:{line: >4} -> {message}",
- colorize=True,
- level=os.getenv("LOG_LEVEL", "DEBUG"), # 根据环境设置日志级别,默认为DEBUG
- )
- else:
- logger.add(
- sys.stderr,
- format="{time:YYYY-MM-DD HH:mm:ss.SSS} |> {level: <7} |> {name:.<8}:{function:.<8}:{line: >4} -> {message}",
- colorize=True,
- level=os.getenv("LOG_LEVEL", "INFO"), # 根据环境设置日志级别,默认为INFO
- filter=lambda record: "nonebot" not in record["name"]
- )
+def load_logger():
+ logger.remove()
+
+ # 配置日志基础路径
+ log_path = os.path.join(os.getcwd(), "logs")
+ if not os.path.exists(log_path):
+ os.makedirs(log_path)
+
+ current_env = os.getenv("ENVIRONMENT", "dev")
+
+ # 公共配置参数
+ log_level = os.getenv("LOG_LEVEL", "INFO" if current_env == "prod" else "DEBUG")
+ log_filter = lambda record: (
+ ("nonebot" not in record["name"] or record["level"].no >= logger.level("ERROR").no)
+ if current_env == "prod"
+ else True
+ )
+ log_format = (
+ "{time:YYYY-MM-DD HH:mm:ss.SSS} "
+ "|> {level: <7} "
+ "|> {name:.<8}:{function:.<8}:{line: >4} "
+ "-> {message}"
+ )
+
+ # 日志文件储存至/logs
+ logger.add(
+ os.path.join(log_path, "maimbot_{time:YYYY-MM-DD}.log"),
+ rotation="00:00",
+ retention="30 days",
+ format=log_format,
+ colorize=False,
+ level=log_level,
+ filter=log_filter,
+ encoding="utf-8",
+ )
+
+ # 终端输出
+ logger.add(sys.stderr, format=log_format, colorize=True, level=log_level, filter=log_filter)
def scan_provider(env_config: dict):
@@ -160,10 +161,7 @@ def scan_provider(env_config: dict):
# 检查每个 provider 是否同时存在 url 和 key
for provider_name, config in provider.items():
if config["url"] is None or config["key"] is None:
- logger.error(
- f"provider 内容:{config}\n"
- f"env_config 内容:{env_config}"
- )
+ logger.error(f"provider 内容:{config}\nenv_config 内容:{env_config}")
raise ValueError(f"请检查 '{provider_name}' 提供商配置是否丢失 BASE_URL 或 KEY 环境变量")
@@ -192,7 +190,7 @@ async def uvicorn_main():
reload=os.getenv("ENVIRONMENT") == "dev",
timeout_graceful_shutdown=5,
log_config=None,
- access_log=False
+ access_log=False,
)
server = uvicorn.Server(config)
uvicorn_server = server
@@ -202,7 +200,7 @@ async def uvicorn_main():
def raw_main():
# 利用 TZ 环境变量设定程序工作的时区
# 仅保证行为一致,不依赖 localtime(),实际对生产环境几乎没有作用
- if platform.system().lower() != 'windows':
+ if platform.system().lower() != "windows":
time.tzset()
easter_egg()
diff --git a/docs/avatars/SengokuCola.jpg b/docs/avatars/SengokuCola.jpg
new file mode 100644
index 000000000..deebf5ed5
Binary files /dev/null and b/docs/avatars/SengokuCola.jpg differ
diff --git a/docs/avatars/default.png b/docs/avatars/default.png
new file mode 100644
index 000000000..5b561dac4
Binary files /dev/null and b/docs/avatars/default.png differ
diff --git a/docs/avatars/run.bat b/docs/avatars/run.bat
new file mode 100644
index 000000000..6b9ca9f2b
--- /dev/null
+++ b/docs/avatars/run.bat
@@ -0,0 +1 @@
+gource gource.log --user-image-dir docs/avatars/ --default-user-image docs/avatars/default.png
\ No newline at end of file
diff --git a/docs/manual_deploy_linux.md b/docs/manual_deploy_linux.md
index b19f3d6a7..a5c91d6e2 100644
--- a/docs/manual_deploy_linux.md
+++ b/docs/manual_deploy_linux.md
@@ -121,6 +121,7 @@ sudo nano /etc/systemd/system/maimbot.service
输入以下内容:
``:你的maimbot目录
+
``:你的venv环境(就是上文创建环境后,执行的代码`source maimbot/bin/activate`中source后面的路径的绝对路径)
```ini
diff --git a/docs/synology_.env.prod.png b/docs/synology_.env.prod.png
new file mode 100644
index 000000000..0bdcacdf3
Binary files /dev/null and b/docs/synology_.env.prod.png differ
diff --git a/docs/synology_create_project.png b/docs/synology_create_project.png
new file mode 100644
index 000000000..f716d4605
Binary files /dev/null and b/docs/synology_create_project.png differ
diff --git a/docs/synology_deploy.md b/docs/synology_deploy.md
new file mode 100644
index 000000000..23e24e704
--- /dev/null
+++ b/docs/synology_deploy.md
@@ -0,0 +1,67 @@
+# 群晖 NAS 部署指南
+
+**笔者使用的是 DSM 7.2.2,其他 DSM 版本的操作可能不完全一样**
+**需要使用 Container Manager,群晖的部分部分入门级 NAS 可能不支持**
+
+## 部署步骤
+
+### 创建配置文件目录
+
+打开 `DSM ➡️ 控制面板 ➡️ 共享文件夹`,点击 `新增` ,创建一个共享文件夹
+只需要设置名称,其他设置均保持默认即可。如果你已经有 docker 专用的共享文件夹了,就跳过这一步
+
+打开 `DSM ➡️ FileStation`, 在共享文件夹中创建一个 `MaiMBot` 文件夹
+
+### 准备配置文件
+
+docker-compose.yml: https://github.com/SengokuCola/MaiMBot/blob/main/docker-compose.yml
+下载后打开,将 `services-mongodb-image` 修改为 `mongo:4.4.24`。这是因为最新的 MongoDB 强制要求 AVX 指令集,而群晖似乎不支持这个指令集
+
+
+bot_config.toml: https://github.com/SengokuCola/MaiMBot/blob/main/template/bot_config_template.toml
+下载后,重命名为 `bot_config.toml`
+打开它,按自己的需求填写配置文件
+
+.env.prod: https://github.com/SengokuCola/MaiMBot/blob/main/template.env
+下载后,重命名为 `.env.prod`
+按下图修改 mongodb 设置,使用 `MONGODB_URI`
+
+
+把 `bot_config.toml` 和 `.env.prod` 放入之前创建的 `MaiMBot`文件夹
+
+#### 如何下载?
+
+点这里!
+
+### 创建项目
+
+打开 `DSM ➡️ ContainerManager ➡️ 项目`,点击 `新增` 创建项目,填写以下内容:
+
+- 项目名称: `maimbot`
+- 路径:之前创建的 `MaiMBot` 文件夹
+- 来源: `上传 docker-compose.yml`
+- 文件:之前下载的 `docker-compose.yml` 文件
+
+图例:
+
+
+
+一路点下一步,等待项目创建完成
+
+### 设置 Napcat
+
+1. 登陆 napcat
+ 打开 napcat: `http://<你的nas地址>:6099` ,输入token登陆
+ token可以打开 `DSM ➡️ ContainerManager ➡️ 项目 ➡️ MaiMBot ➡️ 容器 ➡️ Napcat ➡️ 日志`,找到类似 `[WebUi] WebUi Local Panel Url: http://127.0.0.1:6099/webui?token=xxxx` 的日志
+ 这个 `token=` 后面的就是你的 napcat token
+
+2. 按提示,登陆你给麦麦准备的QQ小号
+
+3. 设置 websocket 客户端
+ `网络配置 -> 新建 -> Websocket客户端`,名称自定,URL栏填入 `ws://maimbot:8080/onebot/v11/ws`,启用并保存即可。
+ 若修改过容器名称,则替换 `maimbot` 为你自定的名称
+
+### 部署完成
+
+找个群,发送 `麦麦,你在吗` 之类的
+如果一切正常,应该能正常回复了
\ No newline at end of file
diff --git a/docs/synology_docker-compose.png b/docs/synology_docker-compose.png
new file mode 100644
index 000000000..f70003e29
Binary files /dev/null and b/docs/synology_docker-compose.png differ
diff --git a/docs/synology_how_to_download.png b/docs/synology_how_to_download.png
new file mode 100644
index 000000000..011f98876
Binary files /dev/null and b/docs/synology_how_to_download.png differ
diff --git a/run.sh b/run.sh
new file mode 100644
index 000000000..c3f6969b6
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,278 @@
+#!/bin/bash
+
+# Maimbot 一键安装脚本 by Cookie987
+# 适用于Debian系
+# 请小心使用任何一键脚本!
+
+# 如无法访问GitHub请修改此处镜像地址
+
+LANG=C.UTF-8
+
+GITHUB_REPO="https://ghfast.top/https://github.com/SengokuCola/MaiMBot.git"
+
+# 颜色输出
+GREEN="\e[32m"
+RED="\e[31m"
+RESET="\e[0m"
+
+# 需要的基本软件包
+REQUIRED_PACKAGES=("git" "sudo" "python3" "python3-venv" "curl" "gnupg" "python3-pip")
+
+# 默认项目目录
+DEFAULT_INSTALL_DIR="/opt/maimbot"
+
+# 服务名称
+SERVICE_NAME="maimbot"
+
+IS_INSTALL_MONGODB=false
+IS_INSTALL_NAPCAT=false
+
+# 1/6: 检测是否安装 whiptail
+if ! command -v whiptail &>/dev/null; then
+ echo -e "${RED}[1/6] whiptail 未安装,正在安装...${RESET}"
+ apt update && apt install -y whiptail
+fi
+
+get_os_info() {
+ if command -v lsb_release &>/dev/null; then
+ OS_INFO=$(lsb_release -d | cut -f2)
+ elif [[ -f /etc/os-release ]]; then
+ OS_INFO=$(grep "^PRETTY_NAME=" /etc/os-release | cut -d '"' -f2)
+ else
+ OS_INFO="Unknown OS"
+ fi
+ echo "$OS_INFO"
+}
+
+# 检查系统
+check_system() {
+ # 检查是否为 root 用户
+ if [[ "$(id -u)" -ne 0 ]]; then
+ whiptail --title "🚫 权限不足" --msgbox "请使用 root 用户运行此脚本!\n执行方式: sudo bash $0" 10 60
+ exit 1
+ fi
+
+ if [[ -f /etc/os-release ]]; then
+ source /etc/os-release
+ if [[ "$ID" != "debian" || "$VERSION_ID" != "12" ]]; then
+ whiptail --title "🚫 不支持的系统" --msgbox "此脚本仅支持 Debian 12 (Bookworm)!\n当前系统: $PRETTY_NAME\n安装已终止。" 10 60
+ exit 1
+ fi
+ else
+ whiptail --title "⚠️ 无法检测系统" --msgbox "无法识别系统版本,安装已终止。" 10 60
+ exit 1
+ fi
+}
+
+# 3/6: 询问用户是否安装缺失的软件包
+install_packages() {
+ missing_packages=()
+ for package in "${REQUIRED_PACKAGES[@]}"; do
+ if ! dpkg -s "$package" &>/dev/null; then
+ missing_packages+=("$package")
+ fi
+ done
+
+ if [[ ${#missing_packages[@]} -gt 0 ]]; then
+ whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到以下必须的依赖项目缺失:\n${missing_packages[*]}\n\n是否要自动安装?" 12 60
+ if [[ $? -eq 0 ]]; then
+ return 0
+ else
+ whiptail --title "⚠️ 注意" --yesno "某些必要的依赖项未安装,可能会影响运行!\n是否继续?" 10 60 || exit 1
+ fi
+ fi
+}
+
+# 4/6: Python 版本检查
+check_python() {
+ PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
+
+ python3 -c "import sys; exit(0) if sys.version_info >= (3,9) else exit(1)"
+ if [[ $? -ne 0 ]]; then
+ whiptail --title "⚠️ [4/6] Python 版本过低" --msgbox "检测到 Python 版本为 $PYTHON_VERSION,需要 3.9 或以上!\n请升级 Python 后重新运行本脚本。" 10 60
+ exit 1
+ fi
+}
+
+# 5/6: 选择分支
+choose_branch() {
+ BRANCH=$(whiptail --title "🔀 [5/6] 选择 Maimbot 分支" --menu "请选择要安装的 Maimbot 分支:" 15 60 2 \
+ "main" "稳定版本(推荐)" \
+ "debug" "开发版本(可能不稳定)" 3>&1 1>&2 2>&3)
+
+ if [[ -z "$BRANCH" ]]; then
+ BRANCH="main"
+ whiptail --title "🔀 默认选择" --msgbox "未选择分支,默认安装稳定版本(main)" 10 60
+ fi
+}
+
+# 6/6: 选择安装路径
+choose_install_dir() {
+ INSTALL_DIR=$(whiptail --title "📂 [6/6] 选择安装路径" --inputbox "请输入 Maimbot 的安装目录:" 10 60 "$DEFAULT_INSTALL_DIR" 3>&1 1>&2 2>&3)
+
+ if [[ -z "$INSTALL_DIR" ]]; then
+ whiptail --title "⚠️ 取消输入" --yesno "未输入安装路径,是否退出安装?" 10 60
+ if [[ $? -ne 0 ]]; then
+ INSTALL_DIR="$DEFAULT_INSTALL_DIR"
+ else
+ exit 1
+ fi
+ fi
+}
+
+# 显示确认界面
+confirm_install() {
+ local confirm_message="请确认以下更改:\n\n"
+
+ if [[ ${#missing_packages[@]} -gt 0 ]]; then
+ confirm_message+="📦 安装缺失的依赖项: ${missing_packages[*]}\n"
+ else
+ confirm_message+="✅ 所有依赖项已安装\n"
+ fi
+
+ confirm_message+="📂 安装麦麦Bot到: $INSTALL_DIR\n"
+ confirm_message+="🔀 分支: $BRANCH\n"
+
+ if [[ "$MONGODB_INSTALLED" == "true" ]]; then
+ confirm_message+="✅ MongoDB 已安装\n"
+ else
+ if [[ "$IS_INSTALL_MONGODB" == "true" ]]; then
+ confirm_message+="📦 安装 MongoDB\n"
+ fi
+ fi
+
+ if [[ "$NAPCAT_INSTALLED" == "true" ]]; then
+ confirm_message+="✅ NapCat 已安装\n"
+ else
+ if [[ "$IS_INSTALL_NAPCAT" == "true" ]]; then
+ confirm_message+="📦 安装 NapCat\n"
+ fi
+ fi
+
+ confirm_message+="🛠️ 添加麦麦Bot作为系统服务 ($SERVICE_NAME.service)\n"
+
+ confitm_message+="\n\n注意:本脚本默认使用ghfast.top为GitHub进行加速,如不想使用请手动修改脚本开头的GITHUB_REPO变量。"
+ whiptail --title "🔧 安装确认" --yesno "$confirm_message\n\n是否继续安装?" 15 60
+ if [[ $? -ne 0 ]]; then
+ whiptail --title "🚫 取消安装" --msgbox "安装已取消。" 10 60
+ exit 1
+ fi
+}
+
+check_mongodb() {
+ if command -v mongod &>/dev/null; then
+ MONGO_INSTALLED=true
+ else
+ MONGO_INSTALLED=false
+ fi
+}
+
+# 安装 MongoDB
+install_mongodb() {
+ if [[ "$MONGO_INSTALLED" == "true" ]]; then
+ return 0
+ fi
+
+ whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到未安装MongoDB,是否安装?\n如果您想使用远程数据库,请跳过此步。" 10 60
+ if [[ $? -ne 0 ]]; then
+ return 1
+ fi
+ IS_INSTALL_MONGODB=true
+}
+
+check_napcat() {
+ if command -v napcat &>/dev/null; then
+ NAPCAT_INSTALLED=true
+ else
+ NAPCAT_INSTALLED=false
+ fi
+}
+
+install_napcat() {
+ if [[ "$NAPCAT_INSTALLED" == "true" ]]; then
+ return 0
+ fi
+
+ whiptail --title "📦 [3/6] 软件包检查" --yesno "检测到未安装NapCat,是否安装?\n如果您想使用远程NapCat,请跳过此步。" 10 60
+ if [[ $? -ne 0 ]]; then
+ return 1
+ fi
+ IS_INSTALL_NAPCAT=true
+}
+
+# 运行安装步骤
+check_system
+check_mongodb
+check_napcat
+install_packages
+install_mongodb
+install_napcat
+check_python
+choose_branch
+choose_install_dir
+confirm_install
+
+# 开始安装
+whiptail --title "🚀 开始安装" --msgbox "所有环境检查完毕,即将开始安装麦麦Bot!" 10 60
+
+echo -e "${GREEN}安装依赖项...${RESET}"
+
+apt update && apt install -y "${missing_packages[@]}"
+
+
+if [[ "$IS_INSTALL_MONGODB" == "true" ]]; then
+ echo -e "${GREEN}安装 MongoDB...${RESET}"
+ curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor
+ echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] http://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
+ apt-get update
+ apt-get install -y mongodb-org
+
+ systemctl enable mongod
+ systemctl start mongod
+fi
+
+if [[ "$IS_INSTALL_NAPCAT" == "true" ]]; then
+ echo -e "${GREEN}安装 NapCat...${RESET}"
+ curl -o napcat.sh https://nclatest.znin.net/NapNeko/NapCat-Installer/main/script/install.sh && bash napcat.sh
+fi
+
+echo -e "${GREEN}创建 Python 虚拟环境...${RESET}"
+mkdir -p "$INSTALL_DIR"
+cd "$INSTALL_DIR" || exit
+python3 -m venv venv
+source venv/bin/activate
+
+echo -e "${GREEN}克隆仓库...${RESET}"
+# 安装 Maimbot
+mkdir -p "$INSTALL_DIR/repo"
+cd "$INSTALL_DIR/repo" || exit 1
+git clone -b "$BRANCH" $GITHUB_REPO .
+
+echo -e "${GREEN}安装 Python 依赖...${RESET}"
+pip install -r requirements.txt
+
+echo -e "${GREEN}设置服务...${RESET}"
+
+# 设置 Maimbot 服务
+cat < None:
+ """处理收到的通知"""
+ # 戳一戳通知
+ if isinstance(event, PokeNotifyEvent):
+ # 用户屏蔽,不区分私聊/群聊
+ if event.user_id in global_config.ban_user_id:
+ return
+ reply_poke_probability = 1 # 回复戳一戳的概率
+
+ if random() < reply_poke_probability:
+ user_info = UserInfo(
+ user_id=event.user_id,
+ user_nickname=get_user_nickname(event.user_id) or None,
+ user_cardname=get_user_cardname(event.user_id) or None,
+ platform="qq",
+ )
+ group_info = GroupInfo(group_id=event.group_id, group_name=None, platform="qq")
+ message_cq = MessageRecvCQ(
+ message_id=None,
+ user_info=user_info,
+ raw_message=str("[戳了戳]你"),
+ group_info=group_info,
+ reply_message=None,
+ platform="qq",
+ )
+ message_json = message_cq.to_dict()
+
+ # 进入maimbot
+ message = MessageRecv(message_json)
+ groupinfo = message.message_info.group_info
+ userinfo = message.message_info.user_info
+ messageinfo = message.message_info
+
+ chat = await chat_manager.get_or_create_stream(
+ platform=messageinfo.platform, user_info=userinfo, group_info=groupinfo
+ )
+ message.update_chat_stream(chat)
+ await message.process()
+
+ bot_user_info = UserInfo(
+ user_id=global_config.BOT_QQ,
+ user_nickname=global_config.BOT_NICKNAME,
+ platform=messageinfo.platform,
+ )
+
+ response, raw_content = await self.gpt.generate_response(message)
+
+ if response:
+ for msg in response:
+ message_segment = Seg(type="text", data=msg)
+
+ bot_message = MessageSending(
+ message_id=None,
+ chat_stream=chat,
+ bot_user_info=bot_user_info,
+ sender_info=userinfo,
+ message_segment=message_segment,
+ reply=None,
+ is_head=False,
+ is_emoji=False,
+ )
+ message_manager.add_message(bot_message)
+
async def handle_message(self, event: MessageEvent, bot: Bot) -> None:
"""处理收到的消息"""
@@ -54,7 +120,10 @@ class ChatBot:
# 用户屏蔽,不区分私聊/群聊
if event.user_id in global_config.ban_user_id:
return
-
+
+ if event.reply and hasattr(event.reply, 'sender') and hasattr(event.reply.sender, 'user_id') and event.reply.sender.user_id in global_config.ban_user_id:
+ logger.debug(f"跳过处理回复来自被ban用户 {event.reply.sender.user_id} 的消息")
+ return
# 处理私聊消息
if isinstance(event, PrivateMessageEvent):
if not global_config.enable_friend_chat: # 私聊过滤
@@ -126,7 +195,7 @@ class ChatBot:
for word in global_config.ban_words:
if word in message.processed_plain_text:
logger.info(
- f"[{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{userinfo.user_nickname}:{message.processed_plain_text}"
+ f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{message.processed_plain_text}"
)
logger.info(f"[过滤词识别]消息中含有{word},filtered")
return
@@ -135,7 +204,7 @@ class ChatBot:
for pattern in global_config.ban_msgs_regex:
if re.search(pattern, message.raw_message):
logger.info(
- f"[{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{userinfo.user_nickname}:{message.raw_message}"
+ f"[{chat.group_info.group_name if chat.group_info else '私聊'}]{userinfo.user_nickname}:{message.raw_message}"
)
logger.info(f"[正则表达式过滤]消息匹配到{pattern},filtered")
return
@@ -143,7 +212,7 @@ class ChatBot:
current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(messageinfo.time))
# topic=await topic_identifier.identify_topic_llm(message.processed_plain_text)
-
+
topic = ""
interested_rate = await hippocampus.memory_activate_value(message.processed_plain_text) / 100
logger.debug(f"对{message.processed_plain_text}的激活度:{interested_rate}")
@@ -159,11 +228,12 @@ class ChatBot:
config=global_config,
is_emoji=message.is_emoji,
interested_rate=interested_rate,
+ sender_id=str(message.message_info.user_info.user_id),
)
current_willing = willing_manager.get_willing(chat_stream=chat)
logger.info(
- f"[{current_time}][{chat.group_info.group_name if chat.group_info.group_id else '私聊'}]{chat.user_info.user_nickname}:"
+ f"[{current_time}][{chat.group_info.group_name if chat.group_info else '私聊'}]{chat.user_info.user_nickname}:"
f"{message.processed_plain_text}[回复意愿:{current_willing:.2f}][概率:{reply_probability * 100:.1f}%]"
)
@@ -189,6 +259,9 @@ class ChatBot:
willing_manager.change_reply_willing_sent(chat)
response, raw_content = await self.gpt.generate_response(message)
+ else:
+ # 决定不回复时,也更新回复意愿
+ willing_manager.change_reply_willing_not_sent(chat)
# print(f"response: {response}")
if response:
diff --git a/src/plugins/chat/cq_code.py b/src/plugins/chat/cq_code.py
index bc40cff80..049419f1c 100644
--- a/src/plugins/chat/cq_code.py
+++ b/src/plugins/chat/cq_code.py
@@ -86,9 +86,12 @@ class CQCode:
else:
self.translated_segments = Seg(type="text", data="[图片]")
elif self.type == "at":
- user_nickname = get_user_nickname(self.params.get("qq", ""))
- self.translated_segments = Seg(
- type="text", data=f"[@{user_nickname or '某人'}]"
+ if self.params.get("qq") == "all":
+ self.translated_segments = Seg(type="text", data="@[全体成员]")
+ else:
+ user_nickname = get_user_nickname(self.params.get("qq", ""))
+ self.translated_segments = Seg(
+ type="text", data=f"[@{user_nickname or '某人'}]"
)
elif self.type == "reply":
reply_segments = self.translate_reply()
diff --git a/src/plugins/chat/emoji_manager.py b/src/plugins/chat/emoji_manager.py
index 657b17c67..e3342d1a7 100644
--- a/src/plugins/chat/emoji_manager.py
+++ b/src/plugins/chat/emoji_manager.py
@@ -25,7 +25,7 @@ image_manager = ImageManager()
class EmojiManager:
_instance = None
- EMOJI_DIR = "data/emoji" # 表情包存储目录
+ EMOJI_DIR = os.path.join("data", "emoji") # 表情包存储目录
def __new__(cls):
if cls._instance is None:
@@ -211,7 +211,7 @@ class EmojiManager:
async def scan_new_emojis(self):
"""扫描新的表情包"""
try:
- emoji_dir = "data/emoji"
+ emoji_dir = self.EMOJI_DIR
os.makedirs(emoji_dir, exist_ok=True)
# 获取所有支持的图片文件
@@ -232,7 +232,7 @@ class EmojiManager:
image_hash = hashlib.md5(image_bytes).hexdigest()
image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
# 检查是否已经注册过
- existing_emoji = db["emoji"].find_one({"filename": filename})
+ existing_emoji = db["emoji"].find_one({"hash": image_hash})
description = None
if existing_emoji:
diff --git a/src/plugins/chat/utils_image.py b/src/plugins/chat/utils_image.py
index f4969d3e9..dd6d7d4d1 100644
--- a/src/plugins/chat/utils_image.py
+++ b/src/plugins/chat/utils_image.py
@@ -44,18 +44,23 @@ class ImageManager:
"""确保images集合存在并创建索引"""
if "images" not in db.list_collection_names():
db.create_collection("images")
- # 创建索引
- db.images.create_index([("hash", 1)], unique=True)
- db.images.create_index([("url", 1)])
- db.images.create_index([("path", 1)])
+
+ # 删除旧索引
+ db.images.drop_indexes()
+ # 创建新的复合索引
+ db.images.create_index([("hash", 1), ("type", 1)], unique=True)
+ db.images.create_index([("url", 1)])
+ db.images.create_index([("path", 1)])
def _ensure_description_collection(self):
"""确保image_descriptions集合存在并创建索引"""
if "image_descriptions" not in db.list_collection_names():
db.create_collection("image_descriptions")
- # 创建索引
- db.image_descriptions.create_index([("hash", 1)], unique=True)
- db.image_descriptions.create_index([("type", 1)])
+
+ # 删除旧索引
+ db.image_descriptions.drop_indexes()
+ # 创建新的复合索引
+ db.image_descriptions.create_index([("hash", 1), ("type", 1)], unique=True)
def _get_description_from_db(self, image_hash: str, description_type: str) -> Optional[str]:
"""从数据库获取图片描述
@@ -78,151 +83,21 @@ class ImageManager:
description: 描述文本
description_type: 描述类型 ('emoji' 或 'image')
"""
- db.image_descriptions.update_one(
- {"hash": image_hash, "type": description_type},
- {"$set": {"description": description, "timestamp": int(time.time())}},
- upsert=True,
- )
-
- async def save_image(
- self, image_data: Union[str, bytes], url: str = None, description: str = None, is_base64: bool = False
- ) -> Optional[str]:
- """保存图像
- Args:
- image_data: 图像数据(base64字符串或字节)
- url: 图像URL
- description: 图像描述
- is_base64: image_data是否为base64格式
- Returns:
- str: 保存后的文件路径,失败返回None
- """
try:
- # 转换为字节格式
- if is_base64:
- if isinstance(image_data, str):
- image_bytes = base64.b64decode(image_data)
- else:
- return None
- else:
- if isinstance(image_data, bytes):
- image_bytes = image_data
- else:
- return None
-
- # 计算哈希值
- image_hash = hashlib.md5(image_bytes).hexdigest()
- image_format = Image.open(io.BytesIO(image_bytes)).format.lower()
-
- # 查重
- existing = db.images.find_one({"hash": image_hash})
- if existing:
- return existing["path"]
-
- # 生成文件名和路径
- timestamp = int(time.time())
- filename = f"{timestamp}_{image_hash[:8]}.{image_format}"
- file_path = os.path.join(self.IMAGE_DIR, filename)
-
- # 保存文件
- with open(file_path, "wb") as f:
- f.write(image_bytes)
-
- # 保存到数据库
- image_doc = {
- "hash": image_hash,
- "path": file_path,
- "url": url,
- "description": description,
- "timestamp": timestamp,
- }
- db.images.insert_one(image_doc)
-
- return file_path
-
+ db.image_descriptions.update_one(
+ {"hash": image_hash, "type": description_type},
+ {
+ "$set": {
+ "description": description,
+ "timestamp": int(time.time()),
+ "hash": image_hash, # 确保hash字段存在
+ "type": description_type, # 确保type字段存在
+ }
+ },
+ upsert=True,
+ )
except Exception as e:
- logger.error(f"保存图像失败: {str(e)}")
- return None
-
- async def get_image_by_url(self, url: str) -> Optional[str]:
- """根据URL获取图像路径(带查重)
- Args:
- url: 图像URL
- Returns:
- str: 本地文件路径,不存在返回None
- """
- try:
- # 先查找是否已存在
- existing = db.images.find_one({"url": url})
- if existing:
- return existing["path"]
-
- # 下载图像
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as resp:
- if resp.status == 200:
- image_bytes = await resp.read()
- return await self.save_image(image_bytes, url=url)
- return None
-
- except Exception as e:
- logger.error(f"获取图像失败: {str(e)}")
- return None
-
- async def get_base64_by_url(self, url: str) -> Optional[str]:
- """根据URL获取base64(带查重)
- Args:
- url: 图像URL
- Returns:
- str: base64字符串,失败返回None
- """
- try:
- image_path = await self.get_image_by_url(url)
- if not image_path:
- return None
-
- with open(image_path, "rb") as f:
- image_bytes = f.read()
- return base64.b64encode(image_bytes).decode("utf-8")
-
- except Exception as e:
- logger.error(f"获取base64失败: {str(e)}")
- return None
-
- def check_url_exists(self, url: str) -> bool:
- """检查URL是否已存在
- Args:
- url: 图像URL
- Returns:
- bool: 是否存在
- """
- return db.images.find_one({"url": url}) is not None
-
- def check_hash_exists(self, image_data: Union[str, bytes], is_base64: bool = False) -> bool:
- """检查图像是否已存在
- Args:
- image_data: 图像数据(base64或字节)
- is_base64: 是否为base64格式
- Returns:
- bool: 是否存在
- """
- try:
- if is_base64:
- if isinstance(image_data, str):
- image_bytes = base64.b64decode(image_data)
- else:
- return False
- else:
- if isinstance(image_data, bytes):
- image_bytes = image_data
- else:
- return False
-
- image_hash = hashlib.md5(image_bytes).hexdigest()
- return db.images.find_one({"hash": image_hash}) is not None
-
- except Exception as e:
- logger.error(f"检查哈希失败: {str(e)}")
- return False
+ logger.error(f"保存描述到数据库失败: {str(e)}")
async def get_emoji_description(self, image_base64: str) -> str:
"""获取表情包描述,带查重和保存功能"""
@@ -242,6 +117,11 @@ class ImageManager:
prompt = "这是一个表情包,使用中文简洁的描述一下表情包的内容和表情包所表达的情感"
description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format)
+ cached_description = self._get_description_from_db(image_hash, "emoji")
+ if cached_description:
+ logger.warning(f"虽然生成了描述,但是找到缓存表情包描述: {cached_description}")
+ return f"[表情包:{cached_description}]"
+
# 根据配置决定是否保存图片
if global_config.EMOJI_SAVE:
# 生成文件名和路径
@@ -280,7 +160,6 @@ class ImageManager:
async def get_image_description(self, image_base64: str) -> str:
"""获取普通图片描述,带查重和保存功能"""
try:
- print("处理图片中")
# 计算图片哈希
image_bytes = base64.b64decode(image_base64)
image_hash = hashlib.md5(image_bytes).hexdigest()
@@ -289,7 +168,7 @@ class ImageManager:
# 查询缓存的描述
cached_description = self._get_description_from_db(image_hash, "image")
if cached_description:
- print("图片描述缓存中")
+ logger.info(f"图片描述缓存中 {cached_description}")
return f"[图片:{cached_description}]"
# 调用AI获取描述
@@ -298,7 +177,12 @@ class ImageManager:
)
description, _ = await self._llm.generate_response_for_image(prompt, image_base64, image_format)
- print(f"描述是{description}")
+ cached_description = self._get_description_from_db(image_hash, "image")
+ if cached_description:
+ logger.warning(f"虽然生成了描述,但是找到缓存图片描述 {cached_description}")
+ return f"[图片:{cached_description}]"
+
+ logger.info(f"描述是{description}")
if description is None:
logger.warning("AI未能生成图片描述")
diff --git a/src/plugins/chat/utils_user.py b/src/plugins/chat/utils_user.py
index 489eb7a1d..90c93eeb2 100644
--- a/src/plugins/chat/utils_user.py
+++ b/src/plugins/chat/utils_user.py
@@ -5,14 +5,16 @@ from .relationship_manager import relationship_manager
def get_user_nickname(user_id: int) -> str:
if int(user_id) == int(global_config.BOT_QQ):
return global_config.BOT_NICKNAME
-# print(user_id)
+ # print(user_id)
return relationship_manager.get_name(user_id)
+
def get_user_cardname(user_id: int) -> str:
if int(user_id) == int(global_config.BOT_QQ):
return global_config.BOT_NICKNAME
-# print(user_id)
- return ''
+ # print(user_id)
+ return ""
+
def get_groupname(group_id: int) -> str:
- return f"群{group_id}"
\ No newline at end of file
+ return f"群{group_id}"
diff --git a/src/plugins/chat/willing_manager.py b/src/plugins/chat/willing_manager.py
index 773d40c6e..6df27f3a4 100644
--- a/src/plugins/chat/willing_manager.py
+++ b/src/plugins/chat/willing_manager.py
@@ -1,109 +1,259 @@
import asyncio
+import random
+import time
from typing import Dict
+from loguru import logger
from .config import global_config
from .chat_stream import ChatStream
-from loguru import logger
-
class WillingManager:
def __init__(self):
self.chat_reply_willing: Dict[str, float] = {} # 存储每个聊天流的回复意愿
+ self.chat_high_willing_mode: Dict[str, bool] = {} # 存储每个聊天流是否处于高回复意愿期
+ self.chat_msg_count: Dict[str, int] = {} # 存储每个聊天流接收到的消息数量
+ self.chat_last_mode_change: Dict[str, float] = {} # 存储每个聊天流上次模式切换的时间
+ self.chat_high_willing_duration: Dict[str, int] = {} # 高意愿期持续时间(秒)
+ self.chat_low_willing_duration: Dict[str, int] = {} # 低意愿期持续时间(秒)
+ self.chat_last_reply_time: Dict[str, float] = {} # 存储每个聊天流上次回复的时间
+ self.chat_last_sender_id: Dict[str, str] = {} # 存储每个聊天流上次回复的用户ID
+ self.chat_conversation_context: Dict[str, bool] = {} # 标记是否处于对话上下文中
self._decay_task = None
+ self._mode_switch_task = None
self._started = False
-
+
async def _decay_reply_willing(self):
"""定期衰减回复意愿"""
while True:
await asyncio.sleep(5)
for chat_id in self.chat_reply_willing:
- self.chat_reply_willing[chat_id] = max(0, self.chat_reply_willing[chat_id] * 0.6)
-
+ is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
+ if is_high_mode:
+ # 高回复意愿期内轻微衰减
+ self.chat_reply_willing[chat_id] = max(0.5, self.chat_reply_willing[chat_id] * 0.95)
+ else:
+ # 低回复意愿期内正常衰减
+ self.chat_reply_willing[chat_id] = max(0, self.chat_reply_willing[chat_id] * 0.8)
+
+ async def _mode_switch_check(self):
+ """定期检查是否需要切换回复意愿模式"""
+ while True:
+ current_time = time.time()
+ await asyncio.sleep(10) # 每10秒检查一次
+
+ for chat_id in self.chat_high_willing_mode:
+ last_change_time = self.chat_last_mode_change.get(chat_id, 0)
+ is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
+
+ # 获取当前模式的持续时间
+ duration = 0
+ if is_high_mode:
+ duration = self.chat_high_willing_duration.get(chat_id, 180) # 默认3分钟
+ else:
+ duration = self.chat_low_willing_duration.get(chat_id, random.randint(300, 1200)) # 默认5-20分钟
+
+ # 检查是否需要切换模式
+ if current_time - last_change_time > duration:
+ self._switch_willing_mode(chat_id)
+ elif not is_high_mode and random.random() < 0.1:
+ # 低回复意愿期有10%概率随机切换到高回复期
+ self._switch_willing_mode(chat_id)
+
+ # 检查对话上下文状态是否需要重置
+ last_reply_time = self.chat_last_reply_time.get(chat_id, 0)
+ if current_time - last_reply_time > 300: # 5分钟无交互,重置对话上下文
+ self.chat_conversation_context[chat_id] = False
+
+ def _switch_willing_mode(self, chat_id: str):
+ """切换聊天流的回复意愿模式"""
+ is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
+
+ if is_high_mode:
+ # 从高回复期切换到低回复期
+ self.chat_high_willing_mode[chat_id] = False
+ self.chat_reply_willing[chat_id] = 0.1 # 设置为最低回复意愿
+ self.chat_low_willing_duration[chat_id] = random.randint(600, 1200) # 10-20分钟
+ logger.debug(f"聊天流 {chat_id} 切换到低回复意愿期,持续 {self.chat_low_willing_duration[chat_id]} 秒")
+ else:
+ # 从低回复期切换到高回复期
+ self.chat_high_willing_mode[chat_id] = True
+ self.chat_reply_willing[chat_id] = 1.0 # 设置为较高回复意愿
+ self.chat_high_willing_duration[chat_id] = random.randint(180, 240) # 3-4分钟
+ logger.debug(f"聊天流 {chat_id} 切换到高回复意愿期,持续 {self.chat_high_willing_duration[chat_id]} 秒")
+
+ self.chat_last_mode_change[chat_id] = time.time()
+ self.chat_msg_count[chat_id] = 0 # 重置消息计数
+
def get_willing(self, chat_stream: ChatStream) -> float:
"""获取指定聊天流的回复意愿"""
stream = chat_stream
if stream:
return self.chat_reply_willing.get(stream.stream_id, 0)
return 0
-
+
def set_willing(self, chat_id: str, willing: float):
"""设置指定聊天流的回复意愿"""
self.chat_reply_willing[chat_id] = willing
-
- async def change_reply_willing_received(
- self,
- chat_stream: ChatStream,
- topic: str = None,
- is_mentioned_bot: bool = False,
- config=None,
- is_emoji: bool = False,
- interested_rate: float = 0,
- ) -> float:
+
+ def _ensure_chat_initialized(self, chat_id: str):
+ """确保聊天流的所有数据已初始化"""
+ if chat_id not in self.chat_reply_willing:
+ self.chat_reply_willing[chat_id] = 0.1
+
+ if chat_id not in self.chat_high_willing_mode:
+ self.chat_high_willing_mode[chat_id] = False
+ self.chat_last_mode_change[chat_id] = time.time()
+ self.chat_low_willing_duration[chat_id] = random.randint(300, 1200) # 5-20分钟
+
+ if chat_id not in self.chat_msg_count:
+ self.chat_msg_count[chat_id] = 0
+
+ if chat_id not in self.chat_conversation_context:
+ self.chat_conversation_context[chat_id] = False
+
+ async def change_reply_willing_received(self,
+ chat_stream: ChatStream,
+ topic: str = None,
+ is_mentioned_bot: bool = False,
+ config = None,
+ is_emoji: bool = False,
+ interested_rate: float = 0,
+ sender_id: str = None) -> float:
"""改变指定聊天流的回复意愿并返回回复概率"""
# 获取或创建聊天流
stream = chat_stream
chat_id = stream.stream_id
-
+ current_time = time.time()
+
+ self._ensure_chat_initialized(chat_id)
+
+ # 增加消息计数
+ self.chat_msg_count[chat_id] = self.chat_msg_count.get(chat_id, 0) + 1
+
current_willing = self.chat_reply_willing.get(chat_id, 0)
-
- if is_mentioned_bot and current_willing < 1.0:
- current_willing += 0.9
+ is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
+ msg_count = self.chat_msg_count.get(chat_id, 0)
+ in_conversation_context = self.chat_conversation_context.get(chat_id, False)
+
+ # 检查是否是对话上下文中的追问
+ last_reply_time = self.chat_last_reply_time.get(chat_id, 0)
+ last_sender = self.chat_last_sender_id.get(chat_id, "")
+ is_follow_up_question = False
+
+ # 如果是同一个人在短时间内(2分钟内)发送消息,且消息数量较少(<=5条),视为追问
+ if sender_id and sender_id == last_sender and current_time - last_reply_time < 120 and msg_count <= 5:
+ is_follow_up_question = True
+ in_conversation_context = True
+ self.chat_conversation_context[chat_id] = True
+ logger.debug(f"检测到追问 (同一用户), 提高回复意愿")
+ current_willing += 0.3
+
+ # 特殊情况处理
+ if is_mentioned_bot:
+ current_willing += 0.5
+ in_conversation_context = True
+ self.chat_conversation_context[chat_id] = True
logger.debug(f"被提及, 当前意愿: {current_willing}")
- elif is_mentioned_bot:
- current_willing += 0.05
- logger.debug(f"被重复提及, 当前意愿: {current_willing}")
-
+
if is_emoji:
current_willing *= 0.1
logger.debug(f"表情包, 当前意愿: {current_willing}")
-
- logger.debug(f"放大系数_interested_rate: {global_config.response_interested_rate_amplifier}")
- interested_rate *= global_config.response_interested_rate_amplifier # 放大回复兴趣度
- if interested_rate > 0.4:
- # print(f"兴趣度: {interested_rate}, 当前意愿: {current_willing}")
- current_willing += interested_rate - 0.4
-
- current_willing *= global_config.response_willing_amplifier # 放大回复意愿
- # print(f"放大系数_willing: {global_config.response_willing_amplifier}, 当前意愿: {current_willing}")
-
- reply_probability = max((current_willing - 0.45) * 2, 0)
-
+
+ # 根据话题兴趣度适当调整
+ if interested_rate > 0.5:
+ current_willing += (interested_rate - 0.5) * 0.5
+
+ # 根据当前模式计算回复概率
+ base_probability = 0.0
+
+ if in_conversation_context:
+ # 在对话上下文中,降低基础回复概率
+ base_probability = 0.5 if is_high_mode else 0.25
+ logger.debug(f"处于对话上下文中,基础回复概率: {base_probability}")
+ elif is_high_mode:
+ # 高回复周期:4-8句话有50%的概率会回复一次
+ base_probability = 0.50 if 4 <= msg_count <= 8 else 0.2
+ else:
+ # 低回复周期:需要最少15句才有30%的概率会回一句
+ base_probability = 0.30 if msg_count >= 15 else 0.03 * min(msg_count, 10)
+
+ # 考虑回复意愿的影响
+ reply_probability = base_probability * current_willing
+
# 检查群组权限(如果是群聊)
- if chat_stream.group_info:
+ if chat_stream.group_info and config:
if chat_stream.group_info.group_id in config.talk_frequency_down_groups:
reply_probability = reply_probability / global_config.down_frequency_rate
- reply_probability = min(reply_probability, 1)
+ # 限制最大回复概率
+ reply_probability = min(reply_probability, 0.75) # 设置最大回复概率为75%
if reply_probability < 0:
reply_probability = 0
-
+
+ # 记录当前发送者ID以便后续追踪
+ if sender_id:
+ self.chat_last_sender_id[chat_id] = sender_id
+
self.chat_reply_willing[chat_id] = min(current_willing, 3.0)
return reply_probability
-
+
def change_reply_willing_sent(self, chat_stream: ChatStream):
"""开始思考后降低聊天流的回复意愿"""
stream = chat_stream
if stream:
- current_willing = self.chat_reply_willing.get(stream.stream_id, 0)
- self.chat_reply_willing[stream.stream_id] = max(0, current_willing - 2)
-
- def change_reply_willing_after_sent(self, chat_stream: ChatStream):
- """发送消息后提高聊天流的回复意愿"""
+ chat_id = stream.stream_id
+ self._ensure_chat_initialized(chat_id)
+ is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
+ current_willing = self.chat_reply_willing.get(chat_id, 0)
+
+ # 回复后减少回复意愿
+ self.chat_reply_willing[chat_id] = max(0, current_willing - 0.3)
+
+ # 标记为对话上下文中
+ self.chat_conversation_context[chat_id] = True
+
+ # 记录最后回复时间
+ self.chat_last_reply_time[chat_id] = time.time()
+
+ # 重置消息计数
+ self.chat_msg_count[chat_id] = 0
+
+ def change_reply_willing_not_sent(self, chat_stream: ChatStream):
+ """决定不回复后提高聊天流的回复意愿"""
stream = chat_stream
if stream:
- current_willing = self.chat_reply_willing.get(stream.stream_id, 0)
- if current_willing < 1:
- self.chat_reply_willing[stream.stream_id] = min(1, current_willing + 0.2)
-
+ chat_id = stream.stream_id
+ self._ensure_chat_initialized(chat_id)
+ is_high_mode = self.chat_high_willing_mode.get(chat_id, False)
+ current_willing = self.chat_reply_willing.get(chat_id, 0)
+ in_conversation_context = self.chat_conversation_context.get(chat_id, False)
+
+ # 根据当前模式调整不回复后的意愿增加
+ if is_high_mode:
+ willing_increase = 0.1
+ elif in_conversation_context:
+ # 在对话上下文中但决定不回复,小幅增加回复意愿
+ willing_increase = 0.15
+ else:
+ willing_increase = random.uniform(0.05, 0.1)
+
+ self.chat_reply_willing[chat_id] = min(2.0, current_willing + willing_increase)
+
+ def change_reply_willing_after_sent(self, chat_stream: ChatStream):
+ """发送消息后提高聊天流的回复意愿"""
+ # 由于已经在sent中处理,这个方法保留但不再需要额外调整
+ pass
+
async def ensure_started(self):
- """确保衰减任务已启动"""
+ """确保所有任务已启动"""
if not self._started:
if self._decay_task is None:
self._decay_task = asyncio.create_task(self._decay_reply_willing())
+ if self._mode_switch_task is None:
+ self._mode_switch_task = asyncio.create_task(self._mode_switch_check())
self._started = True
-
# 创建全局实例
-willing_manager = WillingManager()
+willing_manager = WillingManager()
\ No newline at end of file
diff --git a/src/plugins/models/utils_model.py b/src/plugins/models/utils_model.py
index afe4baeb5..0f5bb335c 100644
--- a/src/plugins/models/utils_model.py
+++ b/src/plugins/models/utils_model.py
@@ -132,7 +132,7 @@ class LLM_request:
# 常见Error Code Mapping
error_code_mapping = {
400: "参数不正确",
- 401: "API key 错误,认证失败",
+ 401: "API key 错误,认证失败,请检查/config/bot_config.toml和.env.prod中的配置是否正确哦~",
402: "账号余额不足",
403: "需要实名,或余额不足",
404: "Not Found",
diff --git a/template.env b/template.env
index d2a763112..322776ce7 100644
--- a/template.env
+++ b/template.env
@@ -23,7 +23,7 @@ CHAT_ANY_WHERE_BASE_URL=https://api.chatanywhere.tech/v1
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1/
DEEP_SEEK_BASE_URL=https://api.deepseek.com/v1
-#定义你要用的api的base_url
+#定义你要用的api的key(需要去对应网站申请哦)
DEEP_SEEK_KEY=
CHAT_ANY_WHERE_KEY=
SILICONFLOW_KEY=