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