This commit is contained in:
minecraft1024a
2025-11-02 10:50:22 +08:00
29 changed files with 626 additions and 493 deletions

View File

@@ -80,9 +80,8 @@
- 🔄 **数据库切换** - 支持 SQLite 与 MySQL 自由切换,采用 SQLAlchemy 2.0 重新构建
- 🛡️ **反注入集成** - 内置一整套回复前注入过滤系统,为人格保驾护航
- 🎥 **视频分析** - 支持多种视频识别模式,拓展原版视觉
- 😴 **苏醒系统** - 能够睡觉、失眠、被吵醒,更具乐趣
- 📅 **日程系统** - 让墨狐规划每一天
- 🧠 **拓展记忆系统** - 支持瞬时记忆等多种记忆
- 📅 **日程系统** - 让MoFox规划每一天
- 🧠 **拓展记忆系统** - 支持瞬时记忆和长期记忆等多种记忆方式
- 🎪 **完善的 Event** - 支持动态事件注册和处理器订阅,并实现了聚合结果管理
- 🔍 **内嵌魔改插件** - 内置联网搜索等诸多功能,等你来探索
- 🔌 **MCP 协议支持** - 集成 Model Context Protocol支持外部工具服务器连接仅 Streamable HTTP
@@ -103,7 +102,7 @@
| 项目 | 要求 |
| ------------ | ---------------------------------------- |
| 🖥️ 操作系统 | Windows 10/11、macOS 10.14+、Linux (Ubuntu 18.04+) |
| 🐍 Python 版本 | Python 3.10 或更高版本 |
| 🐍 Python 版本 | Python 3.11 或更高版本 |
| 💾 内存 | 建议 ≥ 4GB 可用内存 |
| 💿 存储空间 | 建议 ≥ 2GB 可用空间 |

8
bot.py
View File

@@ -122,7 +122,7 @@ class EULAManager:
confirm_logger.critical(" - EULA.md (用户许可协议)")
confirm_logger.critical(" - PRIVACY.md (隐私条款)")
confirm_logger.critical(
f"然后编辑 .env 文件,将 'EULA_CONFIRMED=false' 改为 'EULA_CONFIRMED=true'"
"然后编辑 .env 文件,将 'EULA_CONFIRMED=false' 改为 'EULA_CONFIRMED=true'"
)
attempts = 0
@@ -617,9 +617,9 @@ async def wait_for_user_input():
# 在非生产环境下,使用异步方式等待输入
if os.getenv("ENVIRONMENT") != "production":
logger.info("程序执行完成,按 Ctrl+C 退出...")
# 简单的异步等待,避免阻塞事件循环
while True:
await asyncio.sleep(1)
# 使用 Event 替代 sleep 循环,避免阻塞事件循环
shutdown_event = asyncio.Event()
await shutdown_event.wait()
except KeyboardInterrupt:
logger.info("用户中断程序")
return True

View File

@@ -192,8 +192,8 @@ class BilibiliPlugin(BasePlugin):
# 插件基本信息
plugin_name: str = "bilibili_video_watcher"
enable_plugin: bool = False
dependencies: list[str] = []
python_dependencies: list[str] = []
dependencies: ClassVar[list[str]] = []
python_dependencies: ClassVar[list[str]] = []
config_file_name: str = "config.toml"
# 配置节描述

View File

@@ -7,6 +7,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from threading import Lock
import aiofiles
import orjson
from json_repair import repair_json
@@ -158,8 +159,9 @@ async def extract_info_async(pg_hash, paragraph, llm_api):
with file_lock:
if os.path.exists(temp_file_path):
try:
with open(temp_file_path, "rb") as f:
return orjson.loads(f.read()), None
async with aiofiles.open(temp_file_path, "rb") as f:
content = await f.read()
return orjson.loads(content), None
except orjson.JSONDecodeError:
os.remove(temp_file_path)
@@ -182,8 +184,8 @@ async def extract_info_async(pg_hash, paragraph, llm_api):
"extracted_triples": extracted_data.get("triples", []),
}
with file_lock:
with open(temp_file_path, "wb") as f:
f.write(orjson.dumps(doc_item))
async with aiofiles.open(temp_file_path, "wb") as f:
await f.write(orjson.dumps(doc_item))
return doc_item, None
except Exception as e:
logger.error(f"提取信息失败:{pg_hash}, 错误:{e}")

View File

@@ -3,6 +3,7 @@ import base64
import binascii
import hashlib
import io
import json
import os
import random
import re
@@ -974,141 +975,91 @@ class EmojiManager:
logger.debug(f"查询已有表情包描述时出错: {e}")
# 3. 如果没有现有描述则调用VLM生成新的详细描述
# 3. 如果有现有描述则复用或解析否则调用VLM生成新的统一描述
if existing_description:
description = existing_description
logger.info("[优化] 复用已有的详细描述跳过VLM调用")
# 兼容旧格式的 final_description尝试从中解析出各个部分
logger.info("[优化] 复用已有的描述跳过VLM调用")
description_match = re.search(r"Desc: (.*)", existing_description, re.DOTALL)
keywords_match = re.search(r"Keywords: \[(.*?)\]", existing_description)
refined_match = re.search(r"^(.*?) Keywords:", existing_description, re.DOTALL)
description = description_match.group(1).strip() if description_match else existing_description
emotions_text = keywords_match.group(1) if keywords_match else ""
emotions = [e.strip() for e in emotions_text.split(",") if e.strip()]
refined_description = refined_match.group(1).strip() if refined_match else ""
final_description = existing_description
else:
logger.info("[VLM分析] 开始为新表情包生成详细描述")
# 为动态图GIF和静态图构建不同的、要求简洁的prompt
logger.info("[VLM分析] 开始为新表情包生成统一描述")
description, emotions, refined_description, is_compliant = "", [], "", False
prompt = f"""这是一个表情包。请你作为一位互联网“梗”学家和情感分析师对这个表情包进行全面分析并以JSON格式返回你的分析结果。
你的分析需要包含以下四个部分:
1. **detailed_description**: 对图片的详尽描述不超过250字。请遵循以下结构
- 概括图片主题和氛围。
- 详细描述核心元素,识别二次元角色及出处。
- 描述传达的核心情绪或梗。
- 准确转述图中文字。
- 特别注意识别网络文化特殊含义(如“滑稽”表情)。
2. **keywords**: 提炼5到8个核心关键词或短语数组形式应包含核心文字、表情动作、情绪氛围、主体或构图特点。
3. **refined_sentence**: 生成一句自然的精炼描述,应包含:角色名称、出处、核心文字,并体现核心情绪。
4. **is_compliant**: 根据以下标准判断是否合规布尔值true/false
- 主题符合:“{global_config.emoji.filtration_prompt}”。
- 内容健康,无不良元素。
- 必须是表情包,非普通截图。
- 图中文字不超过5个。
请确保你的最终输出是严格的JSON对象不要添加任何额外解释或文本。
"""
image_data_for_vlm, image_format_for_vlm = image_base64, image_format
if image_format in ["gif", "GIF"]:
image_base64_frames = get_image_manager().transform_gif(image_base64)
if not image_base64_frames:
raise RuntimeError("GIF表情包转换失败")
prompt = "这是一个GIF动图表情包的关键帧。请用不超过250字进行详尽且严谨的描述。请按照以下结构组织首先概括图片的主题和整体氛围。其次详细描述图片中的核心元素如果包含二次元角色请尝试识别角色名称和出处。接着描述动态画面展现了什么变化以及它传达的核心情绪或玩的梗。最后如果图片中包含任何文字请准确地转述出来这部分不计入字数限制。请特别注意识别网络文化中的特殊含义例如“滑稽”表情应被识别为“滑稽”而不仅仅是“黄色的脸”。"
description = None
for i in range(3):
try:
logger.info(f"[VLM调用] 正在为GIF表情包生成描述 (第 {i+1}/3 次)...")
description, _ = await self.vlm.generate_response_for_image(
prompt, image_base64_frames, "jpeg", temperature=0.3, max_tokens=600
)
if description and description.strip():
break
except Exception as e:
logger.error(f"VLM调用失败 (第 {i+1}/3 次): {e}", exc_info=True)
if i < 2:
logger.warning("表情包识别失败将在1秒后重试...")
await asyncio.sleep(1)
else:
prompt = "这是一个表情包。请用不超过250字进行详尽且严谨的描述。请按照以下结构组织首先概括图片的主题和整体氛围。其次详细描述图片中的核心元素如果包含二次元角色请尝试识别角色名称和出处。接着描述它传达的核心情绪或玩的梗。最后如果图片中包含任何文字请准确地转述出来这部分不计入字数限制。请特别注意识别网络文化中的特殊含义例如“滑稽”表情应被识别为“滑稽”而不仅仅是“黄色的脸”。"
description = None
for i in range(3):
try:
logger.info(f"[VLM调用] 正在为静态表情包生成描述 (第 {i+1}/3 次)...")
description, _ = await self.vlm.generate_response_for_image(
prompt, image_base64, image_format, temperature=0.3, max_tokens=600
)
if description and description.strip():
break
except Exception as e:
logger.error(f"VLM调用失败 (第 {i+1}/3 次): {e}", exc_info=True)
if i < 2:
logger.warning("表情包识别失败将在1秒后重试...")
await asyncio.sleep(1)
image_data_for_vlm, image_format_for_vlm = image_base64_frames, "jpeg"
prompt = "这是一个GIF动图表情包的关键帧。" + prompt
# 4. 检查VLM描述是否有效
if not description or not description.strip():
logger.warning("VLM未能生成有效的详细描述,中止处理。")
return "", []
for i in range(3):
try:
logger.info(f"[VLM调用] 正在为表情包生成统一描述 (第 {i+1}/3 次)...")
vlm_response_str, _ = await self.vlm.generate_response_for_image(
prompt, image_data_for_vlm, image_format_for_vlm, temperature=0.3, max_tokens=800
)
if not vlm_response_str:
continue
# 5. 内容审核,确保表情包符合规定
if global_config.emoji.content_filtration:
prompt = f"""
请根据以下标准审核这个表情包:
1. 主题必须符合:"{global_config.emoji.filtration_prompt}"
2. 内容健康,不含色情、暴力、政治敏感等元素。
3. 必须是表情包,而不是普通的聊天截图或视频截图。
4. 表情包中的文字数量如果有不能超过5个。
这个表情包是否完全满足以上所有要求?请只回答“是”或“否”。
"""
content, _ = await self.vlm.generate_response_for_image(
prompt, image_base64, image_format, temperature=0.1, max_tokens=10
)
if "" in content:
match = re.search(r"\{.*\}", vlm_response_str, re.DOTALL)
if match:
vlm_response_json = json.loads(match.group(0))
description = vlm_response_json.get("detailed_description", "")
emotions = vlm_response_json.get("keywords", [])
refined_description = vlm_response_json.get("refined_sentence", "")
is_compliant = vlm_response_json.get("is_compliant", False)
if description and emotions and refined_description:
logger.info("[VLM分析] 成功解析VLM返回的JSON数据。")
break
logger.warning("[VLM分析] VLM返回的JSON数据不完整或格式错误准备重试。")
except (json.JSONDecodeError, AttributeError) as e:
logger.error(f"VLM JSON解析失败 (第 {i+1}/3 次): {e}", exc_info=True)
except Exception as e:
logger.error(f"VLM调用失败 (第 {i+1}/3 次): {e}", exc_info=True)
description, emotions, refined_description = "", [], "" # Reset for retry
if i < 2:
await asyncio.sleep(1)
if not description or not emotions or not refined_description:
logger.warning("VLM未能生成有效的统一描述中止处理。")
return "", []
if global_config.emoji.content_filtration and not is_compliant:
logger.warning(f"表情包审核未通过,内容: {description[:50]}...")
return "", []
# 6. 基于VLM的详细描述提炼“精炼关键词”
emotions = []
emotions_text = ""
if global_config.emoji.enable_emotion_analysis:
logger.info("[情感分析] 开始提炼表情包的“精炼关键词”")
emotion_prompt = f"""
你是一个互联网“梗”学家和情感分析师。
这里有一份关于某个表情包的详细描述:
---
{description}
---
请你基于这份描述,提炼出这个表情包最核心的、可用于检索的关键词。
final_description = f"{refined_description} Keywords: [{','.join(emotions)}] Desc: {description}"
你的任务是:
1. **全面分析**:仔细阅读描述,理解表情包的全部细节,包括**图中文字、人物表情、动作、情绪、构图**等。
2. **提炼关键词**:总结出 5 到 8 个最能代表这个表情包的关键词或短语。
3. **关键词要求**
- 必须包含表情包中的**核心文字**(如果有)。
- 必须描述核心的**表情和动作**(例如:“歪头杀”、“摊手”、“无奈苦笑”)。
- 必须体现核心的**情绪和氛围**(例如:“悲伤”、“喜悦”、“沙雕”、“阴阳怪气”)。
- 可以包含**核心主体或构图特点**(例如:“猫猫头”、“大头贴”、“模糊画质”)。
4. **格式要求**:请直接输出这些关键词,并用**逗号**分隔,不要添加任何其他解释或编号。
"""
emotions_text, _ = await self.llm_emotion_judge.generate_response_async(
emotion_prompt, temperature=0.6, max_tokens=150
)
emotions = [e.strip() for e in emotions_text.split(",") if e.strip()]
else:
logger.info("[情感分析] 表情包感情关键词二次识别已禁用,跳过此步骤")
# 7. 基于详细描述和关键词,生成“精炼自然语言描述”
refined_description = ""
if emotions: # 只有在成功提取关键词后才进行精炼
logger.info("[自然语言精炼] 开始生成“点睛之笔”的自然语言描述")
refine_prompt = f"""
你的任务是为一张表情包生成一句自然的、包含核心信息的精炼描述。
这里是关于这个表情包的分析信息:
# 详细描述
{description}
# 核心关键词
{emotions_text}
# 你的任务
请结合以上所有信息,用一句自然的语言,概括出这个表情包的核心内容。
# 规则 (非常重要!)
1. **自然流畅**:描述应该像一个普通人看到图片后的自然反应,而不是生硬的机器分析。
2. **包含关键信息**:如果详细描述中识别出了角色名称、出处,必须包含在精炼描述中。
3. **体现情绪**:描述需要体现出表情包传达的核心情绪。
4. **包含核心文字**:如果表情包中有文字,必须将文字完整地包含在描述中。
5. **输出格式****请直接返回这句描述,不要添加任何前缀、标题、引号或多余的解释。**
示例:
- 详细描述“图片的核心是一位面带微笑的少女她被识别为游戏《崩坏3rd》中的角色爱莉希雅Elysia...”
- 正确输出游戏《崩坏3rd》中的角色爱莉希雅她面带微笑看起来很开心。
"""
refined_description, _ = await self.llm_emotion_judge.generate_response_async(
refine_prompt, temperature=0.7, max_tokens=100
)
refined_description = refined_description.strip()
# 8. 格式化最终的描述,并返回结果
final_description = (
f"{refined_description} Keywords: [{','.join(emotions)}] Desc: {description}"
)
logger.info(f"[注册分析] VLM描述: {description}")
logger.info(f"[注册分析] 提炼出的情感标签: {emotions}")
logger.info(f"[注册分析] 精炼后的自然语言描述: {refined_description}")
return final_description, emotions
except Exception as e:

View File

@@ -4,6 +4,7 @@ import time
from datetime import datetime
from typing import Any
import aiofiles
import orjson
from sqlalchemy import select
@@ -729,8 +730,9 @@ class ExpressionLearnerManager:
if not os.path.exists(expr_file):
continue
try:
with open(expr_file, encoding="utf-8") as f:
expressions = orjson.loads(f.read())
async with aiofiles.open(expr_file, encoding="utf-8") as f:
content = await f.read()
expressions = orjson.loads(content)
if not isinstance(expressions, list):
logger.warning(f"表达方式文件格式错误,跳过: {expr_file}")
@@ -791,8 +793,8 @@ class ExpressionLearnerManager:
os.makedirs(done_parent_dir, exist_ok=True)
logger.debug(f"为done.done创建父目录: {done_parent_dir}")
with open(done_flag, "w", encoding="utf-8") as f:
f.write("done\n")
async with aiofiles.open(done_flag, "w", encoding="utf-8") as f:
await f.write("done\n")
logger.info(f"表达方式JSON迁移已完成共迁移 {migrated_count} 个表达方式已写入done.done标记文件")
except PermissionError as e:
logger.error(f"权限不足无法写入done.done标记文件: {e}")

View File

@@ -4,6 +4,7 @@ import os
from dataclasses import dataclass
# import tqdm
import aiofiles
import faiss
import numpy as np
import orjson
@@ -194,8 +195,8 @@ class EmbeddingStore:
test_vectors[str(idx)] = []
with open(self.get_test_file_path(), "w", encoding="utf-8") as f:
f.write(orjson.dumps(test_vectors, option=orjson.OPT_INDENT_2).decode("utf-8"))
async with aiofiles.open(self.get_test_file_path(), "w", encoding="utf-8") as f:
await f.write(orjson.dumps(test_vectors, option=orjson.OPT_INDENT_2).decode("utf-8"))
logger.info("测试字符串嵌入向量保存完成")

View File

@@ -25,6 +25,9 @@ from src.llm_models.utils_model import LLMRequest
logger = get_logger(__name__)
# 全局背景任务集合
_background_tasks = set()
@dataclass
class HippocampusSampleConfig:
@@ -89,7 +92,9 @@ class HippocampusSampler:
task_config = getattr(model_config.model_task_config, "utils", None)
if task_config:
self.memory_builder_model = LLMRequest(model_set=task_config, request_type="memory.hippocampus_build")
asyncio.create_task(self.start_background_sampling())
task = asyncio.create_task(self.start_background_sampling())
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
logger.info("✅ 海马体采样器初始化成功")
else:
raise RuntimeError("未找到记忆构建模型配置")

View File

@@ -38,6 +38,7 @@ E = TypeVar("E", bound=Enum)
import orjson
from json_repair import repair_json
from src.chat.memory_system.memory_chunk import (
ConfidenceLevel,
@@ -140,6 +141,12 @@ class MemoryBuilder:
prompt = self._build_llm_extraction_prompt(text, context)
response, _ = await self.llm_model.generate_response_async(prompt, temperature=0.3)
# 记录原始响应用于调试
if response:
logger.debug(f"LLM记忆提取原始响应长度: {len(response)}, 前300字符: {response[:300]}")
else:
logger.warning("LLM记忆提取返回空响应")
# 解析LLM响应
memories = self._parse_llm_response(response, user_id, timestamp, context)
@@ -333,26 +340,75 @@ class MemoryBuilder:
return prompt
def _extract_json_payload(self, response: str) -> str | None:
"""从模型响应中提取JSON部分兼容Markdown代码块等格式"""
"""从模型响应中提取JSON部分兼容Markdown代码块等格式
增强的JSON提取策略支持多种格式
1. Markdown代码块: ```json ... ```
2. 普通代码块: ``` ... ```
3. 大括号包围的JSON对象
4. 直接的JSON字符串
"""
if not response:
return None
stripped = response.strip()
# 优先处理Markdown代码块格式 ```json ... ```
code_block_match = re.search(r"```(?:json)?\s*(.*?)```", stripped, re.IGNORECASE | re.DOTALL)
if code_block_match:
candidate = code_block_match.group(1).strip()
if candidate:
return candidate
# 策略1: 优先处理Markdown代码块格式 ```json ... ``` 或 ``` ... ```
code_block_patterns = [
r"```json\s*(.*?)```", # 明确标记json的代码块
r"```\s*(.*?)```", # 普通代码块
]
for pattern in code_block_patterns:
code_block_match = re.search(pattern, stripped, re.IGNORECASE | re.DOTALL)
if code_block_match:
candidate = code_block_match.group(1).strip()
if candidate and (candidate.startswith("{") or candidate.startswith("[")):
logger.debug(f"从代码块中提取JSON长度: {len(candidate)}")
return candidate
# 回退到查找第一个 JSON 对象大括号范围
# 策略2: 查找第一个完整的JSON对象大括号匹配)
start = stripped.find("{")
end = stripped.rfind("}")
if start != -1 and end != -1 and end > start:
return stripped[start : end + 1].strip()
if start != -1:
# 使用栈来找到匹配的结束大括号
brace_count = 0
in_string = False
escape_next = False
for i in range(start, len(stripped)):
char = stripped[i]
if escape_next:
escape_next = False
continue
if char == "\\":
escape_next = True
continue
if char == '"' and not escape_next:
in_string = not in_string
continue
if not in_string:
if char == "{":
brace_count += 1
elif char == "}":
brace_count -= 1
if brace_count == 0:
# 找到完整的JSON对象
candidate = stripped[start : i + 1].strip()
logger.debug(f"通过大括号匹配提取JSON长度: {len(candidate)}")
return candidate
return stripped if stripped.startswith("{") and stripped.endswith("}") else None
# 策略3: 简单的整体检查作为最后的fallback
if stripped.startswith("{") and stripped.endswith("}"):
logger.debug(f"整体作为JSON长度: {len(stripped)}")
return stripped
# 所有策略都失败
logger.warning(f"无法从响应中提取JSON响应预览: {stripped[:200]}")
return None
def _parse_llm_response(
self, response: str, user_id: str, timestamp: float, context: dict[str, Any]
@@ -368,11 +424,52 @@ class MemoryBuilder:
try:
data = orjson.loads(json_payload)
logger.debug(f"JSON直接解析成功数据keys: {list(data.keys()) if isinstance(data, dict) else type(data)}")
except Exception as e:
preview = json_payload[:200]
raise MemoryExtractionError(f"LLM响应JSON解析失败: {e}, 片段: {preview}") from e
# 尝试使用 json_repair 修复 JSON
logger.warning(f"JSON直接解析失败: {type(e).__name__}: {e}尝试使用json_repair修复")
logger.debug(f"失败的JSON片段(前500字符): {json_payload[:500]}")
try:
repaired_json = repair_json(json_payload)
# repair_json 可能返回字符串或已解析的对象
if isinstance(repaired_json, str):
data = orjson.loads(repaired_json)
logger.info(f"✅ JSON修复成功(字符串模式)数据keys: {list(data.keys()) if isinstance(data, dict) else type(data)}")
else:
data = repaired_json
logger.info(f"✅ JSON修复成功(对象模式)数据keys: {list(data.keys()) if isinstance(data, dict) else type(data)}")
except Exception as repair_error:
preview = json_payload[:300]
logger.error(f"❌ JSON修复也失败: {type(repair_error).__name__}: {repair_error}")
logger.error(f"完整JSON payload(前800字符):\n{json_payload[:800]}")
raise MemoryExtractionError(
f"LLM响应JSON解析失败\n"
f"原始错误: {type(e).__name__}: {e}\n"
f"修复错误: {type(repair_error).__name__}: {repair_error}\n"
f"JSON片段(前300字符): {preview}"
) from e
# 提取 memories 列表,兼容多种格式
memory_list = data.get("memories", [])
# 如果没有 memories 字段,尝试其他可能的字段名
if not memory_list:
for possible_key in ["memory", "results", "items", "data"]:
if possible_key in data:
memory_list = data[possible_key]
logger.debug(f"使用备选字段 '{possible_key}' 作为记忆列表")
break
# 如果整个data就是一个列表直接使用
if not memory_list and isinstance(data, list):
memory_list = data
logger.debug("整个JSON就是记忆列表")
if not isinstance(memory_list, list):
logger.warning(f"记忆列表格式错误期望list但得到 {type(memory_list)}, 尝试包装为列表")
memory_list = [memory_list] if memory_list else []
logger.debug(f"提取到 {len(memory_list)} 个记忆候选项")
bot_identifiers = self._collect_bot_identifiers(context)
system_identifiers = self._collect_system_identifiers(context)

View File

@@ -19,6 +19,9 @@ from src.chat.memory_system.memory_builder import MemoryBuilder, MemoryExtractio
from src.chat.memory_system.memory_chunk import MemoryChunk
from src.chat.memory_system.memory_fusion import MemoryFusionEngine
from src.chat.memory_system.memory_query_planner import MemoryQueryPlanner
# 全局背景任务集合
_background_tasks = set()
from src.chat.memory_system.message_collection_storage import MessageCollectionStorage
@@ -1611,7 +1614,9 @@ class MemorySystem:
def start_hippocampus_sampling(self):
"""启动海马体采样"""
if self.hippocampus_sampler:
asyncio.create_task(self.hippocampus_sampler.start_background_sampling())
task = asyncio.create_task(self.hippocampus_sampler.start_background_sampling())
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
logger.info("海马体后台采样已启动")
else:
logger.warning("海马体采样器未初始化,无法启动采样")

View File

@@ -19,6 +19,9 @@ from .distribution_manager import stream_loop_manager
logger = get_logger("context_manager")
# 全局背景任务集合
_background_tasks = set()
class SingleStreamContextManager:
"""单流上下文管理器 - 每个实例只管理一个 stream 的上下文"""
@@ -42,7 +45,9 @@ class SingleStreamContextManager:
logger.debug(f"单流上下文管理器初始化: {stream_id}")
# 异步初始化历史消息(不阻塞构造函数)
asyncio.create_task(self._initialize_history_from_db())
task = asyncio.create_task(self._initialize_history_from_db())
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
def get_context(self) -> StreamContext:
"""获取流上下文"""
@@ -93,7 +98,9 @@ class SingleStreamContextManager:
logger.debug(f"消息已缓存,等待当前处理完成: stream={self.stream_id}")
# 启动流的循环任务(如果还未启动)
asyncio.create_task(stream_loop_manager.start_stream_loop(self.stream_id))
task = asyncio.create_task(stream_loop_manager.start_stream_loop(self.stream_id))
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
logger.debug(f"添加消息到缓存系统: {self.stream_id}")
return True
else:
@@ -113,7 +120,9 @@ class SingleStreamContextManager:
self.total_messages += 1
self.last_access_time = time.time()
# 启动流的循环任务(如果还未启动)
asyncio.create_task(stream_loop_manager.start_stream_loop(self.stream_id))
task = asyncio.create_task(stream_loop_manager.start_stream_loop(self.stream_id))
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
logger.debug(f"添加消息{message.processed_plain_text}到单流上下文: {self.stream_id}")
return True
except Exception as e:

View File

@@ -330,10 +330,7 @@ class StreamLoopManager:
try:
start_time = time.time()
# 在处理开始前,先刷新缓存到未读消息
cached_messages = await self._flush_cached_messages_to_unread(stream_id)
if cached_messages:
logger.debug(f"处理开始前刷新缓存消息: stream={stream_id}, 数量={len(cached_messages)}")
# 注意缓存消息刷新已移至planner开始时执行动作修改器之后此处不再刷新
# 设置触发用户ID以实现回复保护
last_message = context.get_last_message()

View File

@@ -240,6 +240,8 @@ class ActionModifier:
action_instance = cast(BaseAction, action_instance)
# 设置必要的属性
action_instance.log_prefix = self.log_prefix
# 强制注入 chat_content 以供 go_activate 内部的辅助函数使用
setattr(action_instance, "_activation_chat_content", chat_content)
# 调用 go_activate 方法
task = action_instance.go_activate(
llm_judge_model=self.llm_judge

View File

@@ -71,11 +71,12 @@ def init_prompt():
不要复读你前面发过的内容,意思相近也不行。
不要浮夸,不要夸张修辞,平淡且不要输出多余内容(包括前后缀冒号和引号括号表情包at或 @等 ),只输出一条回复就好。
**【重要】不要在回复中输出任何格式化标记**
- 不要输出类似 [表情包xxx]、[图片xxx]、[回复<xxx>xxx] 这样的格式
- 如果想表达笑的情绪,直接说"哈哈""笑死"等,不要说"[表情包:笑哭]"
- 如果想提到某人,直接说""、或者他的名字,不要说"[回复<某人>]"
- 说什么就直接输出什么,不要加任何格式化标记
**【!!!绝对禁止!!!】在回复中输出任何格式化标记**
- **核心原则**: 你的回复**只能**包含纯粹的口语化文本。任何看起来像程序指令、系统提示或格式标签的内容都**绝对不允许**出现在你的回复里。
- **禁止模仿系统消息**: 绝对禁止输出任何类似 `[回复<xxx>xxx]`、`[表情包xxx]`、`[图片xxx]` 的格式。这些都是系统用于展示消息的方式,不是你应该说的话。
- **禁止模仿动作指令**: 绝对禁止输出 `[戳了戳]` 或 `[poke]`。这类互动由名为 `poke_user` 的特殊动作处理,不是文本消息。
- **正确提及用户**: 如果想提到某人,直接说“你”或他/她的名字,绝对禁止使用 `[回复<某人>]` 或 `@某人` 的格式。
- **正确表达情绪**: 如果想表达笑的情绪,直接说“哈哈”、“嘻嘻”等,绝对禁止使用 `[表情包:笑哭]` 这样的文字。
*你叫{bot_name},也有人叫你{bot_nickname}*
@@ -144,11 +145,12 @@ def init_prompt():
请注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容。
**【重要】不要在回复中输出任何格式化标记**
- 不要输出类似 [表情包xxx]、[图片xxx]、[回复<xxx>xxx] 这样的格式
- 如果想表达笑的情绪,直接说"哈哈""笑死"等,不要说"[表情包:笑哭]"
- 如果想提到某人,直接说"""",不要说"[回复<某人>]"
- 说什么就直接输出什么,不要加任何标记或括号
**【!!!绝对禁止!!!】在回复中输出任何格式化标记**
- **核心原则**: 你的回复**只能**包含纯粹的口语化文本。任何看起来像程序指令、系统提示或格式标签的内容都**绝对不允许**出现在你的回复里。
- **禁止模仿系统消息**: 绝对禁止输出任何类似 `[回复<xxx>xxx]`、`[表情包xxx]`、`[图片xxx]` 的格式。这些都是系统用于展示消息的方式,不是你应该说的话。
- **禁止模仿动作指令**: 绝对禁止输出 `[戳了戳]` 或 `[poke]`。这类互动由名为 `poke_user` 的特殊动作处理,不是文本消息。
- **正确提及用户**: 如果想提到某人,直接说“你”或他/她的名字,绝对禁止使用 `[回复<某人>]` 或 `@某人` 的格式。
- **正确表达情绪**: 如果想表达笑的情绪,直接说“哈哈”、“嘻嘻”等,绝对禁止使用 `[表情包:笑哭]` 这样的文字。
{moderation_prompt}
@@ -216,11 +218,12 @@ If you need to use the search tool, please directly call the function "lpmm_sear
{keywords_reaction_prompt}
请注意不要输出多余内容(包括前后缀冒号和引号at或 @等 )。只输出回复内容。
**【重要】不要在回复中输出任何格式化标记**
- 不要输出类似 [表情包xxx]、[图片xxx]、[回复<xxx>xxx] 这样的格式
- 如果想表达笑的情绪,直接说"哈哈""笑死"等,不要说"[表情包:笑哭]"
- 如果想提到某人,直接说"""",不要说"[回复<某人>]"
- 说什么就直接输出什么,不要加任何标记或括号
**【!!!绝对禁止!!!】在回复中输出任何格式化标记**
- **核心原则**: 你的回复**只能**包含纯粹的口语化文本。任何看起来像程序指令、系统提示或格式标签的内容都**绝对不允许**出现在你的回复里。
- **禁止模仿系统消息**: 绝对禁止输出任何类似 `[回复<xxx>xxx]`、`[表情包xxx]`、`[图片xxx]` 的格式。这些都是系统用于展示消息的方式,不是你应该说的话。
- **禁止模仿动作指令**: 绝对禁止输出 `[戳了戳]` 或 `[poke]`。这类互动由名为 `poke_user` 的特殊动作处理,不是文本消息。
- **正确提及用户**: 如果想提到某人,直接说“你”或他/她的名字,绝对禁止使用 `[回复<某人>]` 或 `@某人` 的格式。
- **正确表达情绪**: 如果想表达笑的情绪,直接说“哈哈”、“嘻嘻”等,绝对禁止使用 `[表情包:笑哭]` 这样的文字。
{moderation_prompt}
你的核心任务是针对 {reply_target_block} 中提到的内容,{relation_info_block}生成一段紧密相关且能推动对话的回复。你的回复应该:
@@ -1266,7 +1269,7 @@ class DefaultReplyer:
# 构建action描述 (如果启用planner)
action_descriptions = ""
if available_actions:
action_descriptions = "你有以下的动作能力,但执行这些动作不由你决定,由另外一个模型同步决定,因此你只需要知道有如下能力即可:\n"
action_descriptions = "以下是系统中可用的动作列表。**【重要】**这些动作将由一个独立的决策模型决定是否执行,**并非你的职责**。你只需要了解这些能力的存在,以便更好地理解对话情景,**严禁**在你的回复中模仿、调用或提及这些动作本身。\n"
for action_name, action_info in available_actions.items():
action_description = action_info.description
action_descriptions += f"- {action_name}: {action_description}\n"
@@ -1976,7 +1979,7 @@ class DefaultReplyer:
return f"你与{sender}是普通朋友关系。"
async def _store_chat_memory_async(self, reply_to: str, reply_message: dict[str, Any] | None = None):
async def _store_chat_memory_async(self, reply_to: str, reply_message: DatabaseMessages | dict[str, Any] | None = None):
"""
异步存储聊天记忆从build_memory_block迁移而来

View File

@@ -3,6 +3,8 @@ from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any
import aiofiles
from src.common.database.compatibility import db_get, db_query
from src.common.database.core.models import LLMUsage, Messages, OnlineTime
from src.common.logger import get_logger
@@ -1002,8 +1004,8 @@ class StatisticOutputTask(AsyncTask):
"""
)
with open(self.record_file_path, "w", encoding="utf-8") as f:
f.write(html_template)
async with aiofiles.open(self.record_file_path, "w", encoding="utf-8") as f:
await f.write(html_template)
async def _generate_chart_data(self, stat: dict[str, Any]) -> dict:
"""生成图表数据 (异步)"""

View File

@@ -7,6 +7,7 @@ import time
import uuid
from typing import Any
import aiofiles
import numpy as np
from PIL import Image
from rich.traceback import install
@@ -198,8 +199,8 @@ class ImageManager:
os.makedirs(emoji_dir, exist_ok=True)
file_path = os.path.join(emoji_dir, filename)
with open(file_path, "wb") as f:
f.write(image_bytes)
async with aiofiles.open(file_path, "wb") as f:
await f.write(image_bytes)
logger.info(f"新表情包已保存至待注册目录: {file_path}")
except Exception as e:
logger.error(f"保存待注册表情包文件失败: {e!s}")
@@ -436,8 +437,8 @@ class ImageManager:
os.makedirs(image_dir, exist_ok=True)
file_path = os.path.join(image_dir, filename)
with open(file_path, "wb") as f:
f.write(image_bytes)
async with aiofiles.open(file_path, "wb") as f:
await f.write(image_bytes)
new_img = Images(
image_id=image_id,

View File

@@ -214,9 +214,9 @@ class AdaptiveBatchScheduler:
for priority in sorted(Priority, reverse=True):
queue = self.operation_queues[priority]
count = min(len(queue), self.current_batch_size - len(operations))
for _ in range(count):
if queue:
operations.append(queue.popleft())
if queue and count > 0:
# 使用 list.extend 代替循环 append
operations.extend(queue.popleft() for _ in range(count))
if not operations:
return

View File

@@ -9,7 +9,6 @@
from .decorators import (
cached,
db_operation,
generate_cache_key,
measure_time,
retry,
timeout,

View File

@@ -7,11 +7,13 @@ import time
from collections.abc import Callable
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any
import orjson
import structlog
import tomlkit
from rich.console import Console
from rich.text import Text
from structlog.typing import EventDict, WrappedLogger
# 创建logs目录
LOG_DIR = Path("logs")
@@ -26,37 +28,11 @@ _LOGGER_META_LOCK = threading.Lock()
_LOGGER_META: dict[str, dict[str, str | None]] = {}
def _normalize_color(color: str | None) -> str | None:
"""接受 ANSI 码 / #RRGGBB / rgb(r,g,b) / 颜色名(直接返回) -> ANSI 码.
不做复杂解析,只支持 #RRGGBB 转 24bit ANSI。
"""
if not color:
return None
color = color.strip()
if color.startswith("\033["):
return color # 已经是ANSI
if color.startswith("#") and len(color) == 7:
try:
r = int(color[1:3], 16)
g = int(color[3:5], 16)
b = int(color[5:7], 16)
return f"\033[38;2;{r};{g};{b}m"
except ValueError:
return None
# 简单 rgb(r,g,b)
if color.lower().startswith("rgb(") and color.endswith(")"):
try:
nums = color[color.find("(") + 1 : -1].split(",")
r, g, b = (int(x) for x in nums[:3])
return f"\033[38;2;{r};{g};{b}m"
except Exception:
return None
# 其他情况直接返回假设是短ANSI或名称控制台渲染器不做翻译仅输出
return color
def _register_logger_meta(name: str, *, alias: str | None = None, color: str | None = None):
"""注册/更新 logger 元数据。"""
"""注册/更新 logger 元数据。
color 参数直接存储 #RRGGBB 格式的颜色值。
"""
if not name:
return
with _LOGGER_META_LOCK:
@@ -64,7 +40,8 @@ def _register_logger_meta(name: str, *, alias: str | None = None, color: str | N
if alias is not None:
meta["alias"] = alias
if color is not None:
meta["color"] = _normalize_color(color)
# 直接存储颜色值(假设已经是 #RRGGBB 格式)
meta["color"] = color.upper() if color.startswith("#") else color
def get_logger_meta(name: str) -> dict[str, str | None]:
@@ -428,200 +405,200 @@ def reconfigure_existing_loggers():
DEFAULT_MODULE_COLORS = {
# 核心模块
"main": "\033[1;97m", # 亮白色+粗体 (主程序)
"api": "\033[92m", # 亮绿色
"emoji": "\033[38;5;214m", # 橙黄色偏向橙色但与replyer和action_manager不同
"chat": "\033[92m", # 亮蓝色
"config": "\033[93m", # 亮黄色
"common": "\033[95m", # 亮紫色
"tools": "\033[96m", # 亮青色
"lpmm": "\033[96m",
"plugin_system": "\033[91m", # 亮红色
"person_info": "\033[32m", # 绿色
"individuality": "\033[94m", # 显眼的亮蓝色
"manager": "\033[35m", # 紫色
"llm_models": "\033[36m", # 青色
"remote": "\033[38;5;242m", # 深灰色,更不显眼
"planner": "\033[36m",
"memory": "\033[38;5;117m", # 天蓝色
"hfc": "\033[38;5;81m", # 稍微暗一些的青色,保持可读
"action_manager": "\033[38;5;208m", # 橙色不与replyer重复
"message_manager": "\033[38;5;27m", # 深蓝色,消息管理器
"chatter_manager": "\033[38;5;129m", # 紫色,聊天管理器
"chatter_interest_scoring": "\033[38;5;214m", # 橙黄色,兴趣评分
"plan_executor": "\033[38;5;172m", # 橙褐色,计划执行器
"main": "#FFFFFF", # 亮白色+粗体 (主程序)
"api": "#00FF00", # 亮绿色
"emoji": "#FFAF00", # 橙黄色偏向橙色但与replyer和action_manager不同
"chat": "#00FF00", # 亮蓝色
"config": "#FFFF00", # 亮黄色
"common": "#FF00FF", # 亮紫色
"tools": "#00FFFF", # 亮青色
"lpmm": "#00FFFF",
"plugin_system": "#FF0000", # 亮红色
"person_info": "#008000", # 绿色
"individuality": "#0000FF", # 显眼的亮蓝色
"manager": "#800080", # 紫色
"llm_models": "#008080", # 青色
"remote": "#6C6C6C", # 深灰色,更不显眼
"planner": "#008080",
"memory": "#87D7FF", # 天蓝色
"hfc": "#5FD7FF", # 稍微暗一些的青色,保持可读
"action_manager": "#FF8700", # 橙色不与replyer重复
"message_manager": "#005FFF", # 深蓝色,消息管理器
"chatter_manager": "#AF00FF", # 紫色,聊天管理器
"chatter_interest_scoring": "#FFAF00", # 橙黄色,兴趣评分
"plan_executor": "#D78700", # 橙褐色,计划执行器
# 关系系统
"relation": "\033[38;5;139m", # 柔和的紫色,不刺眼
"relation": "#AF87AF", # 柔和的紫色,不刺眼
# 聊天相关模块
"normal_chat": "\033[38;5;81m", # 亮蓝绿色
"heartflow": "\033[38;5;175m", # 柔和的粉色,不显眼但保持粉色系
"sub_heartflow": "\033[38;5;207m", # 粉紫色
"subheartflow_manager": "\033[38;5;201m", # 深粉色
"background_tasks": "\033[38;5;240m", # 灰色
"chat_message": "\033[38;5;45m", # 青色
"chat_stream": "\033[38;5;51m", # 亮青色
"sender": "\033[38;5;67m", # 稍微暗一些的蓝色,不显眼
"message_storage": "\033[38;5;33m", # 深蓝色
"expressor": "\033[38;5;166m", # 橙色
"normal_chat": "#5FD7FF", # 亮蓝绿色
"heartflow": "#D787AF", # 柔和的粉色,不显眼但保持粉色系
"sub_heartflow": "#FF5FFF", # 粉紫色
"subheartflow_manager": "#FF00FF", # 深粉色
"background_tasks": "#585858", # 灰色
"chat_message": "#00D7FF", # 青色
"chat_stream": "#00FFFF", # 亮青色
"sender": "#5F87AF", # 稍微暗一些的蓝色,不显眼
"message_storage": "#0087FF", # 深蓝色
"expressor": "#D75F00", # 橙色
# 专注聊天模块
"replyer": "\033[38;5;166m", # 橙色
"memory_activator": "\033[38;5;117m", # 天蓝色
"replyer": "#D75F00", # 橙色
"memory_activator": "#87D7FF", # 天蓝色
# 插件系统
"plugins": "\033[31m", # 红色
"plugin_api": "\033[33m", # 黄色
"plugin_manager": "\033[38;5;208m", # 红色
"base_plugin": "\033[38;5;202m", # 橙红色
"send_api": "\033[38;5;208m", # 橙色
"base_command": "\033[38;5;208m", # 橙色
"component_registry": "\033[38;5;214m", # 橙黄色
"stream_api": "\033[38;5;220m", # 黄色
"plugin_hot_reload": "\033[38;5;226m", # 品红色
"config_api": "\033[38;5;226m", # 亮黄色
"heartflow_api": "\033[38;5;154m", # 黄绿色
"action_apis": "\033[38;5;118m", # 绿色
"independent_apis": "\033[38;5;82m", # 绿色
"llm_api": "\033[38;5;46m", # 亮绿色
"database_api": "\033[38;5;10m", # 绿色
"utils_api": "\033[38;5;14m", # 青色
"message_api": "\033[38;5;6m", # 青色
"plugins": "#800000", # 红色
"plugin_api": "#808000", # 黄色
"plugin_manager": "#FF8700", # 红色
"base_plugin": "#FF5F00", # 橙红色
"send_api": "#FF8700", # 橙色
"base_command": "#FF8700", # 橙色
"component_registry": "#FFAF00", # 橙黄色
"stream_api": "#FFD700", # 黄色
"plugin_hot_reload": "#FFFF00", # 品红色
"config_api": "#FFFF00", # 亮黄色
"heartflow_api": "#AFFF00", # 黄绿色
"action_apis": "#87FF00", # 绿色
"independent_apis": "#5FFF00", # 绿色
"llm_api": "#00FF00", # 亮绿色
"database_api": "#00FF00", # 绿色
"utils_api": "#00FFFF", # 青色
"message_api": "#008080", # 青色
# 管理器模块
"async_task_manager": "\033[38;5;129m", # 紫色
"mood": "\033[38;5;135m", # 紫红色
"local_storage": "\033[38;5;141m", # 紫色
"willing": "\033[38;5;147m", # 浅紫色
"async_task_manager": "#AF00FF", # 紫色
"mood": "#AF5FFF", # 紫红色
"local_storage": "#AF87FF", # 紫色
"willing": "#AFAFFF", # 浅紫色
# 工具模块
"tool_use": "\033[38;5;172m", # 橙褐色
"tool_executor": "\033[38;5;172m", # 橙褐色
"base_tool": "\033[38;5;178m", # 金黄色
"tool_use": "#D78700", # 橙褐色
"tool_executor": "#D78700", # 橙褐色
"base_tool": "#D7AF00", # 金黄色
# 工具和实用模块
"prompt_build": "\033[38;5;105m", # 紫色
"chat_utils": "\033[38;5;111m", # 蓝色
"chat_image": "\033[38;5;117m", # 浅蓝色
"maibot_statistic": "\033[38;5;129m", # 紫色
"prompt_build": "#8787FF", # 紫色
"chat_utils": "#87AFFF", # 蓝色
"chat_image": "#87D7FF", # 浅蓝色
"maibot_statistic": "#AF00FF", # 紫色
# 特殊功能插件
"mute_plugin": "\033[38;5;240m", # 灰色
"core_actions": "\033[38;5;117m", # 深红色
"tts_action": "\033[38;5;58m", # 深黄色
"doubao_pic_plugin": "\033[38;5;64m", # 深绿色
"mute_plugin": "#585858", # 灰色
"core_actions": "#87D7FF", # 深红色
"tts_action": "#5F5F00", # 深黄色
"doubao_pic_plugin": "#5F8700", # 深绿色
# Action组件
"no_reply_action": "\033[38;5;214m", # 亮橙色,显眼但不像警告
"reply_action": "\033[38;5;46m", # 亮绿色
"base_action": "\033[38;5;250m", # 浅灰色
"no_reply_action": "#FFAF00", # 亮橙色,显眼但不像警告
"reply_action": "#00FF00", # 亮绿色
"base_action": "#BCBCBC", # 浅灰色
# 数据库和消息
"database_model": "\033[38;5;94m", # 橙褐色
"database": "\033[38;5;46m", # 橙褐色
"maim_message": "\033[38;5;140m", # 紫褐色
"database_model": "#875F00", # 橙褐色
"database": "#00FF00", # 橙褐色
"maim_message": "#AF87D7", # 紫褐色
# 日志系统
"logger": "\033[38;5;8m", # 深灰色
"confirm": "\033[1;93m", # 黄色+粗体
"logger": "#808080", # 深灰色
"confirm": "#FFFF00", # 黄色+粗体
# 模型相关
"model_utils": "\033[38;5;164m", # 紫红色
"relationship_fetcher": "\033[38;5;170m", # 浅紫色
"relationship_builder": "\033[38;5;93m", # 浅蓝色
"sqlalchemy_init": "\033[38;5;105m", #
"sqlalchemy_models": "\033[38;5;105m",
"sqlalchemy_database_api": "\033[38;5;105m",
"model_utils": "#D700D7", # 紫红色
"relationship_fetcher": "#D75FD7", # 浅紫色
"relationship_builder": "#8700FF", # 浅蓝色
"sqlalchemy_init": "#8787FF", #
"sqlalchemy_models": "#8787FF",
"sqlalchemy_database_api": "#8787FF",
# s4u
"context_web_api": "\033[38;5;240m", # 深灰色
"S4U_chat": "\033[92m", # 亮绿色
"context_web_api": "#585858", # 深灰色
"S4U_chat": "#00FF00", # 亮绿色
# API相关扩展
"chat_api": "\033[38;5;34m", # 深绿色
"emoji_api": "\033[38;5;40m", # 亮绿色
"generator_api": "\033[38;5;28m", # 森林绿
"person_api": "\033[38;5;22m", # 深绿色
"tool_api": "\033[38;5;76m", # 绿色
"OpenAI客户端": "\033[38;5;81m",
"Gemini客户端": "\033[38;5;81m",
"chat_api": "#00AF00", # 深绿色
"emoji_api": "#00D700", # 亮绿色
"generator_api": "#008700", # 森林绿
"person_api": "#005F00", # 深绿色
"tool_api": "#5FD700", # 绿色
"OpenAI客户端": "#5FD7FF",
"Gemini客户端": "#5FD7FF",
# 插件系统扩展
"plugin_base": "\033[38;5;196m", # 红色
"base_event_handler": "\033[38;5;203m", # 粉红色
"events_manager": "\033[38;5;209m", # 橙红色
"global_announcement_manager": "\033[38;5;215m", # 浅橙色
"plugin_base": "#FF0000", # 红色
"base_event_handler": "#FF5F5F", # 粉红色
"events_manager": "#FF875F", # 橙红色
"global_announcement_manager": "#FFAF5F", # 浅橙色
# 工具和依赖管理
"dependency_config": "\033[38;5;24m", # 深蓝色
"dependency_manager": "\033[38;5;30m", # 深青色
"manifest_utils": "\033[38;5;39m", # 蓝色
"schedule_manager": "\033[38;5;27m", # 深蓝色
"monthly_plan_manager": "\033[38;5;171m",
"plan_manager": "\033[38;5;171m",
"llm_generator": "\033[38;5;171m",
"schedule_bridge": "\033[38;5;171m",
"sleep_manager": "\033[38;5;171m",
"official_configs": "\033[38;5;171m",
"mmc_com_layer": "\033[38;5;67m",
"dependency_config": "#005F87", # 深蓝色
"dependency_manager": "#008787", # 深青色
"manifest_utils": "#00AFFF", # 蓝色
"schedule_manager": "#005FFF", # 深蓝色
"monthly_plan_manager": "#D75FFF",
"plan_manager": "#D75FFF",
"llm_generator": "#D75FFF",
"schedule_bridge": "#D75FFF",
"sleep_manager": "#D75FFF",
"official_configs": "#D75FFF",
"mmc_com_layer": "#5F87AF",
# 聊天和多媒体扩展
"chat_voice": "\033[38;5;87m", # 浅青色
"typo_gen": "\033[38;5;123m", # 天蓝色
"utils_video": "\033[38;5;75m", # 亮蓝色
"ReplyerManager": "\033[38;5;173m", # 浅橙色
"relationship_builder_manager": "\033[38;5;176m", # 浅紫色
"expression_selector": "\033[38;5;176m",
"chat_message_builder": "\033[38;5;176m",
"chat_voice": "#5FFFFF", # 浅青色
"typo_gen": "#87FFFF", # 天蓝色
"utils_video": "#5FAFFF", # 亮蓝色
"ReplyerManager": "#D7875F", # 浅橙色
"relationship_builder_manager": "#D787D7", # 浅紫色
"expression_selector": "#D787D7",
"chat_message_builder": "#D787D7",
# MaiZone QQ空间相关
"MaiZone": "\033[38;5;98m", # 紫色
"MaiZone-Monitor": "\033[38;5;104m", # 深紫色
"MaiZone.ConfigLoader": "\033[38;5;110m", # 蓝紫色
"MaiZone-Scheduler": "\033[38;5;134m", # 紫红色
"MaiZone-Utils": "\033[38;5;140m", # 浅紫色
"MaiZone": "#875FD7", # 紫色
"MaiZone-Monitor": "#8787D7", # 深紫色
"MaiZone.ConfigLoader": "#87AFD7", # 蓝紫色
"MaiZone-Scheduler": "#AF5FD7", # 紫红色
"MaiZone-Utils": "#AF87D7", # 浅紫色
# MaiZone Refactored
"MaiZone.HistoryUtils": "\033[38;5;140m",
"MaiZone.SchedulerService": "\033[38;5;134m",
"MaiZone.QZoneService": "\033[38;5;98m",
"MaiZone.MonitorService": "\033[38;5;104m",
"MaiZone.ImageService": "\033[38;5;110m",
"MaiZone.CookieService": "\033[38;5;140m",
"MaiZone.ContentService": "\033[38;5;110m",
"MaiZone.Plugin": "\033[38;5;98m",
"MaiZone.SendFeedCommand": "\033[38;5;134m",
"MaiZone.SendFeedAction": "\033[38;5;134m",
"MaiZone.ReadFeedAction": "\033[38;5;134m",
"MaiZone.HistoryUtils": "#AF87D7",
"MaiZone.SchedulerService": "#AF5FD7",
"MaiZone.QZoneService": "#875FD7",
"MaiZone.MonitorService": "#8787D7",
"MaiZone.ImageService": "#87AFD7",
"MaiZone.CookieService": "#AF87D7",
"MaiZone.ContentService": "#87AFD7",
"MaiZone.Plugin": "#875FD7",
"MaiZone.SendFeedCommand": "#AF5FD7",
"MaiZone.SendFeedAction": "#AF5FD7",
"MaiZone.ReadFeedAction": "#AF5FD7",
# 网络工具
"web_surfing_tool": "\033[38;5;130m", # 棕色
"tts": "\033[38;5;136m", # 浅棕色
"poke_plugin": "\033[38;5;136m",
"set_emoji_like_plugin": "\033[38;5;136m",
"web_surfing_tool": "#AF5F00", # 棕色
"tts": "#AF8700", # 浅棕色
"poke_plugin": "#AF8700",
"set_emoji_like_plugin": "#AF8700",
# mais4u系统扩展
"s4u_config": "\033[38;5;18m", # 深蓝色
"action": "\033[38;5;52m", # 深红色mais4u的action
"context_web": "\033[38;5;58m", # 深黄色
"gift_manager": "\033[38;5;161m", # 粉红色
"prompt": "\033[38;5;99m", # 紫色mais4u的prompt
"super_chat_manager": "\033[38;5;125m", # 紫红色
"watching": "\033[38;5;131m", # 深橙色
"offline_llm": "\033[38;5;236m", # 深灰色
"s4u_stream_generator": "\033[38;5;60m", # 深紫色
"s4u_config": "#000087", # 深蓝色
"action": "#5F0000", # 深红色mais4u的action
"context_web": "#5F5F00", # 深黄色
"gift_manager": "#D7005F", # 粉红色
"prompt": "#875FFF", # 紫色mais4u的prompt
"super_chat_manager": "#AF005F", # 紫红色
"watching": "#AF5F5F", # 深橙色
"offline_llm": "#303030", # 深灰色
"s4u_stream_generator": "#5F5F87", # 深紫色
# 其他工具
"消息压缩工具": "\033[38;5;244m", # 灰色
"lpmm_get_knowledge_tool": "\033[38;5;102m", # 绿色
"message_chunker": "\033[38;5;244m",
"plan_generator": "\033[38;5;171m",
"Permission": "\033[38;5;196m",
"web_search_plugin": "\033[38;5;130m",
"url_parser_tool": "\033[38;5;130m",
"api_key_manager": "\033[38;5;130m",
"tavily_engine": "\033[38;5;130m",
"exa_engine": "\033[38;5;130m",
"ddg_engine": "\033[38;5;130m",
"bing_engine": "\033[38;5;130m",
"vector_instant_memory_v2": "\033[38;5;117m",
"async_memory_optimizer": "\033[38;5;117m",
"async_instant_memory_wrapper": "\033[38;5;117m",
"action_diagnostics": "\033[38;5;214m",
"anti_injector.message_processor": "\033[38;5;196m",
"anti_injector.user_ban": "\033[38;5;196m",
"anti_injector.statistics": "\033[38;5;196m",
"anti_injector.decision_maker": "\033[38;5;196m",
"anti_injector.counter_attack": "\033[38;5;196m",
"hfc.processor": "\033[38;5;81m",
"hfc.normal_mode": "\033[38;5;81m",
"wakeup": "\033[38;5;81m",
"cache_manager": "\033[38;5;244m",
"monthly_plan_db": "\033[38;5;94m",
"db_migration": "\033[38;5;94m",
"小彩蛋": "\033[38;5;214m",
"AioHTTP-Gemini客户端": "\033[38;5;81m",
"napcat_adapter": "\033[38;5;67m", # 柔和的灰蓝色,不刺眼且低调
"event_manager": "\033[38;5;79m", # 柔和的蓝绿色,稍微醒目但不刺眼
"消息压缩工具": "#808080", # 灰色
"lpmm_get_knowledge_tool": "#878787", # 绿色
"message_chunker": "#808080",
"plan_generator": "#D75FFF",
"Permission": "#FF0000",
"web_search_plugin": "#AF5F00",
"url_parser_tool": "#AF5F00",
"api_key_manager": "#AF5F00",
"tavily_engine": "#AF5F00",
"exa_engine": "#AF5F00",
"ddg_engine": "#AF5F00",
"bing_engine": "#AF5F00",
"vector_instant_memory_v2": "#87D7FF",
"async_memory_optimizer": "#87D7FF",
"async_instant_memory_wrapper": "#87D7FF",
"action_diagnostics": "#FFAF00",
"anti_injector.message_processor": "#FF0000",
"anti_injector.user_ban": "#FF0000",
"anti_injector.statistics": "#FF0000",
"anti_injector.decision_maker": "#FF0000",
"anti_injector.counter_attack": "#FF0000",
"hfc.processor": "#5FD7FF",
"hfc.normal_mode": "#5FD7FF",
"wakeup": "#5FD7FF",
"cache_manager": "#808080",
"monthly_plan_db": "#875F00",
"db_migration": "#875F00",
"小彩蛋": "#FFAF00",
"AioHTTP-Gemini客户端": "#5FD7FF",
"napcat_adapter": "#5F87AF", # 柔和的灰蓝色,不刺眼且低调
"event_manager": "#5FD7AF", # 柔和的蓝绿色,稍微醒目但不刺眼
}
DEFAULT_MODULE_ALIASES = {
@@ -752,25 +729,27 @@ DEFAULT_MODULE_ALIASES = {
"AioHTTP-Gemini客户端": "AioHTTP-Gemini客户端",
}
RESET_COLOR = "\033[0m"
# 创建全局 Rich Console 实例用于颜色渲染
_rich_console = Console(force_terminal=True, color_system="truecolor")
class ModuleColoredConsoleRenderer:
"""自定义控制台渲染器,为不同模块提供不同颜色"""
"""自定义控制台渲染器,使用 Rich 库原生支持 hex 颜色"""
def __init__(self, colors=True):
# sourcery skip: merge-duplicate-blocks, remove-redundant-if
self._colors = colors
self._config = LOG_CONFIG
# 日志级别颜色
self._level_colors = {
"debug": "\033[38;5;208m", # 橙色
"info": "\033[38;5;117m", # 天蓝色
"success": "\033[32m", # 绿色
"warning": "\033[33m", # 黄色
"error": "\033[31m", # 红色
"critical": "\033[35m", # 紫色
# 日志级别颜色 (#RRGGBB 格式)
self._level_colors_hex = {
"debug": "#D78700", # 橙色 (ANSI 208)
"info": "#87D7FF", # 天蓝色 (ANSI 117)
"success": "#00FF00", # 绿色
"warning": "#FFFF00", # 黄色
"error": "#FF0000", # 红色
"critical": "#FF00FF", # 紫色
}
# 根据配置决定是否启用颜色
@@ -793,71 +772,65 @@ class ModuleColoredConsoleRenderer:
def __call__(self, logger, method_name, event_dict):
# sourcery skip: merge-duplicate-blocks
"""渲染日志消息"""
# 获取基本信息
timestamp = event_dict.get("timestamp", "")
level = event_dict.get("level", "info")
logger_name = event_dict.get("logger_name", "")
event = event_dict.get("event", "")
# 构建输出
# 构建 Rich Text 对象列表
parts = []
# 日志级别样式配置
log_level_style = self._config.get("log_level_style", "lite")
level_color = self._level_colors.get(level.lower(), "") if self._colors else ""
level_hex_color = self._level_colors_hex.get(level.lower(), "")
# 时间戳lite模式下按级别着色
if timestamp:
if log_level_style == "lite" and level_color:
timestamp_part = f"{level_color}{timestamp}{RESET_COLOR}"
if log_level_style == "lite" and self._colors and level_hex_color:
parts.append(Text(timestamp, style=level_hex_color))
else:
timestamp_part = timestamp
parts.append(timestamp_part)
parts.append(Text(timestamp))
# 日志级别显示(根据配置样式)
if log_level_style == "full":
# 显示完整级别名并着色
level_text = level.upper()
if level_color:
level_part = f"{level_color}[{level_text:>8}]{RESET_COLOR}"
level_text = f"[{level.upper():>8}]"
if self._colors and level_hex_color:
parts.append(Text(level_text, style=level_hex_color))
else:
level_part = f"[{level_text:>8}]"
parts.append(level_part)
parts.append(Text(level_text))
elif log_level_style == "compact":
# 只显示首字母并着色
level_text = level.upper()[0]
if level_color:
level_part = f"{level_color}[{level_text:>8}]{RESET_COLOR}"
level_text = f"[{level.upper()[0]:>8}]"
if self._colors and level_hex_color:
parts.append(Text(level_text, style=level_hex_color))
else:
level_part = f"[{level_text:>8}]"
parts.append(level_part)
parts.append(Text(level_text))
# lite模式不显示级别只给时间戳着色
# 获取模块颜色用于full模式下的整体着色
module_color = ""
# 获取模块颜色
module_hex_color = ""
meta: dict[str, str | None] = {"alias": None, "color": None}
if logger_name:
meta = get_logger_meta(logger_name)
if self._colors and self._enable_module_colors and logger_name:
# 动态优先,其次默认表
meta = get_logger_meta(logger_name)
module_color = meta.get("color") or DEFAULT_MODULE_COLORS.get(logger_name, "")
module_hex_color = meta.get("color") or DEFAULT_MODULE_COLORS.get(logger_name, "")
# 模块名称(带颜色和别名支持)
if logger_name:
# 获取别名,如果没有别名则使用原名称
# 若上面条件不成立需要再次获取 meta
if "meta" not in locals():
meta = get_logger_meta(logger_name)
display_name = meta.get("alias") or DEFAULT_MODULE_ALIASES.get(logger_name, logger_name)
if self._colors and self._enable_module_colors:
if module_color:
module_part = f"{module_color}[{display_name}]{RESET_COLOR}"
else:
module_part = f"[{display_name}]"
module_text = f"[{display_name}]"
if self._colors and self._enable_module_colors and module_hex_color:
parts.append(Text(module_text, style=module_hex_color))
else:
module_part = f"[{display_name}]"
parts.append(module_part)
parts.append(Text(module_text))
# 消息内容(确保转换为字符串)
event_content = ""
@@ -873,38 +846,41 @@ class ModuleColoredConsoleRenderer:
# 其他类型直接转换为字符串
event_content = str(event)
# 在full模式下为消息内容着色
# 在 full 模式下为消息内容着色
if self._colors and self._enable_full_content_colors:
# 检查是否包含“内心思考:”
if "内心思考:" in event_content:
# 使用明亮的粉色
thought_color = "\033[38;5;218m"
# 分割消息内容
# 使用明亮的粉色用于“内心思考”段落
thought_hex_color = "#FFAFD7"
prefix, thought = event_content.split("内心思考:", 1)
# 前缀部分(“决定进行回复,”)使用模块颜色
if module_color:
prefix_colored = f"{module_color}{prefix.strip()}{RESET_COLOR}"
else:
prefix_colored = prefix.strip()
prefix = prefix.strip()
thought = thought.strip()
# “内心思考”部分换行并使用专属颜色
thought_colored = f"\n\n{thought_color}内心思考:{thought.strip()}{RESET_COLOR}\n"
# 组合为一个 Text避免 join 时插入多余空格
content_text = Text()
if prefix:
if module_hex_color:
content_text.append(prefix, style=module_hex_color)
else:
content_text.append(prefix)
# 重新组合
# parts.append(prefix_colored + thought_colored)
# 将前缀和思考内容作为独立的part添加避免它们之间出现多余的空格
if prefix_colored:
parts.append(prefix_colored)
parts.append(thought_colored)
# 与“内心思考”段落之间插入空行
if prefix:
content_text.append("\n\n")
elif module_color:
event_content = f"{module_color}{event_content}{RESET_COLOR}"
parts.append(event_content)
# “内心思考”标题+内容
content_text.append("内心思考:", style=thought_hex_color)
if thought:
content_text.append(thought, style=thought_hex_color)
parts.append(content_text)
else:
parts.append(event_content)
if module_hex_color:
parts.append(Text(event_content, style=module_hex_color))
else:
parts.append(Text(event_content))
else:
parts.append(event_content)
parts.append(Text(event_content))
# 处理其他字段
extras = []
@@ -921,15 +897,24 @@ class ModuleColoredConsoleRenderer:
# 在full模式下为额外字段着色
extra_field = f"{key}={value_str}"
if self._colors and self._enable_full_content_colors and module_color:
extra_field = f"{module_color}{extra_field}{RESET_COLOR}"
extras.append(extra_field)
# 在full模式下为额外字段着色
if self._colors and self._enable_full_content_colors and module_hex_color:
extras.append(Text(extra_field, style=module_hex_color))
else:
extras.append(Text(extra_field))
if extras:
parts.append(" ".join(extras))
parts.append(Text(" "))
parts.extend(extras)
return " ".join(parts)
# 使用 Rich 拼接并返回字符串
result = Text(" ").join(parts)
# 将 Rich Text 对象转换为带 ANSI 颜色码的字符串
from io import StringIO
string_io = StringIO()
temp_console = Console(file=string_io, force_terminal=True, color_system="truecolor", width=999)
temp_console.print(result, end="")
return string_io.getvalue()
# 配置标准logging以支持文件输出和压缩
@@ -945,8 +930,11 @@ logging.basicConfig(
)
def add_logger_metadata(logger: Any, method_name: str, event_dict: dict): # type: ignore[override]
"""structlog 自定义处理器: 注入 color / alias 字段 (用于 JSON 输出)。"""
def add_logger_metadata(logger: WrappedLogger, method_name: str, event_dict: EventDict) -> EventDict: # type: ignore[override]
"""structlog 自定义处理器: 注入 color / alias 字段 (用于 JSON 输出)。
color 使用 #RRGGBB 格式(已通过 _normalize_color 统一)。
"""
name = event_dict.get("logger_name")
if name:
meta = get_logger_meta(name)
@@ -955,7 +943,7 @@ def add_logger_metadata(logger: Any, method_name: str, event_dict: dict): # typ
meta["color"] = DEFAULT_MODULE_COLORS[name]
if meta.get("alias") is None and name in DEFAULT_MODULE_ALIASES:
meta["alias"] = DEFAULT_MODULE_ALIASES[name]
# 注入
# 注入 - color 已经是 #RRGGBB 格式
if meta.get("color"):
event_dict["color"] = meta["color"]
if meta.get("alias"):

View File

@@ -19,6 +19,9 @@ from src.chat.message_receive.chat_stream import get_chat_manager
from src.chat.utils.statistic import OnlineTimeRecordTask, StatisticOutputTask
from src.common.logger import get_logger
from src.common.message import get_global_api
# 全局背景任务集合
_background_tasks = set()
from src.common.remote import TelemetryHeartBeatTask
from src.common.server import Server, get_global_server
from src.config.config import global_config
@@ -461,7 +464,9 @@ MoFox_Bot(第三方修改版)
logger.info("情绪管理器初始化成功")
# 启动聊天管理器的自动保存任务
asyncio.create_task(get_chat_manager()._auto_save_task())
task = asyncio.create_task(get_chat_manager()._auto_save_task())
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
# 初始化增强记忆系统
if global_config.memory.enable_memory:

View File

@@ -1,5 +1,5 @@
from abc import abstractmethod
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar
from src.common.data_models.database_data_model import DatabaseMessages
from src.common.logger import get_logger
@@ -33,7 +33,7 @@ class BaseCommand(PlusCommand):
"""命令匹配的正则表达式"""
# 用于存储正则匹配组
matched_groups: dict[str, str] = {}
matched_groups: ClassVar[dict[str, str]] = {}
def __init__(self, message: DatabaseMessages, plugin_config: dict | None = None):
"""初始化Command组件"""

View File

@@ -14,6 +14,9 @@ from .component_registry import component_registry
logger = get_logger("plugin_manager")
# 全局背景任务集合
_background_tasks = set()
class PluginManager:
"""
@@ -142,7 +145,9 @@ class PluginManager:
logger.debug(f"为插件 '{plugin_name}' 调用 on_plugin_loaded 钩子")
try:
# 使用 asyncio.create_task 确保它不会阻塞加载流程
asyncio.create_task(plugin_instance.on_plugin_loaded())
task = asyncio.create_task(plugin_instance.on_plugin_loaded())
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
except Exception as e:
logger.error(f"调用插件 '{plugin_name}' 的 on_plugin_loaded 钩子时出错: {e}")

View File

@@ -14,6 +14,9 @@ from src.config.config import global_config
logger = get_logger("plan_executor")
# 全局背景任务集合
_background_tasks = set()
class ChatterPlanExecutor:
"""
@@ -89,7 +92,9 @@ class ChatterPlanExecutor:
# 将其他动作放入后台任务执行,避免阻塞主流程
if other_actions:
asyncio.create_task(self._execute_other_actions(other_actions, plan))
task = asyncio.create_task(self._execute_other_actions(other_actions, plan))
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
logger.info(f"已将 {len(other_actions)} 个其他动作放入后台任务执行。")
# 注意:后台任务的结果不会立即计入本次返回的统计数据

View File

@@ -254,15 +254,7 @@ class ChatterPlanFilter:
plan
)
actions_before_now = await get_actions_by_timestamp_with_chat(
chat_id=plan.chat_id,
timestamp_start=time.time() - 3600,
timestamp_end=time.time(),
limit=5,
)
actions_before_now_block = build_readable_actions(actions=actions_before_now)
actions_before_now_block = f"你刚刚选择并执行过的action是\n{actions_before_now_block}"
actions_before_now_block = ""
self.last_obs_time_mark = time.time()
@@ -285,6 +277,7 @@ class ChatterPlanFilter:
动作描述:不进行回复,等待合适的回复时机
- 当你刚刚发送了消息没有人回复时选择no_reply
- 当你一次发送了太多消息为了避免打扰聊天节奏选择no_reply
- 在认为对方话没有讲完的时候选择这个
{{
"action": "no_reply",
"reason":"不回复的原因"

View File

@@ -168,6 +168,9 @@ class ChatterActionPlanner:
action_modifier = ActionModifier(self.action_manager, self.chat_id)
await action_modifier.modify_actions()
# 在生成初始计划前,刷新缓存消息到未读列表
await self._flush_cached_messages_to_unread(context)
initial_plan = await self.generator.generate(chat_mode)
# 确保Plan中包含所有当前可用的动作
@@ -258,6 +261,9 @@ class ChatterActionPlanner:
# 重新运行主规划流程这次将正确使用Focus模式
return await self._enhanced_plan_flow(context)
try:
# Normal模式开始时刷新缓存消息到未读列表
await self._flush_cached_messages_to_unread(context)
unread_messages = context.get_unread_messages() if context else []
if not unread_messages:
@@ -459,6 +465,45 @@ class ChatterActionPlanner:
except Exception as e:
logger.warning(f"同步chat_mode到ChatStream失败: {e}")
async def _flush_cached_messages_to_unread(self, context: "StreamContext | None") -> list:
"""在planner开始时将缓存消息刷新到未读消息列表
此方法在动作修改器执行后、生成初始计划前调用,确保计划阶段能看到所有积累的消息。
Args:
context: 流上下文
Returns:
list: 刷新的消息列表
"""
if not context:
return []
try:
from src.chat.message_manager.message_manager import message_manager
stream_id = context.stream_id
if message_manager.is_running and message_manager.has_cached_messages(stream_id):
# 获取缓存消息
cached_messages = message_manager.flush_cached_messages(stream_id)
if cached_messages:
# 直接添加到上下文的未读消息列表
for message in cached_messages:
context.unread_messages.append(message)
logger.info(f"Planner开始前刷新缓存消息到未读列表: stream={stream_id}, 数量={len(cached_messages)}")
return cached_messages
return []
except ImportError:
logger.debug("MessageManager不可用跳过缓存刷新")
return []
except Exception as e:
logger.warning(f"Planner刷新缓存消息失败: error={e}")
return []
def _update_stats_from_execution_result(self, execution_result: dict[str, Any]):
"""根据执行结果更新规划器统计"""
if not execution_result:

View File

@@ -11,6 +11,9 @@ from src.plugin_system import BasePlugin, ComponentInfo, register_plugin
from src.plugin_system.base.component_types import PermissionNodeField
from src.plugin_system.base.config_types import ConfigField
# 全局背景任务集合
_background_tasks = set()
from .actions.read_feed_action import ReadFeedAction
from .actions.send_feed_action import SendFeedAction
from .commands.send_feed_command import SendFeedCommand
@@ -117,8 +120,14 @@ class MaiZoneRefactoredPlugin(BasePlugin):
logger.info("MaiZone重构版插件服务已注册。")
# --- 启动后台任务 ---
asyncio.create_task(scheduler_service.start())
asyncio.create_task(monitor_service.start())
task1 = asyncio.create_task(scheduler_service.start())
_background_tasks.add(task1)
task1.add_done_callback(_background_tasks.discard)
task2 = asyncio.create_task(monitor_service.start())
_background_tasks.add(task2)
task2.add_done_callback(_background_tasks.discard)
logger.info("MaiZone后台监控和定时任务已启动。")
def get_plugin_components(self) -> list[tuple[ComponentInfo, type]]:

View File

@@ -7,6 +7,7 @@ import base64
from collections.abc import Callable
from pathlib import Path
import aiofiles
import aiohttp
from src.common.logger import get_logger
@@ -86,8 +87,8 @@ class ImageService:
if b64_json:
image_bytes = base64.b64decode(b64_json)
file_path = Path(image_dir) / f"image_{i + 1}.png"
with open(file_path, "wb") as f:
f.write(image_bytes)
async with aiofiles.open(file_path, "wb") as f:
await f.write(image_bytes)
logger.info(f"成功保存AI图片到: {file_path}")
return True
else:

View File

@@ -12,6 +12,7 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any
import aiofiles
import aiohttp
import bs4
import json5
@@ -397,8 +398,8 @@ class QZoneService:
}
# 成功获取后,异步写入本地文件作为备份
try:
with open(cookie_file_path, "wb") as f:
f.write(orjson.dumps(parsed_cookies))
async with aiofiles.open(cookie_file_path, "wb") as f:
await f.write(orjson.dumps(parsed_cookies))
logger.info(f"通过Napcat服务成功更新Cookie并已保存至: {cookie_file_path}")
except Exception as e:
logger.warning(f"保存Cookie到文件时出错: {e}")
@@ -413,8 +414,9 @@ class QZoneService:
logger.info("尝试从本地Cookie文件加载...")
if cookie_file_path.exists():
try:
with open(cookie_file_path, "rb") as f:
cookies = orjson.loads(f.read())
async with aiofiles.open(cookie_file_path, "rb") as f:
content = await f.read()
cookies = orjson.loads(content)
logger.info(f"成功从本地文件加载Cookie: {cookie_file_path}")
return cookies
except Exception as e:

View File

@@ -13,6 +13,8 @@ logger = get_logger("stt_whisper_plugin")
# 全局变量来缓存模型,避免重复加载
_whisper_model = None
_is_loading = False
_model_ready_event = asyncio.Event()
_background_tasks = set() # 背景任务集合
class LocalASRTool(BaseTool):
"""
@@ -29,7 +31,7 @@ class LocalASRTool(BaseTool):
"""
一个类方法,用于在插件加载时触发一次模型加载。
"""
global _whisper_model, _is_loading
global _whisper_model, _is_loading, _model_ready_event
if _whisper_model is None and not _is_loading:
_is_loading = True
try:
@@ -47,6 +49,7 @@ class LocalASRTool(BaseTool):
_whisper_model = None
finally:
_is_loading = False
_model_ready_event.set() # 通知等待的任务
async def execute(self, function_args: dict) -> str:
audio_path = function_args.get("audio_path")
@@ -55,9 +58,9 @@ class LocalASRTool(BaseTool):
return "错误:缺少 audio_path 参数。"
global _whisper_model
# 增强的等待逻辑:只要模型还没准备好,就一直等待后台加载任务完成
while _is_loading:
await asyncio.sleep(0.2)
# 使用 Event 等待模型加载完成
if _is_loading:
await _model_ready_event.wait()
if _whisper_model is None:
return "Whisper 模型加载失败,无法识别语音。"
@@ -90,7 +93,9 @@ class STTWhisperPlugin(BasePlugin):
from src.config.config import global_config
if global_config.voice.asr_provider == "local":
# 使用 create_task 在后台开始加载,不阻塞主流程
asyncio.create_task(LocalASRTool.load_model_once(self.config or {}))
task = asyncio.create_task(LocalASRTool.load_model_once(self.config or {}))
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
except Exception as e:
logger.error(f"触发 Whisper 模型预加载时出错: {e}")