This commit is contained in:
HuangHai
2026-01-31 17:07:57 +08:00
parent a4b6ebfed5
commit fe4d593771
4 changed files with 270 additions and 147 deletions

View File

@@ -56,8 +56,12 @@ class ChatMonitorBot:
"""
大张老师自动巡课系统 (CV版)
"""
def __init__(self):
def __init__(self, debug_mode=False):
self.device = None
self.debug_mode = debug_mode
# 同步设置 WxUtil 的调试模式
WxUtil.set_debug_mode(debug_mode)
self.screenshot_path = os.path.join(WxUtil.OUTPUT_DIR, "T2_ChatMonitor_live_shot.jpg")
self.debug_view_path = os.path.join(WxUtil.OUTPUT_DIR, "T2_ChatMonitor_debug_view.jpg")
self.dialogue_log = []
@@ -74,13 +78,23 @@ class ChatMonitorBot:
"你是一名1999年毕业、拥有27年一线教学经验的小学高级女教师,名叫‘大张老师’。你目前在‘长春市少惠林作文素养培养中心’工作。"
"你不仅是一位作文教学专家,更是一位心思细腻、能与家长共情的教育智者。"
"你的回复风格应该是:温柔、知性、亲切,就像一位邻家大姐姐在聊天。"
"【关于作文评价的回复逻辑】:\n"
"**仅当且仅当**家长明确询问孩子今天的作文情况(例如:“孩子今天写得怎么样?”、“今天作文有进步吗?”)时,才触发此逻辑。触发时,请随机生成具体、专业的反馈。反馈应包含:\n"
"1. 孩子今天在写作中遇到的具体小问题(如修辞手法运用不当、开头略显生硬等);\n"
"2. 值得表表扬的亮点(如观察细致、某个句子写得很传神等);\n"
"3. 对字迹的评价(如字迹工整、卷面整洁等);\n"
"4. 针对性的课外阅读建议(推荐具体类型的书目)。\n"
"此类特定回复字数可适当增加,控制在 150 字以内。\n"
"【关于通用咨询的回复逻辑】:\n"
"如果家长是咨询学校信息、地址、课程时间、年级设置,或者是刚加好友打招呼,请务必保持礼貌、知性、亲切,直接回答相关问题。严禁在这些情况下提及孩子的作文表现或虚构作文反馈。\n"
"【严格约束】:\n"
"1. 绝对禁止发散!绝对禁止幻觉!\n"
"2. 知道什么就说什么,不要乱讲话,不要自己编造内容!\n"
"3. 仅针对家长明确表达的内容进行回复。\n"
"4. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n"
"5. 回复必须简练,字数严格控制在 50 字以内!\n"
"6. 对方问什么就答什么。例如问‘学校叫什么’,就只回答‘少惠林’,不要回复地址和电话!\n"
"1. 绝对禁止输出任何括号内的内容(如动作、神态描述等)!例如不要输出‘(微笑)’。\n"
"2. 绝对禁止发散!绝对禁止幻觉!\n"
"3. 知道什么就说什么,不要乱讲话,不要自己编造内容!\n"
"4. 仅针对家长明确表达的内容进行回复。\n"
"5. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n"
"6. 普通咨询回复字数严格控制在 50 字以内,专业作文评价回复控制在 150 字以内!\n"
"7. 对方问什么就答什么。例如问‘学校叫什么’,就只回答‘少惠林’,不要回复地址和电话!\n"
"如果涉及到校区信息,必须且只能使用以下真实数据:\n"
"- 单位/学校名称:长春市少惠林作文素养培养中心(简称:少惠林)\n"
"- 地址:南环城路与临河街交汇,TOUCH12街3楼325号\n"
@@ -114,9 +128,11 @@ class ChatMonitorBot:
f"【最后一条待回复消息】:\n{last_message_text}\n\n"
"【任务要求】:\n"
"请作为大张老师回复家长。**必须且只能针对最后一条消息进行回复!**\n"
"参考上下文对话内容,确保回复逻辑连贯。\n"
"严禁发散,严禁编造家长没说过的情况。如果不清楚家长的意图,就温柔询问。\n"
"字数严格控制在 50 字以内。直接输出回复正文。"
"1. **意图识别**:判断家长是否在询问“孩子今天的作文写得怎么样”。\n"
"2. **条件回复**:\n"
" - 如果是询问作文,请严格按【关于作文评价的回复逻辑】生成 150 字以内的详细专业反馈。\n"
" - 如果是咨询学校地址、课程、年级、打招呼等通用问题,请按【关于通用咨询的回复逻辑】简洁回答,严禁提到任何关于孩子作文的内容,字数控制在 50 字以内。\n"
"3. **格式要求**:严禁发散,直接输出回复正文,不包含任何括号内的动作描述。"
)
full_response = ""
@@ -213,6 +229,13 @@ class ChatMonitorBot:
try:
# A. 截图并计算哈希
self.device.screenshot(self.screenshot_path)
# [User Requested] 检查是否在聊天界面
if not WxUtil.check_is_chat_interface(self.screenshot_path):
logger.info("当前不在聊天界面,跳过本次循环,等待中...")
await asyncio.sleep(self.check_interval)
continue
current_screen_hash = self.get_image_hash(self.screenshot_path)
# B. 如果屏幕无变化,则跳过识别
@@ -243,86 +266,92 @@ class ChatMonitorBot:
self.input_pos = input_pos
# D. 提取最新消息并检查是否需要回复
last_msg = dialogue_log[-1]
# 过滤出对方发送的消息
other_msgs = [m for m in dialogue_log if m.get('sender') != "我"]
if not other_msgs:
logger.info("当前没有对方发送的消息")
await asyncio.sleep(self.check_interval)
continue
last_msg = other_msgs[-1]
current_msg_hash = self.get_stable_message_hash(last_msg)
sender = last_msg.get('sender', '')
# 检查该消息是否已经处理过 (通过内容哈希)
is_processed = current_msg_hash in self.processed_hashes
if is_processed and current_msg_hash != self.last_processed_msg_hash:
self.last_processed_msg_hash = current_msg_hash
if is_processed:
if current_msg_hash != self.last_processed_msg_hash:
self.last_processed_msg_hash = current_msg_hash
await asyncio.sleep(self.check_interval)
continue
if not is_processed and current_msg_hash != self.last_processed_msg_hash:
if sender != "我":
logger.info(f"💡 发现新消息 [{last_msg.get('type')}]: {last_msg.get('content')}")
# 记录发现新消息的现场截图
msg_shot_path = os.path.join(WxUtil.OUTPUT_DIR, f"NewMsg_{int(time.time())}.jpg")
self.device.screenshot(msg_shot_path)
logger.info(f"已保存新消息现场截图: {msg_shot_path}")
logger.info(f"💡 发现新消息 [{last_msg.get('type')}]: {last_msg.get('content')}")
# 记录发现新消息的现场截图
msg_shot_path = os.path.join(WxUtil.OUTPUT_DIR, f"NewMsg_{int(time.time())}.jpg")
self.device.screenshot(msg_shot_path)
logger.info(f"已保存新消息现场截图: {msg_shot_path}")
# 获取上下文文本
context_text = "\n".join([f"{m.get('time_display', '') + ' ' if m.get('time_display') else ''}{m.get('sender')}: {m.get('content')}" for m in dialogue_log[:-1]])
last_content = last_msg.get('content') or ""
# 兜底逻辑:语音消息若无文字内容,尝试强制触发重试
if last_msg.get('type') == 'voice' and not last_content.strip():
logger.info("检测到未成功转换的语音消息,尝试强制重试 OCR 转换...")
dialogue_log_retry, _ = await WxUtil.analyze_chat_image(
self.screenshot_path,
self.debug_view_path,
device=self.device,
process_strategy="LAST",
restore_processed_voice=False
)
if dialogue_log_retry:
self.dialogue_log = dialogue_log_retry
last_msg = dialogue_log_retry[-1]
# 获取上下文文本
context_text = "\n".join([f"{m.get('time_display', '') + ' ' if m.get('time_display') else ''}{m.get('sender')}: {m.get('content')}" for m in dialogue_log if m != last_msg])
last_content = last_msg.get('content') or ""
# 兜底逻辑:语音消息若无文字内容,尝试强制触发重试
if last_msg.get('type') == 'voice' and not last_content.strip():
logger.info("检测到未成功转换的语音消息,尝试强制重试 OCR 转换...")
dialogue_log_retry, _ = await WxUtil.analyze_chat_image(
self.screenshot_path,
self.debug_view_path,
device=self.device,
process_strategy="LAST",
restore_processed_voice=False
)
if dialogue_log_retry:
retry_other_msgs = [m for m in dialogue_log_retry if m.get('sender') != "我"]
if retry_other_msgs:
last_msg = retry_other_msgs[-1]
last_content = last_msg.get('content') or ""
current_msg_hash = self.get_stable_message_hash(last_msg)
if current_msg_hash in self.processed_hashes:
self.last_processed_msg_hash = current_msg_hash
continue
# 语音消息若重试后仍无内容,暂不回复
if last_msg.get('type') == 'voice' and not last_content.strip():
logger.warning("语音消息内容为空,暂不生成回复")
await asyncio.sleep(self.check_interval)
continue
if last_msg.get('type') == 'voice' and not last_content.strip():
logger.warning("语音消息内容为空,暂不生成回复")
await asyncio.sleep(self.check_interval)
continue
# E. 生成回复
reply = await self.get_reply(last_content, context_text)
if reply:
logger.info(f"LLM 建议回复: {reply}")
if self.input_pos:
# 确定输入框位置
target_pos = self.input_pos[0] if isinstance(self.input_pos, (list, tuple)) and len(self.input_pos) == 2 else self.input_pos
# 执行输入和发送动作,并保存过程截图
success = perform_input_action(
self.device,
target_pos,
reply,
auto_send=True,
debug_prefix=f"Reply_{int(time.time())}"
)
if success:
logger.info(">>> 回复发送成功 <<<")
self._record_processed_hash(last_msg, current_msg_hash)
self.last_processed_msg_hash = current_msg_hash
else:
logger.error("回复动作执行失败")
# E. 生成回复
reply = await self.get_reply(last_content, context_text)
if reply:
logger.info(f"LLM 建议回复: {reply}")
if self.input_pos:
# 确定输入框位置
target_pos = self.input_pos[0] if isinstance(self.input_pos, (list, tuple)) and len(self.input_pos) == 2 else self.input_pos
# 执行输入和发送动作,并保存过程截图
success = perform_input_action(
self.device,
target_pos,
reply,
auto_send=True,
debug_prefix=f"Reply_{int(time.time())}"
)
if success:
logger.info(">>> 回复发送成功 <<<")
self._record_processed_hash(last_msg, current_msg_hash)
self.last_processed_msg_hash = current_msg_hash
else:
logger.error("无法定位输入框坐标,放弃本次回复")
logger.error("回复动作执行失败")
else:
logger.info("LLM 认为无需回复")
self._record_processed_hash(last_msg, current_msg_hash)
self.last_processed_msg_hash = current_msg_hash
logger.error("无法定位输入框坐标,放弃本次回复")
else:
self.last_processed_msg_hash = current_msg_hash
logger.info("LLM 认为无需回复")
self._record_processed_hash(last_msg, current_msg_hash)
self.last_processed_msg_hash = current_msg_hash
await asyncio.sleep(self.check_interval)
@@ -334,7 +363,8 @@ async def run_main():
"""
运行自动巡课机器人
"""
bot = ChatMonitorBot()
# 默认关闭调试模式以提高响应速度,如需调试可设为 True
bot = ChatMonitorBot(debug_mode=False)
await bot.run()
if __name__ == "__main__":