2026-01-25 10:28:31 +08:00
|
|
|
|
# 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)
|
|
|
|
|
|
|
2026-01-25 11:52:06 +08:00
|
|
|
|
from WeiXin.WxUtil import find_input_box_center, perform_input_action, analyze_chat_image, clean_screenshots_dir, is_in_chat_interface
|
2026-01-25 10:28:31 +08:00
|
|
|
|
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=[
|
2026-01-25 12:52:52 +08:00
|
|
|
|
logging.FileHandler(os.path.join(log_dir, "T5_AutoChatMonitor.log"), encoding='utf-8', mode='w'),
|
2026-01-25 10:28:31 +08:00
|
|
|
|
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 = (
|
2026-01-25 12:52:52 +08:00
|
|
|
|
"你是一名1999年毕业、拥有27年一线教学经验的小学高级女教师,名叫‘大张老师’。你目前在‘长春市少惠林作文素养培养中心’工作。"
|
|
|
|
|
|
"你不仅是一位作文教学专家,更是一位心思细腻、能与家长共情的教育智者。"
|
|
|
|
|
|
"你的回复风格应该是:温柔、知性、亲切,就像一位邻家大姐姐在聊天。"
|
|
|
|
|
|
"【严格约束】:\n"
|
|
|
|
|
|
"1. 绝对禁止发散!绝对禁止幻觉!\n"
|
|
|
|
|
|
"2. 知道什么就说什么,不要乱讲话,不要自己编造内容!\n"
|
|
|
|
|
|
"3. 仅针对家长明确表达的内容进行回复。\n"
|
|
|
|
|
|
"4. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n"
|
|
|
|
|
|
"5. 回复必须简练,字数严格控制在 50 字以内!\n"
|
|
|
|
|
|
"如果涉及到校区信息,必须且只能使用以下真实数据:\n"
|
2026-01-25 11:52:06 +08:00
|
|
|
|
"- 单位:长春市少惠林作文素养培养中心\n"
|
|
|
|
|
|
"- 地址:南环城路与临河街交汇,TOUCH12街3楼325号\n"
|
2026-01-25 12:52:52 +08:00
|
|
|
|
"- 联系人:小张老师(电话:18686619970)"
|
2026-01-25 10:28:31 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def get_reply(self, history_text, is_proactive=False):
|
|
|
|
|
|
if is_proactive:
|
|
|
|
|
|
prompt = (
|
2026-01-25 11:52:06 +08:00
|
|
|
|
f"【教师人设】:{self.persona}\n\n"
|
|
|
|
|
|
f"【对话背景】:家长已经超过5分钟没有回应了。\n"
|
|
|
|
|
|
f"【近期聊天记录】:\n{history_text}\n\n"
|
|
|
|
|
|
"【任务要求】:\n"
|
2026-01-25 12:52:52 +08:00
|
|
|
|
"请作为大张老师,给家长发一段简短的关怀消息。不要催促,语气温柔。"
|
|
|
|
|
|
"字数严格控制在 50 字以内。不要编造事实。"
|
2026-01-25 10:28:31 +08:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
prompt = (
|
2026-01-25 11:52:06 +08:00
|
|
|
|
f"【教师人设】:{self.persona}\n\n"
|
|
|
|
|
|
f"【近期聊天记录】:\n{history_text}\n\n"
|
|
|
|
|
|
"【任务要求】:\n"
|
2026-01-25 12:52:52 +08:00
|
|
|
|
"请作为大张老师回复家长。针对家长的具体问题或话语进行回复。"
|
|
|
|
|
|
"严禁发散,严禁编造家长没说过的情况。如果不清楚家长的意图,就温柔询问。"
|
|
|
|
|
|
"字数严格控制在 50 字以内。直接输出回复正文。"
|
2026-01-25 10:28:31 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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:
|
2026-01-25 12:52:52 +08:00
|
|
|
|
# 1. 检查是否在微信聊天界面 (改为通过 VLM 识别结果判断,不再使用 UI 检查)
|
|
|
|
|
|
# if not is_in_chat_interface(self.d):
|
|
|
|
|
|
# logger.warning("⚠️ 当前不在微信聊天界面,等待下一次扫描...")
|
|
|
|
|
|
# await asyncio.sleep(CHECK_INTERVAL)
|
|
|
|
|
|
# continue
|
2026-01-25 11:52:06 +08:00
|
|
|
|
|
|
|
|
|
|
logger.info("🔍 正在扫描当前界面内容...")
|
2026-01-25 10:28:31 +08:00
|
|
|
|
# 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")
|
2026-01-25 11:52:06 +08:00
|
|
|
|
|
|
|
|
|
|
logger.info(f"📸 正在截取屏幕... ({datetime.now().strftime('%H:%M:%S')})")
|
2026-01-25 10:28:31 +08:00
|
|
|
|
self.d.screenshot(tmp_shot)
|
|
|
|
|
|
|
2026-01-25 11:52:06 +08:00
|
|
|
|
logger.info("🎨 正在分析聊天界面内容 (检测头像与对话)...")
|
2026-01-25 12:52:52 +08:00
|
|
|
|
# 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
|
2026-01-25 10:28:31 +08:00
|
|
|
|
|
2026-01-25 11:52:06 +08:00
|
|
|
|
# 语音转文字处理
|
|
|
|
|
|
if dialogue_log == "VOICE_CONVERTING":
|
|
|
|
|
|
logger.info("🎙️ 检测到语音消息,已触发转文字,等待处理完成 (5秒)...")
|
|
|
|
|
|
await asyncio.sleep(5)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-01-25 10:28:31 +08:00
|
|
|
|
if not dialogue_log:
|
2026-01-25 11:52:06 +08:00
|
|
|
|
logger.info("⏳ 界面分析完成,未发现有效对话内容,继续监控...")
|
2026-01-25 10:28:31 +08:00
|
|
|
|
await asyncio.sleep(CHECK_INTERVAL)
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-01-25 11:52:06 +08:00
|
|
|
|
logger.info(f"📑 界面扫描完成,当前对话历史共 {len(dialogue_log)} 条")
|
|
|
|
|
|
|
2026-01-25 10:28:31 +08:00
|
|
|
|
# 2. 检查是否有新消息
|
|
|
|
|
|
current_last_msg = dialogue_log[-1]
|
2026-01-25 11:52:06 +08:00
|
|
|
|
logger.info(f"💬 当前最后一条消息: {current_last_msg}")
|
2026-01-25 10:28:31 +08:00
|
|
|
|
history_text = "\n".join(dialogue_log)
|
|
|
|
|
|
|
|
|
|
|
|
# 判断逻辑:如果最后一条消息是“对方”发的,且与上次不同,则回复
|
|
|
|
|
|
if "对方:" in current_last_msg and current_last_msg != self.last_message_text:
|
2026-01-25 12:52:52 +08:00
|
|
|
|
# 关键检查:如果包含 "(待转换)",说明语音还没转文字,绝对不能回复
|
|
|
|
|
|
if "(待转换)" in current_last_msg:
|
|
|
|
|
|
logger.info(f"🚫 检测到未转换的语音消息,跳过回复生成,等待转文字... ({current_last_msg})")
|
|
|
|
|
|
await asyncio.sleep(2) # 稍作等待
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-01-25 10:28:31 +08:00
|
|
|
|
logger.info(f"📩 检测到新消息: {current_last_msg}")
|
|
|
|
|
|
|
|
|
|
|
|
reply = await self.get_reply(history_text)
|
|
|
|
|
|
logger.info(f"🤖 生成回复: {reply}")
|
|
|
|
|
|
|
|
|
|
|
|
# 执行输入发送
|
2026-01-25 12:52:52 +08:00
|
|
|
|
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}")
|
|
|
|
|
|
|
2026-01-25 11:52:06 +08:00
|
|
|
|
# 即使 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 # 重置主动询问计数
|
2026-01-25 10:28:31 +08:00
|
|
|
|
|
|
|
|
|
|
# 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}")
|
|
|
|
|
|
|
2026-01-25 12:52:52 +08:00
|
|
|
|
if input_center:
|
|
|
|
|
|
center_point = input_center
|
|
|
|
|
|
else:
|
|
|
|
|
|
center_point, _ = find_input_box_center(tmp_shot)
|
|
|
|
|
|
|
2026-01-25 11:52:06 +08:00
|
|
|
|
# 同上,解耦 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}"
|
2026-01-25 10:28:31 +08:00
|
|
|
|
|
|
|
|
|
|
# 更新最后一条消息记录(仅用于对比)
|
|
|
|
|
|
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())
|