'commit'
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 113 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
@@ -12,7 +12,7 @@ if project_root not in sys.path:
|
|||||||
sys.path.append(project_root)
|
sys.path.append(project_root)
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs")
|
log_dir = WxUtil.LOG_DIR
|
||||||
if not os.path.exists(log_dir):
|
if not os.path.exists(log_dir):
|
||||||
os.makedirs(log_dir)
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
@@ -27,6 +27,9 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger("T1_OpenChat")
|
logger = logging.getLogger("T1_OpenChat")
|
||||||
|
|
||||||
def open_chat(target_name="糖豆爸爸"):
|
def open_chat(target_name="糖豆爸爸"):
|
||||||
|
# 运行前清理 Logs 和 Output
|
||||||
|
WxUtil.setup_script_environment()
|
||||||
|
|
||||||
logger.info(f"开始执行 T1: 打开微信并进入 '{target_name}' 的对话框...")
|
logger.info(f"开始执行 T1: 打开微信并进入 '{target_name}' 的对话框...")
|
||||||
|
|
||||||
# 连接设备
|
# 连接设备
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from WeiXin import WxUtil
|
|||||||
from WeiXin.WxUtil import analyze_chat_image
|
from WeiXin.WxUtil import analyze_chat_image
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs")
|
log_dir = WxUtil.LOG_DIR
|
||||||
if not os.path.exists(log_dir):
|
if not os.path.exists(log_dir):
|
||||||
os.makedirs(log_dir)
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
@@ -28,6 +28,9 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger("T2_GetHistory")
|
logger = logging.getLogger("T2_GetHistory")
|
||||||
|
|
||||||
async def get_history(target_name="对方"):
|
async def get_history(target_name="对方"):
|
||||||
|
# 运行前清理 Logs 和 Output
|
||||||
|
WxUtil.setup_script_environment()
|
||||||
|
|
||||||
logger.info("开始执行 T2: 获取当前屏幕对话历史...")
|
logger.info("开始执行 T2: 获取当前屏幕对话历史...")
|
||||||
|
|
||||||
d = WxUtil.connect_device()
|
d = WxUtil.connect_device()
|
||||||
@@ -35,7 +38,7 @@ async def get_history(target_name="对方"):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 截图
|
# 截图
|
||||||
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
|
screenshot_dir = WxUtil.OUTPUT_DIR
|
||||||
if not os.path.exists(screenshot_dir):
|
if not os.path.exists(screenshot_dir):
|
||||||
os.makedirs(screenshot_dir)
|
os.makedirs(screenshot_dir)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from WeiXin.WxUtil import find_input_box_center, perform_input_action, analyze_c
|
|||||||
from Util.LlmUtil import get_llm_response
|
from Util.LlmUtil import get_llm_response
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs")
|
log_dir = WxUtil.LOG_DIR
|
||||||
if not os.path.exists(log_dir):
|
if not os.path.exists(log_dir):
|
||||||
os.makedirs(log_dir)
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
@@ -30,10 +30,10 @@ logging.basicConfig(
|
|||||||
logger = logging.getLogger("T3_InputLlmText")
|
logger = logging.getLogger("T3_InputLlmText")
|
||||||
|
|
||||||
async def generate_and_input():
|
async def generate_and_input():
|
||||||
logger.info("开始执行 T3: 生成 LLM 回复并输入...")
|
# 运行前清理 Logs 和 Output
|
||||||
|
WxUtil.setup_script_environment()
|
||||||
|
|
||||||
# 0. 清除旧截图
|
logger.info("开始执行 T3: 生成 LLM 回复并输入...")
|
||||||
clean_screenshots_dir()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1. 连接设备
|
# 1. 连接设备
|
||||||
@@ -42,9 +42,7 @@ async def generate_and_input():
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 2. 截图并识别对话历史
|
# 2. 截图并识别对话历史
|
||||||
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
|
screenshot_dir = WxUtil.OUTPUT_DIR
|
||||||
if not os.path.exists(screenshot_dir):
|
|
||||||
os.makedirs(screenshot_dir)
|
|
||||||
|
|
||||||
tmp_shot = os.path.join(screenshot_dir, "t4_temp_history_check.jpg")
|
tmp_shot = os.path.join(screenshot_dir, "t4_temp_history_check.jpg")
|
||||||
analyzed_shot = os.path.join(screenshot_dir, "t4_temp_history_analyzed.jpg")
|
analyzed_shot = os.path.join(screenshot_dir, "t4_temp_history_analyzed.jpg")
|
||||||
|
|||||||
@@ -3,98 +3,116 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
|
import logging
|
||||||
# 添加项目根目录到 sys.path
|
|
||||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
# 添加项目根目录到 sys.path
|
||||||
if project_root not in sys.path:
|
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
sys.path.append(project_root)
|
if project_root not in sys.path:
|
||||||
|
sys.path.append(project_root)
|
||||||
from WeiXin import WxUtil
|
|
||||||
from WeiXin.WxUtil import find_all_template_matches
|
from WeiXin import WxUtil
|
||||||
|
from WeiXin.WxUtil import find_all_template_matches
|
||||||
|
|
||||||
def run_cv_debug():
|
# 配置日志
|
||||||
# 1. 拍照 (获取当前设备屏幕)
|
log_dir = WxUtil.LOG_DIR
|
||||||
print("📸 正在连接设备并截取屏幕...")
|
if not os.path.exists(log_dir):
|
||||||
d = WxUtil.connect_device()
|
os.makedirs(log_dir)
|
||||||
if not d:
|
|
||||||
return
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
try:
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
|
handlers=[
|
||||||
if not os.path.exists(screenshot_dir):
|
logging.FileHandler(os.path.join(log_dir, "T4_CV_Voice_Debug.log"), encoding='utf-8'),
|
||||||
os.makedirs(screenshot_dir)
|
logging.StreamHandler()
|
||||||
|
]
|
||||||
image_path = os.path.join(screenshot_dir, "t4_live_shot.jpg")
|
)
|
||||||
output_path = os.path.join(screenshot_dir, "T4_debug_view.jpg")
|
logger = logging.getLogger("T4_CV_Voice_Debug")
|
||||||
|
|
||||||
d.screenshot(image_path)
|
|
||||||
print(f"✅ 截图已保存: {image_path}")
|
def run_cv_debug():
|
||||||
except Exception as e:
|
# 运行前清理 Logs 和 Output
|
||||||
print(f"❌ 拍照失败: {e}")
|
WxUtil.setup_script_environment()
|
||||||
return
|
|
||||||
|
# 1. 拍照 (获取当前设备屏幕)
|
||||||
print(f"🔍 正在分析实时图片...")
|
logger.info("📸 正在连接设备并截取屏幕...")
|
||||||
|
d = WxUtil.connect_device()
|
||||||
# 模板路径
|
if not d:
|
||||||
audio_template = r"d:\dsWork\aiData\WeiXin\Templates\audio.jpg"
|
return
|
||||||
red_point_template = r"d:\dsWork\aiData\WeiXin\Templates\red_point.jpg"
|
|
||||||
|
try:
|
||||||
if not os.path.exists(audio_template) or not os.path.exists(red_point_template):
|
screenshot_dir = WxUtil.OUTPUT_DIR
|
||||||
print("错误: 模板文件不存在")
|
|
||||||
return
|
image_path = os.path.join(screenshot_dir, "t4_live_shot.jpg")
|
||||||
|
output_path = os.path.join(screenshot_dir, "T4_debug_view.jpg")
|
||||||
# 2. 识别逻辑
|
|
||||||
audio_matches = find_all_template_matches(image_path, audio_template, threshold=0.8)
|
d.screenshot(image_path)
|
||||||
red_points = find_all_template_matches(image_path, red_point_template, threshold=0.8)
|
logger.info(f"✅ 截图已保存: {image_path}")
|
||||||
|
except Exception as e:
|
||||||
print(f"发现语音图标数量: {len(audio_matches)}")
|
logger.error(f"❌ 拍照失败: {e}")
|
||||||
print(f"发现红点数量: {len(red_points)}")
|
return
|
||||||
|
|
||||||
# 3. 读取图片并绘制
|
logger.info(f"🔍 正在分析实时图片...")
|
||||||
img = cv2.imread(image_path)
|
|
||||||
if img is None:
|
# 模板路径
|
||||||
print("错误: 无法读取图片")
|
template_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Templates")
|
||||||
return
|
audio_template = os.path.join(template_dir, "audio.jpg")
|
||||||
|
red_point_template = os.path.join(template_dir, "red_point.jpg")
|
||||||
for ax, ay in audio_matches:
|
|
||||||
# 排除顶部标题栏和底部输入区 (假设 300-1800 为有效区)
|
if not os.path.exists(audio_template) or not os.path.exists(red_point_template):
|
||||||
if ay < 300 or ay > 1800:
|
logger.error("错误: 模板文件不存在")
|
||||||
continue
|
return
|
||||||
|
|
||||||
sender = "对方" if ax < 500 else "我"
|
# 2. 识别逻辑
|
||||||
|
audio_matches = find_all_template_matches(image_path, audio_template, threshold=0.8)
|
||||||
# --- 1. 先判断是否未读 (寻找附近的红点) ---
|
red_points = find_all_template_matches(image_path, red_point_template, threshold=0.8)
|
||||||
is_unread = False
|
|
||||||
for rx, ry in red_points:
|
logger.info(f"发现语音图标数量: {len(audio_matches)}")
|
||||||
if abs(ry - ay) < 50 and rx > ax:
|
logger.info(f"发现红点数量: {len(red_points)}")
|
||||||
is_unread = True
|
|
||||||
break
|
# 3. 读取图片并绘制
|
||||||
|
img = cv2.imread(image_path)
|
||||||
# --- 2. 根据状态选择颜色 ---
|
if img is None:
|
||||||
# BGR 格式: 红色 (0, 0, 255), 绿色 (0, 255, 0)
|
logger.error("错误: 无法读取图片")
|
||||||
color = (0, 0, 255) if is_unread else (0, 255, 0)
|
return
|
||||||
status_text = "未读" if is_unread else "已读"
|
|
||||||
|
for ax, ay in audio_matches:
|
||||||
# --- 3. 绘制标注 ---
|
# 排除顶部标题栏和底部输入区 (假设 300-1800 为有效区)
|
||||||
# 语音图标框 (加粗)
|
if ay < 300 or ay > 1800:
|
||||||
cv2.rectangle(img, (int(ax-35), int(ay-35)), (int(ax+35), int(ay+35)), color, 3)
|
continue
|
||||||
|
|
||||||
# 中心点击位置 (实心圆)
|
sender = "对方" if ax < 500 else "我"
|
||||||
cv2.circle(img, (int(ax), int(ay)), 15, color, -1)
|
|
||||||
|
# --- 1. 先判断是否未读 (寻找附近的红点) ---
|
||||||
# 如果是未读,把原本识别到的红点也再次标出
|
is_unread = False
|
||||||
if is_unread:
|
for rx, ry in red_points:
|
||||||
for rx, ry in red_points:
|
if abs(ry - ay) < 50 and rx > ax:
|
||||||
if abs(ry - ay) < 50 and rx > ax:
|
is_unread = True
|
||||||
cv2.circle(img, (int(rx), int(ry)), 12, (0, 0, 255), -1)
|
break
|
||||||
|
|
||||||
print(f"标注语音消息: ({ax}, {ay}), 发送者: {sender}, 状态: {status_text}")
|
# --- 2. 根据状态选择颜色 ---
|
||||||
|
# BGR 格式: 红色 (0, 0, 255), 绿色 (0, 255, 0)
|
||||||
# 保存结果
|
color = (0, 0, 255) if is_unread else (0, 255, 0)
|
||||||
cv2.imwrite(output_path, img)
|
status_text = "未读" if is_unread else "已读"
|
||||||
print(f"✅ 调试图片已保存至: {output_path}")
|
|
||||||
|
# --- 3. 绘制标注 ---
|
||||||
|
# 语音图标框 (加粗)
|
||||||
|
cv2.rectangle(img, (int(ax-35), int(ay-35)), (int(ax+35), int(ay+35)), color, 3)
|
||||||
|
|
||||||
|
# 中心点击位置 (实心圆)
|
||||||
|
cv2.circle(img, (int(ax), int(ay)), 15, color, -1)
|
||||||
|
|
||||||
|
# 如果是未读,把原本识别到的红点也再次标出
|
||||||
|
if is_unread:
|
||||||
|
for rx, ry in red_points:
|
||||||
|
if abs(ry - ay) < 50 and rx > ax:
|
||||||
|
cv2.circle(img, (int(rx), int(ry)), 12, (0, 0, 255), -1)
|
||||||
|
|
||||||
|
logger.info(f"标注语音消息: ({ax}, {ay}), 发送者: {sender}, 状态: {status_text}")
|
||||||
|
|
||||||
|
# 保存结果
|
||||||
|
cv2.imwrite(output_path, img)
|
||||||
|
logger.info(f"✅ 调试图片已保存至: {output_path}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run_cv_debug()
|
run_cv_debug()
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ from Util.LlmUtil import get_llm_response
|
|||||||
from Util.EasyOcrKit import EasyOcrKit
|
from Util.EasyOcrKit import EasyOcrKit
|
||||||
|
|
||||||
# 配置日志
|
# 配置日志
|
||||||
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Logs")
|
log_dir = WxUtil.LOG_DIR
|
||||||
if not os.path.exists(log_dir):
|
if not os.path.exists(log_dir):
|
||||||
os.makedirs(log_dir)
|
os.makedirs(log_dir)
|
||||||
|
|
||||||
# 设置 logger
|
# 设置 logger
|
||||||
logger = logging.getLogger("T6_AutoChatMonitor")
|
logger = logging.getLogger("T5_AutoChatMonitor")
|
||||||
logger.setLevel(logging.INFO)
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# 清除现有的 handlers,防止重复打印或配置冲突
|
# 清除现有的 handlers,防止重复打印或配置冲突
|
||||||
@@ -32,7 +32,7 @@ if logger.hasHandlers():
|
|||||||
logger.handlers.clear()
|
logger.handlers.clear()
|
||||||
|
|
||||||
# 创建 FileHandler
|
# 创建 FileHandler
|
||||||
log_file_path = os.path.join(log_dir, "T6_AutoChatMonitor.log")
|
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 = logging.FileHandler(log_file_path, encoding='utf-8', mode='w')
|
||||||
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
|
||||||
logger.addHandler(file_handler)
|
logger.addHandler(file_handler)
|
||||||
@@ -50,17 +50,19 @@ logger.info(f"日志文件路径: {log_file_path}")
|
|||||||
|
|
||||||
# 配置参数
|
# 配置参数
|
||||||
CHECK_INTERVAL = 5 # 检查频率 (秒)
|
CHECK_INTERVAL = 5 # 检查频率 (秒)
|
||||||
|
TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Templates")
|
||||||
|
|
||||||
class ChatBot:
|
class ChatBot:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
# 运行前清理 Logs 和 Output
|
||||||
|
WxUtil.setup_script_environment()
|
||||||
|
|
||||||
self.d = WxUtil.connect_device()
|
self.d = WxUtil.connect_device()
|
||||||
if not self.d:
|
if not self.d:
|
||||||
raise Exception("无法连接到设备,任务终止")
|
raise Exception("无法连接到设备,任务终止")
|
||||||
self.last_message_text = ""
|
self.last_message_text = ""
|
||||||
self.last_processed_msg = None # 记录上一条已处理/回复过的对方消息内容
|
self.last_processed_msg_id = None # 记录上一条已处理的消息标识 (文本+坐标)
|
||||||
self.screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
|
self.screenshot_dir = WxUtil.OUTPUT_DIR
|
||||||
if not os.path.exists(self.screenshot_dir):
|
|
||||||
os.makedirs(self.screenshot_dir)
|
|
||||||
|
|
||||||
self.ocr_kit = EasyOcrKit(gpu=True)
|
self.ocr_kit = EasyOcrKit(gpu=True)
|
||||||
|
|
||||||
@@ -121,7 +123,7 @@ class ChatBot:
|
|||||||
logger.info(f"📸 截取菜单图: {menu_shot_path}")
|
logger.info(f"📸 截取菜单图: {menu_shot_path}")
|
||||||
self.d.screenshot(menu_shot_path)
|
self.d.screenshot(menu_shot_path)
|
||||||
|
|
||||||
convert_template = r"d:\dsWork\aiData\WeiXin\Templates\zhun_wen_zi.jpg"
|
convert_template = os.path.join(TEMPLATE_DIR, "zhun_wen_zi.jpg")
|
||||||
logger.info(f"🔍 寻找模板: {convert_template}")
|
logger.info(f"🔍 寻找模板: {convert_template}")
|
||||||
convert_btn = find_template_match(menu_shot_path, convert_template, threshold=0.6)
|
convert_btn = find_template_match(menu_shot_path, convert_template, threshold=0.6)
|
||||||
|
|
||||||
@@ -234,7 +236,7 @@ class ChatBot:
|
|||||||
menu_shot_path_cancel = os.path.join(self.screenshot_dir, "t6_menu_shot_cancel.jpg")
|
menu_shot_path_cancel = os.path.join(self.screenshot_dir, "t6_menu_shot_cancel.jpg")
|
||||||
self.d.screenshot(menu_shot_path_cancel)
|
self.d.screenshot(menu_shot_path_cancel)
|
||||||
|
|
||||||
cancel_template = r"d:\dsWork\aiData\WeiXin\Templates\cancel_zhuan_wen_zi.jpg"
|
cancel_template = os.path.join(TEMPLATE_DIR, "cancel_zhuan_wen_zi.jpg")
|
||||||
cancel_btn = find_template_match(menu_shot_path_cancel, cancel_template, threshold=0.6)
|
cancel_btn = find_template_match(menu_shot_path_cancel, cancel_template, threshold=0.6)
|
||||||
|
|
||||||
if cancel_btn:
|
if cancel_btn:
|
||||||
@@ -249,9 +251,6 @@ class ChatBot:
|
|||||||
async def run(self):
|
async def run(self):
|
||||||
logger.info("🚀 大张老师自动巡课系统启动...")
|
logger.info("🚀 大张老师自动巡课系统启动...")
|
||||||
|
|
||||||
# 0. 清除旧截图
|
|
||||||
clean_screenshots_dir()
|
|
||||||
|
|
||||||
last_screen_md5 = None
|
last_screen_md5 = None
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -279,8 +278,8 @@ class ChatBot:
|
|||||||
logger.info("<EFBFBD>️ 正在进行本地视觉扫描...")
|
logger.info("<EFBFBD>️ 正在进行本地视觉扫描...")
|
||||||
|
|
||||||
# A. 寻找语音图标 (audio.jpg) 和 红点 (red_point.jpg)
|
# A. 寻找语音图标 (audio.jpg) 和 红点 (red_point.jpg)
|
||||||
audio_template = r"d:\dsWork\aiData\WeiXin\Templates\audio.jpg"
|
audio_template = os.path.join(TEMPLATE_DIR, "audio.jpg")
|
||||||
red_point_template = r"d:\dsWork\aiData\WeiXin\Templates\red_point.jpg"
|
red_point_template = os.path.join(TEMPLATE_DIR, "red_point.jpg")
|
||||||
|
|
||||||
audio_matches = find_all_template_matches(tmp_shot, audio_template, threshold=0.8)
|
audio_matches = find_all_template_matches(tmp_shot, audio_template, threshold=0.8)
|
||||||
red_points = find_all_template_matches(tmp_shot, red_point_template, threshold=0.8)
|
red_points = find_all_template_matches(tmp_shot, red_point_template, threshold=0.8)
|
||||||
@@ -353,7 +352,8 @@ class ChatBot:
|
|||||||
"y": ay,
|
"y": ay,
|
||||||
"text": f"{sender}: [语音] {duration_text}",
|
"text": f"{sender}: [语音] {duration_text}",
|
||||||
"is_voice": True,
|
"is_voice": True,
|
||||||
"id": f"voice_{ax}_{ay}"
|
"id": f"voice_{ax}_{ay}",
|
||||||
|
"v_msg": v_msg
|
||||||
})
|
})
|
||||||
|
|
||||||
# 处理剩余的 OCR 文字块 (普通文本)
|
# 处理剩余的 OCR 文字块 (普通文本)
|
||||||
@@ -391,7 +391,7 @@ class ChatBot:
|
|||||||
logger.info(f"🎨 已保存视觉调试图: {debug_shot_path}")
|
logger.info(f"🎨 已保存视觉调试图: {debug_shot_path}")
|
||||||
|
|
||||||
# C. 寻找输入框 (CV 模板匹配)
|
# C. 寻找输入框 (CV 模板匹配)
|
||||||
input_template = r"d:\dsWork\aiData\WeiXin\Templates\input_box.jpg" # 假设有这个模板
|
input_template = os.path.join(TEMPLATE_DIR, "input_box.jpg") # 假设有这个模板
|
||||||
input_center = find_template_match(tmp_shot, input_template, threshold=0.6)
|
input_center = find_template_match(tmp_shot, input_template, threshold=0.6)
|
||||||
if not input_center:
|
if not input_center:
|
||||||
# 几何兜底:屏幕底部 88% 处
|
# 几何兜底:屏幕底部 88% 处
|
||||||
@@ -401,81 +401,65 @@ class ChatBot:
|
|||||||
input_center = [w // 2, int(h * 0.88)]
|
input_center = [w // 2, int(h * 0.88)]
|
||||||
logger.info(f"<EFBFBD> 使用几何兜底输入框坐标: {input_center}")
|
logger.info(f"<EFBFBD> 使用几何兜底输入框坐标: {input_center}")
|
||||||
|
|
||||||
# 4. 语音处理逻辑
|
# 4. & 5. 统一处理最后一条消息逻辑 (只看最后一条)
|
||||||
processed_voice_content = None
|
should_reply = False
|
||||||
input_y = input_center[1] if input_center else None
|
input_y = input_center[1] if input_center else None
|
||||||
|
|
||||||
# 只有未读的才处理
|
|
||||||
for v_msg in voice_messages:
|
|
||||||
if v_msg.get("is_unread") or self.is_first_run:
|
|
||||||
logger.info(f"🔴 发现未读/待处理语音: {v_msg['content']}")
|
|
||||||
# 找到 OCR 结果中的下一条作为边界
|
|
||||||
idx = -1
|
|
||||||
# 这里简化逻辑,直接处理
|
|
||||||
text = await self.process_single_voice(v_msg, None, input_y)
|
|
||||||
if text:
|
|
||||||
# 更新 log 中的内容
|
|
||||||
for item in dialogue_log:
|
|
||||||
if item.get("is_voice") and f"[语音] {v_msg['content']}" in item["text"]:
|
|
||||||
item["text"] = item["text"].replace("[语音]", f"[语音转文字: {text}]")
|
|
||||||
break
|
|
||||||
|
|
||||||
self.is_first_run = False
|
|
||||||
|
|
||||||
# 5. LLM 回复逻辑
|
|
||||||
final_dialogue_texts = [item['text'] for item in dialogue_log]
|
|
||||||
history_text = "\n".join(final_dialogue_texts)
|
|
||||||
|
|
||||||
# 判断是否需要回复:
|
|
||||||
# 核心规则:只有当最后一条消息是“对方”说的,且内容未处理过,才回复。
|
|
||||||
|
|
||||||
should_reply = False
|
|
||||||
current_last_content = ""
|
|
||||||
|
|
||||||
if dialogue_log:
|
if dialogue_log:
|
||||||
last_item = dialogue_log[-1]
|
last_item = dialogue_log[-1]
|
||||||
last_log = last_item["text"]
|
last_text = last_item["text"]
|
||||||
|
# 构造唯一标识符:文本内容 + 坐标 (Y坐标取整到10像素以容纳轻微位移)
|
||||||
|
current_msg_id = f"{last_text}_{int(last_item['y']/10)*10}"
|
||||||
|
|
||||||
# 检查最后一条消息的发送者
|
# 核心规则:只有当最后一条消息是“对方”说的,且内容未处理过,才回复。
|
||||||
if last_log.startswith("对方"):
|
if last_text.startswith("对方"):
|
||||||
parts = last_log.split(":", 1)
|
if current_msg_id != self.last_processed_msg_id:
|
||||||
if len(parts) > 1:
|
logger.info(f"💡 发现新消息: {last_text}")
|
||||||
current_last_content = parts[1].strip()
|
|
||||||
else:
|
# 如果是语音,且需要回复,则先转换
|
||||||
current_last_content = last_log
|
if last_item.get("is_voice"):
|
||||||
|
v_msg = last_item.get("v_msg")
|
||||||
if current_last_content != self.last_processed_msg:
|
if v_msg:
|
||||||
logger.info(f"💡 发现新消息,准备回复。内容: {current_last_content}")
|
logger.info(f"🎤 最后一条是语音,开始转换: {v_msg['content']}")
|
||||||
should_reply = True
|
converted_text = await self.process_single_voice(v_msg, None, input_y)
|
||||||
else:
|
if converted_text:
|
||||||
# logger.info(f"⚪ 消息已回复过,跳过: {current_last_content}")
|
# 更新文本内容以便 LLM 理解
|
||||||
should_reply = False
|
last_item["text"] = f"对方: [语音转文字: {converted_text}]"
|
||||||
|
logger.info(f"✅ 语音转换成功: {converted_text}")
|
||||||
|
else:
|
||||||
|
logger.warning("⚠️ 语音转换未提取到文字,将尝试直接回复或跳过。")
|
||||||
|
|
||||||
|
should_reply = True
|
||||||
|
else:
|
||||||
|
# 消息已处理过
|
||||||
|
should_reply = False
|
||||||
else:
|
else:
|
||||||
# logger.info(f"⚪ 最后一条消息是我发送的,无需回复。")
|
# 最后一条是我发送的,或者是系统消息
|
||||||
should_reply = False
|
should_reply = False
|
||||||
# 如果最后一条是我发的,重置 last_processed_msg
|
# 记录一下,避免在没有新消息时重复进入逻辑
|
||||||
self.last_processed_msg = None
|
if current_msg_id != self.last_processed_msg_id:
|
||||||
|
self.last_processed_msg_id = current_msg_id
|
||||||
|
logger.info(f"⚪ 最后一条消息非对方发送,无需回复: {last_text}")
|
||||||
|
|
||||||
if should_reply:
|
if should_reply:
|
||||||
logger.info("🤖 准备调用 LLM 生成回复...")
|
logger.info("🤖 准备调用 LLM 生成回复...")
|
||||||
# 立即更新状态,防止在回复生成期间(如果耗时)重复触发
|
# 立即更新状态,防止在回复生成期间重复触发
|
||||||
self.last_processed_msg = current_last_content
|
self.last_processed_msg_id = current_msg_id
|
||||||
|
|
||||||
|
# 构建完整历史用于上下文
|
||||||
|
final_dialogue_texts = [item['text'] for item in dialogue_log]
|
||||||
|
history_text = "\n".join(final_dialogue_texts)
|
||||||
|
|
||||||
reply = await self.get_reply(history_text)
|
reply = await self.get_reply(history_text)
|
||||||
if reply:
|
if reply:
|
||||||
logger.info(f"💡 LLM 回复: {reply}")
|
logger.info(f"💡 LLM 回复: {reply}")
|
||||||
|
|
||||||
if input_center:
|
if input_center:
|
||||||
# 输入并发送
|
|
||||||
perform_input_action(self.d, input_center, reply)
|
perform_input_action(self.d, input_center, reply)
|
||||||
# 发送后,为了防止下一轮 OCR 识别到自己的回复片段并误判为对方消息
|
time.sleep(1) # 等待发送完成
|
||||||
# 我们把 last_processed_msg 设置为一个特殊的占位符,直到下一次真正识别到对方的新消息
|
|
||||||
# 或者更简单:在下一轮循环开始前稍微多等一下,让消息气泡完全显示
|
|
||||||
time.sleep(1)
|
|
||||||
# 将最后处理的消息内容标记为已处理,防止 LLM 回复逻辑在下一轮立即触发
|
|
||||||
# 注意:这里的 current_last_content 是对方的最后一条
|
|
||||||
else:
|
else:
|
||||||
logger.warning("⚠️ LLM 未生成有效回复。")
|
logger.warning("⚠️ LLM 未生成有效回复。")
|
||||||
|
|
||||||
|
self.is_first_run = False
|
||||||
|
|
||||||
# 休眠
|
# 休眠
|
||||||
await asyncio.sleep(CHECK_INTERVAL)
|
await asyncio.sleep(CHECK_INTERVAL)
|
||||||
|
|||||||
@@ -25,6 +25,35 @@ ocr_kit = EasyOcrKit()
|
|||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger("WxUtil")
|
logger = logging.getLogger("WxUtil")
|
||||||
|
|
||||||
|
# 目录配置
|
||||||
|
BASE_DATA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
LOG_DIR = os.path.join(BASE_DATA_DIR, "Logs")
|
||||||
|
OUTPUT_DIR = os.path.join(BASE_DATA_DIR, "Output")
|
||||||
|
TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Templates")
|
||||||
|
|
||||||
|
def clear_directory(dir_path):
|
||||||
|
"""清理指定目录下的所有文件"""
|
||||||
|
if not os.path.exists(dir_path):
|
||||||
|
os.makedirs(dir_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
for filename in os.listdir(dir_path):
|
||||||
|
file_path = os.path.join(dir_path, filename)
|
||||||
|
try:
|
||||||
|
if os.path.isfile(file_path) or os.path.islink(file_path):
|
||||||
|
os.unlink(file_path)
|
||||||
|
elif os.path.isdir(file_path):
|
||||||
|
shutil.rmtree(file_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to delete {file_path}. Reason: {e}")
|
||||||
|
|
||||||
|
def setup_script_environment():
|
||||||
|
"""运行前清理日志和输出目录"""
|
||||||
|
logger.info("清理运行环境: Logs 和 Output 目录...")
|
||||||
|
clear_directory(LOG_DIR)
|
||||||
|
clear_directory(OUTPUT_DIR)
|
||||||
|
|
||||||
def connect_device():
|
def connect_device():
|
||||||
"""
|
"""
|
||||||
连接设备并返回设备对象,同时打印详细的设备信息
|
连接设备并返回设备对象,同时打印详细的设备信息
|
||||||
@@ -328,15 +357,14 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name="
|
|||||||
|
|
||||||
def clean_screenshots_dir():
|
def clean_screenshots_dir():
|
||||||
"""清理截图目录"""
|
"""清理截图目录"""
|
||||||
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
|
if not os.path.exists(OUTPUT_DIR):
|
||||||
if not os.path.exists(screenshot_dir):
|
os.makedirs(OUTPUT_DIR)
|
||||||
os.makedirs(screenshot_dir)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
for f in os.listdir(screenshot_dir):
|
for f in os.listdir(OUTPUT_DIR):
|
||||||
if f.lower().endswith(('.jpg', '.png', '.jpeg')):
|
if f.lower().endswith(('.jpg', '.png', '.jpeg')):
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(screenshot_dir, f))
|
os.remove(os.path.join(OUTPUT_DIR, f))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Failed to delete {f}: {e}")
|
logger.warning(f"Failed to delete {f}: {e}")
|
||||||
|
|
||||||
@@ -475,11 +503,11 @@ def perform_input_action(d, center_point, text, auto_send=True):
|
|||||||
try:
|
try:
|
||||||
# --- 新增逻辑:确保处于文字输入模式 ---
|
# --- 新增逻辑:确保处于文字输入模式 ---
|
||||||
logger.info("正在检查输入模式...")
|
logger.info("正在检查输入模式...")
|
||||||
tmp_check_shot = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_input_check.jpg")
|
tmp_check_shot = os.path.join(OUTPUT_DIR, "temp_input_check.jpg")
|
||||||
d.screenshot(tmp_check_shot)
|
d.screenshot(tmp_check_shot)
|
||||||
|
|
||||||
wen_zi_template = r"d:\dsWork\aiData\WeiXin\Templates\wen_zi_input.jpg"
|
wen_zi_template = os.path.join(TEMPLATE_DIR, "wen_zi_input.jpg")
|
||||||
input_text_template = r"d:\dsWork\aiData\WeiXin\Templates\input_text.jpg"
|
input_text_template = os.path.join(TEMPLATE_DIR, "input_text.jpg")
|
||||||
|
|
||||||
# 1. 检查是否存在 '切换到文字' 图标 (表示当前是语音模式)
|
# 1. 检查是否存在 '切换到文字' 图标 (表示当前是语音模式)
|
||||||
# 注意:这里假设 wen_zi_input.jpg 是那个“键盘”图标
|
# 注意:这里假设 wen_zi_input.jpg 是那个“键盘”图标
|
||||||
@@ -548,8 +576,8 @@ def perform_input_action(d, center_point, text, auto_send=True):
|
|||||||
tmp_screen = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_send_check.jpg")
|
tmp_screen = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_send_check.jpg")
|
||||||
d.screenshot(tmp_screen)
|
d.screenshot(tmp_screen)
|
||||||
|
|
||||||
# 使用绝对路径
|
# 使用相对路径
|
||||||
template_path = r"d:\dsWork\aiData\WeiXin\Templates\send.jpg"
|
template_path = os.path.join(TEMPLATE_DIR, "send.jpg")
|
||||||
send_btn_pos = find_template_match(tmp_screen, template_path, threshold=0.7) # 稍微降低阈值以提高召回
|
send_btn_pos = find_template_match(tmp_screen, template_path, threshold=0.7) # 稍微降低阈值以提高召回
|
||||||
|
|
||||||
if send_btn_pos:
|
if send_btn_pos:
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user