diff --git a/Test/Screenshots/chat_result_20260125_085757.jpg b/Test/Screenshots/chat_result_20260125_085757.jpg
new file mode 100644
index 0000000..9ccc93d
Binary files /dev/null and b/Test/Screenshots/chat_result_20260125_085757.jpg differ
diff --git a/Test/Screenshots/chat_result_20260125_085757_analyzed.jpg b/Test/Screenshots/chat_result_20260125_085757_analyzed.jpg
new file mode 100644
index 0000000..ab242e9
Binary files /dev/null and b/Test/Screenshots/chat_result_20260125_085757_analyzed.jpg differ
diff --git a/Test/Screenshots/chat_result_20260125_085849.jpg b/Test/Screenshots/chat_result_20260125_085849.jpg
new file mode 100644
index 0000000..5d97738
Binary files /dev/null and b/Test/Screenshots/chat_result_20260125_085849.jpg differ
diff --git a/Test/Screenshots/chat_result_20260125_085849_analyzed.jpg b/Test/Screenshots/chat_result_20260125_085849_analyzed.jpg
new file mode 100644
index 0000000..85bff18
Binary files /dev/null and b/Test/Screenshots/chat_result_20260125_085849_analyzed.jpg differ
diff --git a/Test/Screenshots/chat_result_20260125_090600.jpg b/Test/Screenshots/chat_result_20260125_090600.jpg
new file mode 100644
index 0000000..b8eeebe
Binary files /dev/null and b/Test/Screenshots/chat_result_20260125_090600.jpg differ
diff --git a/Test/Screenshots/chat_result_20260125_090600_analyzed.jpg b/Test/Screenshots/chat_result_20260125_090600_analyzed.jpg
new file mode 100644
index 0000000..4d459f6
Binary files /dev/null and b/Test/Screenshots/chat_result_20260125_090600_analyzed.jpg differ
diff --git a/Test/Screenshots/debug_20260125_085800_coord_01_before_click_input.jpg b/Test/Screenshots/debug_20260125_085800_coord_01_before_click_input.jpg
new file mode 100644
index 0000000..5533f8b
Binary files /dev/null and b/Test/Screenshots/debug_20260125_085800_coord_01_before_click_input.jpg differ
diff --git a/Test/Screenshots/debug_20260125_085802_coord_02_after_click_input_keyboard.jpg b/Test/Screenshots/debug_20260125_085802_coord_02_after_click_input_keyboard.jpg
new file mode 100644
index 0000000..e81d6c6
Binary files /dev/null and b/Test/Screenshots/debug_20260125_085802_coord_02_after_click_input_keyboard.jpg differ
diff --git a/Test/Screenshots/debug_20260125_085803_coord_03_after_input_text.jpg b/Test/Screenshots/debug_20260125_085803_coord_03_after_input_text.jpg
new file mode 100644
index 0000000..4e8b6d6
Binary files /dev/null and b/Test/Screenshots/debug_20260125_085803_coord_03_after_input_text.jpg differ
diff --git a/Test/Screenshots/debug_20260125_085804_coord_04_after_click_send_image.jpg b/Test/Screenshots/debug_20260125_085804_coord_04_after_click_send_image.jpg
new file mode 100644
index 0000000..3a76a37
Binary files /dev/null and b/Test/Screenshots/debug_20260125_085804_coord_04_after_click_send_image.jpg differ
diff --git a/Test/Screenshots/debug_20260125_090605_coord_01_before_click_input.jpg b/Test/Screenshots/debug_20260125_090605_coord_01_before_click_input.jpg
new file mode 100644
index 0000000..b8eeebe
Binary files /dev/null and b/Test/Screenshots/debug_20260125_090605_coord_01_before_click_input.jpg differ
diff --git a/Test/Screenshots/debug_20260125_090608_coord_02_after_click_input_keyboard.jpg b/Test/Screenshots/debug_20260125_090608_coord_02_after_click_input_keyboard.jpg
new file mode 100644
index 0000000..b8eeebe
Binary files /dev/null and b/Test/Screenshots/debug_20260125_090608_coord_02_after_click_input_keyboard.jpg differ
diff --git a/Test/Screenshots/debug_20260125_090609_coord_03_after_input_text.jpg b/Test/Screenshots/debug_20260125_090609_coord_03_after_input_text.jpg
new file mode 100644
index 0000000..b8eeebe
Binary files /dev/null and b/Test/Screenshots/debug_20260125_090609_coord_03_after_input_text.jpg differ
diff --git a/Test/Screenshots/debug_20260125_090610_coord_04_after_click_send_image.jpg b/Test/Screenshots/debug_20260125_090610_coord_04_after_click_send_image.jpg
new file mode 100644
index 0000000..1517c46
Binary files /dev/null and b/Test/Screenshots/debug_20260125_090610_coord_04_after_click_send_image.jpg differ
diff --git a/Test/Screenshots/debug_send_check_20260125_085804.jpg b/Test/Screenshots/debug_send_check_20260125_085804.jpg
new file mode 100644
index 0000000..e0a2eb4
Binary files /dev/null and b/Test/Screenshots/debug_send_check_20260125_085804.jpg differ
diff --git a/Test/Screenshots/debug_send_check_20260125_090610.jpg b/Test/Screenshots/debug_send_check_20260125_090610.jpg
new file mode 100644
index 0000000..1517c46
Binary files /dev/null and b/Test/Screenshots/debug_send_check_20260125_090610.jpg differ
diff --git a/Test/T2_testWeXinChat.py b/Test/T2_testWeXinChat.py
deleted file mode 100644
index 58f6f42..0000000
--- a/Test/T2_testWeXinChat.py
+++ /dev/null
@@ -1,271 +0,0 @@
-# coding=utf-8
-import uiautomator2 as u2
-import time
-import logging
-import sys
-import os
-import cv2
-import numpy as np
-
-# 配置日志
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-logger = logging.getLogger("TestWeChat")
-
-def analyze_chat_image(image_path, output_path):
- """
- 识别微信聊天截图中的头像并画框
- """
- logger.info(f"正在分析图片: {image_path}")
-
- # 读取图片
- # 注意:cv2.imread 不支持中文路径,需要用 np.fromfile 读取
- try:
- img_data = np.fromfile(image_path, dtype=np.uint8)
- img = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
- except Exception as e:
- logger.error(f"读取图片失败: {e}")
- return
-
- if img is None:
- logger.error("图片读取为空")
- return
-
- height, width = img.shape[:2]
- logger.info(f"图片尺寸: {width}x{height}")
-
- # 1. 预处理
- # 转为灰度图
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
-
- # 使用自适应阈值二值化
- thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
- cv2.THRESH_BINARY_INV, 11, 2)
-
- # 形态学操作:闭运算,填充内部空洞
- kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
- closed = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, kernel)
-
- # 2. 轮廓查找
- contours, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
- logger.info(f"检测到轮廓数量: {len(contours)}")
-
- # 收集所有符合条件的头像
- avatars = []
-
- # 3. 筛选轮廓
- for contour in contours:
- # 获取外接矩形
- x, y, w, h = cv2.boundingRect(contour)
-
- # 筛选条件优化:
- # 1. 形状接近正方形 (放宽宽高比限制: 0.8 ~ 1.2)
- aspect_ratio = float(w) / h
-
- # 2. 尺寸适中
- # 假设头像宽度在屏幕宽度的 6% 到 15% 之间
- min_w = width * 0.06
- max_w = width * 0.15
-
- # 3. 位置筛选
- # 排除底部输入框区域 (假设底部 10% 为输入区域)
- if y > height * 0.9:
- continue
-
- # 左侧头像:靠左边 (x < width * 0.18)
- # 右侧头像:靠右边 (x > width * 0.82)
- is_left = x < width * 0.18
- is_right = x > width * 0.82
-
- if 0.8 <= aspect_ratio <= 1.2 and min_w < w < max_w:
- if is_left or is_right:
- side = "Left" if is_left else "Right"
- avatars.append({
- 'x': x, 'y': y, 'w': w, 'h': h,
- 'side': side
- })
-
- # 按 y 坐标排序
- avatars.sort(key=lambda a: a['y'])
-
- logger.info(f"找到有效头像数量: {len(avatars)}")
-
- # 4. 绘制对话内容框 (Green/Red Boxes)
- # 策略:按顺序遍历头像,如果发现同侧连续,则视为一组。
- # 从当前组的第一个头像上方开始,直到下一个不同侧的头像上方(或底部)。
-
- if avatars:
- i = 0
- while i < len(avatars):
- current_group_start = i
- current_side = avatars[i]['side']
-
- # 找到当前组的结束位置 (即下一个不同侧头像的索引)
- j = i + 1
- while j < len(avatars) and avatars[j]['side'] == current_side:
- j += 1
-
- # 当前组范围: avatars[i] ... avatars[j-1]
- # 确定绘制区域的 Y 轴范围
-
- # Start Y: 当前组第一个头像的上方 (例如 -10px)
- start_y = max(0, avatars[i]['y'] - 10)
-
- # End Y: 下一组第一个头像的上方 (减去较大间距,例如 -30px),或者当前组最后一个头像的底部加上边距
- # 为了让框之间有明显间隔,我们采取策略:
- # 如果有下一组,End Y = 下一组第一个头像的 y - 30 (留出间隙)
- # 如果没有下一组,End Y = 屏幕底部区域上方
-
- if j < len(avatars):
- end_y = max(start_y + 10, avatars[j]['y'] - 30)
- else:
- end_y = int(height * 0.9) # 到底部输入框上方
-
- # 绘制大框
- # 左侧 (Left) -> 对方 -> 绿色 (0, 255, 0)
- # 右侧 (Right) -> 我 -> 红色 (0, 0, 255)
- # 注意 OpenCV 颜色是 BGR
- box_color = (0, 255, 0) if current_side == "Left" else (0, 0, 255)
-
- # 绘制矩形 (空心,线宽 5)
- # X 轴范围:0 到 width
- cv2.rectangle(img, (0, start_y), (width, end_y), box_color, 5)
-
- logger.info(f"绘制内容框: 侧别={current_side}, 范围 Y={start_y} to {end_y}")
-
- # 移动到下一组
- i = j
-
- # 5. 绘制头像框 (Blue/Yellow Boxes) - 画在内容框之上
- for av in avatars:
- x, y, w, h = av['x'], av['y'], av['w'], av['h']
- # 左侧:蓝色 (BGR: 255, 0, 0)
- # 右侧:黄色 (BGR: 0, 255, 255)
- color = (255, 0, 0) if av['side'] == "Left" else (0, 255, 255)
-
- cv2.rectangle(img, (x, y), (x + w, y + h), color, 10)
- logger.info(f"绘制头像: 位置=({x},{y}), 侧别={av['side']}")
-
- logger.info(f"共标记了 {len(avatars)} 个头像")
-
- # 6. 保存结果
- try:
- # cv2.imwrite 不支持中文路径,使用 imencode + tofile
- ext = os.path.splitext(output_path)[1]
- cv2.imencode(ext, img)[1].tofile(output_path)
- logger.info(f"✅ 分析结果已保存至: {output_path}")
- except Exception as e:
- logger.error(f"保存分析图片失败: {e}")
-
-def main():
- logger.info("开始执行微信搜索测试...")
-
- # 连接设备
- try:
- d = u2.connect()
- logger.info(f"设备连接成功: {d.info.get('serial')}")
- except Exception as e:
- logger.error(f"设备连接失败: {e}")
- return
-
- # 1. 启动微信
- logger.info("步骤 1: 启动微信...")
- d.app_start("com.tencent.mm", stop=True)
- # 等待微信启动完成
- time.sleep(5)
-
- # 获取屏幕尺寸
- w, h = d.window_size()
- logger.info(f"屏幕尺寸: {w}x{h}")
-
- # 2. 点击搜索按钮 (参考 Opener.py 的坐标比例: w * 0.84, h * 0.08)
- search_x = int(w * 0.84)
- search_y = int(h * 0.08)
- logger.info(f"步骤 2: 点击搜索按钮 (坐标: {search_x}, {search_y})...")
- d.click(search_x, search_y)
- time.sleep(2)
-
- # 3. 输入 "糖豆爸爸"
- target_name = "糖豆爸爸"
- logger.info(f"步骤 3: 输入搜索内容 '{target_name}'...")
-
- try:
- # 启用 FastInputIME 以支持中文输入
- d.set_input_ime(True)
-
- # 点击搜索框获取焦点 (参考 Opener.py: w * 0.4, h * 0.08)
- d.click(int(w * 0.4), int(h * 0.08))
- time.sleep(1)
-
- # 输入文字
- d.send_keys(target_name)
- time.sleep(2)
-
- # 恢复输入法
- d.set_input_ime(False)
- except Exception as e:
- logger.error(f"输入文字失败: {e}")
- try:
- d(focused=True).set_text(target_name)
- except:
- pass
-
- # 4. 点击搜索结果
- logger.info("步骤 4: 查找并点击搜索结果...")
- time.sleep(2)
-
- found = False
-
- # 策略 A: 精确匹配文本
- if d(text=target_name).exists:
- logger.info(f"找到文本为 '{target_name}' 的元素,点击...")
- d(text=target_name).click()
- found = True
- else:
- logger.warning(f"未找到文本为 '{target_name}' 的元素,尝试模糊匹配或坐标点击...")
-
- # 策略 B: 坐标点击 (参考 Opener.py 点击第一个结果: w * 0.5, h * 0.18)
- result_x = int(w * 0.5)
- result_y = int(h * 0.18)
- logger.info(f"尝试点击第一个搜索结果位置 ({result_x}, {result_y})...")
- d.click(result_x, result_y)
- found = True
-
- if found:
- logger.info("✅ 已执行点击操作,应该已进入对话框。")
-
- # 5. 截图保存结果
- logger.info("步骤 5: 截图并分析...")
- # 等待界面加载稳定
- time.sleep(3)
-
- # 创建截图目录
- screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
- if not os.path.exists(screenshot_dir):
- os.makedirs(screenshot_dir)
-
- # 生成文件名
- timestamp = time.strftime("%Y%m%d_%H%M%S")
- filename = f"chat_result_{timestamp}.jpg"
- save_path = os.path.join(screenshot_dir, filename)
-
- try:
- d.screenshot(save_path)
- logger.info(f"✅ 原始截图已保存: {save_path}")
-
- # 6. 分析截图并标记头像
- logger.info("步骤 6: 自动标记头像...")
- analyzed_filename = f"chat_result_{timestamp}_analyzed.jpg"
- analyzed_path = os.path.join(screenshot_dir, analyzed_filename)
-
- analyze_chat_image(save_path, analyzed_path)
-
- except Exception as e:
- logger.error(f"❌ 截图或分析失败: {e}")
-
- else:
- logger.error("❌ 未能定位到搜索结果,跳过截图。")
-
- logger.info("测试结束。")
-
-if __name__ == "__main__":
- main()
diff --git a/WeiXin/T1_OpenChat.py b/WeiXin/T1_OpenChat.py
new file mode 100644
index 0000000..fff150c
--- /dev/null
+++ b/WeiXin/T1_OpenChat.py
@@ -0,0 +1,94 @@
+# coding=utf-8
+import uiautomator2 as u2
+import time
+import logging
+import sys
+import os
+
+# 添加项目根目录到 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)
+
+# 配置日志
+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, "T1_OpenChat.log"), encoding='utf-8'),
+ logging.StreamHandler()
+ ]
+)
+logger = logging.getLogger("T1_OpenChat")
+
+def open_chat(target_name="糖豆爸爸"):
+ logger.info(f"开始执行 T1: 打开微信并进入 '{target_name}' 的对话框...")
+
+ # 连接设备
+ try:
+ d = u2.connect()
+ logger.info(f"设备连接成功: {d.info.get('serial')}")
+ except Exception as e:
+ logger.error(f"设备连接失败: {e}")
+ return
+
+ # 1. 启动微信
+ logger.info("步骤 1: 启动微信...")
+ d.app_start("com.tencent.mm", stop=True)
+ time.sleep(5)
+
+ # 获取屏幕尺寸
+ w, h = d.window_size()
+ logger.info(f"屏幕尺寸: {w}x{h}")
+
+ # 2. 点击搜索按钮
+ search_x = int(w * 0.84)
+ search_y = int(h * 0.08)
+ logger.info(f"步骤 2: 点击搜索按钮 (坐标: {search_x}, {search_y})...")
+ d.click(search_x, search_y)
+ time.sleep(2)
+
+ # 3. 输入搜索内容
+ logger.info(f"步骤 3: 输入搜索内容 '{target_name}'...")
+ try:
+ d.set_input_ime(True)
+ d.click(int(w * 0.4), int(h * 0.08))
+ time.sleep(1)
+ d.send_keys(target_name)
+ time.sleep(2)
+ d.set_input_ime(False)
+ except Exception as e:
+ logger.error(f"输入文字失败: {e}")
+ try:
+ d(focused=True).set_text(target_name)
+ except:
+ pass
+
+ # 4. 点击搜索结果
+ logger.info("步骤 4: 查找并点击搜索结果...")
+ time.sleep(2)
+
+ found = False
+ if d(text=target_name).exists:
+ logger.info(f"找到文本为 '{target_name}' 的元素,点击...")
+ d(text=target_name).click()
+ found = True
+ else:
+ logger.warning(f"未找到文本为 '{target_name}' 的元素,尝试模糊匹配或坐标点击...")
+ result_x = int(w * 0.5)
+ result_y = int(h * 0.18)
+ logger.info(f"尝试点击第一个搜索结果位置 ({result_x}, {result_y})...")
+ d.click(result_x, result_y)
+ found = True
+
+ if found:
+ logger.info("✅ T1 执行完成: 已进入对话框。")
+ else:
+ logger.error("❌ T1 执行失败: 未能进入对话框。")
+
+if __name__ == "__main__":
+ open_chat()
diff --git a/WeiXin/T2_GetHistory.py b/WeiXin/T2_GetHistory.py
new file mode 100644
index 0000000..369fa6f
--- /dev/null
+++ b/WeiXin/T2_GetHistory.py
@@ -0,0 +1,69 @@
+# coding=utf-8
+import uiautomator2 as u2
+import time
+import logging
+import sys
+import os
+
+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 analyze_chat_image
+
+# 配置日志
+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, "T2_GetHistory.log"), encoding='utf-8'),
+ logging.StreamHandler()
+ ]
+)
+logger = logging.getLogger("T2_GetHistory")
+
+def get_history(target_name="对方"):
+ logger.info("开始执行 T2: 获取当前屏幕对话历史...")
+
+ try:
+ d = u2.connect()
+ logger.info(f"设备连接成功: {d.info.get('serial')}")
+ except Exception as e:
+ logger.error(f"设备连接失败: {e}")
+ return
+
+ # 截图
+ screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
+ if not os.path.exists(screenshot_dir):
+ os.makedirs(screenshot_dir)
+
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
+ filename = f"T2_history_{timestamp}.jpg"
+ save_path = os.path.join(screenshot_dir, filename)
+
+ try:
+ d.screenshot(save_path)
+ logger.info(f"截图已保存: {save_path}")
+
+ analyzed_filename = f"T2_history_{timestamp}_analyzed.jpg"
+ analyzed_path = os.path.join(screenshot_dir, analyzed_filename)
+
+ # 调用 WxUtil 中的分析函数
+ dialogue_log = analyze_chat_image(save_path, analyzed_path, target_name=target_name)
+
+ logger.info("✅ T2 执行完成。历史对话如下:")
+ if dialogue_log:
+ for log in dialogue_log:
+ print(log) # 打印到控制台
+ else:
+ logger.info("未提取到对话内容或当前屏幕无对话气泡。")
+
+ except Exception as e:
+ logger.error(f"❌ T2 执行失败: {e}")
+
+if __name__ == "__main__":
+ get_history()
diff --git a/WeiXin/T3_MarkInputBox.py b/WeiXin/T3_MarkInputBox.py
new file mode 100644
index 0000000..25fd0a8
--- /dev/null
+++ b/WeiXin/T3_MarkInputBox.py
@@ -0,0 +1,88 @@
+# coding=utf-8
+import uiautomator2 as u2
+import time
+import logging
+import sys
+import os
+import cv2
+import numpy as np
+
+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
+
+# 配置日志
+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, "T3_MarkInputBox.log"), encoding='utf-8'),
+ logging.StreamHandler()
+ ]
+)
+logger = logging.getLogger("T3_MarkInputBox")
+
+def mark_input_box():
+ logger.info("开始执行 T3: 标识输入框位置...")
+
+ try:
+ d = u2.connect()
+ logger.info(f"设备连接成功: {d.info.get('serial')}")
+ except Exception as e:
+ logger.error(f"设备连接失败: {e}")
+ return
+
+ screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
+ if not os.path.exists(screenshot_dir):
+ os.makedirs(screenshot_dir)
+
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
+ filename = f"T3_input_{timestamp}.jpg"
+ save_path = os.path.join(screenshot_dir, filename)
+
+ try:
+ d.screenshot(save_path)
+ logger.info(f"截图已保存: {save_path}")
+
+ # 调用 WxUtil 识别输入框
+ center_point, rect_box = find_input_box_center(save_path)
+
+ if center_point and rect_box:
+ # 读取图片进行绘制
+ img_data = np.fromfile(save_path, dtype=np.uint8)
+ img = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
+
+ x, y, w, h = rect_box
+ cx, cy = center_point
+
+ # 要求:绿色框,模块点击点处标识小红点
+ # 绿色框 (Green): BGR (0, 255, 0)
+ cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 5)
+
+ # 小红点 (Red): BGR (0, 0, 255)
+ # 半径 10, 实心
+ cv2.circle(img, (cx, cy), 10, (0, 0, 255), -1)
+
+ output_filename = f"T3_input_{timestamp}_marked.jpg"
+ output_path = os.path.join(screenshot_dir, output_filename)
+
+ # 保存
+ ext = os.path.splitext(output_path)[1]
+ cv2.imencode(ext, img)[1].tofile(output_path)
+ logger.info(f"✅ T3 执行完成: 标记图片已保存至 {output_path}")
+ logger.info(f"输入框区域: {rect_box}, 点击中心点: {center_point}")
+
+ else:
+ logger.warning("❌ T3 执行结果: 未能识别到输入框。")
+
+ except Exception as e:
+ logger.error(f"❌ T3 执行失败: {e}")
+
+if __name__ == "__main__":
+ mark_input_box()
diff --git a/Test/T1_StartWeiXin.py b/WeiXin/WxUtil.py
similarity index 77%
rename from Test/T1_StartWeiXin.py
rename to WeiXin/WxUtil.py
index 9a4449f..206dc99 100644
--- a/Test/T1_StartWeiXin.py
+++ b/WeiXin/WxUtil.py
@@ -16,8 +16,9 @@ if project_root not in sys.path:
from Util.EasyOcrKit import get_easyocr_reader
# 配置日志
-logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-logger = logging.getLogger("TestWeChat")
+# 注意:作为库模块,不应直接调用 basicConfig,以免干扰调用者的日志配置
+# 调用者应自行配置日志(包括 FileHandler 等)
+logger = logging.getLogger("WxUtil")
def find_input_box_center(image_path):
"""
@@ -48,8 +49,7 @@ def find_input_box_center(image_path):
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
- best_cnt = None
- max_area = 0
+ candidates = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
@@ -61,15 +61,36 @@ def find_input_box_center(image_path):
global_y = crop_y_start + y + h // 2
# 排除过于靠下的区域 (导航栏/手势条),通常在最后 2%
- if global_y > height * 0.98:
+ if global_y > height * 0.99:
continue
-
- if w * h > max_area:
- max_area = w * h
- best_cnt = (x, y, w, h)
-
- if best_cnt:
- x, y, w, h = best_cnt
+
+ candidates.append({
+ 'x': x, 'y': y, 'w': w, 'h': h,
+ 'global_y': global_y,
+ 'area': w * h
+ })
+
+ best_candidate = None
+ if candidates:
+ # 优先选择最靠下的候选框 (Y 坐标最大)
+ # 同时考虑宽度不能太离谱
+ candidates.sort(key=lambda c: c['global_y'], reverse=True)
+
+ # 检查最靠下的候选框是否合理
+ # 正常情况下输入框应该在屏幕底部 92% - 98% 之间
+ # 如果最靠下的候选框 Y < 92%,说明可能识别错了 (可能是倒数第二条消息)
+ for cand in candidates:
+ if cand['global_y'] > height * 0.92:
+ best_candidate = cand
+ break
+
+ # 如果没有找到足够靠下的,但有候选框,尝试放宽条件或者使用最靠下的那个
+ if not best_candidate and candidates:
+ logger.warning("未找到 >92% 高度的输入框,尝试使用最靠下的候选框")
+ best_candidate = candidates[0]
+
+ if best_candidate:
+ x, y, w, h = best_candidate['x'], best_candidate['y'], best_candidate['w'], best_candidate['h']
center_x = x + w // 2
center_y = crop_y_start + y + h // 2
logger.info(f"找到输入框(CV-Canny): ({center_x}, {center_y}), 尺寸: {w}x{h}")
@@ -80,27 +101,33 @@ def find_input_box_center(image_path):
cv2.THRESH_BINARY_INV, 11, 2)
contours_thresh, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
+ candidates_thresh = []
for cnt in contours_thresh:
x, y, w, h = cv2.boundingRect(cnt)
if width * 0.4 < w < width * 0.95 and 20 < h < bottom_h * 0.8:
global_y = crop_y_start + y + h // 2
- if global_y > height * 0.98:
+ if global_y > height * 0.99:
continue
- if w * h > max_area:
- max_area = w * h
- best_cnt = (x, y, w, h)
-
- if best_cnt:
- x, y, w, h = best_cnt
- center_x = x + w // 2
- center_y = crop_y_start + y + h // 2
- logger.info(f"找到输入框(CV-Adaptive): ({center_x}, {center_y}), 尺寸: {w}x{h}")
- return (center_x, center_y), (x, crop_y_start + y, w, h)
+ candidates_thresh.append({
+ 'x': x, 'y': y, 'w': w, 'h': h,
+ 'global_y': global_y
+ })
+
+ if candidates_thresh:
+ candidates_thresh.sort(key=lambda c: c['global_y'], reverse=True)
+ best = candidates_thresh[0]
+ # 同样应用 > 92% 规则
+ if best['global_y'] > height * 0.92:
+ x, y, w, h = best['x'], best['y'], best['w'], best['h']
+ center_x = x + w // 2
+ center_y = crop_y_start + y + h // 2
+ logger.info(f"找到输入框(CV-Adaptive): ({center_x}, {center_y}), 尺寸: {w}x{h}")
+ return (center_x, center_y), (x, crop_y_start + y, w, h)
- # 兜底策略:使用更靠下的默认坐标 (94%)
- # 之前的 90.5% 用户反馈偏上
- logger.warning("未找到明显输入框轮廓,使用更靠下的默认坐标 (94%)")
- default_y = int(height * 0.94)
+ # 兜底策略:使用更靠下的默认坐标 (96% - 微信输入框通常很靠下)
+ # 之前的 94% 可能还是偏高
+ logger.warning("未找到明显输入框轮廓,使用更靠下的默认坐标 (96.5%)")
+ default_y = int(height * 0.965)
center_x = width // 2
# 构造假想框
@@ -357,6 +384,13 @@ def perform_input_action(coords, text):
d.send_keys(text)
except Exception as sk_e:
logger.error(f"send_keys 失败: {sk_e}")
+
+ # 尝试广播 input
+ try:
+ escaped_text = text.replace("'", "\\'").replace(" ", "%s")
+ d.shell(f"input text '{escaped_text}'")
+ except:
+ pass
# 3. 截图:输入文本后
take_debug_screenshot(d, "coord_03_after_input_text")
@@ -596,130 +630,4 @@ def analyze_chat_image(image_path, output_path, target_name="对方"):
logger.info(line)
logger.info("="*30 + "\n")
-def main():
- logger.info("开始执行微信搜索测试...")
-
- # 连接设备
- try:
- d = u2.connect()
- logger.info(f"设备连接成功: {d.info.get('serial')}")
- except Exception as e:
- logger.error(f"设备连接失败: {e}")
- return
-
- # 1. 启动微信
- logger.info("步骤 1: 启动微信...")
- d.app_start("com.tencent.mm", stop=True)
- # 等待微信启动完成
- time.sleep(5)
-
- # 获取屏幕尺寸
- w, h = d.window_size()
- logger.info(f"屏幕尺寸: {w}x{h}")
-
- # 2. 点击搜索按钮 (参考 Opener.py 的坐标比例: w * 0.84, h * 0.08)
- search_x = int(w * 0.84)
- search_y = int(h * 0.08)
- logger.info(f"步骤 2: 点击搜索按钮 (坐标: {search_x}, {search_y})...")
- d.click(search_x, search_y)
- time.sleep(2)
-
- # 3. 输入 "糖豆爸爸"
- target_name = "糖豆爸爸"
- logger.info(f"步骤 3: 输入搜索内容 '{target_name}'...")
-
- try:
- # 启用 FastInputIME 以支持中文输入
- d.set_input_ime(True)
-
- # 点击搜索框获取焦点 (参考 Opener.py: w * 0.4, h * 0.08)
- d.click(int(w * 0.4), int(h * 0.08))
- time.sleep(1)
-
- # 输入文字
- d.send_keys(target_name)
- time.sleep(2)
-
- # 恢复输入法
- d.set_input_ime(False)
- except Exception as e:
- logger.error(f"输入文字失败: {e}")
- try:
- d(focused=True).set_text(target_name)
- except:
- pass
-
- # 4. 点击搜索结果
- logger.info("步骤 4: 查找并点击搜索结果...")
- time.sleep(2)
-
- found = False
-
- # 策略 A: 精确匹配文本
- if d(text=target_name).exists:
- logger.info(f"找到文本为 '{target_name}' 的元素,点击...")
- d(text=target_name).click()
- found = True
- else:
- logger.warning(f"未找到文本为 '{target_name}' 的元素,尝试模糊匹配或坐标点击...")
-
- # 策略 B: 坐标点击 (参考 Opener.py 点击第一个结果: w * 0.5, h * 0.18)
- result_x = int(w * 0.5)
- result_y = int(h * 0.18)
- logger.info(f"尝试点击第一个搜索结果位置 ({result_x}, {result_y})...")
- d.click(result_x, result_y)
- found = True
-
- if found:
- logger.info("✅ 已执行点击操作,应该已进入对话框。")
-
- # 5. 截图保存结果
- logger.info("步骤 5: 截图并分析...")
- # 等待界面加载稳定
- time.sleep(3)
-
- # 创建截图目录
- screenshot_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Screenshots")
- if not os.path.exists(screenshot_dir):
- os.makedirs(screenshot_dir)
-
- # 生成文件名
- timestamp = time.strftime("%Y%m%d_%H%M%S")
- filename = f"chat_result_{timestamp}.jpg"
- save_path = os.path.join(screenshot_dir, filename)
-
- try:
- d.screenshot(save_path)
- logger.info(f"✅ 原始截图已保存: {save_path}")
-
- # 6. 分析截图并标记头像
- logger.info("步骤 6: 自动标记头像和识别内容...")
- analyzed_filename = f"chat_result_{timestamp}_analyzed.jpg"
- analyzed_path = os.path.join(screenshot_dir, analyzed_filename)
-
- analyze_chat_image(save_path, analyzed_path, target_name=target_name)
-
- # 7. 查找输入框并执行自动化操作
- logger.info("步骤 7: 自动回复...")
- # 注意:analyze_chat_image 内部已经调用了 find_input_box_center 并且画在图上了
- # 但我们需要返回值来执行操作。analyze_chat_image 并没有返回坐标。
- # 为了简单,再次调用一次 (或者修改 analyze_chat_image 返回坐标,但改动较大)
- # 这里直接再次调用 find_input_box_center 获取坐标
- coords, _ = find_input_box_center(save_path)
-
- if coords:
- logger.info(f"输入框坐标: {coords}")
- perform_input_action(coords, "AI助手我现在可以开始和你聊天了!")
- else:
- logger.warning("未找到输入框坐标,跳过回复")
-
- except Exception as e:
- logger.error(f"❌ 截图或分析失败: {e}")
-
- else:
- logger.error("❌ 未能定位到搜索结果,跳过截图。")
-
- logger.info("测试结束。")
-
-if __name__ == "__main__":
- main()
+ return dialogue_log
diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/01824cf5-110f-4df4-bef9-da7d42a8723b.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/01824cf5-110f-4df4-bef9-da7d42a8723b.vsidx
new file mode 100644
index 0000000..89942fb
Binary files /dev/null and b/WordAddIn/.vs/WordAddIn/FileContentIndex/01824cf5-110f-4df4-bef9-da7d42a8723b.vsidx differ
diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/267372ec-0c92-491b-b3e2-fc07590dfc90.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/267372ec-0c92-491b-b3e2-fc07590dfc90.vsidx
new file mode 100644
index 0000000..3d8c1dd
Binary files /dev/null and b/WordAddIn/.vs/WordAddIn/FileContentIndex/267372ec-0c92-491b-b3e2-fc07590dfc90.vsidx differ
diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/2c66d38d-0c2c-4ae7-b23e-a980a4a7e1fb.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/2c66d38d-0c2c-4ae7-b23e-a980a4a7e1fb.vsidx
new file mode 100644
index 0000000..70aef67
Binary files /dev/null and b/WordAddIn/.vs/WordAddIn/FileContentIndex/2c66d38d-0c2c-4ae7-b23e-a980a4a7e1fb.vsidx differ
diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/abbe6c8c-a6a1-4efb-831f-84215a9cba5b.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/abbe6c8c-a6a1-4efb-831f-84215a9cba5b.vsidx
new file mode 100644
index 0000000..3b0a55c
Binary files /dev/null and b/WordAddIn/.vs/WordAddIn/FileContentIndex/abbe6c8c-a6a1-4efb-831f-84215a9cba5b.vsidx differ
diff --git a/WordAddIn/.vs/WordAddIn/FileContentIndex/daa1de98-5b55-4225-858f-4d331877adf6.vsidx b/WordAddIn/.vs/WordAddIn/FileContentIndex/daa1de98-5b55-4225-858f-4d331877adf6.vsidx
new file mode 100644
index 0000000..1cfa1c3
Binary files /dev/null and b/WordAddIn/.vs/WordAddIn/FileContentIndex/daa1de98-5b55-4225-858f-4d331877adf6.vsidx differ
diff --git a/WordAddIn/.vs/WordAddIn/v17/.suo b/WordAddIn/.vs/WordAddIn/v17/.suo
index 3b19fbd..5a9feda 100644
Binary files a/WordAddIn/.vs/WordAddIn/v17/.suo and b/WordAddIn/.vs/WordAddIn/v17/.suo differ
diff --git a/WordAddIn/WordAddIn/CreateOutlineForm.cs b/WordAddIn/WordAddIn/CreateOutlineForm.cs
new file mode 100644
index 0000000..ffdb706
--- /dev/null
+++ b/WordAddIn/WordAddIn/CreateOutlineForm.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace WordAddIn
+{
+ ///
+ /// 创建大纲输入窗体。
+ /// 功能描述:
+ /// 1. 提供一个文本框,让用户输入大纲的主题、关键词或详细描述。
+ /// 2. 作为生成大纲的前置步骤,获取用户的初始意图。
+ ///
+ public class CreateOutlineForm : Form
+ {
+ private TextBox txtTopic;
+ private Button btnGenerate;
+ private Button btnCancel;
+ private Label lblTopic;
+
+ ///
+ /// 获取用户输入的主题内容。
+ ///
+ public string Topic { get; private set; }
+
+ public CreateOutlineForm()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ /// 初始化窗体组件。
+ ///
+ private void InitializeComponent()
+ {
+ this.lblTopic = new System.Windows.Forms.Label();
+ this.txtTopic = new System.Windows.Forms.TextBox();
+ this.btnGenerate = new System.Windows.Forms.Button();
+ this.btnCancel = new System.Windows.Forms.Button();
+ this.SuspendLayout();
+ //
+ // lblTopic
+ //
+ this.lblTopic.AutoSize = true;
+ this.lblTopic.Location = new System.Drawing.Point(20, 20);
+ this.lblTopic.Name = "lblTopic";
+ this.lblTopic.Size = new System.Drawing.Size(191, 12);
+ this.lblTopic.TabIndex = 0;
+ this.lblTopic.Text = "请输入文档主题或描述 (支持多行):";
+ //
+ // txtTopic
+ //
+ this.txtTopic.Location = new System.Drawing.Point(20, 50);
+ this.txtTopic.Multiline = true;
+ this.txtTopic.Name = "txtTopic";
+ this.txtTopic.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
+ this.txtTopic.Size = new System.Drawing.Size(440, 240);
+ this.txtTopic.TabIndex = 1;
+ //
+ // btnGenerate
+ //
+ this.btnGenerate.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.btnGenerate.Location = new System.Drawing.Point(240, 310);
+ this.btnGenerate.Name = "btnGenerate";
+ this.btnGenerate.Size = new System.Drawing.Size(100, 35);
+ this.btnGenerate.TabIndex = 2;
+ this.btnGenerate.Text = "生成大纲";
+ this.btnGenerate.UseVisualStyleBackColor = true;
+ this.btnGenerate.Click += new System.EventHandler(this.BtnGenerate_Click);
+ //
+ // btnCancel
+ //
+ this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.btnCancel.Location = new System.Drawing.Point(360, 310);
+ this.btnCancel.Name = "btnCancel";
+ this.btnCancel.Size = new System.Drawing.Size(100, 35);
+ this.btnCancel.TabIndex = 3;
+ this.btnCancel.Text = "取消";
+ this.btnCancel.UseVisualStyleBackColor = true;
+ //
+ // CreateOutlineForm
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.CancelButton = this.btnCancel;
+ this.ClientSize = new System.Drawing.Size(500, 370);
+ this.Controls.Add(this.btnCancel);
+ this.Controls.Add(this.btnGenerate);
+ this.Controls.Add(this.txtTopic);
+ this.Controls.Add(this.lblTopic);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.Name = "CreateOutlineForm";
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+ this.Text = "创建大纲 - AI助手";
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ ///
+ /// 生成按钮点击事件。
+ /// 验证输入是否为空,若通过则设置 Topic 属性并关闭窗体。
+ ///
+ private void BtnGenerate_Click(object sender, EventArgs e)
+ {
+ Topic = txtTopic.Text.Trim();
+ if (string.IsNullOrWhiteSpace(Topic))
+ {
+ MessageBox.Show("请输入主题或描述内容。");
+ this.DialogResult = DialogResult.None;
+ return;
+ }
+ }
+ }
+}
diff --git a/WordAddIn/WordAddIn/OutlineSettingsForm.cs b/WordAddIn/WordAddIn/OutlineSettingsForm.cs
new file mode 100644
index 0000000..9614294
--- /dev/null
+++ b/WordAddIn/WordAddIn/OutlineSettingsForm.cs
@@ -0,0 +1,177 @@
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+using WordAddIn.Models;
+using WordAddIn.Properties;
+using Newtonsoft.Json;
+
+namespace WordAddIn
+{
+ ///
+ /// 大纲样式配置窗体。
+ /// 功能描述:
+ /// 1. 允许用户为大纲的不同层级(主标题、一级目录、二级目录、三级目录)配置样式。
+ /// 2. 使用 TabControl 组织不同层级的样式编辑器(StyleEditorControl)。
+ /// 3. 支持保存配置到本地设置(Settings.Default.OutlineStyles)和加载已有配置。
+ ///
+ public class OutlineSettingsForm : Form
+ {
+ private TabControl tabControl;
+ private Button btnSave;
+ private Button btnCancel;
+
+ private StyleEditorControl mainTitleEditor;
+ private StyleEditorControl h1Editor;
+ private StyleEditorControl h2Editor;
+ private StyleEditorControl h3Editor;
+
+ public OutlineSettingsForm()
+ {
+ InitializeComponent();
+ InitializeCustomControls();
+ LoadSettings();
+ }
+
+ ///
+ /// 初始化基本窗体组件。
+ ///
+ private void InitializeComponent()
+ {
+ this.tabControl = new System.Windows.Forms.TabControl();
+ this.btnSave = new System.Windows.Forms.Button();
+ this.btnCancel = new System.Windows.Forms.Button();
+ this.SuspendLayout();
+ //
+ // tabControl
+ //
+ this.tabControl.Location = new System.Drawing.Point(12, 12);
+ this.tabControl.Name = "tabControl";
+ this.tabControl.SelectedIndex = 0;
+ this.tabControl.Size = new System.Drawing.Size(410, 280);
+ this.tabControl.TabIndex = 0;
+ //
+ // btnSave
+ //
+ this.btnSave.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.btnSave.Location = new System.Drawing.Point(210, 310);
+ this.btnSave.Name = "btnSave";
+ this.btnSave.Size = new System.Drawing.Size(100, 35);
+ this.btnSave.TabIndex = 1;
+ this.btnSave.Text = "保存配置";
+ this.btnSave.UseVisualStyleBackColor = true;
+ this.btnSave.Click += new System.EventHandler(this.BtnSave_Click);
+ //
+ // btnCancel
+ //
+ this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.btnCancel.Location = new System.Drawing.Point(320, 310);
+ this.btnCancel.Name = "btnCancel";
+ this.btnCancel.Size = new System.Drawing.Size(100, 35);
+ this.btnCancel.TabIndex = 2;
+ this.btnCancel.Text = "取消";
+ this.btnCancel.UseVisualStyleBackColor = true;
+ //
+ // OutlineSettingsForm
+ //
+ this.AcceptButton = this.btnSave;
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.CancelButton = this.btnCancel;
+ this.ClientSize = new System.Drawing.Size(450, 360);
+ this.Controls.Add(this.btnCancel);
+ this.Controls.Add(this.btnSave);
+ this.Controls.Add(this.tabControl);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.Name = "OutlineSettingsForm";
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+ this.Text = "大纲样式配置";
+ this.ResumeLayout(false);
+
+ }
+
+ ///
+ /// 初始化自定义样式编辑器控件,并添加到 Tab 页中。
+ ///
+ private void InitializeCustomControls()
+ {
+ mainTitleEditor = new StyleEditorControl("主标题", true);
+ h1Editor = new StyleEditorControl("一级目录", false);
+ h2Editor = new StyleEditorControl("二级目录", false);
+ h3Editor = new StyleEditorControl("三级目录", false);
+
+ AddTab("主标题", mainTitleEditor);
+ AddTab("一级目录", h1Editor);
+ AddTab("二级目录", h2Editor);
+ AddTab("三级目录", h3Editor);
+ }
+
+ ///
+ /// 辅助方法:添加一个新的 Tab 页。
+ ///
+ private void AddTab(string title, Control content)
+ {
+ TabPage page = new TabPage(title);
+ content.Dock = DockStyle.Fill;
+ page.Controls.Add(content);
+ tabControl.TabPages.Add(page);
+ }
+
+ ///
+ /// 从 Settings 中加载保存的样式配置。
+ ///
+ private void LoadSettings()
+ {
+ try
+ {
+ string json = Settings.Default.OutlineStyles;
+ OutlineStyleConfig config = null;
+
+ if (!string.IsNullOrEmpty(json))
+ {
+ config = JsonConvert.DeserializeObject(json);
+ }
+
+ if (config == null)
+ {
+ config = new OutlineStyleConfig(); // Use defaults
+ }
+
+ mainTitleEditor.SetStyle(config.MainTitle);
+ h1Editor.SetStyle(config.Heading1);
+ h2Editor.SetStyle(config.Heading2);
+ h3Editor.SetStyle(config.Heading3);
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("加载配置失败: " + ex.Message);
+ }
+ }
+
+ ///
+ /// 保存按钮点击事件。
+ /// 将当前配置序列化为 JSON 并保存到 Settings。
+ ///
+ private void BtnSave_Click(object sender, EventArgs e)
+ {
+ try
+ {
+ OutlineStyleConfig config = new OutlineStyleConfig();
+ config.MainTitle = mainTitleEditor.GetStyle();
+ config.Heading1 = h1Editor.GetStyle();
+ config.Heading2 = h2Editor.GetStyle();
+ config.Heading3 = h3Editor.GetStyle();
+
+ string json = JsonConvert.SerializeObject(config);
+ Settings.Default.OutlineStyles = json;
+ Settings.Default.Save();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("保存配置失败: " + ex.Message);
+ this.DialogResult = DialogResult.None;
+ }
+ }
+ }
+}
diff --git a/WordAddIn/WordAddIn/StyleEditorControl.cs b/WordAddIn/WordAddIn/StyleEditorControl.cs
new file mode 100644
index 0000000..220573f
--- /dev/null
+++ b/WordAddIn/WordAddIn/StyleEditorControl.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+using WordAddIn.Models;
+
+namespace WordAddIn
+{
+ ///
+ /// 样式编辑器控件。
+ /// 功能描述:
+ /// 1. 提供可复用的用户界面,用于配置文本样式(字体、字号、加粗、对齐方式)。
+ /// 2. 封装了 WinForms 控件(ComboBox, NumericUpDown, CheckBox),简化样式设置。
+ /// 3. 支持数据绑定:SetStyle() 设置界面状态,GetStyle() 获取用户配置。
+ ///
+ public class StyleEditorControl : UserControl
+ {
+ private ComboBox cmbFont;
+ private NumericUpDown numSize;
+ private CheckBox chkBold;
+ private ComboBox cmbAlign;
+ private bool _showAlign;
+
+ ///
+ /// 默认构造函数 (设计器支持)。
+ ///
+ public StyleEditorControl()
+ {
+ // Default constructor for designer support
+ _showAlign = false;
+ Initialize("样式");
+ }
+
+ ///
+ /// 带参数的构造函数。
+ ///
+ /// 分组标题文本 (如 "一级目录")
+ /// 是否显示对齐方式选项
+ public StyleEditorControl(string labelText, bool showAlign)
+ {
+ _showAlign = showAlign;
+ Initialize(labelText);
+ }
+
+ ///
+ /// 初始化控件界面。
+ ///
+ /// 分组标题
+ private void Initialize(string labelText)
+ {
+ this.Padding = new Padding(20);
+ this.Size = new Size(380, 200); // Set a default size
+
+ // 字体选择
+ Label lblFont = new Label { Text = "字体:", Location = new Point(20, 30), AutoSize = true };
+ cmbFont = new ComboBox { Location = new Point(80, 27), Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
+ // Add common Chinese fonts
+ cmbFont.Items.AddRange(new object[] { "宋体", "黑体", "微软雅黑", "楷体", "仿宋", "Arial", "Times New Roman" });
+ cmbFont.SelectedIndex = 0;
+
+ // 字号选择
+ Label lblSize = new Label { Text = "字号:", Location = new Point(20, 70), AutoSize = true };
+ numSize = new NumericUpDown { Location = new Point(80, 67), Width = 80, Minimum = 5, Maximum = 72, DecimalPlaces = 1 };
+
+ // 加粗选项
+ chkBold = new CheckBox { Text = "加粗", Location = new Point(80, 110), AutoSize = true };
+
+ this.Controls.Add(lblFont);
+ this.Controls.Add(cmbFont);
+ this.Controls.Add(lblSize);
+ this.Controls.Add(numSize);
+ this.Controls.Add(chkBold);
+
+ // 对齐方式 (可选)
+ if (_showAlign)
+ {
+ Label lblAlign = new Label { Text = "对齐:", Location = new Point(20, 150), AutoSize = true };
+ cmbAlign = new ComboBox { Location = new Point(80, 147), Width = 100, DropDownStyle = ComboBoxStyle.DropDownList };
+ cmbAlign.Items.AddRange(new object[] { "左对齐", "居中", "右对齐" });
+ cmbAlign.SelectedIndex = 0;
+
+ this.Controls.Add(lblAlign);
+ this.Controls.Add(cmbAlign);
+ }
+ }
+
+ ///
+ /// 将 TextStyle 对象应用到界面控件。
+ ///
+ /// 要显示的样式对象
+ public void SetStyle(TextStyle style)
+ {
+ if (style == null) return;
+
+ if (cmbFont.Items.Contains(style.FontName))
+ cmbFont.SelectedItem = style.FontName;
+ else
+ cmbFont.Text = style.FontName; // Fallback
+
+ numSize.Value = (decimal)style.FontSize;
+ chkBold.Checked = style.IsBold;
+
+ if (_showAlign && cmbAlign != null)
+ {
+ if (style.Alignment >= 0 && style.Alignment < cmbAlign.Items.Count)
+ cmbAlign.SelectedIndex = style.Alignment;
+ }
+ }
+
+ ///
+ /// 从界面控件获取当前的 TextStyle 配置。
+ ///
+ /// 包含用户配置的样式对象
+ public TextStyle GetStyle()
+ {
+ TextStyle style = new TextStyle();
+ style.FontName = cmbFont.Text;
+ style.FontSize = (float)numSize.Value;
+ style.IsBold = chkBold.Checked;
+ if (_showAlign && cmbAlign != null)
+ {
+ style.Alignment = cmbAlign.SelectedIndex;
+ }
+ return style;
+ }
+ }
+}
diff --git a/WordAddIn/WordAddIn/WordAddIn.csproj.user b/WordAddIn/WordAddIn/WordAddIn.csproj.user
new file mode 100644
index 0000000..65b9c48
--- /dev/null
+++ b/WordAddIn/WordAddIn/WordAddIn.csproj.user
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WordAddIn/WordAddIn/obj/Debug/DesignTimeResolveAssemblyReferences.cache b/WordAddIn/WordAddIn/obj/Debug/DesignTimeResolveAssemblyReferences.cache
new file mode 100644
index 0000000..fb25e66
Binary files /dev/null and b/WordAddIn/WordAddIn/obj/Debug/DesignTimeResolveAssemblyReferences.cache differ
diff --git a/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/Microsoft.Office.Tools.Common.v4.0.Utilities.dll.deploy b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/Microsoft.Office.Tools.Common.v4.0.Utilities.dll.deploy
new file mode 100644
index 0000000..03a6ef8
Binary files /dev/null and b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/Microsoft.Office.Tools.Common.v4.0.Utilities.dll.deploy differ
diff --git a/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/Newtonsoft.Json.dll.deploy b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/Newtonsoft.Json.dll.deploy
new file mode 100644
index 0000000..3f6541a
Binary files /dev/null and b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/Newtonsoft.Json.dll.deploy differ
diff --git a/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.dll.deploy b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.dll.deploy
new file mode 100644
index 0000000..2ade180
Binary files /dev/null and b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.dll.deploy differ
diff --git a/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.dll.manifest b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.dll.manifest
new file mode 100644
index 0000000..549870c
--- /dev/null
+++ b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.dll.manifest
@@ -0,0 +1,203 @@
+
+
+
+ WordAddIn
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ jLCTF8Mm6bD4PDN+rnzN6q0+ReXaNgPh68kMWgatFwI=
+
+
+
+
+
+
+
+
+
+
+
+ 8MB68OhNTdTaS9eCO6RTW8BIGzv2I+1Atlm2gUemu3U=
+
+
+
+
+
+
+
+
+
+
+
+ y4xivocoIp8Py1pQadRda9HJK1lJPDi2e/MIWHzz8V4=
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ WordAddIn
+ WordAddIn
+
+
+
+
+
+
+nzFmuqGMTufidijFdHzd5EGSf5WOKf7c2AjATAY/QXQ=kec11gHTkSnlIv0W9zVym1S7bKdmUlth8lply4yAThpZy4vYVECTxPs+WnqUb2jfF2GZ0S5HnDirRBDuabJWZ6hzWxH0qZhiwVS4gqQz64IHo6xn+WZ7Jewbq6HRgUNl+JYFoe4MfmT9BsyOaSjjqcSttZHJocW2TMZvSC2CDuY=23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaE=AQABCN=DESKTOP-Q6H7B6L\Administratorlkht93KCRD6xvp4ITwaBZBfKJqkHiRlwzLkUqlOxs2o=B+FiLgSCXupEf1Nhejue8+299l1qYdgURdD7C2LLB2VbzFLLk9sR8wmGoQhBa3fIFUBAtCJxIMlYOlebZChMrXiiSiol05gBWxrMYk3wprnNUMUOQrJQ+SvHyOWYXiuFgWJMLW3z/VzeNZmelF2HMuyUPOqz3Sd3rnphDvmiu9k=23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaE=AQABMIICDTCCAXagAwIBAgIQJpSlsxfGbpRNgyjD5Q/yDjANBgkqhkiG9w0BAQsFADBFMUMwQQYDVQQDHjoARABFAFMASwBUAE8AUAAtAFEANgBIADcAQgA2AEwAXABBAGQAbQBpAG4AaQBzAHQAcgBhAHQAbwByMB4XDTI2MDEyNDAwMzYxNVoXDTI3MDEyNDA2MzYxNVowRTFDMEEGA1UEAx46AEQARQBTAEsAVABPAFAALQBRADYASAA3AEIANgBMAFwAQQBkAG0AaQBuAGkAcwB0AHIAYQB0AG8AcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaECAwEAATANBgkqhkiG9w0BAQsFAAOBgQC3GjWMhEFFrn9PGYrQVGNAaibG1sR/eRX3ZWNRjYLhiqbsdeHvm4CX/W0EWd9yZlbaej87XyeUbX+mC3q3yMFVPCRn/W65WYyBPbWTWmSfU9VLKKULAH6WqjS7CqEU3cQSuKcU6nYG4AJu2/FkQJaA82tr3pvcQD28HmOlDeNh0Q==
\ No newline at end of file
diff --git a/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.vsto b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.vsto
new file mode 100644
index 0000000..ad65739
--- /dev/null
+++ b/WordAddIn/WordAddIn/publish/Application Files/WordAddIn_1_0_0_0/WordAddIn.vsto
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ l0YIXWniJAOVvXIuq2gcsxgh6HiLzzq3Qbzl/vvdGcU=
+
+
+
+XIAhjSP9NMvto3hNCon8XWja0spk/WAz/YRgsaC9NAo=ZEub2vJdHkXv+rnwN2Rfq6vBnN+mUYuOr4/X76AcTre2N9HNv36HIeuufOaCwSIm9I37R09oAa0YKgWsr0K/CCgSF7wqaVqBe6fJcvepI/Gtaolrmta1FwKCh1C53YDt7PNakBpydLsreZW0P0aBY68U2MJAfTNWUs2NmeqXVG4=23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaE=AQABCN=DESKTOP-Q6H7B6L\AdministratorcV57IfAuq85wNkKjrzi3jhRZNc7Cf6ZvKM70ci4T5wQ=AzHMNpKdtaXkn+WGDS5auR1B95LwvsxcHO+9+9oxPZqVD9RSQ+E/LmN9xteIZF2ugOZn2LwMDUW7Kc2fOkvwoKycCxJE9avHhK284WaQR+94tOjrlBpm7Tiw8GsWz2Ts/sqhI4n5v07hPY9nGJVcJgGLJEBKrMMdGTFJwbOE7Ps=23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaE=AQABMIICDTCCAXagAwIBAgIQJpSlsxfGbpRNgyjD5Q/yDjANBgkqhkiG9w0BAQsFADBFMUMwQQYDVQQDHjoARABFAFMASwBUAE8AUAAtAFEANgBIADcAQgA2AEwAXABBAGQAbQBpAG4AaQBzAHQAcgBhAHQAbwByMB4XDTI2MDEyNDAwMzYxNVoXDTI3MDEyNDA2MzYxNVowRTFDMEEGA1UEAx46AEQARQBTAEsAVABPAFAALQBRADYASAA3AEIANgBMAFwAQQBkAG0AaQBuAGkAcwB0AHIAYQB0AG8AcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaECAwEAATANBgkqhkiG9w0BAQsFAAOBgQC3GjWMhEFFrn9PGYrQVGNAaibG1sR/eRX3ZWNRjYLhiqbsdeHvm4CX/W0EWd9yZlbaej87XyeUbX+mC3q3yMFVPCRn/W65WYyBPbWTWmSfU9VLKKULAH6WqjS7CqEU3cQSuKcU6nYG4AJu2/FkQJaA82tr3pvcQD28HmOlDeNh0Q==
\ No newline at end of file
diff --git a/WordAddIn/WordAddIn/publish/WordAddIn.vsto b/WordAddIn/WordAddIn/publish/WordAddIn.vsto
new file mode 100644
index 0000000..ad65739
--- /dev/null
+++ b/WordAddIn/WordAddIn/publish/WordAddIn.vsto
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ l0YIXWniJAOVvXIuq2gcsxgh6HiLzzq3Qbzl/vvdGcU=
+
+
+
+XIAhjSP9NMvto3hNCon8XWja0spk/WAz/YRgsaC9NAo=ZEub2vJdHkXv+rnwN2Rfq6vBnN+mUYuOr4/X76AcTre2N9HNv36HIeuufOaCwSIm9I37R09oAa0YKgWsr0K/CCgSF7wqaVqBe6fJcvepI/Gtaolrmta1FwKCh1C53YDt7PNakBpydLsreZW0P0aBY68U2MJAfTNWUs2NmeqXVG4=23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaE=AQABCN=DESKTOP-Q6H7B6L\AdministratorcV57IfAuq85wNkKjrzi3jhRZNc7Cf6ZvKM70ci4T5wQ=AzHMNpKdtaXkn+WGDS5auR1B95LwvsxcHO+9+9oxPZqVD9RSQ+E/LmN9xteIZF2ugOZn2LwMDUW7Kc2fOkvwoKycCxJE9avHhK284WaQR+94tOjrlBpm7Tiw8GsWz2Ts/sqhI4n5v07hPY9nGJVcJgGLJEBKrMMdGTFJwbOE7Ps=23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaE=AQABMIICDTCCAXagAwIBAgIQJpSlsxfGbpRNgyjD5Q/yDjANBgkqhkiG9w0BAQsFADBFMUMwQQYDVQQDHjoARABFAFMASwBUAE8AUAAtAFEANgBIADcAQgA2AEwAXABBAGQAbQBpAG4AaQBzAHQAcgBhAHQAbwByMB4XDTI2MDEyNDAwMzYxNVoXDTI3MDEyNDA2MzYxNVowRTFDMEEGA1UEAx46AEQARQBTAEsAVABPAFAALQBRADYASAA3AEIANgBMAFwAQQBkAG0AaQBuAGkAcwB0AHIAYQB0AG8AcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA23ghUI75yy8GcLhPT5yA+v2ssRfSW+chNqMI+pq5OAr7RXQJOt/+bHdoT9NoS9GQkvblc7ekoUNShIcN601ZZOPgAxkXaQS+La14Wp8GOwnM1qHNJwgla4VAIYjmCj3CUMXLsxspOFVZKRnB6/idYku4Gzhc7NjRBAwe8qWBXaECAwEAATANBgkqhkiG9w0BAQsFAAOBgQC3GjWMhEFFrn9PGYrQVGNAaibG1sR/eRX3ZWNRjYLhiqbsdeHvm4CX/W0EWd9yZlbaej87XyeUbX+mC3q3yMFVPCRn/W65WYyBPbWTWmSfU9VLKKULAH6WqjS7CqEU3cQSuKcU6nYG4AJu2/FkQJaA82tr3pvcQD28HmOlDeNh0Q==
\ No newline at end of file
diff --git a/WordAddIn/WordAddIn/publish/setup.exe b/WordAddIn/WordAddIn/publish/setup.exe
new file mode 100644
index 0000000..46f3761
Binary files /dev/null and b/WordAddIn/WordAddIn/publish/setup.exe differ