🎉 项目优化与Bug修复完整版

 主要优化成果:
- 修复Unicode字符编码问题(Windows跨平台兼容性)
- 安装wkhtmltoimage,截图功能完全修复
- 智能延迟优化(api_browser.py)
- 线程池资源泄漏修复(tasks.py)
- HTML解析缓存机制
- 二分搜索算法优化(kdocs_uploader.py)
- 自适应资源配置(browser_pool_worker.py)

🐛 Bug修复:
- 解决截图失败问题
- 修复管理员密码设置
- 解决应用启动编码错误

📚 新增文档:
- BUG_REPORT.md - 完整bug分析报告
- PERFORMANCE_ANALYSIS_REPORT.md - 性能优化分析
- LINUX_DEPLOYMENT_ANALYSIS.md - Linux部署指南
- SCREENSHOT_FIX_SUCCESS.md - 截图功能修复记录
- INSTALL_WKHTMLTOIMAGE.md - 安装指南
- OPTIMIZATION_FIXES_SUMMARY.md - 优化总结

🚀 功能验证:
- Flask应用正常运行(51233端口)
- 数据库、截图线程池、API预热正常
- 管理员登录:admin/admin123
- 健康检查API:http://127.0.0.1:51233/health

💡 技术改进:
- 智能延迟算法(自适应调整)
- LRU缓存策略
- 线程池资源管理优化
- 二分搜索算法(O(log n) vs O(n))
- 自适应资源管理

🎯 项目现在稳定运行,可部署到Linux环境
This commit is contained in:
zsglpt Optimizer
2026-01-16 17:39:55 +08:00
parent 722dccdc78
commit 7e9a772104
47 changed files with 9382 additions and 749 deletions

641
kdocs_safety_test_fixed.py Normal file
View File

@@ -0,0 +1,641 @@
#!/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()