feat: 添加依赖自动检测与安装、选项记忆、KDocs登录优化

- 新增依赖检测模块:启动时自动检测wkhtmltoimage和Playwright Chromium
- 新增依赖安装对话框:缺失时提示用户一键下载安装
- 修复选项记忆功能:浏览类型、自动截图、自动上传选项现在会保存
- 优化KDocs登录检测:未登录时自动切换到金山文档页面并显示二维码
- 简化日志输出:移除debug信息,保留用户友好的状态提示
- 新增账号变化信号:账号管理页面的修改会自动同步到浏览任务页面

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 01:28:06 +08:00
parent 83fef6dff2
commit 9743186a9e
11 changed files with 713 additions and 506 deletions

View File

@@ -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"""

View File

@@ -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)

201
ui/dependency_dialog.py Normal file
View 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()

View File

@@ -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()

View File

@@ -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):
"""窗口关闭事件"""
# 保存配置