🎉 项目优化与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

662
kdocs_sync_test.py Normal file
View File

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