This commit is contained in:
HuangHai
2026-01-26 09:42:25 +08:00
parent 16b4d30f91
commit 437df303c5
11 changed files with 221 additions and 187 deletions

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

View File

@@ -12,7 +12,7 @@ if project_root not in sys.path:
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):
os.makedirs(log_dir)
@@ -27,6 +27,9 @@ logging.basicConfig(
logger = logging.getLogger("T1_OpenChat")
def open_chat(target_name="糖豆爸爸"):
# 运行前清理 Logs 和 Output
WxUtil.setup_script_environment()
logger.info(f"开始执行 T1: 打开微信并进入 '{target_name}' 的对话框...")
# 连接设备

View File

@@ -13,7 +13,7 @@ from WeiXin import WxUtil
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):
os.makedirs(log_dir)
@@ -28,6 +28,9 @@ logging.basicConfig(
logger = logging.getLogger("T2_GetHistory")
async def get_history(target_name="对方"):
# 运行前清理 Logs 和 Output
WxUtil.setup_script_environment()
logger.info("开始执行 T2: 获取当前屏幕对话历史...")
d = WxUtil.connect_device()
@@ -35,7 +38,7 @@ async def get_history(target_name="对方"):
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):
os.makedirs(screenshot_dir)

View File

@@ -15,7 +15,7 @@ from WeiXin.WxUtil import find_input_box_center, perform_input_action, analyze_c
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):
os.makedirs(log_dir)
@@ -30,10 +30,10 @@ logging.basicConfig(
logger = logging.getLogger("T3_InputLlmText")
async def generate_and_input():
logger.info("开始执行 T3: 生成 LLM 回复并输入...")
# 运行前清理 Logs 和 Output
WxUtil.setup_script_environment()
# 0. 清除旧截图
clean_screenshots_dir()
logger.info("开始执行 T3: 生成 LLM 回复并输入...")
try:
# 1. 连接设备
@@ -42,9 +42,7 @@ async def generate_and_input():
return
# 2. 截图并识别对话历史
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
screenshot_dir = WxUtil.OUTPUT_DIR
tmp_shot = os.path.join(screenshot_dir, "t4_temp_history_check.jpg")
analyzed_shot = os.path.join(screenshot_dir, "t4_temp_history_analyzed.jpg")

View File

@@ -4,6 +4,7 @@ import sys
import time
import cv2
import logging
# 添加项目根目录到 sys.path
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -13,49 +14,66 @@ if project_root not in sys.path:
from WeiXin import WxUtil
from WeiXin.WxUtil import find_all_template_matches
# 配置日志
log_dir = WxUtil.LOG_DIR
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, "T4_CV_Voice_Debug.log"), encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger("T4_CV_Voice_Debug")
def run_cv_debug():
# 运行前清理 Logs 和 Output
WxUtil.setup_script_environment()
# 1. 拍照 (获取当前设备屏幕)
print("📸 正在连接设备并截取屏幕...")
logger.info("📸 正在连接设备并截取屏幕...")
d = WxUtil.connect_device()
if not d:
return
try:
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
screenshot_dir = WxUtil.OUTPUT_DIR
image_path = os.path.join(screenshot_dir, "t4_live_shot.jpg")
output_path = os.path.join(screenshot_dir, "T4_debug_view.jpg")
d.screenshot(image_path)
print(f"✅ 截图已保存: {image_path}")
logger.info(f"✅ 截图已保存: {image_path}")
except Exception as e:
print(f"❌ 拍照失败: {e}")
logger.error(f"❌ 拍照失败: {e}")
return
print(f"🔍 正在分析实时图片...")
logger.info(f"🔍 正在分析实时图片...")
# 模板路径
audio_template = r"d:\dsWork\aiData\WeiXin\Templates\audio.jpg"
red_point_template = r"d:\dsWork\aiData\WeiXin\Templates\red_point.jpg"
template_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Templates")
audio_template = os.path.join(template_dir, "audio.jpg")
red_point_template = os.path.join(template_dir, "red_point.jpg")
if not os.path.exists(audio_template) or not os.path.exists(red_point_template):
print("错误: 模板文件不存在")
logger.error("错误: 模板文件不存在")
return
# 2. 识别逻辑
audio_matches = find_all_template_matches(image_path, audio_template, threshold=0.8)
red_points = find_all_template_matches(image_path, red_point_template, threshold=0.8)
print(f"发现语音图标数量: {len(audio_matches)}")
print(f"发现红点数量: {len(red_points)}")
logger.info(f"发现语音图标数量: {len(audio_matches)}")
logger.info(f"发现红点数量: {len(red_points)}")
# 3. 读取图片并绘制
img = cv2.imread(image_path)
if img is None:
print("错误: 无法读取图片")
logger.error("错误: 无法读取图片")
return
for ax, ay in audio_matches:
@@ -90,11 +108,11 @@ def run_cv_debug():
if abs(ry - ay) < 50 and rx > ax:
cv2.circle(img, (int(rx), int(ry)), 12, (0, 0, 255), -1)
print(f"标注语音消息: ({ax}, {ay}), 发送者: {sender}, 状态: {status_text}")
logger.info(f"标注语音消息: ({ax}, {ay}), 发送者: {sender}, 状态: {status_text}")
# 保存结果
cv2.imwrite(output_path, img)
print(f"✅ 调试图片已保存至: {output_path}")
logger.info(f"✅ 调试图片已保存至: {output_path}")
if __name__ == "__main__":
run_cv_debug()

View File

@@ -19,12 +19,12 @@ from Util.LlmUtil import get_llm_response
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):
os.makedirs(log_dir)
# 设置 logger
logger = logging.getLogger("T6_AutoChatMonitor")
logger = logging.getLogger("T5_AutoChatMonitor")
logger.setLevel(logging.INFO)
# 清除现有的 handlers,防止重复打印或配置冲突
@@ -32,7 +32,7 @@ if logger.hasHandlers():
logger.handlers.clear()
# 创建 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.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(file_handler)
@@ -50,17 +50,19 @@ logger.info(f"日志文件路径: {log_file_path}")
# 配置参数
CHECK_INTERVAL = 5 # 检查频率 (秒)
TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Templates")
class ChatBot:
def __init__(self):
# 运行前清理 Logs 和 Output
WxUtil.setup_script_environment()
self.d = WxUtil.connect_device()
if not self.d:
raise Exception("无法连接到设备,任务终止")
self.last_message_text = ""
self.last_processed_msg = None # 记录上一条已处理/回复过的对方消息内容
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.last_processed_msg_id = None # 记录上一条已处理的消息标识 (文本+坐标)
self.screenshot_dir = WxUtil.OUTPUT_DIR
self.ocr_kit = EasyOcrKit(gpu=True)
@@ -121,7 +123,7 @@ class ChatBot:
logger.info(f"📸 截取菜单图: {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}")
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")
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)
if cancel_btn:
@@ -249,9 +251,6 @@ class ChatBot:
async def run(self):
logger.info("🚀 大张老师自动巡课系统启动...")
# 0. 清除旧截图
clean_screenshots_dir()
last_screen_md5 = None
while True:
@@ -279,8 +278,8 @@ class ChatBot:
logger.info("<EFBFBD>️ 正在进行本地视觉扫描...")
# A. 寻找语音图标 (audio.jpg) 和 红点 (red_point.jpg)
audio_template = r"d:\dsWork\aiData\WeiXin\Templates\audio.jpg"
red_point_template = r"d:\dsWork\aiData\WeiXin\Templates\red_point.jpg"
audio_template = os.path.join(TEMPLATE_DIR, "audio.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)
red_points = find_all_template_matches(tmp_shot, red_point_template, threshold=0.8)
@@ -353,7 +352,8 @@ class ChatBot:
"y": ay,
"text": f"{sender}: [语音] {duration_text}",
"is_voice": True,
"id": f"voice_{ax}_{ay}"
"id": f"voice_{ax}_{ay}",
"v_msg": v_msg
})
# 处理剩余的 OCR 文字块 (普通文本)
@@ -391,7 +391,7 @@ class ChatBot:
logger.info(f"🎨 已保存视觉调试图: {debug_shot_path}")
# 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)
if not input_center:
# 几何兜底:屏幕底部 88% 处
@@ -401,82 +401,66 @@ class ChatBot:
input_center = [w // 2, int(h * 0.88)]
logger.info(f"<EFBFBD> 使用几何兜底输入框坐标: {input_center}")
# 4. 语音处理逻辑
processed_voice_content = 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)
# 判断是否需要回复:
# 核心规则:只有当最后一条消息是“对方”说的,且内容未处理过,才回复。
# 4. & 5. 统一处理最后一条消息逻辑 (只看最后一条)
should_reply = False
current_last_content = ""
input_y = input_center[1] if input_center else None
if dialogue_log:
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("对方"):
parts = last_log.split(":", 1)
if len(parts) > 1:
current_last_content = parts[1].strip()
else:
current_last_content = last_log
# 核心规则:只有当最后一条消息是“对方”说的,且内容未处理过,才回复。
if last_text.startswith("对方"):
if current_msg_id != self.last_processed_msg_id:
logger.info(f"💡 发现新消息: {last_text}")
if current_last_content != self.last_processed_msg:
logger.info(f"💡 发现新消息,准备回复。内容: {current_last_content}")
should_reply = True
else:
# logger.info(f"⚪ 消息已回复过,跳过: {current_last_content}")
should_reply = False
# 如果是语音,且需要回复,则先转换
if last_item.get("is_voice"):
v_msg = last_item.get("v_msg")
if v_msg:
logger.info(f"🎤 最后一条是语音,开始转换: {v_msg['content']}")
converted_text = await self.process_single_voice(v_msg, None, input_y)
if converted_text:
# 更新文本内容以便 LLM 理解
last_item["text"] = f"对方: [语音转文字: {converted_text}]"
logger.info(f"✅ 语音转换成功: {converted_text}")
else:
logger.warning("⚠️ 语音转换未提取到文字,将尝试直接回复或跳过。")
should_reply = True
else:
# 消息已处理过
should_reply = False
else:
# logger.info(f"⚪ 最后一条消息是我发送的,无需回复。")
should_reply = False
# 如果最后一条是我发的,重置 last_processed_msg
self.last_processed_msg = None
# 最后一条是我发送的,或者是系统消息
should_reply = False
# 记录一下,避免在没有新消息时重复进入逻辑
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:
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)
if reply:
logger.info(f"💡 LLM 回复: {reply}")
if input_center:
# 输入并发送
perform_input_action(self.d, input_center, reply)
# 发送后,为了防止下一轮 OCR 识别到自己的回复片段并误判为对方消息
# 我们把 last_processed_msg 设置为一个特殊的占位符,直到下一次真正识别到对方的新消息
# 或者更简单:在下一轮循环开始前稍微多等一下,让消息气泡完全显示
time.sleep(1)
# 将最后处理的消息内容标记为已处理,防止 LLM 回复逻辑在下一轮立即触发
# 注意:这里的 current_last_content 是对方的最后一条
time.sleep(1) # 等待发送完成
else:
logger.warning("⚠️ LLM 未生成有效回复。")
self.is_first_run = False
# 休眠
await asyncio.sleep(CHECK_INTERVAL)

View File

@@ -25,6 +25,35 @@ ocr_kit = EasyOcrKit()
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
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():
"""
连接设备并返回设备对象,同时打印详细的设备信息
@@ -328,15 +357,14 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name="
def clean_screenshots_dir():
"""清理截图目录"""
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
if not os.path.exists(screenshot_dir):
os.makedirs(screenshot_dir)
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
return
for f in os.listdir(screenshot_dir):
for f in os.listdir(OUTPUT_DIR):
if f.lower().endswith(('.jpg', '.png', '.jpeg')):
try:
os.remove(os.path.join(screenshot_dir, f))
os.remove(os.path.join(OUTPUT_DIR, f))
except Exception as 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:
# --- 新增逻辑:确保处于文字输入模式 ---
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)
wen_zi_template = r"d:\dsWork\aiData\WeiXin\Templates\wen_zi_input.jpg"
input_text_template = r"d:\dsWork\aiData\WeiXin\Templates\input_text.jpg"
wen_zi_template = os.path.join(TEMPLATE_DIR, "wen_zi_input.jpg")
input_text_template = os.path.join(TEMPLATE_DIR, "input_text.jpg")
# 1. 检查是否存在 '切换到文字' 图标 (表示当前是语音模式)
# 注意:这里假设 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")
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) # 稍微降低阈值以提高召回
if send_btn_pos: