🤖 自动格式化代码 [skip ci]

pull/937/head
github-actions[bot] 2025-05-15 11:47:45 +00:00
parent ffc247eaa8
commit a90f735af9
7 changed files with 232 additions and 136 deletions

View File

@ -22,20 +22,77 @@ logger = get_module_logger("chat_utils")
# 预编译正则表达式以提高性能
_L_REGEX = regex.compile(r"\p{L}") # 匹配任何Unicode字母
_HAN_CHAR_REGEX = regex.compile(r"\p{Han}") # 匹配汉字 (Unicode属性)
_Nd_REGEX = regex.compile(r'\p{Nd}') # 新增匹配Unicode数字 (Nd = Number, decimal digit)
_Nd_REGEX = regex.compile(r"\p{Nd}") # 新增匹配Unicode数字 (Nd = Number, decimal digit)
SEPARATORS = {"", "", ",", " ", ";", "\xa0", "\n", ".", "", "", ""}
KNOWN_ABBREVIATIONS_ENDING_WITH_DOT = {
"Mr.", "Mrs.", "Ms.", "Dr.", "Prof.", "St.", "Messrs.", "Mmes.", "Capt.", "Gov.",
"Inc.", "Ltd.", "Corp.", "Co.", "PLC", # PLC通常不带点但有些可能
"vs.", "etc.", "i.e.", "e.g.", "viz.", "al.", "et al.", "ca.", "cf.",
"No.", "Vol.", "pp.", "fig.", "figs.", "ed.", "Ph.D.", "M.D.", "B.A.", "M.A.",
"Jan.", "Feb.", "Mar.", "Apr.", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec.", # May. 通常不用点
"Mon.", "Tue.", "Wed.", "Thu.", "Fri.", "Sat.", "Sun.",
"U.S.", "U.K.", "E.U.", "U.S.A.", "U.S.S.R.",
"Ave.", "Blvd.", "Rd.", "Ln.", # Street suffixes
"approx.", "dept.", "appt.", "श्री." # Hindi Shri.
"Mr.",
"Mrs.",
"Ms.",
"Dr.",
"Prof.",
"St.",
"Messrs.",
"Mmes.",
"Capt.",
"Gov.",
"Inc.",
"Ltd.",
"Corp.",
"Co.",
"PLC", # PLC通常不带点但有些可能
"vs.",
"etc.",
"i.e.",
"e.g.",
"viz.",
"al.",
"et al.",
"ca.",
"cf.",
"No.",
"Vol.",
"pp.",
"fig.",
"figs.",
"ed.",
"Ph.D.",
"M.D.",
"B.A.",
"M.A.",
"Jan.",
"Feb.",
"Mar.",
"Apr.",
"Jun.",
"Jul.",
"Aug.",
"Sep.",
"Oct.",
"Nov.",
"Dec.", # May. 通常不用点
"Mon.",
"Tue.",
"Wed.",
"Thu.",
"Fri.",
"Sat.",
"Sun.",
"U.S.",
"U.K.",
"E.U.",
"U.S.A.",
"U.S.S.R.",
"Ave.",
"Blvd.",
"Rd.",
"Ln.", # Street suffixes
"approx.",
"dept.",
"appt.",
"श्री.", # Hindi Shri.
}
def is_letter_not_han(char_str: str) -> bool:
"""
检查字符是否为字母非汉字
@ -68,7 +125,7 @@ def is_digit(char_str: str) -> bool:
return _Nd_REGEX.fullmatch(char_str) is not None
def is_relevant_word_char(char_str: str) -> bool: # 新增辅助函数
def is_relevant_word_char(char_str: str) -> bool: # 新增辅助函数
"""
检查字符是否为相关词语字符非汉字字母 数字
用于判断在非中文语境下空格两侧是否应被视为一个词内部的部分
@ -85,7 +142,7 @@ def is_relevant_word_char(char_str: str) -> bool: # 新增辅助函数
# 检查是否为Unicode数字
if _Nd_REGEX.fullmatch(char_str):
return True # 数字本身被视为相关词语字符
return True # 数字本身被视为相关词语字符
return False
@ -249,15 +306,17 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
# print(f"DEBUG: 输入文本 (repr): {repr(text)}")
# 预处理
text = regex.sub(r"\n\s*\n+", "\n", text) # 合并多个换行符
text = regex.sub(r"\n\s*([—。.,;\s\xa0])", r"\1", text)
text = regex.sub(r"([—。.,;\s\xa0])\s*\n", r"\1", text)
text = regex.sub(r"\n\s*\n+", "\n", text) # 合并多个换行符
text = regex.sub(r"\n\s*([—。.,;\s\xa0])", r"\1", text)
text = regex.sub(r"([—。.,;\s\xa0])\s*\n", r"\1", text)
def replace_han_newline(match):
char1 = match.group(1)
char2 = match.group(2)
if is_han_character(char1) and is_han_character(char2):
return char1 + "" + char2 # 汉字间的换行符替换为逗号
return char1 + "" + char2 # 汉字间的换行符替换为逗号
return match.group(0)
text = regex.sub(r"(.)\n(.)", replace_han_newline, text)
len_text = len(text)
@ -277,25 +336,26 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
if char in SEPARATORS:
can_split_current_char = True
if char == '.':
if char == ".":
can_split_this_dot = True
# 规则1: 小数点 (数字.数字)
if 0 < i < len_text - 1 and is_digit(text[i-1]) and is_digit(text[i+1]):
if 0 < i < len_text - 1 and is_digit(text[i - 1]) and is_digit(text[i + 1]):
can_split_this_dot = False
# 规则2: 西文缩写/域名内部的点 (西文字母.西文字母)
elif 0 < i < len_text - 1 and is_letter_not_han(text[i-1]) and is_letter_not_han(text[i+1]):
elif 0 < i < len_text - 1 and is_letter_not_han(text[i - 1]) and is_letter_not_han(text[i + 1]):
can_split_this_dot = False
# 规则3: 已知缩写词的末尾点 (例如 "e.g. ", "U.S.A. ")
else:
potential_abbreviation_word = current_segment + char
is_followed_by_space = (i + 1 < len_text and text[i+1] == ' ')
is_at_end_of_text = (i + 1 == len_text)
is_followed_by_space = i + 1 < len_text and text[i + 1] == " "
is_at_end_of_text = i + 1 == len_text
if potential_abbreviation_word in KNOWN_ABBREVIATIONS_ENDING_WITH_DOT and \
(is_followed_by_space or is_at_end_of_text):
if potential_abbreviation_word in KNOWN_ABBREVIATIONS_ENDING_WITH_DOT and (
is_followed_by_space or is_at_end_of_text
):
can_split_this_dot = False
can_split_current_char = can_split_this_dot
elif char == ' ' or char == '\xa0': # 处理空格/NBSP
elif char == " " or char == "\xa0": # 处理空格/NBSP
if 0 < i < len_text - 1:
prev_char = text[i - 1]
next_char = text[i + 1]
@ -304,19 +364,19 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
can_split_current_char = False
if can_split_current_char:
if current_segment: # 如果当前段落有内容,则添加 (内容, 分隔符)
if current_segment: # 如果当前段落有内容,则添加 (内容, 分隔符)
segments.append((current_segment, char))
# 如果当前段落为空,但分隔符不是简单的排版空格 (除非是换行符这种有意义的空行分隔)
elif char not in [' ', '\xa0'] or char == '\n':
segments.append(("", char)) # 添加 ("", 分隔符)
current_segment = "" # 重置当前段落
elif char not in [" ", "\xa0"] or char == "\n":
segments.append(("", char)) # 添加 ("", 分隔符)
current_segment = "" # 重置当前段落
else:
current_segment += char # 不分割,将当前分隔符加入到当前段落
current_segment += char # 不分割,将当前分隔符加入到当前段落
else:
current_segment += char # 非分隔符,加入当前段落
current_segment += char # 非分隔符,加入当前段落
i += 1
if current_segment: # 处理末尾剩余的段落
if current_segment: # 处理末尾剩余的段落
segments.append((current_segment, ""))
# 过滤掉仅由空格组成的segment但保留其后的有效分隔符
@ -325,7 +385,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
stripped_content = content.strip()
if stripped_content:
filtered_segments.append((stripped_content, sep))
elif sep and (sep not in [' ', '\xa0'] or sep == '\n'):
elif sep and (sep not in [" ", "\xa0"] or sep == "\n"):
filtered_segments.append(("", sep))
segments = filtered_segments
@ -335,33 +395,32 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
preliminary_final_sentences = []
current_sentence_build = ""
for k, (content, sep) in enumerate(segments):
current_sentence_build += content # 先添加内容部分
current_sentence_build += content # 先添加内容部分
# 判断分隔符类型
is_strong_terminator = sep in {"", ".", "", "", "\n", ""}
is_space_separator = sep in [' ', '\xa0']
is_space_separator = sep in [" ", "\xa0"]
if is_strong_terminator:
current_sentence_build += sep # 将强终止符加入
current_sentence_build += sep # 将强终止符加入
if current_sentence_build.strip():
preliminary_final_sentences.append(current_sentence_build.strip())
current_sentence_build = "" # 开始新的句子构建
current_sentence_build = "" # 开始新的句子构建
elif is_space_separator:
# 如果是空格,并且当前构建的句子不以空格结尾,则添加空格并继续构建
if not current_sentence_build.endswith(sep):
current_sentence_build += sep
elif sep: # 其他分隔符 (如 ',', ';')
current_sentence_build += sep # 加入并继续构建,这些通常不独立成句
elif sep: # 其他分隔符 (如 ',', ';')
current_sentence_build += sep # 加入并继续构建,这些通常不独立成句
# 如果这些弱分隔符后紧跟的就是文本末尾,则它们可能结束一个句子
if k == len(segments) -1 and current_sentence_build.strip():
if k == len(segments) - 1 and current_sentence_build.strip():
preliminary_final_sentences.append(current_sentence_build.strip())
current_sentence_build = ""
if current_sentence_build.strip(): # 处理最后一个构建中的句子
if current_sentence_build.strip(): # 处理最后一个构建中的句子
preliminary_final_sentences.append(current_sentence_build.strip())
preliminary_final_sentences = [s for s in preliminary_final_sentences if s.strip()] # 清理空字符串
preliminary_final_sentences = [s for s in preliminary_final_sentences if s.strip()] # 清理空字符串
# print(f"DEBUG: 初步分割(优化组装后)的句子: {preliminary_final_sentences}")
if not preliminary_final_sentences:
@ -377,12 +436,12 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
if merge_probability == 1.0 and len(preliminary_final_sentences) > 1:
merged_text = " ".join(preliminary_final_sentences).strip()
if merged_text.endswith(',') or merged_text.endswith(''):
if merged_text.endswith(",") or merged_text.endswith(""):
merged_text = merged_text[:-1].strip()
return [merged_text] if merged_text else []
elif len(preliminary_final_sentences) == 1:
s = preliminary_final_sentences[0].strip()
if s.endswith(',') or s.endswith(''):
if s.endswith(",") or s.endswith(""):
s = s[:-1].strip()
return [s] if s else []
@ -407,7 +466,7 @@ def split_into_sentences_w_remove_punctuation(text: str) -> list[str]:
processed_sentences_after_merge = []
for sentence in final_sentences_merged:
s = sentence.strip()
if s.endswith(',') or s.endswith(''):
if s.endswith(",") or s.endswith(""):
s = s[:-1].strip()
if s:
s = random_remove_punctuation(s)

View File

@ -294,7 +294,11 @@ async def _build_prompt_focus(reason, current_mind_info, structured_info, chat_s
if isinstance(expr, dict) and "situation" in expr and "style" in expr:
style_habbits.append(f"{expr['situation']}时,使用 {expr['style']}")
style_habbits_str = "\n你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:\n".join(style_habbits)
style_habbits_str = (
"\n你可以参考以下的语言习惯,如果情景合适就使用,不要盲目使用,不要生硬使用,而是结合到表达中:\n".join(
style_habbits
)
)
grammar_habbits_str = "\n请你根据情景使用以下句法:\n".join(grammar_habbits)
else:
reply_styles1 = [
@ -947,6 +951,7 @@ class PromptBuilder:
logger.error(traceback.format_exc())
return "[构建 Planner Prompt 时出错]"
def weighted_sample_no_replacement(items, weights, k) -> list:
"""
加权且不放回地随机抽取k个元素

View File

@ -292,14 +292,16 @@ class ActionPlanner:
"reason",
"emoji_query",
default_values={"action": "wait", "reason": "LLM返回格式错误或未提供原因默认等待", "emoji_query": ""},
allow_empty_string_fields=["emoji_query"]
allow_empty_string_fields=["emoji_query"],
)
initial_action = initial_result.get("action", "wait")
initial_reason = initial_result.get("reason", "LLM未提供原因默认等待")
current_emoji_query = initial_result.get("emoji_query", "") # 获取 emoji_query
logger.info(f"[私聊][{self.private_name}] LLM 初步规划行动: {initial_action}, 原因: {initial_reason}表情查询: '{current_emoji_query}'")
if conversation_info: # 确保 conversation_info 存在
current_emoji_query = initial_result.get("emoji_query", "") # 获取 emoji_query
logger.info(
f"[私聊][{self.private_name}] LLM 初步规划行动: {initial_action}, 原因: {initial_reason}表情查询: '{current_emoji_query}'"
)
if conversation_info: # 确保 conversation_info 存在
conversation_info.current_emoji_query = current_emoji_query
except Exception as llm_err:
logger.error(f"[私聊][{self.private_name}] 调用 LLM 或解析初步规划结果时出错: {llm_err}")

View File

@ -11,10 +11,10 @@ from src.chat.utils.chat_message_builder import build_readable_messages
from .pfc_types import ConversationState
from .observation_info import ObservationInfo
from .conversation_info import ConversationInfo
from src.chat.utils.utils_image import image_path_to_base64 # 假设路径正确
from maim_message import Seg, UserInfo # 从 maim_message 导入 Seg 和 UserInfo
from src.chat.message_receive.message import MessageSending, MessageSet # PFC 的发送器依赖这些
from src.chat.message_receive.message_sender import message_manager # PFC 的发送器依赖这个
from src.chat.utils.utils_image import image_path_to_base64 # 假设路径正确
from maim_message import Seg, UserInfo # 从 maim_message 导入 Seg 和 UserInfo
from src.chat.message_receive.message import MessageSending, MessageSet # PFC 的发送器依赖这些
from src.chat.message_receive.message_sender import message_manager # PFC 的发送器依赖这个
if TYPE_CHECKING:
from .conversation import Conversation # 用于类型提示以避免循环导入
@ -468,7 +468,7 @@ async def handle_action(
final_status = "max_checker_attempts_failed"
final_reason = f"达到最大回复尝试次数({max_reply_attempts})ReplyChecker仍判定不合适: {check_reason}"
action_successful = False
if conversation_info: # 确保 conversation_info 存在
if conversation_info: # 确保 conversation_info 存在
conversation_info.last_successful_reply_action = None
# my_message_count 保持不变
@ -632,49 +632,61 @@ async def handle_action(
elif action == "send_memes":
conversation_instance.state = ConversationState.GENERATING
final_reason_prefix = "发送表情包"
action_successful = False # 先假设不成功
action_successful = False # 先假设不成功
# 确保 conversation_info 和 observation_info 存在
if not conversation_info or not observation_info:
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ConversationInfo 或 ObservationInfo 为空。")
logger.error(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ConversationInfo 或 ObservationInfo 为空。"
)
final_status = "error"
final_reason = f"{final_reason_prefix}失败:内部信息缺失"
# done_action 的更新会在 finally 中处理
# 理论上这不应该发生,因为调用 handle_action 前应该有检查
# 但作为防御性编程,可以加上
if conversation_info: # 即使另一个为空,也尝试更新
conversation_info.last_successful_reply_action = None
if conversation_info: # 即使另一个为空,也尝试更新
conversation_info.last_successful_reply_action = None
# 直接跳到 finally 块
# 注意:此处不能直接 return否则 finally 不会被完整执行。
# 而是让后续的 final_status 和 action_successful 决定流程。
# 这里我们通过设置 action_successful = False 和 final_status = "error" 来让 finally 处理
# 更好的方式可能是直接在 finally 前面抛出异常,但为了简化,我们先这样。
# 此处保持 action_successful = False后续的 finally 会处理状态。
pass # 让代码继续到 try...except...finally 的末尾
pass # 让代码继续到 try...except...finally 的末尾
else: # conversation_info 和 observation_info 都存在
else: # conversation_info 和 observation_info 都存在
emoji_query = conversation_info.current_emoji_query
if not emoji_query:
logger.warning(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': emoji_query 为空,无法获取表情包。")
logger.warning(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': emoji_query 为空,无法获取表情包。"
)
final_status = "recall"
final_reason = f"{final_reason_prefix}失败:缺少表情包查询语句"
conversation_info.last_successful_reply_action = None
else:
logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 使用查询 '{emoji_query}' 获取表情包...")
logger.info(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 使用查询 '{emoji_query}' 获取表情包..."
)
try:
emoji_result = await emoji_manager.get_emoji_for_text(emoji_query)
if emoji_result:
emoji_path, emoji_description = emoji_result
logger.info(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 获取到表情包: {emoji_path}, 描述: {emoji_description}")
logger.info(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 获取到表情包: {emoji_path}, 描述: {emoji_description}"
)
if not conversation_instance.chat_stream:
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ChatStream 未初始化,无法发送。")
logger.error(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': ChatStream 未初始化,无法发送。"
)
raise RuntimeError("ChatStream 未初始化")
image_b64_content = image_path_to_base64(emoji_path)
if not image_b64_content:
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 无法将图片 {emoji_path} 转换为 base64。")
logger.error(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 无法将图片 {emoji_path} 转换为 base64。"
)
raise ValueError(f"无法将图片 {emoji_path} 转换为Base64")
# --- 统一 Seg 构造方式 (与群聊类似) ---
@ -707,22 +719,24 @@ async def handle_action(
message_id=message_id_emoji,
chat_stream=conversation_instance.chat_stream,
bot_user_info=bot_user_info,
sender_info=None, # 表情通常不是对特定消息的回复
message_segment=message_segment_for_emoji, # 直接使用构造的 Seg
sender_info=None, # 表情通常不是对特定消息的回复
message_segment=message_segment_for_emoji, # 直接使用构造的 Seg
reply=None,
is_head=True,
is_emoji=True,
thinking_start_time=action_start_time, # 使用动作开始时间作为近似
thinking_start_time=action_start_time, # 使用动作开始时间作为近似
)
await message_to_send.process() # 消息预处理
await message_to_send.process() # 消息预处理
message_set_emoji = MessageSet(conversation_instance.chat_stream, message_id_emoji)
message_set_emoji.add_message(message_to_send)
await message_manager.add_message(message_set_emoji) # 使用全局管理器发送
await message_manager.add_message(message_set_emoji) # 使用全局管理器发送
logger.info(f"[私聊][{conversation_instance.private_name}] PFC 动作 'send_memes': 表情包已发送: {emoji_path} ({emoji_description})")
action_successful = True # 标记发送成功
logger.info(
f"[私聊][{conversation_instance.private_name}] PFC 动作 'send_memes': 表情包已发送: {emoji_path} ({emoji_description})"
)
action_successful = True # 标记发送成功
# final_status 和 final_reason 会在 finally 中设置
# --- 后续成功处理逻辑 (与之前相同,但要确保 conversation_info 存在) ---
@ -745,7 +759,7 @@ async def handle_action(
"user_info": bot_user_info.to_dict(),
"processed_plain_text": f"[表情包: {emoji_description}]",
"detailed_plain_text": f"[表情包: {emoji_path} - {emoji_description}]",
"raw_message": "[CQ:image,file=base64://...]" # 示例
"raw_message": "[CQ:image,file=base64://...]", # 示例
}
observation_info.chat_history.append(bot_meme_message_dict)
observation_info.chat_history_count = len(observation_info.chat_history)
@ -756,11 +770,16 @@ async def handle_action(
history_slice_for_str = observation_info.chat_history[-30:]
try:
observation_info.chat_history_str = await build_readable_messages(
history_slice_for_str, replace_bot_name=True, merge_messages=False,
timestamp_mode="relative", read_mark=0.0
history_slice_for_str,
replace_bot_name=True,
merge_messages=False,
timestamp_mode="relative",
read_mark=0.0,
)
except Exception as e_build_hist_meme:
logger.error(f"[私聊][{conversation_instance.private_name}] 更新 chat_history_str (表情包后) 时出错: {e_build_hist_meme}")
logger.error(
f"[私聊][{conversation_instance.private_name}] 更新 chat_history_str (表情包后) 时出错: {e_build_hist_meme}"
)
current_unprocessed_messages_emoji = observation_info.unprocessed_messages
message_ids_to_clear_emoji: Set[str] = set()
@ -768,7 +787,9 @@ async def handle_action(
msg_time_item = msg_item.get("time")
msg_id_item = msg_item.get("message_id")
sender_id_info_item = msg_item.get("user_info", {})
sender_id_item = str(sender_id_info_item.get("user_id")) if sender_id_info_item else None
sender_id_item = (
str(sender_id_info_item.get("user_id")) if sender_id_info_item else None
)
if (
msg_id_item
and msg_time_item
@ -793,20 +814,23 @@ async def handle_action(
chat_observer_for_history=conversation_instance.chat_observer,
event_description=event_for_emotion_update_emoji,
)
else: # emoji_result is None
logger.warning(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 未能根据查询 '{emoji_query}' 获取到合适的表情包。")
else: # emoji_result is None
logger.warning(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 未能根据查询 '{emoji_query}' 获取到合适的表情包。"
)
final_status = "recall"
final_reason = f"{final_reason_prefix}失败:未找到合适表情包"
action_successful = False
if conversation_info:
conversation_info.last_successful_reply_action = None
conversation_info.current_emoji_query = None
conversation_info.last_successful_reply_action = None
conversation_info.current_emoji_query = None
except Exception as get_send_emoji_err:
logger.error(f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 处理过程中出错: {get_send_emoji_err}")
logger.error(
f"[私聊][{conversation_instance.private_name}] 动作 'send_memes': 处理过程中出错: {get_send_emoji_err}"
)
logger.error(traceback.format_exc())
final_status = "recall" # 或 "error"
final_status = "recall" # 或 "error"
final_reason = f"{final_reason_prefix}失败:处理表情包时出错 ({get_send_emoji_err})"
action_successful = False
if conversation_info:
@ -854,7 +878,7 @@ async def handle_action(
conversation_info.last_rejected_reply_content = None
if action != "send_memes" or not action_successful:
if conversation_info and hasattr(conversation_info, 'current_emoji_query'):
if conversation_info and hasattr(conversation_info, "current_emoji_query"):
conversation_info.current_emoji_query = None
except asyncio.CancelledError:
@ -887,8 +911,8 @@ async def handle_action(
and action_index < len(conversation_info.done_action)
):
# 确定最终状态和原因
if action_successful: # 如果动作本身标记为成功
if final_status not in ["done", "done_no_reply"]: # 如果没有被特定成功状态覆盖
if action_successful: # 如果动作本身标记为成功
if final_status not in ["done", "done_no_reply"]: # 如果没有被特定成功状态覆盖
final_status = "done"
if not final_reason or final_reason == "动作未成功执行":
if action == "send_memes":
@ -896,8 +920,8 @@ async def handle_action(
# ... (其他动作的默认成功原因) ...
else:
final_reason = f"动作 {action} 成功完成"
else: # action_successful is False
if final_status in ["recall", "start", "unknown"]: # 如果状态还是初始或未定
else: # action_successful is False
if final_status in ["recall", "start", "unknown"]: # 如果状态还是初始或未定
# 尝试从 conversation_info 获取更具体的失败原因
specific_rejection_reason = getattr(conversation_info, "last_reply_rejection_reason", None)
if specific_rejection_reason and (not final_reason or final_reason == "动作未成功执行"):
@ -932,37 +956,38 @@ async def handle_action(
# cancelled 会让 loop 捕获异常并停止。
# 重置非回复动作的追问状态 (确保 send_memes 被视为回复动作)
if action not in ["direct_reply", "send_new_message", "say_goodbye", "send_memes"]: # <--- 把 send_memes 加到这里
if action not in [
"direct_reply",
"send_new_message",
"say_goodbye",
"send_memes",
]: # <--- 把 send_memes 加到这里
if conversation_info:
conversation_info.last_successful_reply_action = None
conversation_info.last_reply_rejection_reason = None
conversation_info.last_rejected_reply_content = None
# 清理表情查询如果动作不是send_memes但查询还存在或者send_memes失败了
if action != "send_memes" or not action_successful:
if conversation_info and hasattr(conversation_info, 'current_emoji_query'):
if conversation_info and hasattr(conversation_info, "current_emoji_query"):
conversation_info.current_emoji_query = None
log_final_reason_msg = final_reason if final_reason else "无明确原因"
if (
final_status == "done"
and action_successful
and action in ["direct_reply", "send_new_message"] # send_memes 的发送内容不同
and action in ["direct_reply", "send_new_message"] # send_memes 的发送内容不同
and hasattr(conversation_instance, "generated_reply")
and conversation_instance.generated_reply
):
log_final_reason_msg += f" (发送内容: '{conversation_instance.generated_reply[:30]}...')"
elif (
final_status == "done"
and action_successful
and action == "send_memes"
final_status == "done" and action_successful and action == "send_memes"
# emoji_description 在 send_memes 内部获取,这里不再重复记录到 log_final_reason_msg
# 因为 logger.info 已经记录过发送的表情描述
):
pass
logger.info(
f"[私聊][{conversation_instance.private_name}] 动作 '{action}' 处理完成。最终状态: {final_status}, 原因: {log_final_reason_msg}"
)

View File

@ -16,4 +16,4 @@ class ConversationInfo:
self.current_emotion_text: Optional[str] = "心情平静。" # 机器人当前的情绪描述文本
self.current_instance_message_count: int = 0 # 当前私聊实例中的消息计数
self.other_new_messages_during_planning_count: int = 0 # 在计划阶段期间收到的其他新消息计数
self.current_emoji_query: Optional[str] = None # 表情包
self.current_emoji_query: Optional[str] = None # 表情包

View File

@ -339,20 +339,22 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
last_action_final_status = "unknown"
# 从 conversation_info.done_action 获取上一个动作的最终状态
if conversation_instance.conversation_info and conversation_instance.conversation_info.done_action:
if conversation_instance.conversation_info.done_action: # 确保列表不为空
last_action_record = conversation_instance.conversation_info.done_action[-1]
last_action_final_status = last_action_record.get("status", "unknown")
if conversation_instance.conversation_info.done_action: # 确保列表不为空
last_action_record = conversation_instance.conversation_info.done_action[-1]
last_action_final_status = last_action_record.get("status", "unknown")
if last_action_final_status == "max_checker_attempts_failed":
original_planned_action = last_action_record.get("action", "unknown_original_action")
original_plan_reason = last_action_record.get("plan_reason", "unknown_original_reason")
checker_fail_reason_from_history = last_action_record.get("final_reason", "ReplyChecker判定不合适")
checker_fail_reason_from_history = last_action_record.get(
"final_reason", "ReplyChecker判定不合适"
)
logger.warning(
f"[私聊][{conversation_instance.private_name}] (Loop) 原规划动作 '{original_planned_action}' 因达到ReplyChecker最大尝试次数而失败。将强制执行 'wait' 动作。"
)
action_to_perform_now = "wait" # 强制动作为 "wait"
action_to_perform_now = "wait" # 强制动作为 "wait"
reason_for_forced_wait = f"原动作 '{original_planned_action}' (规划原因: {original_plan_reason}) 因 ReplyChecker 多次判定不合适 ({checker_fail_reason_from_history}) 而失败,现强制等待。"
if conversation_instance.conversation_info:
@ -360,21 +362,20 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
conversation_instance.conversation_info.last_successful_reply_action = None
# 重置连续LLM失败计数器因为我们已经用特定的“等待”动作处理了这种失败类型
conversation_instance.consecutive_llm_action_failures = 0
logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 执行强制等待动作...")
await actions.handle_action(
conversation_instance,
action_to_perform_now, # "wait"
action_to_perform_now, # "wait"
reason_for_forced_wait,
conversation_instance.observation_info,
conversation_instance.conversation_info,
)
# "wait" 动作执行后,其内部逻辑会将状态设置为 ANALYZING (通过 finally 块)
# 所以循环的下一轮会自然地重新规划或根据等待结果行动
_force_reflect_and_act_next_iter = False # 确保此路径不会强制反思
await asyncio.sleep(0.1) # 短暂暂停,等待状态更新等
continue # 进入主循环的下一次迭代
_force_reflect_and_act_next_iter = False # 确保此路径不会强制反思
await asyncio.sleep(0.1) # 短暂暂停,等待状态更新等
continue # 进入主循环的下一次迭代
elif conversation_instance.consecutive_llm_action_failures >= MAX_CONSECUTIVE_LLM_ACTION_FAILURES:
logger.error(
@ -382,8 +383,10 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
)
forced_wait_action_on_consecutive_failure = "wait"
reason_for_consecutive_failure_wait = f"LLM连续失败{conversation_instance.consecutive_llm_action_failures}次,强制等待"
reason_for_consecutive_failure_wait = (
f"LLM连续失败{conversation_instance.consecutive_llm_action_failures}次,强制等待"
)
conversation_instance.consecutive_llm_action_failures = 0
if conversation_instance.conversation_info:
@ -392,7 +395,7 @@ async def run_conversation_loop(conversation_instance: "Conversation"):
logger.info(f"[私聊][{conversation_instance.private_name}] (Loop) 执行强制等待动作...")
await actions.handle_action(
conversation_instance,
forced_wait_action_on_consecutive_failure, # "wait"
forced_wait_action_on_consecutive_failure, # "wait"
reason_for_consecutive_failure_wait,
conversation_instance.observation_info,
conversation_instance.conversation_info,

View File

@ -505,12 +505,14 @@ def get_items_from_json(
valid_item = False
break
if not valid_item:
if not valid_item:
continue
if required_types:
for field, expected_type in required_types.items():
if field in current_item_result and not isinstance(current_item_result[field], expected_type):
if field in current_item_result and not isinstance(
current_item_result[field], expected_type
):
logger.warning(
f"[私聊][{private_name}] JSON数组元素字段 '{field}' 类型错误 (应为 {expected_type.__name__}, 实际为 {type(current_item_result[field]).__name__}): {item_json}"
)
@ -547,16 +549,16 @@ def get_items_from_json(
except Exception as e:
logger.error(f"[私聊][{private_name}] 尝试解析JSON数组时发生未知错误: {str(e)}")
# result = default_result.copy()
json_data = None
valid_single_object = True # <--- 将初始化提前到这里
valid_single_object = True # <--- 将初始化提前到这里
try:
json_data = json.loads(cleaned_content)
if not isinstance(json_data, dict):
logger.error(f"[私聊][{private_name}] 解析为单个对象,但结果不是字典类型: {type(json_data)}")
# 如果不是字典,即使 allow_array 为 False这里也应该认为单个对象解析失败
valid_single_object = False # 标记为无效
valid_single_object = False # 标记为无效
# return False, default_result.copy() # 不立即返回,让后续逻辑统一处理 valid_single_object
except json.JSONDecodeError:
json_pattern = r"\{[\s\S]*?\}"
@ -567,31 +569,30 @@ def get_items_from_json(
json_data = json.loads(potential_json_str)
if not isinstance(json_data, dict):
logger.error(f"[私聊][{private_name}] 正则提取后解析,但结果不是字典类型: {type(json_data)}")
valid_single_object = False # 标记为无效
valid_single_object = False # 标记为无效
# return False, default_result.copy()
else:
logger.debug(f"[私聊][{private_name}] 通过正则提取并成功解析JSON对象。")
# valid_single_object 保持 True
except json.JSONDecodeError:
logger.error(f"[私聊][{private_name}] 正则提取的部分 '{potential_json_str[:100]}...' 无法解析为JSON。")
valid_single_object = False # 标记为无效
valid_single_object = False # 标记为无效
# return False, default_result.copy()
else:
logger.error(
f"[私聊][{private_name}] 无法在返回内容中找到有效的JSON对象部分。原始内容: {cleaned_content[:100]}..."
)
valid_single_object = False # 标记为无效
valid_single_object = False # 标记为无效
# return False, default_result.copy()
# 如果前面的步骤未能成功解析出一个 dict 类型的 json_data则 valid_single_object 会是 False
if not isinstance(json_data, dict) or not valid_single_object: # 增加对 json_data 类型的检查
if not isinstance(json_data, dict) or not valid_single_object: # 增加对 json_data 类型的检查
# 如果 allow_array 为 True 且数组解析成功过,这里不应该执行 (因为之前会 return True, valid_items_list)
# 如果 allow_array 为 False或者数组解析也失败了那么到这里就意味着整体解析失败
if not (allow_array and isinstance(json_array, list) and valid_items_list): # 修正:检查之前数组解析是否成功
logger.debug(f"[私聊][{private_name}] 未能成功解析为有效的JSON对象或数组。")
if not (allow_array and isinstance(json_array, list) and valid_items_list): # 修正:检查之前数组解析是否成功
logger.debug(f"[私聊][{private_name}] 未能成功解析为有效的JSON对象或数组。")
return False, default_result.copy()
# 如果成功解析了单个 JSON 对象 (json_data 是 dict 且 valid_single_object 仍为 True)
# current_single_result 的初始化和填充逻辑可以保持
current_single_result = default_result.copy()
@ -604,9 +605,9 @@ def get_items_from_json(
logger.error(f"[私聊][{private_name}] JSON对象缺少必要字段 '{item_field}'。JSON内容: {json_data}")
valid_single_object = False
break
if not valid_single_object:
return False, default_result.copy() # 如果字段缺失,则校验失败
return False, default_result.copy() # 如果字段缺失,则校验失败
if required_types:
for field, expected_type in required_types.items():
@ -616,15 +617,17 @@ def get_items_from_json(
)
valid_single_object = False
break
if not valid_single_object:
return False, default_result.copy() # 如果类型错误,则校验失败
return False, default_result.copy() # 如果类型错误,则校验失败
for field in items:
if field in current_single_result and \
isinstance(current_single_result[field], str) and \
not current_single_result[field].strip() and \
field not in _allow_empty_string_fields:
if (
field in current_single_result
and isinstance(current_single_result[field], str)
and not current_single_result[field].strip()
and field not in _allow_empty_string_fields
):
logger.error(f"[私聊][{private_name}] JSON对象字段 '{field}' 不能为空字符串 (除非特别允许)")
valid_single_object = False
break
@ -634,7 +637,6 @@ def get_items_from_json(
return True, current_single_result
else:
return False, default_result.copy()
async def get_person_id(private_name: str, chat_stream: ChatStream):