'commit'
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 159 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 130 KiB |
BIN
WeiXin/Screenshots/debug_send_check_20260125_102807.jpg
Normal file
|
After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 332 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 93 KiB |
BIN
WeiXin/Screenshots/t5_monitor_analyzed.jpg
Normal file
|
After Width: | Height: | Size: 323 KiB |
BIN
WeiXin/Screenshots/t5_monitor_temp.jpg
Normal file
|
After Width: | Height: | Size: 161 KiB |
@@ -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):
|
||||
|
||||
157
WeiXin/T5_AutoChatMonitor.py
Normal 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())
|
||||
@@ -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)
|
||||
|
||||