feat: 添加依赖自动检测与安装、选项记忆、KDocs登录优化
- 新增依赖检测模块:启动时自动检测wkhtmltoimage和Playwright Chromium - 新增依赖安装对话框:缺失时提示用户一键下载安装 - 修复选项记忆功能:浏览类型、自动截图、自动上传选项现在会保存 - 优化KDocs登录检测:未登录时自动切换到金山文档页面并显示二维码 - 简化日志输出:移除debug信息,保留用户友好的状态提示 - 新增账号变化信号:账号管理页面的修改会自动同步到浏览任务页面 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
22
config.py
22
config.py
@@ -79,6 +79,14 @@ class ZSGLConfig:
|
|||||||
index_url_pattern: str = "index.aspx"
|
index_url_pattern: str = "index.aspx"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TaskConfig:
|
||||||
|
"""浏览任务配置"""
|
||||||
|
browse_type: str = "应读" # 浏览类型
|
||||||
|
auto_screenshot: bool = True # 浏览后自动截图
|
||||||
|
auto_upload: bool = False # 截图后自动上传
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
"""应用总配置"""
|
"""应用总配置"""
|
||||||
@@ -87,6 +95,7 @@ class AppConfig:
|
|||||||
screenshot: ScreenshotConfig = field(default_factory=ScreenshotConfig)
|
screenshot: ScreenshotConfig = field(default_factory=ScreenshotConfig)
|
||||||
proxy: ProxyConfig = field(default_factory=ProxyConfig)
|
proxy: ProxyConfig = field(default_factory=ProxyConfig)
|
||||||
zsgl: ZSGLConfig = field(default_factory=ZSGLConfig)
|
zsgl: ZSGLConfig = field(default_factory=ZSGLConfig)
|
||||||
|
task: TaskConfig = field(default_factory=TaskConfig)
|
||||||
theme: str = "light" # light/dark
|
theme: str = "light" # light/dark
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
@@ -130,6 +139,11 @@ class AppConfig:
|
|||||||
"login_url": self.zsgl.login_url,
|
"login_url": self.zsgl.login_url,
|
||||||
"index_url_pattern": self.zsgl.index_url_pattern,
|
"index_url_pattern": self.zsgl.index_url_pattern,
|
||||||
},
|
},
|
||||||
|
"task": {
|
||||||
|
"browse_type": self.task.browse_type,
|
||||||
|
"auto_screenshot": self.task.auto_screenshot,
|
||||||
|
"auto_upload": self.task.auto_upload,
|
||||||
|
},
|
||||||
"theme": self.theme,
|
"theme": self.theme,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +205,14 @@ class AppConfig:
|
|||||||
index_url_pattern=zsgl_data.get("index_url_pattern", "index.aspx"),
|
index_url_pattern=zsgl_data.get("index_url_pattern", "index.aspx"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 加载任务配置
|
||||||
|
task_data = data.get("task", {})
|
||||||
|
config.task = TaskConfig(
|
||||||
|
browse_type=task_data.get("browse_type", "应读"),
|
||||||
|
auto_screenshot=task_data.get("auto_screenshot", True),
|
||||||
|
auto_upload=task_data.get("auto_upload", False),
|
||||||
|
)
|
||||||
|
|
||||||
# 主题
|
# 主题
|
||||||
config.theme = data.get("theme", "light")
|
config.theme = data.get("theme", "light")
|
||||||
|
|
||||||
|
|||||||
@@ -107,10 +107,10 @@ class APIBrowser:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
if attempt < max_retries:
|
if attempt < max_retries:
|
||||||
self.log(f"[API] 请求超时,{retry_delay}秒后重试 ({attempt}/{max_retries})...")
|
self.log(f" 请求超时,{retry_delay}秒后重试 ({attempt}/{max_retries})...")
|
||||||
time.sleep(retry_delay)
|
time.sleep(retry_delay)
|
||||||
else:
|
else:
|
||||||
self.log(f"[API] 请求失败,已重试{max_retries}次: {str(e)}")
|
self.log(f" 请求失败,已重试{max_retries}次: {str(e)}")
|
||||||
|
|
||||||
raise last_error
|
raise last_error
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ class APIBrowser:
|
|||||||
|
|
||||||
def login(self, username: str, password: str) -> bool:
|
def login(self, username: str, password: str) -> bool:
|
||||||
"""登录"""
|
"""登录"""
|
||||||
self.log(f"[API] 登录: {username}")
|
self.log(f" 登录: {username}")
|
||||||
self._username = username
|
self._username = username
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -152,17 +152,17 @@ class APIBrowser:
|
|||||||
|
|
||||||
if self.index_url_pattern in resp.url:
|
if self.index_url_pattern in resp.url:
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
self.log(f"[API] 登录成功")
|
self.log(f" 登录成功")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
soup = BeautifulSoup(resp.text, "html.parser")
|
soup = BeautifulSoup(resp.text, "html.parser")
|
||||||
error = soup.find(id="lblMsg")
|
error = soup.find(id="lblMsg")
|
||||||
error_msg = error.get_text().strip() if error else "未知错误"
|
error_msg = error.get_text().strip() if error else "未知错误"
|
||||||
self.log(f"[API] 登录失败: {error_msg}")
|
self.log(f" 登录失败: {error_msg}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"[API] 登录异常: {str(e)}")
|
self.log(f" 登录异常: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_real_name(self) -> Optional[str]:
|
def get_real_name(self) -> Optional[str]:
|
||||||
@@ -217,10 +217,10 @@ class APIBrowser:
|
|||||||
with open(cookies_path, "w", encoding="utf-8") as f:
|
with open(cookies_path, "w", encoding="utf-8") as f:
|
||||||
f.write("\n".join(lines) + "\n")
|
f.write("\n".join(lines) + "\n")
|
||||||
|
|
||||||
self.log(f"[API] Cookies已保存供截图使用")
|
self.log(f" Cookies已保存供截图使用")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"[API] 保存cookies失败: {e}")
|
self.log(f" 保存cookies失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_article_list_page(self, bz: int = 0, page: int = 1) -> tuple:
|
def get_article_list_page(self, bz: int = 0, page: int = 1) -> tuple:
|
||||||
@@ -377,7 +377,7 @@ class APIBrowser:
|
|||||||
# 根据浏览类型确定bz参数(网站更新后 bz=0 为应读)
|
# 根据浏览类型确定bz参数(网站更新后 bz=0 为应读)
|
||||||
bz = 0
|
bz = 0
|
||||||
|
|
||||||
self.log(f"[API] 开始浏览 '{browse_type}' (bz={bz})...")
|
self.log(f" 开始浏览 '{browse_type}' (bz={bz})...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
total_items = 0
|
total_items = 0
|
||||||
@@ -387,12 +387,12 @@ class APIBrowser:
|
|||||||
articles, total_pages, _ = self.get_article_list_page(bz, 1)
|
articles, total_pages, _ = self.get_article_list_page(bz, 1)
|
||||||
|
|
||||||
if not articles:
|
if not articles:
|
||||||
self.log(f"[API] '{browse_type}' 没有待处理内容")
|
self.log(f" '{browse_type}' 没有待处理内容")
|
||||||
result.success = True
|
result.success = True
|
||||||
return result
|
return result
|
||||||
|
|
||||||
total_records = self.last_total_records
|
total_records = self.last_total_records
|
||||||
self.log(f"[API] 共 {total_records} 条记录,开始处理...")
|
self.log(f" 共 {total_records} 条记录,开始处理...")
|
||||||
|
|
||||||
# 上报初始进度
|
# 上报初始进度
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
@@ -404,7 +404,7 @@ class APIBrowser:
|
|||||||
|
|
||||||
for iteration in range(max_iterations):
|
for iteration in range(max_iterations):
|
||||||
if should_stop_callback and should_stop_callback():
|
if should_stop_callback and should_stop_callback():
|
||||||
self.log("[API] 收到停止信号")
|
self.log(" 收到停止信号")
|
||||||
break
|
break
|
||||||
|
|
||||||
if not articles:
|
if not articles:
|
||||||
@@ -428,7 +428,7 @@ class APIBrowser:
|
|||||||
try:
|
try:
|
||||||
attachments, article_info = self.get_article_attachments(article_href)
|
attachments, article_info = self.get_article_attachments(article_href)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"[API] 获取文章失败: {title} | {str(e)}")
|
self.log(f" 获取文章失败: {title} | {str(e)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
total_items += 1
|
total_items += 1
|
||||||
@@ -446,10 +446,10 @@ class APIBrowser:
|
|||||||
for attach in attachments:
|
for attach in attachments:
|
||||||
if self.mark_attachment_read(attach["id"], attach["channel_id"]):
|
if self.mark_attachment_read(attach["id"], attach["channel_id"]):
|
||||||
total_attachments += 1
|
total_attachments += 1
|
||||||
self.log(f"[API] [{total_items}] {title} - {len(attachments)}个附件")
|
self.log(f" [{total_items}] {title} - {len(attachments)}个附件")
|
||||||
else:
|
else:
|
||||||
status = "已标记" if article_marked else "标记失败"
|
status = "已标记" if article_marked else "标记失败"
|
||||||
self.log(f"[API] [{total_items}] {title} - 无附件({status})")
|
self.log(f" [{total_items}] {title} - 无附件({status})")
|
||||||
|
|
||||||
# 上报进度
|
# 上报进度
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
@@ -472,10 +472,10 @@ class APIBrowser:
|
|||||||
if new_total_pages > 0:
|
if new_total_pages > 0:
|
||||||
total_pages = new_total_pages
|
total_pages = new_total_pages
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"[API] 获取第{current_page}页列表失败: {str(e)}")
|
self.log(f" 获取第{current_page}页列表失败: {str(e)}")
|
||||||
break
|
break
|
||||||
|
|
||||||
self.log(f"[API] 浏览完成: {total_items} 条内容,{total_attachments} 个附件")
|
self.log(f" 浏览完成: {total_items} 条内容,{total_attachments} 个附件")
|
||||||
result.success = True
|
result.success = True
|
||||||
result.total_items = total_items
|
result.total_items = total_items
|
||||||
result.total_attachments = total_attachments
|
result.total_attachments = total_attachments
|
||||||
@@ -483,7 +483,7 @@ class APIBrowser:
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result.error_message = str(e)
|
result.error_message = str(e)
|
||||||
self.log(f"[API] 浏览出错: {str(e)}")
|
self.log(f" 浏览出错: {str(e)}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|||||||
@@ -41,6 +41,40 @@ class KDocsUploader:
|
|||||||
if self._log_callback:
|
if self._log_callback:
|
||||||
self._log_callback(msg)
|
self._log_callback(msg)
|
||||||
|
|
||||||
|
def _find_visible_element(self, text: str, use_role: bool = False, role: str = "button"):
|
||||||
|
"""找到包含指定文本的可见元素
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: 要查找的文本
|
||||||
|
use_role: 是否使用role查找(默认False,使用文本查找)
|
||||||
|
role: 使用role查找时的角色类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
可见的元素,或None
|
||||||
|
"""
|
||||||
|
if not self._page:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if use_role:
|
||||||
|
els = self._page.get_by_role(role, name=text)
|
||||||
|
else:
|
||||||
|
els = self._page.locator(f"text={text}")
|
||||||
|
|
||||||
|
count = els.count()
|
||||||
|
for i in range(count):
|
||||||
|
el = els.nth(i)
|
||||||
|
try:
|
||||||
|
if el.is_visible(timeout=500):
|
||||||
|
box = el.bounding_box()
|
||||||
|
if box and box.get('width', 0) > 0 and box.get('height', 0) > 0:
|
||||||
|
return el
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
def _ensure_playwright(self, use_storage_state: bool = True) -> bool:
|
def _ensure_playwright(self, use_storage_state: bool = True) -> bool:
|
||||||
"""确保Playwright已启动"""
|
"""确保Playwright已启动"""
|
||||||
if sync_playwright is None:
|
if sync_playwright is None:
|
||||||
@@ -53,9 +87,8 @@ class KDocsUploader:
|
|||||||
if self._playwright is None:
|
if self._playwright is None:
|
||||||
self._playwright = sync_playwright().start()
|
self._playwright = sync_playwright().start()
|
||||||
if self._browser is None:
|
if self._browser is None:
|
||||||
# 调试模式:有头模式,方便查看浏览器行为
|
# 默认无头模式,设置环境变量 KDOCS_HEADLESS=false 可切换为有头模式调试
|
||||||
# 生产环境改回 "true"
|
headless = os.environ.get("KDOCS_HEADLESS", "true").lower() != "false"
|
||||||
headless = os.environ.get("KDOCS_HEADLESS", "false").lower() != "false"
|
|
||||||
# 使用系统安装的Chrome浏览器(支持微信快捷登录)
|
# 使用系统安装的Chrome浏览器(支持微信快捷登录)
|
||||||
# channel='chrome' 会使用系统Chrome,而不是Playwright自带的Chromium
|
# channel='chrome' 会使用系统Chrome,而不是Playwright自带的Chromium
|
||||||
chrome_args = [
|
chrome_args = [
|
||||||
@@ -69,10 +102,8 @@ class KDocsUploader:
|
|||||||
channel='chrome', # 使用系统Chrome
|
channel='chrome', # 使用系统Chrome
|
||||||
args=chrome_args
|
args=chrome_args
|
||||||
)
|
)
|
||||||
self.log("[KDocs] 使用系统Chrome浏览器")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 如果系统没有Chrome,回退到Chromium
|
# 如果系统没有Chrome,回退到Chromium
|
||||||
self.log(f"[KDocs] 系统Chrome不可用({e}),使用Chromium")
|
|
||||||
self._browser = self._playwright.chromium.launch(headless=headless, args=chrome_args)
|
self._browser = self._playwright.chromium.launch(headless=headless, args=chrome_args)
|
||||||
if self._context is None:
|
if self._context is None:
|
||||||
storage_state = str(KDOCS_LOGIN_STATE_FILE)
|
storage_state = str(KDOCS_LOGIN_STATE_FILE)
|
||||||
@@ -162,7 +193,6 @@ class KDocsUploader:
|
|||||||
try:
|
try:
|
||||||
join_btn = page.get_by_role("button", name="登录并加入编辑")
|
join_btn = page.get_by_role("button", name="登录并加入编辑")
|
||||||
if join_btn.count() > 0 and join_btn.first.is_visible(timeout=500):
|
if join_btn.count() > 0 and join_btn.first.is_visible(timeout=500):
|
||||||
self.log("[KDocs] 点击加入编辑按钮")
|
|
||||||
join_btn.first.click()
|
join_btn.first.click()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -172,7 +202,6 @@ class KDocsUploader:
|
|||||||
|
|
||||||
# 检查是否在登录页面
|
# 检查是否在登录页面
|
||||||
if self._is_login_url(url):
|
if self._is_login_url(url):
|
||||||
self.log(f"[KDocs] 检测到登录页面URL: {url}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 只检查登录页面上的登录按钮(排除文档页面的邀请对话框)
|
# 只检查登录页面上的登录按钮(排除文档页面的邀请对话框)
|
||||||
@@ -181,7 +210,6 @@ class KDocsUploader:
|
|||||||
try:
|
try:
|
||||||
btn = page.get_by_role("button", name=text)
|
btn = page.get_by_role("button", name=text)
|
||||||
if btn.count() > 0 and btn.first.is_visible(timeout=500):
|
if btn.count() > 0 and btn.first.is_visible(timeout=500):
|
||||||
self.log(f"[KDocs] 检测到登录按钮: {text}")
|
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -198,7 +226,6 @@ class KDocsUploader:
|
|||||||
if el.is_visible(timeout=200):
|
if el.is_visible(timeout=200):
|
||||||
box = el.bounding_box()
|
box = el.bounding_box()
|
||||||
if box and 80 <= box.get("width", 0) <= 400:
|
if box and 80 <= box.get("width", 0) <= 400:
|
||||||
self.log(f"[KDocs] 检测到二维码元素: {selector}")
|
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -220,120 +247,60 @@ class KDocsUploader:
|
|||||||
storage_state = str(KDOCS_LOGIN_STATE_FILE)
|
storage_state = str(KDOCS_LOGIN_STATE_FILE)
|
||||||
KDOCS_LOGIN_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
KDOCS_LOGIN_STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
self._context.storage_state(path=storage_state)
|
self._context.storage_state(path=storage_state)
|
||||||
self.log("[KDocs] 登录状态已保存")
|
except Exception:
|
||||||
except Exception as e:
|
pass
|
||||||
self.log(f"[KDocs] 保存登录状态失败: {e}")
|
|
||||||
|
|
||||||
def _ensure_login_dialog(self, use_quick_login: bool = False):
|
def _ensure_login_dialog(self, use_quick_login: bool = False):
|
||||||
"""确保打开登录对话框
|
"""确保打开登录对话框并进入扫码页面"""
|
||||||
|
buttons_priority = [
|
||||||
|
"登录并加入编辑",
|
||||||
|
"立即登录",
|
||||||
|
"去登录",
|
||||||
|
]
|
||||||
|
|
||||||
Args:
|
max_clicks = 12
|
||||||
use_quick_login: 是否尝试使用微信快捷登录
|
for _ in range(max_clicks):
|
||||||
"""
|
|
||||||
agree_names = ["同意", "同意并继续", "我同意", "确定", "确认"]
|
|
||||||
|
|
||||||
# 循环处理登录流程
|
|
||||||
max_clicks = 8
|
|
||||||
for round_num in range(max_clicks):
|
|
||||||
clicked = False
|
clicked = False
|
||||||
current_url = self._page.url
|
current_url = self._page.url
|
||||||
|
|
||||||
# 检查是否已经到达文档页面(登录成功)
|
# 检查是否已经到达文档页面(登录成功)
|
||||||
# 需要确保不是临时跳转,等待页面稳定
|
if "kdocs.cn/l/" in current_url and "account.wps.cn" not in current_url:
|
||||||
if "kdocs.cn/l/" in current_url or "www.kdocs.cn/l/" in current_url:
|
|
||||||
time.sleep(1) # 等待页面稳定
|
|
||||||
stable_url = self._page.url
|
|
||||||
if "kdocs.cn/l/" in stable_url and "account.wps.cn" not in stable_url:
|
|
||||||
self.log("[KDocs] 已到达文档页面,登录成功")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 1. 先检查是否有隐私协议同意按钮
|
|
||||||
for name in agree_names:
|
|
||||||
try:
|
|
||||||
btn = self._page.get_by_role("button", name=name)
|
|
||||||
if btn.count() > 0 and btn.first.is_visible(timeout=300):
|
|
||||||
self.log(f"[KDocs] 点击同意按钮: {name}")
|
|
||||||
btn.first.click()
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
clicked = True
|
has_login_elements = (
|
||||||
break
|
self._find_visible_element("立即登录", use_role=True) or
|
||||||
except Exception:
|
self._find_visible_element("登录并加入编辑", use_role=True) or
|
||||||
pass
|
self._find_visible_element("微信扫码登录") or
|
||||||
if clicked:
|
self._find_visible_element("微信快捷登录")
|
||||||
continue
|
)
|
||||||
|
if not has_login_elements:
|
||||||
# 2. 如果启用快捷登录且在登录页面(account.wps.cn),尝试点击"微信快捷登录"
|
|
||||||
if use_quick_login and "account.wps.cn" in current_url:
|
|
||||||
try:
|
|
||||||
quick_login = self._page.get_by_text("微信快捷登录", exact=False)
|
|
||||||
if quick_login.count() > 0 and quick_login.first.is_visible(timeout=500):
|
|
||||||
self.log("[KDocs] 点击微信快捷登录")
|
|
||||||
quick_login.first.click()
|
|
||||||
time.sleep(3) # 等待快捷登录处理
|
|
||||||
# 检查是否登录成功
|
|
||||||
if "kdocs.cn/l/" in self._page.url:
|
|
||||||
self.log("[KDocs] 微信快捷登录成功")
|
|
||||||
return
|
return
|
||||||
clicked = True
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 3. 点击"立即登录"进入登录页面
|
# 检查是否已经到达登录二维码页面
|
||||||
try:
|
qr_page_indicators = ["微信扫码登录", "微信快捷登录"]
|
||||||
btn = self._page.get_by_role("button", name="立即登录")
|
for indicator in qr_page_indicators:
|
||||||
if btn.count() > 0 and btn.first.is_visible(timeout=500):
|
if self._find_visible_element(indicator):
|
||||||
self.log("[KDocs] 点击立即登录")
|
return
|
||||||
btn.first.click()
|
|
||||||
|
# 按优先级点击登录按钮
|
||||||
|
for btn_name in buttons_priority:
|
||||||
|
el = self._find_visible_element(btn_name, use_role=True)
|
||||||
|
if el:
|
||||||
|
el.click(force=True)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
clicked = True
|
clicked = True
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 4. 点击"登录并加入编辑"(文档页面的邀请对话框)
|
|
||||||
try:
|
|
||||||
btn = self._page.get_by_role("button", name="登录并加入编辑")
|
|
||||||
if btn.count() > 0 and btn.first.is_visible(timeout=500):
|
|
||||||
self.log("[KDocs] 点击登录并加入编辑")
|
|
||||||
btn.first.click()
|
|
||||||
time.sleep(1.5)
|
|
||||||
clicked = True
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 如果没有点击到任何按钮,退出循环
|
|
||||||
if not clicked:
|
|
||||||
self.log("[KDocs] 未找到更多可点击的按钮")
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# 最后确保点击微信扫码登录(切换到扫码模式)
|
if not clicked:
|
||||||
wechat_names = ["微信登录", "微信扫码登录", "扫码登录", "微信扫码"]
|
for btn_name in buttons_priority:
|
||||||
for name in wechat_names:
|
el = self._find_visible_element(btn_name)
|
||||||
try:
|
if el:
|
||||||
btn = self._page.get_by_role("button", name=name)
|
el.click(force=True)
|
||||||
if btn.is_visible(timeout=1000):
|
time.sleep(2)
|
||||||
self.log(f"[KDocs] 点击微信登录: {name}")
|
clicked = True
|
||||||
btn.click()
|
break
|
||||||
time.sleep(1)
|
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 尝试用文本查找微信登录
|
if not clicked:
|
||||||
for name in wechat_names:
|
|
||||||
try:
|
|
||||||
el = self._page.get_by_text(name, exact=False).first
|
|
||||||
if el.is_visible(timeout=500):
|
|
||||||
self.log(f"[KDocs] 点击微信登录文本: {name}")
|
|
||||||
el.click()
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
return
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.log("[KDocs] 未找到登录按钮,可能页面已在登录状态或需要手动操作")
|
|
||||||
|
|
||||||
def _capture_qr_image(self) -> Optional[bytes]:
|
def _capture_qr_image(self) -> Optional[bytes]:
|
||||||
"""捕获登录二维码图片"""
|
"""捕获登录二维码图片"""
|
||||||
@@ -436,31 +403,24 @@ class KDocsUploader:
|
|||||||
return {"success": False, "error": self._last_error or "打开文档失败"}
|
return {"success": False, "error": self._last_error or "打开文档失败"}
|
||||||
|
|
||||||
# 检查是否已登录
|
# 检查是否已登录
|
||||||
self.log(f"[KDocs] 当前页面URL: {self._page.url}")
|
|
||||||
if not force and self._is_logged_in():
|
if not force and self._is_logged_in():
|
||||||
self._logged_in = True
|
self._logged_in = True
|
||||||
self._save_login_state()
|
self._save_login_state()
|
||||||
return {"success": True, "logged_in": True, "qr_image": ""}
|
return {"success": True, "logged_in": True, "qr_image": ""}
|
||||||
|
|
||||||
# 需要登录,获取二维码
|
# 需要登录,获取二维码
|
||||||
self.log("[KDocs] 需要登录,尝试打开登录对话框...")
|
|
||||||
self._ensure_login_dialog()
|
self._ensure_login_dialog()
|
||||||
time.sleep(2) # 等待登录对话框加载
|
time.sleep(2)
|
||||||
|
|
||||||
self.log("[KDocs] 尝试捕获二维码...")
|
|
||||||
qr_image = None
|
qr_image = None
|
||||||
for i in range(15): # 增加尝试次数
|
for _ in range(15):
|
||||||
qr_image = self._capture_qr_image()
|
qr_image = self._capture_qr_image()
|
||||||
if qr_image and len(qr_image) > 1024:
|
if qr_image and len(qr_image) > 1024:
|
||||||
self.log(f"[KDocs] 二维码捕获成功,大小: {len(qr_image)} bytes")
|
|
||||||
break
|
break
|
||||||
self.log(f"[KDocs] 第{i+1}次尝试捕获二维码...")
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
if not qr_image:
|
if not qr_image:
|
||||||
# 尝试截取整个页面帮助调试
|
return {"success": False, "error": "二维码获取失败,请检查网络"}
|
||||||
self.log("[KDocs] 二维码捕获失败,当前页面可能没有显示二维码")
|
|
||||||
return {"success": False, "error": "二维码获取失败,请检查网络或手动打开金山文档链接确认"}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -490,7 +450,6 @@ class KDocsUploader:
|
|||||||
try:
|
try:
|
||||||
confirm_btn = frame.get_by_role("button", name=name)
|
confirm_btn = frame.get_by_role("button", name=name)
|
||||||
if confirm_btn.count() > 0 and confirm_btn.first.is_visible(timeout=200):
|
if confirm_btn.count() > 0 and confirm_btn.first.is_visible(timeout=200):
|
||||||
self.log(f"[KDocs] 找到确认按钮: {name}")
|
|
||||||
confirm_btn.first.click()
|
confirm_btn.first.click()
|
||||||
clicked_confirm = True
|
clicked_confirm = True
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
@@ -504,7 +463,6 @@ class KDocsUploader:
|
|||||||
try:
|
try:
|
||||||
el = frame.get_by_text(name, exact=True)
|
el = frame.get_by_text(name, exact=True)
|
||||||
if el.count() > 0 and el.first.is_visible(timeout=200):
|
if el.count() > 0 and el.first.is_visible(timeout=200):
|
||||||
self.log(f"[KDocs] 找到确认文本: {name}")
|
|
||||||
el.first.click()
|
el.first.click()
|
||||||
clicked_confirm = True
|
clicked_confirm = True
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
@@ -515,7 +473,6 @@ class KDocsUploader:
|
|||||||
# 尝试用CSS选择器查找
|
# 尝试用CSS选择器查找
|
||||||
if not clicked_confirm:
|
if not clicked_confirm:
|
||||||
try:
|
try:
|
||||||
# WPS登录页面的确认按钮可能的选择器
|
|
||||||
selectors = [
|
selectors = [
|
||||||
"button.ant-btn-primary",
|
"button.ant-btn-primary",
|
||||||
"button[type='primary']",
|
"button[type='primary']",
|
||||||
@@ -537,7 +494,6 @@ class KDocsUploader:
|
|||||||
if btn.is_visible(timeout=100):
|
if btn.is_visible(timeout=100):
|
||||||
btn_text = btn.inner_text() or ""
|
btn_text = btn.inner_text() or ""
|
||||||
if any(kw in btn_text for kw in ["确认", "登录", "确定"]):
|
if any(kw in btn_text for kw in ["确认", "登录", "确定"]):
|
||||||
self.log(f"[KDocs] 找到按钮(CSS): {btn_text}")
|
|
||||||
btn.click()
|
btn.click()
|
||||||
clicked_confirm = True
|
clicked_confirm = True
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
@@ -549,37 +505,28 @@ class KDocsUploader:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 如果点击了确认按钮,等待页面自动跳转(不要reload!)
|
|
||||||
if clicked_confirm:
|
if clicked_confirm:
|
||||||
self.log("[KDocs] 已点击确认,等待页面跳转...")
|
time.sleep(3)
|
||||||
time.sleep(3) # 等待页面自动跳转
|
|
||||||
|
|
||||||
# 检查当前URL是否已经到达文档页面
|
|
||||||
current_url = self._page.url
|
current_url = self._page.url
|
||||||
self.log(f"[KDocs] 当前URL: {current_url}")
|
|
||||||
|
|
||||||
# 直接检查URL判断是否已登录
|
# 直接检查URL判断是否已登录
|
||||||
if "kdocs.cn/l/" in current_url and "account.wps.cn" not in current_url:
|
if "kdocs.cn/l/" in current_url and "account.wps.cn" not in current_url:
|
||||||
# 已到达文档页面,登录成功
|
|
||||||
logged_in = True
|
logged_in = True
|
||||||
# 尝试点击可能存在的"加入编辑"按钮
|
|
||||||
try:
|
try:
|
||||||
join_btn = self._page.get_by_role("button", name="登录并加入编辑")
|
join_btn = self._page.get_by_role("button", name="登录并加入编辑")
|
||||||
if join_btn.count() > 0 and join_btn.first.is_visible(timeout=500):
|
if join_btn.count() > 0 and join_btn.first.is_visible(timeout=500):
|
||||||
self.log("[KDocs] 点击加入编辑")
|
|
||||||
join_btn.first.click()
|
join_btn.first.click()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# 还在登录页面或其他页面
|
|
||||||
logged_in = self._is_logged_in()
|
logged_in = self._is_logged_in()
|
||||||
|
|
||||||
self._logged_in = logged_in
|
self._logged_in = logged_in
|
||||||
|
|
||||||
if logged_in:
|
if logged_in:
|
||||||
self._save_login_state()
|
self._save_login_state()
|
||||||
self.log("[KDocs] 登录状态检测:已登录")
|
|
||||||
|
|
||||||
return {"success": True, "logged_in": logged_in}
|
return {"success": True, "logged_in": logged_in}
|
||||||
|
|
||||||
@@ -701,7 +648,6 @@ class KDocsUploader:
|
|||||||
file_chooser.set_files(image_path)
|
file_chooser.set_files(image_path)
|
||||||
|
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
self.log(f"[KDocs] 图片已上传到 {cell_address}")
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -764,7 +710,6 @@ class KDocsUploader:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# 搜索姓名找到行
|
# 搜索姓名找到行
|
||||||
self.log(f"[KDocs] 搜索人员: {name}")
|
|
||||||
row_num = self._search_and_get_row(
|
row_num = self._search_and_get_row(
|
||||||
name,
|
name,
|
||||||
expected_col=kdocs_config.name_column,
|
expected_col=kdocs_config.name_column,
|
||||||
@@ -775,8 +720,6 @@ class KDocsUploader:
|
|||||||
if row_num < 0:
|
if row_num < 0:
|
||||||
return {"success": False, "error": f"未找到人员: {name}"}
|
return {"success": False, "error": f"未找到人员: {name}"}
|
||||||
|
|
||||||
self.log(f"[KDocs] 找到人员在第 {row_num} 行")
|
|
||||||
|
|
||||||
# 上传图片
|
# 上传图片
|
||||||
if self._upload_image_to_cell(row_num, image_path, kdocs_config.image_column):
|
if self._upload_image_to_cell(row_num, image_path, kdocs_config.image_column):
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ def take_screenshot(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if success and os.path.exists(screenshot_path) and os.path.getsize(screenshot_path) > 1000:
|
if success and os.path.exists(screenshot_path) and os.path.getsize(screenshot_path) > 1000:
|
||||||
log(f"[OK] 截图成功: {screenshot_filename}")
|
|
||||||
result.success = True
|
result.success = True
|
||||||
result.filename = screenshot_filename
|
result.filename = screenshot_filename
|
||||||
result.filepath = screenshot_path
|
result.filepath = screenshot_path
|
||||||
|
|||||||
54
main.py
54
main.py
@@ -55,32 +55,44 @@ def main():
|
|||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
|
|
||||||
# Check for JSON to SQLite migration
|
# Check for JSON to SQLite migration (silent)
|
||||||
from config import CONFIG_FILE
|
from config import CONFIG_FILE
|
||||||
if CONFIG_FILE.exists():
|
if CONFIG_FILE.exists():
|
||||||
from utils.storage import migrate_from_json
|
from utils.storage import migrate_from_json
|
||||||
window.log("检测到旧JSON配置,正在迁移到SQLite...")
|
migrate_from_json()
|
||||||
if migrate_from_json():
|
|
||||||
window.log("✅ 配置迁移成功")
|
# 简洁启动日志
|
||||||
|
window.log("✅ 应用启动成功")
|
||||||
|
window.log("✅ 数据加载成功")
|
||||||
|
|
||||||
|
# 检查依赖(wkhtmltoimage和Playwright Chromium)
|
||||||
|
from utils.dependency_installer import get_missing_dependencies
|
||||||
|
missing_deps = get_missing_dependencies()
|
||||||
|
|
||||||
|
# 如果有缺失的依赖,显示安装对话框
|
||||||
|
has_missing = any(missing_deps.values())
|
||||||
|
if has_missing:
|
||||||
|
missing_names = []
|
||||||
|
if missing_deps.get("wkhtmltoimage"):
|
||||||
|
missing_names.append("wkhtmltoimage(截图功能)")
|
||||||
|
if missing_deps.get("chromium"):
|
||||||
|
missing_names.append("Chromium(金山文档上传)")
|
||||||
|
window.log(f"⚠️ 缺少运行环境: {', '.join(missing_names)}")
|
||||||
|
|
||||||
|
# 显示安装对话框
|
||||||
|
from ui.dependency_dialog import DependencyDialog
|
||||||
|
dialog = DependencyDialog(missing_deps, window)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
# 重新检查
|
||||||
|
missing_deps = get_missing_dependencies()
|
||||||
|
if not missing_deps.get("wkhtmltoimage") and not missing_deps.get("chromium"):
|
||||||
|
window.log("✅ 运行环境已就绪")
|
||||||
else:
|
else:
|
||||||
window.log("⚠️ 配置迁移失败,将使用默认配置")
|
window.log("✅ 运行环境已就绪")
|
||||||
|
|
||||||
# 启动日志
|
# 启动时自动检测金山文档登录状态(后台无头模式)
|
||||||
window.log("应用启动成功")
|
window.init_kdocs_login_check()
|
||||||
window.log(f"数据目录: {os.path.abspath('data')}")
|
|
||||||
|
|
||||||
# Show database info
|
|
||||||
from utils.storage import _get_db_path
|
|
||||||
db_path = _get_db_path()
|
|
||||||
window.log(f"数据库: {db_path}")
|
|
||||||
|
|
||||||
# 检查wkhtmltoimage
|
|
||||||
from core.screenshot import _resolve_wkhtmltoimage_path
|
|
||||||
wkhtml = _resolve_wkhtmltoimage_path()
|
|
||||||
if wkhtml:
|
|
||||||
window.log(f"wkhtmltoimage: {wkhtml}")
|
|
||||||
else:
|
|
||||||
window.log("⚠️ 警告: 未找到 wkhtmltoimage,截图功能可能不可用")
|
|
||||||
|
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ class AccountWidget(QWidget):
|
|||||||
"""Account management panel"""
|
"""Account management panel"""
|
||||||
|
|
||||||
log_signal = pyqtSignal(str)
|
log_signal = pyqtSignal(str)
|
||||||
|
accounts_changed = pyqtSignal() # 账号列表变化时发出通知
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
@@ -315,6 +316,7 @@ class AccountWidget(QWidget):
|
|||||||
save_config(config)
|
save_config(config)
|
||||||
self._load_accounts()
|
self._load_accounts()
|
||||||
self.log_signal.emit(f"账号 {account.username} 添加成功")
|
self.log_signal.emit(f"账号 {account.username} 添加成功")
|
||||||
|
self.accounts_changed.emit() # 通知其他模块账号列表已更新
|
||||||
|
|
||||||
def _edit_account(self, row: int):
|
def _edit_account(self, row: int):
|
||||||
"""Edit account"""
|
"""Edit account"""
|
||||||
@@ -326,6 +328,7 @@ class AccountWidget(QWidget):
|
|||||||
save_config(config)
|
save_config(config)
|
||||||
self._load_accounts()
|
self._load_accounts()
|
||||||
self.log_signal.emit(f"账号 {account.username} 已更新")
|
self.log_signal.emit(f"账号 {account.username} 已更新")
|
||||||
|
self.accounts_changed.emit() # 通知其他模块账号列表已更新
|
||||||
|
|
||||||
def _delete_account(self, row: int):
|
def _delete_account(self, row: int):
|
||||||
"""Delete account"""
|
"""Delete account"""
|
||||||
@@ -342,6 +345,7 @@ class AccountWidget(QWidget):
|
|||||||
save_config(config)
|
save_config(config)
|
||||||
self._load_accounts()
|
self._load_accounts()
|
||||||
self.log_signal.emit(f"账号 {account.username} 已删除")
|
self.log_signal.emit(f"账号 {account.username} 已删除")
|
||||||
|
self.accounts_changed.emit() # 通知其他模块账号列表已更新
|
||||||
|
|
||||||
def _toggle_enabled(self, row: int, state: int):
|
def _toggle_enabled(self, row: int, state: int):
|
||||||
"""Toggle account enabled state"""
|
"""Toggle account enabled state"""
|
||||||
@@ -349,6 +353,7 @@ class AccountWidget(QWidget):
|
|||||||
if row < len(config.accounts):
|
if row < len(config.accounts):
|
||||||
config.accounts[row].enabled = state == Qt.CheckState.Checked.value
|
config.accounts[row].enabled = state == Qt.CheckState.Checked.value
|
||||||
save_config(config)
|
save_config(config)
|
||||||
|
self.accounts_changed.emit() # 通知其他模块账号启用状态已更新
|
||||||
|
|
||||||
def _test_login(self):
|
def _test_login(self):
|
||||||
"""Test login for selected account"""
|
"""Test login for selected account"""
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
||||||
|
|
||||||
from config import get_config, AccountConfig
|
from config import get_config, save_config, AccountConfig
|
||||||
from utils.crypto import decrypt_password, is_encrypted
|
from utils.crypto import decrypt_password, is_encrypted
|
||||||
from .constants import (
|
from .constants import (
|
||||||
PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE,
|
PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE,
|
||||||
@@ -33,6 +33,7 @@ class BrowseWidget(QWidget):
|
|||||||
self._worker = None
|
self._worker = None
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
|
self._load_task_config()
|
||||||
self._refresh_accounts()
|
self._refresh_accounts()
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
@@ -125,16 +126,19 @@ class BrowseWidget(QWidget):
|
|||||||
self.browse_type_combo.setMinimumWidth(140)
|
self.browse_type_combo.setMinimumWidth(140)
|
||||||
self.browse_type_combo.setFixedHeight(32)
|
self.browse_type_combo.setFixedHeight(32)
|
||||||
self.browse_type_combo.setStyleSheet("QComboBox { padding-left: 10px; }")
|
self.browse_type_combo.setStyleSheet("QComboBox { padding-left: 10px; }")
|
||||||
|
self.browse_type_combo.currentTextChanged.connect(self._save_task_config)
|
||||||
options_layout.addWidget(self.browse_type_combo)
|
options_layout.addWidget(self.browse_type_combo)
|
||||||
|
|
||||||
options_layout.addSpacing(20)
|
options_layout.addSpacing(20)
|
||||||
|
|
||||||
self.auto_screenshot_check = QCheckBox("浏览后自动截图")
|
self.auto_screenshot_check = QCheckBox("浏览后自动截图")
|
||||||
self.auto_screenshot_check.setChecked(True)
|
self.auto_screenshot_check.setChecked(True)
|
||||||
|
self.auto_screenshot_check.stateChanged.connect(self._save_task_config)
|
||||||
options_layout.addWidget(self.auto_screenshot_check)
|
options_layout.addWidget(self.auto_screenshot_check)
|
||||||
|
|
||||||
self.auto_upload_check = QCheckBox("截图后自动上传")
|
self.auto_upload_check = QCheckBox("截图后自动上传")
|
||||||
self.auto_upload_check.setChecked(False)
|
self.auto_upload_check.setChecked(False)
|
||||||
|
self.auto_upload_check.stateChanged.connect(self._save_task_config)
|
||||||
options_layout.addWidget(self.auto_upload_check)
|
options_layout.addWidget(self.auto_upload_check)
|
||||||
|
|
||||||
options_layout.addStretch()
|
options_layout.addStretch()
|
||||||
@@ -241,7 +245,7 @@ class BrowseWidget(QWidget):
|
|||||||
self.total_progress.setMaximum(len(accounts))
|
self.total_progress.setMaximum(len(accounts))
|
||||||
self.total_progress.setValue(0)
|
self.total_progress.setValue(0)
|
||||||
|
|
||||||
self.log_signal.emit(f"开始任务: {len(accounts)} 个账号, 类型: {browse_type}")
|
self.log_signal.emit(f"🚀 开始任务: {len(accounts)}个账号")
|
||||||
|
|
||||||
from utils.worker import Worker
|
from utils.worker import Worker
|
||||||
|
|
||||||
@@ -251,8 +255,8 @@ class BrowseWidget(QWidget):
|
|||||||
if _should_stop and _should_stop():
|
if _should_stop and _should_stop():
|
||||||
break
|
break
|
||||||
|
|
||||||
_signals.log.emit(f"[{i+1}/{len(accounts)}] 处理账号: {account.username}")
|
display_name = account.remark or account.username
|
||||||
_signals.progress.emit(i, f"正在处理: {account.username}")
|
_signals.progress.emit(i, f"正在处理: {display_name}")
|
||||||
|
|
||||||
password = decrypt_password(account.password) if is_encrypted(account.password) else account.password
|
password = decrypt_password(account.password) if is_encrypted(account.password) else account.password
|
||||||
|
|
||||||
@@ -265,10 +269,10 @@ class BrowseWidget(QWidget):
|
|||||||
proxy_config = {"server": cfg.proxy.server}
|
proxy_config = {"server": cfg.proxy.server}
|
||||||
|
|
||||||
browse_result = None
|
browse_result = None
|
||||||
with APIBrowser(log_callback=lambda msg: _signals.log.emit(msg), proxy_config=proxy_config) as browser:
|
# 静默模式浏览,不输出详细日志
|
||||||
|
with APIBrowser(log_callback=None, proxy_config=proxy_config) as browser:
|
||||||
if browser.login(account.username, password):
|
if browser.login(account.username, password):
|
||||||
browser.save_cookies_for_screenshot(account.username)
|
browser.save_cookies_for_screenshot(account.username)
|
||||||
|
|
||||||
result = browser.browse_content(
|
result = browser.browse_content(
|
||||||
browse_type,
|
browse_type,
|
||||||
should_stop_callback=_should_stop,
|
should_stop_callback=_should_stop,
|
||||||
@@ -278,22 +282,28 @@ class BrowseWidget(QWidget):
|
|||||||
"total_items": result.total_items,
|
"total_items": result.total_items,
|
||||||
"total_attachments": result.total_attachments,
|
"total_attachments": result.total_attachments,
|
||||||
}
|
}
|
||||||
|
if result.success:
|
||||||
|
_signals.log.emit(f"✅ {display_name} 浏览完成 ({result.total_items}条)")
|
||||||
|
else:
|
||||||
|
_signals.log.emit(f"❌ {display_name} 登录失败")
|
||||||
|
|
||||||
screenshot_path = None
|
screenshot_path = None
|
||||||
if browse_result and browse_result.get("success") and auto_screenshot:
|
if browse_result and browse_result.get("success") and auto_screenshot:
|
||||||
from core.screenshot import take_screenshot
|
from core.screenshot import take_screenshot
|
||||||
|
# 静默截图
|
||||||
_signals.log.emit("正在截图...")
|
|
||||||
ss_result = take_screenshot(
|
ss_result = take_screenshot(
|
||||||
account.username,
|
account.username,
|
||||||
password,
|
password,
|
||||||
browse_type,
|
browse_type,
|
||||||
remark=account.remark,
|
remark=account.remark,
|
||||||
log_callback=lambda msg: _signals.log.emit(msg),
|
log_callback=None,
|
||||||
proxy_config=proxy_config
|
proxy_config=proxy_config
|
||||||
)
|
)
|
||||||
if ss_result.success:
|
if ss_result.success:
|
||||||
screenshot_path = ss_result.filepath
|
screenshot_path = ss_result.filepath
|
||||||
|
_signals.log.emit(f"📸 {display_name} 截图完成")
|
||||||
|
else:
|
||||||
|
_signals.log.emit(f"⚠️ {display_name} 截图失败")
|
||||||
|
|
||||||
if screenshot_path and auto_upload:
|
if screenshot_path and auto_upload:
|
||||||
from core.kdocs_uploader import get_kdocs_uploader
|
from core.kdocs_uploader import get_kdocs_uploader
|
||||||
@@ -301,18 +311,17 @@ class BrowseWidget(QWidget):
|
|||||||
|
|
||||||
cfg2 = _get_config2()
|
cfg2 = _get_config2()
|
||||||
if cfg2.kdocs.enabled:
|
if cfg2.kdocs.enabled:
|
||||||
_signals.log.emit("正在上传到金山文档...")
|
|
||||||
uploader = get_kdocs_uploader()
|
uploader = get_kdocs_uploader()
|
||||||
uploader._log_callback = lambda msg: _signals.log.emit(msg)
|
uploader._log_callback = None # 静默模式
|
||||||
upload_result = uploader.upload_image(
|
upload_result = uploader.upload_image(
|
||||||
screenshot_path,
|
screenshot_path,
|
||||||
cfg2.kdocs.unit,
|
cfg2.kdocs.unit,
|
||||||
account.remark or account.username
|
account.remark or account.username
|
||||||
)
|
)
|
||||||
if upload_result.get("success"):
|
if upload_result.get("success"):
|
||||||
_signals.log.emit("[OK] 上传成功")
|
_signals.log.emit(f"📤 {display_name} 已上传文档")
|
||||||
else:
|
else:
|
||||||
_signals.log.emit(f"上传失败: {upload_result.get('error', '未知错误')}")
|
_signals.log.emit(f"⚠️ {display_name} 上传失败")
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
"account": account.username,
|
"account": account.username,
|
||||||
@@ -320,7 +329,7 @@ class BrowseWidget(QWidget):
|
|||||||
"screenshot": screenshot_path,
|
"screenshot": screenshot_path,
|
||||||
})
|
})
|
||||||
|
|
||||||
_signals.progress.emit(i + 1, f"完成: {account.username}")
|
_signals.progress.emit(i + 1, f"完成: {display_name}")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -332,8 +341,8 @@ class BrowseWidget(QWidget):
|
|||||||
self._is_running = False
|
self._is_running = False
|
||||||
self.start_btn.setEnabled(True)
|
self.start_btn.setEnabled(True)
|
||||||
self.stop_btn.setEnabled(False)
|
self.stop_btn.setEnabled(False)
|
||||||
self.current_task_label.setText("当前任务: 完成")
|
self.current_task_label.setText("当前任务: 无")
|
||||||
self.log_signal.emit(f"任务完成: {message}")
|
self.log_signal.emit("🎉 全部任务完成")
|
||||||
|
|
||||||
def on_result(results):
|
def on_result(results):
|
||||||
if results:
|
if results:
|
||||||
@@ -354,3 +363,31 @@ class BrowseWidget(QWidget):
|
|||||||
self._worker.stop()
|
self._worker.stop()
|
||||||
self.log_signal.emit("正在停止任务...")
|
self.log_signal.emit("正在停止任务...")
|
||||||
self.stop_btn.setEnabled(False)
|
self.stop_btn.setEnabled(False)
|
||||||
|
|
||||||
|
def _load_task_config(self):
|
||||||
|
"""加载任务配置"""
|
||||||
|
config = get_config()
|
||||||
|
task = config.task
|
||||||
|
|
||||||
|
# 设置浏览类型
|
||||||
|
index = self.browse_type_combo.findText(task.browse_type)
|
||||||
|
if index >= 0:
|
||||||
|
self.browse_type_combo.setCurrentIndex(index)
|
||||||
|
|
||||||
|
# 设置选项(先断开信号避免循环保存)
|
||||||
|
self.auto_screenshot_check.blockSignals(True)
|
||||||
|
self.auto_upload_check.blockSignals(True)
|
||||||
|
|
||||||
|
self.auto_screenshot_check.setChecked(task.auto_screenshot)
|
||||||
|
self.auto_upload_check.setChecked(task.auto_upload)
|
||||||
|
|
||||||
|
self.auto_screenshot_check.blockSignals(False)
|
||||||
|
self.auto_upload_check.blockSignals(False)
|
||||||
|
|
||||||
|
def _save_task_config(self):
|
||||||
|
"""保存任务配置"""
|
||||||
|
config = get_config()
|
||||||
|
config.task.browse_type = self.browse_type_combo.currentText()
|
||||||
|
config.task.auto_screenshot = self.auto_screenshot_check.isChecked()
|
||||||
|
config.task.auto_upload = self.auto_upload_check.isChecked()
|
||||||
|
save_config(config)
|
||||||
|
|||||||
201
ui/dependency_dialog.py
Normal file
201
ui/dependency_dialog.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
依赖安装对话框
|
||||||
|
检测到缺失依赖时显示,让用户选择是否下载安装
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||||
|
QProgressBar, QCheckBox, QGroupBox, QMessageBox
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadThread(QThread):
|
||||||
|
"""下载线程"""
|
||||||
|
progress = pyqtSignal(int, int) # downloaded, total
|
||||||
|
log = pyqtSignal(str)
|
||||||
|
finished = pyqtSignal(bool, str) # success, message
|
||||||
|
|
||||||
|
def __init__(self, dep_name: str):
|
||||||
|
super().__init__()
|
||||||
|
self.dep_name = dep_name
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
from utils.dependency_installer import download_and_install_dependency
|
||||||
|
|
||||||
|
def on_progress(downloaded, total):
|
||||||
|
self.progress.emit(downloaded, total)
|
||||||
|
|
||||||
|
def on_log(msg):
|
||||||
|
self.log.emit(msg)
|
||||||
|
|
||||||
|
success, message = download_and_install_dependency(
|
||||||
|
self.dep_name,
|
||||||
|
progress_callback=on_progress,
|
||||||
|
log_callback=on_log
|
||||||
|
)
|
||||||
|
self.finished.emit(success, message)
|
||||||
|
|
||||||
|
|
||||||
|
class DependencyDialog(QDialog):
|
||||||
|
"""依赖安装对话框"""
|
||||||
|
|
||||||
|
def __init__(self, missing_deps: dict, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.missing_deps = missing_deps
|
||||||
|
self.download_threads = []
|
||||||
|
self.setWindowTitle("环境检测")
|
||||||
|
self.setMinimumWidth(450)
|
||||||
|
self.setModal(True)
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setSpacing(16)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title = QLabel("⚠️ 检测到缺少必要的运行环境")
|
||||||
|
title.setStyleSheet("font-size: 16px; font-weight: bold; color: #fa8c16;")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# 说明
|
||||||
|
desc = QLabel("以下组件缺失,部分功能可能无法正常使用:")
|
||||||
|
desc.setStyleSheet("color: #666;")
|
||||||
|
layout.addWidget(desc)
|
||||||
|
|
||||||
|
# 缺失组件列表
|
||||||
|
deps_group = QGroupBox("缺失组件")
|
||||||
|
deps_layout = QVBoxLayout(deps_group)
|
||||||
|
|
||||||
|
self.checkboxes = {}
|
||||||
|
|
||||||
|
if self.missing_deps.get("wkhtmltoimage"):
|
||||||
|
cb = QCheckBox("wkhtmltoimage(截图功能需要)")
|
||||||
|
cb.setChecked(True)
|
||||||
|
self.checkboxes["wkhtmltoimage"] = cb
|
||||||
|
deps_layout.addWidget(cb)
|
||||||
|
|
||||||
|
if self.missing_deps.get("chromium"):
|
||||||
|
cb = QCheckBox("Playwright Chromium(金山文档上传需要)")
|
||||||
|
cb.setChecked(True)
|
||||||
|
self.checkboxes["chromium"] = cb
|
||||||
|
deps_layout.addWidget(cb)
|
||||||
|
|
||||||
|
layout.addWidget(deps_group)
|
||||||
|
|
||||||
|
# 进度区域
|
||||||
|
self.progress_group = QGroupBox("安装进度")
|
||||||
|
progress_layout = QVBoxLayout(self.progress_group)
|
||||||
|
|
||||||
|
self.status_label = QLabel("等待开始...")
|
||||||
|
self.status_label.setStyleSheet("color: #666;")
|
||||||
|
progress_layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setRange(0, 100)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
progress_layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
self.progress_group.setVisible(False)
|
||||||
|
layout.addWidget(self.progress_group)
|
||||||
|
|
||||||
|
# 按钮
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
btn_layout.addStretch()
|
||||||
|
|
||||||
|
self.skip_btn = QPushButton("跳过")
|
||||||
|
self.skip_btn.setMinimumWidth(80)
|
||||||
|
self.skip_btn.clicked.connect(self.reject)
|
||||||
|
btn_layout.addWidget(self.skip_btn)
|
||||||
|
|
||||||
|
self.install_btn = QPushButton("立即安装")
|
||||||
|
self.install_btn.setObjectName("primary")
|
||||||
|
self.install_btn.setMinimumWidth(100)
|
||||||
|
self.install_btn.clicked.connect(self._start_install)
|
||||||
|
btn_layout.addWidget(self.install_btn)
|
||||||
|
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
def _start_install(self):
|
||||||
|
"""开始安装"""
|
||||||
|
# 获取选中的组件
|
||||||
|
selected = [name for name, cb in self.checkboxes.items() if cb.isChecked()]
|
||||||
|
|
||||||
|
if not selected:
|
||||||
|
QMessageBox.information(self, "提示", "请选择要安装的组件")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 禁用按钮和复选框
|
||||||
|
self.install_btn.setEnabled(False)
|
||||||
|
self.skip_btn.setText("取消")
|
||||||
|
for cb in self.checkboxes.values():
|
||||||
|
cb.setEnabled(False)
|
||||||
|
|
||||||
|
# 显示进度
|
||||||
|
self.progress_group.setVisible(True)
|
||||||
|
|
||||||
|
# 开始安装
|
||||||
|
self._install_queue = selected.copy()
|
||||||
|
self._install_next()
|
||||||
|
|
||||||
|
def _install_next(self):
|
||||||
|
"""安装下一个组件"""
|
||||||
|
if not self._install_queue:
|
||||||
|
# 全部完成
|
||||||
|
self.status_label.setText("✅ 安装完成!")
|
||||||
|
self.status_label.setStyleSheet("color: #52c41a; font-weight: bold;")
|
||||||
|
self.skip_btn.setText("完成")
|
||||||
|
self.skip_btn.clicked.disconnect()
|
||||||
|
self.skip_btn.clicked.connect(self.accept)
|
||||||
|
return
|
||||||
|
|
||||||
|
dep_name = self._install_queue.pop(0)
|
||||||
|
display_name = "wkhtmltoimage" if dep_name == "wkhtmltoimage" else "Chromium 浏览器"
|
||||||
|
self.status_label.setText(f"正在下载 {display_name}...")
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
|
||||||
|
# 启动下载线程
|
||||||
|
thread = DownloadThread(dep_name)
|
||||||
|
thread.progress.connect(self._on_progress)
|
||||||
|
thread.log.connect(self._on_log)
|
||||||
|
thread.finished.connect(self._on_finished)
|
||||||
|
thread.start()
|
||||||
|
self.download_threads.append(thread)
|
||||||
|
|
||||||
|
def _on_progress(self, downloaded: int, total: int):
|
||||||
|
"""下载进度更新"""
|
||||||
|
if total > 0:
|
||||||
|
percent = int(downloaded * 100 / total)
|
||||||
|
self.progress_bar.setValue(percent)
|
||||||
|
mb_downloaded = downloaded / 1024 / 1024
|
||||||
|
mb_total = total / 1024 / 1024
|
||||||
|
self.status_label.setText(f"下载中... {mb_downloaded:.1f}MB / {mb_total:.1f}MB")
|
||||||
|
|
||||||
|
def _on_log(self, msg: str):
|
||||||
|
"""日志更新"""
|
||||||
|
self.status_label.setText(msg)
|
||||||
|
|
||||||
|
def _on_finished(self, success: bool, message: str):
|
||||||
|
"""安装完成"""
|
||||||
|
if success:
|
||||||
|
if message:
|
||||||
|
# wkhtmltoimage 需要手动完成安装向导
|
||||||
|
QMessageBox.information(self, "提示", message)
|
||||||
|
# 继续安装下一个
|
||||||
|
self._install_next()
|
||||||
|
else:
|
||||||
|
self.status_label.setText(f"❌ 安装失败: {message}")
|
||||||
|
self.status_label.setStyleSheet("color: #ff4d4f;")
|
||||||
|
self.install_btn.setEnabled(True)
|
||||||
|
self.skip_btn.setText("跳过")
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""关闭时停止所有线程"""
|
||||||
|
for thread in self.download_threads:
|
||||||
|
if thread.isRunning():
|
||||||
|
thread.terminate()
|
||||||
|
thread.wait()
|
||||||
|
event.accept()
|
||||||
@@ -34,7 +34,6 @@ class KDocsWidget(QWidget):
|
|||||||
self._login_check_timer = None
|
self._login_check_timer = None
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
self._load_config()
|
self._load_config()
|
||||||
self._check_wechat_process() # 检测微信进程
|
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
main_layout = QVBoxLayout(self)
|
main_layout = QVBoxLayout(self)
|
||||||
@@ -191,29 +190,6 @@ class KDocsWidget(QWidget):
|
|||||||
self.get_qr_btn.clicked.connect(self._get_qr_code)
|
self.get_qr_btn.clicked.connect(self._get_qr_code)
|
||||||
status_btn_layout.addWidget(self.get_qr_btn)
|
status_btn_layout.addWidget(self.get_qr_btn)
|
||||||
|
|
||||||
# 快捷登录按钮(检测到微信进程时显示)
|
|
||||||
self.quick_login_btn = QPushButton("⚡ 微信快捷登录")
|
|
||||||
self.quick_login_btn.setFixedHeight(36)
|
|
||||||
self.quick_login_btn.setMinimumWidth(130)
|
|
||||||
self.quick_login_btn.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background-color: #07C160;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #06AD56;
|
|
||||||
}
|
|
||||||
QPushButton:disabled {
|
|
||||||
background-color: #ccc;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.quick_login_btn.clicked.connect(self._quick_login)
|
|
||||||
self.quick_login_btn.setVisible(False) # 默认隐藏
|
|
||||||
status_btn_layout.addWidget(self.quick_login_btn)
|
|
||||||
|
|
||||||
self.clear_login_btn = QPushButton("清除登录")
|
self.clear_login_btn = QPushButton("清除登录")
|
||||||
self.clear_login_btn.setFixedHeight(36)
|
self.clear_login_btn.setFixedHeight(36)
|
||||||
self.clear_login_btn.setMinimumWidth(120)
|
self.clear_login_btn.setMinimumWidth(120)
|
||||||
@@ -271,7 +247,7 @@ class KDocsWidget(QWidget):
|
|||||||
"""Get login QR code and poll for login status"""
|
"""Get login QR code and poll for login status"""
|
||||||
self.get_qr_btn.setEnabled(False)
|
self.get_qr_btn.setEnabled(False)
|
||||||
self.qr_label.setText("获取中...")
|
self.qr_label.setText("获取中...")
|
||||||
self.log_signal.emit("正在获取金山文档登录二维码...")
|
self.log_signal.emit("🔄 正在获取登录二维码...")
|
||||||
|
|
||||||
# 停止之前的检查
|
# 停止之前的检查
|
||||||
if self._login_check_timer:
|
if self._login_check_timer:
|
||||||
@@ -281,12 +257,12 @@ class KDocsWidget(QWidget):
|
|||||||
from utils.worker import Worker
|
from utils.worker import Worker
|
||||||
|
|
||||||
def get_qr_and_wait_login(_signals=None, _should_stop=None):
|
def get_qr_and_wait_login(_signals=None, _should_stop=None):
|
||||||
"""获取二维码并轮询等待登录(在同一个线程中完成所有Playwright操作)"""
|
"""获取二维码并轮询等待登录"""
|
||||||
from core.kdocs_uploader import get_kdocs_uploader
|
from core.kdocs_uploader import get_kdocs_uploader
|
||||||
import time
|
import time
|
||||||
|
|
||||||
uploader = get_kdocs_uploader()
|
uploader = get_kdocs_uploader()
|
||||||
uploader._log_callback = lambda msg: _signals.log.emit(msg) if _signals else None
|
uploader._log_callback = None # 静默模式
|
||||||
|
|
||||||
# 1. 获取二维码
|
# 1. 获取二维码
|
||||||
result = uploader.request_qr(force=True)
|
result = uploader.request_qr(force=True)
|
||||||
@@ -301,11 +277,10 @@ class KDocsWidget(QWidget):
|
|||||||
if qr_image:
|
if qr_image:
|
||||||
_signals.screenshot_ready.emit(qr_image) # 复用这个信号传递二维码
|
_signals.screenshot_ready.emit(qr_image) # 复用这个信号传递二维码
|
||||||
|
|
||||||
# 3. 在同一个线程中轮询检查登录状态
|
# 3. 轮询检查登录状态
|
||||||
max_wait = 120 # 最多等待120秒
|
max_wait = 120
|
||||||
check_interval = 3 # 每3秒检查一次
|
check_interval = 3
|
||||||
waited = 0
|
waited = 0
|
||||||
check_count = 0
|
|
||||||
|
|
||||||
while waited < max_wait:
|
while waited < max_wait:
|
||||||
if _should_stop and _should_stop():
|
if _should_stop and _should_stop():
|
||||||
@@ -313,21 +288,12 @@ class KDocsWidget(QWidget):
|
|||||||
|
|
||||||
time.sleep(check_interval)
|
time.sleep(check_interval)
|
||||||
waited += check_interval
|
waited += check_interval
|
||||||
check_count += 1
|
|
||||||
|
|
||||||
# 检查登录状态(在同一个线程中,不会有线程问题)
|
|
||||||
if _signals:
|
|
||||||
_signals.log.emit(f"[KDocs] 检查登录状态... ({check_count})")
|
|
||||||
check_result = uploader.check_login_status()
|
check_result = uploader.check_login_status()
|
||||||
if check_result.get("logged_in"):
|
if check_result.get("logged_in"):
|
||||||
return {"success": True, "logged_in": True}
|
return {"success": True, "logged_in": True}
|
||||||
|
|
||||||
# 每30秒提醒一下用户
|
return {"success": False, "error": "登录超时,请重新获取二维码"}
|
||||||
if waited % 30 == 0 and _signals:
|
|
||||||
remaining = max_wait - waited
|
|
||||||
_signals.log.emit(f"[KDocs] 等待扫码登录...(剩余{remaining}秒)")
|
|
||||||
|
|
||||||
return {"success": False, "error": "登录超时(120秒),请重新获取二维码"}
|
|
||||||
|
|
||||||
def on_qr_ready(qr_base64):
|
def on_qr_ready(qr_base64):
|
||||||
"""二维码准备好了,显示出来"""
|
"""二维码准备好了,显示出来"""
|
||||||
@@ -337,7 +303,7 @@ class KDocsWidget(QWidget):
|
|||||||
pixmap.loadFromData(qr_bytes)
|
pixmap.loadFromData(qr_bytes)
|
||||||
scaled = pixmap.scaled(140, 140, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
scaled = pixmap.scaled(140, 140, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
||||||
self.qr_label.setPixmap(scaled)
|
self.qr_label.setPixmap(scaled)
|
||||||
self.log_signal.emit("请使用微信扫描二维码登录(120秒内有效)")
|
self.log_signal.emit("📱 请使用微信扫描二维码登录")
|
||||||
|
|
||||||
def on_result(result):
|
def on_result(result):
|
||||||
self.get_qr_btn.setEnabled(True)
|
self.get_qr_btn.setEnabled(True)
|
||||||
@@ -346,21 +312,18 @@ class KDocsWidget(QWidget):
|
|||||||
self.status_label.setText("✅ 已登录")
|
self.status_label.setText("✅ 已登录")
|
||||||
self.status_label.setStyleSheet(get_status_style(True))
|
self.status_label.setStyleSheet(get_status_style(True))
|
||||||
self.qr_label.setText("登录成功!")
|
self.qr_label.setText("登录成功!")
|
||||||
self.log_signal.emit("金山文档登录成功")
|
self.log_signal.emit("✅ 金山文档登录成功")
|
||||||
else:
|
|
||||||
# 不应该走到这里,因为上面会返回logged_in=True
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
error = result.get("error", "未知错误") if result else "未知错误"
|
error = result.get("error", "未知错误") if result else "未知错误"
|
||||||
self.qr_label.setText(f"失败: {error}")
|
self.qr_label.setText(f"失败")
|
||||||
self.status_label.setText("❌ 未登录")
|
self.status_label.setText("❌ 未登录")
|
||||||
self.status_label.setStyleSheet(get_status_style(False))
|
self.status_label.setStyleSheet(get_status_style(False))
|
||||||
self.log_signal.emit(f"登录失败: {error}")
|
self.log_signal.emit(f"❌ 登录失败: {error}")
|
||||||
|
|
||||||
def on_error(error):
|
def on_error(error):
|
||||||
self.get_qr_btn.setEnabled(True)
|
self.get_qr_btn.setEnabled(True)
|
||||||
self.qr_label.setText(f"错误: {error}")
|
self.qr_label.setText("获取失败")
|
||||||
self.log_signal.emit(f"获取二维码出错: {error}")
|
self.log_signal.emit(f"❌ 二维码获取失败: {error}")
|
||||||
|
|
||||||
self._worker = Worker(get_qr_and_wait_login)
|
self._worker = Worker(get_qr_and_wait_login)
|
||||||
self._worker.signals.log.connect(self.log_signal.emit)
|
self._worker.signals.log.connect(self.log_signal.emit)
|
||||||
@@ -378,273 +341,9 @@ class KDocsWidget(QWidget):
|
|||||||
self.status_label.setText("❌ 未登录")
|
self.status_label.setText("❌ 未登录")
|
||||||
self.status_label.setStyleSheet(get_status_style(False))
|
self.status_label.setStyleSheet(get_status_style(False))
|
||||||
self.qr_label.setText("点击获取二维码")
|
self.qr_label.setText("点击获取二维码")
|
||||||
self.log_signal.emit("金山文档登录状态已清除")
|
self.log_signal.emit("🔓 登录状态已清除")
|
||||||
|
|
||||||
if self._login_check_timer:
|
if self._login_check_timer:
|
||||||
self._login_check_timer.stop()
|
self._login_check_timer.stop()
|
||||||
self._login_check_timer = None
|
self._login_check_timer = None
|
||||||
|
|
||||||
def _check_wechat_process(self) -> bool:
|
|
||||||
"""检测微信进程是否运行
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: 微信是否在运行
|
|
||||||
"""
|
|
||||||
import subprocess
|
|
||||||
try:
|
|
||||||
# Windows下检测微信进程(可能是Weixin.exe或WeChat.exe)
|
|
||||||
result = subprocess.run(
|
|
||||||
['tasklist'],
|
|
||||||
capture_output=True, text=True, timeout=5
|
|
||||||
)
|
|
||||||
output_lower = result.stdout.lower()
|
|
||||||
# 检测多种可能的微信进程名
|
|
||||||
is_running = 'weixin.exe' in output_lower or 'wechat.exe' in output_lower
|
|
||||||
if is_running:
|
|
||||||
self.log_signal.emit("检测到微信进程,可使用快捷登录")
|
|
||||||
# 始终显示快捷登录按钮,让用户自己决定
|
|
||||||
self.quick_login_btn.setVisible(True)
|
|
||||||
return is_running
|
|
||||||
except Exception:
|
|
||||||
# 检测失败也显示按钮
|
|
||||||
self.quick_login_btn.setVisible(True)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _quick_login(self):
|
|
||||||
"""微信快捷登录"""
|
|
||||||
# 先检测微信是否运行
|
|
||||||
is_wechat_running = self._check_wechat_process()
|
|
||||||
if not is_wechat_running:
|
|
||||||
self.log_signal.emit("⚠️ 未检测到微信进程,快捷登录可能失败。建议先打开微信客户端。")
|
|
||||||
|
|
||||||
self.quick_login_btn.setEnabled(False)
|
|
||||||
self.quick_login_btn.setText("登录中...")
|
|
||||||
self.log_signal.emit("正在执行微信快捷登录...")
|
|
||||||
|
|
||||||
from utils.worker import Worker
|
|
||||||
|
|
||||||
def do_quick_login(_signals=None, _should_stop=None):
|
|
||||||
from core.kdocs_uploader import get_kdocs_uploader
|
|
||||||
import time
|
|
||||||
|
|
||||||
uploader = get_kdocs_uploader()
|
|
||||||
uploader._log_callback = lambda msg: _signals.log.emit(msg) if _signals else None
|
|
||||||
|
|
||||||
# 强制重新登录,使用快捷登录
|
|
||||||
from config import KDOCS_LOGIN_STATE_FILE
|
|
||||||
try:
|
|
||||||
if KDOCS_LOGIN_STATE_FILE.exists():
|
|
||||||
KDOCS_LOGIN_STATE_FILE.unlink()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
uploader._cleanup_browser()
|
|
||||||
|
|
||||||
# 启动浏览器并执行快捷登录
|
|
||||||
if not uploader._ensure_playwright(use_storage_state=False):
|
|
||||||
return {"success": False, "error": uploader._last_error or "浏览器不可用"}
|
|
||||||
|
|
||||||
from config import get_config
|
|
||||||
config = get_config()
|
|
||||||
doc_url = config.kdocs.doc_url.strip()
|
|
||||||
if not doc_url:
|
|
||||||
return {"success": False, "error": "未配置金山文档链接"}
|
|
||||||
|
|
||||||
if not uploader._open_document(doc_url):
|
|
||||||
return {"success": False, "error": uploader._last_error or "打开文档失败"}
|
|
||||||
|
|
||||||
# 执行登录流程,优先使用微信快捷登录
|
|
||||||
agree_names = ["同意", "同意并继续", "我同意", "确定", "确认"]
|
|
||||||
|
|
||||||
for loop_idx in range(15):
|
|
||||||
current_url = uploader._page.url
|
|
||||||
uploader.log(f"[KDocs] 循环{loop_idx+1}: URL={current_url[:60]}...")
|
|
||||||
|
|
||||||
# 【优先】检查是否有"登录并加入编辑"按钮(可能在任何页面弹出)
|
|
||||||
try:
|
|
||||||
join_btn = uploader._page.get_by_role("button", name="登录并加入编辑")
|
|
||||||
btn_count = join_btn.count()
|
|
||||||
if btn_count > 0:
|
|
||||||
uploader.log(f"[KDocs] 找到'登录并加入编辑'按钮,直接点击")
|
|
||||||
join_btn.first.click(timeout=3000)
|
|
||||||
time.sleep(2)
|
|
||||||
continue
|
|
||||||
except Exception as e:
|
|
||||||
uploader.log(f"[KDocs] 点击'登录并加入编辑'失败: {e}")
|
|
||||||
|
|
||||||
# 检查是否已到达文档页面(且没有登录相关元素)
|
|
||||||
if "kdocs.cn/l/" in current_url and "account.wps.cn" not in current_url:
|
|
||||||
uploader.log(f"[KDocs] URL是文档页面,检查是否真的登录成功...")
|
|
||||||
time.sleep(1) # 等待页面稳定
|
|
||||||
|
|
||||||
# 检查页面上是否还有登录相关元素
|
|
||||||
has_login_elements = False
|
|
||||||
|
|
||||||
# 检查是否有"立即登录"按钮
|
|
||||||
try:
|
|
||||||
login_btn = uploader._page.get_by_role("button", name="立即登录")
|
|
||||||
if login_btn.count() > 0 and login_btn.first.is_visible(timeout=500):
|
|
||||||
uploader.log("[KDocs] 页面上有'立即登录'按钮,未真正登录")
|
|
||||||
has_login_elements = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 检查是否有二维码(说明还在登录页)
|
|
||||||
if not has_login_elements:
|
|
||||||
try:
|
|
||||||
qr_text = uploader._page.get_by_text("微信扫码登录", exact=False)
|
|
||||||
if qr_text.count() > 0 and qr_text.first.is_visible(timeout=500):
|
|
||||||
uploader.log("[KDocs] 页面上有'微信扫码登录',未真正登录")
|
|
||||||
has_login_elements = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 检查是否有"微信快捷登录"按钮
|
|
||||||
if not has_login_elements:
|
|
||||||
try:
|
|
||||||
quick_btn = uploader._page.get_by_text("微信快捷登录", exact=False)
|
|
||||||
if quick_btn.count() > 0 and quick_btn.first.is_visible(timeout=500):
|
|
||||||
uploader.log("[KDocs] 页面上有'微信快捷登录'按钮,未真正登录")
|
|
||||||
has_login_elements = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if has_login_elements:
|
|
||||||
uploader.log("[KDocs] 检测到登录元素,继续处理登录流程...")
|
|
||||||
# 不return,继续下面的登录处理
|
|
||||||
else:
|
|
||||||
uploader.log("[KDocs] 确认登录成功!")
|
|
||||||
uploader._logged_in = True
|
|
||||||
uploader._save_login_state()
|
|
||||||
return {"success": True, "logged_in": True}
|
|
||||||
|
|
||||||
clicked = False
|
|
||||||
|
|
||||||
# 点击同意按钮
|
|
||||||
for name in agree_names:
|
|
||||||
try:
|
|
||||||
btn = uploader._page.get_by_role("button", name=name)
|
|
||||||
if btn.count() > 0 and btn.first.is_visible(timeout=300):
|
|
||||||
uploader.log(f"[KDocs] 点击同意: {name}")
|
|
||||||
btn.first.click()
|
|
||||||
time.sleep(1)
|
|
||||||
clicked = True
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if clicked:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# 检查页面上是否有"微信快捷登录"按钮(可能URL是kdocs但内容是登录页)
|
|
||||||
has_quick_login_btn = False
|
|
||||||
try:
|
|
||||||
quick_btn = uploader._page.get_by_text("微信快捷登录", exact=False)
|
|
||||||
if quick_btn.count() > 0 and quick_btn.first.is_visible(timeout=300):
|
|
||||||
has_quick_login_btn = True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 在登录页面,先勾选协议复选框,再点击微信快捷登录
|
|
||||||
# 条件:URL是account.wps.cn 或者 页面上有微信快捷登录按钮
|
|
||||||
if "account.wps.cn" in current_url or has_quick_login_btn:
|
|
||||||
uploader.log("[KDocs] 检测到登录页面,尝试勾选协议...")
|
|
||||||
# 1. 先勾选"我已阅读并同意"复选框
|
|
||||||
try:
|
|
||||||
# 尝试多种方式找到协议复选框
|
|
||||||
checkbox_selectors = [
|
|
||||||
"input[type='checkbox']",
|
|
||||||
"span.checkbox",
|
|
||||||
"[class*='checkbox']",
|
|
||||||
"[class*='agree']",
|
|
||||||
]
|
|
||||||
checkbox_clicked = False
|
|
||||||
for selector in checkbox_selectors:
|
|
||||||
try:
|
|
||||||
checkboxes = uploader._page.locator(selector)
|
|
||||||
if checkboxes.count() > 0:
|
|
||||||
for i in range(min(checkboxes.count(), 3)):
|
|
||||||
cb = checkboxes.nth(i)
|
|
||||||
if cb.is_visible(timeout=200):
|
|
||||||
# 检查是否已勾选
|
|
||||||
is_checked = cb.is_checked() if hasattr(cb, 'is_checked') else False
|
|
||||||
if not is_checked:
|
|
||||||
uploader.log("[KDocs] 勾选协议复选框")
|
|
||||||
cb.click()
|
|
||||||
time.sleep(0.5)
|
|
||||||
checkbox_clicked = True
|
|
||||||
break
|
|
||||||
if checkbox_clicked:
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 也尝试点击包含"我已阅读"的文本区域
|
|
||||||
if not checkbox_clicked:
|
|
||||||
try:
|
|
||||||
agree_text = uploader._page.get_by_text("我已阅读并同意", exact=False)
|
|
||||||
if agree_text.count() > 0 and agree_text.first.is_visible(timeout=300):
|
|
||||||
uploader.log("[KDocs] 点击协议文本区域")
|
|
||||||
agree_text.first.click()
|
|
||||||
time.sleep(0.5)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
uploader.log(f"[KDocs] 勾选协议失败: {e}")
|
|
||||||
|
|
||||||
# 2. 点击微信快捷登录按钮
|
|
||||||
try:
|
|
||||||
quick = uploader._page.get_by_text("微信快捷登录", exact=False)
|
|
||||||
if quick.count() > 0 and quick.first.is_visible(timeout=500):
|
|
||||||
uploader.log("[KDocs] 点击微信快捷登录")
|
|
||||||
quick.first.click()
|
|
||||||
time.sleep(4) # 等待快捷登录完成
|
|
||||||
clicked = True
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 点击立即登录
|
|
||||||
try:
|
|
||||||
btn = uploader._page.get_by_role("button", name="立即登录")
|
|
||||||
if btn.count() > 0 and btn.first.is_visible(timeout=500):
|
|
||||||
uploader.log("[KDocs] 点击立即登录")
|
|
||||||
btn.first.click()
|
|
||||||
time.sleep(2)
|
|
||||||
clicked = True
|
|
||||||
continue
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if not clicked:
|
|
||||||
break
|
|
||||||
|
|
||||||
# 最终检查
|
|
||||||
if "kdocs.cn/l/" in uploader._page.url:
|
|
||||||
uploader._logged_in = True
|
|
||||||
uploader._save_login_state()
|
|
||||||
return {"success": True, "logged_in": True}
|
|
||||||
else:
|
|
||||||
return {"success": False, "error": "快捷登录失败,请尝试扫码登录"}
|
|
||||||
|
|
||||||
def on_result(result):
|
|
||||||
self.quick_login_btn.setEnabled(True)
|
|
||||||
self.quick_login_btn.setText("⚡ 微信快捷登录")
|
|
||||||
if result and result.get("success") and result.get("logged_in"):
|
|
||||||
self.status_label.setText("✅ 已登录")
|
|
||||||
self.status_label.setStyleSheet(get_status_style(True))
|
|
||||||
self.qr_label.setText("快捷登录成功!")
|
|
||||||
self.log_signal.emit("金山文档快捷登录成功")
|
|
||||||
else:
|
|
||||||
error = result.get("error", "未知错误") if result else "未知错误"
|
|
||||||
self.log_signal.emit(f"快捷登录失败: {error}")
|
|
||||||
|
|
||||||
def on_error(error):
|
|
||||||
self.quick_login_btn.setEnabled(True)
|
|
||||||
self.quick_login_btn.setText("⚡ 微信快捷登录")
|
|
||||||
self.log_signal.emit(f"快捷登录出错: {error}")
|
|
||||||
|
|
||||||
self._worker = Worker(do_quick_login)
|
|
||||||
self._worker.signals.log.connect(self.log_signal.emit)
|
|
||||||
self._worker.signals.result.connect(on_result)
|
|
||||||
self._worker.signals.error.connect(on_error)
|
|
||||||
self._worker.start()
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||||
QPushButton, QStackedWidget, QFrame, QLabel, QSplitter
|
QPushButton, QStackedWidget, QFrame, QLabel, QSplitter
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
|
|
||||||
from .styles import get_stylesheet, LIGHT_THEME, DARK_THEME
|
from .styles import get_stylesheet, LIGHT_THEME, DARK_THEME
|
||||||
from .log_widget import LogWidget
|
from .log_widget import LogWidget
|
||||||
@@ -157,6 +157,9 @@ class MainWindow(QMainWindow):
|
|||||||
self.kdocs_widget.log_signal.connect(self.log_widget.append_log)
|
self.kdocs_widget.log_signal.connect(self.log_widget.append_log)
|
||||||
self.settings_widget.log_signal.connect(self.log_widget.append_log)
|
self.settings_widget.log_signal.connect(self.log_widget.append_log)
|
||||||
|
|
||||||
|
# 账号变化时自动刷新浏览任务页面的账号列表
|
||||||
|
self.account_widget.accounts_changed.connect(self.browse_widget._refresh_accounts)
|
||||||
|
|
||||||
def _switch_page(self, index: int):
|
def _switch_page(self, index: int):
|
||||||
"""切换页面"""
|
"""切换页面"""
|
||||||
# 更新导航按钮状态
|
# 更新导航按钮状态
|
||||||
@@ -176,6 +179,64 @@ class MainWindow(QMainWindow):
|
|||||||
"""记录日志"""
|
"""记录日志"""
|
||||||
self.log_widget.append_log(message)
|
self.log_widget.append_log(message)
|
||||||
|
|
||||||
|
def init_kdocs_login_check(self):
|
||||||
|
"""启动时自动检测金山文档登录状态(后台无头模式)"""
|
||||||
|
from config import get_config
|
||||||
|
config = get_config()
|
||||||
|
|
||||||
|
# 如果金山文档未启用或未配置链接,直接显示就绪
|
||||||
|
if not config.kdocs.enabled or not config.kdocs.doc_url.strip():
|
||||||
|
self.log("✅ 环境准备就绪")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("🔄 正在检测金山文档登录状态...")
|
||||||
|
self._kdocs_init_worker = None
|
||||||
|
|
||||||
|
# 延迟100ms执行,让UI先显示出来
|
||||||
|
QTimer.singleShot(100, self._do_kdocs_login_check)
|
||||||
|
|
||||||
|
def _do_kdocs_login_check(self):
|
||||||
|
"""执行金山文档登录检测"""
|
||||||
|
from utils.worker import Worker
|
||||||
|
|
||||||
|
def check_kdocs_login(_signals=None, _should_stop=None):
|
||||||
|
from core.kdocs_uploader import get_kdocs_uploader
|
||||||
|
|
||||||
|
uploader = get_kdocs_uploader()
|
||||||
|
# 静默模式,不输出详细日志
|
||||||
|
uploader._log_callback = None
|
||||||
|
|
||||||
|
result = uploader.request_qr(force=False)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def on_result(result):
|
||||||
|
if result and result.get("success"):
|
||||||
|
if result.get("logged_in"):
|
||||||
|
self.log("✅ 金山文档已登录")
|
||||||
|
self.log("✅ 环境准备就绪")
|
||||||
|
# 更新金山文档面板状态
|
||||||
|
self.kdocs_widget.status_label.setText("✅ 已登录")
|
||||||
|
from .constants import get_status_style
|
||||||
|
self.kdocs_widget.status_label.setStyleSheet(get_status_style(True))
|
||||||
|
else:
|
||||||
|
self.log("⚠️ 金山文档未登录,正在获取登录二维码...")
|
||||||
|
# 自动切换到金山文档页面并获取二维码
|
||||||
|
self._switch_page(3) # 索引3是金山文档页面
|
||||||
|
QTimer.singleShot(300, self.kdocs_widget._get_qr_code)
|
||||||
|
else:
|
||||||
|
error = result.get("error", "未知错误") if result else "检测失败"
|
||||||
|
self.log(f"⚠️ 金山文档检测失败: {error}")
|
||||||
|
self.log("✅ 环境准备就绪")
|
||||||
|
|
||||||
|
def on_error(error):
|
||||||
|
self.log(f"⚠️ 金山文档检测出错: {error}")
|
||||||
|
self.log("✅ 环境准备就绪")
|
||||||
|
|
||||||
|
self._kdocs_init_worker = Worker(check_kdocs_login)
|
||||||
|
self._kdocs_init_worker.signals.result.connect(on_result)
|
||||||
|
self._kdocs_init_worker.signals.error.connect(on_error)
|
||||||
|
self._kdocs_init_worker.start()
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""窗口关闭事件"""
|
"""窗口关闭事件"""
|
||||||
# 保存配置
|
# 保存配置
|
||||||
|
|||||||
228
utils/dependency_installer.py
Normal file
228
utils/dependency_installer.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
依赖检测和安装模块
|
||||||
|
检测 wkhtmltoimage 和 Playwright Chromium 是否已安装
|
||||||
|
如果缺失则从直链下载安装
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Callable, Dict, Tuple
|
||||||
|
|
||||||
|
# 直链下载地址
|
||||||
|
DOWNLOAD_URLS = {
|
||||||
|
"wkhtmltopdf": "http://by.haory.cn/3/1223/wkhtmltox.exe",
|
||||||
|
"chromium": "http://by.haory.cn/3/1223/chromium-win64.zip",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Playwright chromium 版本目录名(与 playwright 1.57.0 对应)
|
||||||
|
CHROMIUM_VERSION_DIR = "chromium-1200"
|
||||||
|
|
||||||
|
|
||||||
|
def check_wkhtmltoimage() -> bool:
|
||||||
|
"""检测 wkhtmltoimage 是否已安装"""
|
||||||
|
# 先检查 PATH
|
||||||
|
if shutil.which("wkhtmltoimage"):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查默认安装路径
|
||||||
|
default_paths = [
|
||||||
|
r"C:\Program Files\wkhtmltopdf\bin\wkhtmltoimage.exe",
|
||||||
|
r"C:\Program Files (x86)\wkhtmltopdf\bin\wkhtmltoimage.exe",
|
||||||
|
]
|
||||||
|
for p in default_paths:
|
||||||
|
if os.path.exists(p):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def check_playwright_chromium() -> bool:
|
||||||
|
"""检测 Playwright Chromium 浏览器是否已安装"""
|
||||||
|
local_appdata = os.environ.get("LOCALAPPDATA", "")
|
||||||
|
if not local_appdata:
|
||||||
|
return False
|
||||||
|
|
||||||
|
chromium_path = Path(local_appdata) / "ms-playwright" / CHROMIUM_VERSION_DIR
|
||||||
|
|
||||||
|
# 检查目录是否存在且包含chrome.exe
|
||||||
|
if chromium_path.exists():
|
||||||
|
chrome_exe = chromium_path / "chrome-win" / "chrome.exe"
|
||||||
|
if chrome_exe.exists():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_missing_dependencies() -> Dict[str, bool]:
|
||||||
|
"""获取缺失的依赖列表"""
|
||||||
|
return {
|
||||||
|
"wkhtmltoimage": not check_wkhtmltoimage(),
|
||||||
|
"chromium": not check_playwright_chromium(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url: str, dest_path: str, progress_callback: Optional[Callable] = None) -> bool:
|
||||||
|
"""
|
||||||
|
下载文件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: 下载地址
|
||||||
|
dest_path: 保存路径
|
||||||
|
progress_callback: 进度回调 (downloaded_bytes, total_bytes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
try:
|
||||||
|
def report_progress(block_num, block_size, total_size):
|
||||||
|
if progress_callback:
|
||||||
|
downloaded = block_num * block_size
|
||||||
|
progress_callback(downloaded, total_size)
|
||||||
|
|
||||||
|
urllib.request.urlretrieve(url, dest_path, reporthook=report_progress)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"下载失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_wkhtmltopdf(installer_path: str, log_callback: Optional[Callable] = None) -> bool:
|
||||||
|
"""
|
||||||
|
安装 wkhtmltopdf
|
||||||
|
|
||||||
|
Args:
|
||||||
|
installer_path: 安装包路径
|
||||||
|
log_callback: 日志回调
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
def log(msg):
|
||||||
|
if log_callback:
|
||||||
|
log_callback(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
log("正在启动 wkhtmltopdf 安装程序...")
|
||||||
|
# 启动安装程序(需要用户手动完成安装向导)
|
||||||
|
subprocess.Popen([installer_path], shell=True)
|
||||||
|
log("请在安装向导中完成安装(建议保持默认路径)")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log(f"启动安装程序失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def install_playwright_chromium(zip_path: str, log_callback: Optional[Callable] = None) -> bool:
|
||||||
|
"""
|
||||||
|
安装 Playwright Chromium 浏览器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
zip_path: zip包路径
|
||||||
|
log_callback: 日志回调
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
|
def log(msg):
|
||||||
|
if log_callback:
|
||||||
|
log_callback(msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
local_appdata = os.environ.get("LOCALAPPDATA", "")
|
||||||
|
if not local_appdata:
|
||||||
|
log("无法获取 LOCALAPPDATA 路径")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 目标目录
|
||||||
|
playwright_dir = Path(local_appdata) / "ms-playwright"
|
||||||
|
chromium_dir = playwright_dir / CHROMIUM_VERSION_DIR
|
||||||
|
|
||||||
|
# 创建目录
|
||||||
|
playwright_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
log("正在解压 Chromium 浏览器...")
|
||||||
|
|
||||||
|
# 解压
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(chromium_dir)
|
||||||
|
|
||||||
|
# 创建标记文件(Playwright需要这些文件来确认安装完成)
|
||||||
|
(chromium_dir / "INSTALLATION_COMPLETE").touch()
|
||||||
|
(chromium_dir / "DEPENDENCIES_VALIDATED").touch()
|
||||||
|
|
||||||
|
log("Chromium 浏览器安装成功")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
log(f"安装 Chromium 失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def download_and_install_dependency(
|
||||||
|
dep_name: str,
|
||||||
|
progress_callback: Optional[Callable] = None,
|
||||||
|
log_callback: Optional[Callable] = None
|
||||||
|
) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
下载并安装指定依赖
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dep_name: 依赖名称 ("wkhtmltoimage" 或 "chromium")
|
||||||
|
progress_callback: 下载进度回调
|
||||||
|
log_callback: 日志回调
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(是否成功, 错误信息)
|
||||||
|
"""
|
||||||
|
def log(msg):
|
||||||
|
if log_callback:
|
||||||
|
log_callback(msg)
|
||||||
|
|
||||||
|
# 获取下载地址
|
||||||
|
if dep_name == "wkhtmltoimage":
|
||||||
|
url = DOWNLOAD_URLS["wkhtmltopdf"]
|
||||||
|
filename = "wkhtmltox_installer.exe"
|
||||||
|
elif dep_name == "chromium":
|
||||||
|
url = DOWNLOAD_URLS["chromium"]
|
||||||
|
filename = "chromium-win64.zip"
|
||||||
|
else:
|
||||||
|
return False, f"未知依赖: {dep_name}"
|
||||||
|
|
||||||
|
# 下载到临时目录
|
||||||
|
temp_dir = tempfile.gettempdir()
|
||||||
|
download_path = os.path.join(temp_dir, filename)
|
||||||
|
|
||||||
|
log(f"正在下载 {dep_name}...")
|
||||||
|
|
||||||
|
if not download_file(url, download_path, progress_callback):
|
||||||
|
return False, "下载失败"
|
||||||
|
|
||||||
|
log("下载完成,正在安装...")
|
||||||
|
|
||||||
|
# 安装
|
||||||
|
if dep_name == "wkhtmltoimage":
|
||||||
|
success = install_wkhtmltopdf(download_path, log_callback)
|
||||||
|
if success:
|
||||||
|
return True, "安装程序已启动,请完成安装向导"
|
||||||
|
else:
|
||||||
|
return False, "启动安装程序失败"
|
||||||
|
elif dep_name == "chromium":
|
||||||
|
success = install_playwright_chromium(download_path, log_callback)
|
||||||
|
# 清理下载的zip文件
|
||||||
|
try:
|
||||||
|
os.remove(download_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if success:
|
||||||
|
return True, ""
|
||||||
|
else:
|
||||||
|
return False, "安装失败"
|
||||||
|
|
||||||
|
return False, "未知错误"
|
||||||
Reference in New Issue
Block a user