Merge branch 'dev' of https://github.com/MoFox-Studio/MoFox_Bot into dev
This commit is contained in:
@@ -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
8
bot.py
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
# 配置节描述
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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("测试字符串嵌入向量保存完成")
|
||||
|
||||
|
||||
@@ -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("未找到记忆构建模型配置")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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("海马体采样器未初始化,无法启动采样")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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迁移而来)
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""生成图表数据 (异步)"""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
from .decorators import (
|
||||
cached,
|
||||
db_operation,
|
||||
generate_cache_key,
|
||||
measure_time,
|
||||
retry,
|
||||
timeout,
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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组件"""
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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)} 个其他动作放入后台任务执行。")
|
||||
# 注意:后台任务的结果不会立即计入本次返回的统计数据
|
||||
|
||||
|
||||
@@ -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":"不回复的原因"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user