diff --git a/WeiXin/T2_ChatMonitor.py b/WeiXin/T2_ChatMonitor.py deleted file mode 100644 index 55559b2..0000000 --- a/WeiXin/T2_ChatMonitor.py +++ /dev/null @@ -1,333 +0,0 @@ -# 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() - 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_hash): - """记录已处理的消息哈希 (仅内存)""" - self.processed_hashes.add(msg_hash) - # 仅保留最近 100 条记录,防止无限增长 - if len(self.processed_hashes) > 100: - # 简单丢弃旧的(转列表切片再转回集合) - temp = list(self.processed_hashes)[-100:] - self.processed_hashes = set(temp) - - 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" # 监控阶段:只处理带红点的新语音 - ) - - 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" - ) - 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] - - 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(current_msg_hash) - self.last_processed_msg_hash = current_msg_hash - else: - logger.error("❌ 未找到输入框位置,无法发送回复") - else: - logger.info("⚪ [监控] LLM 认为无需回复") - self._record_processed_hash(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()) diff --git a/WeiXin/WxUtil.py b/WeiXin/WxUtil.py index 28e25a7..ce56993 100644 --- a/WeiXin/WxUtil.py +++ b/WeiXin/WxUtil.py @@ -40,9 +40,16 @@ def parse_wechat_time(time_str): 支持: "10:03", "昨天 10:03", "星期三 10:03", "2025年1月1日 10:03" """ try: + clean_str = time_str.strip() + + # 0. 预处理:过滤纯数字(防止电话号码被误识别为时间) + # 微信时间戳通常包含中文或冒号,单纯的数字串(如 "18686619970")不是有效时间 + if re.match(r'^\d+$', clean_str): + logger.warning(f"忽略疑似电话号码/纯数字的时间字符串: '{clean_str}'") + return "" + now = datetime.now() today = now.date() - clean_str = time_str.strip() # 1. HH:mm (当天) # 注意:有时候 OCR 会把冒号识别成其他字符,这里假设是标准的 HH:mm @@ -130,10 +137,11 @@ def parse_wechat_time(time_str): dt = datetime.combine(today, datetime.min.time().replace(hour=h, minute=m)) return dt.strftime("%Y-%m-%d %H:%M") - return clean_str # 解析失败,返回原串 + # 解析失败,返回空字符串,避免将无关文本(如电话号码)误认为时间注入到上下文中 + return "" except Exception as e: logger.warning(f"时间解析失败 '{time_str}': {e}") - return time_str + return "" def get_next_debug_path(desc="step"): """获取下一个顺序命名的调试图片路径 (debug_N_desc.jpg)""" @@ -305,6 +313,11 @@ def _detect_bubble_color(img, bbox): # 且亮度不能太低 (太低可能是黑色背景或深色物体) if g > 150: return "white" + + # 特殊补丁:如果 B, G, R 都很接近且在 130 左右,可能是微信的背景灰 (通常用于时间戳或系统消息) + if 110 < r < 160 and 110 < g < 160 and 110 < b < 160: + if abs(r - g) < 15 and abs(g - b) < 15: + return "system_gray" return "unknown" @@ -359,7 +372,7 @@ def _scan_chat_messages(image_path): # 微信菜单关键字(用于排除干扰) MENU_KEYWORDS = ["听筒播放", "收藏", "背景播放", "删除", "多选", "取消转文字", "转文字", "引用", "提醒"] # 忽略的系统消息内容 - IGNORE_CONTENT = ["撤回了一条消息", "打招呼的消息", "拍了拍", "你撤回了一条消息", "引用", "Clear Text", "Switch IME", "Done"] + IGNORE_CONTENT = ["撤回了一条消息", "打招呼的消息", "拍了拍", "你撤回了一条消息", "引用", "Clear Text", "Switch IME", "Done", "按住说话", "发送"] # 5. 整合所有消息 messages = [] @@ -367,9 +380,9 @@ def _scan_chat_messages(image_path): # 绘制过滤区域边界 (可视化) cv2.line(debug_img, (0, 150), (w, 150), (255, 0, 255), 2) # 顶部线 - cv2.line(debug_img, (0, h - 100), (w, h - 100), (255, 0, 255), 2) # 底部线 + cv2.line(debug_img, (0, h - 60), (w, h - 60), (255, 0, 255), 2) # 底部线 (放宽到底部 60px) cv2.putText(debug_img, "TOP_FILTER", (10, 140), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 255), 1) - cv2.putText(debug_img, "BOTTOM_FILTER", (10, h - 110), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 255), 1) + cv2.putText(debug_img, "BOTTOM_FILTER", (10, h - 70), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 0, 255), 1) claimed_ocr_indices = set() @@ -379,7 +392,7 @@ def _scan_chat_messages(image_path): cv2.circle(debug_img, (ax, ay), 10, (255, 255, 0), -1) # 过滤掉顶部和底部的非聊天区域 - if ay < 150 or ay > h - 100: + if ay < 150 or ay > h - 60: logger.info(f"忽略区域外语音图标: ({ax}, {ay})") cv2.rectangle(debug_img, (ax-35, ay-35), (ax+35, ay+35), (128, 128, 128), 1) cv2.putText(debug_img, "FILTERED", (ax - 40, ay - 45), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (128, 128, 128), 1) @@ -416,25 +429,32 @@ def _scan_chat_messages(image_path): if voice_is_left: # 语音在左 (对方): 文本必须也是左对齐 - # - min_x 必须靠左 (< 300) + # - min_x 必须靠左 (< 450) [Fix] 放宽阈值,防止长文本或缩进文本被过滤 # - max_x 不能太靠右 (> w - 150),否则可能是"我"的消息 - if min_x > 300 or max_x > w - 150: + if min_x > 450 or max_x > w - 150: + logger.debug(f"忽略文本 '{text[:10]}' (Left Voice): min_x={min_x}, max_x={max_x} 不满足左对齐条件") continue else: # 语音在右 (我): 文本必须也是右对齐 # - max_x 必须靠右 (> w - 300) # - min_x 不能太靠左 (< 100) if max_x < w - 300 or min_x < 100: + logger.debug(f"忽略文本 '{text[:10]}' (Right Voice): min_x={min_x}, max_x={max_x} 不满足右对齐条件") continue - if -50 < c_y - ay < 800 and abs(c_x - ax) < 500: + # [Fix] 放宽 X 轴判定范围 (500 -> 600) 以适应更宽的文本 + if -50 < c_y - ay < 800 and abs(c_x - ax) < 600: # 检查中间是否有其他语音图标 has_intermediate_audio = False for other_ax, other_ay in audio_matches: + # 只有当中间的语音图标在 [150, h-60] 的有效聊天区域内时,才视为阻断 if ay + 20 < other_ay < c_y - 10: - has_intermediate_audio = True - logger.info(f"语音({ax},{ay}) 被中间语音图标({other_ax},{other_ay}) 阻断,无法关联文本 '{text[:10]}...'") - break + if 150 <= other_ay <= h - 60: + has_intermediate_audio = True + logger.info(f"语音({ax},{ay}) 被中间有效区域内的语音图标({other_ax},{other_ay}) 阻断,无法关联文本 '{text[:10]}...'") + break + else: + logger.info(f"语音({ax},{ay}) 忽略非聊天区域(Y={other_ay})的语音图标阻断") if has_intermediate_audio: continue @@ -532,7 +552,7 @@ def _scan_chat_messages(image_path): c_x = int((bbox[0][0] + bbox[2][0]) / 2) c_y = int((bbox[0][1] + bbox[2][1]) / 2) - if c_y < 150 or c_y > h - 100: + if c_y < 150 or c_y > h - 60: continue # 判定发送者 (增强版: 几何 + 颜色) @@ -544,6 +564,9 @@ def _scan_chat_messages(image_path): sender = "我" elif sender_color == "white": sender = "对方" + elif sender_color == "system_gray": + # 灰底文字通常是时间戳或系统消息,由系统发送,几何上居中 + sender = "system" # 2. 几何特征强制修正 (Double Check) # 假设头像+边距约占 15% 宽度 @@ -561,12 +584,15 @@ def _scan_chat_messages(image_path): # 规则 B: 如果这一行极其靠左 (小于 35% 宽度),且不靠右,那肯定是"对方" # 扩大判定范围,防止因为 OCR 稍微缩进导致判定失效 # 注意:如果颜色明确为"我"(绿色),则跳过此规则,因为"我"的长消息也可能靠左 - elif min_x < w * 0.35 and max_x < w * 0.85: + elif min_x < w * 0.35 and max_x < w * 0.75: # 修正:max_x 阈值从 0.85 降低到 0.75 if sender == "我": logger.info(f"Geometry says '对方' (min_x={min_x} < {w*0.35}) but Color is '我' (Green). Trusting Color.") + elif sender == "system": + # 即使颜色是系统灰,但如果位置极其靠左,也可能是对方的某种特殊气泡 + pass else: sender = "对方" - + # 规则 C: 如果颜色是 unknown,且不在极端位置,使用中心点兜底 if sender == "unknown": c_x = int((min_x + max_x) / 2) @@ -584,6 +610,9 @@ def _scan_chat_messages(image_path): # [Fix] 如果颜色明确是绿色,说明是"我"的左对齐文本(长文换行),不应被几何规则强制改为"对方" if sender == "我" and sender_color == "green": logger.info(f"Geometry says '对方' (center={c_x} < {w*0.45}) but Color is 'green'. Keeping '我'.") + elif sender == "system": + # 系统消息允许居中或偏左 + pass else: if sender == "我": logger.warning(f"Sender detected as '我' by color but center is left ({c_x} < {w*0.45}). Correcting to '对方'.") @@ -592,7 +621,10 @@ def _scan_chat_messages(image_path): elif c_x > w * 0.55: if sender == "对方": logger.warning(f"Sender detected as '对方' by color but center is right ({c_x} > {w*0.55}). Correcting to '我'.") - sender = "我" + elif sender == "system": + pass + else: + sender = "我" else: logger.info(f"Message in middle zone ({w*0.45} < {c_x} < {w*0.55}), trusting color detection: {sender}") else: @@ -665,10 +697,12 @@ def _scan_chat_messages(image_path): return final_messages_with_time, debug_img, chat_title -async def analyze_chat_image(image_path, output_path, device=None, target_name="对方", process_strategy="ALL"): +async def analyze_chat_image(image_path, output_path, device=None, target_name="对方", process_strategy="ALL", restore_processed_voice=True): """ 全面采用 CV + OCR 识别微信聊天截图中的最后一条消息 :param process_strategy: 语音处理策略 (ALL/UNREAD/LAST) + :param restore_processed_voice: 是否在转文字后还原(隐藏文字)。默认为 True。 + 设为 False 可防止最后一条消息在无内容时陷入"转文字->还原->空内容"的死循环。 注意:此函数现在包含一个循环,如果发现需要转文字的语音,会逐个处理并重新截图。 """ try: @@ -799,21 +833,39 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" # 1. 截图 (但不立即 OCR,而是丢给异步任务) peek_shot = get_next_debug_path("step_peek_content") d.screenshot(peek_shot) - logger.info("已截图,启动异步OCR任务以提取内容...") + logger.info(f"已截图 {peek_shot},启动异步OCR任务以提取内容...") async def _async_ocr_task(img_path, target_y): """内部异步任务:在线程池中运行 OCR""" - loop = asyncio.get_running_loop() - # 在默认执行器(线程池)中运行耗时的 _scan_chat_messages - msgs, _, _ = await loop.run_in_executor(None, _scan_chat_messages, img_path) - - found = None - for pm in msgs: - if pm['type'] == 'voice' and pm.get('is_converted'): - if abs(pm['y'] - target_y) < 50: - found = pm.get('content') - break - return target_y, found + try: + loop = asyncio.get_running_loop() + # 在默认执行器(线程池)中运行耗时的 _scan_chat_messages + logger.info(f"🚀 [Async OCR] 开始分析截图 {os.path.basename(img_path)} (目标 Y={target_y})") + msgs, _, _ = await loop.run_in_executor(None, _scan_chat_messages, img_path) + + found = None + # 收集所有可能是该语音消息转换出的文本 + all_found_texts = [] + for pm in msgs: + if pm['type'] == 'voice' and pm.get('is_converted'): + # 容差稍微放大,因为转文字展开后 Y 坐标会变 + if abs(pm['y'] - target_y) < 150: # 进一步放宽容差 + content = pm.get('content', '').strip() + if content: + all_found_texts.append((pm['y'], content)) + + if all_found_texts: + # 按 Y 轴排序,确保多行文本顺序正确 + all_found_texts.sort(key=lambda x: x[0]) + found = " ".join([t[1] for t in all_found_texts]) + logger.info(f"✨ [Async OCR] 在 Y={target_y} 附近找到转换文字: {found}") + + if not found: + logger.warning(f"⚠️ [Async OCR] 未能在 Y={target_y} 附近找到已转换文字") + return target_y, found + except Exception as e: + logger.error(f"❌ [Async OCR] 任务执行失败: {e}") + return target_y, None # 创建并保存任务 task = asyncio.create_task(_async_ocr_task(peek_shot, vy)) @@ -822,44 +874,61 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" analyze_chat_image._ocr_tasks = [] analyze_chat_image._ocr_tasks.append(task) - # 2. 立即还原状态 (取消转文字) + # 2. 还原状态 (取消转文字) # 注意:由于 OCR 还没出结果,我们无法精确定位展开后的文字位置 # 但通常点击原语音气泡位置 (vx, vy) 也能触发菜单 - logger.info("准备还原状态 (取消转文字)...") - - d.long_click(vx, vy, 1.0) # 盲点原坐标 - - logger.info("正在快速寻找'隐藏文字'按钮...") - cancel_template = os.path.join(TEMPLATE_DIR, "cancel_zhuan_wen_zi.jpg") - cancel_btn = None - - poll_start = time.time() - while time.time() - poll_start < 3.0: - restore_menu_shot = get_next_debug_path("step_restore_poll") - d.screenshot(restore_menu_shot) - cancel_btn = find_template_match(restore_menu_shot, cancel_template, threshold=0.7) + if restore_processed_voice: + logger.info("准备还原状态 (取消转文字)...") + + d.long_click(vx, vy, 1.0) # 盲点原坐标 + + logger.info("正在快速寻找'隐藏文字'按钮...") + cancel_template = os.path.join(TEMPLATE_DIR, "cancel_zhuan_wen_zi.jpg") + cancel_btn = None + + poll_start = time.time() + while time.time() - poll_start < 3.0: + restore_menu_shot = get_next_debug_path("step_restore_poll") + d.screenshot(restore_menu_shot) + cancel_btn = find_template_match(restore_menu_shot, cancel_template, threshold=0.7) + if cancel_btn: + break + time.sleep(0.2) + if cancel_btn: + cx, cy = int(cancel_btn[0]), int(cancel_btn[1]) + logger.info(f"✅ 找到'隐藏文字'按钮: ({cx}, {cy}),点击还原...") + safe_device_click(d, cx, cy) + convert_closed_count += 1 + time.sleep(2.0) # 等待收起动画 + else: + logger.warning("❌ 未找到'隐藏文字'按钮,无法还原状态!(后续可能导致重复处理)") + + # 3. 准备下一次循环 + if len(target_voices) == 1: + logger.info("✅ 当前屏幕所有目标语音已处理完毕,无需再次全屏扫描。") + final_messages = messages # 使用本轮初始扫描的消息列表 break - time.sleep(0.2) - - if cancel_btn: - cx, cy = int(cancel_btn[0]), int(cancel_btn[1]) - logger.info(f"✅ 找到'隐藏文字'按钮: ({cx}, {cy}),点击还原...") - safe_device_click(d, cx, cy) - convert_closed_count += 1 - time.sleep(2.0) # 等待收起动画 + + # 重新截图,因为界面可能微调,或者只是恢复了 + next_screenshot = get_next_debug_path("step_restored") + d.screenshot(next_screenshot) + + current_image_path = next_screenshot + current_output_path = get_next_debug_path("flag_restored") + + continue else: - logger.warning("❌ 未找到'隐藏文字'按钮,无法还原状态!(后续可能导致重复处理)") - - # 3. 准备下一次循环 - # 重新截图,因为界面可能微调,或者只是恢复了 - next_screenshot = get_next_debug_path("step_restored") - d.screenshot(next_screenshot) - - current_image_path = next_screenshot - current_output_path = get_next_debug_path("flag_restored") - - continue + logger.info("⏩ [配置] 跳过还原状态步骤 (保持文字展开)。") + # 即使不还原,我们也不建议继续处理下一条,因为界面已经大幅变动(展开了文字)。 + # 除非我们重新截图并重新定位。 + # 但在这里,如果 restore_processed_voice=False,通常意味着我们只关心最后一条(LAST策略),或者我们接受界面变动。 + + # 为了安全起见,如果不还原,我们最好终止循环(假设只处理这一条,或者下一轮主循环再处理其他的) + # 否则后续的 target_voices 坐标全都不准了。 + logger.info("🛑 因不还原状态,终止本轮多语音处理循环,等待下一次主监控循环。") + final_messages = messages # 这里的 messages 其实是展开前的,但没关系,我们的内容通过 captured_voice_contents 注入 + break else: logger.warning("❌ 未找到'转文字'按钮,可能是已转换或误判") @@ -887,12 +956,13 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" if captured_voice_contents: logger.info(f"正在注入 {len(captured_voice_contents)} 条已还原的语音内容...") for m in final_messages: - if m['type'] == 'voice' and not m.get('content'): + if m['type'] == 'voice' and (not m.get('content') or m.get('content').strip() == ""): for py, content in captured_voice_contents.items(): - if abs(m['y'] - py) < 30: # 匹配原始 Y 坐标 + # 注入时的容差也要放大,因为 final_messages 的 Y 可能和点击时的 vy 略有差异 + if abs(m['y'] - py) < 100: m['content'] = content m['is_converted'] = True # 标记为逻辑上已转换 - logger.info(f" -> 注入内容: {content[:10]}...") + logger.info(f" -> 注入内容到 Y={m['y']} (原 py={py}): {content[:20]}...") break # 构造返回值 @@ -931,15 +1001,16 @@ async def analyze_chat_image(image_path, output_path, device=None, target_name=" # 尝试注入异步获取的语音内容 if msg['type'] == 'voice': # 模糊匹配 Y 坐标 (增大容差到 100,应对界面滚动) - for y_key, content in captured_voice_contents.items(): - if abs(msg['y'] - y_key) < 100: - msg['is_converted'] = True - msg['content'] = content - logger.info(f"注入语音内容到最终消息列表: {content}") - break + # 优先检查 content 是否为空或为 placeholder + if not msg.get('content') or msg.get('content').strip() == "": + for y_key, content in captured_voice_contents.items(): + if abs(msg['y'] - y_key) < 100: + msg['is_converted'] = True + msg['content'] = content + logger.info(f"✅ [注入] 成功将异步语音内容 '{content}' 注入到 Y={msg['y']} 的消息中") + break # 无论是否有内容,都加入 dialogue_log - # 如果是语音且没内容,T2 会有兜底逻辑去处理 if msg['type'] == 'text': if msg.get('content'): # 文本消息没内容通常是识别错误,可以丢弃 dialogue_log.append(msg) diff --git a/WeiXin/__pycache__/WxUtil.cpython-310.pyc b/WeiXin/__pycache__/WxUtil.cpython-310.pyc index 9973afc..2ca1d49 100644 Binary files a/WeiXin/__pycache__/WxUtil.cpython-310.pyc and b/WeiXin/__pycache__/WxUtil.cpython-310.pyc differ diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/2de1c258-fcc8-40bc-a5e7-7b89e28d1bee.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/2de1c258-fcc8-40bc-a5e7-7b89e28d1bee.vsidx deleted file mode 100644 index 37b9cf3..0000000 Binary files a/WordAddIn/.vs/WordAddIn/FileContentIndex/2de1c258-fcc8-40bc-a5e7-7b89e28d1bee.vsidx and /dev/null differ diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/d95b7591-1712-4381-b7ef-a387afb40f1b.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/d95b7591-1712-4381-b7ef-a387afb40f1b.vsidx deleted file mode 100644 index 3d8c1dd..0000000 Binary files a/WordAddIn/.vs/WordAddIn/FileContentIndex/d95b7591-1712-4381-b7ef-a387afb40f1b.vsidx and /dev/null differ diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/f0de7db9-fb84-460b-9e62-5323d5caa827.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/f0de7db9-fb84-460b-9e62-5323d5caa827.vsidx deleted file mode 100644 index 694549d..0000000 Binary files a/WordAddIn/.vs/WordAddIn/FileContentIndex/f0de7db9-fb84-460b-9e62-5323d5caa827.vsidx and /dev/null differ diff --git a/WordAddIn/.vs/WordAddIn/v17/.suo b/WordAddIn/.vs/WordAddIn/v17/.suo index 270cdd7..aaa3233 100644 Binary files a/WordAddIn/.vs/WordAddIn/v17/.suo and b/WordAddIn/.vs/WordAddIn/v17/.suo differ diff --git a/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.backup.json b/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.backup.json index 53c6430..131493c 100644 --- a/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.backup.json +++ b/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.backup.json @@ -1,20 +1,7 @@ { "Version": 1, "WorkspaceRootPath": "D:\\dsWork\\aiData\\WordAddIn\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|d:\\dswork\\aidata\\wordaddin\\wordaddin\\airibbon.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|solutionrelative:wordaddin\\airibbon.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|D:\\dsWork\\aiData\\WordAddIn\\wordaddin\\referencegeneratorform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form", - "RelativeMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|solutionrelative:wordaddin\\referencegeneratorform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form" - }, - { - "AbsoluteMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|D:\\dsWork\\aiData\\WordAddIn\\wordaddin\\outlinesettingsform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form", - "RelativeMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|solutionrelative:wordaddin\\outlinesettingsform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form" - } - ], + "Documents": [], "DocumentGroupContainers": [ { "Orientation": 0, @@ -22,46 +9,11 @@ "DocumentGroups": [ { "DockedWidth": 200, - "SelectedChildIndex": 1, + "SelectedChildIndex": -1, "Children": [ { "$type": "Bookmark", "Name": "ST:0:0:{3ae79031-e1bc-11d0-8f78-00a0c9110057}" - }, - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "AiRibbon.cs", - "DocumentMoniker": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\AiRibbon.cs", - "RelativeDocumentMoniker": "WordAddIn\\AiRibbon.cs", - "ToolTip": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\AiRibbon.cs", - "RelativeToolTip": "WordAddIn\\AiRibbon.cs", - "ViewState": "AgIAAPcAAAAAAAAAAAA1wAsBAABFAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-01-24T10:05:10.056Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 1, - "Title": "ReferenceGeneratorForm.cs [\u8BBE\u8BA1]", - "DocumentMoniker": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\ReferenceGeneratorForm.cs", - "RelativeDocumentMoniker": "WordAddIn\\ReferenceGeneratorForm.cs", - "ToolTip": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\ReferenceGeneratorForm.cs [\u8BBE\u8BA1]", - "RelativeToolTip": "WordAddIn\\ReferenceGeneratorForm.cs [\u8BBE\u8BA1]", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-01-24T10:05:02.111Z" - }, - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "OutlineSettingsForm.cs [\u8BBE\u8BA1]", - "DocumentMoniker": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\OutlineSettingsForm.cs", - "RelativeDocumentMoniker": "WordAddIn\\OutlineSettingsForm.cs", - "ToolTip": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\OutlineSettingsForm.cs [\u8BBE\u8BA1]", - "RelativeToolTip": "WordAddIn\\OutlineSettingsForm.cs [\u8BBE\u8BA1]", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-01-24T10:04:59.465Z" } ] } diff --git a/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.json b/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.json index 53c6430..131493c 100644 --- a/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.json +++ b/WordAddIn/.vs/WordAddIn/v17/DocumentLayout.json @@ -1,20 +1,7 @@ { "Version": 1, "WorkspaceRootPath": "D:\\dsWork\\aiData\\WordAddIn\\", - "Documents": [ - { - "AbsoluteMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|d:\\dswork\\aidata\\wordaddin\\wordaddin\\airibbon.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}", - "RelativeMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|solutionrelative:wordaddin\\airibbon.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}" - }, - { - "AbsoluteMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|D:\\dsWork\\aiData\\WordAddIn\\wordaddin\\referencegeneratorform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form", - "RelativeMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|solutionrelative:wordaddin\\referencegeneratorform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form" - }, - { - "AbsoluteMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|D:\\dsWork\\aiData\\WordAddIn\\wordaddin\\outlinesettingsform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form", - "RelativeMoniker": "D:0:0:{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}|WordAddIn\\WordAddIn.csproj|solutionrelative:wordaddin\\outlinesettingsform.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}|Form" - } - ], + "Documents": [], "DocumentGroupContainers": [ { "Orientation": 0, @@ -22,46 +9,11 @@ "DocumentGroups": [ { "DockedWidth": 200, - "SelectedChildIndex": 1, + "SelectedChildIndex": -1, "Children": [ { "$type": "Bookmark", "Name": "ST:0:0:{3ae79031-e1bc-11d0-8f78-00a0c9110057}" - }, - { - "$type": "Document", - "DocumentIndex": 0, - "Title": "AiRibbon.cs", - "DocumentMoniker": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\AiRibbon.cs", - "RelativeDocumentMoniker": "WordAddIn\\AiRibbon.cs", - "ToolTip": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\AiRibbon.cs", - "RelativeToolTip": "WordAddIn\\AiRibbon.cs", - "ViewState": "AgIAAPcAAAAAAAAAAAA1wAsBAABFAAAAAAAAAA==", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-01-24T10:05:10.056Z", - "EditorCaption": "" - }, - { - "$type": "Document", - "DocumentIndex": 1, - "Title": "ReferenceGeneratorForm.cs [\u8BBE\u8BA1]", - "DocumentMoniker": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\ReferenceGeneratorForm.cs", - "RelativeDocumentMoniker": "WordAddIn\\ReferenceGeneratorForm.cs", - "ToolTip": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\ReferenceGeneratorForm.cs [\u8BBE\u8BA1]", - "RelativeToolTip": "WordAddIn\\ReferenceGeneratorForm.cs [\u8BBE\u8BA1]", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-01-24T10:05:02.111Z" - }, - { - "$type": "Document", - "DocumentIndex": 2, - "Title": "OutlineSettingsForm.cs [\u8BBE\u8BA1]", - "DocumentMoniker": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\OutlineSettingsForm.cs", - "RelativeDocumentMoniker": "WordAddIn\\OutlineSettingsForm.cs", - "ToolTip": "D:\\dsWork\\aiData\\WordAddIn\\WordAddIn\\OutlineSettingsForm.cs [\u8BBE\u8BA1]", - "RelativeToolTip": "WordAddIn\\OutlineSettingsForm.cs [\u8BBE\u8BA1]", - "Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|", - "WhenOpened": "2026-01-24T10:04:59.465Z" } ] } diff --git a/WordAddIn/WordAddIn.sln b/WordAddIn/WordAddIn.sln index e0775fe..f10feb8 100644 --- a/WordAddIn/WordAddIn.sln +++ b/WordAddIn/WordAddIn.sln @@ -1,10 +1,12 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.14.36915.13 d17.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WordAddIn", "WordAddIn\WordAddIn.csproj", "{EC0A80FA-FCBD-4239-945A-627B20CCCBAA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpsInstaller", "WpsInstaller\WpsInstaller.csproj", "{A1B2C3D4-E5F6-4789-0011-223344556677}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -14,7 +16,11 @@ Global {EC0A80FA-FCBD-4239-945A-627B20CCCBAA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EC0A80FA-FCBD-4239-945A-627B20CCCBAA}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC0A80FA-FCBD-4239-945A-627B20CCCBAA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC0A80FA-FCBD-4239-945A-627B20CCCBAA}.Release|Any CPU.Build.0 = Release|Any CPU + {EC0A80FA-FCBD-4239-945A-627B20CCCBAA}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-4789-0011-223344556677}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-4789-0011-223344556677}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-4789-0011-223344556677}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-4789-0011-223344556677}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WordAddIn/WordAddIn/AiRibbon.cs b/WordAddIn/WordAddIn/AiRibbon.cs index e0b4059..a155147 100644 --- a/WordAddIn/WordAddIn/AiRibbon.cs +++ b/WordAddIn/WordAddIn/AiRibbon.cs @@ -132,7 +132,7 @@ namespace WordAddIn else if (ext == ".docx" || ext == ".doc") { // 使用 Word Interop 打开并读取文档内容 - dynamic wordApp = Globals.ThisAddIn.Application; + dynamic wordApp = AddInContext.CurrentApplication; dynamic doc = wordApp.Documents.Open(FileName: file, Visible: false, ReadOnly: true, AddToRecentFiles: false); content = doc.Content.Text; doc.Close(SaveChanges: false); @@ -174,7 +174,7 @@ namespace WordAddIn LoadingForm loading = null; try { - var doc = Globals.ThisAddIn.Application.ActiveDocument; + var doc = AddInContext.CurrentApplication.ActiveDocument; string fullText = doc.Content.Text; if (string.IsNullOrWhiteSpace(fullText) || fullText.Length < 10) @@ -230,7 +230,7 @@ namespace WordAddIn LoadingForm loading = null; try { - var selection = Globals.ThisAddIn.Application.Selection; + var selection = AddInContext.CurrentApplication.Selection; string originalText = null; // 1. 优先使用选区文本 @@ -313,7 +313,7 @@ namespace WordAddIn LoadingForm loading = null; try { - var selection = Globals.ThisAddIn.Application.Selection; + var selection = AddInContext.CurrentApplication.Selection; // 1. 判断是否有选中文本 bool hasSelection = selection.Type == Word.WdSelectionType.wdSelectionNormal || @@ -403,7 +403,7 @@ namespace WordAddIn LoadingForm loading = null; try { - var selection = Globals.ThisAddIn.Application.Selection; + var selection = AddInContext.CurrentApplication.Selection; // 1. 判断是否有选中文本或剪贴板内容 bool hasSelection = selection.Type == Word.WdSelectionType.wdSelectionNormal || @@ -489,7 +489,7 @@ namespace WordAddIn LoadingForm loading = null; try { - var selection = Globals.ThisAddIn.Application.Selection; + var selection = AddInContext.CurrentApplication.Selection; string originalText = selection.Text; // 可以为空,作为生成的上下文参考 string referenceContent = null; @@ -521,7 +521,7 @@ namespace WordAddIn // 尝试用 Word Interop 打开读取 (针对 .doc, .docx) try { - dynamic wordApp = Globals.ThisAddIn.Application; + dynamic wordApp = AddInContext.CurrentApplication; dynamic doc = wordApp.Documents.Open(FileName: path, Visible: false, ReadOnly: true, AddToRecentFiles: false); referenceContent = doc.Content.Text; doc.Close(SaveChanges: false); @@ -621,7 +621,7 @@ namespace WordAddIn LoadingForm loading = null; try { - var doc = Globals.ThisAddIn.Application.ActiveDocument; + var doc = AddInContext.CurrentApplication.ActiveDocument; string fullText = doc.Content.Text; // 1. 基础校验:文档内容太少则不处理 @@ -732,7 +732,7 @@ namespace WordAddIn LoadingForm loading = null; try { - var selection = Globals.ThisAddIn.Application.Selection; + var selection = AddInContext.CurrentApplication.Selection; string prompt = selection.Text; // 1. 尝试获取选区文本 @@ -869,8 +869,8 @@ namespace WordAddIn ? new OutlineStyleConfig() : JsonConvert.DeserializeObject(styleJson); - var doc = Globals.ThisAddIn.Application.ActiveDocument; - var selection = Globals.ThisAddIn.Application.Selection; + var doc = AddInContext.CurrentApplication.ActiveDocument; + var selection = AddInContext.CurrentApplication.Selection; Word.Range insertRange = selection.Range; insertRange.Collapse(Word.WdCollapseDirection.wdCollapseEnd); @@ -968,7 +968,7 @@ namespace WordAddIn client.DownloadFile(imageUrl, tempPath); } - var selection = Globals.ThisAddIn.Application.Selection; + var selection = AddInContext.CurrentApplication.Selection; var inlineShape = selection.InlineShapes.AddPicture(FileName: tempPath, LinkToFile: false, SaveWithDocument: true); // 强制将图片所在的段落设为“正文”样式 diff --git a/WordAddIn/WordAddIn/ThisAddIn.cs b/WordAddIn/WordAddIn/ThisAddIn.cs index cf97ac2..e43805b 100644 --- a/WordAddIn/WordAddIn/ThisAddIn.cs +++ b/WordAddIn/WordAddIn/ThisAddIn.cs @@ -15,7 +15,8 @@ namespace WordAddIn private void ThisAddIn_Startup(object sender, System.EventArgs e) { - // 在此添加初始化代码 + // 初始化全局上下文,供 AiRibbon 等使用 + AddInContext.CurrentApplication = this.Application; } private void ThisAddIn_Shutdown(object sender, System.EventArgs e) diff --git a/WordAddIn/WordAddIn/WordAddIn.csproj b/WordAddIn/WordAddIn/WordAddIn.csproj index 008899e..9e9e319 100644 --- a/WordAddIn/WordAddIn/WordAddIn.csproj +++ b/WordAddIn/WordAddIn/WordAddIn.csproj @@ -31,10 +31,10 @@ true False true - publish\ + D:\办公\ zh-chs - 1.0.0.1 + 1.0.0.5 true true 7 @@ -197,6 +197,15 @@ False true + + {AC0714F2-3D04-11D1-C2E4-00600893B433} + 1 + 0 + 0 + tlbimp + False + True + @@ -204,6 +213,9 @@ False + + True +