refactor(chat): 重构图片在聊天记录中的处理与表示方式
为了简化LLM的上下文并提高代码可维护性,对聊天记录中图片的处理方式进行了彻底重构。 旧系统使用 [图片1] 等占位符,并在消息头部附加一个独立的图片描述映射块。这种方式结构复杂,容易造成上下文分离。 新系统将图片描述直接内联到消息文本中,格式为 `[图片:一只猫]`,使聊天记录对LLM更加自然和易于理解。 主要变更: - **消息构建 (`chat_message_builder`):** 在构建可读消息时,异步将数据库中的 `[picid:...]` 标签直接替换为完整的 `[图片:描述]`。 - **废弃映射:** 移除了独立的图片映射信息块 (`build_pic_mapping_info` 函数),所有信息都在消息内联。 - **图片处理 (`utils_image`):** `process_image` 流程现在同步返回完整的描述字符串,并增强了VLM调用的重试逻辑和缓存机制,提高了健壮性。 - **消息存储 (`storage`):** 在消息存入数据库前,将 `[图片:描述]` 转换为 `[picid:...]`,以保持存储规范化。 - **修复:** 增加了多处空值检查,提高了代码的稳定性。这不得之前稳定好用多了😋😋😋
This commit is contained in:
@@ -547,6 +547,36 @@ async def _build_readable_messages_internal(
|
||||
if pic_id_mapping is None:
|
||||
pic_id_mapping = {}
|
||||
current_pic_counter = pic_counter
|
||||
|
||||
# --- 异步图片ID处理器 (修复核心问题) ---
|
||||
async def process_pic_ids(content: str) -> str:
|
||||
"""异步处理内容中的图片ID,将其直接替换为[图片:描述]格式"""
|
||||
pic_pattern = r"\[picid:([^\]]+)\]"
|
||||
matches = list(re.finditer(pic_pattern, content))
|
||||
if not matches:
|
||||
return content
|
||||
|
||||
new_content = ""
|
||||
last_end = 0
|
||||
for match in matches:
|
||||
new_content += content[last_end : match.start()]
|
||||
pic_id = match.group(1)
|
||||
description = "[图片内容未知]"
|
||||
try:
|
||||
async with get_db_session() as session:
|
||||
result = await session.execute(select(Images.description).where(Images.image_id == pic_id))
|
||||
desc_scalar = result.scalar_one_or_none()
|
||||
if desc_scalar and desc_scalar.strip():
|
||||
description = f"[图片:{desc_scalar}]"
|
||||
else:
|
||||
description = "[图片内容未知]"
|
||||
except Exception as e:
|
||||
logger.debug(f"[chat_message_builder] 查询图片 {pic_id} 描述失败: {e}")
|
||||
description = "[图片内容未知]"
|
||||
new_content += description
|
||||
last_end = match.end()
|
||||
new_content += content[last_end:]
|
||||
return new_content
|
||||
|
||||
# 创建时间戳到消息ID的映射,用于在消息前添加[id]标识符
|
||||
timestamp_to_id = {}
|
||||
@@ -557,25 +587,6 @@ async def _build_readable_messages_internal(
|
||||
if timestamp is not None:
|
||||
timestamp_to_id[timestamp] = item.get("id", "")
|
||||
|
||||
def process_pic_ids(content: str) -> str:
|
||||
"""处理内容中的图片ID,将其替换为[图片x]格式"""
|
||||
nonlocal current_pic_counter
|
||||
|
||||
# 匹配 [picid:xxxxx] 格式
|
||||
pic_pattern = r"\[picid:([^\]]+)\]"
|
||||
|
||||
def replace_pic_id(match):
|
||||
nonlocal current_pic_counter
|
||||
pic_id = match.group(1)
|
||||
|
||||
if pic_id not in pic_id_mapping:
|
||||
pic_id_mapping[pic_id] = f"图片{current_pic_counter}"
|
||||
current_pic_counter += 1
|
||||
|
||||
return f"[{pic_id_mapping[pic_id]}]"
|
||||
|
||||
return re.sub(pic_pattern, replace_pic_id, content)
|
||||
|
||||
# 1 & 2: 获取发送者信息并提取消息组件
|
||||
for msg in messages:
|
||||
# 检查是否是动作记录
|
||||
@@ -583,8 +594,8 @@ async def _build_readable_messages_internal(
|
||||
is_action = True
|
||||
timestamp: float = msg.get("time") # type: ignore
|
||||
content = msg.get("display_message", "")
|
||||
# 对于动作记录,也处理图片ID
|
||||
content = process_pic_ids(content)
|
||||
if show_pic:
|
||||
content = await process_pic_ids(content)
|
||||
message_details_raw.append((timestamp, global_config.bot.nickname, content, is_action))
|
||||
continue
|
||||
|
||||
@@ -619,7 +630,7 @@ async def _build_readable_messages_internal(
|
||||
|
||||
# 处理图片ID
|
||||
if show_pic:
|
||||
content = process_pic_ids(content)
|
||||
content = await process_pic_ids(content)
|
||||
|
||||
# 检查必要信息是否存在
|
||||
if not all([platform, user_id, timestamp is not None]):
|
||||
@@ -808,43 +819,12 @@ async def _build_readable_messages_internal(
|
||||
current_pic_counter,
|
||||
)
|
||||
|
||||
|
||||
async def build_pic_mapping_info(pic_id_mapping: dict[str, str]) -> str:
|
||||
# sourcery skip: use-contextlib-suppress
|
||||
"""
|
||||
构建图片映射信息字符串,显示图片的具体描述内容
|
||||
|
||||
Args:
|
||||
pic_id_mapping: 图片ID到显示名称的映射字典
|
||||
|
||||
Returns:
|
||||
格式化的映射信息字符串
|
||||
此函数已废弃,因为图片描述现在被内联处理。
|
||||
保留此函数以确保向后兼容性,但它将始终返回一个空字符串。
|
||||
"""
|
||||
if not pic_id_mapping:
|
||||
return ""
|
||||
|
||||
mapping_lines = []
|
||||
|
||||
# 按图片编号排序
|
||||
sorted_items = sorted(pic_id_mapping.items(), key=lambda x: int(x[1].replace("图片", "")))
|
||||
|
||||
for pic_id, display_name in sorted_items:
|
||||
# 从数据库中获取图片描述
|
||||
description = "[图片内容未知]" # 默认描述
|
||||
try:
|
||||
async with get_db_session() as session:
|
||||
result = await session.execute(select(Images).where(Images.image_id == pic_id))
|
||||
image = result.scalar_one_or_none()
|
||||
if image and hasattr(image, "description") and image.description:
|
||||
description = image.description
|
||||
except Exception as e:
|
||||
# 如果查询失败,保持默认描述
|
||||
logger.debug(f"[chat_message_builder] 查询图片描述失败: {e}")
|
||||
pass
|
||||
|
||||
mapping_lines.append(f"[{display_name}] 的内容:{description}")
|
||||
|
||||
return "\n".join(mapping_lines)
|
||||
return ""
|
||||
|
||||
|
||||
def build_readable_actions(actions: list[dict[str, Any]]) -> str:
|
||||
@@ -932,13 +912,9 @@ async def build_readable_messages_with_list(
|
||||
将消息列表转换为可读的文本格式,并返回原始(时间戳, 昵称, 内容)列表。
|
||||
允许通过参数控制格式化行为。
|
||||
"""
|
||||
formatted_string, details_list, pic_id_mapping, _ = await _build_readable_messages_internal(
|
||||
formatted_string, details_list, _, _ = await _build_readable_messages_internal(
|
||||
messages, replace_bot_name, merge_messages, timestamp_mode, truncate
|
||||
)
|
||||
|
||||
if pic_mapping_info := await build_pic_mapping_info(pic_id_mapping):
|
||||
formatted_string = f"{pic_mapping_info}\n\n{formatted_string}"
|
||||
|
||||
return formatted_string, details_list
|
||||
|
||||
|
||||
@@ -970,12 +946,6 @@ async def build_readable_messages_with_id(
|
||||
message_id_list=message_id_list,
|
||||
)
|
||||
|
||||
# 如果存在图片映射信息,附加之
|
||||
if pic_mapping_info := await build_pic_mapping_info({}):
|
||||
# 如果当前没有图片映射则不附加
|
||||
if pic_mapping_info:
|
||||
formatted_string = f"{pic_mapping_info}\n\n{formatted_string}"
|
||||
|
||||
return formatted_string, message_id_list
|
||||
|
||||
|
||||
@@ -1078,7 +1048,7 @@ async def build_readable_messages(
|
||||
|
||||
if read_mark <= 0:
|
||||
# 没有有效的 read_mark,直接格式化所有消息
|
||||
formatted_string, _, pic_id_mapping, _ = await _build_readable_messages_internal(
|
||||
formatted_string, _, _, _ = await _build_readable_messages_internal(
|
||||
copy_messages,
|
||||
replace_bot_name,
|
||||
merge_messages,
|
||||
@@ -1088,12 +1058,7 @@ async def build_readable_messages(
|
||||
message_id_list=message_id_list,
|
||||
)
|
||||
|
||||
# 生成图片映射信息并添加到最前面
|
||||
pic_mapping_info = await build_pic_mapping_info(pic_id_mapping)
|
||||
if pic_mapping_info:
|
||||
return f"{pic_mapping_info}\n\n{formatted_string}"
|
||||
else:
|
||||
return formatted_string
|
||||
return formatted_string
|
||||
else:
|
||||
# 按 read_mark 分割消息
|
||||
messages_before_mark = [msg for msg in copy_messages if msg.get("time", 0) <= read_mark]
|
||||
@@ -1128,23 +1093,15 @@ async def build_readable_messages(
|
||||
)
|
||||
|
||||
read_mark_line = "\n--- 以上消息是你已经看过,请关注以下未读的新消息---\n"
|
||||
|
||||
# 生成图片映射信息
|
||||
if pic_id_mapping:
|
||||
pic_mapping_info = f"图片信息:\n{await build_pic_mapping_info(pic_id_mapping)}\n聊天记录信息:\n"
|
||||
else:
|
||||
pic_mapping_info = "聊天记录信息:\n"
|
||||
|
||||
|
||||
# 组合结果
|
||||
result_parts = []
|
||||
if pic_mapping_info:
|
||||
result_parts.extend((pic_mapping_info, "\n"))
|
||||
if formatted_before and formatted_after:
|
||||
result_parts.extend([formatted_before, read_mark_line, formatted_after])
|
||||
elif formatted_before:
|
||||
result_parts.extend([formatted_before, read_mark_line])
|
||||
elif formatted_after:
|
||||
result_parts.extend([read_mark_line, formatted_after])
|
||||
result_parts.extend([read_mark_line.strip(), formatted_after])
|
||||
else:
|
||||
result_parts.append(read_mark_line.strip())
|
||||
|
||||
@@ -1164,28 +1121,9 @@ async def build_anonymous_messages(messages: list[dict[str, Any]]) -> str:
|
||||
current_char = ord("A")
|
||||
output_lines = []
|
||||
|
||||
# 图片ID映射字典
|
||||
pic_id_mapping = {}
|
||||
pic_counter = 1
|
||||
|
||||
def process_pic_ids(content: str) -> str:
|
||||
"""处理内容中的图片ID,将其替换为[图片x]格式"""
|
||||
nonlocal pic_counter
|
||||
|
||||
# 匹配 [picid:xxxxx] 格式
|
||||
pic_pattern = r"\[picid:([^\]]+)\]"
|
||||
|
||||
def replace_pic_id(match):
|
||||
nonlocal pic_counter
|
||||
pic_id = match.group(1)
|
||||
|
||||
if pic_id not in pic_id_mapping:
|
||||
pic_id_mapping[pic_id] = f"图片{pic_counter}"
|
||||
pic_counter += 1
|
||||
|
||||
return f"[{pic_id_mapping[pic_id]}]"
|
||||
|
||||
return re.sub(pic_pattern, replace_pic_id, content)
|
||||
# This function builds anonymous messages, so we don't need full descriptions.
|
||||
# The existing placeholder logic is sufficient.
|
||||
# However, to maintain consistency, we will adapt it slightly.
|
||||
|
||||
def get_anon_name(platform, user_id):
|
||||
# print(f"get_anon_name: platform:{platform}, user_id:{user_id}")
|
||||
@@ -1222,8 +1160,8 @@ async def build_anonymous_messages(messages: list[dict[str, Any]]) -> str:
|
||||
if "ⁿ" in content:
|
||||
content = content.replace("ⁿ", "")
|
||||
|
||||
# 处理图片ID
|
||||
content = process_pic_ids(content)
|
||||
# For anonymous messages, we just replace with a placeholder.
|
||||
content = re.sub(r"\[picid:([^\]]+)\]", "[图片]", content)
|
||||
|
||||
# if not all([platform, user_id, timestamp is not None]):
|
||||
# continue
|
||||
@@ -1252,15 +1190,8 @@ async def build_anonymous_messages(messages: list[dict[str, Any]]) -> str:
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 在最前面添加图片映射信息
|
||||
final_output_lines = []
|
||||
pic_mapping_info = await build_pic_mapping_info(pic_id_mapping)
|
||||
if pic_mapping_info:
|
||||
final_output_lines.append(pic_mapping_info)
|
||||
final_output_lines.append("\n\n")
|
||||
|
||||
final_output_lines.extend(output_lines)
|
||||
formatted_string = "".join(final_output_lines).strip()
|
||||
# Since we are not generating a pic_mapping_info block, just join and return.
|
||||
formatted_string = "".join(output_lines).strip()
|
||||
return formatted_string
|
||||
|
||||
|
||||
@@ -1295,4 +1226,4 @@ async def get_person_id_list(messages: list[dict[str, Any]]) -> list[str]:
|
||||
if person_id := get_person_id(platform, user_id):
|
||||
person_ids_set.add(person_id)
|
||||
|
||||
return list(person_ids_set) # 将集合转换为列表返回
|
||||
return list(person_ids_set)
|
||||
|
||||
Reference in New Issue
Block a user