feat: Enhance emoji and image management with asynchronous background processing
- Added support for scheduling background tasks to build emoji and image descriptions when not found in cache. - Improved error handling and logging for emoji and image processing. - Updated `SessionMessage` processing to allow for optional heavy media analysis and voice transcription. - Refactored logging messages for better clarity and consistency across various modules. - Introduced a new function to build outbound log previews for messages, enhancing logging capabilities.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
你需要根据提供的参考信息,当前场景和输出规则来进行分析
|
||||
在当前场景中,用户正在与AI麦麦进行聊天互动,你的任务不是生成对用户可见的发言,而是进行分析来指导AI进行回复。
|
||||
“分析”应该体现你对当前局面的判断、你的建议、你的下一步计划,以及你为什么这样想。
|
||||
|
||||
没必要刻意友好回复,符合你的人格就行
|
||||
|
||||
你可以使用这些工具:
|
||||
- wait(seconds) - 暂时停止对话,等待(seconds)秒,把话语权交给用户,等待对方新的发言。
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
你的名字是麦麦,也有人叫你牢麦,麦叠,你是一个大二在读女大学生,现在正在上网和群友聊天,有点攻击性;
|
||||
你的任务是根据内部想法生成一条对用户可见的自然回复。
|
||||
|
||||
【参考信息】
|
||||
{bot_name}的人设:{identity}
|
||||
回复风格要求:{reply_style}
|
||||
【参考信息结束】
|
||||
|
||||
你正在群里聊天,现在请你读读之前的聊天记录,然后给出日常且口语化的回复,
|
||||
尽量简短一些。
|
||||
没必要刻意友好回复,符合你的人格就行。没必要刻意友好回复,符合你的人格就行。没必要刻意友好回复,符合你的人格就行。
|
||||
请注意把握聊天内容,不要回复的太有条理。
|
||||
你的风格平淡但不失讽刺,不过分兴奋,很简短。可以参考贴吧,知乎和微博的回复风格。很平淡和白话,不浮夸不长篇大论,b站评论风格,但一定注意不要过分修辞和复杂句。
|
||||
请注意不要输出多余内容(包括不必要的前后缀,冒号,括号,表情包,at或 @等 ),只输出发言内容就好。
|
||||
|
||||
@@ -189,7 +189,7 @@ def _run(non_interactive: bool = False) -> None: # sourcery skip: comprehension
|
||||
elif doc_item:
|
||||
with open_ie_doc_lock:
|
||||
open_ie_doc.append(doc_item)
|
||||
logger.info('已处理"%s"', doc_item.get("passage", ""))
|
||||
logger.info(f'已处理"{doc_item.get("passage", "")}"')
|
||||
progress.update(task, advance=1)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("\n接收到中断信号,正在优雅地关闭程序...")
|
||||
|
||||
@@ -110,7 +110,7 @@ def run_action(action: str, extra_args: Optional[List[str]] = None) -> None:
|
||||
这里不重复解析子参数,而是直接调用各脚本的 main(),
|
||||
让子脚本保留原有的交互/参数行为。
|
||||
"""
|
||||
logger.info("开始执行操作: %s", action)
|
||||
logger.info(f"开始执行操作: {action}")
|
||||
|
||||
extra_args = extra_args or []
|
||||
|
||||
@@ -162,14 +162,14 @@ def run_action(action: str, extra_args: Optional[List[str]] = None) -> None:
|
||||
_warn_if_lpmm_disabled()
|
||||
_with_overridden_argv(extra_args, refresh_lpmm_knowledge_main)
|
||||
else:
|
||||
logger.error("未知操作: %s", action)
|
||||
logger.error(f"未知操作: {action}")
|
||||
except KeyboardInterrupt:
|
||||
logger.info("用户中断当前操作(Ctrl+C)")
|
||||
except SystemExit:
|
||||
# 子脚本里大量使用 sys.exit,直接透传即可
|
||||
raise
|
||||
except Exception as exc: # pragma: no cover - 防御性兜底
|
||||
logger.error("执行操作 %s 时发生未捕获异常: %s", action, exc)
|
||||
logger.error(f"执行操作 {action} 时发生未捕获异常: {exc}")
|
||||
raise
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ def _run_embedding_helper() -> None:
|
||||
try:
|
||||
test_path.rename(archive_path)
|
||||
except Exception as exc: # pragma: no cover - 防御性兜底
|
||||
logger.error("归档 embedding_model_test.json 失败: %s", exc)
|
||||
logger.error(f"归档 embedding_model_test.json 失败: {exc}")
|
||||
print("[ERROR] 归档 embedding_model_test.json 失败,请检查文件权限与路径。错误详情已写入日志。")
|
||||
return
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from rich.traceback import install
|
||||
from sqlmodel import select
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
@@ -51,11 +52,13 @@ class EmojiManager:
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""初始化表情包管理器。"""
|
||||
_ensure_directories()
|
||||
|
||||
self._emoji_num: int = 0
|
||||
self.emojis: list[MaiEmoji] = []
|
||||
self.emojis: List[MaiEmoji] = []
|
||||
self._maintenance_wakeup_event: asyncio.Event = asyncio.Event()
|
||||
self._pending_description_tasks: Dict[str, asyncio.Task[None]] = {}
|
||||
self._reload_callback_registered: bool = False
|
||||
|
||||
config_manager.register_reload_callback(self.reload_runtime_config)
|
||||
@@ -78,7 +81,11 @@ class EmojiManager:
|
||||
logger.info("[关闭] Emoji 模块已注销配置热重载回调")
|
||||
|
||||
async def get_emoji_description(
|
||||
self, *, emoji_bytes: Optional[bytes] = None, emoji_hash: Optional[str] = None
|
||||
self,
|
||||
*,
|
||||
emoji_bytes: Optional[bytes] = None,
|
||||
emoji_hash: Optional[str] = None,
|
||||
wait_for_build: bool = True,
|
||||
) -> Optional[Tuple[str, List[str]]]:
|
||||
"""
|
||||
根据表情包哈希获取表情包描述和情感列表的封装方法
|
||||
@@ -86,6 +93,7 @@ class EmojiManager:
|
||||
Args:
|
||||
emoji_bytes (Optional[bytes]): 表情包的字节数据,如果提供了字节数据但数据库中没有找到对应记录,则会尝试构建表情包描述
|
||||
emoji_hash (Optional[str]): 表情包的哈希值,如果提供了哈希值则优先使用哈希值查找表情包描述
|
||||
wait_for_build (bool): 未命中缓存时是否同步等待描述构建完成
|
||||
Returns:
|
||||
return (Optional[Tuple[str, List[str]]]): 如果找到对应的表情包,则返回包含描述和情感标签的元组;若没找到,则尝试构建表情包描述并返回,如果构建失败则返回 None
|
||||
Raises:
|
||||
@@ -113,27 +121,88 @@ class EmojiManager:
|
||||
# 如果提供了字节数据但数据库中没有找到,尝试构建
|
||||
if not emoji_bytes:
|
||||
return None
|
||||
if not wait_for_build:
|
||||
self._schedule_description_build(emoji_hash, emoji_bytes)
|
||||
return None
|
||||
|
||||
# 找不到尝试构建
|
||||
return await self._build_and_cache_emoji_description(emoji_hash, emoji_bytes)
|
||||
|
||||
def _schedule_description_build(self, emoji_hash: str, emoji_bytes: bytes) -> None:
|
||||
"""调度表情包描述后台构建任务。
|
||||
|
||||
Args:
|
||||
emoji_hash: 表情包哈希值。
|
||||
emoji_bytes: 表情包字节数据。
|
||||
"""
|
||||
if emoji_hash in self._pending_description_tasks:
|
||||
return
|
||||
|
||||
task = asyncio.create_task(self._build_description_in_background(emoji_hash, emoji_bytes))
|
||||
self._pending_description_tasks[emoji_hash] = task
|
||||
task.add_done_callback(lambda finished_task: self._finalize_description_build(emoji_hash, finished_task))
|
||||
|
||||
async def _build_description_in_background(self, emoji_hash: str, emoji_bytes: bytes) -> None:
|
||||
"""在后台构建并缓存表情包描述。
|
||||
|
||||
Args:
|
||||
emoji_hash: 表情包哈希值。
|
||||
emoji_bytes: 表情包字节数据。
|
||||
"""
|
||||
try:
|
||||
logger.info(f"表情包描述后台构建已开始,哈希值: {emoji_hash}")
|
||||
await self._build_and_cache_emoji_description(emoji_hash, emoji_bytes)
|
||||
logger.info(f"表情包描述后台构建完成,哈希值: {emoji_hash}")
|
||||
except Exception as exc:
|
||||
logger.warning(f"表情包描述后台构建失败,哈希值: {emoji_hash},错误: {exc}")
|
||||
|
||||
def _finalize_description_build(self, emoji_hash: str, task: asyncio.Task[None]) -> None:
|
||||
"""回收表情包描述后台构建任务。
|
||||
|
||||
Args:
|
||||
emoji_hash: 表情包哈希值。
|
||||
task: 已完成的后台任务。
|
||||
"""
|
||||
self._pending_description_tasks.pop(emoji_hash, None)
|
||||
try:
|
||||
task.result()
|
||||
except Exception as exc:
|
||||
logger.debug(f"表情包描述后台任务结束时捕获异常,哈希值: {emoji_hash},错误: {exc}")
|
||||
|
||||
async def _build_and_cache_emoji_description(
|
||||
self,
|
||||
emoji_hash: str,
|
||||
emoji_bytes: bytes,
|
||||
) -> Optional[Tuple[str, List[str]]]:
|
||||
"""构建并缓存表情包描述与情感标签。
|
||||
|
||||
Args:
|
||||
emoji_hash: 表情包哈希值。
|
||||
emoji_bytes: 表情包字节数据。
|
||||
|
||||
Returns:
|
||||
Optional[Tuple[str, List[str]]]: 构建成功时返回描述和情感标签,否则返回 ``None``。
|
||||
"""
|
||||
logger.info(f"未找到哈希值为 {emoji_hash} 的表情包与其描述,尝试构建描述")
|
||||
full_path = EMOJI_DIR / f"{emoji_hash}.png"
|
||||
try:
|
||||
full_path.write_bytes(emoji_bytes)
|
||||
new_emoji = MaiEmoji(full_path=full_path, image_bytes=emoji_bytes)
|
||||
await new_emoji.calculate_hash_format()
|
||||
except Exception as e:
|
||||
logger.error(f"缓存表情包文件时出错: {e}")
|
||||
raise e
|
||||
except Exception as exc:
|
||||
logger.error(f"缓存表情包文件时出错: {exc}")
|
||||
raise exc
|
||||
|
||||
success_desc, new_emoji = await self.build_emoji_description(new_emoji)
|
||||
if not success_desc:
|
||||
logger.error("构建表情包描述失败")
|
||||
return None
|
||||
|
||||
success_emotion, new_emoji = await self.build_emoji_emotion(new_emoji)
|
||||
if not success_emotion:
|
||||
logger.error("构建表情包情感标签失败")
|
||||
return None
|
||||
|
||||
# 缓存结果到数据库
|
||||
with get_db_session() as session:
|
||||
try:
|
||||
image_record = new_emoji.to_db_instance()
|
||||
@@ -142,8 +211,8 @@ class EmojiManager:
|
||||
image_record.register_time = datetime.now()
|
||||
image_record.no_file_flag = True
|
||||
session.add(image_record)
|
||||
except Exception as e:
|
||||
logger.error(f"缓存表情包描述时出错: {e}")
|
||||
except Exception as exc:
|
||||
logger.error(f"缓存表情包描述时出错: {exc}")
|
||||
return new_emoji.description, new_emoji.emotion or []
|
||||
|
||||
def load_emojis_from_db(self) -> None:
|
||||
@@ -520,45 +589,56 @@ class EmojiManager:
|
||||
image_bytes = target_emoji.image_bytes or await asyncio.to_thread(
|
||||
target_emoji.read_image_bytes, target_emoji.full_path
|
||||
)
|
||||
image_base64 = ImageUtils.image_bytes_to_base64(image_bytes)
|
||||
try:
|
||||
if image_format == "gif":
|
||||
try:
|
||||
image_bytes = await asyncio.to_thread(ImageUtils.gif_2_static_image, image_bytes)
|
||||
except Exception as e:
|
||||
logger.error(f"[构建描述] 转换 GIF 图片时出错: {e}")
|
||||
return False, target_emoji
|
||||
prompt: str = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,简短描述一下表情包表达的情感和内容,从互联网梗、meme的角度去分析,精简回答"
|
||||
image_base64 = ImageUtils.image_bytes_to_base64(image_bytes)
|
||||
description_result = await emoji_manager_vlm.generate_response_for_image(
|
||||
prompt,
|
||||
image_base64,
|
||||
"jpg",
|
||||
options=LLMImageOptions(temperature=0.5),
|
||||
)
|
||||
description = description_result.response
|
||||
else:
|
||||
prompt: str = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,简短描述细节,从互联网梗、meme的角度去分析,精简回答"
|
||||
description_result = await emoji_manager_vlm.generate_response_for_image(
|
||||
prompt,
|
||||
image_base64,
|
||||
image_format,
|
||||
options=LLMImageOptions(temperature=0.5),
|
||||
)
|
||||
description = description_result.response
|
||||
except Exception as e:
|
||||
logger.error(f"[构建描述] 调用视觉模型生成表情包描述时出错: {e}")
|
||||
return False, target_emoji
|
||||
|
||||
if image_format == "gif":
|
||||
try:
|
||||
image_bytes = await asyncio.to_thread(ImageUtils.gif_2_static_image, image_bytes)
|
||||
except Exception as e:
|
||||
logger.error(f"[构建描述] 转换 GIF 图片时出错: {e}")
|
||||
return False, target_emoji
|
||||
prompt: str = "这是一个动态图表情包,每一张图代表了动态图的某一帧,黑色背景代表透明,简短描述一下表情包表达的情感和内容,从互联网梗、meme的角度去分析,精简回答"
|
||||
image_base64 = ImageUtils.image_bytes_to_base64(image_bytes)
|
||||
description_result = await emoji_manager_vlm.generate_response_for_image(
|
||||
prompt,
|
||||
image_base64,
|
||||
"jpg",
|
||||
options=LLMImageOptions(temperature=0.5),
|
||||
)
|
||||
description = description_result.response
|
||||
else:
|
||||
prompt: str = "这是一个表情包,请详细描述一下表情包所表达的情感和内容,简短描述细节,从互联网梗、meme的角度去分析,精简回答"
|
||||
image_base64 = ImageUtils.image_bytes_to_base64(image_bytes)
|
||||
description_result = await emoji_manager_vlm.generate_response_for_image(
|
||||
prompt,
|
||||
image_base64,
|
||||
image_format,
|
||||
options=LLMImageOptions(temperature=0.5),
|
||||
)
|
||||
description = description_result.response
|
||||
if not description:
|
||||
logger.warning(f"[构建描述] 视觉模型返回空描述,跳过注册: {target_emoji.file_name}")
|
||||
return False, target_emoji
|
||||
|
||||
# 表情包审查
|
||||
if global_config.emoji.content_filtration:
|
||||
filtration_prompt_template = prompt_manager.get_prompt("emoji_content_filtration")
|
||||
filtration_prompt_template.add_context("demand", global_config.emoji.filtration_prompt)
|
||||
filtration_prompt = await prompt_manager.render_prompt(filtration_prompt_template)
|
||||
filtration_result = await emoji_manager_vlm.generate_response_for_image(
|
||||
filtration_prompt,
|
||||
image_base64,
|
||||
image_format,
|
||||
options=LLMImageOptions(temperature=0.3),
|
||||
)
|
||||
llm_response = filtration_result.response
|
||||
try:
|
||||
filtration_prompt_template = prompt_manager.get_prompt("emoji_content_filtration")
|
||||
filtration_prompt_template.add_context("demand", global_config.emoji.filtration_prompt)
|
||||
filtration_prompt = await prompt_manager.render_prompt(filtration_prompt_template)
|
||||
filtration_result = await emoji_manager_vlm.generate_response_for_image(
|
||||
filtration_prompt,
|
||||
image_base64,
|
||||
image_format,
|
||||
options=LLMImageOptions(temperature=0.3),
|
||||
)
|
||||
llm_response = filtration_result.response
|
||||
except Exception as e:
|
||||
logger.error(f"[表情包审查] 调用视觉模型审查表情包时出错: {e}")
|
||||
return False, target_emoji
|
||||
if "否" in llm_response:
|
||||
logger.warning(f"[表情包审查] 表情包内容不符合要求,拒绝注册: {target_emoji.file_name}")
|
||||
return False, target_emoji
|
||||
@@ -584,11 +664,19 @@ class EmojiManager:
|
||||
emotion_prompt_template.add_context("description", target_emoji.description)
|
||||
emotion_prompt = await prompt_manager.render_prompt(emotion_prompt_template)
|
||||
# 调用LLM生成情感标签
|
||||
emotion_generation_result = await emoji_manager_emotion_judge_llm.generate_response(
|
||||
emotion_prompt,
|
||||
options=LLMGenerationOptions(temperature=0.3, max_tokens=200),
|
||||
)
|
||||
emotion_result = emotion_generation_result.response
|
||||
try:
|
||||
emotion_generation_result = await emoji_manager_emotion_judge_llm.generate_response(
|
||||
emotion_prompt,
|
||||
options=LLMGenerationOptions(temperature=0.3, max_tokens=200),
|
||||
)
|
||||
emotion_result = emotion_generation_result.response
|
||||
except Exception as e:
|
||||
logger.error(f"[构建情感标签] 调用模型生成情感标签时出错: {e}")
|
||||
return False, target_emoji
|
||||
|
||||
if not emotion_result:
|
||||
logger.warning(f"[构建情感标签] 情感标签结果为空,跳过注册: {target_emoji.file_name}")
|
||||
return False, target_emoji
|
||||
|
||||
# 解析情感标签结果
|
||||
emotions = [e.strip() for e in emotion_result.replace(",", ",").split(",") if e.strip()]
|
||||
@@ -670,7 +758,12 @@ class EmojiManager:
|
||||
for emoji_file in EMOJI_DIR.iterdir():
|
||||
if not emoji_file.is_file():
|
||||
continue
|
||||
if await self.register_emoji_by_filename(emoji_file):
|
||||
try:
|
||||
register_success = await self.register_emoji_by_filename(emoji_file)
|
||||
except Exception as e:
|
||||
logger.error(f"[定期维护] 注册表情包 {emoji_file.name} 时发生未处理异常: {e}")
|
||||
register_success = False
|
||||
if register_success:
|
||||
break # 每次只注册一个表情包
|
||||
try:
|
||||
emoji_file.unlink()
|
||||
|
||||
@@ -2,39 +2,39 @@ from typing import Dict
|
||||
|
||||
import traceback
|
||||
|
||||
from src.chat.heart_flow.heartFC_chat import HeartFChatting
|
||||
from src.chat.message_receive.chat_manager import chat_manager
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.maisaka.runtime import MaisakaHeartFlowChatting
|
||||
# from src.chat.brain_chat.brain_chat import BrainChatting
|
||||
|
||||
logger = get_logger("heartflow")
|
||||
|
||||
|
||||
# TODO: 恢复PFC,现在暂时禁用
|
||||
class HeartflowManager:
|
||||
"""主心流协调器,负责初始化并协调聊天,控制聊天属性"""
|
||||
"""主心流协调器。
|
||||
|
||||
def __init__(self):
|
||||
# self.heartflow_chat_list: Dict[str, HeartFChatting | BrainChatting] = {}
|
||||
self.heartflow_chat_list: Dict[str, HeartFChatting | MaisakaHeartFlowChatting] = {}
|
||||
当前群聊统一使用 Maisaka runtime 作为消息核心循环实现。
|
||||
"""
|
||||
|
||||
async def get_or_create_heartflow_chat(self, session_id: str): # -> Optional[HeartFChatting | BrainChatting]:
|
||||
"""获取或创建一个新的HeartFChatting实例"""
|
||||
def __init__(self) -> None:
|
||||
"""初始化心流聊天实例缓存。"""
|
||||
self.heartflow_chat_list: Dict[str, MaisakaHeartFlowChatting] = {}
|
||||
|
||||
async def get_or_create_heartflow_chat(self, session_id: str) -> MaisakaHeartFlowChatting:
|
||||
"""获取或创建群聊心流实例。
|
||||
|
||||
Args:
|
||||
session_id: 聊天会话 ID。
|
||||
|
||||
Returns:
|
||||
MaisakaHeartFlowChatting: 当前会话绑定的 Maisaka runtime。
|
||||
"""
|
||||
try:
|
||||
if chat := self.heartflow_chat_list.get(session_id):
|
||||
return chat
|
||||
chat_session = chat_manager.get_session_by_session_id(session_id)
|
||||
if not chat_session:
|
||||
raise ValueError(f"未找到 session_id={session_id} 的聊天流")
|
||||
# new_chat = (
|
||||
# HeartFChatting(session_id=session_id) if chat_session.group_id else BrainChatting(session_id=session_id)
|
||||
# )
|
||||
if global_config.maisaka.take_over_hfc:
|
||||
new_chat = MaisakaHeartFlowChatting(session_id=session_id)
|
||||
else:
|
||||
new_chat = HeartFChatting(session_id=session_id)
|
||||
new_chat = MaisakaHeartFlowChatting(session_id=session_id)
|
||||
await new_chat.start()
|
||||
self.heartflow_chat_list[session_id] = new_chat
|
||||
return new_chat
|
||||
@@ -43,10 +43,15 @@ class HeartflowManager:
|
||||
traceback.print_exc()
|
||||
raise e
|
||||
|
||||
def adjust_talk_frequency(self, session_id: str, frequency: float):
|
||||
"""调整指定聊天流的说话频率"""
|
||||
def adjust_talk_frequency(self, session_id: str, frequency: float) -> None:
|
||||
"""调整指定聊天流的说话频率。
|
||||
|
||||
Args:
|
||||
session_id: 聊天会话 ID。
|
||||
frequency: 目标频率系数。
|
||||
"""
|
||||
chat = self.heartflow_chat_list.get(session_id)
|
||||
if chat and hasattr(chat, "adjust_talk_frequency"):
|
||||
if chat:
|
||||
chat.adjust_talk_frequency(frequency)
|
||||
logger.info(f"已调整聊天 {session_id} 的说话频率为 {frequency}")
|
||||
else:
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from rich.traceback import install
|
||||
from sqlmodel import select
|
||||
from typing import Optional
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
|
||||
@@ -24,7 +26,8 @@ IMAGE_DIR = DATA_DIR / "images"
|
||||
logger = get_logger("image")
|
||||
|
||||
|
||||
def _ensure_image_dir_exists():
|
||||
def _ensure_image_dir_exists() -> None:
|
||||
"""确保图片缓存目录存在。"""
|
||||
IMAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
@@ -32,13 +35,21 @@ vlm = LLMServiceClient(task_name="vlm", request_type="image")
|
||||
|
||||
|
||||
class ImageManager:
|
||||
def __init__(self):
|
||||
"""图片描述管理器。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""初始化图片管理器。"""
|
||||
_ensure_image_dir_exists()
|
||||
self._pending_description_tasks: Dict[str, asyncio.Task[None]] = {}
|
||||
|
||||
logger.info("图片管理器初始化完成")
|
||||
|
||||
async def get_image_description(
|
||||
self, *, image_hash: Optional[str] = None, image_bytes: Optional[bytes] = None
|
||||
self,
|
||||
*,
|
||||
image_hash: Optional[str] = None,
|
||||
image_bytes: Optional[bytes] = None,
|
||||
wait_for_build: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
获取图片描述的封装方法
|
||||
@@ -50,6 +61,7 @@ class ImageManager:
|
||||
Args:
|
||||
image_hash (Optional[str]): 图片的哈希值,如果提供则优先使用该
|
||||
image_bytes (Optional[bytes]): 图片的字节数据,如果提供则在数据库中找不到哈希值时使用该数据生成描述
|
||||
wait_for_build (bool): 未命中缓存时是否同步等待描述构建完成
|
||||
Returns:
|
||||
return (str): 图片描述,如果发生错误或无法生成描述则返回空字符串
|
||||
Raises:
|
||||
@@ -74,6 +86,9 @@ class ImageManager:
|
||||
if not image_bytes:
|
||||
logger.warning("图片哈希值未找到,且未提供图片字节数据,返回无描述")
|
||||
return ""
|
||||
if not wait_for_build:
|
||||
self._schedule_description_build(hash_str, image_bytes)
|
||||
return ""
|
||||
logger.info(f"图片描述未找到,哈希值: {hash_str},准备生成新描述")
|
||||
try:
|
||||
image = await self.save_image_and_process(image_bytes)
|
||||
@@ -82,6 +97,47 @@ class ImageManager:
|
||||
logger.error(f"生成图片描述时发生错误: {e}")
|
||||
return ""
|
||||
|
||||
def _schedule_description_build(self, image_hash: str, image_bytes: bytes) -> None:
|
||||
"""调度图片描述后台构建任务。
|
||||
|
||||
Args:
|
||||
image_hash: 图片哈希值。
|
||||
image_bytes: 图片字节数据。
|
||||
"""
|
||||
if image_hash in self._pending_description_tasks:
|
||||
return
|
||||
|
||||
task = asyncio.create_task(self._build_description_in_background(image_hash, image_bytes))
|
||||
self._pending_description_tasks[image_hash] = task
|
||||
task.add_done_callback(lambda finished_task: self._finalize_description_build(image_hash, finished_task))
|
||||
|
||||
async def _build_description_in_background(self, image_hash: str, image_bytes: bytes) -> None:
|
||||
"""在后台构建并缓存图片描述。
|
||||
|
||||
Args:
|
||||
image_hash: 图片哈希值。
|
||||
image_bytes: 图片字节数据。
|
||||
"""
|
||||
try:
|
||||
logger.info(f"图片描述后台构建已开始,哈希值: {image_hash}")
|
||||
await self.save_image_and_process(image_bytes)
|
||||
logger.info(f"图片描述后台构建完成,哈希值: {image_hash}")
|
||||
except Exception as exc:
|
||||
logger.warning(f"图片描述后台构建失败,哈希值: {image_hash},错误: {exc}")
|
||||
|
||||
def _finalize_description_build(self, image_hash: str, task: asyncio.Task[None]) -> None:
|
||||
"""回收图片描述后台构建任务。
|
||||
|
||||
Args:
|
||||
image_hash: 图片哈希值。
|
||||
task: 已完成的后台任务。
|
||||
"""
|
||||
self._pending_description_tasks.pop(image_hash, None)
|
||||
try:
|
||||
task.result()
|
||||
except Exception as exc:
|
||||
logger.debug(f"图片描述后台任务结束时捕获异常,哈希值: {image_hash},错误: {exc}")
|
||||
|
||||
def get_image_from_db(self, image_hash: str) -> Optional[MaiImage]:
|
||||
"""
|
||||
从数据库中根据图片哈希值获取图片记录
|
||||
|
||||
@@ -303,9 +303,13 @@ class ChatBot:
|
||||
# pass
|
||||
|
||||
# 处理消息内容,识别表情包等二进制数据并转化为文本描述
|
||||
if global_config.maisaka.take_over_hfc and global_config.maisaka.direct_image_input:
|
||||
if group_info is not None and global_config.maisaka.direct_image_input:
|
||||
message.maisaka_original_raw_message = deepcopy(message.raw_message) # type: ignore[attr-defined]
|
||||
await message.process()
|
||||
# 入站主链优先保证消息尽快入队,避免图片、表情包、语音分析阻塞适配器超时。
|
||||
await message.process(
|
||||
enable_heavy_media_analysis=False,
|
||||
enable_voice_transcription=False,
|
||||
)
|
||||
|
||||
# 平台层的 @ 检测由底层 is_mentioned_bot_in_message 统一处理;此处不做用户名硬编码匹配
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from asyncio import Task
|
||||
from typing import Dict, List, Sequence, Tuple
|
||||
|
||||
from rich.traceback import install
|
||||
from sqlmodel import select
|
||||
from typing import List, Dict, Tuple, Sequence
|
||||
|
||||
import asyncio
|
||||
|
||||
@@ -27,14 +28,36 @@ logger = get_logger("chat_message")
|
||||
|
||||
|
||||
class MsgIDMapping:
|
||||
def __init__(self):
|
||||
self.mapping: Dict[str, Tuple[str | Task, UserInfo]] = {}
|
||||
"""回复消息内容缓存。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""初始化消息 ID 到内容的映射缓存。"""
|
||||
self.mapping: Dict[str, Tuple[str | Task[str], UserInfo]] = {}
|
||||
|
||||
|
||||
class SessionMessage(MaiMessage):
|
||||
async def process(self):
|
||||
"""处理消息内容,识别消息内容并转化为文本(会修改消息组件属性)"""
|
||||
tasks = [self.process_single_component(component, MsgIDMapping()) for component in self.raw_message.components]
|
||||
async def process(
|
||||
self,
|
||||
*,
|
||||
enable_heavy_media_analysis: bool = True,
|
||||
enable_voice_transcription: bool = True,
|
||||
) -> None:
|
||||
"""处理消息内容并转化为纯文本。
|
||||
|
||||
Args:
|
||||
enable_heavy_media_analysis: 是否同步执行图片与表情包描述生成。
|
||||
enable_voice_transcription: 是否同步执行语音转写。
|
||||
"""
|
||||
id_content_map = MsgIDMapping()
|
||||
tasks = [
|
||||
self.process_single_component(
|
||||
component,
|
||||
id_content_map,
|
||||
enable_heavy_media_analysis=enable_heavy_media_analysis,
|
||||
enable_voice_transcription=enable_voice_transcription,
|
||||
)
|
||||
for component in self.raw_message.components
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
processed_texts: List[str] = []
|
||||
for result in results:
|
||||
@@ -45,50 +68,116 @@ class SessionMessage(MaiMessage):
|
||||
self.processed_plain_text = " ".join(processed_texts)
|
||||
|
||||
async def process_single_component(
|
||||
self, component: StandardMessageComponents, id_content_map: MsgIDMapping, recursion_depth: int = 0
|
||||
self,
|
||||
component: StandardMessageComponents,
|
||||
id_content_map: MsgIDMapping,
|
||||
recursion_depth: int = 0,
|
||||
*,
|
||||
enable_heavy_media_analysis: bool = True,
|
||||
enable_voice_transcription: bool = True,
|
||||
) -> str:
|
||||
"""按照类型处理单个消息组件,返回处理后的文本内容(会修改消息组件属性)"""
|
||||
"""按类型处理单个消息组件。
|
||||
|
||||
Args:
|
||||
component: 待处理的消息组件。
|
||||
id_content_map: 回复消息解析缓存。
|
||||
recursion_depth: 当前递归深度。
|
||||
enable_heavy_media_analysis: 是否同步执行图片与表情包描述生成。
|
||||
enable_voice_transcription: 是否同步执行语音转写。
|
||||
|
||||
Returns:
|
||||
str: 组件对应的文本表示。
|
||||
"""
|
||||
if isinstance(component, TextComponent):
|
||||
return component.text
|
||||
elif isinstance(component, ImageComponent):
|
||||
return await self.process_image_component(component)
|
||||
return await self.process_image_component(
|
||||
component,
|
||||
enable_heavy_media_analysis=enable_heavy_media_analysis,
|
||||
)
|
||||
elif isinstance(component, EmojiComponent):
|
||||
return await self.process_emoji_component(component)
|
||||
return await self.process_emoji_component(
|
||||
component,
|
||||
enable_heavy_media_analysis=enable_heavy_media_analysis,
|
||||
)
|
||||
elif isinstance(component, AtComponent):
|
||||
return await self.process_at_component(component)
|
||||
elif isinstance(component, VoiceComponent):
|
||||
return await self.process_voice_component(component)
|
||||
return await self.process_voice_component(
|
||||
component,
|
||||
enable_voice_transcription=enable_voice_transcription,
|
||||
)
|
||||
elif isinstance(component, ReplyComponent):
|
||||
return await self.process_reply_component(component, id_content_map)
|
||||
elif isinstance(component, ForwardNodeComponent):
|
||||
return await self.process_forward_component(component, id_content_map, recursion_depth=recursion_depth + 1)
|
||||
return await self.process_forward_component(
|
||||
component,
|
||||
id_content_map,
|
||||
recursion_depth=recursion_depth + 1,
|
||||
enable_heavy_media_analysis=enable_heavy_media_analysis,
|
||||
enable_voice_transcription=enable_voice_transcription,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"暂时不支持的消息组件类型: {type(component)}")
|
||||
|
||||
async def process_image_component(self, component: ImageComponent) -> str:
|
||||
async def process_image_component(
|
||||
self,
|
||||
component: ImageComponent,
|
||||
*,
|
||||
enable_heavy_media_analysis: bool = True,
|
||||
) -> str:
|
||||
"""处理图片组件。
|
||||
|
||||
Args:
|
||||
component: 图片组件。
|
||||
enable_heavy_media_analysis: 是否同步执行图片描述生成。
|
||||
|
||||
Returns:
|
||||
str: 图片组件对应的文本表示。
|
||||
"""
|
||||
if component.content: # 先检查是否处理过
|
||||
return component.content
|
||||
from src.chat.image_system.image_manager import image_manager
|
||||
|
||||
# 获取描述
|
||||
try:
|
||||
desc = await image_manager.get_image_description(image_bytes=component.binary_data)
|
||||
desc = await image_manager.get_image_description(
|
||||
image_bytes=component.binary_data,
|
||||
wait_for_build=enable_heavy_media_analysis,
|
||||
)
|
||||
except Exception:
|
||||
desc = None # 失败置空
|
||||
|
||||
content = f"[图片:{desc}]" if desc else "[一张图片,网卡了加载不出来]"
|
||||
content = f"[图片:{desc}]" if desc else "[图片]"
|
||||
component.content = content
|
||||
component.binary_data = b"" # 处理完就丢掉二进制数据,节省内存
|
||||
return content
|
||||
|
||||
async def process_emoji_component(self, component: EmojiComponent) -> str:
|
||||
async def process_emoji_component(
|
||||
self,
|
||||
component: EmojiComponent,
|
||||
*,
|
||||
enable_heavy_media_analysis: bool = True,
|
||||
) -> str:
|
||||
"""处理表情包组件。
|
||||
|
||||
Args:
|
||||
component: 表情包组件。
|
||||
enable_heavy_media_analysis: 是否同步执行表情包描述生成。
|
||||
|
||||
Returns:
|
||||
str: 表情包组件对应的文本表示。
|
||||
"""
|
||||
if component.content: # 先检查是否处理过
|
||||
return component.content
|
||||
from src.chat.emoji_system.emoji_manager import emoji_manager
|
||||
|
||||
# 获取表情包描述
|
||||
try:
|
||||
tuple_content = await emoji_manager.get_emoji_description(emoji_bytes=component.binary_data)
|
||||
tuple_content = await emoji_manager.get_emoji_description(
|
||||
emoji_bytes=component.binary_data,
|
||||
wait_for_build=enable_heavy_media_analysis,
|
||||
)
|
||||
except Exception:
|
||||
tuple_content = None # 失败置空
|
||||
|
||||
@@ -96,7 +185,7 @@ class SessionMessage(MaiMessage):
|
||||
desc, _ = tuple_content
|
||||
content = f"[表情包: {desc}]"
|
||||
else:
|
||||
content = "[一个表情,网卡了加载不出来]"
|
||||
content = "[表情包]"
|
||||
component.content = content
|
||||
component.binary_data = b"" # 处理完就丢掉二进制数据,节省内存
|
||||
return content
|
||||
@@ -124,9 +213,26 @@ class SessionMessage(MaiMessage):
|
||||
else: # 最后使用用户ID
|
||||
return f"@{component.target_user_id}"
|
||||
|
||||
async def process_voice_component(self, component: VoiceComponent) -> str:
|
||||
async def process_voice_component(
|
||||
self,
|
||||
component: VoiceComponent,
|
||||
*,
|
||||
enable_voice_transcription: bool = True,
|
||||
) -> str:
|
||||
"""处理语音组件。
|
||||
|
||||
Args:
|
||||
component: 语音组件。
|
||||
enable_voice_transcription: 是否同步执行语音转写。
|
||||
|
||||
Returns:
|
||||
str: 语音组件对应的文本表示。
|
||||
"""
|
||||
if component.content: # 先检查是否处理过
|
||||
return component.content
|
||||
if not enable_voice_transcription:
|
||||
component.content = "[语音消息]"
|
||||
return component.content
|
||||
from src.common.utils.utils_voice import get_voice_text
|
||||
|
||||
text = await get_voice_text(component.binary_data)
|
||||
@@ -169,13 +275,37 @@ class SessionMessage(MaiMessage):
|
||||
return "[回复了一条消息,但原消息已无法访问]"
|
||||
|
||||
async def process_forward_component(
|
||||
self, component: ForwardNodeComponent, id_content_map: MsgIDMapping, recursion_depth: int = 0
|
||||
self,
|
||||
component: ForwardNodeComponent,
|
||||
id_content_map: MsgIDMapping,
|
||||
recursion_depth: int = 0,
|
||||
*,
|
||||
enable_heavy_media_analysis: bool = True,
|
||||
enable_voice_transcription: bool = True,
|
||||
) -> str:
|
||||
"""处理合并转发组件。
|
||||
|
||||
Args:
|
||||
component: 合并转发组件。
|
||||
id_content_map: 回复消息解析缓存。
|
||||
recursion_depth: 当前递归深度。
|
||||
enable_heavy_media_analysis: 是否同步执行图片与表情包描述生成。
|
||||
enable_voice_transcription: 是否同步执行语音转写。
|
||||
|
||||
Returns:
|
||||
str: 合并转发组件对应的文本表示。
|
||||
"""
|
||||
task_list: List[Task] = []
|
||||
node_user_info_list: List[UserInfo] = []
|
||||
for node in component.forward_components:
|
||||
task = asyncio.create_task(
|
||||
self._process_multiple_components(node.content, id_content_map, recursion_depth + 1)
|
||||
self._process_multiple_components(
|
||||
node.content,
|
||||
id_content_map,
|
||||
recursion_depth + 1,
|
||||
enable_heavy_media_analysis=enable_heavy_media_analysis,
|
||||
enable_voice_transcription=enable_voice_transcription,
|
||||
)
|
||||
)
|
||||
node_user_info = UserInfo(node.user_id or "未知用户", node.user_nickname, node.user_cardname)
|
||||
# 传入ID缓存映射,方便Reply组件获取并等待处理结果
|
||||
@@ -196,9 +326,36 @@ class SessionMessage(MaiMessage):
|
||||
return "【合并转发消息: \n" + "\n".join(forward_texts) + "\n】"
|
||||
|
||||
async def _process_multiple_components(
|
||||
self, components: Sequence[StandardMessageComponents], id_content_map: MsgIDMapping, recursion_depth: int = 0
|
||||
self,
|
||||
components: Sequence[StandardMessageComponents],
|
||||
id_content_map: MsgIDMapping,
|
||||
recursion_depth: int = 0,
|
||||
*,
|
||||
enable_heavy_media_analysis: bool = True,
|
||||
enable_voice_transcription: bool = True,
|
||||
) -> str:
|
||||
tasks = [self.process_single_component(component, id_content_map, recursion_depth) for component in components]
|
||||
"""并行处理多个消息组件。
|
||||
|
||||
Args:
|
||||
components: 待处理的组件序列。
|
||||
id_content_map: 回复消息解析缓存。
|
||||
recursion_depth: 当前递归深度。
|
||||
enable_heavy_media_analysis: 是否同步执行图片与表情包描述生成。
|
||||
enable_voice_transcription: 是否同步执行语音转写。
|
||||
|
||||
Returns:
|
||||
str: 多个组件拼接后的文本表示。
|
||||
"""
|
||||
tasks = [
|
||||
self.process_single_component(
|
||||
component,
|
||||
id_content_map,
|
||||
recursion_depth,
|
||||
enable_heavy_media_analysis=enable_heavy_media_analysis,
|
||||
enable_voice_transcription=enable_voice_transcription,
|
||||
)
|
||||
for component in components
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True) # 并行处理多个组件
|
||||
processed_texts: List[str] = []
|
||||
for result in results:
|
||||
|
||||
@@ -46,9 +46,7 @@ class I18nManager:
|
||||
self._log_once(
|
||||
("invalid_env_locale", "env", env_locale),
|
||||
logging.WARNING,
|
||||
"检测到非法 MAIBOT_LOCALE=%s,已回退到默认 locale %s",
|
||||
env_locale,
|
||||
self._default_locale,
|
||||
f"检测到非法 MAIBOT_LOCALE={env_locale},已回退到默认 locale {self._default_locale}",
|
||||
)
|
||||
return self._default_locale
|
||||
|
||||
@@ -84,15 +82,14 @@ class I18nManager:
|
||||
self._log_once(
|
||||
("non_plural_key", translation_locale, key),
|
||||
logging.WARNING,
|
||||
"翻译 key '%s' 不是 plural 节点,已回退到普通 t()",
|
||||
key,
|
||||
f"翻译 key '{key}' 不是 plural 节点,已回退到普通 t()",
|
||||
)
|
||||
return self.t(key, locale=translation_locale, count=count, **kwargs)
|
||||
|
||||
try:
|
||||
plural_category = select_plural_category(translation_locale, count)
|
||||
except Exception as exc:
|
||||
logger.warning("为 key '%s' 选择 plural category 失败: %s,已回退到 other", key, exc)
|
||||
logger.warning(f"为 key '{key}' 选择 plural category 失败: {exc},已回退到 other")
|
||||
plural_category = "other"
|
||||
|
||||
template = translation_value.get(plural_category) or translation_value.get("other")
|
||||
@@ -100,8 +97,7 @@ class I18nManager:
|
||||
self._log_once(
|
||||
("plural_missing_template", translation_locale, key),
|
||||
logging.WARNING,
|
||||
"翻译 key '%s' 缺少 plural 模板,已回退到 key 本身",
|
||||
key,
|
||||
f"翻译 key '{key}' 缺少 plural 模板,已回退到 key 本身",
|
||||
)
|
||||
return key
|
||||
|
||||
@@ -125,8 +121,7 @@ class I18nManager:
|
||||
self._log_once(
|
||||
("plural_missing_other", translation_locale, key),
|
||||
logging.WARNING,
|
||||
"翻译 key '%s' 缺少 other plural category,已回退到 key 本身",
|
||||
key,
|
||||
f"翻译 key '{key}' 缺少 other plural category,已回退到 key 本身",
|
||||
)
|
||||
return template
|
||||
|
||||
@@ -134,7 +129,7 @@ class I18nManager:
|
||||
try:
|
||||
return format_template(template, **kwargs)
|
||||
except Exception as exc:
|
||||
logger.error("翻译 key '%s' 格式化失败: %s", key, exc)
|
||||
logger.error(f"翻译 key '{key}' 格式化失败: {exc}")
|
||||
return template
|
||||
|
||||
def _get_translation_value(self, key: str, locale: str | None) -> tuple[TranslationValue | None, str]:
|
||||
@@ -149,20 +144,15 @@ class I18nManager:
|
||||
self._log_once(
|
||||
("missing_key_fallback", target_locale, key),
|
||||
logging.WARNING,
|
||||
"翻译 key '%s' 在 locale '%s' 中缺失,已回退到默认 locale '%s'",
|
||||
key,
|
||||
target_locale,
|
||||
self._default_locale,
|
||||
f"翻译 key '{key}' 在 locale '{target_locale}' 中缺失,"
|
||||
f"已回退到默认 locale '{self._default_locale}'",
|
||||
)
|
||||
return default_catalog[key], self._default_locale
|
||||
|
||||
self._log_once(
|
||||
("missing_key", target_locale, key),
|
||||
logging.WARNING,
|
||||
"翻译 key '%s' 缺失,locale='%s',默认 locale='%s'",
|
||||
key,
|
||||
target_locale,
|
||||
self._default_locale,
|
||||
f"翻译 key '{key}' 缺失,locale='{target_locale}',默认 locale='{self._default_locale}'",
|
||||
)
|
||||
return None, target_locale
|
||||
|
||||
@@ -177,9 +167,7 @@ class I18nManager:
|
||||
self._log_once(
|
||||
("invalid_locale", "explicit", locale),
|
||||
logging.WARNING,
|
||||
"检测到非法 locale='%s',已回退到当前默认 locale %s",
|
||||
locale,
|
||||
current_locale,
|
||||
f"检测到非法 locale='{locale}',已回退到当前默认 locale {current_locale}",
|
||||
)
|
||||
return current_locale
|
||||
|
||||
@@ -195,9 +183,7 @@ class I18nManager:
|
||||
self._log_once(
|
||||
("load_failed", normalized_locale, exc.__class__.__name__),
|
||||
logging.WARNING,
|
||||
"加载 locale '%s' 失败: %s",
|
||||
normalized_locale,
|
||||
exc,
|
||||
f"加载 locale '{normalized_locale}' 失败: {exc}",
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ def _format_prompt_template(name: str, template: str, **kwargs: object) -> str:
|
||||
error = KeyError(t("prompt.missing_placeholder", name=name, placeholder=missing_placeholder))
|
||||
if is_strict_prompt_i18n_mode():
|
||||
raise error from exc
|
||||
logger.error("%s", error)
|
||||
logger.error(f"{error}")
|
||||
return template
|
||||
except Exception as exc:
|
||||
logger.error(t("prompt.format_failed", name=name, error=exc))
|
||||
|
||||
@@ -627,7 +627,7 @@ class GeminiClient(AdapterClient[AsyncIterator[GenerateContentResponse], Generat
|
||||
try:
|
||||
thinking_budget = int(extra_params["thinking_budget"])
|
||||
except (TypeError, ValueError):
|
||||
logger.warning("无效的 thinking_budget=%s,已回退为自动模式", extra_params["thinking_budget"])
|
||||
logger.warning(f"无效的 thinking_budget={extra_params['thinking_budget']},已回退为自动模式")
|
||||
|
||||
limits: Dict[str, int | bool] | None = None
|
||||
if model_id in THINKING_BUDGET_LIMITS:
|
||||
@@ -646,21 +646,21 @@ class GeminiClient(AdapterClient[AsyncIterator[GenerateContentResponse], Generat
|
||||
return THINKING_BUDGET_DISABLED
|
||||
if limits:
|
||||
minimum_value = int(limits["min"])
|
||||
logger.warning("模型 %s 不支持禁用思考预算,已回退为最小值 %s", model_id, minimum_value)
|
||||
logger.warning(f"模型 {model_id} 不支持禁用思考预算,已回退为最小值 {minimum_value}")
|
||||
return minimum_value
|
||||
return THINKING_BUDGET_AUTO
|
||||
|
||||
if limits is None:
|
||||
logger.warning("模型 %s 未配置思考预算范围,已回退为自动模式", model_id)
|
||||
logger.warning(f"模型 {model_id} 未配置思考预算范围,已回退为自动模式")
|
||||
return THINKING_BUDGET_AUTO
|
||||
|
||||
minimum_value = int(limits["min"])
|
||||
maximum_value = int(limits["max"])
|
||||
if thinking_budget < minimum_value:
|
||||
logger.warning("模型 %s 的 thinking_budget=%s 过小,已调整为 %s", model_id, thinking_budget, minimum_value)
|
||||
logger.warning(f"模型 {model_id} 的 thinking_budget={thinking_budget} 过小,已调整为 {minimum_value}")
|
||||
return minimum_value
|
||||
if thinking_budget > maximum_value:
|
||||
logger.warning("模型 %s 的 thinking_budget=%s 过大,已调整为 %s", model_id, thinking_budget, maximum_value)
|
||||
logger.warning(f"模型 {model_id} 的 thinking_budget={thinking_budget} 过大,已调整为 {maximum_value}")
|
||||
return maximum_value
|
||||
return thinking_budget
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ def _normalize_reasoning_parse_mode(parse_mode: str | ReasoningParseMode) -> Rea
|
||||
try:
|
||||
return ReasoningParseMode(parse_mode)
|
||||
except ValueError:
|
||||
logger.warning("未识别的推理解析模式 %s,已回退为 auto", parse_mode)
|
||||
logger.warning(f"未识别的推理解析模式 {parse_mode},已回退为 auto")
|
||||
return ReasoningParseMode.AUTO
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ def _normalize_tool_argument_parse_mode(parse_mode: str | ToolArgumentParseMode)
|
||||
try:
|
||||
return ToolArgumentParseMode(parse_mode)
|
||||
except ValueError:
|
||||
logger.warning("未识别的工具参数解析模式 %s,已回退为 auto", parse_mode)
|
||||
logger.warning(f"未识别的工具参数解析模式 {parse_mode},已回退为 auto")
|
||||
return ToolArgumentParseMode.AUTO
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ def _log_length_truncation(finish_reason: str | None, model_name: str | None) ->
|
||||
model_name: 上游返回的模型标识。
|
||||
"""
|
||||
if finish_reason == "length":
|
||||
logger.info("模型%s因为超过最大 max_token 限制,可能仅输出部分内容,可视情况调整", model_name or "")
|
||||
logger.info(f"模型{model_name or ''}因为超过最大 max_token 限制,可能仅输出部分内容,可视情况调整")
|
||||
|
||||
|
||||
def _coerce_openai_argument(value: Any) -> Any | Omit:
|
||||
|
||||
@@ -752,7 +752,12 @@ class MaiSakaLLMService:
|
||||
|
||||
# 获取回复提示词
|
||||
try:
|
||||
system_prompt = load_prompt("maidairy_replyer")
|
||||
system_prompt = load_prompt(
|
||||
"maidairy_replyer",
|
||||
bot_name=global_config.bot.nickname,
|
||||
identity=self._personality_prompt,
|
||||
reply_style=global_config.personality.reply_style,
|
||||
)
|
||||
except Exception:
|
||||
system_prompt = "你是一个友好的 AI 助手,请根据用户的想法生成自然的回复。"
|
||||
|
||||
|
||||
@@ -578,19 +578,16 @@ class HookDispatcher:
|
||||
return
|
||||
if not hook_spec.allow_abort:
|
||||
logger.warning(
|
||||
"Hook %s 禁止 abort,已将 %s 的错误策略按 skip 处理",
|
||||
dispatch_result.hook_name,
|
||||
target.entry.full_name,
|
||||
f"Hook {dispatch_result.hook_name} 禁止 abort,"
|
||||
f"已将 {target.entry.full_name} 的错误策略按 skip 处理"
|
||||
)
|
||||
return
|
||||
|
||||
dispatch_result.aborted = True
|
||||
dispatch_result.stopped_by = target.entry.full_name
|
||||
logger.warning(
|
||||
"HookHandler %s 因错误策略 abort 中止了 Hook %s: %s",
|
||||
target.entry.full_name,
|
||||
dispatch_result.hook_name,
|
||||
error_message,
|
||||
f"HookHandler {target.entry.full_name} 因错误策略 abort "
|
||||
f"中止了 Hook {dispatch_result.hook_name}: {error_message}"
|
||||
)
|
||||
|
||||
def _schedule_observe_handler(
|
||||
@@ -610,7 +607,7 @@ class HookDispatcher:
|
||||
"""
|
||||
|
||||
if not hook_spec.allow_observe:
|
||||
logger.warning("Hook %s 不允许 observe 处理器,已跳过 %s", hook_name, target.entry.full_name)
|
||||
logger.warning(f"Hook {hook_name} 不允许 observe 处理器,已跳过 {target.entry.full_name}")
|
||||
return
|
||||
|
||||
task = asyncio.create_task(
|
||||
@@ -649,20 +646,15 @@ class HookDispatcher:
|
||||
|
||||
if not execution_result.success:
|
||||
logger.warning(
|
||||
"观察型 HookHandler %s 执行失败: %s",
|
||||
target.entry.full_name,
|
||||
execution_result.error_message or "未知错误",
|
||||
f"观察型 HookHandler {target.entry.full_name} 执行失败: "
|
||||
f"{execution_result.error_message or '未知错误'}"
|
||||
)
|
||||
return
|
||||
|
||||
if execution_result.modified_kwargs is not None:
|
||||
logger.warning(
|
||||
"观察型 HookHandler %s 返回了 modified_kwargs,已忽略", target.entry.full_name
|
||||
)
|
||||
logger.warning(f"观察型 HookHandler {target.entry.full_name} 返回了 modified_kwargs,已忽略")
|
||||
if execution_result.action == "abort":
|
||||
logger.warning(
|
||||
"观察型 HookHandler %s 请求 abort,已忽略", target.entry.full_name
|
||||
)
|
||||
logger.warning(f"观察型 HookHandler {target.entry.full_name} 请求 abort,已忽略")
|
||||
|
||||
def _handle_background_task_done(self, task: asyncio.Task[Any]) -> None:
|
||||
"""处理观察任务完成回调。
|
||||
|
||||
@@ -164,7 +164,7 @@ class RunnerIPCLogHandler(logging.Handler):
|
||||
return f"{event_text} {' '.join(extras)}".strip()
|
||||
return event_text
|
||||
|
||||
# format() 会处理 %s 参数替换和 exc_info 文本拼接。
|
||||
# format() 会处理占位参数替换和 exc_info 文本拼接。
|
||||
return self.format(record)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -281,6 +281,26 @@ def _build_processed_plain_text(message: SessionMessage) -> str:
|
||||
return " ".join(part for part in processed_parts if part)
|
||||
|
||||
|
||||
def _build_outbound_log_preview(message: SessionMessage, max_length: int = 160) -> str:
|
||||
"""构造出站消息的日志预览文本。
|
||||
|
||||
Args:
|
||||
message: 待发送的内部消息对象。
|
||||
max_length: 预览文本最大长度。
|
||||
|
||||
Returns:
|
||||
str: 适用于日志展示的消息摘要。
|
||||
"""
|
||||
preview_text = (message.processed_plain_text or message.display_message or "").strip()
|
||||
if not preview_text:
|
||||
preview_text = f"[{_describe_message_sequence(message.raw_message)}]"
|
||||
|
||||
normalized_preview = " ".join(preview_text.split())
|
||||
if len(normalized_preview) <= max_length:
|
||||
return normalized_preview
|
||||
return f"{normalized_preview[:max_length]}..."
|
||||
|
||||
|
||||
def _build_outbound_session_message(
|
||||
message_sequence: MessageSequence,
|
||||
stream_id: str,
|
||||
@@ -424,11 +444,7 @@ def _log_platform_io_failures(delivery_batch: DeliveryBatch) -> None:
|
||||
f"driver={receipt.driver_id} status={receipt.status} error={receipt.error}"
|
||||
for receipt in delivery_batch.failed_receipts
|
||||
) or "未命中任何发送路由"
|
||||
logger.warning(
|
||||
"[SendService] Platform IO 发送失败: platform=%s %s",
|
||||
delivery_batch.route_key.platform,
|
||||
failed_details,
|
||||
)
|
||||
logger.warning(f"[SendService] Platform IO 发送失败: platform={delivery_batch.route_key.platform} {failed_details}")
|
||||
|
||||
|
||||
async def _send_via_platform_io(
|
||||
@@ -493,9 +509,9 @@ async def _send_via_platform_io(
|
||||
for receipt in delivery_batch.sent_receipts
|
||||
]
|
||||
logger.info(
|
||||
"[SendService] 已通过 Platform IO 将消息发往平台 '%s' (drivers: %s)",
|
||||
route_key.platform,
|
||||
", ".join(successful_driver_ids),
|
||||
f"[SendService] 已通过 Platform IO 将消息发往平台 '{route_key.platform}' "
|
||||
f"(drivers: {', '.join(successful_driver_ids)}) "
|
||||
f"message={_build_outbound_log_preview(message)}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user