'commit'
This commit is contained in:
361
WeiXin/ChatMonitorAll.py
Normal file
361
WeiXin/ChatMonitorAll.py
Normal file
@@ -0,0 +1,361 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
import cv2
|
||||
|
||||
# 添加项目根目录到 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 import WxUtil
|
||||
from WeiXin.WxUtil import perform_input_action
|
||||
from Util.LlmUtil import get_llm_response
|
||||
from Util import Win32Patch
|
||||
|
||||
# 配置日志
|
||||
log_dir = WxUtil.LOG_DIR
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
log_file_path = os.path.join(log_dir, "T2_ChatMonitor.log")
|
||||
|
||||
# 设置 logger
|
||||
logger = logging.getLogger("T2_ChatMonitor")
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if logger.hasHandlers():
|
||||
logger.handlers.clear()
|
||||
|
||||
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"🚀 日志文件路径: {os.path.abspath(log_file_path)}")
|
||||
|
||||
# 同时将 WxUtil 的日志也输出到同一个文件
|
||||
wx_logger = logging.getLogger("WxUtil")
|
||||
wx_logger.propagate = False # 防止日志向上传递导致重复 (因为 WxUtil 中调用了 basicConfig)
|
||||
if not any(isinstance(h, logging.FileHandler) and os.path.abspath(h.baseFilename) == os.path.abspath(log_file_path) for h in wx_logger.handlers):
|
||||
wx_logger.addHandler(file_handler)
|
||||
wx_logger.addHandler(stream_handler) # 确保 WxUtil 也输出到控制台
|
||||
|
||||
class ChatMonitorBot:
|
||||
"""
|
||||
大张老师自动巡课系统 (CV版)
|
||||
"""
|
||||
def __init__(self):
|
||||
self.device = None
|
||||
self.screenshot_path = os.path.join(WxUtil.OUTPUT_DIR, "T2_ChatMonitor_live_shot.jpg")
|
||||
self.debug_view_path = os.path.join(WxUtil.OUTPUT_DIR, "T2_ChatMonitor_debug_view.jpg")
|
||||
self.dialogue_log = []
|
||||
self.input_pos = None
|
||||
self.last_screen_hash = None
|
||||
self.last_processed_msg_hash = None
|
||||
# [User Requested] 移除持久化存储,只在内存中记录,重启即忘
|
||||
self.processed_hashes = set()
|
||||
# 新增:记录已处理消息的元数据 (sender, time_display, type) 用于防止空内容重试循环
|
||||
self.processed_meta = set()
|
||||
self.check_interval = 3 # 检查频率 (秒)
|
||||
|
||||
self.persona = (
|
||||
"你是一名1999年毕业、拥有27年一线教学经验的小学高级女教师,名叫‘大张老师’。你目前在‘长春市少惠林作文素养培养中心’工作。"
|
||||
"你不仅是一位作文教学专家,更是一位心思细腻、能与家长共情的教育智者。"
|
||||
"你的回复风格应该是:温柔、知性、亲切,就像一位邻家大姐姐在聊天。"
|
||||
"【严格约束】:\n"
|
||||
"1. 绝对禁止发散!绝对禁止幻觉!\n"
|
||||
"2. 知道什么就说什么,不要乱讲话,不要自己编造内容!\n"
|
||||
"3. 仅针对家长明确表达的内容进行回复。\n"
|
||||
"4. 严禁使用列表格式。严禁使用‘首先、其次’等逻辑词。\n"
|
||||
"5. 回复必须简练,字数严格控制在 50 字以内!\n"
|
||||
"6. 对方问什么就答什么。例如问‘学校叫什么’,就只回答‘少惠林’,不要回复地址和电话!\n"
|
||||
"如果涉及到校区信息,必须且只能使用以下真实数据:\n"
|
||||
"- 单位/学校名称:长春市少惠林作文素养培养中心(简称:少惠林)\n"
|
||||
"- 地址:南环城路与临河街交汇,TOUCH12街3楼325号\n"
|
||||
"- 联系人:小张老师(电话:18686619970)\n"
|
||||
"- 每学期开学招收小学三年级至六年级,初中七年级的学生入学,其它年段不招生。\n"
|
||||
)
|
||||
|
||||
def _record_processed_hash(self, msg, msg_hash):
|
||||
"""记录已处理的消息哈希和元数据 (仅内存)"""
|
||||
self.processed_hashes.add(msg_hash)
|
||||
|
||||
# 记录元数据 (Sender, Time, Type)
|
||||
if msg:
|
||||
meta = (msg.get("sender", ""), msg.get("time_display", ""), msg.get("type", ""))
|
||||
self.processed_meta.add(meta)
|
||||
|
||||
# 仅保留最近 100 条记录,防止无限增长
|
||||
if len(self.processed_hashes) > 100:
|
||||
# 简单丢弃旧的
|
||||
temp = list(self.processed_hashes)[-100:]
|
||||
self.processed_hashes = set(temp)
|
||||
|
||||
if len(self.processed_meta) > 100:
|
||||
temp_meta = list(self.processed_meta)[-100:]
|
||||
self.processed_meta = set(temp_meta)
|
||||
|
||||
async def get_reply(self, last_message_text, context_text=""):
|
||||
prompt = (
|
||||
f"【教师人设】:{self.persona}\n\n"
|
||||
f"【上下文对话内容】:\n{context_text}\n\n"
|
||||
f"【最后一条待回复消息】:\n{last_message_text}\n\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('”')
|
||||
|
||||
def step_1_prepare_env(self):
|
||||
"""步骤1: 环境准备"""
|
||||
logger.info("--- [Step 1] 环境准备 ---")
|
||||
WxUtil.setup_script_environment()
|
||||
return True
|
||||
|
||||
def step_2_connect_device(self):
|
||||
"""步骤2: 连接设备"""
|
||||
logger.info("--- [Step 2] 连接设备 ---")
|
||||
self.device = WxUtil.connect_device()
|
||||
if not self.device:
|
||||
logger.error("❌ 设备连接失败,请检查手机是否连接且开启了调试模式")
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_image_hash(self, file_path):
|
||||
"""计算图片的 MD5 哈希值 (忽略顶部 100 像素的状态栏)"""
|
||||
if not os.path.exists(file_path):
|
||||
return None
|
||||
try:
|
||||
# 使用 OpenCV 读取图片
|
||||
img = cv2.imread(file_path)
|
||||
if img is None:
|
||||
# 如果读取失败,回退到文件哈希
|
||||
with open(file_path, "rb") as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
# 裁剪掉顶部 150 像素 (状态栏/时间)
|
||||
h, w = img.shape[:2]
|
||||
if h > 150:
|
||||
cropped_img = img[150:h, 0:w]
|
||||
else:
|
||||
cropped_img = img
|
||||
|
||||
# 计算裁剪后数据的哈希
|
||||
return hashlib.md5(cropped_img.tobytes()).hexdigest()
|
||||
except Exception as e:
|
||||
logger.error(f"计算哈希出错: {e}, 回退到文件哈希")
|
||||
with open(file_path, "rb") as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
def get_stable_message_hash(self, msg):
|
||||
"""
|
||||
计算消息的稳定哈希值(忽略坐标等易变字段)
|
||||
仅包含: sender, content, time_display, type
|
||||
"""
|
||||
if not msg:
|
||||
return ""
|
||||
|
||||
stable_data = {
|
||||
"sender": msg.get("sender", ""),
|
||||
"content": msg.get("content") or "", # 确保 None 转为空字符串
|
||||
"time_display": msg.get("time_display", ""),
|
||||
"type": msg.get("type", "")
|
||||
}
|
||||
|
||||
# 序列化并计算哈希
|
||||
msg_str = json.dumps(stable_data, sort_keys=True, ensure_ascii=False)
|
||||
return hashlib.md5(msg_str.encode('utf-8')).hexdigest()
|
||||
|
||||
async def run(self):
|
||||
"""
|
||||
主运行循环
|
||||
"""
|
||||
logger.info("🚀 正在启动 T2_ChatMonitor (Auto-Reply)...")
|
||||
|
||||
# 定义 JSON 序列化辅助函数
|
||||
def numpy_serializer(obj):
|
||||
if isinstance(obj, np.integer):
|
||||
return int(obj)
|
||||
if isinstance(obj, np.floating):
|
||||
return float(obj)
|
||||
if isinstance(obj, np.ndarray):
|
||||
return obj.tolist()
|
||||
raise TypeError(f"Type {type(obj)} not serializable")
|
||||
|
||||
# 1. 环境准备
|
||||
if not self.step_1_prepare_env(): return
|
||||
if not self.step_2_connect_device(): return
|
||||
|
||||
# [User Requested] 移除首屏概念,直接进入监控循环
|
||||
# 以前说过什么都不管了,只关注最后一条
|
||||
logger.info("🚀 启动完成,直接进入实时监控阶段...")
|
||||
|
||||
# 3. 进入循环阶段
|
||||
while True:
|
||||
try:
|
||||
# A. 截图并计算哈希
|
||||
self.device.screenshot(self.screenshot_path)
|
||||
current_screen_hash = self.get_image_hash(self.screenshot_path)
|
||||
|
||||
# B. 如果屏幕无变化,则跳过识别
|
||||
if current_screen_hash == self.last_screen_hash:
|
||||
await asyncio.sleep(self.check_interval)
|
||||
continue
|
||||
|
||||
self.last_screen_hash = current_screen_hash
|
||||
logger.info("📸 屏幕发生变化,正在分析...")
|
||||
|
||||
# C. 分析最新图片
|
||||
dialogue_log, input_pos = await WxUtil.analyze_chat_image(
|
||||
self.screenshot_path,
|
||||
self.debug_view_path,
|
||||
device=self.device,
|
||||
process_strategy="UNREAD", # 监控阶段:只处理带红点的新语音
|
||||
restore_processed_voice=False # 不还原状态,防止红点未消导致重复转换
|
||||
)
|
||||
|
||||
if not dialogue_log:
|
||||
logger.info("😴 未识别到有效消息")
|
||||
await asyncio.sleep(self.check_interval)
|
||||
continue
|
||||
|
||||
logger.info(f"📊 当前识别到 {len(dialogue_log)} 条消息,最后一条: {dialogue_log[-1]}")
|
||||
|
||||
# 更新当前对话日志(可用于上下文参考)
|
||||
self.dialogue_log = dialogue_log
|
||||
self.input_pos = input_pos
|
||||
|
||||
# D. 只关注最后一条消息
|
||||
last_msg = dialogue_log[-1]
|
||||
# 计算稳定哈希(忽略坐标变化)
|
||||
current_msg_hash = self.get_stable_message_hash(last_msg)
|
||||
|
||||
# E. 判断是否需要回复 (对方发送且非重复消息)
|
||||
sender = last_msg.get('sender', '')
|
||||
|
||||
# Check if hash is already processed (in-memory only)
|
||||
is_processed = current_msg_hash in self.processed_hashes
|
||||
|
||||
# Log only if it changed from last *in-memory* check to avoid spam
|
||||
if is_processed and current_msg_hash != self.last_processed_msg_hash:
|
||||
# logger.info(f"🚫 [监控] 消息哈希已存在于历史记录中,跳过回复 (Hash: {current_msg_hash})")
|
||||
self.last_processed_msg_hash = current_msg_hash
|
||||
|
||||
if not is_processed and current_msg_hash != self.last_processed_msg_hash:
|
||||
if sender != "我":
|
||||
event_shot = WxUtil.get_next_debug_path("event_new_msg")
|
||||
self.device.screenshot(event_shot)
|
||||
logger.info(f"💡 [监控] 发现新消息: {last_msg},保存现场截图: {event_shot}")
|
||||
|
||||
# 获取上下文文本 (格式化为 Sender: Content)
|
||||
context_text = "\n".join([f"{m.get('time_display', '') + ' ' if m.get('time_display') else ''}{m.get('sender')}: {m.get('content')}" for m in dialogue_log[:-1]])
|
||||
last_content = last_msg.get('content') or ""
|
||||
|
||||
# 兜底逻辑:如果最后一条是语音且内容为空(可能因无红点未被 UNREAD 策略处理),尝试强制转换
|
||||
if last_msg.get('type') == 'voice' and not last_content.strip():
|
||||
logger.info("⚠️ [监控] 最后一条语音消息未获取到内容(可能已读无红点),尝试强制转换...")
|
||||
# 强制使用 LAST 策略重试,并且不还原状态(保持文字展开),防止下次循环因无内容再次触发重试
|
||||
dialogue_log_retry, _ = await WxUtil.analyze_chat_image(
|
||||
self.screenshot_path,
|
||||
self.debug_view_path,
|
||||
device=self.device,
|
||||
process_strategy="LAST",
|
||||
restore_processed_voice=False
|
||||
)
|
||||
if dialogue_log_retry:
|
||||
# 更新引用
|
||||
self.dialogue_log = dialogue_log_retry
|
||||
dialogue_log = dialogue_log_retry
|
||||
last_msg = dialogue_log[-1]
|
||||
last_content = last_msg.get('content') or ""
|
||||
logger.info(f"🔄 [重试] 强制转换后内容: {last_content}")
|
||||
|
||||
# 重新构建哈希
|
||||
current_msg_hash = self.get_stable_message_hash(last_msg)
|
||||
# 再次检查是否已处理 (因为内容变了,哈希变了)
|
||||
if current_msg_hash in self.processed_hashes:
|
||||
logger.info(f"🚫 [重试] 转换后发现该消息已处理,跳过。")
|
||||
self.last_processed_msg_hash = current_msg_hash
|
||||
# 跳过本次循环的剩余部分
|
||||
continue
|
||||
|
||||
# 生成回复
|
||||
reply = await self.get_reply(last_content, context_text)
|
||||
|
||||
if reply:
|
||||
logger.info(f"🤖 [监控] LLM 建议回复: {reply}")
|
||||
if self.input_pos:
|
||||
logger.info(f"⚡ [监控] 执行自动回复...")
|
||||
# input_pos 是 ((x,y), box) 格式,取第一个元素坐标点
|
||||
target_pos = self.input_pos[0] if isinstance(self.input_pos, (list, tuple)) and len(self.input_pos) == 2 and isinstance(self.input_pos[0], (list, tuple)) else self.input_pos
|
||||
# 简单兼容处理:如果 input_pos[0] 是 tuple/list 且 input_pos[1] 是 None/box,则取 input_pos[0]
|
||||
if isinstance(self.input_pos, (list, tuple)) and len(self.input_pos) == 2 and isinstance(self.input_pos[0], (list, tuple)):
|
||||
target_pos = self.input_pos[0]
|
||||
|
||||
# [User Request] 语音转文字未关闭 -> 顺序不对
|
||||
# 在回复发送前,尝试关闭语音转文字(如果最后一条是语音)
|
||||
# 这样截图看起来更干净,且符合用户期望的顺序
|
||||
if last_msg.get('type') == 'voice':
|
||||
logger.info("🧹 [监控] 尝试关闭语音转文字显示...")
|
||||
try:
|
||||
# 点击语音消息中心,关闭文字
|
||||
cx, cy = last_msg['center']
|
||||
WxUtil.safe_device_click(self.device, cx, cy)
|
||||
# 稍微等待界面刷新
|
||||
await asyncio.sleep(1)
|
||||
except Exception as e:
|
||||
logger.warning(f"关闭语音转文字失败: {e}")
|
||||
|
||||
perform_input_action(self.device, target_pos, reply)
|
||||
|
||||
# 发送后截图留存
|
||||
reply_sent_shot = WxUtil.get_next_debug_path("event_reply_sent")
|
||||
self.device.screenshot(reply_sent_shot)
|
||||
logger.info(f"✅ [监控] 回复已发送,保存发送后截图: {reply_sent_shot}")
|
||||
|
||||
self._record_processed_hash(last_msg, current_msg_hash)
|
||||
self.last_processed_msg_hash = current_msg_hash
|
||||
else:
|
||||
logger.error("❌ 未找到输入框位置,无法发送回复")
|
||||
else:
|
||||
logger.info("⚪ [监控] LLM 认为无需回复")
|
||||
self._record_processed_hash(last_msg, current_msg_hash)
|
||||
self.last_processed_msg_hash = current_msg_hash
|
||||
else:
|
||||
# 是我发的消息,更新哈希,不再处理
|
||||
self.last_processed_msg_hash = current_msg_hash
|
||||
|
||||
await asyncio.sleep(self.check_interval)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in monitoring loop: {e}", exc_info=True)
|
||||
await asyncio.sleep(self.check_interval)
|
||||
|
||||
async def run_main():
|
||||
"""
|
||||
运行自动巡课机器人
|
||||
"""
|
||||
bot = ChatMonitorBot()
|
||||
await bot.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 应用 Win32 补丁
|
||||
Win32Patch.patch()
|
||||
asyncio.run(run_main())
|
||||
126
WeiXin/T2_Identify.py
Normal file
126
WeiXin/T2_Identify.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import cv2
|
||||
import json
|
||||
|
||||
# Add project root
|
||||
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 import WxUtil
|
||||
|
||||
# Setup Logger
|
||||
logger = logging.getLogger("T2_Identify")
|
||||
logger.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Console Handler
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
# File Handler
|
||||
log_file = os.path.join(WxUtil.LOG_DIR, "T2_Identify.log")
|
||||
fh = logging.FileHandler(log_file, mode='w', encoding='utf-8')
|
||||
fh.setFormatter(formatter)
|
||||
logger.addHandler(fh)
|
||||
|
||||
def main():
|
||||
logger.info("--- T2: Identify Elements ---")
|
||||
|
||||
# 1. Connect
|
||||
d = WxUtil.connect_device()
|
||||
if not d:
|
||||
logger.error("Device connection failed.")
|
||||
return
|
||||
|
||||
# 2. Screenshot/Image Source
|
||||
if len(sys.argv) > 1:
|
||||
screenshot_path = sys.argv[1]
|
||||
logger.info(f"Using provided image: {screenshot_path}")
|
||||
else:
|
||||
screenshot_path = os.path.join(WxUtil.OUTPUT_DIR, "T2_Screenshot.jpg")
|
||||
logger.info("Taking screenshot...")
|
||||
try:
|
||||
d.screenshot(screenshot_path)
|
||||
logger.info(f"Screenshot saved to: {screenshot_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to take screenshot: {e}")
|
||||
# If screenshot fails, try to use existing one for debugging if it exists
|
||||
if os.path.exists(screenshot_path):
|
||||
logger.warning(f"Using existing screenshot at {screenshot_path}")
|
||||
else:
|
||||
return
|
||||
|
||||
# 3. Analyze (Using the internal _scan function to see raw detection)
|
||||
logger.info("Scanning image for elements...")
|
||||
messages, debug_img, chat_title = WxUtil._scan_chat_messages(screenshot_path)
|
||||
|
||||
# 4. Output Raw Detection
|
||||
logger.info(f"Chat Title: {chat_title}")
|
||||
logger.info(f"--- Raw Detection ({len(messages)} items) ---")
|
||||
|
||||
for i, msg in enumerate(messages):
|
||||
# Format for readability
|
||||
content = msg.get('content', '')
|
||||
if content is None: content = "[None]"
|
||||
|
||||
type_str = msg.get('type', 'unknown')
|
||||
sender_str = msg.get('sender', 'unknown')
|
||||
center = msg.get('center', (0,0))
|
||||
y = msg.get('y', 0)
|
||||
time_disp = msg.get('time_display', 'N/A')
|
||||
|
||||
logger.info(f"[{i}] {type_str:<8} | {sender_str:<4} | y={y:<4} | time={time_disp} | {content}")
|
||||
|
||||
# 5. Merging Logic (Simulate what the bot SHOULD do)
|
||||
logger.info("--- Merged Dialogues (Simulated) ---")
|
||||
merged_msgs = []
|
||||
if messages:
|
||||
current_msg = messages[0].copy()
|
||||
last_y = current_msg['y'] # Track Y of the last element in the current merge block
|
||||
|
||||
for i in range(1, len(messages)):
|
||||
next_msg = messages[i]
|
||||
|
||||
# Merge Criteria:
|
||||
# 1. Same Sender
|
||||
# 2. Same Type (text)
|
||||
# 3. Vertically close (y_diff < 100) relative to the LAST line, not the first
|
||||
# 4. Not a timestamp
|
||||
|
||||
is_same_sender = (current_msg['sender'] == next_msg['sender'])
|
||||
is_text = (current_msg['type'] == 'text' and next_msg['type'] == 'text')
|
||||
|
||||
# Use last_y to compare with next_msg['y']
|
||||
y_diff = next_msg['y'] - last_y
|
||||
is_close = (y_diff < 120) # Tolerance for line spacing (slightly increased)
|
||||
|
||||
if is_same_sender and is_text and is_close:
|
||||
# Merge content
|
||||
current_msg['content'] += " " + next_msg['content']
|
||||
# Update last_y to this message's y
|
||||
last_y = next_msg['y']
|
||||
else:
|
||||
merged_msgs.append(current_msg)
|
||||
current_msg = next_msg.copy()
|
||||
last_y = current_msg['y']
|
||||
|
||||
merged_msgs.append(current_msg)
|
||||
|
||||
for i, msg in enumerate(merged_msgs):
|
||||
content = msg.get('content', '')
|
||||
sender = msg.get('sender', 'unknown')
|
||||
logger.info(f"[{i}] {sender}: {content}")
|
||||
|
||||
# 6. Save Debug Image
|
||||
debug_path = os.path.join(WxUtil.OUTPUT_DIR, "T2_Identify_Debug.jpg")
|
||||
if debug_img is not None:
|
||||
cv2.imwrite(debug_path, debug_img)
|
||||
logger.info(f"Debug image saved to: {debug_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
85
WeiXin/T3_Voice.py
Normal file
85
WeiXin/T3_Voice.py
Normal file
@@ -0,0 +1,85 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import asyncio
|
||||
import cv2
|
||||
|
||||
# Add project root
|
||||
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 import WxUtil
|
||||
|
||||
# Setup Logger
|
||||
logger = logging.getLogger("T3_Voice")
|
||||
|
||||
# Configure Root Logger to ensure all logs (including WxUtil) go to file
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
# Ensure Log Directory exists
|
||||
if hasattr(WxUtil, 'LOG_DIR'):
|
||||
log_dir = WxUtil.LOG_DIR
|
||||
else:
|
||||
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Logs")
|
||||
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
# File Handler
|
||||
log_file = os.path.join(log_dir, "T3_Voice.log")
|
||||
fh = logging.FileHandler(log_file, mode='w', encoding='utf-8')
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
fh.setFormatter(formatter)
|
||||
root_logger.addHandler(fh)
|
||||
|
||||
# Console Handler (if not already added by WxUtil)
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in root_logger.handlers):
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
root_logger.addHandler(ch)
|
||||
|
||||
async def main():
|
||||
logger.info("--- T3: Voice Processing Test ---")
|
||||
|
||||
# 1. Connect
|
||||
d = WxUtil.connect_device()
|
||||
if not d:
|
||||
logger.error("Device connection failed.")
|
||||
return
|
||||
|
||||
# 2. Screenshot
|
||||
screenshot_path = os.path.join(WxUtil.OUTPUT_DIR, "T3_Screenshot.jpg")
|
||||
debug_path = os.path.join(WxUtil.OUTPUT_DIR, "T3_Debug.jpg")
|
||||
|
||||
logger.info("Taking screenshot...")
|
||||
d.screenshot(screenshot_path)
|
||||
logger.info("Initial screenshot taken.")
|
||||
|
||||
# 3. Use analyze_chat_image with strategy='ALL' to test conversion
|
||||
# This simulates the real bot behavior for voice messages
|
||||
logger.info("Running WxUtil.analyze_chat_image (Strategy=ALL)...")
|
||||
|
||||
# This function handles the click -> convert -> rescan loop internally
|
||||
final_messages, _ = await WxUtil.analyze_chat_image(
|
||||
screenshot_path,
|
||||
debug_path,
|
||||
device=d,
|
||||
process_strategy="ALL"
|
||||
)
|
||||
|
||||
# 4. Output Result
|
||||
logger.info("--- Final Voice Results ---")
|
||||
voice_msgs = [m for m in final_messages if m['type'] == 'voice']
|
||||
if not voice_msgs:
|
||||
logger.info("No voice messages found.")
|
||||
else:
|
||||
for i, msg in enumerate(voice_msgs):
|
||||
content = msg.get('content')
|
||||
converted_status = "YES" if msg.get('is_converted') else "NO"
|
||||
logger.info(f"Voice [{i}] Sender: {msg['sender']} | Converted: {converted_status} | Content: {content}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
104
WeiXin/T4_Text.py
Normal file
104
WeiXin/T4_Text.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import cv2
|
||||
|
||||
# Add project root
|
||||
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 import WxUtil
|
||||
|
||||
# Setup Logger
|
||||
logger = logging.getLogger("T4_Text")
|
||||
|
||||
# Configure Root Logger to ensure all logs (including WxUtil) go to file
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
# Ensure Log Directory exists
|
||||
if hasattr(WxUtil, 'LOG_DIR'):
|
||||
log_dir = WxUtil.LOG_DIR
|
||||
else:
|
||||
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Logs")
|
||||
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
# File Handler
|
||||
log_file = os.path.join(log_dir, "T4_Text.log")
|
||||
fh = logging.FileHandler(log_file, mode='w', encoding='utf-8')
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
fh.setFormatter(formatter)
|
||||
root_logger.addHandler(fh)
|
||||
|
||||
# Console Handler (if not already added by WxUtil)
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in root_logger.handlers):
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
root_logger.addHandler(ch)
|
||||
|
||||
def main():
|
||||
logger.info("--- T4: Text Processing Test ---")
|
||||
|
||||
# 1. Connect
|
||||
d = WxUtil.connect_device()
|
||||
if not d:
|
||||
logger.error("Device connection failed.")
|
||||
return
|
||||
|
||||
# 2. Screenshot
|
||||
screenshot_path = os.path.join(WxUtil.OUTPUT_DIR, "T4_Screenshot.jpg")
|
||||
logger.info("Taking screenshot...")
|
||||
d.screenshot(screenshot_path)
|
||||
|
||||
# 3. Scan
|
||||
logger.info("Scanning image...")
|
||||
messages, debug_img, _ = WxUtil._scan_chat_messages(screenshot_path)
|
||||
|
||||
# 4. Filter and Output
|
||||
logger.info("--- Raw Text Messages ---")
|
||||
text_msgs = [m for m in messages if m['type'] == 'text']
|
||||
|
||||
if not text_msgs:
|
||||
logger.info("No text messages found.")
|
||||
else:
|
||||
for i, msg in enumerate(text_msgs):
|
||||
logger.info(f"Text [{i}] Sender: {msg['sender']} | y={msg['y']} | Content: {msg['content']}")
|
||||
|
||||
# 5. Merging Logic (Same as T2)
|
||||
logger.info("--- Merged Text Dialogues ---")
|
||||
merged_msgs = []
|
||||
if text_msgs:
|
||||
current_msg = text_msgs[0].copy()
|
||||
last_y = current_msg['y']
|
||||
|
||||
for i in range(1, len(text_msgs)):
|
||||
next_msg = text_msgs[i]
|
||||
|
||||
is_same_sender = (current_msg['sender'] == next_msg['sender'])
|
||||
y_diff = next_msg['y'] - last_y
|
||||
is_close = (y_diff < 120)
|
||||
|
||||
if is_same_sender and is_close:
|
||||
current_msg['content'] += " " + next_msg['content']
|
||||
last_y = next_msg['y']
|
||||
else:
|
||||
merged_msgs.append(current_msg)
|
||||
current_msg = next_msg.copy()
|
||||
last_y = current_msg['y']
|
||||
|
||||
merged_msgs.append(current_msg)
|
||||
|
||||
for i, msg in enumerate(merged_msgs):
|
||||
logger.info(f"[{i}] {msg['sender']}: {msg['content']}")
|
||||
|
||||
# Save debug
|
||||
debug_path = os.path.join(WxUtil.OUTPUT_DIR, "T4_Debug.jpg")
|
||||
cv2.imwrite(debug_path, debug_img)
|
||||
logger.info(f"Debug image saved to: {debug_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
80
WeiXin/T5_SendText.py
Normal file
80
WeiXin/T5_SendText.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# coding=utf-8
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import time
|
||||
|
||||
# Add project root
|
||||
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 import WxUtil
|
||||
|
||||
# Setup Logger
|
||||
logger = logging.getLogger("T5_SendText")
|
||||
|
||||
# Configure Root Logger to ensure all logs go to file
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.INFO)
|
||||
|
||||
# Ensure Log Directory exists
|
||||
if hasattr(WxUtil, 'LOG_DIR'):
|
||||
log_dir = WxUtil.LOG_DIR
|
||||
else:
|
||||
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "Logs")
|
||||
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
# File Handler
|
||||
log_file = os.path.join(log_dir, "T5_SendText.log")
|
||||
fh = logging.FileHandler(log_file, mode='w', encoding='utf-8')
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
fh.setFormatter(formatter)
|
||||
root_logger.addHandler(fh)
|
||||
|
||||
# Console Handler
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in root_logger.handlers):
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(formatter)
|
||||
root_logger.addHandler(ch)
|
||||
|
||||
def main():
|
||||
logger.info("--- T5: Text Input and Send Test ---")
|
||||
|
||||
# 1. Connect
|
||||
d = WxUtil.connect_device()
|
||||
if not d:
|
||||
logger.error("Device connection failed.")
|
||||
return
|
||||
|
||||
# 2. Prepare text
|
||||
test_text = "我是AI返回的文本测试"
|
||||
logger.info(f"Test Content: {test_text}")
|
||||
|
||||
# 3. Find Input Box and Send (Using perform_input_action which encapsulates the steps)
|
||||
# We pass auto_send=True to click the send button
|
||||
# The function will:
|
||||
# - Ensure keyboard mode
|
||||
# - Find input box (UI Tree -> Template -> Fallback)
|
||||
# - Input text
|
||||
# - Find and click Send button
|
||||
|
||||
logger.info("Step 1 & 2: Finding input box and writing text...")
|
||||
|
||||
# We can get a rough position first for logging, though perform_input_action is more robust
|
||||
screenshot_path = os.path.join(WxUtil.OUTPUT_DIR, "T5_Initial_Check.jpg")
|
||||
d.screenshot(screenshot_path)
|
||||
input_pos, _ = WxUtil.find_input_box_center(screenshot_path)
|
||||
logger.info(f"Initial target input position: {input_pos}")
|
||||
|
||||
success = WxUtil.perform_input_action(d, input_pos, test_text, auto_send=True)
|
||||
|
||||
if success:
|
||||
logger.info("✅ Step 3: Send action completed successfully.")
|
||||
else:
|
||||
logger.error("❌ Send action failed.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user