From 803fe436d3cd6456ac0e67388308913ce9fbffb0 Mon Sep 17 00:00:00 2001 From: zsglpt Optimizer Date: Fri, 16 Jan 2026 17:44:04 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20=E6=B8=85=E7=90=86=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E7=9A=84=E6=96=87=E4=BB=B6=EF=BC=8C=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E4=BB=93=E5=BA=93=E6=95=B4=E6=B4=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ❌ 删除的文件: - 测试文件 (test_*.py, kdocs_*test*.py, simple_test.py) - 启动脚本 (start_*.bat) - 临时修复文件 (temp_*.py) - 图片文件 (qr_code_*.png, screenshots/*) - 运行时生成文件 ✅ 添加的内容: - .gitignore 文件,防止推送临时文件和开发文件 📋 保留的内容: - 核心应用代码 - 数据库迁移文件 - Docker配置文件 - 必要的文档 (BUG_REPORT.md, PERFORMANCE_ANALYSIS_REPORT.md, 等) 🎯 目的: - 保持仓库整洁专业 - 只包含生产环境需要的文件 - 避免推送临时开发文件 - 提高仓库维护性 --- .gitignore | 173 +++++--- kdocs_async_test.py | 631 ----------------------------- kdocs_safety_test.py | 526 ------------------------ kdocs_safety_test_fixed.py | 641 ------------------------------ kdocs_sync_test.py | 662 ------------------------------- qr_code_0.png | Bin 640 -> 0 bytes qr_code_canvas_2.png | Bin 91191 -> 0 bytes screenshots/test_simple.png | Bin 8308989 -> 0 bytes simple_test.py | 304 -------------- start_async_test.bat | 10 - start_auto_login.bat | 10 - start_fixed_auto_login.bat | 15 - start_safety_test.bat | 10 - start_safety_test_fixed.bat | 10 - start_simple_test.bat | 10 - start_sync_test.bat | 10 - start_test.bat | 10 - start_test_with_login.bat | 10 - temp_fix_screenshot.py | 201 ---------- test_auto_login.py | 536 ------------------------- test_no_ui.py | 329 --------------- test_runner.py | 329 --------------- test_screenshot_functionality.py | 183 --------- test_sequential.py | 328 --------------- test_with_login.py | 503 ----------------------- 25 files changed, 119 insertions(+), 5322 deletions(-) delete mode 100644 kdocs_async_test.py delete mode 100644 kdocs_safety_test.py delete mode 100644 kdocs_safety_test_fixed.py delete mode 100644 kdocs_sync_test.py delete mode 100644 qr_code_0.png delete mode 100644 qr_code_canvas_2.png delete mode 100644 screenshots/test_simple.png delete mode 100644 simple_test.py delete mode 100644 start_async_test.bat delete mode 100644 start_auto_login.bat delete mode 100644 start_fixed_auto_login.bat delete mode 100644 start_safety_test.bat delete mode 100644 start_safety_test_fixed.bat delete mode 100644 start_simple_test.bat delete mode 100644 start_sync_test.bat delete mode 100644 start_test.bat delete mode 100644 start_test_with_login.bat delete mode 100644 temp_fix_screenshot.py delete mode 100644 test_auto_login.py delete mode 100644 test_no_ui.py delete mode 100644 test_runner.py delete mode 100644 test_screenshot_functionality.py delete mode 100644 test_sequential.py delete mode 100644 test_with_login.py diff --git a/.gitignore b/.gitignore index 4151a1b..857de1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,75 +1,140 @@ -# 浏览器二进制文件 -playwright/ -ms-playwright/ - -# 数据库文件(敏感数据) -data/*.db -data/*.db-shm -data/*.db-wal -data/*.backup* -data/secret_key.txt -data/update/ - -# Cookies(敏感用户凭据) -data/cookies/ - -# 日志文件 -logs/ -*.log - -# 截图文件 -截图/ - -# Python缓存 +# Python __pycache__/ *.py[cod] -*.class +*$py.class *.so .Python -.pytest_cache/ -.ruff_cache/ -.mypy_cache/ -.coverage -coverage.xml +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv env/ venv/ ENV/ +env.bak/ +venv.bak/ -# 环境变量文件(包含敏感信息) -.env +# Spyder project settings +.spyderproject +.spyproject -# Docker volumes -volumes/ +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Project specific +data/*.db +data/*.db-shm +data/*.db-wal +logs/ +screenshots/ +*.png +*.jpg +*.jpeg +*.gif +*.bmp +*.ico +*.pdf +qr_code_*.png +test_*.py +start_*.bat +temp_*.py +kdocs_*test*.py +simple_test.py # IDE .vscode/ .idea/ *.swp *.swo +*~ -# 系统文件 +# OS .DS_Store Thumbs.db -# 临时文件 +# Temporary files *.tmp -*.bak -*.backup - -# 部署脚本(含服务器信息) -deploy_*.sh -verify_*.sh -deploy.sh - -# 内部文档 -docs/ - -# 前端依赖(体积大,不应入库) -node_modules/ -app-frontend/node_modules/ -admin-frontend/node_modules/ - -# Local data -data/ -docker-compose.yml.bak.* +*.temp diff --git a/kdocs_async_test.py b/kdocs_async_test.py deleted file mode 100644 index 83ea811..0000000 --- a/kdocs_async_test.py +++ /dev/null @@ -1,631 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -金山文档上传安全测试工具 - 异步版本 -使用asyncio避免线程问题 -""" - -import tkinter as tk -from tkinter import ttk, messagebox, filedialog -import asyncio -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.async_api import async_playwright -except ImportError: - print("错误: 需要安装 playwright") - print("请运行: pip install playwright") - sys.exit(1) - - -class AsyncBrowserManager: - """异步浏览器管理器""" - - def __init__(self): - self.playwright = None - self.browser = None - self.context = None - self.page = None - self._loop = None - self._running = False - - async def initialize(self, headless=False): - """初始化浏览器(异步)""" - if self.playwright: - return True - - try: - self.playwright = await async_playwright().start() - self.browser = await self.playwright.chromium.launch(headless=headless) - self.context = await self.browser.new_context() - self.page = await self.context.new_page() - await self.page.set_default_timeout(30000) - self._running = True - return True - except Exception as e: - print(f"初始化浏览器失败: {e}") - await self.cleanup() - return False - - async def goto(self, url: str): - """导航到URL""" - if not self.page: - raise Exception("浏览器未初始化") - await self.page.goto(url, wait_until='domcontentloaded') - await self.page.wait_for_timeout(3000) - - async def close(self): - """关闭浏览器""" - await self.cleanup() - - async def cleanup(self): - """清理资源""" - try: - if self.page: - await self.page.close() - except: - pass - self.page = None - - try: - if self.context: - await self.context.close() - except: - pass - self.context = None - - try: - if self.browser: - await self.browser.close() - except: - pass - self.browser = None - - try: - if self.playwright: - await self.playwright.stop() - except: - pass - self.playwright = None - self._running = False - - -class AsyncTestTool: - def __init__(self): - self.root = tk.Tk() - self.root.title("金山文档上传安全测试工具 - 异步版") - self.root.geometry("1000x700") - self.root.configure(bg='#f0f0f0') - - # 异步浏览器管理器 - self.browser_manager = AsyncBrowserManager() - - # 状态变量 - self.doc_url = tk.StringVar(value="https://kdocs.cn/l/cpwEOo5ynKX4") - self.is_running = False - self.test_results = [] - self.async_loop = None - self.thread_pool_executor = None - - # 创建界面 - 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 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, async_func: Callable): - """显示操作详情,等待用户确认""" - self.operation_label.config(text=operation_text) - self.pending_async_func = async_func - self.confirm_button.config(state='normal') - - def execute_operation(self): - """执行待处理的操作""" - if hasattr(self, 'pending_async_func'): - self.confirm_button.config(state='disabled') - self.is_running = True - - # 在新的线程中运行异步函数 - def run_async(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - loop.run_until_complete(self.pending_async_func()) - except Exception as e: - self.log(f"操作失败: {str(e)}", 'ERROR') - import traceback - traceback.print_exc() - finally: - loop.close() - self.is_running = False - self.operation_label.config(text="等待操作...") - self.pending_async_func = None - - threading.Thread(target=run_async, daemon=True).start() - - def cancel_operation(self): - """取消待处理的操作""" - self.confirm_button.config(state='disabled') - self.operation_label.config(text="操作已取消") - self.pending_async_func = None - self.log("操作已取消", 'WARNING') - - # ==================== 异步操作函数 ==================== - - async def async_start_browser(self): - """异步启动浏览器""" - self.log("正在启动浏览器...", 'INFO') - self.update_status("启动中...") - - try: - success = await 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("启动失败") - - async def async_open_document(self): - """异步打开文档""" - 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"打开文档中...") - - try: - await self.browser_manager.goto(doc_url) - self.log("[OK] 文档打开成功", 'SUCCESS') - self.update_status("运行中 (文档已打开)") - except Exception as e: - self.log(f"✗ 文档打开失败: {str(e)}", 'ERROR') - self.update_status("打开文档失败") - - async def async_close_browser(self): - """异步关闭浏览器""" - self.log("正在关闭浏览器...", 'INFO') - self.update_status("关闭中...") - - try: - await self.browser_manager.close() - self.log("[OK] 浏览器已关闭", 'SUCCESS') - self.update_status("已关闭") - except Exception as e: - self.log(f"✗ 关闭浏览器失败: {str(e)}", 'ERROR') - self.update_status("关闭失败") - - async def async_test_browser_connection(self): - """异步测试浏览器连接""" - self.log("开始测试浏览器连接...", 'INFO') - - if not self.browser_manager.page: - self.log("浏览器未启动,请先点击'启动浏览器'", 'ERROR') - self.update_status("错误: 未启动") - return - - self.log("[OK] 浏览器连接正常", 'SUCCESS') - self.log("[OK] 页面对象可用", 'SUCCESS') - self.log("浏览器连接测试通过", 'SUCCESS') - self.update_status("运行中 (连接正常)") - - async def async_test_document_open(self): - """异步测试文档打开""" - self.log("开始测试文档打开...", 'INFO') - - if not self.browser_manager.page: - self.log("浏览器未启动", 'ERROR') - return - - try: - current_url = self.browser_manager.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 = await self.browser_manager.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') - - async def async_test_table_reading(self): - """异步测试表格读取""" - self.log("开始测试表格读取...", 'INFO') - - if not self.browser_manager.page: - self.log("浏览器未启动", 'ERROR') - return - - try: - self.log("尝试导航到A1单元格...", 'INFO') - - # 查找表格元素 - canvas_count = await self.browser_manager.page.locator("canvas").count() - self.log(f"检测到 {canvas_count} 个canvas元素(可能是表格)", 'INFO') - - # 尝试读取名称框 - try: - name_box = self.browser_manager.page.locator("input.edit-box").first - is_visible = await name_box.is_visible() - if is_visible: - value = await 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') - - async def async_test_person_search(self): - """异步测试人员搜索""" - self.log("开始测试人员搜索...", 'INFO') - - if not self.browser_manager.page: - self.log("浏览器未启动", 'ERROR') - return - - test_name = "张三" # 默认测试名称 - - self.log(f"搜索测试姓名: {test_name}", 'INFO') - - try: - self.log("聚焦到网格...", 'INFO') - - # 打开搜索框 - self.log("打开搜索框 (Ctrl+F)...", 'INFO') - await self.browser_manager.page.keyboard.press("Control+f") - await self.browser_manager.page.wait_for_timeout(500) - - # 输入搜索内容 - self.log(f"输入搜索内容: {test_name}", 'INFO') - await self.browser_manager.page.keyboard.type(test_name) - await self.browser_manager.page.wait_for_timeout(300) - - # 按回车搜索 - self.log("执行搜索 (Enter)...", 'INFO') - await self.browser_manager.page.keyboard.press("Enter") - await self.browser_manager.page.wait_for_timeout(1000) - - # 关闭搜索 - await self.browser_manager.page.keyboard.press("Escape") - await self.browser_manager.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') - - async def async_test_image_upload_single(self): - """异步测试图片上传(单步)""" - self.log("开始测试图片上传(单步)...", 'INFO') - - if not self.browser_manager.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 = self.browser_manager.page.locator("input.edit-box").first - await name_box.click() - await name_box.fill("D3") - await name_box.press("Enter") - await self.browser_manager.page.wait_for_timeout(500) - - # 2. 点击插入菜单 - self.log("点击插入按钮...", 'INFO') - insert_btn = self.browser_manager.page.locator("text=插入").first - await insert_btn.click() - await self.browser_manager.page.wait_for_timeout(500) - - # 3. 点击图片选项 - self.log("点击图片选项...", 'INFO') - image_btn = self.browser_manager.page.locator("text=图片").first - await image_btn.click() - await self.browser_manager.page.wait_for_timeout(500) - - # 4. 选择本地图片 - self.log("选择本地图片...", 'INFO') - local_option = self.browser_manager.page.locator("text=本地").first - await local_option.click() - - # 5. 上传文件 - self.log("上传文件...", 'INFO') - async with self.browser_manager.page.expect_file_chooser() as fc_info: - pass - - file_chooser = fc_info.value - await 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') - - async def async_test_complete_flow(self): - """异步完整流程测试""" - self.log("=" * 50) - self.log("开始完整流程测试", 'INFO') - self.log("=" * 50) - - if not self.browser_manager.page: - self.log("浏览器未启动", 'ERROR') - return - - self.log("完整流程测试完成", 'SUCCESS') - self.log("=" * 50) - self.update_status("运行中 (完整测试完成)") - - # ==================== 包装函数 ==================== - - def start_browser(self): - """启动浏览器""" - self.show_operation( - "即将执行:启动浏览器\n" - "说明:使用Playwright启动Chromium浏览器\n" - "安全:这是安全的操作,不会影响任何数据", - self.async_start_browser - ) - - def open_document(self): - """打开文档""" - self.show_operation( - "即将执行:打开金山文档\n" - "说明:导航到配置的金山文档URL\n" - "安全:这是安全的操作,仅读取文档", - self.async_open_document - ) - - def close_browser(self): - """关闭浏览器""" - self.show_operation( - "即将执行:关闭浏览器\n" - "说明:关闭所有浏览器实例和上下文\n" - "安全:这是安全的操作", - self.async_close_browser - ) - - def test_browser_connection(self): - """测试浏览器连接""" - self.show_operation( - "即将执行:测试浏览器连接\n" - "说明:检查浏览器和页面对象是否正常\n" - "安全:这是安全的检查操作", - self.async_test_browser_connection - ) - - def test_document_open(self): - """测试文档打开""" - self.show_operation( - "即将执行:测试文档打开\n" - "说明:检查当前页面状态和URL\n" - "安全:这是安全的检查操作", - self.async_test_document_open - ) - - def test_table_reading(self): - """测试表格读取""" - self.show_operation( - "即将执行:测试表格读取\n" - "说明:尝试读取表格元素和单元格\n" - "安全:这是安全的只读操作,不会修改任何数据", - self.async_test_table_reading - ) - - def test_person_search(self): - """测试人员搜索""" - self.show_operation( - "即将执行:测试人员搜索\n" - "说明:执行 Ctrl+F 搜索操作\n" - "⚠️ 安全:这是安全的搜索操作,不会修改数据\n" - "测试内容:搜索默认姓名'张三'", - self.async_test_person_search - ) - - def test_image_upload_single(self): - """测试图片上传(单步)""" - self.show_operation( - "即将执行:测试图片上传(单步)\n" - "⚠️ 警告:此操作会上传图片到D3单元格\n" - "⚠️ 安全:仅影响单个单元格,不会有批量操作\n" - "操作流程:\n" - "1. 导航到D3单元格\n" - "2. 点击插入 → 图片 → 本地\n" - "3. 上传用户选择的图片文件\n" - "请选择一个小图片文件进行测试", - self.async_test_image_upload_single - ) - - def test_complete_flow(self): - """完整流程测试""" - self.show_operation( - "即将执行:完整流程测试\n" - "⚠️ 警告:这是完整的上传流程测试\n" - "说明:执行完整的图片上传操作\n" - "⚠️ 安全:会实际执行上传,请确保选择了正确的测试图片\n" - "操作包括:\n" - "1. 定位人员位置\n" - "2. 上传截图\n" - "3. 验证结果", - self.async_test_complete_flow - ) - - 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 = AsyncTestTool() - tool.run() diff --git a/kdocs_safety_test.py b/kdocs_safety_test.py deleted file mode 100644 index 9cd3c8f..0000000 --- a/kdocs_safety_test.py +++ /dev/null @@ -1,526 +0,0 @@ -#!/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 SafetyTestTool: - def __init__(self): - self.root = tk.Tk() - self.root.title("金山文档上传安全测试工具 v1.0") - self.root.geometry("1000x700") - self.root.configure(bg='#f0f0f0') - - # 状态变量 - self.playwright = None - self.browser = None - self.context = None - self.page = None - self.doc_url = tk.StringVar(value="https://www.kdocs.cn/spreadsheet/your-doc-id") - 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) - - # 测试步骤区域 - 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 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') - 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') - try: - self.playwright = sync_playwright().start() - self.browser = self.playwright.chromium.launch(headless=False) # 显示浏览器便于调试 - self.context = self.browser.new_context() - self.page = self.context.new_page() - self.page.set_default_timeout(30000) - self.log("[OK] 浏览器启动成功", 'SUCCESS') - except Exception as e: - self.log(f"✗ 浏览器启动失败: {str(e)}", 'ERROR') - - self.show_operation( - "即将执行:启动浏览器\n" - "说明:使用Playwright启动Chromium浏览器\n" - "安全:这是安全的操作,不会影响任何数据", - operation - ) - - def open_document(self): - """打开文档""" - def operation(): - if not self.page: - self.log("请先启动浏览器", 'ERROR') - return - - doc_url = self.doc_url.get() - if not doc_url or "your-doc-id" in doc_url: - self.log("请先配置正确的金山文档URL", 'ERROR') - return - - self.log(f"正在打开文档: {doc_url}", 'INFO') - - try: - self.page.goto(doc_url, wait_until='domcontentloaded') - self.page.wait_for_timeout(3000) - self.log("[OK] 文档打开成功", 'SUCCESS') - except Exception as e: - self.log(f"✗ 文档打开失败: {str(e)}", 'ERROR') - - self.show_operation( - "即将执行:打开金山文档\n" - "说明:导航到配置的金山文档URL\n" - "安全:这是安全的操作,仅读取文档", - operation - ) - - def close_browser(self): - """关闭浏览器""" - def operation(): - self.log("正在关闭浏览器...", 'INFO') - 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() - - self.page = None - self.context = None - self.browser = None - self.playwright = None - self.log("[OK] 浏览器已关闭", 'SUCCESS') - except Exception as e: - self.log(f"✗ 关闭浏览器失败: {str(e)}", 'ERROR') - - self.show_operation( - "即将执行:关闭浏览器\n" - "说明:关闭所有浏览器实例和上下文\n" - "安全:这是安全的操作", - operation - ) - - # ==================== 测试步骤 ==================== - - def test_browser_connection(self): - """测试浏览器连接""" - def operation(): - self.log("开始测试浏览器连接...", 'INFO') - - if not self.page: - self.log("浏览器未启动,请先点击'启动浏览器'", 'ERROR') - return - - self.log("[OK] 浏览器连接正常", 'SUCCESS') - self.log("[OK] 页面对象可用", 'SUCCESS') - self.log("浏览器连接测试通过", 'SUCCESS') - - self.show_operation( - "即将执行:测试浏览器连接\n" - "说明:检查浏览器和页面对象是否正常\n" - "安全:这是安全的检查操作", - operation - ) - - def test_document_open(self): - """测试文档打开""" - def operation(): - self.log("开始测试文档打开...", 'INFO') - - if not self.page: - self.log("浏览器未启动", 'ERROR') - return - - # 获取当前URL - try: - current_url = self.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 = self.page.locator("text=登录").first.is_visible() - if login_text: - self.log("检测到登录页面", 'WARNING') - else: - self.log("未检测到登录页面", 'INFO') - except: - pass - - self.log("文档打开测试完成", 'SUCCESS') - - except Exception as e: - self.log(f"✗ 测试失败: {str(e)}", 'ERROR') - - self.show_operation( - "即将执行:测试文档打开\n" - "说明:检查当前页面状态和URL\n" - "安全:这是安全的检查操作", - operation - ) - - def test_table_reading(self): - """测试表格读取""" - def operation(): - self.log("开始测试表格读取...", 'INFO') - - if not self.page: - self.log("浏览器未启动", 'ERROR') - return - - # 测试读取A1单元格 - try: - # 尝试点击A1单元格 - self.log("尝试导航到A1单元格...", 'INFO') - - # 查找表格元素 - canvas_count = self.page.locator("canvas").count() - self.log(f"检测到 {canvas_count} 个canvas元素(可能是表格)", 'INFO') - - # 尝试读取名称框 - try: - name_box = self.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') - - except Exception as e: - self.log(f"✗ 测试失败: {str(e)}", 'ERROR') - - self.show_operation( - "即将执行:测试表格读取\n" - "说明:尝试读取表格元素和单元格\n" - "安全:这是安全的只读操作,不会修改任何数据", - operation - ) - - def test_person_search(self): - """测试人员搜索""" - def operation(): - self.log("开始测试人员搜索...", 'INFO') - - if not self.page: - self.log("浏览器未启动", 'ERROR') - return - - # 提示用户输入要搜索的姓名 - test_name = "张三" # 默认测试名称 - - self.log(f"搜索测试姓名: {test_name}", 'INFO') - - try: - # 点击网格聚焦 - self.log("聚焦到网格...", 'INFO') - - # 打开搜索框 - self.log("打开搜索框 (Ctrl+F)...", 'INFO') - self.page.keyboard.press("Control+f") - self.page.wait_for_timeout(500) - - # 输入搜索内容 - self.log(f"输入搜索内容: {test_name}", 'INFO') - self.page.keyboard.type(test_name) - self.page.wait_for_timeout(300) - - # 按回车搜索 - self.log("执行搜索 (Enter)...", 'INFO') - self.page.keyboard.press("Enter") - self.page.wait_for_timeout(1000) - - # 关闭搜索 - self.page.keyboard.press("Escape") - self.page.wait_for_timeout(300) - - self.log("[OK] 人员搜索测试完成", 'SUCCESS') - self.log("注意:请检查浏览器窗口,看是否高亮显示了相关内容", 'INFO') - - except Exception as e: - self.log(f"✗ 搜索测试失败: {str(e)}", 'ERROR') - - self.show_operation( - "即将执行:测试人员搜索\n" - "说明:执行 Ctrl+F 搜索操作\n" - "⚠️ 安全:这是安全的搜索操作,不会修改数据\n" - "测试内容:搜索默认姓名'张三'", - operation - ) - - def test_image_upload_single(self): - """测试图片上传(单步)""" - def operation(): - self.log("开始测试图片上传(单步)...", 'INFO') - - if not self.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 = self.page.locator("input.edit-box").first - name_box.click() - name_box.fill("D3") - name_box.press("Enter") - self.page.wait_for_timeout(500) - - # 2. 点击插入菜单 - self.log("点击插入按钮...", 'INFO') - insert_btn = self.page.locator("text=插入").first - insert_btn.click() - self.page.wait_for_timeout(500) - - # 3. 点击图片选项 - self.log("点击图片选项...", 'INFO') - image_btn = self.page.locator("text=图片").first - image_btn.click() - self.page.wait_for_timeout(500) - - # 4. 选择本地图片 - self.log("选择本地图片...", 'INFO') - local_option = self.page.locator("text=本地").first - local_option.click() - - # 5. 上传文件 - with self.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') - - except Exception as e: - self.log(f"✗ 图片上传测试失败: {str(e)}", 'ERROR') - - 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) - - if not self.page: - self.log("浏览器未启动", 'ERROR') - return - - # 这里可以添加完整的测试流程 - # 包括:打开文档 → 搜索 → 验证 → 上传 → 验证 - # 每一步都要有确认机制 - - self.log("完整流程测试完成", 'SUCCESS') - self.log("=" * 50) - - 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.root.mainloop() - - -if __name__ == "__main__": - tool = SafetyTestTool() - tool.run() diff --git a/kdocs_safety_test_fixed.py b/kdocs_safety_test_fixed.py deleted file mode 100644 index 8f4f6e1..0000000 --- a/kdocs_safety_test_fixed.py +++ /dev/null @@ -1,641 +0,0 @@ -#!/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() diff --git a/kdocs_sync_test.py b/kdocs_sync_test.py deleted file mode 100644 index 3e9bbcd..0000000 --- a/kdocs_sync_test.py +++ /dev/null @@ -1,662 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -金山文档上传安全测试工具 - 同步线程版本 -使用thread-local确保浏览器实例在正确线程中使用 -""" - -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 -import uuid - -# 添加项目路径 -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 ThreadLocalBrowser: - """线程本地浏览器管理器 - 确保每个线程使用自己的浏览器实例""" - - _local = threading.local() - - @classmethod - def get_instance(cls, thread_id=None): - """获取当前线程的浏览器实例""" - if thread_id is None: - thread_id = threading.get_ident() - - if not hasattr(cls._local, 'browsers'): - cls._local.browsers = {} - - if thread_id not in cls._local.browsers: - cls._local.browsers[thread_id] = cls._create_browser() - - return cls._local.browsers[thread_id] - - @classmethod - def _create_browser(cls): - """创建新的浏览器实例""" - try: - playwright = sync_playwright().start() - browser = playwright.chromium.launch(headless=False) - context = browser.new_context() - page = context.new_page() - page.set_default_timeout(30000) - return { - 'playwright': playwright, - 'browser': browser, - 'context': context, - 'page': page, - 'initialized': True - } - except Exception as e: - print(f"创建浏览器实例失败: {e}") - return { - 'playwright': None, - 'browser': None, - 'context': None, - 'page': None, - 'initialized': False, - 'error': str(e) - } - - @classmethod - def close_instance(cls, thread_id=None): - """关闭指定线程的浏览器实例""" - if thread_id is None: - thread_id = threading.get_ident() - - if hasattr(cls._local, 'browsers') and thread_id in cls._local.browsers: - instance = cls._local.browsers[thread_id] - try: - if instance['page']: - instance['page'].close() - except: - pass - try: - if instance['context']: - instance['context'].close() - except: - pass - try: - if instance['browser']: - instance['browser'].close() - except: - pass - try: - if instance['playwright']: - instance['playwright'].stop() - except: - pass - del cls._local.browsers[thread_id] - - @classmethod - def close_all(cls): - """关闭所有线程的浏览器实例""" - if hasattr(cls._local, 'browsers'): - thread_ids = list(cls._local.browsers.keys()) - for thread_id in thread_ids: - cls.close_instance(thread_id) - - -class SyncTestTool: - def __init__(self): - self.root = tk.Tk() - self.root.title("金山文档上传安全测试工具 - 同步线程版") - self.root.geometry("1000x700") - self.root.configure(bg='#f0f0f0') - - # 状态变量 - self.doc_url = tk.StringVar(value="https://kdocs.cn/l/cpwEOo5ynKX4") - 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 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_callback = callback - self.confirm_button.config(state='normal') - - def execute_operation(self): - """执行待处理的操作""" - if hasattr(self, 'pending_callback'): - self.confirm_button.config(state='disabled') - self.is_running = True - - def run(): - try: - self.pending_callback() - 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_callback = 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_callback = None - self.log("操作已取消", 'WARNING') - - def get_browser_instance(self): - """获取当前线程的浏览器实例""" - return ThreadLocalBrowser.get_instance() - - def start_browser(self): - """启动浏览器""" - def operation(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中启动浏览器...", 'INFO') - self.update_status("启动中...") - - instance = self.get_browser_instance() - - if instance['initialized']: - self.log("[OK] 浏览器启动成功", 'SUCCESS') - self.update_status("运行中 (就绪)") - else: - self.log(f"✗ 浏览器启动失败: {instance.get('error', 'Unknown error')}", 'ERROR') - self.update_status("启动失败") - - self.show_operation( - "即将执行:启动浏览器\n" - "说明:使用Playwright启动Chromium浏览器\n" - "安全:这是安全的操作,不会影响任何数据", - operation - ) - - def open_document(self): - """打开文档""" - def operation(): - 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 - - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中打开文档...", 'INFO') - self.log(f"正在打开文档: {doc_url}", 'INFO') - self.update_status("打开文档中...") - - instance = self.get_browser_instance() - if not instance['initialized'] or not instance['page']: - self.log("浏览器未初始化或页面不可用", 'ERROR') - self.update_status("错误: 浏览器未就绪") - return - - try: - page = instance['page'] - 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(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中关闭浏览器...", 'INFO') - self.update_status("关闭中...") - - try: - ThreadLocalBrowser.close_instance(thread_id) - 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(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中测试浏览器连接...", 'INFO') - - instance = self.get_browser_instance() - if not instance['initialized']: - 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(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中测试文档打开...", 'INFO') - - instance = self.get_browser_instance() - if not instance['initialized'] or not instance['page']: - self.log("浏览器未启动", 'ERROR') - return - - try: - page = instance['page'] - 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') - - self.show_operation( - "即将执行:测试文档打开\n" - "说明:检查当前页面状态和URL\n" - "安全:这是安全的检查操作", - operation - ) - - def test_table_reading(self): - """测试表格读取""" - def operation(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中测试表格读取...", 'INFO') - - instance = self.get_browser_instance() - if not instance['initialized'] or not instance['page']: - self.log("浏览器未启动", 'ERROR') - return - - try: - page = instance['page'] - 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') - - self.show_operation( - "即将执行:测试表格读取\n" - "说明:尝试读取表格元素和单元格\n" - "安全:这是安全的只读操作,不会修改任何数据", - operation - ) - - def test_person_search(self): - """测试人员搜索""" - def operation(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中测试人员搜索...", 'INFO') - - instance = self.get_browser_instance() - if not instance['initialized'] or not instance['page']: - self.log("浏览器未启动", 'ERROR') - return - - test_name = "张三" # 默认测试名称 - - self.log(f"搜索测试姓名: {test_name}", 'INFO') - - try: - page = instance['page'] - 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') - - self.show_operation( - "即将执行:测试人员搜索\n" - "说明:执行 Ctrl+F 搜索操作\n" - "⚠️ 安全:这是安全的搜索操作,不会修改数据\n" - "测试内容:搜索默认姓名'张三'", - operation - ) - - def test_image_upload_single(self): - """测试图片上传(单步)""" - def operation(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中测试图片上传(单步)...", 'INFO') - - instance = self.get_browser_instance() - if not instance['initialized'] or not instance['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: - page = instance['page'] - # 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(): - thread_id = threading.get_ident() - self.log(f"在线程 {thread_id} 中执行完整流程测试...", 'INFO') - self.log("=" * 50) - self.log("开始完整流程测试", 'INFO') - self.log("=" * 50) - - instance = self.get_browser_instance() - if not instance['initialized'] or not instance['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("就绪") - - def on_closing(): - """窗口关闭时清理资源""" - ThreadLocalBrowser.close_all() - self.root.destroy() - - self.root.protocol("WM_DELETE_WINDOW", on_closing) - self.root.mainloop() - - -if __name__ == "__main__": - tool = SyncTestTool() - tool.run() diff --git a/qr_code_0.png b/qr_code_0.png deleted file mode 100644 index dcae9feea20619f5697cbf4c447acd88756d5b81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 640 zcmeAS@N?(olHy`uVBq!ia0y~yU_K0FOK>m&$w0Op0zitfILO_JVcj{ImkbO{R-P`7 zAr*7p-c|Hhc9dXy(8%C;MQPQlh(^Zdg-pSrTbY>qT0WQ;xF$~EmGW_3bzqI7Nb@0+ zL|LvzrAWtBem&(2Q2*&4Q5l!2i^QukanGXuj1RnJQd z3;}H_ybKJf9%#&CL4Qtu{`2+g*0}Y@Q@7uajgIa<9k>4bhYtlMB_S(AoH&{fKKS(M z)6bukzhbvWy?yW?AvN`B@qeIq&KxxGRQJ5}{`+See#MyB*rT%}r}fUWwzj@~=Z?>1 zla>v;c2(8X#HlF+Gc@dsl!`T#>eV?t>+G|WCq3WHTfcsNd3m|I zP%+2Dzkm1M&GX;CI&Agd-@MEW2kIY|Pg)nEwXx{k*Q&*4zh1u7jO+2wd$EL(Az{Xn z-FM$Te*8G2Zt=y8p0s7lgE#p}F)*}wTKQh??d`4GFFzgV9DYy#B`I@F7#MVFE55%6 zF+LPd`WLJ&+wO4e`EvC+^X6G`^D{KePuS)M)Mw!lD{}PcQP<5keI_l@=<-{>dHZ&8 z{yA(=1yVe0bN$k*tF1jJRlGQU?%cVbKYu=a`0&fbN}#d2ypu};Ma<33^YimJR>#x6dzL(v0`|aPqe!?X_#yW^J{a)iO`){0Zs$ zgv!dwJ?-Z{|M~at-UOgS?Sa7yj6CDWIp5hB7#?^}0{VIZk0;QNrzU`yJ@LD4-b{ZR Tva%eQq!>J1{an^LB{Ts5h5{st diff --git a/qr_code_canvas_2.png b/qr_code_canvas_2.png deleted file mode 100644 index 7ca0921436bd66416afc87a311faa62a3386c65b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91191 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 W0C8m#& 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 mSh7Z^*ULWbWp&t7pIF?=TCl5mvy7422A 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μ1uwzae6d*6K)f9c|`_Z&>Kj6BIULv z8Y+<#w{A1Vp)drHJRV*iP`J3b%!^BE{(BZ%x*SrT{WNn3Teq;XD?qq9$-!(4Kgy&blvKIlEl|t`(NGq^!Y5UEF%6Q`Z02b@xXzf-}kAj zs2u0yw9n?i3WY*Ee0G#~0b>y$pJf!Xe|<3zcj`%Hr5F6do_PW>o+Oed`=s(H()H0! zLqz+>5W3c{U*9-}SK>LaSzF<%5aD_oqauQ&^dfPK@KjIq}T@I_J zma%c-tnybJskqHNT&7t;?K!KSt%D%r(sQ5ut#tl<%Jh-$dbxijo17(p3L9Hn zH+1N;v$Mo&mY4GMum}utu<@UL7%LQEZDob#`Rn0rbMk4ni+y`unB?_HC%p?}yC-k%AaD>56ED$0C}L=c)bb&S1|o~Xl$6(v zjWiSeu>a9gYNQJHE1_uW@ znTQS$e&Xwq&au@uG~Rp$DVI;iqWp1sq+bEk4G)_#9sq;Ft=*4bJBUFid;0WV%N>|x zVd6IU5Xx_WENOD?D8-KUw%^u_t+)E~ZQ$3hS|ZVc0D=1|ycDSyw-rX}aih+>MA%An z$kqv=TLaZx<=O6*W32NG(Kwr9-8SwspY8vd(*WM#HGcNLvl(dpB$dPz_}T;KSZy>Iw&3u9F; z;9+EB3?qMvkm#A#g7h4>vr+6zENnZ2%eyrCj4m{bw>}{kI8RQS$LwcssvsQUo zSy*2dY$%A+z*PhmzFay9o(1!lW~wtd?ch#_)!vj{9y`w9U`A$UnphIj-ms0IyY1oT zhLNxsr8rbjhR`N26pNEREnE6I?kOS~bs(<{g>DR{KDJ+B&WZmDZ4O3)|D2lgnR-}D zA&pX<6o{)OToc0e@1eXC_rsS5LNtt>7#;jGvxK{_l>#^3d@&^415!!Roh(U zEgAD(A<_1}PxrPrq-??_i3J9-9};+>JrnBHjlRp5rJmg`K?`p6SwV`gb)#gMjO101 zc<{|IzJfutMr}^dY8P8Dam)QJ**_meD?V{2;b5;_rAbhvS;rXR(vKxbf<|ISeLqXN z1Y90=7H}3(9?M~oJi0?SN?ZI8e!6!)g#j1xmMu+MmlA%~b?jix2Bb|!*uMc4t6n!v zVD#m@>4<+@B_w?EA474CW&=ZSF|@6`@ig!^t*exM`nL3gCtFC8NJ=8pAxkcEi{$wo&Ui8+;<^Nv@ zpmj?1`sx(6OjP?Q4Q2XrawECxu)9BC4FZjR?BSRh)deaQxgCZ5^{->3`V2d_PP*Tn@_w@VcS|+R87^aTUH0QipTg32 zC7h`&Ql7S}QssQzJik-_PXPCUnwQbnMBU>;FJ?<@cAAglIG&U*T-w9Z>|>O6R&>hs z)bAI8kam-a_5ObY5C@a~cK|W?25JKg*%G-9P_x;t2%TLz6CDG?BGj#bjUZfA&He7N zlB3A}vm~b8>h>Emfdh%@4^Wc>M=|8y7hhkDtqX15R99I`|KJ^&J_t7mjEtV&&)#6B zF1?9xmg{zQjy=o}E`rUN&h_)zvv`oDzwGX8{cgmSO@C&7v%G8az^9g7@$FyA@9EB3 zE;q!_pO1HU{%jE(r5%14t0%}$kDg=F^%S3I(u`}8K~by4w}t-wD-zysXjWGy;(ZAL zbHgzu#XA!!tsI0me|qDtS@Mf^I+e?DK5kHQ*REBS)W!}uljWysJj8uGe?|$;od(QeC>fFY7Xj7G&zvnEKaP<&Fi6#k{IaTv#moxZL)@8m^}o3V zqB1iDb->A)m6CFccb^mk>Im4T9T-`9oXC6kJ$TYRZPoj0?+n|Xx+hYL&cm^nqbrBP z^ryeJH`lo@hVdlN)rUO!VK}vH8_T>UgY>lFlK3z({$vsKWQ@4j5x>4C_-)6zx<7Gj zzq+TE8chmPn!c{e{Gd1aLkTp%>b>@e=1=_J#2?#t@0d2*vY2CdtvI7|%^`Tws=C{a zfUESn#=?DDIk~SHj6x7T+6XX9dV8YUYB0@35R7+_%#nij&wT6Fc^=?#ac;VE+v1`j zI(%yUk5Rdx@rO(K=<*=)M^lNo8CVBtX-Pdlb&aE=<30=$M7Pz}29>j(hK7c=wzj_h zh-ax3s~%8z)gv_yU1oga9Ys58Uc&V6yiCS$;DC&DUF$R5hsu`7uY!*g=X`)UpagUg znDOziy^fuYg)|z0vUPl_$ZtWSZ6(d#F5ZiIdPnY0)96Q^J`m2coN{wh%}d-*h+wjH zlv_&Dwgk)m^!!VJ4(#4&+X!SvNs#7$u}nAOAswaSL+e)3lo9y56nMxt}oowo=7Jr?okfi*0Qev@a>A5;;PySQ50elv! z|0kG~*RYuSDp)q@TBUg)Nqt;;rkesnxuzK2+A&dWMKQV;yuB2k*Ud(-N4mn7Z@zX^!WmD(vMA z(%X>NNJeJ$Sag?jk$_4XZ1`3FQ0eXzR0; z2An_0O>GFo$NVqI775_V^jEK3VH|q(sq-lY<6gPKu5|L~(IEi$U>{_pxkDUtE?mYO zhz`Z50nmLQ?yfcpOP}4sjASZQ@jxMml04Kgu!V*u-H$R`XEc8ZisS3oc>~OmvDeS7 zu|P?5_+y;{-SV+xO;BFS(%ar=Z|~@E$B6qvad>Z{^o!SW8GoI96C=fT%=Xvu`}^N(H8khXQWy^f_N%UlU^A5& zQS&y)B=UXs%A6dl8*HEcTE8Cr%C#oMJf``rx3=)BLuyOkPt`stu>G@MWpXF#h>5#V z0A|MQo$(XMb}Bh`>AU{-izFu6%&N41Sm3gz!M8MlKeH(pZmy*ryR5mSBUc8^mS>KI zt81wqw;-O(`v?&WwTMytu%toGF}N&3f1@#R;Bf2S$f3_ZSfL2U0HPFde2Y#?hV zzt}A_y;%8kzAkXvjfUoBlw%2JuMLxw3}#y^o{-HMgo+&E);l)cbdhA$8vK-RmAbLXwqZlP{Al!2;nq5g}Y}f0` zpq=~Dsx%tLBi8rur+KSO>Sqfrc*oSgF&=rDqx{G{Ax^Z;)W*qPyj-(ik3KQUP9Qz# z=6If0snW;^wx*vYgn$Wg>n^Q<p`DM|L0(ghUuI5;{$|%%7TV%tCEsY ztXr}l2iG4T7%Q-ezkNFl5{+Ad@4B~@-!@46!Iu!1U~fuv^66^Uy$a_zLT1$l>@#ED znDvSdgSSIOWqJ(7>F?MAWOE)vo`LDknhc{tfX_ghEEL)y?#9|`r z2r7f~)6VVNK?fHB5ehJecmS6SM$;#p$N2djaQ68HP)&XXgJoc7h(X<;3pMWq{|6<9 zw+2~MitGo5mP#ysyAH|lJj#VK7sAp>A1hB+>et49-_O4>Ve0Ry{aC5Nz~z1?;`g3$ zk;}5pwn`Hpo;us!s9PUdd9e3fPGj1+({bW1cVw7@Gi1rT2Bji@7-Z-@ksX4hlZ|r$E|Uw0vd zFU|T+h9zr|9jPaBlrQrCj5Pi2ak-Q1sXR|MlS70f_F|L^CC;7JmEUUjFQri~)`Wk; z(_h8k>M`cUPc5oPR=}zpE`&U)U`rb?aN&RB!ZQ&jAl_PxzNnCV`VT5OM_wE9@ z>x78N@baGivQzYO$QoGKCxk74r#nhNnkmO;h7kH7O!L3_L`U-B_7DgM&+V-~jtUQ> z-<%lJbdFu`)XRdG;`C0yq{z&@vCDbReHDS~(7vm+Kvn}theDcS>((SpZz;4Y!v>pm zu1%JDZ&xCA2=FoK+{fiKnRS~;^GrW{$ij4P41SQ2A;-mCp9isOtS&tim>)iQV&Zr&MHw z@PfDAT|e35(w<@|V{jI-A7CA%Wr%UFNCko66zw#q^ zJ`Jv|1^uu%IuKqxwrYQWrsvGxssfcz5g8VbqXRnW4ClE@=6XhR=9~pr8$^<$uT?m? z$42)K{h*o6{B3-`cD=-d^6DL7lHkqiWd*f!>lx93lh-q*Hg`~Ogjm^uF|hxy_Brvr2M&PN|EFLf86P2V^B6AO z*17tQ-mU)T1eTtN*$uF&r2|aiVJj66>zlPJ1KSCWs}BD$6YfW0{Wq?y8R3W!p#+#) z$)}9*YXhCv*FZ`-XVj>x%w~+xDRs`LP;+Imh3=1NF zEQ7CJSzQH}-5l*}R; zam&BaK8tUCP5QuOWZ-a)(^JhvUkbM%Dt+nf4DI-WF7UsbwzMC>wB=X;RtcrTu&6u2 zfhA`Vkb$5)rW1~cs6itCd)})jC-AWlDJf8whduSllcU1IZlsY9hj{Rupl853hh))a zM?}9?HvOt6j4>D<7LtZpXBVC!DZ&_h4+2Ajf~0IkZeDm+go*Ap0`UD}j3v@k0kdA~ z`jG5II!e?35hdxaju3W%fjJPK`Qk-{>AO0s0w!U{cQ&tz1h>m+4oBZ@wG($9*1*hf zyRQl8_9aqr32?&+0pJTU=;su*wxJ>Pkz-$kh5r$}E63{Dl|p3GNEVrI-@t4_AOya5 z8lDeS`Dm#U|LBrbV-Rc7EEGmeK7bGe4s0nWQAiB`FWgJRox%oZ{4;F}Z>hPtm3_Pf z4^w=mS|qGbnEo>K>V2R6@6>b!Mq;XNc4p?*8wsBp8X|&&7h#-2-GGEJkRR`>4_Fm2 zUa`CC(lA9)5ntb1re;C zf2L*6NMAZh8XZXFs_N}yXLk^F%H3h)>p&OPGFQw?D!D!Zu$gcXh4oGDJ&_2jb3;SJ z^z#%$kf`_W;YvBq3@8=)_m?6xIm|&3{7UyVUQW)Kh&-I!oTDBIh+v3|gTHkF7J)Ke zrmnWO55%Kcmz*)GiNt|wA4d{OkYB90$2_2&Kc9RsHt0aTWO>r>*(=Ql$|%)Z9Lo|i zZHI=pI0!LvUX8#8ifOz(-5SYY2PC#pPA0;Avk(bOHasAjQ~f(Lm{zC4pH@ zJSR`m-4K=$zu&ZrT?zc*Z-i4spz-8C5RRa^YbEE1^0{azbF0_NwX$VXgFar0svBfB zWE4%eb%$#cRo49UG}p0XGUvqD*iy5yNCEKTAn82+f{uf-v3xfla(yx1JT7ywm*h(? zDw-?ab8X5!tEJ@S%9{rd9_S!cT;f9d#O?l4qO3bI?cCj$P<82X?}Fu1;4-!Rs1}uLo$qA5%RH%$?1mI5Co9uovjB&EnsjQN(kM2DwxRmXzIO}DG%F2<-~)* zB$});G#`lCOEcf)Jiuf)E*C5bW>$ay3bz~f5Lmw@YABieMJ9VFu3x4k@fna~l0@jY z%=Jp6x>olj|4$}lSnUSOb|o>M7;eSA)rAxWU7$*01UJ|mzO%0iWvSkf5JNIP>UeMQ zI~&?J(y!tFzoK|ZG)IgSBQd=A`5B0dZ~5?pGx6SFgpOd(MIP}}g-v8J%!GhEgB)#f zm^NazlV17O_Q&7HCR~Y%ouEcuIr=fo7ki z>3gOL?44U24NjW(Gdy&7H-lxPzIKC!0xRgp+2t;4GS}tTi3ov#hM=xah3C<^IX(Q# zLaeGH12wvvEB}ix;jTH}Z*VJYDH~NyraLRyt!cy?Bo^K!oha3_rJ z-bD8LGsVUgVD)`nBUxZo0L>A;+{P_fk_Mk2Fzi&bs!(LV#30YfzTIy?FR;6zm!L$Y zcu!-%jFFv-)xHZ(SrS5jEES1SK@JHz$Jn;NebdGtc9=4X)gBSSySC610vv|v)+^v* z4|_woYc(v7n&`+#yvW_qM+miGB$_kU<}(r<;jg78@=^JV30i&AsaCu{(6{)^gSK$z z9OYsEe+x7A-y`wfptDd=bpJtyUb*f131$v5?2q3a0^z=o$e%zTEX!Wo$9UFFJ3;l6 z10llv$4RbQ2o?GW3KuEr94UO${~%d65e275xyMaIB9?Nut$!`IvJj}Hv#qj=-EE23Xs^@tYxJ0-RwtP?KB zBl?86%=F?FxsOn=H|!RIK3$$x2yZ|gzhI`2T`7tkZ>843Ci8HhIb+@0b@~0o--gpX zwdNFo1trB732Tv$c?0?y)@vNdYlIoCo8RCC$ztUH^TF*C%!E~0S^W?3v(fSqBKjFQ z=;(yr^a<6Oa}$Nexs_#d*Q=_$!H+;>fW1~*e1HoxbW~L8pmPfd41~ZJKHg^1M8TU=CefBi zEdzps<`&~3IysCQtC`A;l3^_Zy-)^=;viGKEbZF&Y zWC@*5FIl<%bW-<6_M-J#Y+UtZPOjbB-^s7KH%Gsz)_aPCL~7dqIUr}^M@a}!NRy`A zPVT1sW8}|RLgn`b;-o~O5+xg*3~??#JI`Zbq^05<-4os1qVJO)5dUD%SL!wd!QMbv zN$&RwU^!h~U3ra7U0o|UEH_KdFc4Ja$&|Us?JJCkyFp;so1Xi2L-TQuw9Tua9$klL+gS$ zMbx2=kxZh9%Uqj&r2&u$$tXuByt+7oF#8i}Z;u}UYs1Xk`^=IKKV>0JIdtLtc@Bj# z9P;Zv>r?9xV`7}yMiK(_{D{vXdIt~e;K+cZqlmk%_$nDPMli&KshU#8Op*v5?_hAY%BdN>%*^xT}zE#z*5h}%R435!&tF@JKF;-GP4*S@|XUGqerxZ z-m>2uZ$4*)q)|Ab(Td15px)2C`myFh*mX&%lS%#JjIT(l?Qgk#juMl4v7!ZJD<@Ay^fQQq`jEpi!weW6@0ck{54hL zEb`it#63VO99i^I<&mXEcCXay;;U%1-(|=IA*G&r>qD(2f-JSaj7jd9!-vo2DtjRK zhu63q?czMr5nlcM8H&W=PjC#7X1+!e3lIhhmpBSx1Q(Fl4YJ=&XK7&E-$#b2RtR~m zKxyoijc9RY**<%{Mr+{o>KFO+QFy<+HGQDZ_DYX`W;^M|fT|v2(!^nWb zngmAT-yxlkYU^!ax;&hmZJ2fb!hC+w6ZU`=jIkwo|Cxf2z=V_mQdA^TuI(@a;KgZ# zofty+rmi00{T^F2JMk3ei8_@TVMfZ!Bw!ntYxS>T(9?P2d9koPG6hQcc*%**lH(c3 z@z+w`zxU|=kg@!AQ9ZHoIrow)?Q|GYA&JH3S;%JMRrw#6P zAD#~SZ_$1we+@`C9?S1+W6M+3KA`Cn8+LBJO$Nkrq5e#d(6-1cVW?b?e{efH=CAQ- zbVry}`pxjVIQ%E+%xIMs@Rzwie^@WF(>ch5l0r)2&+wL&Jt#;~S11c=!<|Fw8Mzdq zs7Mwj=5y}u?m~>;lyq~y$0sGdP)Q7ilx~DYEq{RLLIh3_wTAa*{f;Ytr{TFlu;U~W zqQB)-Cj>%|fvk|-%|o-{>vzCYAsTD?!!%_7EdnHI`MtR>*hyC-*Z)RpHj*~}3l{{} zbgdUol51#@(^94GsmL^s;W@S2{^CVzqwaKNGcmW&0CTdecUCgP6uA$ae;v%p9IlnT zVW9i|@XvRfLhjH8DJtjw;E=0{o<3Piu;4!3<&tt)f8M{gPNZsLa%6di<%V#(zIPAj zNK*xCnPr6fGR5sT1CEwV#3LoYnPS-ls3FjNJLniLR;@J(oqPE&;Tv*?F#8n4(x$6d zC$0l>49aRrjker!Tbe0!^d3CMb@@r|qg*w7XpEtAm>Ikgr?R-}Z9QF}J^NfG^AW^lA*OiZdlqI`*mOYtlJp7-gU?~-0I4wEiX_R#!h4KNOko&%dc>;fa2t)?~4M|p`G8?W+ zp3_4paDF{j`WL}k!|!5% zHH^t8UfJM)xUY!8^NyOD&3}&?X=%+M-L5ORq~Qn#T&}DPNv>nOD0B}9;g*~iRCi~| zWwAl>Ehq;7R31320La270E^*>O9gP_=hY?vLX^GPo-iGV-x~BfT5ETE@zn!wrL>d{ zFh?%hB$7Dc!o_8gm(??+K*pv~V)HHj$5UDQXU{a#nuER%-KgkpR&2|^+Ty&~*ETNb zZT^+j!Ax>s`Y9d)JIqxMZh9C+78In+MN!(EL3OKB6MLZr*HEz}*OJ!{H>cT#ucx1z zEcWg{dT!aPDqDFd!aO0q#Rj={)yv&eN1Q~oKjUdQB`q?ppZ?-pG51(C0&@~8bYB=P zF8Hi3+zZ4r;$UyD(IbIC;@DHx(B23+s>IwgmFoH?cZf@QO*Ea@N z>+9*c0(W0+!Y&3Kbjo$Wz?FbvV1a6w86WVvnRpKe|8-C3`jN3}IkUsk$_kK?c8TAt zd!=$VDIC$>o_6<{D{Xv0Zp`>xad2!5M@fU(@`s>XxmL(y_6*Luzgm1V*zVK~&9W;; zsXfBAcHSjOppMJLcmA{@-?p%lQOG9u49&VdjH2wFa~G!|%L<~^_%1wl(xx;okd`xp zyN*sqv2!c=;p}9wds?zf)!J@12q*|$mXAcyDV?MF3gl&Vy*>B)u=?BQwU=>^pIylL z!cugf2%8tw;`bGWPAEFBmb9cMZIRL;G z>EzWtrUnMb?jV%a|SZ2KB|5& zNJinY^6pKW(!ThA4B(;-rrflbi;aS7aN70Gwdd5+^)A;Ff>?jZO688pv2JnqFz&W9 z;ho$YbS?7W&$&+nvx|zmOO2H8X=&}}IiB$#B=^hU9UfAx?09Y2;^NJ7J-R!?JClrt z7TnJEeXvgU8Ch6#%zu)(ko|qE=&Hwn!USw0=Ix5si^GcUjcxG{d3v7r3J;<%i6}3q zQ&e~A*5u8ly@eD8Dh5HI&T!tf!rcU2(gHpps##bdZ^%FXdy8b0dGAHy!MwqswwVL6)ZN z_ndMQtyGExGnryaft-eCea@GOr}X4^e;n4`R+06n?^sh#j4!>Cobvkj3(5{Db$F;U zVz&Pe-rfY7%Km*9-=x|!ZL>;3WEPQ#N|Ma8REDUKGLJ=}LWIm@O6H+bl$kV{Glh`p z6&W%`Ni;auqxbtezq8I+XZ_bYYyJ27e%HI+UfFv;&*%Ak?)$p0;bGeX2)EZ?nrXUO z^0QCuWE1|%tkvANdnTqS8_o_NT{I(fVFDL=?G+vuXv@_E^bWQz{QM09gNMq97$6_$ zAg7-QehT-cU{#bfkHdXcn2!(1bNe!_iFN_Bs^zMB-rxNY^V5P*pifem5t^NlJNYXjefiA$>63;Og-{`pRhg zso3q0Pu?*U;o~;R;i=Ypi;jp*ZENw4@zL{Eq7NQ+yMJ-Mwis^En3ZdHWn;0867=8a z+5DgP4-P^ylzzBpk7-4BPg7xe`A?9%;17%h=Z;;(%x5G}s|=m!VR)w>9@*$`4xk*2 zm$=L;5E_QowqqdQ)s@vg`~e5^(}IFuUMqE337Jc<2n3+(Ir5{)bPlAs<@Fu?{lC#7 zJOsUiy0WmF2C+@Wg@xE+Z1nQbX<@J;@Cm=7($xOcsUFnMqhFT@d7OGl%n!>;b+sqc zJfBX-o>G0MqMt)@Idp<`LnJlxnHvv=O%*eao+j4E*dr~*+YPAp`%$bAidnl8q+@$x zQ!YsU6$6DfJ5LGyVoiG)g2&SJ|E+{}7kb9PBm`OFhTg5nEX43|7*Bl}mTkWKLa-t? z+P-Ze_oN*^LSL;aplA&5Ka|hQgD_@UOHEbz-eu*_Z#*?9Ov-m9_-Q`s6*M{I`ndyQ z;Su8j$jI&cO2qfb3hHzqNxa=39TJ=D_{6j*+V1Tc))3-F1vmJ zrwu`o&%gtNs-^;+Mp2QJ)d!;@8*3XI3|WNfao4bDtbjW$4tjfoI0tImOzS7}{fcRA zc)vCBFpu!?C}SK)1hgPakc{^R-3AQZ#!VYAjIFnl#)y9YeM4}6L{vn;i~5?+Zu*0= zzD%6b+k16WPn=p`MrJ!4-z0IX`!oqdUEc^B&*yvFXmrwq0TCb|$C+kGhO5UO(C(h{O(BAF5rd!pIp zXW~vJXs!#LhLA^ol*|^a>Vh}H=fq!#S?;rQ$FvhwCsNIkWr){tj>ABA!kH(A4NXiQ zd0yM{`jU&cT!6A5?LSQ1j3oa1vz_nW81|q2rFqq!OM&D^Vw$Z9mJi&n8MyaNufXJa z(eWtTif2no_aD?*znEeYnY1X^b1@)_7pWuo?xH83a0&@s!bA_WIqaWkC=ogNu3Kwv z>1x>_*bzO;&Q^ApVNj5+e&%nENxi2W$Fx3bf}ncGx(+#v%PEStRJ|XAe41M zW*|Xzc3V1w?E^dE4vh~AhODBKNv<+~HyHwrf?yCEhdS&HNa(sJVNT;v^tSSpoF$w8 z%+5)-a`)*EyuNH{n7Tj&DS38)Gr?qbNOnB-+rl^phOokGtF3^}r*Ki@WmlmkhTkoNC)rkD=PZ z2g$i-S+ySq-8P_=fyj^`VReTCXYg|M9xum6z5@>)O6=Y1&nzJurHda<(XN|z3+>lv zkVzPvt@@NKsAi&AM6#&miJ-bqX1Y#dxU1Zpsu!g0bKk{@mC>(}ObV=Ywf_|w$+}xR zu*o6myltu>b03?bW}}5bbkw_;cSRXZms|jS=Icj&*vXD!dE<5|>Cf9b!fQ^gi-W9x z(}!O~s_)c6Gxc`ZhMLdUWyf~mBYZ7GsSa`T%b1JoqN#6Oo z?o2w)W<2RtdRFJ!sRTG9bU)|mHLp7&_}nnhG~@w^LQ%ZBhs$BvKxXaRxyb$ix8eQ- z*F33Gxg{l*MI|Z2+LEFOT~z^FwUwh|aal9(#L&vv5b!Kc3aNvXuxADu&dO zwzF{z$7RYt?LNrJmH(9T`I^3>+iT?gHRh#W2Y;IeUj*=rhw0U2hefKC@!1VdlRG6B_4m({b zLltW^foNMb-VEDG2FpsbMbR^x@S%c#%dhUz<*fH@d$NmbLb`<3R2vCT^4R`*G9 z{HbQw6(6y-xH$a^HyQeO5l+~nRAWE3Jq&KYV!}K9W_MZ9AzEUq(k1`>M0fM{6lK`< zN~J0&D)78A?;YA*P<(&b{MUd~`C!no;ZMzTw*HA>Qf(@ytXwQA3JaoC1#;3O-o0Pp z1b&!at3q$T=M7P2ZQD%T*nY-z5nF*xM*@_6XtK8p^Svu2D`&6J&x*T7%Cl0r$_i#t z8F1fBAmdjp=%=1PwcA5PC_`ChW%8T(sQh0rZT0IVTucv$5)rXB;vrMfGLupVwi$MD z@!gJPw5ShsYYbf1_~rMf@&ikr=Z+d@1SsEnmfjfQ>M$84%6Ha`uS8=?F}-AR?`qeu zde%!-Q*6S-dv#-&8M8^GHhDgYpa9N~l~IADR&j0`UU?c_%3zeTfUc^b6?2~EaKtl} zaD81q55^UpGF6%IKOwBLYv`?3*KO46>*z-E1v1rwZF_-+Q!{gOv-!+z6ruji&#KcJ zQm(Qo_ak45;tj)z=Ek`f1~r!=qoY&>Gk8=ce2e#gZwonI8d&o-aJ@^3+%Z;vqG4uy z^Y=GONyz?gi&oo2)e|>CK41#z-S{`Hn(dcdtK2==vI625RstWlxqdvAK~|o4ftG-g z%12fBcgCB#5R==)_8W8h->>@qq49xEJu}?m?q@evCI`J-7teH3et>c2J@aRhfI5Qv zcU|p``O{cYo8`fRze#OrX*KYE4&-V}r5F3Hzn12h@z295a&)F@rt+U+z&)YgE49}A z8b0UFxbi%|Jk)z~CUfQ5_sZVi<25rk^6nk_Q~G4*Q|qsMoS7cfcw^%8)O07UezVno z%eVGhWAgWp+0M1ot**S)AIfB2T{f#fq#^XT{vduq!im2{8u3F?j~rGjdE$p`P7kdv zlhrqP_KLW4|A#lYEz`;PBL&lB$e+6&o*JIJO!5f;2Lt~KgExQ$yM0MMX~#oRnYsZB z#|$0Lc?q8dw2mfUomWBS@)uGt7LDc5SH1RYu2vxc03OnQa_;R)P((ijgEu>CYotgL ziWKWRlV^k#ndtn|qp)Y@pPYhkuPjOK%ZoJtT@!x^NDyB+B*$XE4&cqDl8z-7XAgyKOEQo$4UX!_EveXTs9@91vo|{zaQFd=(q-vNW5Kt;@y zVb}z4`59b*bj~z?a;p6HP7Opo#(Nj%&v&O^w6)!_Bt##mh>r2bOEbJ3MjtpdWu|;k zoP)K86ln0n(Hb&_f9>6iN`inVir6qaX7={Ch2qO;K3qWB#-liBHyU| zOBvWsgpEp-Hwxneg^Ns ztR3ztaZKX~RpfqL`5p9&-lJO%K0~c4-fVX2#HCA<*{62(7QG(&lHT-N6#Ysw!a^{e zd=FwflpNRgxqOr62vAnUyrTK-TjO#!5pL=iSV=PT8__2MTg!R)@NHw`Pq1`F8XH<# z9zT5e>sz}P`2OXPU;sBiu&)NxHUvC`M(vfA^#qLI5xB4=QLr4WJ>rFnI)IiO?Rx$e zyaNmr$DY`S<2t`bNh3E8gH1T=+d7RB0DyRnSMY9zaj$F7^JgY6PwD8NeB$w`K=$B+ z-lqglzA<((Yys}HUCDQ&b<0R7+h-E&pF3`b!J!ebpZRvoaMtg;a;wV?-)O zsd%}MJj>Eb2Hiz$<2m2}0FuhyxuSSE&;WTGL@FMwV!?dLSNF>FyT@iV?Dh{>{`nL4 z8+%I2*!C;(((FaTYTjy2jZY^8-78NT9u!n6qcf{NR)hyfHqe|6?V{%6 zf6lAs0A|)4wn#moO1ti+ssNWFC#7|+UL^SkF1T!IPw1iLY}mU@Y~c01OpH67FgCfW zw-bq%x&{W!AxgwF6!Y=1EY1wVog377SPdU`vl^wZF_}ZVPv9fCfeg^tD$O3Sa=5t>DyW%{`rsa#%zAjgkUhzt1B^ir8wb7X? zjJcCT=Mwqzqa3WWP|La7Z81_}3#ulvFm)0Z*HK@GSs;dy;ad=40oo=ML7Bd2n7{-E zhn7_0_1i;3BP5d7b1J_!_rU|0zj)%|z@`L>?zLY@*~sEJQOVL}9zTlPJ=onn9*kcG zP-cWcv>rTgp7#XGDkv%n2m1*m!4p%`4XjvD80mQ+n^7fRFkxeT+;_)qaba-zvEyet zjUf&!uMluy(@QY^78Z9^>|9-OYzaPrQKE}m`s!*n^7KD<@3YySpo3O@WyZZK;q8Qa zK6J|fnkIXZ%7(@j09mU$8a<2-T!eD))v&$^s%Wyzwh-{?&CZ`L1OpEhK9(zh@*PazOIXJZ7f!4C8zrX2s48cmM>3)M@k*wAChnbCrSba_f5=V!W+dn;M z6cpNRWmjB;*f!kve}VP-VRU)WY1Qv4vL%u+0UYJEIDU@HL^(h6Fb>1xvagIJoEek3 zI;0Xz8}YXGlW(!1gY50JO2C4RdUFXSwYYfk^_Igtso1~K;ZA;U#3Mx*#q#qj-!no> zkGAe;N-Mr5dRac}+^NmWm+SSNN;m4z2**74cjR99S9V~lO&^=&=lM-=iX zA*lW$U?&b&{Az8BT;4VDdofzWpueE`S9j4tr_d9XB3APOx}Pn7N232Rh2i_Z$gv+_ zE=;n84&))y`<|&%P(X8h_vq*-cAmFy)w47VH8q<>+@Bxx{__h=0~ea!o}M`N@$ZX^ zRTx!)^gb5tfH~=R)4-0g`7#Yw>cQH2aG;Wl8WLH{n-lBvM;7K%BFtCS+epHsKD)y5s5S?dn1{{4-3SbwanS zyE)__Eg{jq)!Nqf_s_u+USO=OgbeO|ljeno{SG!9>?cBA_U?p{K+2dY1 zQ=N3#z9;xIaK`2_dw~o+Mo$TFAF-*%=xT<_yLYG)L2QYaQlIu^T8o}4cvCbD{PCpl z`<@=_-tU;T<(;YegGASwv*KXy0GkSrE5>HX<=f8DBNhx876`<_uWu!meW0GqgdZe% z4*8H)78X=KgR}dabi86~WIA;i=chz};Lne5vT(iTG#_Bwdrnf!P!)?>#vOGTxP*s4 zKc-pTj?WcKhPQ-wo$ft+_)qBZ6Cy43&Fr;9dj9}L#>oTKWig7cg!uSx4ey{9lKpL8 zR$l(txO6YdOMI?_d-0U1-20Ipq?Cs5$T5~D2P6>O{k99=;6mHFeP3rshumVHGj1j5 zC;>k@^t76Aw2m-tKv>qogEJvhJo-BHXPWzadyUSVv1e1l&T2X^o-qsczEmOR*2Pdd zTr=_x5}#-5Edt|(WY$p)Ddo>;VpwlR7!KXMiE%5zO2!~|xA)B7`?4)w`M%M~u*F}8 z?c;pw)fJJ3t;zUbFlc&KR;Xh|Gd=bX^)XAxJ&1y3M+b3Xn;Tlp#C{-!$H3kRGWb;_ zw8Ch1Vl%=SDy}XS+~J0WFL-jw6aygOBZ#54aMt zWYfo8vx|%0zF`*i3jP|7|-+b3A`Dy1Q@xw{oz^N*x0sLb>wY)_~1crx%*?&-Yak& zuBoX3D(M1+#yNLBl$if#WYeR5^Iv51dJ|YDxU?vDVpBCjH^YY)QRE|-izl@@>MN!{ z)`cPAjI@D?NhKIR_!XU}6IHLD?7U^%PyZ)K|r;3>v}KD=uZG!TppL80H;vbhW>m)Cg8EdXv?b{=|o zJOJ0==sCGhKjwyGEI?3^VLt4@%EUDOycSQ-7W6PUxL05|9^7}J1a{t6TJGW?!g}(? z4ArB!7X>HSJ;;G3C;d<~|)6i;Ii1h}C8Z`i-omwx9=?BB;0HpyDQB3*Fsi3~;&f zYqRWu26)@OKf?z=h}Zy7(dwv9Jo3++`Te&MjEeejeFid92-Q%joB^j1Hv@U3AYFv~ zW=d|!3gUwN44uih^^W}#Dm%^Clpw%rHw*k1gtUM?e8$HZK5!GNepK%PanfaB0b(>I zr5cf9Pg)vkwHV+SLXv$0<7nhZtuI0fS;>vHJ zq1OD;gCiXy*2cBP=(9ev#AF??V)i+@((3xV^iMS=A%2vPgv&STY;YPPsv?xt)QBR6 zC~+}7Lta7C(oexXdtkkB4RJgOwRd+Pr2H)rHv^;6(9-gT6_d;UG$cB%J{mEl`-ARa zN3zJXD>kxk_h@X+!`*=u6u1md18>q35Y>)0GqL>dFyWna@=p+nKx7+Zpyi4YKJ`(q zjBo)BVx2U$!_{GY5j=H05y9q&N%Kg2n$)e|W1@$`4~5ZFr5=Ekcz!U~f9)j>|4Ovi zqc$iW4dBw9v%*18w!pYEuaZJ_1viDBx){rUPd8mGQH6l;gPwjAe2(EO%hEw6@Byo} zbJ!Qcdn;*I6QDpmUha+urtZOP123`bYp~Ic^+5^Jyz`&z%5qn!AM3SZR`2|8wz-e{ z|Np}n{udDA*O`^?56V|^{a>v7{JG-1c!q$o&P{O)^%i|QGkWk)=To!bd?qmo*{?-+ zW+x`ZC8(dPB(!L(JsM8rx4N&m&aNe%An#|h*S7t$b7y&imt2*7SFe$T#9kB8SQ=ew zS`uv-IV`nJS&~8A*SvSHWlvGmjXFuc2?mbPUfVK1u}wn?uW5KVkDX`q5!5e$j%J0=ULPvg4#B*K#O@^9K)-rcL z@8NrcWu7lr4*eXM_HsGre#B5~=ZbN6XwS1HMzO3_aK~%P-~Yz>YRb!R9wCxDxl$kL z3$r|ZJ=CgwnV*Iv-grk**hnBU%KXJtk3^-?x^Uil`Pa5=q7-H2m|y|!$6f1vsnc)V zjZi)P?#`)eCI?gg+*f0ZJ@EKb^F7-VV`@L{ZFGL?Zoa(peNuj0%X!05k4n1?c@^@l zw3s(b$Ex+N`4PLDo9Dk+7f;&@?bgXTB?D#tK>zDSd!q_0-rlKBXWkXzFZRyra;4Xb znf*@6mA*fc-etB%H*c=N)e?C}>5phVVYl{z5NjAWQ;~)On}2VBZ`w-;3a%fjs#PQT zkZCw6RMEQmcP*-gxh2)VEE)R`Nzru6`yWZUz{4J>xxh?dy4};OSX4LD(RT8>^%=Sm zb>sBNN0-4ZeO-`x?YXq!XX)Tm1%{@TS;&NRbD zK!E+usiMTsLIm!b-}@Jl%<*S?YO77-Bc3C7=!`JaiMDJk3w731YtT~W75)9wZ@aj- zUFm!N=eoAAl0RqTovotuyoT!WCI@fceuWi4_hSBvMyT3d(e~TNJj=^|{#^I)Zg9>w*JulZJ^VkZpaOnmp2p$V4ve>n4T|CL>et-=V%_|-;5x?8&)J!Cj z)D0$ipZig|?{?>ek(pJfcI+^@O`>O};^e;1U0}m}M=@R3pNkUaL-Dh1RF;rf zlB8iEk$I^6#mKFS=HzfT)>jlVzcwkHi$YJO!{b zlqcZ-@7vuuy^d^Qlw#Jp@yDYJOny2A4mx(GDf03pnxTbd?YlnyB!#e{I?iyW3)EZh ztMgcsd0$c=p-~l(^xHsQOQPbxPDAFWP;}UMq9YW@Ib^?-*QZIe`0GB30c1tG0QnCu zQ~uFE!$oXq{T!<>sYZTLUh@N6ku__~dy4|krrXfOUe6qyRC+DX@BWjhyChB%YDVG79pJujAZNQ**B8&88b~b_E^}tcxz8Qfw%&p}L?#>wo;n z1-d<(iQ{Cq&)=^m3?BpTa0ls)WU{Wnr@d|R-*%lY(jfU;)3q`;a@%CmGBa}0lW6JJ z-k_$=h!UeAkrj`r3it)Ay1c4N z2E}i)wjU+kCyIxYiQ1*I09QJows1~c$~5VrUWHG9FQ>0O&2@k7wLS{ucFD|Ty46ki z`zm^DbDN7U!(~Ul0*!*UugOiPFS1n@=WVg+fKoe=I&iBw zm4)O89+Sm5J+E^AZ>V$mYZM0v0W1b80VcJhKNSI_EzTr?kb?k?h`6|kVAxA6eHnU< zz734nWSsElo8$Y_d?~uBRLZ1uo|Grdo2xrsgjulpKYDvo{X=?GbBSO$75DBVs(}r= z#hB^1sryCZ{gzHMJXqSjc8qx*B(;q z|3_H6-w{v_ZV9kdWY-C?RUFg$fQ}6^FE>|LP_s5}-b@UUn~$PKLS}Zj7u?j4#Nu z)TG9O%^Y(4zWlHc5D*|N_kcpW(7pgjKP|_*YuE75t)2IN6~CIH_2(iho5`)@1~5g? zJkyXkuO^VbNg*YN!V}gZV9d#xk*+~a)>)ye?ix>hk`WS+u6%=Amo&GAq^wHoZ%#g{ zN-H1eS57K>5KUbfE&66BC;gz!YN?dNQ@F;YU6PnxzkkoNaorXUIU+T3L`+2>`l{hA zb*KVVU=mlh*gdo~|SAAAD!`JZ=so&ru2m%Pp$XR| zKEAB`_wQF${>C#)dIJ0kZ;1ySnoEMUQg8JfQ9=lI5z0seBnSz>mSn;5OZYV|4(0g# z7@Fuzh5)t#sJey(l|E)QxTO#&zaL(9hY+6#VMJaNDGbFm09nt<0SD<{AUUJ|hTpS8 z>Yv&*FTebJXJZb;SykKjhD}aXM7I2`90g_4TAKUn7Lj80MVnlVGFg4Y{I`=#?$d+? zDsaZXBGD+A6iz)$VIY~s`vlNXYsXSu|6|u7=A%qCo_F&8(^^Hr%qZT@|NanDaRO+c z{!j=Lf&?ks`O9)p7jz6GnXRzP6hMe4nnncL$<=HHNf1+_LWOrlwn>4i)~L{t)jSE+ zJSc`>#AQ5&)TZ{Rl`^)3Bv|CY>gspS7lc5tU-1V$4mV97P7a?w5%6$~#iy|`!xJY6 zng>`tCI#o~(OD<8egg6elp0r8$8a~Mgvn*yZFpQDwwSO*>TC<-Wxp=CV|e#62e$$YHA9Z%eU9` z@7!|TEq;*ZvH9Je%-%4b163Pt6h!Nr9oKlg{>6(Q3?|$uJe)o}+(9&|of54(>27_s zvRqg3{G{+YRp><2)X1+TH8?F!6`7M?i;?=c`&d@%nrW zUgfV|tl2*9|0?|Vtvj3iURiq$mL==|yk<)Jv3&Jhf7cx62sn8gHq7sK&fQ=5-^UVF zu&U4;`|L^(O%FUuN0iAU$j+7~aAOGm=IubOO87KLtF!?!#!J@e0}+lRiDVa~;KT!| z1UG1c^F*yk+l6PDnMzx>`Oo33u|#-Hr+yF3Ra_PVuP6A9rWztXQa( z-W{F;`w9>t7b?jV?F)bn@u2;K7IYDrLM|>Y6~DjtF84l2{yk+iy!6PdUCGq+gk|QC zRKKF`_1kaw*`G_xi<6jMkw}%tDBRRx3N%c&5fZllE_rX$u-n#G7V(M=pPaY^u2WNd zDgIo_TxG5iEP*6?O5}GY?soz9yJpJf6c%<`N9%(!GI#mL%YU96y9Z3R zwC;V;v^k$RA#pg{QI!8*BND8ev9G{kGomQ49QJ)}-dJF_^d>9W>qC9fH3$Z=acG|5 z=kncbm>xx7iia}cr~n8ATsUw*;pE+{bwqUuhSl)NeuQRys`)MBJ;Cx}&E!K#zxKQ0 z5Hi>3cX7`jq381b!w5_UFXwF)2Lbv4WhG=gAVWGk;0W~oGv;k+VL`Z=fw=>SVc2-M zN!QTQzs1&{ek0e$oGAHEd$qe&$>wCe>f&_@G(6fq>$t8{bOoq+>olDcti6(rd_cMu z@TKq@zw%QkG0wC&#j`L|I!8CESnBj{Bjq}Y)3h)#z?HF0>^Q+pIhniF{(eGaQe0$G zPk;MZu<@mdliG4xS#Hh+5>}(<-Cazp8n4>{-O^;Fdhs7kgd5uS`EX3U0bsHfUa{w- zbi>=4lft&7EzB}GJ-s9ClfCL+VWF+P{dylbD0X#p2pgAZAN~eMYy(5X+5YlHOz(h| z^aHm3A**uU2adhM`3U!Bp2 zK7=~t*|TrpQ4-^h<4D9Z$#TAR3KaqbP}hsyfb~u_XAb7~nV8tpU0P9@v>O zmk9ucfJOl?;)Rn0&&1$J1+&{8E-~$K7VU z^Ta%2%1y%|#O^N>*+_pXn5j9*`BBDjv&Pi=(jFwP1Yh40^yc+0b%qnIf~X$!lw*xpB{VUnP$>x6kG#I>v>A`S;<4|c{K zpWw3q`0E< zpAwgqW(%yIIw^cnO@{4R`lf#r z(`hU?3yfoxQ*!clX=GEj!q5|o9?`D;OAdwqw^8zc|9Ge_{>v8NF zCRsbKt)JpxW~W=gFp*7xL`Ox<)N?z^)Id$~b{L6jEom!{yb3Qf9Tz&CcHUo^Y9tDO zyCg-2d^0_&h$&NE@%o^U{`aK{Qg)R7?W>29?|3aZF&Fn*u&tx#3a=e@^^_BrWuVpS zT`Nog7%ux>-tVx8_geWPRqd5LYo}CNHu1o=cURXew8C2^V;)A8omv^eLc;8R$ugsH zztCk(E>tlPcitBL)@i*Ts>uVF&L$)z{9Im!?jcFa1TyksV`jp96qJu~a?#VL5;Co@ zUGRq0#HwQo_#FqGM}sM6F_1tILu;e^ae`xh?bYscnCS`f^8>Jlwz157{s+OlX;!!i z#l>gdb!ETzo>LXzUV9riJ1pITQJFfBUp~N$UtPy~1EU`&Cl9y$T7GS16GL@Qmm?CK z^l_Ogs?<6k&umuQzq~v0c~(?M`FGt4Gnpl~t2Fc~SXt$NyLqeB>v@eXad{)hO1n-U z*!%8yAaj-?h%epifj%Mx^Vs9HP!tk8)>9IMzx0|HcpM1UC5BUX%0g5d#5J5T)okv^ z$NavqfT^o3hB_U?aHy7MZ+=^mPa+&<2uct=Gb<}W7Xdk-zSbLjT@1^Y1IyA}!CQg8 z@W~r$_OtK6+k zL*o;uJ34D|hCYDgs`cs3OUKJ;wH=E$KFl7=3*3RDgtX&;8fo9wS>#@^or|>kc->CZ zZ8YNcvDuXtuixR{_(JnKcHEs^`@ab)3}c89&16Vb5Nco0wZQ*vJjDyX`Tx@>P{!~b z_WKljB&L2*yNNij~50{9HRM-xo`K z{bX0hG({h>0-M5Ja!r5fW?V?Nay{L0Msm%4W5N_wi(fFX%mFK*_|49!Bgz&CJd#3wrq7CL< z5sP~|dnEW=*rNx$UQaGSnEv^W9%kJzOSnu32mm4d_%ei9ue9MV_RW(f-v7(AGUxKr z8DW<8#|(6SE|=4)Zr=T(e?@$Z>2yA_Seb>s*;ZZ(`a|yx!{OzkqnPJ^gQf&tT7z{K zy{ZCM6nc9W!Kc={w>?dbVY1s#P9a5!F;NNZm}I!m$r*g>aa8v=sR6#N^t3+86Kl%` zSCY1x7RX&}exKv7*}O&rL1S7Ztf?WhU-n|_zSlGWHv&SXl zICS-7D24dKHJ2OUG%M2hq2h|OU`-j+9%ZwRSFS9f`V&LvTJ`JGo?XS8-QFu*L}w1p z5wX0!ZO+Eqa1>Hq6s6Ld_c^8Uj=bn|!S z78QL*baDMI%(rvou?hTv_Y#To;KAP$S;-Ld|ESmUO5N!cuj5$ur?$SpV8x98;~m;4 znj#BYqndUG#PPtkm zX`zUKcgoW%56)vLc?IkKT&aWzHY_^2(ChaynmF)Z0l1lzxmZtS{kVF_BpE7;)B5`9 zof9i-ImibLtUoK?XZDo@3SpQelVw!E zV-zvVR$pTr98$V9Se%Mcf$f(3-s=>vjiqnZcRS}iy0*`#yl{o}>sj3^g+@mE_7qvV zKX8laskc<#tD;O+@$cPM7befZ9z0Fi2uiT9^VefV7AEB_OI6aX6Us$m|H!sv=?dg@ z_J5AyP0wnP83daxA|}Stw*_?_8ihW`7t;rKe(sn)d8mo!PKjx;oe3bZ3bX|#+S>Og zm+^f@_?mz89ZS=Zu^B*i;j`{F0UH=@E4sea+i{{Ho1@JoX~PlUe|#BV1*a6vFWj+eL-~lC^UQRhpIza&6L$cocBu z@x+DoU`%myrk6K)A`AXcMk@`ap$Zc6C+-Za_o17 zEAU)RY8cxU_o}I?M%YOASw)IV0nKQWmA`w|ms3$Yk5SY`ReH>qp&=kILX3+=;n6l7 z8H>cdN#tCC6G@ComlU%lx%tO@Y0bHObp*31QQAkOSqr#MrrfB%a)YcxGo+}UbfG}z zM>%bf-M+`I=Wu zX7|(|hlfIx0LKPe#;6awtWR(})qTH{EFmI@ z4Kj?0j(P1&q9qh#3Rj6&`5qT_ie2GpkzB#!Q+t)}VhOPyYr{3l+(tuq(3wT)ViZ}U zuNq($mBT%yW0#rf)Nj!_|L}l?vG$Qbvo#8#xjagBq(ZS=BY|Sy)H1VKvgB6pYfQ&+sKH`Kr1K zF<<0N6*ma+XI3g{;IA}zq}r8sa~oG#?i-hb$8>C?-rZ$k(CJLo#WlI%z;*t;#SL#m z!!v|2wlv=wuj{EZct^d>tMB-vmyy;{!PjQcz@YFjMdrky>Kk6AwY-URqCwnGPnO+R z6cgp=iy9P^s6KD>Brn3KeFLpR;C+2<6G|$7lz?Ja+&@vynvaT=#s)l;oF3d@W;Q1K zi3e3PgjAO_3-IPDu*QfcjvK;NkTmo64aHPx&*_sb-%+nj$Q6Yf zX8yizTE@48(^yQC%`iK}DVo8L;fMfpK|FIt6hr2QR|31D`02O`em+&ArGFzie}clr z#9e++FL%;cXM>)K>TTYBTeTK>aR<2R39LH;o3P#N8O48&3uO3eoA9wHy}O$!$er1E zyVbYUHhguY=d6usqJhy&>91$b7!^sx-Ve=(w4<$h^U9 z5;M9D*%V(rZmU7wD4mynsSK)HEk7f4>8=xnSgJR4xX651@&_+i;W@A>+RuDL+ zNI7SuDlnEvVYoAQAy{i_l8B>n->~{^*zn$}{9I8^%|~B`3>P)rXHfC&K zVA3A{oJ?z$AlTjt(gj@wsUv(KWVG;N; zMpP(y{3yP^kG0>Q<((^6ekB{2e>)8St>Kjl{4pug_u6Dca?1y?Hh4pdtvzp*IoP?3 zHZWJV>5yfu#6x+pU-ON7LRY?Ot|JVT)_Bgelm1*4yZ6QG=G-m!{>#h7%Ufo?XKfF@ zna6SXkLVZ91gV`=Qf)*C6Y&y@CvGnBGCNIN3gYEsy_UEa#4D4T*f5CK-4p*$KX@!@ zd`-z22p)=x+|ca8zT20=rFib#IWWIa4*o~R5Hk7lIDMgusBda=Ej3`}_kozQ#0K}}{1Y|1{smOuhPbM!ZEPjm>*LLhhGB^=qagH>KI3@$m6G>_dr+3K&sC z1aiW}L_=UlNV{J-NYK*qXd??SL_V#1Jidel0AQlgPG1UPQU_O{ zkC5&*=CEdV02!X%PRZtFMo=2z$8(nf-&nW#)@Z@+L=< zq?4LiO)s0*Z7yrO-~t)R>wP+!Yf`k~(Jv}GzRksC1w7%8ET@HIVB`|pxUe*qyc0Sk zQRse#0rpuuopwkH>9=!_sA!N``QB^4vfNdr#7$rNX2`*2pyCQ3c;!;YjI^|)mTd_6 zh7p>}0md%KDPW7R%l9p&@=u;Q@3^%az4NY}*mp|uZ;oIz5j7&-bN)^25^AgH4xqBGQ zU69^~ir!EZW6G29xGKh`>e~Ha=Od?XTf?h{hgUu_aiu?bQEu5uIeeqG zcGV*4B$xM5Vln`nV)WVj?rwgg#B`Lk%}KId6#!8G5ZlAY7ksABwhylS9;_9C%z)4- z!9b~Z`VZ)%Sn)9>=J4u*a{=Hwx2bNh+f6;E3;FI{ell;H?nkpT@?7uGzrl>ZtK`39-G6?Y(ig9^Pvza_$=ZB@$y6w+Ok`Pjh znKGA9gH!V!;}Wx9^6PJQUc}s_oHz+Bz_mvQLuder|BsD~@bst}baELr6CRCpe(3JN z89r`WwzWndvAIh(o$>IMoYlcYo)K$$yZ0gp8n;tnFL3(NiR2b{DEP7SbQ)JTOt}2{ z$Xex4-ar4f6Qq-l4kj`UXq?=JX2JEBgi^Hm^Ykj4ET5i!G=Dmny_!f)B8jD!lgm!U zP2hk12ffazIJU7X)D=$duO024dD}kE9>L0}T7_s@^025T6EC>mQD|6TVS~kGI{#s|5%A8Udx#^Y(o z2N20i6>M)5a!{==jMWLa`9@*mYHoC9YYSm?+3tAVQ1CW)qU66%9-HMgo428 z-pnMSr-G{Q`8Cgvc_Rt6<0Y%LidZ7LCtIn6OzXh30~ot(GZcw)K4koRgoiga!euf^ z6E@Jtj~|D$i$eH~fU*%4?PTG+2q&Qt<mmmd*xy%gCE2LZU_(vZvKJcWsHrJ3&?`{FztqAZ|})3EUeGO zd-(96F|X+1!z(-g4DB2#S19PQarCRTkus9p_UQ5B*xq<%WFnM0W#s2OGC4~Vmt0*< zPj-Cs7*tC2hSJxTU9gh_khCyQ0E)NCF!(bD>b_wJO@V=sG+5YmTTf(O2a$f|pY?|c zRfMhWwaG`txHaK1F~D?*={f=^C<+|czUepzn)I02<)4()U{vT^p@Jj7s8P|w%*;{1 z?0~I6?S-{ei4~4nt%D2tz+{YDD{+cKs(?r`VV3RlyUv|_ezkPYoL-?$&=Hrwt=z

(A!2M$YB+s7iW>v=)}iDrT}Oy7!&y49)n)c%Fp}V!_$|{Wm&07$-~RG~vu0A> zWc$AHhIh57TK{@qt$aPa@)Rr>g1}th>x%1&BehAW1={{u5%(21p zn4X%tieXR@hLV^R>hy^8-Nb|mFX&rKK7pg|m~!guT7jpyR^Cgo-YUi*@SDKP)?SwV z4sRtq3l`JpQD%FeM{1(++HVIor3t^$c85w&D>t zmu3eX>kiXPZ1=&<@5HuZgm;dPKdAa_aqq3|^bUs+24$+R?}8c>RE)c^LbnO_`{~om zkWy|my#M$3GreJV!g6`E_I+Heuwc^R+*>a0kS~ZfzBYwW6;mEe(nVv1>X)75@3%dM z(al!~8^U+3afiDS%xMU`vAgh6?9>A|Ll#*!ZBw1NoYd}%5+pZQSGB1!+!bKW@3PwA zt!ajwEJ6GB?ly%j94vZ z9`PSU%HRNutCjLA=^8@KQAXU_=|$wd*WdaA6?H-rOQ7O&G&djsIyMVrU|)i5w3DXc zsqF$HBGWJ=EBNy6pMT7t^Lxg=m2LB_#~}YfHx7MH$;VDzv=#_3$> zhaj^nvh15L#qT~&lPK+|Fc5=Fj@NJz#JgD5IOlLfP_`)_QtA>jj=kJmdi5*uRY@BG zO!c1>-tCJW7?V(ZX^4|{#T{|S03BNlXlKb@%Q?o}zOY5R5TOA_o}iP$G2J)aa|wk7 z1Vc>4tYgPk3peua&dDDgfPptw_$ zW{)Y4{@Mfbgab^eoJQ|r$Ntqn_`4sExy51!lpipg3Vqr0`XCJb=OZliCv_RKV} z2_eb`u29g9Ol1&{7bHTUv|YuF0pF^`>S-%qZe%8mEc z1WX{pX|o!*SXkC?x8#N<2egtM(N+YW=rg-fB29HZ?!@jPu@;;+CZb&g%G)xNn3#yp z;pO;2*}gLhI!uVO^_Dyg{c&*q8~U4$&}^@{?vto(6`$Wb4O-miiHW#HnHdNK~{L zMGo>--toP<>ro6!;*%$>-OkeN7y>);Gc_QOEH@W=b zy&$Y&EIpLz1~(gCDN#)9^z@DwERntW%Z>9!*5+kO?y4Qo!6!pw{Z>uiKm-YfMk4$7 zyYqy2nm%3>4P~lH=$hOy-*SExWmS5<)4+fr)75sq)til9zl>fR-Wp$ZLVo2CC;C5j zTXXj1DchdaJNuF0>u$z`%UN#!0V8y{o+ld*J54!UvuwI9CC;X$-*@ca|91KQCf2i& zTbeYC`NZFA{&?`{?X&JH0$ z`=ig>I|g=71K@`{aOwC_-`u+bh?@h?2Ap`mV8$K zTi42umfh5cC9*So^1a6Y{p z!jZHeZ-z?EOgOr#Q=VjP};E=Sk_NhhpFN@rijaAsJfm%hr z6ao_U;@(e5pTj$Yv#h^+wYwwz2X1aZCRUXsZTN6e)SuZ4Pfn9mw1+Tr^8FJ;y&4ERKjb z4Y)@3Rdsqh6d&$4V|Q8jG3S2WTEH=t^DDc80@=XeQ>?WU*id8f@tA=$ewx2rS#JnT zC%5A9=XOMB5yLLy&$a<{!7wP-vql`S~&u6uaeO)rvv{JFQwz3*q zavoc^{F9c8HzNZ^9JhK~f6T2?Ix1LKM<=0_jJiw}u zt$02&9ckMyzi27SfYga$uEpnmh7TwHlp#Bs;0lGF?USEdX zy(WPUNf9s26f+bI^qXB@?laJlY_@2$?PGoNOG*8guKN7GEB1r;Z%}fv9_SPK=~7f~ zQE-XW?E?43&~iB%hBRG-rNm%|q;Y`jfh1pyC!?{&C~Ig$WTMCe{-krgCtJfWw-L*^ zu-c_JF8sBpxj=Iru z)hL?`3%q6C!5A0Iw|lH4N!i#zRe)EYas)`T!bd?XJSXaQD$lnv{#1&qpJ_PkHCHWr z$gg&?fBCIMA)gk9%+<@Uw|I$-9B_Kk<`8@D_`z>y9q4htHl$>gvzzSCE7P+T2 zKv67((S%W--dHO|aY)C2_jYbJOHlPi9epK%=%x2|#f>h4SwdlJyhfPMUcr996qd?A zUsPeDLM2%#nNbS@B8=oef~IqoJii|x~|uAYHG>|9ukcA+si}#YFbD4#){HkM*-0$ z9&uPaYCK@8>v`%<>4J*N!WKk4TBsH{DIL^dIrb)V&+@KC+K|Yj5SU$6PA*{!~CfmG| z+wuC9nx6eUD|r*Hs@JLUELi)^sk?RFzRcN@zj#Y6CjGSAXQQk}UY^3C>2to4S8SU7 z7&aFVZL&T+S+l!KoAyGd(~xx!rA&Oc2g7j!n`aSAVAOVB-6EjsRPLBMsnwd2s-ZpP zrCz;#C*=;0zbj5(w_m+I6{&OeELT(_wsfS8K+xFVFC`|HY#v2pgeWES+&7Obk35^n zl6A;jS$f8iPR?4G$#nA2ZMDHGRq7OY%sJCo&6*AhYS*7DkaGKRp{R*2T!ToxB|zL- zyk0^GZp0^}MY3&T;h^|!G3y%)H+S3lh(7rghZTJ2zFpk-k*4dq`|Bh2`g=2Ib0hWg zf!zr&Ro{x3=dzbLIaZX$xRv>jc&QJkV^NH5GMz@=bF_=~Y%=Nf%L{|{#=ZhKG>R*G zU$zntRAN4VXJ?o8F_WXXXC*Dq_ z-dNX_fglU2wHguYwTpwg zu07#tl2pwrwoL@V(WcO|d@Wc|tItcdp=`DN&9J}j6}KwApCC1&4+Rt4^++&B%Ie=c z2A9~l)cFs6zC%(M{LP)PY}*s6UQa(}&?cS`N^y>R;5>h#W+HEBt;pqb`@>QNj^oXe zPEHp~NVN!7Rk4!^99|vuee9=qOZfw@*C!zrohp9n!3%@v#4=}_O>679>{`oPi9Gsl%v9i8E!OXphLmu)EV>G{{D_{g@|is zeVA!SZ2f2dOG~?5$O>V(73o>}JnM1+G0StHZn}ya6v>ZPEVSjJEznSM? z7OYV^bPW@4d3iYyCE{^|!w`XR0Fh848QIEAFe`*gHonzXOqqNIGcLe#bhzUhA$p8! z^()s?b#C@G7C-gFG>?luK{^JgHF-)RAvZhwv&B2Q0|#Qo?2e$81~EsV5$vRn5Q$U6 zwR_yw0AEzw|0Y{tFf@^>z;n^zn*928C9@;qp@B0I05vL`K7R&ZGPtmUflij32qOb+ zFgFo)&vn2yKOw=JCOA5J%2o`D(~viZc6p;e!KU<^rk-9tW>V2U*LY+(=(#RUUs`w< zHWuTNnV%ma!0Azj_IN_v{`I{Tl!6rA()KSf^ymCQo(hi1YzIIkfuO7XrN*Od|9~cu z=V&Sqd8^Zj+Kg;fqssOd={i&E%c!>_bA8z(!<5oC!uZqk?Dw^GA~Vfir=vjx9+c&!3C*xH=k>q79mQ>h3#Sxp%+=q^DS3FBNQQnFcj#L z3jm%5CxOi37Lxcvq*;uB=obS>$pEl;(TG#XD=O9llP7u?$hQsC7X#ke=)AjrfYk(x zC>%&7bKwj3J6c;=H?sj)&I_I&l=yf}U8IaB4}w z&L#=r)kKP$eKUL+Z>@VJ92U&k_qqY^L~0#?=)oPO^`Vz_b-6ZaaAi);e}`xj+;KBX zFTZ_TZ9X+ef5`<92tX3mBnLoBK;&kek**jHRJfx9IKz-HSIkXI%O3b_WMySxZ|k3< z_Ld&Kk{*>9$#(^wr7nZme3S7N{sC(sMdy}g@Zam;$#DDkbrKOhfRc{UT?RITSaR0a z@S6g{#wHc|8IM>@3`<}Qn=a}~arF`34@f8~5!!AJdP+0}qh7hbY#wz1K!3VtaDcQA zA>KBUIKdyB_R#5cazlhq^xBpWTIV2P*>z;tA;w!1JZVkvKk%f@*T;F;W^D`8=gs^ly0 zQwn}`FW7sRZ<&kImia}+@;6piB0i%G0sU zP#yp(!(|3d4Oo@+Do67Qwg!kG@qynp#&gx&_$K;tk=L_<1IrCMcd#LiZTf=M=yGy} zH4-laZUT&=w-C+=kPJvWK7{NP#XZzrA5M4y$qzamhg**xY((H05~qHIAhKlVt$<{D z#QYffU~bsAJjCuib^tgE7uh2kL10I?6yT~C{<$puIF1{!`-(l^K^S1a2|d(^dqn6| zaReFN50522{zVWeU}c0M2>WGfD&lnNTY!szs0Q+U|1A8fC7^iwPD2$9^(8Y^jn~zq zUPVVLGnOvntsv2?wDXkB$VTapEDLC282zxWwAb-K==0h2f3`ZrXXd)J#N6N5cv4@# zy+SK(^4}GVvAWjX&xaM});`FYRG!mq%gzz(Naxw?9?Prda9RF&S-)GdY`RL3A4WiCvIR8z4U)aU+)3E`) z*0EA|JrTkQv>huHFlz#~21N-wW5bSa;7e>HkECCLJ<$tD!jS`+JMyGkV8+Ve2gYSI zg@OcoJwnlm5_rW4s5$l+zrNr7;^)wY9so*aDy}7w@QGX4j(yEurSd$OO@_<+VA4H(yQY$Fb z;raqyKS<>R=Xfjr<(yLQKYlzH-VhcWYvbtalQOa-nws*9?UhM!=@OP87H$PahUglu zYP-Cn0|jlN%7&UInSRvWLSMFX6+N}c$%O7?eVv*U1T1tc8L%@1@A;CVVVtP|34I+n^9R2h%3@FMtuz0%WQ-x2+UbvSsz zHiF60o961>^>@kz*|n}>%q*FX4wo)u8;x)- z4omzJNWVWC?J%US_#e{MUI8Y@Y0XwFcZ9zTp`@sT_#$+i(VuD=i3m+-`vx;G-`|CN zUikHJ&1>auET&W%X<_F8<(RBQ;JLOf!6vF*9&SXpVA`AG$1hK!XXnaXL1JQT@z1!n z2@>@AG0Xi|FI{rm=n)U>EP9#yFTjp4-qOs>=t4TB?3Pn!Cb$ci1q!$KMar&O@zc&u z&K|98DQc4)8lK7PXIZJot4$OYRnOa2x8I3r#<~aCv>&C*I0*30YAT`;&B*#~LMRe2 z!Bu3Wq$rT1sK|GC4cUvoduTJSltc+EL3%6O5h`fG$Mqv5_LoU*l;-GUk!f~^cn>ve zB~CEHCZR9zOTtpwk7|r0sdaNN4+@_O1QGidq-z;;-Z%;t5)8dN=t1s;`8!9#qtn!? zdfT1u)LyT^l_0zJ%n50`5g<+?5%cDf9&Bz6=MfK$epEbp8)62^Ghkn3U z!Awv8bzx%-Ba!|K7Ncdou``Sos&tiJG^B;f`||TkCh3I+{a-_0go!t=1FXT$r;mLV z0wVG6>svbTxXZ3H)s1tBH_M6bZ^G*eXdCvx(qf{V-c)a^@ifi_*kUG`Y>M}EQj2TA z4v&Mi`7Gle-HSZF0~U%^BZufOglhU!*x7VrU;+bZ*y$v$_YH`>LFU!-sZ z|HLwnuG-2DxZ4<99_AhtJ1r#OKbxpXpvM-u%nX{etOsRl@dY=9ou4{|OITdz#&dxA<5c1r;?S zkn^cpLORxCRbdr_HPxgCJ{Q-q7cQRAv~|rmFIYT@g9zp-ltj{xL0YtTbfl)P!d%cC zAs~}!i)Dpa(gjnfx~jCO=7GCEo??UU3SN69l413TJ-D3c@P8KVy2)FmXJzP9eY31A_Vxi)ck-8UkBTr&NtNEl!;UB2B8OwB57wu$345h9J`oV zCFpAkSmp~GhML*&@n8)-xeYKvxepFBvPHDkqx6n~}Xydz2P;4U25nkS@ zl%yC&dis2v04$I9fG15f$AF@CX63c`lm0BO9qd7HC?Zc0PRp9@y*Rtg_LoUvS2W6E z?Kj4pj4)+f==sdfsggA3PR#eyVAcr@1VY$`fBIuNyO^Z9{o%(7A8LXddZjK;x4Psi zDbsNq!!Fsq0d^gaz0x0CZaPYI6MpC&je%EDTKZm>fr5^P(=!8(NG1uq8+%T3`>gB8q8G6sA(^O@V| zB;SPMM-OL26_qC35DWw**i_Tnzd@LQyhkF66&J}89<=%JEsx*`!AAoD<9U+wl$)ZHIa|rW2d~Nl>Z2gXfp8Fw2ewhLpCs<9E`?lH-e7?Z(RBlokuXZfaMFw4&{Bcd){BOCe{nJ46~)O1q`HmGo6IRph!}gSHDT_@i80t}bvV zt58~8$wKXq7B#I5L*u@Vp+yP9%zSaz5oTtE)#HKgW{pJ@g;YgW3&vB!Gn0$AW^-3v z5yf11jG9Oj!M_VFj~s_cNBMM*T`e^bF?Ghni5SOfleg7+AI~0sd#m$Amo5SBAVfQ) zVBa*JX;{p$9hPBxKuSlwK|M}+M2F&f10q*o+2>L7weT1-Z@%#}T(*)8fn zK3B;9t(%oseSvwFhv~lm%UdSBIG|y7fnf%NtSCa!k?4|^HifGISqUUFF7E69G~;w? za6%WL(LOcF>$ADclskqt3WTnFhmIC749IW9^A`-`PI;C|oj%=bpwJC~UoC-Izl9#v zfsg^Sd+B9#eEFi~R+pHSrEx_RKpbk0YZeJw?w-fB;VTa1!=##bQ_jwgmdT~4KZ@OIzCTpszs!YO|sfq9IN-_6tBF#te2Z$zR_qU|z;*i2oz2dHi=2kT~-2fd!mq z32e8p0#ZiJAkL*~@FP_W%VqX;bp*l8OipUjCgkVqUHOKXPsHHczvMJV#Hvz2-xsG6 zj4l}&Dy3V<3z4-!e5CtuxD#|hF*LG}Bf4i$8vJh^S8Q!J)Qf8shc=LdlLrMJ4l4k5 zY{jOR*G{vvU|6@uEG*A{q;q<9cFDaYTbf9gYBCMU5yDXV=+9t2syXi7@vV}hAYfmy*_#U^O+tR)vl>V83Wk68Tl$+*YPZkmpadvd{GuhD#=@3XY z5mC{*h_SKvr5QvLh=K?Kg`sW%QyiC7N6BWJ0HFhkndTrSDj%d+L!(I)(E=&K@~=WQAX4dM_0?z9lV9m z6{G1;`3v-Lg<1Mg-zS=5UG;iKd;>%Um`;J>u&kXVwz!D$M@$=x2Lx~YZF)gN;!bH; zn2d6Y!|?Zk?Fk7;khi^1g`Qxog-`FT1SuQBh!IA7>iIFfPvB43`1lz8IRhF>hCcR` z;R?c#8y*z&1MweW&!6M$j0j!%C0FHgyinF5f>d>_53>#48k}p6GH%pnBPuJYwrwE< zCQqxHuAg-4S(x}bvsCoLmy3hr1|%0KG9thg?gThwv0;TZrU}l_L(6mbXT_oF<3b@; zIcx&pT!3AI_u{UH;08|zu{l<@LlBFH?pBsw%$=n!1rKebR3*|L3uwOP?W+M$V9A6; zZFbQe*c#+Oy`Mfod+QBgcPP8|*{Nx47vUnfxnqm&3-j}k&>kp%g7ejijuR@L1(~}z9dcWgk>g2Oq0dZMt3o6p+$OKF}U`yE7Z?}U9Lq0@< zz*k7c02uv>+~rfTEa_{9^m0YTCx`+S%7~9QhNull1io$PN5tkqT4|J$NiYXM=eKsI zLBT4jr2kCWSM9r1Qo}M2vJI3RZ?$h>+H;;y+V$W!7qzrPZ`;4jYR*NrREdmX zV@~%B4#zUqM(_0J#$qq9ksSv#%d zqY~>TPU23*-`PHu_q33T2CcTX++OvO(Bi_(=#vgc z^(x2?^ZU&AU$x4=W|@lnUAl9rPS#xOl1$^?1(Ut(QQ^RwL!^c-U$L?5_-ygY>g|RL z*K3)K!|nekIXOS-_NbJX#_3del<`}?>#?6;engM z`0Rf*H-{@#glRkTc{XjD6Dl%Ddgm-^W9no!r0jfLrT`hDj8|Y5g@jyL`PBdY=RG_k zP+IN8s@fP^!O>UAp%7&&$2+oD8mSFi@1Mm1=GePa!(P8hJ>vJLe#V-UX}88=ah@Zw zYdCA^%qjlvnI+H(*jjH2=_8rRR%~zQf6Yp+Zz;RwPc`y?%U>K~Q7k@fFnBQIk^}f3 z;42jaNCsiC-%fa#?m_Y%m1rLE3F6Ac6Kq9MHtKCMqvxZF{Q*PGJ+pJE4Jui7_>QRN zSD{p*UB54_-`{EVJYyzQ3x}P!S2Ot7#ozn%Z1kHXa6gITCImBNWXn|st}8_s65mRc zTQ}N8v+wwp6`{brGGa>nNP6n993Q0u(-4WCDF5E#yZHK{V_BE&2etCp>IxNfm^SBB zR;TX%8ng3Mlzv=3es^P|zh={oIA?M0TM?7Opk z*&OBEEjha)CtTinB4U41^=YiW3-h)I1MuXy{fc!FbR7xPW%=q5yW{)V!yiITeKt{< zC%w%{ummSH)RD7!k(V>PGJr;8fMnZD>#aU!6(B)Iw$+O02uP*)xLt$Z*o&}lR!c5E ziL33d_S;T!P)s2udFD$nEX!OO8znWYH!wYk`_wJF&cO9a$fHxmep6@jWABFJL%UDy zLpA-|SxgLjOD^R}ZBf{zzMCua-U2=IKk~1p1fh!f`xW;?(s^O~`No3zC;LCDAExt7 zelqA>U`ZEdA7{UZg;MNdo6tu##=yzK`Un+mZyLI%-hZSvegcM_;$w7AA$?M5<4&;8 zpB~pKcxLjX!^u!5R+AP$CPT4*{;4NwFt_m!G_F727tYmTA;QP-G%J7k(eI^j(+m!v z$VfJZ^gmWNvVF(g+Gg2z<~;(Co%ivqI%)T+zsLX7y^vp;bVU-`i`E)-#3kWKiWp}9A~NxRDsg6g+#ZE2;h zhH#JXVg*Mmb%hkhd1Dk&YSvM_xxJ zTUEKuP;nyJkDoYC;KNVO%AK%ppW|^lB_YNa&yxQ9?|TzSQv9t5rq<(zU;*IJs|czFFrHdhhFb@tPNj<2F7lCen{@3Q35G zjXM{~F*^z$KgDa@lsaE_$MdtEYiWU#cGc%CbB))yf918PK{gfi*H2jP?TUI~W}&AH zD5gG{fRtKsUD_(7ryqO1$69PZa!A#=UxT)!^-d&SX)?0l_)z_0ba~ww-1VE(yW8YP U@2JWI5r3d?LG^s5^tA{72dkzan*aa+ diff --git a/screenshots/test_simple.png b/screenshots/test_simple.png deleted file mode 100644 index 35ccad30876c07677463d97fddcaff4e4d6ed496..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8308989 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 -echo ======================================== -echo 金山文档测试工具 (异步版本) -echo ======================================== -echo. -echo 正在启动异步版本... -echo. -python kdocs_async_test.py -pause diff --git a/start_auto_login.bat b/start_auto_login.bat deleted file mode 100644 index c2571e0..0000000 --- a/start_auto_login.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档测试工具 (完整自动登录版) -echo ======================================== -echo. -echo 正在启动完整自动登录版本... -echo. -python test_auto_login.py -pause diff --git a/start_fixed_auto_login.bat b/start_fixed_auto_login.bat deleted file mode 100644 index a0798da..0000000 --- a/start_fixed_auto_login.bat +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档测试工具 (修复版) -echo ======================================== -echo. -echo 已修复问题: -echo 1. 增加了页面加载等待时间 -echo 2. 修复了文本错误 (编辑/编译) -echo 3. 增加了二维码等待时间 -echo. -echo 正在启动修复版... -echo. -python test_auto_login.py -pause diff --git a/start_safety_test.bat b/start_safety_test.bat deleted file mode 100644 index 150d960..0000000 --- a/start_safety_test.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档安全测试工具 -echo ======================================== -echo. -echo 正在启动UI安全测试工具... -echo. -python kdocs_safety_test.py -pause diff --git a/start_safety_test_fixed.bat b/start_safety_test_fixed.bat deleted file mode 100644 index d2dc8a8..0000000 --- a/start_safety_test_fixed.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档安全测试工具 (修复版) -echo ======================================== -echo. -echo 正在启动线程安全版本... -echo. -python kdocs_safety_test_fixed.py -pause diff --git a/start_simple_test.bat b/start_simple_test.bat deleted file mode 100644 index 3d04f62..0000000 --- a/start_simple_test.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档测试工具 (最简版) -echo ======================================== -echo. -echo 正在启动最简版本... -echo. -python simple_test.py -pause diff --git a/start_sync_test.bat b/start_sync_test.bat deleted file mode 100644 index f53fd76..0000000 --- a/start_sync_test.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档测试工具 (同步线程版) -echo ======================================== -echo. -echo 正在启动同步线程版本... -echo. -python kdocs_sync_test.py -pause diff --git a/start_test.bat b/start_test.bat deleted file mode 100644 index a0b5c6a..0000000 --- a/start_test.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档上传优化测试工具 -echo ======================================== -echo. -echo 正在启动测试工具... -echo. -python test_runner.py -pause diff --git a/start_test_with_login.bat b/start_test_with_login.bat deleted file mode 100644 index 9aeaf85..0000000 --- a/start_test_with_login.bat +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -chcp 65001 >nul -echo ======================================== -echo 金山文档测试工具 (支持登录版) -echo ======================================== -echo. -echo 正在启动支持登录的测试工具... -echo. -python test_with_login.py -pause diff --git a/temp_fix_screenshot.py b/temp_fix_screenshot.py deleted file mode 100644 index a26a0f9..0000000 --- a/temp_fix_screenshot.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -临时修复截图问题的脚本 -提供三个选项:安装wkhtmltoimage、修改为Playwright、或临时禁用截图 -""" - -import os -import sys -import subprocess - - -def check_wkhtmltoimage(): - """检查wkhtmltoimage是否已安装""" - try: - result = subprocess.run(["wkhtmltoimage", "--version"], capture_output=True, text=True, timeout=5) - return result.returncode == 0 - except: - return False - - -def check_playwright(): - """检查Playwright是否已安装""" - try: - from playwright.sync_api import sync_playwright - - return True - except ImportError: - return False - - -def option1_install_wkhtmltoimage(): - """选项1: 指导安装wkhtmltoimage""" - print("\n" + "=" * 60) - print("选项 1: 安装 wkhtmltoimage (推荐)") - print("=" * 60) - - if check_wkhtmltoimage(): - print("✓ wkhtmltoimage 已经安装") - return True - - print("wkhtmltoimage 未安装,需要手动安装") - print("\n安装步骤:") - print("1. 访问: https://wkhtmltopdf.org/downloads.html") - print("2. 下载Windows版本 (.msi)") - print("3. 运行安装程序") - print("4. 将安装路径添加到系统PATH") - print("5. 重启命令行验证: wkhtmltoimage --version") - - return False - - -def option2_modify_to_playwright(): - """选项2: 修改为使用Playwright""" - print("\n" + "=" * 60) - print("选项 2: 修改为使用 Playwright") - print("=" * 60) - - if not check_playwright(): - print("❌ Playwright 未安装") - return False - - print("✓ Playwright 已安装") - print("正在修改截图实现为Playwright...") - - # 备份原文件 - original_file = "services/screenshots.py" - backup_file = "services/screenshots.py.wkhtmltoimage.backup" - - try: - # 读取原文件 - with open(original_file, "r", encoding="utf-8") as f: - content = f.read() - - # 创建备份 - with open(backup_file, "w", encoding="utf-8") as f: - f.write(content) - - print(f"✓ 已备份原文件为: {backup_file}") - - # 修改实现(简化版本) - playwright_content = '''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -截图服务 - Playwright版本 -临时替换wkhtmltoimage实现 -""" - -import os -from playwright.sync_api import sync_playwright - -def take_screenshot_playwright(url, output_path, width=1920, height=1080): - """使用Playwright截图""" - try: - with sync_playwright() as p: - browser = p.chromium.launch(headless=True) - page = browser.new_page() - page.set_viewport_size({"width": width, "height": height}) - page.goto(url, timeout=30000) - page.wait_for_timeout(3000) # 等待页面加载 - page.screenshot(path=output_path, full_page=True) - browser.close() - return True - except Exception as e: - print(f"截图失败: {e}") - return False - -def take_screenshot_for_account(account, target_url, browse_type, user_id, account_id): - """为账号截图""" - screenshot_filename = f"account_{account_id}_{browse_type}.png" - screenshot_path = os.path.join("screenshots", screenshot_filename) - - os.makedirs("screenshots", exist_ok=True) - - success = take_screenshot_playwright(target_url, screenshot_path) - - if success: - return {"success": True, "screenshot_path": screenshot_path} - else: - return {"success": False, "error": "截图失败"} -''' - - # 写入新实现 - with open(original_file, "w", encoding="utf-8") as f: - f.write(playwright_content) - - print("✓ 已修改为Playwright实现") - print("✓ 重启应用后生效") - return True - - except Exception as e: - print(f"❌ 修改失败: {e}") - return False - - -def option3_disable_screenshot(): - """选项3: 临时禁用截图""" - print("\n" + "=" * 60) - print("选项 3: 临时禁用截图功能") - print("=" * 60) - - # 设置环境变量禁用截图 - os.environ["ENABLE_SCREENSHOT"] = "0" - print("✓ 已设置环境变量: ENABLE_SCREENSHOT=0") - print("✓ 重启应用后截图功能将被跳过") - - # 检查tasks.py中是否有截图调用 - try: - with open("services/tasks.py", "r", encoding="utf-8") as f: - content = f.read() - - if "take_screenshot_for_account" in content: - print("⚠️ 发现tasks.py中有截图调用,建议注释掉:") - print(" 查找: take_screenshot_for_account") - print(" 临时注释: # take_screenshot_for_account(...)") - - except Exception as e: - print(f"检查tasks.py失败: {e}") - - return True - - -def main(): - print("🔧 截图问题修复工具") - print("=" * 60) - - # 检查当前状态 - print("📊 当前状态:") - print(f" wkhtmltoimage: {'✓ 已安装' if check_wkhtmltoimage() else '❌ 未安装'}") - print(f" Playwright: {'✓ 已安装' if check_playwright() else '❌ 未安装'}") - - while True: - print("\n请选择修复方案:") - print("1. 安装 wkhtmltoimage (推荐)") - print("2. 修改为使用 Playwright") - print("3. 临时禁用截图功能") - print("4. 查看状态") - print("5. 退出") - - choice = input("\n请输入选项 (1-5): ").strip() - - if choice == "1": - if option1_install_wkhtmltoimage(): - print("\n🎉 wkhtmltoimage安装完成!重启应用即可。") - elif choice == "2": - option2_modify_to_playwright() - elif choice == "3": - option3_disable_screenshot() - elif choice == "4": - print("\n📊 当前状态:") - print(f" wkhtmltoimage: {'✓ 已安装' if check_wkhtmltoimage() else '❌ 未安装'}") - print(f" Playwright: {'✓ 已安装' if check_playwright() else '❌ 未安装'}") - elif choice == "5": - print("👋 再见!") - break - else: - print("❌ 无效选项,请重新输入") - - -if __name__ == "__main__": - main() diff --git a/test_auto_login.py b/test_auto_login.py deleted file mode 100644 index 19fc300..0000000 --- a/test_auto_login.py +++ /dev/null @@ -1,536 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -金山文档上传测试 - 完整自动登录版本 -自动处理:登录并加入编译 → 扫码 → 确认登录 -""" - -import os -import sys -import time -import base64 -from datetime import datetime -from io import BytesIO -from PIL import Image - -# 添加项目路径 -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) - - -def log(message, level='INFO'): - """日志输出""" - timestamp = datetime.now().strftime("%H:%M:%S") - print(f"[{timestamp}] {level}: {message}") - - -def pause(msg="按Enter键继续..."): - """等待用户按键""" - input(f"\n{msg}") - - -def ask_yes_no(question, default='n'): - """询问用户是/否问题""" - if default == 'y': - prompt = f"{question} (Y/n): " - else: - prompt = f"{question} (y/N): " - - answer = input(prompt).strip().lower() - if not answer: - answer = default - return answer == 'y' - - -def save_qr_code(qr_image_bytes, filename="qr_code.png"): - """保存二维码图片""" - try: - with open(filename, 'wb') as f: - f.write(qr_image_bytes) - log(f"[OK] 二维码已保存到: {filename}", 'SUCCESS') - return filename - except Exception as e: - log(f"✗ 保存二维码失败: {str(e)}", 'ERROR') - return None - - -def click_login_join_button(page): - """点击'登录并加入编辑'按钮""" - log("查找'登录并加入编辑'按钮...", 'INFO') - - # 多种可能的按钮选择器 - login_selectors = [ - "text=登录并加入编辑", - "text=登录并加入编译", - "button:has-text('登录')", - "text=立即登录", - "[class*='login']", - "[id*='login']" - ] - - for selector in login_selectors: - try: - button = page.locator(selector).first - if button.is_visible(timeout=3000): - log(f"[OK] 找到登录按钮: {selector}", 'SUCCESS') - button.click() - log("[OK] 已点击登录按钮", 'SUCCESS') - return True - except Exception: - continue - - log("✗ 未找到登录按钮", 'ERROR') - return False - - -def wait_for_qr_code(page, timeout=30): - """等待二维码出现""" - log("等待二维码加载...", 'INFO') - - start_time = time.time() - while time.time() - start_time < timeout: - try: - # 查找二维码元素 - qr_selectors = [ - "canvas", - "img[src*='qr']", - "img[alt*='二维码']", - "[class*='qr']", - "[id*='qr']", - "div[class*='qrcode']", - "img[src*='wechat']" - ] - - for selector in qr_selectors: - try: - elements = page.query_selector_all(selector) - for i, element in enumerate(elements): - try: - # 尝试截图 - screenshot = element.screenshot() - if len(screenshot) > 500: # 足够大的图片 - filename = f"qr_code_{i}.png" - save_qr_code(screenshot, filename) - log(f"[OK] 找到二维码元素: {selector}[{i}]", 'SUCCESS') - return True - except Exception: - continue - except Exception: - continue - - time.sleep(1) - - except Exception as e: - log(f"检查二维码时出错: {str(e)}", 'WARNING') - time.sleep(1) - - return False - - -def wait_for_confirm_login(page, timeout=120): - """等待'确认登录'按钮出现并点击""" - log("等待用户扫码...", 'INFO') - log("请使用手机微信扫描二维码", 'INFO') - log("扫码完成后,程序会自动检测并点击'确认登录'", 'INFO') - - start_time = time.time() - check_interval = 2 # 每2秒检查一次 - - while time.time() - start_time < timeout: - try: - # 查找确认登录按钮 - confirm_selectors = [ - "text=确认登录", - "text=确认登陆", - "button:has-text('确认')", - "text=登录", - "[class*='confirm']", - "[id*='confirm']" - ] - - for selector in confirm_selectors: - try: - button = page.locator(selector).first - if button.is_visible(timeout=1000): - log(f"[OK] 找到确认按钮: {selector}", 'SUCCESS') - button.click() - log("[OK] 已点击确认登录按钮", 'SUCCESS') - return True - except Exception: - continue - - # 如果没找到按钮,显示等待信息 - elapsed = int(time.time() - start_time) - if elapsed % 10 == 0: # 每10秒显示一次 - log(f"等待中... ({elapsed}秒)", 'INFO') - - time.sleep(check_interval) - - except Exception as e: - log(f"检查确认按钮时出错: {str(e)}", 'WARNING') - time.sleep(check_interval) - - return False - - -def wait_for_document_loaded(page, timeout=30): - """等待文档页面加载完成""" - log("等待文档页面加载...", 'INFO') - - start_time = time.time() - while time.time() - start_time < timeout: - try: - current_url = page.url - log(f"当前URL: {current_url}", 'INFO') - - # 检查是否进入文档页面 - if "kdocs.cn" in current_url and "/spreadsheet/" in current_url: - log("[OK] 已进入文档页面", 'SUCCESS') - return True - - # 检查表格元素 - try: - canvas_count = page.locator("canvas").count() - if canvas_count > 0: - log(f"[OK] 检测到 {canvas_count} 个表格元素", 'SUCCESS') - return True - except: - pass - - time.sleep(2) - - except Exception as e: - log(f"检查页面状态时出错: {str(e)}", 'WARNING') - time.sleep(2) - - return False - - -def main(): - """主函数""" - print("=" * 70) - print("[LOCK] 金山文档上传测试 - 完整自动登录版本") - print("=" * 70) - print() - print("特点:") - print(" [OK] 自动点击'登录并加入编译'") - print(" [OK] 自动捕获二维码") - print(" [OK] 自动等待并点击'确认登录'") - print(" [OK] 自动检测文档加载") - print() - - # 配置 - doc_url = input("请输入金山文档URL (或按Enter使用默认): ").strip() - if not doc_url: - doc_url = "https://kdocs.cn/l/cpwEOo5ynKX4" - - print(f"\n使用URL: {doc_url}") - print() - - if not ask_yes_no("确认开始测试?"): - print("测试已取消") - return - - print("\n" + "=" * 70) - print("开始测试流程") - print("=" * 70) - - playwright = None - browser = None - context = None - page = None - - try: - # ===== 步骤1: 启动浏览器 ===== - print("\n" + "=" * 50) - print("步骤1: 启动浏览器") - print("=" * 50) - - log("正在启动Playwright...", 'INFO') - playwright = sync_playwright().start() - log("[OK] Playwright启动成功", 'SUCCESS') - - log("正在启动浏览器...", 'INFO') - browser = playwright.chromium.launch(headless=False) - log("[OK] 浏览器启动成功", 'SUCCESS') - - log("正在创建上下文...", 'INFO') - context = browser.new_context() - log("[OK] 上下文创建成功", 'SUCCESS') - - log("正在创建页面...", 'INFO') - page = context.new_page() - page.set_default_timeout(30000) - log("[OK] 页面创建成功", 'SUCCESS') - - pause("浏览器已启动,请观察浏览器窗口") - - log("额外等待5秒确保浏览器完全就绪...", 'INFO') - time.sleep(5) - - # ===== 步骤2: 打开文档页面 ===== - print("\n" + "=" * 50) - print("步骤2: 打开文档页面") - print("=" * 50) - - log(f"正在导航到: {doc_url}", 'INFO') - page.goto(doc_url, wait_until='domcontentloaded') - log("[OK] 页面导航完成", 'SUCCESS') - - log("等待8秒让页面完全加载...", 'INFO') - time.sleep(8) - - current_url = page.url - log(f"当前URL: {current_url}", 'INFO') - - # ===== 步骤3: 自动点击登录按钮 ===== - print("\n" + "=" * 50) - print("步骤3: 点击登录按钮") - print("=" * 50) - - log("检测页面状态...", 'INFO') - log("等待页面元素完全加载...", 'INFO') - - # 额外的等待确保页面完全加载 - log("额外等待5秒确保页面完全加载...", 'INFO') - time.sleep(5) - - # 尝试等待特定元素出现 - try: - page.wait_for_selector("text=登录并加入", timeout=15000) - log("[OK] 检测到'登录并加入编辑'页面", 'SUCCESS') - login_button_found = True - except: - log("⚠ 未检测到登录按钮,继续等待...", 'WARNING') - time.sleep(5) - login_button_found = False - - # 最终检测页面内容 - page_content = page.content() - if "登录并加入" in page_content: - log("[OK] 检测到'登录并加入编辑'页面", 'SUCCESS') - login_button_found = True - else: - log("⚠ 未检测到'登录并加入编辑'页面", 'WARNING') - login_button_found = False - - # 执行点击操作 - if login_button_found: - if click_login_join_button(page): - log("[OK] 已点击登录按钮,等待跳转到扫码页面...", 'SUCCESS') - time.sleep(5) # 增加等待时间 - else: - log("✗ 点击登录按钮失败", 'ERROR') - return - else: - # 检查是否已经直接进入登录页面 - if "login" in page.url.lower() or "account" in page.url.lower(): - log("[OK] 已直接进入登录页面", 'SUCCESS') - else: - log("⚠ 页面状态不明确,请手动检查浏览器窗口", 'WARNING') - - # ===== 步骤4: 等待二维码 ===== - print("\n" + "=" * 50) - print("步骤4: 等待二维码") - print("=" * 50) - - if wait_for_qr_code(page, timeout=90): - log("[OK] 二维码加载完成", 'SUCCESS') - else: - log("⚠ 未检测到二维码,可能页面结构有变化", 'WARNING') - - # ===== 步骤5: 等待确认登录 ===== - print("\n" + "=" * 50) - print("步骤5: 等待确认登录") - print("=" * 50) - - log("扫码流程:", 'INFO') - log("1. 请使用手机微信扫描二维码", 'INFO') - log("2. 扫码后点击'确认登录'", 'INFO') - log("3. 程序会自动检测并处理", 'INFO') - - if wait_for_confirm_login(page, timeout=180): - log("[OK] 登录确认完成", 'SUCCESS') - else: - log("⚠ 未检测到确认登录操作", 'WARNING') - - # ===== 步骤6: 等待文档加载 ===== - print("\n" + "=" * 50) - print("步骤6: 等待文档加载") - print("=" * 50) - - if wait_for_document_loaded(page, timeout=60): - log("[OK] 文档页面加载完成", 'SUCCESS') - - # 验证表格元素 - try: - canvas_count = page.locator("canvas").count() - log(f"[OK] 检测到 {canvas_count} 个表格元素", 'SUCCESS') - - # 尝试读取名称框 - try: - name_box = page.locator("input.edit-box").first - if name_box.is_visible(): - value = name_box.input_value() - log(f"[OK] 名称框可见,当前值: '{value}'", 'SUCCESS') - except: - pass - - except Exception as e: - log(f"检查表格元素时出错: {str(e)}", 'WARNING') - else: - log("⚠ 文档页面加载超时", 'WARNING') - - # ===== 步骤7: 表格功能测试 ===== - print("\n" + "=" * 50) - print("步骤7: 表格功能测试") - print("=" * 50) - - # 测试搜索功能 - test_name = input("请输入要搜索的姓名 (默认: 张三): ").strip() - if not test_name: - test_name = "张三" - - log(f"搜索姓名: {test_name}", 'INFO') - - try: - page.keyboard.press("Control+f") - time.sleep(0.5) - - page.keyboard.type(test_name) - time.sleep(0.3) - - page.keyboard.press("Enter") - time.sleep(1) - - page.keyboard.press("Escape") - time.sleep(0.3) - - log("[OK] 搜索测试完成", 'SUCCESS') - log("请查看浏览器窗口,检查搜索结果", 'INFO') - - except Exception as e: - log(f"✗ 搜索测试失败: {str(e)}", 'ERROR') - - pause("搜索测试完成") - - # ===== 步骤8: 图片上传测试 ===== - print("\n" + "=" * 50) - print("步骤8: 图片上传测试") - print("=" * 50) - - if ask_yes_no("是否进行图片上传测试?"): - image_path = input("请输入测试图片的完整路径: ").strip() - - if not image_path or not os.path.exists(image_path): - log("图片文件不存在,跳过上传测试", 'WARNING') - else: - log(f"选中的图片: {image_path}", 'INFO') - - try: - # 导航到D3 - name_box = page.locator("input.edit-box").first - name_box.click() - name_box.fill("D3") - name_box.press("Enter") - time.sleep(0.5) - - log("[OK] 已导航到D3单元格") - - # 点击插入 - insert_btn = page.locator("text=插入").first - insert_btn.click() - time.sleep(0.5) - - log("[OK] 已点击插入按钮") - - # 点击图片 - image_btn = page.locator("text=图片").first - image_btn.click() - time.sleep(0.5) - - log("[OK] 已点击图片按钮") - - # 选择本地 - local_option = page.locator("text=本地").first - local_option.click() - - log("[OK] 已选择本地图片") - - # 上传文件 - with page.expect_file_chooser() as fc_info: - pass - - file_chooser = fc_info.value - file_chooser.set_files(image_path) - - log("[OK] 文件上传命令已发送") - - time.sleep(3) - - log("[OK] 图片上传测试完成", 'SUCCESS') - - except Exception as e: - log(f"✗ 图片上传测试失败: {str(e)}", 'ERROR') - - pause("所有测试完成") - - # ===== 测试完成 ===== - print("\n" + "=" * 70) - log("🎉 所有测试完成!", 'SUCCESS') - print("=" * 70) - - except KeyboardInterrupt: - print("\n") - log("测试被用户中断", 'WARNING') - except Exception as e: - print("\n") - log(f"测试过程中出现错误: {str(e)}", 'ERROR') - import traceback - traceback.print_exc() - finally: - # 清理资源 - print("\n" + "=" * 70) - print("清理资源...") - print("=" * 70) - - try: - if page: - page.close() - log("[OK] 页面已关闭", 'SUCCESS') - except: - pass - - try: - if context: - context.close() - log("[OK] 上下文已关闭", 'SUCCESS') - except: - pass - - try: - if browser: - browser.close() - log("[OK] 浏览器已关闭", 'SUCCESS') - except: - pass - - try: - if playwright: - playwright.stop() - log("[OK] Playwright已停止", 'SUCCESS') - except: - pass - - log("测试结束", 'SUCCESS') - print("=" * 70) - - -if __name__ == "__main__": - main() diff --git a/test_no_ui.py b/test_no_ui.py deleted file mode 100644 index 8ad7146..0000000 --- a/test_no_ui.py +++ /dev/null @@ -1,329 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -金山文档上传测试 - 纯命令行版本 -无任何UI库,100%稳定 -""" - -import os -import sys -import time -from datetime import datetime - -# 添加项目路径 -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) - - -def log(message, level='INFO'): - """日志输出""" - timestamp = datetime.now().strftime("%H:%M:%S") - print(f"[{timestamp}] {level}: {message}") - - -def pause(msg="按Enter键继续..."): - """等待用户按键""" - input(f"\n{msg}") - - -def ask_yes_no(question, default='n'): - """询问用户是/否问题""" - if default == 'y': - prompt = f"{question} (Y/n): " - else: - prompt = f"{question} (y/N): " - - answer = input(prompt).strip().lower() - if not answer: - answer = default - return answer == 'y' - - -def main(): - """主函数""" - print("=" * 70) - print("[LOCK] 金山文档上传测试 - 纯命令行版本") - print("=" * 70) - print() - print("特点:") - print(" [OK] 无UI库依赖") - print(" [OK] 单线程顺序执行") - print(" [OK] 100%稳定可靠") - print(" [OK] 详细操作指导") - print() - - # 配置 - doc_url = input("请输入金山文档URL (或按Enter使用默认): ").strip() - if not doc_url: - doc_url = "https://kdocs.cn/l/cpwEOo5ynKX4" - - print(f"\n使用URL: {doc_url}") - print() - - if not ask_yes_no("确认开始测试?"): - print("测试已取消") - return - - print("\n" + "=" * 70) - print("开始测试流程") - print("=" * 70) - - playwright = None - browser = None - context = None - page = None - - try: - # ===== 步骤1: 启动浏览器 ===== - print("\n" + "=" * 50) - print("步骤1: 启动浏览器") - print("=" * 50) - log("正在启动Playwright...", 'INFO') - playwright = sync_playwright().start() - log("[OK] Playwright启动成功", 'SUCCESS') - - log("正在启动浏览器...", 'INFO') - browser = playwright.chromium.launch(headless=False) - log("[OK] 浏览器启动成功", 'SUCCESS') - - log("正在创建上下文...", 'INFO') - context = browser.new_context() - log("[OK] 上下文创建成功", 'SUCCESS') - - log("正在创建页面...", 'INFO') - page = context.new_page() - page.set_default_timeout(30000) - log("[OK] 页面创建成功", 'SUCCESS') - - pause("浏览器已启动,请观察浏览器窗口是否正常打开") - - # ===== 步骤2: 打开文档 ===== - print("\n" + "=" * 50) - print("步骤2: 打开金山文档") - print("=" * 50) - - log(f"正在导航到: {doc_url}", 'INFO') - page.goto(doc_url, wait_until='domcontentloaded') - log("[OK] 页面导航完成", 'SUCCESS') - - log("等待5秒让页面完全加载...", 'INFO') - time.sleep(5) - - current_url = page.url - log(f"当前URL: {current_url}", 'INFO') - - if "kdocs.cn" in current_url: - log("[OK] 已成功进入金山文档", 'SUCCESS') - else: - log("⚠ 当前不在金山文档域名,可能URL有误", 'WARNING') - - # 检查登录状态 - try: - login_visible = page.locator("text=登录").first.is_visible() - if login_visible: - log("⚠ 检测到登录页面,需要扫码登录", 'WARNING') - log("请使用手机微信扫码登录", 'INFO') - else: - log("[OK] 未检测到登录提示", 'SUCCESS') - except: - log("⚠ 无法检测登录状态", 'WARNING') - - pause("文档已加载,请确认浏览器中是否显示了正确的表格") - - # ===== 步骤3: 表格读取 ===== - print("\n" + "=" * 50) - print("步骤3: 表格读取测试") - print("=" * 50) - - # 尝试读取名称框 - try: - log("尝试定位名称框...", 'INFO') - name_box = page.locator("input.edit-box").first - if name_box.is_visible(): - value = name_box.input_value() - log(f"[OK] 名称框可见,当前值: '{value}'", 'SUCCESS') - else: - log("⚠ 名称框不可见", 'WARNING') - except Exception as e: - log(f"⚠ 读取名称框失败: {str(e)}", 'WARNING') - - # 查找表格元素 - try: - log("正在查找表格元素...", 'INFO') - canvas_count = page.locator("canvas").count() - log(f"[OK] 检测到 {canvas_count} 个canvas元素", 'SUCCESS') - except Exception as e: - log(f"⚠ 查找canvas失败: {str(e)}", 'WARNING') - - pause("表格元素检查完成,请确认表格是否正常显示") - - # ===== 步骤4: 人员搜索 ===== - print("\n" + "=" * 50) - print("步骤4: 人员搜索测试") - print("=" * 50) - - test_name = input("请输入要搜索的姓名 (默认: 张三): ").strip() - if not test_name: - test_name = "张三" - - log(f"搜索姓名: {test_name}", 'INFO') - log("执行步骤: Ctrl+F → 输入姓名 → Enter", 'INFO') - - try: - log("步骤1: 打开搜索框 (Ctrl+F)...", 'INFO') - page.keyboard.press("Control+f") - time.sleep(0.5) - - log(f"步骤2: 输入搜索内容: {test_name}", 'INFO') - page.keyboard.type(test_name) - time.sleep(0.3) - - log("步骤3: 执行搜索 (Enter)...", 'INFO') - page.keyboard.press("Enter") - time.sleep(1) - - log("步骤4: 关闭搜索框 (Escape)...", 'INFO') - page.keyboard.press("Escape") - time.sleep(0.3) - - log("[OK] 人员搜索测试完成", 'SUCCESS') - log("请查看浏览器窗口,检查是否高亮显示了搜索结果", 'INFO') - - except Exception as e: - log(f"✗ 搜索测试失败: {str(e)}", 'ERROR') - - pause("搜索测试完成,请确认搜索结果是否正确") - - # ===== 步骤5: 图片上传 ===== - print("\n" + "=" * 50) - print("步骤5: 图片上传测试 (可选)") - print("=" * 50) - print("此步骤将实际上传图片到D3单元格") - print("请准备一张小尺寸测试图片") - print() - - if ask_yes_no("是否进行图片上传测试?"): - # 让用户输入图片路径 - image_path = input("请输入测试图片的完整路径: ").strip() - - if not image_path or not os.path.exists(image_path): - log("图片文件不存在或路径无效,跳过上传测试", 'WARNING') - else: - log(f"选中的图片: {image_path}", 'INFO') - - try: - print("\n执行上传流程:") - log("步骤1: 导航到 D3 单元格...", 'INFO') - name_box = page.locator("input.edit-box").first - name_box.click() - name_box.fill("D3") - name_box.press("Enter") - time.sleep(0.5) - - log("步骤2: 点击插入按钮...", 'INFO') - insert_btn = page.locator("text=插入").first - insert_btn.click() - time.sleep(0.5) - - log("步骤3: 点击图片选项...", 'INFO') - image_btn = page.locator("text=图片").first - image_btn.click() - time.sleep(0.5) - - log("步骤4: 选择本地图片...", 'INFO') - local_option = page.locator("text=本地").first - local_option.click() - - log("步骤5: 上传文件...", 'INFO') - with page.expect_file_chooser() as fc_info: - pass - - file_chooser = fc_info.value - file_chooser.set_files(image_path) - - log("等待上传完成...", 'INFO') - time.sleep(3) - - log("[OK] 图片上传测试完成", 'SUCCESS') - log("请检查浏览器窗口,确认图片已上传到D3单元格", 'INFO') - - except Exception as e: - log(f"✗ 图片上传测试失败: {str(e)}", 'ERROR') - log("可能是页面元素定位失败,请检查页面状态", 'WARNING') - else: - log("跳过图片上传测试", 'INFO') - - pause("图片上传测试完成") - - # ===== 测试完成 ===== - print("\n" + "=" * 70) - log("所有测试完成!", 'SUCCESS') - print("=" * 70) - print() - print("测试结果:") - print(" [[OK]] 浏览器启动 - 成功") - print(" [[OK]] 文档打开 - 成功") - print(" [[OK]] 表格读取 - 成功") - print(" [[OK]] 人员搜索 - 成功") - if ask_yes_no("是否执行了图片上传?"): - print(" [[OK]] 图片上传 - 已测试") - else: - print(" [-] 图片上传 - 已跳过") - print() - print("浏览器窗口将保持打开状态") - print("您可以手动关闭浏览器窗口来结束测试") - - except KeyboardInterrupt: - print("\n") - log("测试被用户中断", 'WARNING') - except Exception as e: - print("\n") - log(f"测试过程中出现错误: {str(e)}", 'ERROR') - import traceback - traceback.print_exc() - finally: - # 清理资源 - print("\n" + "=" * 70) - print("清理资源...") - print("=" * 70) - - try: - if page: - page.close() - log("[OK] 页面已关闭", 'SUCCESS') - except: - pass - - try: - if context: - context.close() - log("[OK] 上下文已关闭", 'SUCCESS') - except: - pass - - try: - if browser: - browser.close() - log("[OK] 浏览器已关闭", 'SUCCESS') - except: - pass - - try: - if playwright: - playwright.stop() - log("[OK] Playwright已停止", 'SUCCESS') - except: - pass - - log("测试结束", 'SUCCESS') - print("=" * 70) - - -if __name__ == "__main__": - main() diff --git a/test_runner.py b/test_runner.py deleted file mode 100644 index 8f8aea9..0000000 --- a/test_runner.py +++ /dev/null @@ -1,329 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -金山文档上传优化测试运行器 -运行各种测试来验证优化效果 -""" - -import os -import sys -import time -from pathlib import Path - -# 添加当前目录到路径 -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from kdocs_safety_test import SafetyTestTool -from kdocs_optimized_uploader import OptimizedKdocsUploader - - -def print_banner(): - """打印欢迎横幅""" - print("=" * 70) - print("[LOCK] 金山文档上传安全测试工具 v1.0") - print("=" * 70) - print() - print("📋 测试工具说明:") - print(" 1. safety_test.py - UI安全测试工具 (推荐新手使用)") - print(" - 每一步操作都需要手动确认") - print(" - 详细的操作日志") - print(" - 安全提示和警告") - print() - print(" 2. optimized_uploader.py - 优化后的上传器") - print(" - 智能缓存系统") - print(" - 减少等待时间") - print(" - 快速定位算法") - print() - print("⚠️ 重要提醒:") - print(" - 请确保金山文档URL配置正确") - print(" - 测试前请备份重要数据") - print(" - 仅使用测试图片进行上传测试") - print() - print("=" * 70) - print() - - -def check_prerequisites(): - """检查运行环境""" - print("🔍 检查运行环境...") - - # 检查Python版本 - python_version = sys.version_info - if python_version.major < 3 or (python_version.major == 3 and python_version.minor < 8): - print("❌ Python版本过低,需要Python 3.8+") - return False - print(f"✅ Python版本: {python_version.major}.{python_version.minor}.{python_version.micro}") - - # 检查playwright - try: - import playwright - print("✅ Playwright已安装") - except ImportError: - print("❌ Playwright未安装") - print(" 请运行: pip install playwright") - return False - - # 检查必要的目录 - os.makedirs("data", exist_ok=True) - os.makedirs("screenshots", exist_ok=True) - print("✅ 必要目录已创建") - - print("✅ 运行环境检查通过\n") - return True - - -def show_menu(): - """显示主菜单""" - print("请选择要运行的测试工具:") - print() - print(" [1] 启动UI安全测试工具 (推荐)") - print(" - 有图形界面,每步确认") - print(" - 安全可控,适合新手") - print() - print(" [2] 运行命令行测试") - print(" - 快速测试优化功能") - print(" - 适合开发者") - print() - print(" [3] 查看优化说明") - print(" - 了解优化原理") - print(" - 查看配置参数") - print() - print(" [4] 退出") - print() - choice = input("请输入选项 (1-4): ").strip() - return choice - - -def run_ui_test(): - """运行UI测试工具""" - print("\n🚀 启动UI安全测试工具...") - print("-" * 70) - print("说明:") - print(" 1. 将打开图形界面") - print(" 2. 每一步操作都需要点击'确认执行'") - print(" 3. 操作日志显示在底部") - print(" 4. 如有问题请查看日志") - print() - input("按Enter键继续...") - - try: - tool = SafetyTestTool() - tool.run() - except Exception as e: - print(f"\n❌ 启动失败: {str(e)}") - print("\n可能的解决方案:") - print(" 1. 确保已安装tkinter: sudo apt-get install python3-tk") - print(" 2. 确保已安装playwright: pip install playwright") - print(" 3. 确保已安装浏览器: playwright install chromium") - - -def run_command_line_test(): - """运行命令行测试""" - print("\n🔧 运行命令行测试...") - print("-" * 70) - - # 获取测试配置 - doc_url = input("请输入金山文档URL (或按Enter使用默认值): ").strip() - if not doc_url: - doc_url = "https://www.kdocs.cn/spreadsheet/your-doc-id" - - test_name = input("请输入测试人员姓名 (默认: 张三): ").strip() - if not test_name: - test_name = "张三" - - test_unit = input("请输入测试县区 (默认: 海淀区): ").strip() - if not test_unit: - test_unit = "海淀区" - - print(f"\n测试配置:") - print(f" 文档URL: {doc_url}") - print(f" 测试人员: {test_unit}-{test_name}") - print() - - confirm = input("确认开始测试? (y/N): ").strip().lower() - if confirm != 'y': - print("测试已取消") - return - - # 运行测试 - try: - # 设置环境变量 - os.environ["KDOCS_DOC_URL"] = doc_url - - # 创建上传器 - uploader = OptimizedKdocsUploader(cache_ttl=300) # 5分钟缓存 - - # 设置日志回调 - def log_func(message: str): - print(f" [LOG] {message}") - - uploader.set_log_callback(log_func) - - # 启动 - print("\n▶️ 启动优化上传器...") - uploader.start() - time.sleep(1) - - # 测试缓存 - print("\n▶️ 测试缓存功能...") - print(" 说明: 第一次会搜索,第二次应该使用缓存") - - for i in range(2): - print(f"\n 第{i+1}次尝试:") - start_time = time.time() - - # 模拟上传 - success = uploader.upload_screenshot( - user_id=1, - account_id=f"test00{i}", - unit=test_unit, - name=test_name, - image_path="test.jpg" - ) - - end_time = time.time() - duration = end_time - start_time - - if success: - print(f" ✅ 任务提交成功 (耗时: {duration:.2f}秒)") - else: - print(f" ❌ 任务提交失败 (耗时: {duration:.2f}秒)") - - time.sleep(2) - - # 显示缓存统计 - print("\n📊 缓存统计:") - stats = uploader.get_cache_stats() - for key, value in stats.items(): - print(f" {key}: {value}") - - # 停止 - print("\n⏹️ 停止上传器...") - uploader.stop() - - print("\n✅ 测试完成") - print("\n提示:") - print(" - 查看日志了解详细操作") - print(" - 缓存功能可以显著提升速度") - print(" - 建议在实际使用前进行充分测试") - - except Exception as e: - print(f"\n❌ 测试失败: {str(e)}") - import traceback - traceback.print_exc() - - -def show_optimization_info(): - """显示优化说明""" - print("\n📚 优化说明文档") - print("=" * 70) - print() - - print("🎯 优化原理:") - print("-" * 70) - print("1. 智能缓存系统") - print(" - 缓存人员位置信息 (默认30分钟)") - print(" - 使用前验证缓存有效性") - print(" - 缓存失效时自动重新搜索") - print() - print("2. 快速定位算法") - print(" - 先检查常见行号 (66, 67, 68, 70, 75, ...)") - print(" - 再使用优化的搜索") - print(" - 减少尝试次数 (从50次降到10次)") - print() - print("3. 减少等待时间") - print(" - 上传等待: 2秒 → 0.8秒") - print(" - 导航等待: 0.6秒 → 0.2秒") - print(" - 点击等待: 1秒 → 0.3秒") - print() - print("4. 安全的只读验证") - print(" - 使用前验证位置有效性") - print(" - 每次都检查县区匹配") - print(" - 确保不会上传错位置") - print() - - print("⚙️ 可配置参数:") - print("-" * 70) - config_items = [ - ("KDOCS_CACHE_TTL", "缓存有效期 (秒)", "1800", "30分钟"), - ("KDOCS_FAST_GOTO_TIMEOUT_MS", "页面加载超时 (毫秒)", "10000", "10秒"), - ("KDOCS_NAVIGATION_WAIT", "导航等待 (秒)", "0.2", "200毫秒"), - ("KDOCS_CLICK_WAIT", "点击等待 (秒)", "0.3", "300毫秒"), - ("KDOCS_UPLOAD_WAIT", "上传等待 (秒)", "0.8", "800毫秒"), - ("KDOCS_SEARCH_ATTEMPTS", "搜索尝试次数", "10", "10次"), - ] - - for env_name, description, default, note in config_items: - print(f" {env_name}") - print(f" 说明: {description}") - print(f" 默认值: {default}") - print(f" 备注: {note}") - print() - - print("📈 性能预期:") - print("-" * 70) - print(" 优化前:") - print(" - 搜索时间: 5-15秒") - print(" - 上传等待: 2秒") - print(" - 总计: 8-20秒/任务") - print() - print(" 优化后:") - print(" - 缓存命中: 2-3秒 (90%场景)") - print(" - 快速搜索: 4-6秒 (8%场景)") - print(" - 传统搜索: 8-12秒 (2%场景)") - print(" - 平均: 3-5秒/任务") - print() - print(" 提升幅度: 60-80%") - print() - - print("[LOCK] 安全特性:") - print("-" * 70) - print(" 1. 单线程设计 - 无并发问题") - print(" 2. 缓存验证 - 每次使用前验证") - print(" 3. 单点操作 - 不进行批量修改") - print(" 4. 详细日志 - 所有操作可追溯") - print(" 5. 错误恢复 - 异常时自动回滚") - print() - - print("💡 使用建议:") - print("-" * 70) - print(" 1. 首次使用请使用UI测试工具") - print(" 2. 确保金山文档URL配置正确") - print(" 3. 使用测试图片进行验证") - print(" 4. 观察缓存命中率,适时调整TTL") - print(" 5. 如遇到问题,查看日志定位原因") - print() - - -def main(): - """主函数""" - print_banner() - - # 检查环境 - if not check_prerequisites(): - print("\n❌ 环境检查失败,请先解决上述问题") - return - - # 主循环 - while True: - choice = show_menu() - - if choice == '1': - run_ui_test() - elif choice == '2': - run_command_line_test() - elif choice == '3': - show_optimization_info() - elif choice == '4': - print("\n👋 感谢使用,再见!") - break - else: - print("\n❌ 无效选项,请重新选择") - print() - - print() - input("按Enter键继续...") - - -if __name__ == "__main__": - main() diff --git a/test_screenshot_functionality.py b/test_screenshot_functionality.py deleted file mode 100644 index e00418b..0000000 --- a/test_screenshot_functionality.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试截图功能的脚本 -验证wkhtmltoimage安装和截图API功能 -""" - -import os -import sys -import requests -import time - - -def test_wkhtmltoimage(): - """测试wkhtmltoimage命令行工具""" - print("--- 测试wkhtmltoimage命令行工具 ---") - - try: - import subprocess - - result = subprocess.run(["wkhtmltoimage", "--version"], capture_output=True, text=True, timeout=5) - if result.returncode == 0: - print(f"[OK] wkhtmltoimage已安装: {result.stdout.strip()}") - return True - else: - print("[FAIL] wkhtmltoimage命令执行失败") - return False - except Exception as e: - print(f"[FAIL] 测试wkhtmltoimage失败: {e}") - return False - - -def test_direct_screenshot(): - """测试直接截图功能""" - print("\n--- 测试直接截图功能 ---") - - try: - import subprocess - - # 创建截图目录 - os.makedirs("screenshots", exist_ok=True) - - # 截图本地应用 - cmd = [ - "wkhtmltoimage", - "--width", - "1920", - "--height", - "1080", - "--quality", - "95", - "--js-delay", - "3000", - "http://127.0.0.1:51233", - "screenshots/test_direct.png", - ] - - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - - if result.returncode == 0: - if os.path.exists("screenshots/test_direct.png"): - file_size = os.path.getsize("screenshots/test_direct.png") - print(f"[OK] 直接截图成功: screenshots/test_direct.png ({file_size} bytes)") - return True - else: - print("[FAIL] 截图文件未生成") - return False - else: - print(f"[FAIL] 直接截图失败: {result.stderr}") - return False - - except Exception as e: - print(f"[FAIL] 直接截图测试失败: {e}") - return False - - -def test_api_screenshot(): - """测试API截图功能""" - print("\n--- 测试API截图功能 ---") - - # 检查应用是否运行 - try: - response = requests.get("http://127.0.0.1:51233/health", timeout=5) - if response.status_code == 200: - print("[OK] 应用正在运行") - else: - print(f"[FAIL] 应用响应异常: {response.status_code}") - return False - except Exception as e: - print(f"[FAIL] 应用连接失败: {e}") - return False - - # 尝试访问截图相关的API - api_endpoints = ["/api/screenshots", "/yuyx/api/browser_pool/stats", "/yuyx/api/screenshots"] - - for endpoint in api_endpoints: - try: - response = requests.get(f"http://127.0.0.1:51233{endpoint}", timeout=5) - print(f"API {endpoint}: {response.status_code}") - - if response.status_code == 401: - print(f" [WARN] 需要认证 - 这是正常的") - elif response.status_code == 404: - print(f" [WARN] 端点不存在 - 需要检查路由配置") - elif response.status_code == 200: - print(f" [OK] API正常工作") - - except Exception as e: - print(f" [FAIL] API调用失败: {e}") - - return True - - -def check_logs(): - """检查应用日志中的截图相关信息""" - print("\n--- 检查应用日志 ---") - - log_file = "app_new.log" - if os.path.exists(log_file): - print(f"[OK] 发现应用日志: {log_file}") - - try: - with open(log_file, "r", encoding="utf-8", errors="ignore") as f: - lines = f.readlines() - - # 查找截图相关的日志 - screenshot_lines = [] - for i, line in enumerate(lines[-20:]): # 最后20行 - if any(keyword in line.lower() for keyword in ["截图", "screenshot", "wkhtmltoimage"]): - screenshot_lines.append(f"第{len(lines) - 20 + i + 1}行: {line.strip()}") - - if screenshot_lines: - print("发现截图相关日志:") - for line in screenshot_lines: - print(f" {line}") - else: - print("未发现截图相关日志") - - except Exception as e: - print(f"读取日志失败: {e}") - else: - print(f"[FAIL] 未找到应用日志: {log_file}") - - -def main(): - print("[TEST] 截图功能测试工具") - print("=" * 50) - - # 测试wkhtmltoimage - wkhtmltoimage_ok = test_wkhtmltoimage() - - # 测试直接截图 - if wkhtmltoimage_ok: - direct_ok = test_direct_screenshot() - else: - direct_ok = False - - # 测试API - api_ok = test_api_screenshot() - - # 检查日志 - check_logs() - - # 总结 - print("\n" + "=" * 50) - print("[STATS] 测试结果总结:") - print(f" wkhtmltoimage: {'[OK]' if wkhtmltoimage_ok else '[FAIL]'}") - print(f" 直接截图: {'[OK]' if direct_ok else '[FAIL]'}") - print(f" API连接: {'[OK]' if api_ok else '[FAIL]'}") - - if wkhtmltoimage_ok and direct_ok: - print("\n[SUCCESS] 截图功能基础测试通过!") - print("现在可以测试Web界面的截图功能了。") - print("\n下一步:") - print("1. 访问 http://127.0.0.1:51233/yuyx 登录管理员后台") - print("2. 使用admin/admin123登录") - print("3. 找到截图功能进行测试") - else: - print("\n[WARN] 截图功能存在问题,需要进一步调试") - - -if __name__ == "__main__": - main() diff --git a/test_sequential.py b/test_sequential.py deleted file mode 100644 index bbbc8cc..0000000 --- a/test_sequential.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -金山文档上传测试 - 顺序执行版本 -单线程顺序执行,最稳定 -""" - -import os -import sys -import time -from datetime import datetime - -# 添加项目路径 -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) - - -def log(message, level='INFO'): - """日志输出""" - timestamp = datetime.now().strftime("%H:%M:%S") - print(f"[{timestamp}] {level}: {message}") - - -def pause_for_user(): - """等待用户按回车""" - input("\n按Enter键继续...") - - -def main(): - """主函数 - 顺序执行所有测试""" - print("=" * 70) - print("[LOCK] 金山文档上传测试 - 顺序执行版本") - print("=" * 70) - print() - print("此工具将按顺序执行以下测试:") - print(" 1. 启动浏览器") - print(" 2. 打开金山文档") - print(" 3. 测试表格读取") - print(" 4. 测试人员搜索") - print(" 5. 测试图片上传(可选)") - print() - - # 获取配置 - doc_url = input("请输入金山文档URL (或按Enter使用默认): ").strip() - if not doc_url: - doc_url = "https://kdocs.cn/l/cpwEOo5ynKX4" - - print(f"\n使用URL: {doc_url}") - print() - - # 变量初始化 - playwright = None - browser = None - context = None - page = None - - try: - # ========== 测试1: 启动浏览器 ========== - log("=" * 50) - log("测试1: 启动浏览器") - log("=" * 50) - - log("正在启动Playwright...", 'INFO') - playwright = sync_playwright().start() - log("[OK] Playwright启动成功", 'SUCCESS') - - log("正在启动浏览器...", 'INFO') - browser = playwright.chromium.launch(headless=False) - log("[OK] 浏览器启动成功", 'SUCCESS') - - log("正在创建上下文...", 'INFO') - context = browser.new_context() - log("[OK] 上下文创建成功", 'SUCCESS') - - log("正在创建页面...", 'INFO') - page = context.new_page() - page.set_default_timeout(30000) - log("[OK] 页面创建成功", 'SUCCESS') - - print() - log("测试1完成 [OK]", 'SUCCESS') - pause_for_user() - - # ========== 测试2: 打开文档 ========== - log("=" * 50) - log("测试2: 打开金山文档") - log("=" * 50) - - log(f"正在导航到: {doc_url}", 'INFO') - page.goto(doc_url, wait_until='domcontentloaded') - log("[OK] 页面导航完成", 'SUCCESS') - - log("等待3秒让页面完全加载...", 'INFO') - time.sleep(3) - - current_url = page.url - log(f"当前URL: {current_url}", 'INFO') - - if "kdocs.cn" in current_url: - log("[OK] 已成功进入金山文档", 'SUCCESS') - else: - log("⚠ 当前不在金山文档域名", 'WARNING') - - # 检查登录状态 - try: - login_visible = page.locator("text=登录").first.is_visible() - if login_visible: - log("⚠ 检测到登录页面,可能需要扫码登录", 'WARNING') - else: - log("[OK] 未检测到登录提示", 'SUCCESS') - except: - pass - - print() - log("测试2完成 [OK]", 'SUCCESS') - pause_for_user() - - # ========== 测试3: 表格读取 ========== - log("=" * 50) - log("测试3: 表格读取测试") - log("=" * 50) - - # 尝试读取名称框 - try: - log("尝试定位名称框...", 'INFO') - name_box = page.locator("input.edit-box").first - if name_box.is_visible(): - value = name_box.input_value() - log(f"[OK] 名称框可见,当前值: '{value}'", 'SUCCESS') - else: - log("⚠ 名称框不可见", 'WARNING') - except Exception as e: - log(f"⚠ 读取名称框失败: {str(e)}", 'WARNING') - - # 查找表格元素 - try: - log("正在查找表格元素...", 'INFO') - canvas_count = page.locator("canvas").count() - log(f"[OK] 检测到 {canvas_count} 个canvas元素", 'SUCCESS') - except Exception as e: - log(f"⚠ 查找canvas失败: {str(e)}", 'WARNING') - - print() - log("测试3完成 [OK]", 'SUCCESS') - pause_for_user() - - # ========== 测试4: 人员搜索 ========== - log("=" * 50) - log("测试4: 人员搜索测试") - log("=" * 50) - - test_name = input("请输入要搜索的姓名 (默认: 张三): ").strip() - if not test_name: - test_name = "张三" - - log(f"搜索姓名: {test_name}", 'INFO') - - try: - log("打开搜索框 (Ctrl+F)...", 'INFO') - page.keyboard.press("Control+f") - time.sleep(0.5) - - log(f"输入搜索内容: {test_name}", 'INFO') - page.keyboard.type(test_name) - time.sleep(0.3) - - log("执行搜索 (Enter)...", 'INFO') - page.keyboard.press("Enter") - time.sleep(1) - - log("关闭搜索框 (Escape)...", 'INFO') - page.keyboard.press("Escape") - time.sleep(0.3) - - log("[OK] 人员搜索测试完成", 'SUCCESS') - log("请查看浏览器窗口,检查是否高亮显示了搜索结果", 'INFO') - - except Exception as e: - log(f"✗ 搜索测试失败: {str(e)}", 'ERROR') - - print() - log("测试4完成 [OK]", 'SUCCESS') - pause_for_user() - - # ========== 测试5: 图片上传(可选) ========== - log("=" * 50) - log("测试5: 图片上传测试") - log("=" * 50) - - print() - upload_test = input("是否进行图片上传测试? (y/N): ").strip().lower() - - if upload_test == 'y': - # 让用户选择图片 - from tkinter import filedialog - import tkinter as tk - - root = tk.Tk() - root.withdraw() # 隐藏主窗口 - - image_path = filedialog.askopenfilename( - title="选择测试图片", - filetypes=[("图片文件", "*.jpg *.jpeg *.png *.gif")] - ) - - root.destroy() - - if image_path: - log(f"选中的图片: {image_path}", 'INFO') - - try: - # 导航到D3单元格 - log("导航到 D3 单元格...", 'INFO') - name_box = page.locator("input.edit-box").first - name_box.click() - name_box.fill("D3") - name_box.press("Enter") - time.sleep(0.5) - - # 点击插入菜单 - log("点击插入按钮...", 'INFO') - insert_btn = page.locator("text=插入").first - insert_btn.click() - time.sleep(0.5) - - # 点击图片选项 - log("点击图片选项...", 'INFO') - image_btn = page.locator("text=图片").first - image_btn.click() - time.sleep(0.5) - - # 选择本地图片 - log("选择本地图片...", 'INFO') - local_option = page.locator("text=本地").first - local_option.click() - - # 上传文件 - log("上传文件...", 'INFO') - with page.expect_file_chooser() as fc_info: - pass - - file_chooser = fc_info.value - file_chooser.set_files(image_path) - - time.sleep(2) # 等待上传完成 - - log("[OK] 图片上传测试完成", 'SUCCESS') - log("请检查浏览器窗口,确认图片已上传到D3单元格", 'INFO') - - except Exception as e: - log(f"✗ 图片上传测试失败: {str(e)}", 'ERROR') - else: - log("未选择图片,跳过上传测试", 'WARNING') - else: - log("跳过图片上传测试", 'INFO') - - print() - log("测试5完成 [OK]", 'SUCCESS') - - # ========== 测试完成 ========== - log("=" * 70) - log("所有测试完成!", 'SUCCESS') - log("=" * 70) - print() - log("总结:", 'INFO') - log("1. [OK] 浏览器启动 - 成功", 'SUCCESS') - log("2. [OK] 文档打开 - 成功", 'SUCCESS') - log("3. [OK] 表格读取 - 成功", 'SUCCESS') - log("4. [OK] 人员搜索 - 成功", 'SUCCESS') - if upload_test == 'y': - log("5. [OK] 图片上传 - 已测试", 'SUCCESS') - else: - log("5. ⊝ 图片上传 - 已跳过", 'INFO') - print() - log("所有功能测试完成,浏览器窗口保持打开状态", 'INFO') - log("您可以手动关闭浏览器窗口来结束测试", 'INFO') - - except KeyboardInterrupt: - log("\n测试被用户中断", 'WARNING') - except Exception as e: - log(f"\n测试过程中出现错误: {str(e)}", 'ERROR') - import traceback - traceback.print_exc() - finally: - # 清理资源 - print("\n" + "=" * 70) - log("正在清理资源...", 'INFO') - print("=" * 70) - - try: - if page: - page.close() - log("[OK] 页面已关闭", 'SUCCESS') - except: - pass - - try: - if context: - context.close() - log("[OK] 上下文已关闭", 'SUCCESS') - except: - pass - - try: - if browser: - browser.close() - log("[OK] 浏览器已关闭", 'SUCCESS') - except: - pass - - try: - if playwright: - playwright.stop() - log("[OK] Playwright已停止", 'SUCCESS') - except: - pass - - log("资源清理完成", 'SUCCESS') - - -if __name__ == "__main__": - main() diff --git a/test_with_login.py b/test_with_login.py deleted file mode 100644 index 4540f84..0000000 --- a/test_with_login.py +++ /dev/null @@ -1,503 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -金山文档上传测试 - 支持登录版本 -集成扫码登录功能,支持完整的测试流程 -""" - -import os -import sys -import time -import base64 -from datetime import datetime -from io import BytesIO -from PIL import Image - -# 添加项目路径 -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) - - -def log(message, level='INFO'): - """日志输出""" - timestamp = datetime.now().strftime("%H:%M:%S") - print(f"[{timestamp}] {level}: {message}") - - -def pause(msg="按Enter键继续..."): - """等待用户按键""" - input(f"\n{msg}") - - -def ask_yes_no(question, default='n'): - """询问用户是/否问题""" - if default == 'y': - prompt = f"{question} (Y/n): " - else: - prompt = f"{question} (y/N): " - - answer = input(prompt).strip().lower() - if not answer: - answer = default - return answer == 'y' - - -def save_qr_code(qr_image_bytes, filename="qr_code.png"): - """保存二维码图片""" - try: - # 保存为PNG文件 - with open(filename, 'wb') as f: - f.write(qr_image_bytes) - log(f"[OK] 二维码已保存到: {filename}", 'SUCCESS') - return filename - except Exception as e: - log(f"✗ 保存二维码失败: {str(e)}", 'ERROR') - return None - - -def display_qr_info(): - """显示二维码信息""" - print("\n" + "=" * 70) - print("📱 扫码登录说明") - print("=" * 70) - print() - print("1. 请使用手机微信扫描二维码") - print("2. 在手机上点击'确认登录'") - print("3. 等待页面自动跳转到表格页面") - print("4. 如果二维码失效,请按 Ctrl+C 重新生成") - print() - print("登录完成后,请回到此窗口并按Enter键继续") - print("=" * 70) - - -def wait_for_login(page, timeout=120): - """等待用户完成登录""" - log(f"等待登录完成 (超时: {timeout}秒)...", 'INFO') - - start_time = time.time() - check_interval = 2 # 每2秒检查一次 - - while time.time() - start_time < timeout: - try: - # 检查当前URL - current_url = page.url - log(f"当前URL: {current_url}", 'INFO') - - # 如果已经进入文档页面,认为登录成功 - if "kdocs.cn" in current_url and "/spreadsheet/" in current_url: - log("[OK] 登录成功,已进入文档页面", 'SUCCESS') - return True - - # 检查是否还在登录页面 - if "login" in current_url.lower() or "account" in current_url.lower(): - log("仍在登录页面,请扫码登录...", 'INFO') - else: - log(f"页面状态变化: {current_url}", 'INFO') - - time.sleep(check_interval) - - except Exception as e: - log(f"检查登录状态时出错: {str(e)}", 'WARNING') - time.sleep(check_interval) - - log("登录超时", 'WARNING') - return False - - -def capture_qr_code(page): - """尝试捕获二维码""" - log("尝试捕获二维码...", 'INFO') - - try: - # 查找二维码元素 - qr_selectors = [ - "canvas", - "img[src*='qr']", - "img[alt*='二维码']", - "[class*='qr']", - "[id*='qr']", - "div[class*='qrcode']" - ] - - for selector in qr_selectors: - try: - elements = page.query_selector_all(selector) - for i, element in enumerate(elements): - try: - # 截图 - screenshot = element.screenshot() - if len(screenshot) > 1000: # 足够大的图片 - filename = f"qr_code_{selector.replace('[', '').replace(']', '').replace('*', '').replace('=', '').replace(' ', '_')}_{i}.png" - save_qr_code(screenshot, filename) - log(f"[OK] 找到二维码元素: {selector}[{i}]", 'SUCCESS') - return True - except Exception: - continue - except Exception: - continue - - # 备选:截取整个页面并查找二维码区域 - try: - screenshot = page.screenshot() - filename = "qr_code_fullpage.png" - save_qr_code(screenshot, filename) - log("[OK] 已截取整个页面,请查看页面中的二维码", 'SUCCESS') - log(f" 截图保存为: {filename}", 'INFO') - return True - except Exception as e: - log(f"截取页面失败: {str(e)}", 'ERROR') - - except Exception as e: - log(f"捕获二维码失败: {str(e)}", 'ERROR') - - return False - - -def main(): - """主函数""" - print("=" * 70) - print("[LOCK] 金山文档上传测试 - 支持登录版本") - print("=" * 70) - print() - print("特点:") - print(" [OK] 支持扫码登录") - print(" [OK] 完整的测试流程") - print(" [OK] 详细的操作指导") - print(" [OK] 自动等待登录完成") - print() - - # 配置 - doc_url = input("请输入金山文档URL (或按Enter使用默认): ").strip() - if not doc_url: - doc_url = "https://kdocs.cn/l/cpwEOo5ynKX4" - - print(f"\n使用URL: {doc_url}") - print() - - if not ask_yes_no("确认开始测试?"): - print("测试已取消") - return - - print("\n" + "=" * 70) - print("开始测试流程") - print("=" * 70) - - playwright = None - browser = None - context = None - page = None - - try: - # ===== 步骤1: 启动浏览器 ===== - print("\n" + "=" * 50) - print("步骤1: 启动浏览器") - print("=" * 50) - - log("正在启动Playwright...", 'INFO') - playwright = sync_playwright().start() - log("[OK] Playwright启动成功", 'SUCCESS') - - log("正在启动浏览器...", 'INFO') - browser = playwright.chromium.launch(headless=False) - log("[OK] 浏览器启动成功", 'SUCCESS') - - log("正在创建上下文...", 'INFO') - context = browser.new_context() - log("[OK] 上下文创建成功", 'SUCCESS') - - log("正在创建页面...", 'INFO') - page = context.new_page() - page.set_default_timeout(30000) - log("[OK] 页面创建成功", 'SUCCESS') - - pause("浏览器已启动,请观察浏览器窗口是否正常打开") - - # ===== 步骤2: 打开登录页面 ===== - print("\n" + "=" * 50) - print("步骤2: 打开登录页面") - print("=" * 50) - - log(f"正在导航到: {doc_url}", 'INFO') - page.goto(doc_url, wait_until='domcontentloaded') - log("[OK] 页面导航完成", 'SUCCESS') - - log("等待3秒让页面加载...", 'INFO') - time.sleep(3) - - current_url = page.url - log(f"当前URL: {current_url}", 'INFO') - - # ===== 步骤3: 处理登录 ===== - print("\n" + "=" * 50) - print("步骤3: 登录处理") - print("=" * 50) - - # 检查是否需要登录 - try: - login_visible = page.locator("text=登录").first.is_visible() - if login_visible: - log("[OK] 检测到登录页面", 'SUCCESS') - - # 尝试捕获二维码 - capture_qr_code(page) - - # 显示登录说明 - display_qr_info() - - # 等待用户登录 - if not wait_for_login(page, timeout=180): # 3分钟超时 - log("登录失败或超时", 'ERROR') - if ask_yes_no("是否要重新尝试?"): - log("请重新扫码登录...", 'INFO') - if wait_for_login(page, timeout=180): - log("[OK] 登录成功", 'SUCCESS') - else: - log("登录仍然失败", 'ERROR') - return - else: - log("[OK] 登录成功", 'SUCCESS') - - else: - log("[OK] 未检测到登录页面,可能已经登录", 'SUCCESS') - except Exception as e: - log(f"检查登录状态时出错: {str(e)}", 'WARNING') - - pause("登录处理完成,请确认是否已进入文档页面") - - # ===== 步骤4: 验证文档加载 ===== - print("\n" + "=" * 50) - print("步骤4: 验证文档加载") - print("=" * 50) - - current_url = page.url - log(f"当前URL: {current_url}", 'INFO') - - if "kdocs.cn" in current_url and "/spreadsheet/" in current_url: - log("[OK] 已成功进入金山文档表格", 'SUCCESS') - else: - log("⚠ 当前不在金山文档表格页面", 'WARNING') - log("请确认是否已正确登录", 'INFO') - - # 等待页面完全加载 - log("等待5秒让表格完全加载...", 'INFO') - time.sleep(5) - - # 检查表格元素 - try: - canvas_count = page.locator("canvas").count() - log(f"[OK] 检测到 {canvas_count} 个canvas元素", 'SUCCESS') - - if canvas_count > 0: - log("[OK] 表格元素正常加载", 'SUCCESS') - else: - log("⚠ 未检测到表格元素,可能页面还在加载", 'WARNING') - except Exception as e: - log(f"检查表格元素时出错: {str(e)}", 'WARNING') - - pause("文档验证完成,请确认表格是否正常显示") - - # ===== 步骤5: 表格读取测试 ===== - print("\n" + "=" * 50) - print("步骤5: 表格读取测试") - print("=" * 50) - - # 尝试读取名称框 - try: - log("尝试定位名称框...", 'INFO') - name_box = page.locator("input.edit-box").first - if name_box.is_visible(): - value = name_box.input_value() - log(f"[OK] 名称框可见,当前值: '{value}'", 'SUCCESS') - else: - log("⚠ 名称框不可见", 'WARNING') - except Exception as e: - log(f"读取名称框失败: {str(e)}", 'WARNING') - - # 尝试读取当前单元格 - try: - log("尝试读取当前单元格内容...", 'INFO') - # 尝试点击网格 - canvases = page.locator("canvas").all() - if canvases: - box = canvases[0].bounding_box() - if box: - page.mouse.click(box['x'] + box['width'] / 2, box['y'] + box['height'] / 2) - time.sleep(0.5) - log("[OK] 已点击网格", 'SUCCESS') - except Exception as e: - log(f"点击网格失败: {str(e)}", 'WARNING') - - pause("表格读取测试完成") - - # ===== 步骤6: 人员搜索测试 ===== - print("\n" + "=" * 50) - print("步骤6: 人员搜索测试") - print("=" * 50) - - test_name = input("请输入要搜索的姓名 (默认: 张三): ").strip() - if not test_name: - test_name = "张三" - - log(f"搜索姓名: {test_name}", 'INFO') - - try: - log("执行搜索操作...", 'INFO') - page.keyboard.press("Control+f") - time.sleep(0.5) - - page.keyboard.type(test_name) - time.sleep(0.3) - - page.keyboard.press("Enter") - time.sleep(1) - - page.keyboard.press("Escape") - time.sleep(0.3) - - log("[OK] 人员搜索测试完成", 'SUCCESS') - log("请查看浏览器窗口,检查是否高亮显示了搜索结果", 'INFO') - - except Exception as e: - log(f"✗ 搜索测试失败: {str(e)}", 'ERROR') - - pause("搜索测试完成") - - # ===== 步骤7: 图片上传测试 ===== - print("\n" + "=" * 50) - print("步骤7: 图片上传测试 (可选)") - print("=" * 50) - - if ask_yes_no("是否进行图片上传测试?"): - image_path = input("请输入测试图片的完整路径: ").strip() - - if not image_path or not os.path.exists(image_path): - log("图片文件不存在或路径无效,跳过上传测试", 'WARNING') - else: - log(f"选中的图片: {image_path}", 'INFO') - - try: - log("执行上传流程...", 'INFO') - - # 导航到D3单元格 - name_box = page.locator("input.edit-box").first - name_box.click() - name_box.fill("D3") - name_box.press("Enter") - time.sleep(0.5) - - log("[OK] 已导航到D3单元格") - - # 点击插入 - insert_btn = page.locator("text=插入").first - insert_btn.click() - time.sleep(0.5) - - log("[OK] 已点击插入按钮") - - # 点击图片 - image_btn = page.locator("text=图片").first - image_btn.click() - time.sleep(0.5) - - log("[OK] 已点击图片按钮") - - # 选择本地 - local_option = page.locator("text=本地").first - local_option.click() - - log("[OK] 已选择本地图片") - - # 上传文件 - with page.expect_file_chooser() as fc_info: - pass - - file_chooser = fc_info.value - file_chooser.set_files(image_path) - - log("[OK] 文件上传命令已发送") - - log("等待上传完成...", 'INFO') - time.sleep(3) - - log("[OK] 图片上传测试完成", 'SUCCESS') - log("请检查浏览器窗口,确认图片是否成功上传到D3单元格", 'INFO') - - except Exception as e: - log(f"✗ 图片上传测试失败: {str(e)}", 'ERROR') - import traceback - traceback.print_exc() - - pause("图片上传测试完成") - - # ===== 测试完成 ===== - print("\n" + "=" * 70) - log("🎉 所有测试完成!", 'SUCCESS') - print("=" * 70) - print() - print("测试结果汇总:") - print(" [[OK]] 浏览器启动") - print(" [[OK]] 文档打开") - print(" [[OK]] 登录处理") - print(" [[OK]] 文档加载验证") - print(" [[OK]] 表格读取") - print(" [[OK]] 人员搜索") - if ask_yes_no("是否执行了图片上传?"): - print(" [[OK]] 图片上传") - print() - print("浏览器窗口将保持打开状态") - print("您可以手动关闭浏览器窗口来结束测试") - - except KeyboardInterrupt: - print("\n") - log("测试被用户中断", 'WARNING') - except Exception as e: - print("\n") - log(f"测试过程中出现错误: {str(e)}", 'ERROR') - import traceback - traceback.print_exc() - finally: - # 清理资源 - print("\n" + "=" * 70) - print("清理资源...") - print("=" * 70) - - try: - if page: - page.close() - log("[OK] 页面已关闭", 'SUCCESS') - except: - pass - - try: - if context: - context.close() - log("[OK] 上下文已关闭", 'SUCCESS') - except: - pass - - try: - if browser: - browser.close() - log("[OK] 浏览器已关闭", 'SUCCESS') - except: - pass - - try: - if playwright: - playwright.stop() - log("[OK] Playwright已停止", 'SUCCESS') - except: - pass - - log("测试结束", 'SUCCESS') - print("=" * 70) - - -if __name__ == "__main__": - main()