# coding=utf-8 import uiautomator2 as u2 import time import logging import sys import os import asyncio from datetime import datetime # 添加项目根目录到 sys.path project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if project_root not in sys.path: sys.path.append(project_root) from WeiXin.WxUtil import find_input_box_center, perform_input_action, analyze_chat_image, clean_screenshots_dir, is_in_chat_interface from Util.LlmUtil import get_llm_response # 配置日志 log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs") if not os.path.exists(log_dir): os.makedirs(log_dir) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(os.path.join(log_dir, "T5_AutoChatMonitor.log"), encoding='utf-8', mode='w'), logging.StreamHandler() ] ) logger = logging.getLogger("T5_Monitor") # 配置参数 CHECK_INTERVAL = 10 # 检查频率 (秒) SILENCE_THRESHOLD = 60 * 5 # 静默阈值 (5分钟) 主动询问 MAX_PROACTIVE_PROMPTS = 1 # 最大主动询问次数 class ChatBot: def __init__(self): self.d = u2.connect() self.last_message_text = "" self.last_interaction_time = time.time() self.proactive_count = 0 self.screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots") if not os.path.exists(self.screenshot_dir): os.makedirs(self.screenshot_dir) self.persona = ( "你是一名1999年毕业、拥有27年一线教学经验的小学高级女教师,名叫‘大张老师’。你目前在‘长春市少惠林作文素养培养中心’工作。" "你不仅是一位作文教学专家,更是一位心思细腻、能与家长共情的教育智者。" "你的回复风格应该是:温柔、知性、亲切,就像一位邻家大姐姐在聊天。" "【严格约束】:\n" "1. 绝对禁止发散!绝对禁止幻觉!\n" "2. 知道什么就说什么,不要乱讲话,不要自己编造内容!\n" "3. 仅针对家长明确表达的内容进行回复。\n" "4. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n" "5. 回复必须简练,字数严格控制在 50 字以内!\n" "如果涉及到校区信息,必须且只能使用以下真实数据:\n" "- 单位:长春市少惠林作文素养培养中心\n" "- 地址:南环城路与临河街交汇,TOUCH12街3楼325号\n" "- 联系人:小张老师(电话:18686619970)" ) async def get_reply(self, history_text, is_proactive=False): if is_proactive: prompt = ( f"【教师人设】:{self.persona}\n\n" f"【对话背景】:家长已经超过5分钟没有回应了。\n" f"【近期聊天记录】:\n{history_text}\n\n" "【任务要求】:\n" "请作为大张老师,给家长发一段简短的关怀消息。不要催促,语气温柔。" "字数严格控制在 50 字以内。不要编造事实。" ) else: prompt = ( f"【教师人设】:{self.persona}\n\n" f"【近期聊天记录】:\n{history_text}\n\n" "【任务要求】:\n" "请作为大张老师回复家长。针对家长的具体问题或话语进行回复。" "严禁发散,严禁编造家长没说过的情况。如果不清楚家长的意图,就温柔询问。" "字数严格控制在 50 字以内。直接输出回复正文。" ) full_response = "" async for chunk in get_llm_response(prompt, stream=False): full_response += chunk return full_response.strip().strip('"').strip('“').strip('”') async def run(self): logger.info("🚀 大张老师自动巡课系统启动...") # 0. 清除旧截图 clean_screenshots_dir() while True: try: # 1. 检查是否在微信聊天界面 (改为通过 VLM 识别结果判断,不再使用 UI 检查) # if not is_in_chat_interface(self.d): # logger.warning("⚠️ 当前不在微信聊天界面,等待下一次扫描...") # await asyncio.sleep(CHECK_INTERVAL) # continue logger.info("🔍 正在扫描当前界面内容...") # 1. 截图并分析 tmp_shot = os.path.join(self.screenshot_dir, "t5_monitor_temp.jpg") analyzed_shot = os.path.join(self.screenshot_dir, "t5_monitor_analyzed.jpg") logger.info(f"📸 正在截取屏幕... ({datetime.now().strftime('%H:%M:%S')})") self.d.screenshot(tmp_shot) logger.info("🎨 正在分析聊天界面内容 (检测头像与对话)...") # analyze_chat_image 现在会返回 None, None 如果不是聊天界面 dialogue_log, input_center = await analyze_chat_image(tmp_shot, analyzed_shot, device=self.d) if dialogue_log is None: logger.warning("⚠️ VLM 判断当前不在微信聊天界面,或无法识别。") await asyncio.sleep(CHECK_INTERVAL) continue # 语音转文字处理 if dialogue_log == "VOICE_CONVERTING": logger.info("🎙️ 检测到语音消息,已触发转文字,等待处理完成 (5秒)...") await asyncio.sleep(5) continue if not dialogue_log: logger.info("⏳ 界面分析完成,未发现有效对话内容,继续监控...") await asyncio.sleep(CHECK_INTERVAL) continue logger.info(f"📑 界面扫描完成,当前对话历史共 {len(dialogue_log)} 条") # 2. 检查是否有新消息 current_last_msg = dialogue_log[-1] logger.info(f"💬 当前最后一条消息: {current_last_msg}") history_text = "\n".join(dialogue_log) # 判断逻辑:如果最后一条消息是“对方”发的,且与上次不同,则回复 if "对方:" in current_last_msg and current_last_msg != self.last_message_text: # 关键检查:如果包含 "(待转换)",说明语音还没转文字,绝对不能回复 if "(待转换)" in current_last_msg: logger.info(f"🚫 检测到未转换的语音消息,跳过回复生成,等待转文字... ({current_last_msg})") await asyncio.sleep(2) # 稍作等待 continue logger.info(f"📩 检测到新消息: {current_last_msg}") reply = await self.get_reply(history_text) logger.info(f"🤖 生成回复: {reply}") # 执行输入发送 if input_center: center_point = input_center logger.info(f"📍 使用 VLM 识别的输入框坐标: {center_point}") else: center_point, _ = find_input_box_center(tmp_shot) logger.info(f"📍 使用 CV 识别的输入框坐标: {center_point}") # 即使 CV 没找到坐标,也尝试执行,因为 perform_input_action 内部有原生控件识别 perform_input_action(self.d, center_point, reply, auto_send=True) self.last_message_text = f"我: {reply}" # 更新状态,避免重复回复自己 self.last_interaction_time = time.time() self.proactive_count = 0 # 重置主动询问计数 # 3. 检查是否需要主动询问 (用户长时间不响应) elif "我:" in current_last_msg: # 如果最后一条是我发的,检查距离现在的时间 elapsed = time.time() - self.last_interaction_time if elapsed > SILENCE_THRESHOLD and self.proactive_count < MAX_PROACTIVE_PROMPTS: logger.info(f"⏳ 用户长时间未响应 ({int(elapsed)}s),准备主动询问...") proactive_reply = await self.get_reply(history_text, is_proactive=True) logger.info(f"🤖 发起主动询问: {proactive_reply}") if input_center: center_point = input_center else: center_point, _ = find_input_box_center(tmp_shot) # 同上,解耦 CV 坐标 perform_input_action(self.d, center_point, proactive_reply, auto_send=True) self.proactive_count += 1 self.last_interaction_time = time.time() # 更新时间,避免连续询问 self.last_message_text = f"我: {proactive_reply}" # 更新最后一条消息记录(仅用于对比) if "对方:" in current_last_msg: self.last_message_text = current_last_msg except Exception as e: logger.error(f"❌ 监控循环出错: {e}", exc_info=True) await asyncio.sleep(CHECK_INTERVAL) if __name__ == "__main__": bot = ChatBot() asyncio.run(bot.run())