diff --git a/config.py b/config.py index 18d1b2e..b8e881c 100644 --- a/config.py +++ b/config.py @@ -79,6 +79,14 @@ class ZSGLConfig: index_url_pattern: str = "index.aspx" +@dataclass +class TaskConfig: + """浏览任务配置""" + browse_type: str = "应读" # 浏览类型 + auto_screenshot: bool = True # 浏览后自动截图 + auto_upload: bool = False # 截图后自动上传 + + @dataclass class AppConfig: """应用总配置""" @@ -87,6 +95,7 @@ class AppConfig: screenshot: ScreenshotConfig = field(default_factory=ScreenshotConfig) proxy: ProxyConfig = field(default_factory=ProxyConfig) zsgl: ZSGLConfig = field(default_factory=ZSGLConfig) + task: TaskConfig = field(default_factory=TaskConfig) theme: str = "light" # light/dark def to_dict(self) -> dict: @@ -130,6 +139,11 @@ class AppConfig: "login_url": self.zsgl.login_url, "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, } @@ -191,6 +205,14 @@ class AppConfig: 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") diff --git a/core/api_browser.py b/core/api_browser.py index dd4feb3..3ca26f4 100644 --- a/core/api_browser.py +++ b/core/api_browser.py @@ -107,10 +107,10 @@ class APIBrowser: except Exception as e: last_error = e 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) else: - self.log(f"[API] 请求失败,已重试{max_retries}次: {str(e)}") + self.log(f" 请求失败,已重试{max_retries}次: {str(e)}") raise last_error @@ -125,7 +125,7 @@ class APIBrowser: def login(self, username: str, password: str) -> bool: """登录""" - self.log(f"[API] 登录: {username}") + self.log(f" 登录: {username}") self._username = username try: @@ -152,17 +152,17 @@ class APIBrowser: if self.index_url_pattern in resp.url: self.logged_in = True - self.log(f"[API] 登录成功") + self.log(f" 登录成功") return True else: soup = BeautifulSoup(resp.text, "html.parser") error = soup.find(id="lblMsg") error_msg = error.get_text().strip() if error else "未知错误" - self.log(f"[API] 登录失败: {error_msg}") + self.log(f" 登录失败: {error_msg}") return False except Exception as e: - self.log(f"[API] 登录异常: {str(e)}") + self.log(f" 登录异常: {str(e)}") return False def get_real_name(self) -> Optional[str]: @@ -217,10 +217,10 @@ class APIBrowser: with open(cookies_path, "w", encoding="utf-8") as f: f.write("\n".join(lines) + "\n") - self.log(f"[API] Cookies已保存供截图使用") + self.log(f" Cookies已保存供截图使用") return True except Exception as e: - self.log(f"[API] 保存cookies失败: {e}") + self.log(f" 保存cookies失败: {e}") return False def get_article_list_page(self, bz: int = 0, page: int = 1) -> tuple: @@ -377,7 +377,7 @@ class APIBrowser: # 根据浏览类型确定bz参数(网站更新后 bz=0 为应读) bz = 0 - self.log(f"[API] 开始浏览 '{browse_type}' (bz={bz})...") + self.log(f" 开始浏览 '{browse_type}' (bz={bz})...") try: total_items = 0 @@ -387,12 +387,12 @@ class APIBrowser: articles, total_pages, _ = self.get_article_list_page(bz, 1) if not articles: - self.log(f"[API] '{browse_type}' 没有待处理内容") + self.log(f" '{browse_type}' 没有待处理内容") result.success = True return result total_records = self.last_total_records - self.log(f"[API] 共 {total_records} 条记录,开始处理...") + self.log(f" 共 {total_records} 条记录,开始处理...") # 上报初始进度 if progress_callback: @@ -404,7 +404,7 @@ class APIBrowser: for iteration in range(max_iterations): if should_stop_callback and should_stop_callback(): - self.log("[API] 收到停止信号") + self.log(" 收到停止信号") break if not articles: @@ -428,7 +428,7 @@ class APIBrowser: try: attachments, article_info = self.get_article_attachments(article_href) except Exception as e: - self.log(f"[API] 获取文章失败: {title} | {str(e)}") + self.log(f" 获取文章失败: {title} | {str(e)}") continue total_items += 1 @@ -446,10 +446,10 @@ class APIBrowser: for attach in attachments: if self.mark_attachment_read(attach["id"], attach["channel_id"]): total_attachments += 1 - self.log(f"[API] [{total_items}] {title} - {len(attachments)}个附件") + self.log(f" [{total_items}] {title} - {len(attachments)}个附件") else: status = "已标记" if article_marked else "标记失败" - self.log(f"[API] [{total_items}] {title} - 无附件({status})") + self.log(f" [{total_items}] {title} - 无附件({status})") # 上报进度 if progress_callback: @@ -472,10 +472,10 @@ class APIBrowser: if new_total_pages > 0: total_pages = new_total_pages except Exception as e: - self.log(f"[API] 获取第{current_page}页列表失败: {str(e)}") + self.log(f" 获取第{current_page}页列表失败: {str(e)}") break - self.log(f"[API] 浏览完成: {total_items} 条内容,{total_attachments} 个附件") + self.log(f" 浏览完成: {total_items} 条内容,{total_attachments} 个附件") result.success = True result.total_items = total_items result.total_attachments = total_attachments @@ -483,7 +483,7 @@ class APIBrowser: except Exception as e: result.error_message = str(e) - self.log(f"[API] 浏览出错: {str(e)}") + self.log(f" 浏览出错: {str(e)}") return result def close(self): diff --git a/core/kdocs_uploader.py b/core/kdocs_uploader.py index ae6122f..4c58b1c 100644 --- a/core/kdocs_uploader.py +++ b/core/kdocs_uploader.py @@ -41,6 +41,40 @@ class KDocsUploader: if self._log_callback: 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: """确保Playwright已启动""" if sync_playwright is None: @@ -53,9 +87,8 @@ class KDocsUploader: if self._playwright is None: self._playwright = sync_playwright().start() if self._browser is None: - # 调试模式:有头模式,方便查看浏览器行为 - # 生产环境改回 "true" - headless = os.environ.get("KDOCS_HEADLESS", "false").lower() != "false" + # 默认无头模式,设置环境变量 KDOCS_HEADLESS=false 可切换为有头模式调试 + headless = os.environ.get("KDOCS_HEADLESS", "true").lower() != "false" # 使用系统安装的Chrome浏览器(支持微信快捷登录) # channel='chrome' 会使用系统Chrome,而不是Playwright自带的Chromium chrome_args = [ @@ -69,10 +102,8 @@ class KDocsUploader: channel='chrome', # 使用系统Chrome args=chrome_args ) - self.log("[KDocs] 使用系统Chrome浏览器") except Exception as e: # 如果系统没有Chrome,回退到Chromium - self.log(f"[KDocs] 系统Chrome不可用({e}),使用Chromium") self._browser = self._playwright.chromium.launch(headless=headless, args=chrome_args) if self._context is None: storage_state = str(KDOCS_LOGIN_STATE_FILE) @@ -162,7 +193,6 @@ class KDocsUploader: try: join_btn = page.get_by_role("button", name="登录并加入编辑") if join_btn.count() > 0 and join_btn.first.is_visible(timeout=500): - self.log("[KDocs] 点击加入编辑按钮") join_btn.first.click() time.sleep(1) except Exception: @@ -172,7 +202,6 @@ class KDocsUploader: # 检查是否在登录页面 if self._is_login_url(url): - self.log(f"[KDocs] 检测到登录页面URL: {url}") return True # 只检查登录页面上的登录按钮(排除文档页面的邀请对话框) @@ -181,7 +210,6 @@ class KDocsUploader: try: btn = page.get_by_role("button", name=text) if btn.count() > 0 and btn.first.is_visible(timeout=500): - self.log(f"[KDocs] 检测到登录按钮: {text}") return True except Exception: pass @@ -198,7 +226,6 @@ class KDocsUploader: if el.is_visible(timeout=200): box = el.bounding_box() if box and 80 <= box.get("width", 0) <= 400: - self.log(f"[KDocs] 检测到二维码元素: {selector}") return True except Exception: pass @@ -220,120 +247,60 @@ class KDocsUploader: storage_state = str(KDOCS_LOGIN_STATE_FILE) KDOCS_LOGIN_STATE_FILE.parent.mkdir(parents=True, exist_ok=True) self._context.storage_state(path=storage_state) - self.log("[KDocs] 登录状态已保存") - except Exception as e: - self.log(f"[KDocs] 保存登录状态失败: {e}") + except Exception: + pass def _ensure_login_dialog(self, use_quick_login: bool = False): - """确保打开登录对话框 + """确保打开登录对话框并进入扫码页面""" + buttons_priority = [ + "登录并加入编辑", + "立即登录", + "去登录", + ] - Args: - use_quick_login: 是否尝试使用微信快捷登录 - """ - agree_names = ["同意", "同意并继续", "我同意", "确定", "确认"] - - # 循环处理登录流程 - max_clicks = 8 - for round_num in range(max_clicks): + max_clicks = 12 + for _ in range(max_clicks): clicked = False current_url = self._page.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] 已到达文档页面,登录成功") + if "kdocs.cn/l/" in current_url and "account.wps.cn" not in current_url: + time.sleep(1) + has_login_elements = ( + self._find_visible_element("立即登录", use_role=True) or + self._find_visible_element("登录并加入编辑", use_role=True) or + self._find_visible_element("微信扫码登录") or + self._find_visible_element("微信快捷登录") + ) + if not has_login_elements: 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) - clicked = True - break - except Exception: - pass - if clicked: - continue + # 检查是否已经到达登录二维码页面 + qr_page_indicators = ["微信扫码登录", "微信快捷登录"] + for indicator in qr_page_indicators: + if self._find_visible_element(indicator): + return - # 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 - clicked = True - continue - except Exception: - pass - - # 3. 点击"立即登录"进入登录页面 - 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() + # 按优先级点击登录按钮 + 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) clicked = True - continue - except Exception: - pass + break - # 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 + for btn_name in buttons_priority: + el = self._find_visible_element(btn_name) + if el: + el.click(force=True) + time.sleep(2) + clicked = True + break - # 最后确保点击微信扫码登录(切换到扫码模式) - wechat_names = ["微信登录", "微信扫码登录", "扫码登录", "微信扫码"] - for name in wechat_names: - try: - btn = self._page.get_by_role("button", name=name) - if btn.is_visible(timeout=1000): - self.log(f"[KDocs] 点击微信登录: {name}") - btn.click() - time.sleep(1) - return - except Exception: - pass - - # 尝试用文本查找微信登录 - 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) - return - except Exception: - pass - - self.log("[KDocs] 未找到登录按钮,可能页面已在登录状态或需要手动操作") + if not clicked: + time.sleep(1) def _capture_qr_image(self) -> Optional[bytes]: """捕获登录二维码图片""" @@ -436,31 +403,24 @@ class KDocsUploader: return {"success": False, "error": self._last_error or "打开文档失败"} # 检查是否已登录 - self.log(f"[KDocs] 当前页面URL: {self._page.url}") if not force and self._is_logged_in(): self._logged_in = True self._save_login_state() return {"success": True, "logged_in": True, "qr_image": ""} # 需要登录,获取二维码 - self.log("[KDocs] 需要登录,尝试打开登录对话框...") self._ensure_login_dialog() - time.sleep(2) # 等待登录对话框加载 + time.sleep(2) - self.log("[KDocs] 尝试捕获二维码...") qr_image = None - for i in range(15): # 增加尝试次数 + for _ in range(15): qr_image = self._capture_qr_image() if qr_image and len(qr_image) > 1024: - self.log(f"[KDocs] 二维码捕获成功,大小: {len(qr_image)} bytes") break - self.log(f"[KDocs] 第{i+1}次尝试捕获二维码...") time.sleep(1) if not qr_image: - # 尝试截取整个页面帮助调试 - self.log("[KDocs] 二维码捕获失败,当前页面可能没有显示二维码") - return {"success": False, "error": "二维码获取失败,请检查网络或手动打开金山文档链接确认"} + return {"success": False, "error": "二维码获取失败,请检查网络"} return { "success": True, @@ -490,7 +450,6 @@ class KDocsUploader: try: confirm_btn = frame.get_by_role("button", name=name) if confirm_btn.count() > 0 and confirm_btn.first.is_visible(timeout=200): - self.log(f"[KDocs] 找到确认按钮: {name}") confirm_btn.first.click() clicked_confirm = True time.sleep(3) @@ -504,7 +463,6 @@ class KDocsUploader: try: el = frame.get_by_text(name, exact=True) if el.count() > 0 and el.first.is_visible(timeout=200): - self.log(f"[KDocs] 找到确认文本: {name}") el.first.click() clicked_confirm = True time.sleep(3) @@ -515,7 +473,6 @@ class KDocsUploader: # 尝试用CSS选择器查找 if not clicked_confirm: try: - # WPS登录页面的确认按钮可能的选择器 selectors = [ "button.ant-btn-primary", "button[type='primary']", @@ -537,7 +494,6 @@ class KDocsUploader: if btn.is_visible(timeout=100): btn_text = btn.inner_text() or "" if any(kw in btn_text for kw in ["确认", "登录", "确定"]): - self.log(f"[KDocs] 找到按钮(CSS): {btn_text}") btn.click() clicked_confirm = True time.sleep(3) @@ -549,37 +505,28 @@ class KDocsUploader: except Exception: pass - # 如果点击了确认按钮,等待页面自动跳转(不要reload!) if clicked_confirm: - self.log("[KDocs] 已点击确认,等待页面跳转...") - time.sleep(3) # 等待页面自动跳转 + time.sleep(3) - # 检查当前URL是否已经到达文档页面 current_url = self._page.url - self.log(f"[KDocs] 当前URL: {current_url}") # 直接检查URL判断是否已登录 if "kdocs.cn/l/" in current_url and "account.wps.cn" not in current_url: - # 已到达文档页面,登录成功 logged_in = True - # 尝试点击可能存在的"加入编辑"按钮 try: join_btn = self._page.get_by_role("button", name="登录并加入编辑") if join_btn.count() > 0 and join_btn.first.is_visible(timeout=500): - self.log("[KDocs] 点击加入编辑") join_btn.first.click() time.sleep(1) except Exception: pass else: - # 还在登录页面或其他页面 logged_in = self._is_logged_in() self._logged_in = logged_in if logged_in: self._save_login_state() - self.log("[KDocs] 登录状态检测:已登录") return {"success": True, "logged_in": logged_in} @@ -701,7 +648,6 @@ class KDocsUploader: file_chooser.set_files(image_path) time.sleep(2) - self.log(f"[KDocs] 图片已上传到 {cell_address}") return True except Exception as e: @@ -764,7 +710,6 @@ class KDocsUploader: pass # 搜索姓名找到行 - self.log(f"[KDocs] 搜索人员: {name}") row_num = self._search_and_get_row( name, expected_col=kdocs_config.name_column, @@ -775,8 +720,6 @@ class KDocsUploader: if row_num < 0: 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): return {"success": True} diff --git a/core/screenshot.py b/core/screenshot.py index 78bccaf..c03d41a 100644 --- a/core/screenshot.py +++ b/core/screenshot.py @@ -312,7 +312,6 @@ def take_screenshot( ) if success and os.path.exists(screenshot_path) and os.path.getsize(screenshot_path) > 1000: - log(f"[OK] 截图成功: {screenshot_filename}") result.success = True result.filename = screenshot_filename result.filepath = screenshot_path diff --git a/main.py b/main.py index bb039e0..d5ddbf3 100644 --- a/main.py +++ b/main.py @@ -55,32 +55,44 @@ def main(): window = MainWindow() window.show() - # Check for JSON to SQLite migration + # Check for JSON to SQLite migration (silent) from config import CONFIG_FILE if CONFIG_FILE.exists(): from utils.storage import migrate_from_json - window.log("检测到旧JSON配置,正在迁移到SQLite...") - if migrate_from_json(): - window.log("✅ 配置迁移成功") - else: - window.log("⚠️ 配置迁移失败,将使用默认配置") + migrate_from_json() - # 启动日志 - window.log("应用启动成功") - window.log(f"数据目录: {os.path.abspath('data')}") + # 简洁启动日志 + window.log("✅ 应用启动成功") + window.log("✅ 数据加载成功") - # Show database info - from utils.storage import _get_db_path - db_path = _get_db_path() - window.log(f"数据库: {db_path}") + # 检查依赖(wkhtmltoimage和Playwright Chromium) + from utils.dependency_installer import get_missing_dependencies + missing_deps = get_missing_dependencies() - # 检查wkhtmltoimage - from core.screenshot import _resolve_wkhtmltoimage_path - wkhtml = _resolve_wkhtmltoimage_path() - if wkhtml: - window.log(f"wkhtmltoimage: {wkhtml}") + # 如果有缺失的依赖,显示安装对话框 + 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: - window.log("⚠️ 警告: 未找到 wkhtmltoimage,截图功能可能不可用") + window.log("✅ 运行环境已就绪") + + # 启动时自动检测金山文档登录状态(后台无头模式) + window.init_kdocs_login_check() sys.exit(app.exec()) diff --git a/ui/account_widget.py b/ui/account_widget.py index b88835e..6ec26f5 100644 --- a/ui/account_widget.py +++ b/ui/account_widget.py @@ -136,6 +136,7 @@ class AccountWidget(QWidget): """Account management panel""" log_signal = pyqtSignal(str) + accounts_changed = pyqtSignal() # 账号列表变化时发出通知 def __init__(self, parent=None): super().__init__(parent) @@ -315,6 +316,7 @@ class AccountWidget(QWidget): save_config(config) self._load_accounts() self.log_signal.emit(f"账号 {account.username} 添加成功") + self.accounts_changed.emit() # 通知其他模块账号列表已更新 def _edit_account(self, row: int): """Edit account""" @@ -326,6 +328,7 @@ class AccountWidget(QWidget): save_config(config) self._load_accounts() self.log_signal.emit(f"账号 {account.username} 已更新") + self.accounts_changed.emit() # 通知其他模块账号列表已更新 def _delete_account(self, row: int): """Delete account""" @@ -342,6 +345,7 @@ class AccountWidget(QWidget): save_config(config) self._load_accounts() self.log_signal.emit(f"账号 {account.username} 已删除") + self.accounts_changed.emit() # 通知其他模块账号列表已更新 def _toggle_enabled(self, row: int, state: int): """Toggle account enabled state""" @@ -349,6 +353,7 @@ class AccountWidget(QWidget): if row < len(config.accounts): config.accounts[row].enabled = state == Qt.CheckState.Checked.value save_config(config) + self.accounts_changed.emit() # 通知其他模块账号启用状态已更新 def _test_login(self): """Test login for selected account""" diff --git a/ui/browse_widget.py b/ui/browse_widget.py index d2be29e..ea13dfa 100644 --- a/ui/browse_widget.py +++ b/ui/browse_widget.py @@ -11,7 +11,7 @@ from PyQt6.QtWidgets import ( ) 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 .constants import ( PAGE_PADDING, SECTION_SPACING, GROUP_PADDING_TOP, GROUP_PADDING_SIDE, @@ -33,6 +33,7 @@ class BrowseWidget(QWidget): self._worker = None self._is_running = False self._setup_ui() + self._load_task_config() self._refresh_accounts() def _setup_ui(self): @@ -125,16 +126,19 @@ class BrowseWidget(QWidget): self.browse_type_combo.setMinimumWidth(140) self.browse_type_combo.setFixedHeight(32) 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.addSpacing(20) self.auto_screenshot_check = QCheckBox("浏览后自动截图") self.auto_screenshot_check.setChecked(True) + self.auto_screenshot_check.stateChanged.connect(self._save_task_config) options_layout.addWidget(self.auto_screenshot_check) self.auto_upload_check = QCheckBox("截图后自动上传") 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.addStretch() @@ -241,7 +245,7 @@ class BrowseWidget(QWidget): self.total_progress.setMaximum(len(accounts)) 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 @@ -251,8 +255,8 @@ class BrowseWidget(QWidget): if _should_stop and _should_stop(): break - _signals.log.emit(f"[{i+1}/{len(accounts)}] 处理账号: {account.username}") - _signals.progress.emit(i, f"正在处理: {account.username}") + display_name = account.remark or account.username + _signals.progress.emit(i, f"正在处理: {display_name}") 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} 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): browser.save_cookies_for_screenshot(account.username) - result = browser.browse_content( browse_type, should_stop_callback=_should_stop, @@ -278,22 +282,28 @@ class BrowseWidget(QWidget): "total_items": result.total_items, "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 if browse_result and browse_result.get("success") and auto_screenshot: from core.screenshot import take_screenshot - - _signals.log.emit("正在截图...") + # 静默截图 ss_result = take_screenshot( account.username, password, browse_type, remark=account.remark, - log_callback=lambda msg: _signals.log.emit(msg), + log_callback=None, proxy_config=proxy_config ) if ss_result.success: screenshot_path = ss_result.filepath + _signals.log.emit(f"📸 {display_name} 截图完成") + else: + _signals.log.emit(f"⚠️ {display_name} 截图失败") if screenshot_path and auto_upload: from core.kdocs_uploader import get_kdocs_uploader @@ -301,18 +311,17 @@ class BrowseWidget(QWidget): cfg2 = _get_config2() if cfg2.kdocs.enabled: - _signals.log.emit("正在上传到金山文档...") uploader = get_kdocs_uploader() - uploader._log_callback = lambda msg: _signals.log.emit(msg) + uploader._log_callback = None # 静默模式 upload_result = uploader.upload_image( screenshot_path, cfg2.kdocs.unit, account.remark or account.username ) if upload_result.get("success"): - _signals.log.emit("[OK] 上传成功") + _signals.log.emit(f"📤 {display_name} 已上传文档") else: - _signals.log.emit(f"上传失败: {upload_result.get('error', '未知错误')}") + _signals.log.emit(f"⚠️ {display_name} 上传失败") results.append({ "account": account.username, @@ -320,7 +329,7 @@ class BrowseWidget(QWidget): "screenshot": screenshot_path, }) - _signals.progress.emit(i + 1, f"完成: {account.username}") + _signals.progress.emit(i + 1, f"完成: {display_name}") return results @@ -332,8 +341,8 @@ class BrowseWidget(QWidget): self._is_running = False self.start_btn.setEnabled(True) self.stop_btn.setEnabled(False) - self.current_task_label.setText("当前任务: 完成") - self.log_signal.emit(f"任务完成: {message}") + self.current_task_label.setText("当前任务: 无") + self.log_signal.emit("🎉 全部任务完成") def on_result(results): if results: @@ -354,3 +363,31 @@ class BrowseWidget(QWidget): self._worker.stop() self.log_signal.emit("正在停止任务...") 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) diff --git a/ui/dependency_dialog.py b/ui/dependency_dialog.py new file mode 100644 index 0000000..f465f96 --- /dev/null +++ b/ui/dependency_dialog.py @@ -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() diff --git a/ui/kdocs_widget.py b/ui/kdocs_widget.py index 5075918..0abb3f6 100644 --- a/ui/kdocs_widget.py +++ b/ui/kdocs_widget.py @@ -34,7 +34,6 @@ class KDocsWidget(QWidget): self._login_check_timer = None self._setup_ui() self._load_config() - self._check_wechat_process() # 检测微信进程 def _setup_ui(self): main_layout = QVBoxLayout(self) @@ -191,29 +190,6 @@ class KDocsWidget(QWidget): self.get_qr_btn.clicked.connect(self._get_qr_code) 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.setFixedHeight(36) self.clear_login_btn.setMinimumWidth(120) @@ -271,7 +247,7 @@ class KDocsWidget(QWidget): """Get login QR code and poll for login status""" self.get_qr_btn.setEnabled(False) self.qr_label.setText("获取中...") - self.log_signal.emit("正在获取金山文档登录二维码...") + self.log_signal.emit("🔄 正在获取登录二维码...") # 停止之前的检查 if self._login_check_timer: @@ -281,12 +257,12 @@ class KDocsWidget(QWidget): from utils.worker import Worker def get_qr_and_wait_login(_signals=None, _should_stop=None): - """获取二维码并轮询等待登录(在同一个线程中完成所有Playwright操作)""" + """获取二维码并轮询等待登录""" 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 + uploader._log_callback = None # 静默模式 # 1. 获取二维码 result = uploader.request_qr(force=True) @@ -301,11 +277,10 @@ class KDocsWidget(QWidget): if qr_image: _signals.screenshot_ready.emit(qr_image) # 复用这个信号传递二维码 - # 3. 在同一个线程中轮询检查登录状态 - max_wait = 120 # 最多等待120秒 - check_interval = 3 # 每3秒检查一次 + # 3. 轮询检查登录状态 + max_wait = 120 + check_interval = 3 waited = 0 - check_count = 0 while waited < max_wait: if _should_stop and _should_stop(): @@ -313,21 +288,12 @@ class KDocsWidget(QWidget): time.sleep(check_interval) waited += check_interval - check_count += 1 - # 检查登录状态(在同一个线程中,不会有线程问题) - if _signals: - _signals.log.emit(f"[KDocs] 检查登录状态... ({check_count})") check_result = uploader.check_login_status() if check_result.get("logged_in"): return {"success": True, "logged_in": True} - # 每30秒提醒一下用户 - if waited % 30 == 0 and _signals: - remaining = max_wait - waited - _signals.log.emit(f"[KDocs] 等待扫码登录...(剩余{remaining}秒)") - - return {"success": False, "error": "登录超时(120秒),请重新获取二维码"} + return {"success": False, "error": "登录超时,请重新获取二维码"} def on_qr_ready(qr_base64): """二维码准备好了,显示出来""" @@ -337,7 +303,7 @@ class KDocsWidget(QWidget): pixmap.loadFromData(qr_bytes) scaled = pixmap.scaled(140, 140, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) self.qr_label.setPixmap(scaled) - self.log_signal.emit("请使用微信扫描二维码登录(120秒内有效)") + self.log_signal.emit("📱 请使用微信扫描二维码登录") def on_result(result): self.get_qr_btn.setEnabled(True) @@ -346,21 +312,18 @@ class KDocsWidget(QWidget): self.status_label.setText("✅ 已登录") self.status_label.setStyleSheet(get_status_style(True)) self.qr_label.setText("登录成功!") - self.log_signal.emit("金山文档登录成功") - else: - # 不应该走到这里,因为上面会返回logged_in=True - pass + self.log_signal.emit("✅ 金山文档登录成功") 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.setStyleSheet(get_status_style(False)) - self.log_signal.emit(f"登录失败: {error}") + self.log_signal.emit(f"❌ 登录失败: {error}") def on_error(error): self.get_qr_btn.setEnabled(True) - self.qr_label.setText(f"错误: {error}") - self.log_signal.emit(f"获取二维码出错: {error}") + self.qr_label.setText("获取失败") + self.log_signal.emit(f"❌ 二维码获取失败: {error}") self._worker = Worker(get_qr_and_wait_login) self._worker.signals.log.connect(self.log_signal.emit) @@ -378,273 +341,9 @@ class KDocsWidget(QWidget): self.status_label.setText("❌ 未登录") self.status_label.setStyleSheet(get_status_style(False)) self.qr_label.setText("点击获取二维码") - self.log_signal.emit("金山文档登录状态已清除") + self.log_signal.emit("🔓 登录状态已清除") if self._login_check_timer: self._login_check_timer.stop() 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() - diff --git a/ui/main_window.py b/ui/main_window.py index 9737b77..4590334 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -9,7 +9,7 @@ from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, 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 .log_widget import LogWidget @@ -157,6 +157,9 @@ class MainWindow(QMainWindow): self.kdocs_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): """切换页面""" # 更新导航按钮状态 @@ -176,6 +179,64 @@ class MainWindow(QMainWindow): """记录日志""" 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): """窗口关闭事件""" # 保存配置 diff --git a/utils/dependency_installer.py b/utils/dependency_installer.py new file mode 100644 index 0000000..c6d022b --- /dev/null +++ b/utils/dependency_installer.py @@ -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, "未知错误"