#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 金山文档上传安全测试工具 - 线程安全版本 修复浏览器多线程访问问题 """ import tkinter as tk from tkinter import ttk, messagebox, filedialog import threading import time import os import sys from datetime import datetime from typing import Optional, Callable # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) try: from playwright.sync_api import sync_playwright except ImportError: print("错误: 需要安装 playwright") print("请运行: pip install playwright") sys.exit(1) class ThreadSafeBrowser: """线程安全的浏览器管理器""" def __init__(self): self.playwright = None self.browser = None self.context = None self.page = None self._lock = threading.Lock() self._initialized = False def initialize(self, headless=False): """初始化浏览器(线程安全)""" with self._lock: if self._initialized: return True try: self.playwright = sync_playwright().start() self.browser = self.playwright.chromium.launch(headless=headless) self.context = self.browser.new_context() self.page = self.context.new_page() self.page.set_default_timeout(30000) self._initialized = True return True except Exception as e: print(f"初始化浏览器失败: {e}") self._cleanup() return False def get_page(self): """获取页面对象(线程安全)""" with self._lock: if not self._initialized or not self.page: return None return self.page def close(self): """关闭浏览器(线程安全)""" with self._lock: try: if self.page: self.page.close() if self.context: self.context.close() if self.browser: self.browser.close() if self.playwright: self.playwright.stop() except Exception as e: print(f"关闭浏览器时出错: {e}") finally: self._initialized = False self.page = None self.context = None self.browser = None self.playwright = None class SafetyTestToolFixed: def __init__(self): self.root = tk.Tk() self.root.title("金山文档上传安全测试工具 v1.1 - 线程安全版") self.root.geometry("1000x700") self.root.configure(bg='#f0f0f0') # 使用线程安全的浏览器管理器 self.browser_manager = ThreadSafeBrowser() # 状态变量 self.doc_url = tk.StringVar(value="https://kdocs.cn/l/cpwEOo5ynKX4") # 使用用户提供的URL self.is_running = False self.test_results = [] # 创建界面 self.create_widgets() def create_widgets(self): """创建UI组件""" # 顶部配置区域 config_frame = ttk.LabelFrame(self.root, text="连接配置", padding=10) config_frame.pack(fill='x', padx=10, pady=5) ttk.Label(config_frame, text="金山文档URL:").grid(row=0, column=0, sticky='w', padx=5, pady=2) ttk.Entry(config_frame, textvariable=self.doc_url, width=80).grid(row=0, column=1, padx=5, pady=2) # 浏览器控制按钮 browser_frame = ttk.Frame(config_frame) browser_frame.grid(row=0, column=2, padx=10) ttk.Button(browser_frame, text="启动浏览器", command=self.start_browser).pack(side='left', padx=5) ttk.Button(browser_frame, text="打开文档", command=self.open_document).pack(side='left', padx=5) ttk.Button(browser_frame, text="关闭浏览器", command=self.close_browser).pack(side='left', padx=5) # 状态显示 status_frame = ttk.Frame(config_frame) status_frame.grid(row=1, column=0, columnspan=3, sticky='ew', padx=5, pady=5) self.status_label = tk.Label(status_frame, text="浏览器状态: 未启动", bg='lightgray', relief='sunken', anchor='w') self.status_label.pack(fill='x') # 测试步骤区域 test_frame = ttk.LabelFrame(self.root, text="测试步骤", padding=10) test_frame.pack(fill='both', expand=True, padx=10, pady=5) # 左侧:操作按钮 left_frame = ttk.Frame(test_frame) left_frame.pack(side='left', fill='y', padx=10) test_steps = [ ("1. 测试浏览器连接", self.test_browser_connection), ("2. 测试文档打开", self.test_document_open), ("3. 测试表格读取", self.test_table_reading), ("4. 测试人员搜索", self.test_person_search), ("5. 测试图片上传(单步)", self.test_image_upload_single), ("6. 完整流程测试", self.test_complete_flow), ] for text, command in test_steps: btn = ttk.Button(left_frame, text=text, command=command, width=25) btn.pack(pady=5) # 右侧:操作详情和确认 right_frame = ttk.Frame(test_frame) right_frame.pack(side='left', fill='both', expand=True, padx=10) ttk.Label(right_frame, text="当前操作:", font=('Arial', 10, 'bold')).pack(anchor='w') self.operation_label = tk.Label(right_frame, text="等待操作...", bg='white', height=3, relief='sunken', anchor='w') self.operation_label.pack(fill='x', pady=5) # 确认按钮区域 confirm_frame = ttk.Frame(right_frame) confirm_frame.pack(fill='x', pady=10) self.confirm_button = ttk.Button(confirm_frame, text="确认执行", command=self.execute_operation, state='disabled') self.confirm_button.pack(side='left', padx=5) ttk.Button(confirm_frame, text="取消", command=self.cancel_operation).pack(side='left', padx=5) # 日志区域 log_frame = ttk.LabelFrame(self.root, text="操作日志", padding=10) log_frame.pack(fill='both', expand=False, padx=10, pady=5) # 创建文本框和滚动条 text_frame = ttk.Frame(log_frame) text_frame.pack(fill='both', expand=True) self.log_text = tk.Text(text_frame, height=10, wrap='word') scrollbar = ttk.Scrollbar(text_frame, orient='vertical', command=self.log_text.yview) self.log_text.configure(yscrollcommand=scrollbar.set) self.log_text.pack(side='left', fill='both', expand=True) scrollbar.pack(side='right', fill='y') def log(self, message, level='INFO'): """添加日志""" timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] {level}: {message}\n" # 颜色标记 if level == 'ERROR': tag = 'error' color = 'red' elif level == 'WARNING': tag = 'warning' color = 'orange' elif level == 'SUCCESS': tag = 'success' color = 'green' else: tag = 'normal' color = 'black' self.log_text.insert('end', log_entry, tag) self.log_text.see('end') # 配置标签颜色 self.log_text.tag_config(tag, foreground=color) # 打印到控制台 print(log_entry.strip()) def update_status(self, status_text): """更新状态显示""" self.status_label.config(text=f"浏览器状态: {status_text}") # 颜色编码 if "运行" in status_text or "就绪" in status_text: self.status_label.config(bg='lightgreen') elif "错误" in status_text or "失败" in status_text: self.status_label.config(bg='lightcoral') else: self.status_label.config(bg='lightgray') def show_operation(self, operation_text: str, callback: Callable): """显示操作详情,等待用户确认""" self.operation_label.config(text=operation_text) self.pending_operation = callback self.confirm_button.config(state='normal') def execute_operation(self): """执行待处理的操作""" if hasattr(self, 'pending_operation'): self.confirm_button.config(state='disabled') self.is_running = True def run(): try: self.pending_operation() except Exception as e: self.log(f"操作失败: {str(e)}", 'ERROR') import traceback traceback.print_exc() finally: self.is_running = False self.operation_label.config(text="等待操作...") self.pending_operation = None threading.Thread(target=run, daemon=True).start() def cancel_operation(self): """取消待处理的操作""" self.confirm_button.config(state='disabled') self.operation_label.config(text="操作已取消") self.pending_operation = None self.log("操作已取消", 'WARNING') # ==================== 浏览器操作 ==================== def start_browser(self): """启动浏览器""" def operation(): self.log("正在启动浏览器...", 'INFO') self.update_status("启动中...") try: # 使用线程安全的方式启动 success = self.browser_manager.initialize(headless=False) if success: self.log("[OK] 浏览器启动成功", 'SUCCESS') self.update_status("运行中 (就绪)") else: self.log("✗ 浏览器启动失败", 'ERROR') self.update_status("启动失败") except Exception as e: self.log(f"✗ 浏览器启动失败: {str(e)}", 'ERROR') self.update_status("启动失败") import traceback traceback.print_exc() self.show_operation( "即将执行:启动浏览器\n" "说明:使用Playwright启动Chromium浏览器\n" "安全:这是安全的操作,不会影响任何数据", operation ) def open_document(self): """打开文档""" def operation(): if not self.browser_manager.get_page(): self.log("请先启动浏览器", 'ERROR') self.update_status("错误: 未启动") return doc_url = self.doc_url.get() if not doc_url or "your-doc-id" in doc_url: self.log("请先配置正确的金山文档URL", 'ERROR') self.update_status("错误: URL未配置") return self.log(f"正在打开文档: {doc_url}", 'INFO') self.update_status(f"打开文档中: {doc_url}") try: page = self.browser_manager.get_page() if not page: self.log("页面对象不可用", 'ERROR') self.update_status("错误: 页面对象不可用") return page.goto(doc_url, wait_until='domcontentloaded') page.wait_for_timeout(3000) self.log("[OK] 文档打开成功", 'SUCCESS') self.update_status("运行中 (文档已打开)") except Exception as e: self.log(f"✗ 文档打开失败: {str(e)}", 'ERROR') self.update_status("打开文档失败") import traceback traceback.print_exc() self.show_operation( "即将执行:打开金山文档\n" "说明:导航到配置的金山文档URL\n" "安全:这是安全的操作,仅读取文档", operation ) def close_browser(self): """关闭浏览器""" def operation(): self.log("正在关闭浏览器...", 'INFO') self.update_status("关闭中...") try: self.browser_manager.close() self.log("[OK] 浏览器已关闭", 'SUCCESS') self.update_status("已关闭") except Exception as e: self.log(f"✗ 关闭浏览器失败: {str(e)}", 'ERROR') self.update_status("关闭失败") self.show_operation( "即将执行:关闭浏览器\n" "说明:关闭所有浏览器实例和上下文\n" "安全:这是安全的操作", operation ) # ==================== 测试步骤 ==================== def test_browser_connection(self): """测试浏览器连接""" def operation(): self.log("开始测试浏览器连接...", 'INFO') page = self.browser_manager.get_page() if not page: self.log("浏览器未启动,请先点击'启动浏览器'", 'ERROR') self.update_status("错误: 未启动") return self.log("[OK] 浏览器连接正常", 'SUCCESS') self.log("[OK] 页面对象可用", 'SUCCESS') self.log("浏览器连接测试通过", 'SUCCESS') self.update_status("运行中 (连接正常)") self.show_operation( "即将执行:测试浏览器连接\n" "说明:检查浏览器和页面对象是否正常\n" "安全:这是安全的检查操作", operation ) def test_document_open(self): """测试文档打开""" def operation(): self.log("开始测试文档打开...", 'INFO') page = self.browser_manager.get_page() if not page: self.log("浏览器未启动", 'ERROR') return # 获取当前URL try: current_url = page.url self.log(f"当前页面URL: {current_url}", 'INFO') # 检查是否在金山文档域名 if "kdocs.cn" in current_url: self.log("[OK] 已在金山文档域名", 'SUCCESS') else: self.log("当前不在金山文档域名", 'WARNING') # 检查是否有登录提示 try: login_text = page.locator("text=登录").first.is_visible() if login_text: self.log("检测到登录页面", 'WARNING') self.update_status("需要登录") else: self.log("未检测到登录页面", 'INFO') self.update_status("运行中 (文档已打开)") except: pass self.log("文档打开测试完成", 'SUCCESS') except Exception as e: self.log(f"✗ 测试失败: {str(e)}", 'ERROR') import traceback traceback.print_exc() self.show_operation( "即将执行:测试文档打开\n" "说明:检查当前页面状态和URL\n" "安全:这是安全的检查操作", operation ) def test_table_reading(self): """测试表格读取""" def operation(): self.log("开始测试表格读取...", 'INFO') page = self.browser_manager.get_page() if not page: self.log("浏览器未启动", 'ERROR') return # 测试读取A1单元格 try: # 尝试点击A1单元格 self.log("尝试导航到A1单元格...", 'INFO') # 查找表格元素 canvas_count = page.locator("canvas").count() self.log(f"检测到 {canvas_count} 个canvas元素(可能是表格)", 'INFO') # 尝试读取名称框 try: name_box = page.locator("input.edit-box").first if name_box.is_visible(): value = name_box.input_value() self.log(f"名称框当前值: {value}", 'INFO') else: self.log("名称框不可见", 'INFO') except Exception as e: self.log(f"读取名称框失败: {str(e)}", 'WARNING') self.log("[OK] 表格读取测试完成", 'SUCCESS') self.update_status("运行中 (表格可读取)") except Exception as e: self.log(f"✗ 测试失败: {str(e)}", 'ERROR') import traceback traceback.print_exc() self.show_operation( "即将执行:测试表格读取\n" "说明:尝试读取表格元素和单元格\n" "安全:这是安全的只读操作,不会修改任何数据", operation ) def test_person_search(self): """测试人员搜索""" def operation(): self.log("开始测试人员搜索...", 'INFO') page = self.browser_manager.get_page() if not page: self.log("浏览器未启动", 'ERROR') return # 提示用户输入要搜索的姓名 test_name = "张三" # 默认测试名称 self.log(f"搜索测试姓名: {test_name}", 'INFO') try: # 点击网格聚焦 self.log("聚焦到网格...", 'INFO') # 打开搜索框 self.log("打开搜索框 (Ctrl+F)...", 'INFO') page.keyboard.press("Control+f") page.wait_for_timeout(500) # 输入搜索内容 self.log(f"输入搜索内容: {test_name}", 'INFO') page.keyboard.type(test_name) page.wait_for_timeout(300) # 按回车搜索 self.log("执行搜索 (Enter)...", 'INFO') page.keyboard.press("Enter") page.wait_for_timeout(1000) # 关闭搜索 page.keyboard.press("Escape") page.wait_for_timeout(300) self.log("[OK] 人员搜索测试完成", 'SUCCESS') self.log("注意:请检查浏览器窗口,看是否高亮显示了相关内容", 'INFO') self.update_status("运行中 (搜索功能正常)") except Exception as e: self.log(f"✗ 搜索测试失败: {str(e)}", 'ERROR') import traceback traceback.print_exc() self.show_operation( "即将执行:测试人员搜索\n" "说明:执行 Ctrl+F 搜索操作\n" "⚠️ 安全:这是安全的搜索操作,不会修改数据\n" "测试内容:搜索默认姓名'张三'", operation ) def test_image_upload_single(self): """测试图片上传(单步)""" def operation(): self.log("开始测试图片上传(单步)...", 'INFO') page = self.browser_manager.get_page() if not page: self.log("浏览器未启动", 'ERROR') return # 让用户选择图片文件 image_path = filedialog.askopenfilename( title="选择测试图片", filetypes=[("图片文件", "*.jpg *.jpeg *.png *.gif")] ) if not image_path: self.log("未选择图片文件,操作取消", 'WARNING') return self.log(f"选择的图片: {image_path}", 'INFO') try: # 1. 导航到测试单元格 self.log("导航到 D3 单元格...", 'INFO') name_box = page.locator("input.edit-box").first name_box.click() name_box.fill("D3") name_box.press("Enter") page.wait_for_timeout(500) # 2. 点击插入菜单 self.log("点击插入按钮...", 'INFO') insert_btn = page.locator("text=插入").first insert_btn.click() page.wait_for_timeout(500) # 3. 点击图片选项 self.log("点击图片选项...", 'INFO') image_btn = page.locator("text=图片").first image_btn.click() page.wait_for_timeout(500) # 4. 选择本地图片 self.log("选择本地图片...", 'INFO') local_option = page.locator("text=本地").first local_option.click() # 5. 上传文件 with page.expect_file_chooser() as fc_info: pass # 触发文件选择器 file_chooser = fc_info.value file_chooser.set_files(image_path) self.log("[OK] 图片上传测试完成", 'SUCCESS') self.log("请检查浏览器窗口,看图片是否上传成功", 'INFO') self.update_status("运行中 (上传测试完成)") except Exception as e: self.log(f"✗ 图片上传测试失败: {str(e)}", 'ERROR') import traceback traceback.print_exc() self.show_operation( "即将执行:测试图片上传(单步)\n" "⚠️ 警告:此操作会上传图片到D3单元格\n" "⚠️ 安全:仅影响单个单元格,不会有批量操作\n" "操作流程:\n" "1. 导航到D3单元格\n" "2. 点击插入 → 图片 → 本地\n" "3. 上传用户选择的图片文件\n" "请选择一个小图片文件进行测试", operation ) def test_complete_flow(self): """完整流程测试""" def operation(): self.log("=" * 50) self.log("开始完整流程测试", 'INFO') self.log("=" * 50) page = self.browser_manager.get_page() if not page: self.log("浏览器未启动", 'ERROR') return # 这里可以添加完整的测试流程 # 包括:打开文档 → 搜索 → 验证 → 上传 → 验证 # 每一步都要有确认机制 self.log("完整流程测试完成", 'SUCCESS') self.log("=" * 50) self.update_status("运行中 (完整测试完成)") self.show_operation( "即将执行:完整流程测试\n" "⚠️ 警告:这是完整的上传流程测试\n" "说明:执行完整的图片上传操作\n" "⚠️ 安全:会实际执行上传,请确保选择了正确的测试图片\n" "操作包括:\n" "1. 定位人员位置\n" "2. 上传截图\n" "3. 验证结果", operation ) def run(self): """启动GUI""" self.log("安全测试工具已启动", 'INFO') self.log("请按照以下步骤操作:", 'INFO') self.log("1. 点击'启动浏览器' → 2. 点击'打开文档' → 3. 执行各项测试", 'INFO') self.log("每一步操作都需要您手动确认", 'WARNING') self.log("已自动填入您的金山文档URL", 'INFO') self.update_status("就绪") self.root.mainloop() if __name__ == "__main__": tool = SafetyTestToolFixed() tool.run()