163 lines
7.2 KiB
Python
163 lines
7.2 KiB
Python
# coding=utf-8
|
||
import asyncio
|
||
import logging
|
||
import os
|
||
import sys
|
||
import time
|
||
from datetime import datetime
|
||
import hashlib
|
||
|
||
# 添加项目根目录到 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 Util import Win32Patch
|
||
from WeiXin import WxUtil
|
||
from WeiXin.WxUtil import perform_input_action
|
||
from Util.LlmUtil import get_llm_response
|
||
|
||
# 配置日志
|
||
log_dir = WxUtil.LOG_DIR
|
||
if not os.path.exists(log_dir):
|
||
os.makedirs(log_dir)
|
||
|
||
# 设置 logger
|
||
logger = logging.getLogger("T5_AutoChatMonitor")
|
||
logger.setLevel(logging.INFO)
|
||
|
||
if logger.hasHandlers():
|
||
logger.handlers.clear()
|
||
|
||
log_file_path = os.path.join(log_dir, "T5_AutoChatMonitor.log")
|
||
file_handler = logging.FileHandler(log_file_path, encoding='utf-8', mode='w')
|
||
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||
logger.addHandler(file_handler)
|
||
|
||
stream_handler = logging.StreamHandler()
|
||
stream_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||
logger.addHandler(stream_handler)
|
||
|
||
logger.propagate = False
|
||
logger.info(f"日志文件路径: {log_file_path}")
|
||
|
||
# 配置参数
|
||
CHECK_INTERVAL = 5 # 检查频率 (秒)
|
||
|
||
class ChatBot:
|
||
def __init__(self):
|
||
# 运行前清理 Logs 和 Output
|
||
WxUtil.setup_script_environment()
|
||
|
||
self.d = WxUtil.connect_device()
|
||
if not self.d:
|
||
raise Exception("无法连接到设备,任务终止")
|
||
|
||
self.last_processed_msg_hash = None # 记录最后一条已处理消息的哈希值
|
||
self.screenshot_dir = WxUtil.OUTPUT_DIR
|
||
|
||
self.persona = (
|
||
"你是一名1999年毕业、拥有27年一线教学经验的小学高级女教师,名叫‘大张老师’。你目前在‘长春市少惠林作文素养培养中心’工作。"
|
||
"你不仅是一位作文教学专家,更是一位心思细腻、能与家长共情的教育智者。"
|
||
"你的回复风格应该是:温柔、知性、亲切,就像一位邻家大姐姐在聊天。"
|
||
"【严格约束】:\n"
|
||
"1. 绝对禁止发散!绝对禁止幻觉!\n"
|
||
"2. 知道什么就说什么,不要乱讲话,不要自己编造内容!\n"
|
||
"3. 仅针对家长明确表达的内容进行回复。\n"
|
||
"4. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n"
|
||
"5. 回复必须简练,字数严格控制在 50 字以内!\n"
|
||
"如果涉及到校区信息,必须且只能使用以下真实数据:\n"
|
||
"- 单位:长春市少惠林作文素养培养中心\n"
|
||
"- 地址:南环城路与临河街交汇,TOUCH12街3楼325号\n"
|
||
"- 联系人:小张老师(电话:18686619970)\n"
|
||
"- 每学期开学招收小学三年级至六年级,初中七年级的学生入学,其它年段不招生。\n"
|
||
)
|
||
|
||
async def get_reply(self, last_message_text):
|
||
prompt = (
|
||
f"【教师人设】:{self.persona}\n\n"
|
||
f"【最后一条消息】:\n{last_message_text}\n\n"
|
||
"【任务要求】:\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("🚀 大张老师自动巡课系统启动 (CV版)...")
|
||
|
||
while True:
|
||
try:
|
||
# 1. 截图并分析
|
||
image_path = os.path.join(self.screenshot_dir, "current_screen.jpg")
|
||
self.d.screenshot(image_path)
|
||
|
||
# 使用 WxUtil 的集中式分析逻辑
|
||
# 它会自动处理语音转文字,并返回对话列表和输入框坐标
|
||
dialogue_log, input_pos = await WxUtil.analyze_chat_image(image_path, self.screenshot_dir, device=self.d)
|
||
|
||
if not dialogue_log:
|
||
logger.info("😴 未发现有效消息,等待下一次轮询。")
|
||
await asyncio.sleep(CHECK_INTERVAL)
|
||
continue
|
||
|
||
# 2. 只关注最后一条消息
|
||
last_msg = dialogue_log[-1]
|
||
logger.info(f"最后一条消息: {last_msg}")
|
||
|
||
# 计算最后一条消息的哈希值,用于去重
|
||
current_msg_hash = hashlib.md5(last_msg.encode('utf-8')).hexdigest()
|
||
|
||
# 3. 判断是否需要回复
|
||
# 规则:最后一条消息由“对方”发送,且不是上一次处理过的消息
|
||
if "对方:" in last_msg:
|
||
if current_msg_hash != self.last_processed_msg_hash:
|
||
logger.info(f"💡 发现新消息,准备生成回复: {last_msg}")
|
||
|
||
# 生成回复
|
||
reply = await self.get_reply(last_msg)
|
||
|
||
if reply:
|
||
logger.info(f"🤖 LLM 回复: {reply}")
|
||
# 执行输入和发送
|
||
if input_pos:
|
||
perform_input_action(self.d, input_pos, reply)
|
||
logger.info("✅ 回复已发送")
|
||
# 成功发送后更新最后处理的消息哈希
|
||
self.last_processed_msg_hash = current_msg_hash
|
||
else:
|
||
logger.warning("❌ 未找到输入框位置,无法发送回复")
|
||
else:
|
||
logger.warning("⚠️ LLM 未生成有效回复")
|
||
else:
|
||
# 消息已处理过,不重复回复
|
||
pass
|
||
else:
|
||
# 最后一条是我发送的或者是系统消息,更新哈希以防之后重复处理(如果之后又变成对方发)
|
||
# 或者简单地跳过
|
||
if current_msg_hash != self.last_processed_msg_hash:
|
||
logger.info(f"⚪ 最后一条消息非对方发送,无需回复: {last_msg}")
|
||
self.last_processed_msg_hash = current_msg_hash
|
||
|
||
# 4. 休眠
|
||
await asyncio.sleep(CHECK_INTERVAL)
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 主循环发生错误: {e}", exc_info=True)
|
||
await asyncio.sleep(CHECK_INTERVAL)
|
||
|
||
if __name__ == "__main__":
|
||
Win32Patch.patch()
|
||
bot = ChatBot()
|
||
try:
|
||
asyncio.run(bot.run())
|
||
except KeyboardInterrupt:
|
||
logger.info("🛑 用户手动停止程序。")
|
||
except Exception as e:
|
||
logger.error(f"❌ 程序异常退出: {e}", exc_info=True)
|