This commit is contained in:
HuangHai
2026-01-25 10:28:31 +08:00
parent 291497d371
commit 4551b43781
52 changed files with 194 additions and 7 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -11,7 +11,7 @@ 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.WxUtil import find_input_box_center, perform_input_action, analyze_chat_image
from WeiXin.WxUtil import find_input_box_center, perform_input_action, analyze_chat_image, clean_screenshots_dir
from Util.LlmUtil import get_llm_response
# 配置日志
@@ -32,6 +32,9 @@ logger = logging.getLogger("T4_InputLlmText")
async def generate_and_input():
logger.info("开始执行 T4: 生成 LLM 回复并输入...")
# 0. 清除旧截图
clean_screenshots_dir()
try:
# 1. 连接设备
d = u2.connect()
@@ -58,17 +61,27 @@ async def generate_and_input():
# 3. 调用 LLM 生成回复
persona = (
"你是一名99年毕业有27年教学经验的小学高级教师大张老师。对作文批阅不下20000个"
"非常熟悉小学阶段、初二以前的初中阶段作文教学。你的回复必须专业、严谨、沉稳且富有权威感。"
"严禁使用任何搞笑、幽默、有趣或轻浮的语气。要以资深教育者的身份给出严肃、务实的指导建议。"
"你是一名99年毕业有27年教学经验的小学高级教师大张老师。你在长春市少惠林作文素养培养中心工作"
"校区真实地址就在长春市南环城路与临河街交汇的TOUCH12街3楼325号。你对作文批阅不下20000个"
"非常熟悉小学阶段、初二以前的初中阶段作文教学。你是一位深受家长和学生信任的教育智者,"
"回复时要既专业又充满亲和力像老朋友交流一样自然、温暖。严禁使用1. 2. 3.’或‘首先、其次、最后’等生硬、刻板的列条目回复。"
"要善于用富有经验的话语疏导家长的焦虑,并给出有深度、可操作的详细指导。语言要生动、形象,多用比喻或实例,展现岁月沉淀的智慧。"
"如果涉及到校区信息,必须使用以下真实数据,绝对严禁使用'XX区''XX路''XX号'等占位符:\n"
"1. 校址长春市南环城路与临河街交汇的TOUCH12街3楼325号\n"
"2. 课程:线上和线下作文课、假期读书营、阅读策略营\n"
"3. 上课时间:周二/周四晚上,以及周六周日全天\n"
"4. 联系人:小张老师\n"
"5. 电话18686619970"
)
prompt = (
f"人设:{persona}\n\n"
f"以下是微信聊天记录('对方'''的对话):\n{history_text}\n\n"
"请作为大张老师,针对最后的消息给出一个简短、专业且严肃的回复,不超过 60 字。"
"禁止出现任何幽默或卖萌的词汇,保持特级教师的庄重感。直接输出回复内容,不要包含任何解释性文字。"
"请作为大张老师,针对最后的消息给出一个充满关怀、有深度且专业的回复。回复要亲切自然,"
"字数控制在 150-250 字之间,不要分点列项,而是用连贯的段落叙述。要把方法讲透,把道理讲细,"
"让家长感受到你作为老教师的温度和沉淀。如果提到地址、电话或课程,必须使用人设中提供的真实信息,"
"严禁出现任何 'XX' 形式的占位符。直接输出回复内容。"
)
logger.info(f"正在以'严谨的大张老'身份请求 LLM 生成回复...")
logger.info(f"正在以'亲切而专业的特级教'身份请求 LLM 生成深度回复...")
full_response = ""
async for chunk in get_llm_response(prompt, stream=False):

View File

@@ -0,0 +1,157 @@
# 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)
from WeiXin.WxUtil import find_input_box_center, perform_input_action, analyze_chat_image, clean_screenshots_dir
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=[
logging.FileHandler(os.path.join(log_dir, "T5_AutoChatMonitor.log"), encoding='utf-8'),
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 = (
"你是一名99年毕业有27年教学经验的小学高级教师大张老师。你在长春市少惠林作文素养培养中心工作"
"校区真实地址就在长春市南环城路与临河街交汇的TOUCH12街3楼325号。你对作文批阅不下20000个"
"非常熟悉小学阶段、初二以前的初中阶段作文教学。你是一位深受家长和学生信任的教育智者,"
"回复时要既专业又充满亲和力像老朋友交流一样自然、温暖。严禁使用1. 2. 3.’或‘首先、其次、最后’等生硬、刻板的列条目回复。"
"要善于用富有经验的话语疏导家长的焦虑,并给出有深度、可操作的详细指导。语言要生动、形象,多用比喻或实例,展现岁月沉淀的智慧。"
"如果涉及到校区信息,必须使用以下真实数据,绝对严禁使用'XX区''XX路''XX号'等占位符:\n"
"1. 校址长春市南环城路与临河街交汇的TOUCH12街3楼325号\n"
"2. 课程:线上和线下作文课、假期读书营、阅读策略营\n"
"3. 上课时间:周二/周四晚上,以及周六周日全天\n"
"4. 联系人:小张老师\n"
"5. 电话18686619970"
)
async def get_reply(self, history_text, is_proactive=False):
if is_proactive:
prompt = (
f"人设:{self.persona}\n\n"
f"对话背景用户已经超过5分钟没有回应了。\n"
f"历史对话:\n{history_text}\n\n"
"请作为大张老师,给出一个温暖且有深度的询问或阶段性总结。不要只是简单的问候,"
"可以根据之前的对话内容,再补充一点点有用的教学建议,或者分享一个具体的教学小故事来宽慰家长。"
"严禁分条列项,要像老朋友拉家常一样,字数 120-200 字左右。"
"如果提到地址、电话或课程,必须使用人设中提供的真实信息,严禁出现任何 'XX' 形式的占位符。"
)
else:
prompt = (
f"人设:{self.persona}\n\n"
f"以下是微信聊天记录('对方'''的对话):\n{history_text}\n\n"
"请作为大张老师,针对最后的消息给出一个充满关怀、有深度且专业的回复。回复要亲切自然,"
"字数控制在 150-250 字之间,不要分点列项,而是用连贯的段落叙述。要把方法讲透,把道理讲细,"
"让家长感受到你作为老教师的温度和沉淀。如果提到地址、电话或课程,必须使用人设中提供的真实信息,"
"严禁出现任何 'XX' 形式的占位符。直接输出回复内容。"
)
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:
# 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")
self.d.screenshot(tmp_shot)
dialogue_log = analyze_chat_image(tmp_shot, analyzed_shot)
if not dialogue_log:
logger.debug("未识别到有效对话,跳过...")
await asyncio.sleep(CHECK_INTERVAL)
continue
# 2. 检查是否有新消息
current_last_msg = dialogue_log[-1]
history_text = "\n".join(dialogue_log)
# 判断逻辑:如果最后一条消息是“对方”发的,且与上次不同,则回复
if "对方:" in current_last_msg and current_last_msg != self.last_message_text:
logger.info(f"📩 检测到新消息: {current_last_msg}")
reply = await self.get_reply(history_text)
logger.info(f"🤖 生成回复: {reply}")
# 执行输入发送
center_point, _ = find_input_box_center(tmp_shot)
if center_point:
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 # 重置主动询问计数
else:
logger.error("无法定位输入框")
# 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}")
center_point, _ = find_input_box_center(tmp_shot)
if center_point:
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}"
# 更新最后一条消息记录(仅用于对比)
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())

View File

@@ -20,6 +20,23 @@ from Util.EasyOcrKit import get_easyocr_reader
# 调用者应自行配置日志(包括 FileHandler 等)
logger = logging.getLogger("WxUtil")
def clean_screenshots_dir():
"""
清除 d:\\dsWork\\aiData\\WeiXin\\Screenshots 目录下的所有文件
"""
screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
if os.path.exists(screenshot_dir):
logger.info(f"正在清除截图目录: {screenshot_dir}")
for file in os.listdir(screenshot_dir):
file_path = os.path.join(screenshot_dir, file)
try:
if os.path.isfile(file_path):
os.remove(file_path)
except Exception as e:
logger.error(f"无法删除文件 {file_path}: {e}")
else:
logger.info(f"截图目录不存在,无需清除: {screenshot_dir}")
def find_input_box_center(image_path):
"""
识别底部输入框的中心坐标,返回 (center_x, center_y, rect_box)