- 新增依赖检测模块:启动时自动检测wkhtmltoimage和Playwright Chromium - 新增依赖安装对话框:缺失时提示用户一键下载安装 - 修复选项记忆功能:浏览类型、自动截图、自动上传选项现在会保存 - 优化KDocs登录检测:未登录时自动切换到金山文档页面并显示二维码 - 简化日志输出:移除debug信息,保留用户友好的状态提示 - 新增账号变化信号:账号管理页面的修改会自动同步到浏览任务页面 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
394 lines
16 KiB
Python
394 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Browse task panel - select accounts, browsing type, execute task with progress
|
||
"""
|
||
|
||
from PyQt6.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
|
||
QPushButton, QProgressBar, QGroupBox, QCheckBox, QListWidget,
|
||
QListWidgetItem, QMessageBox, QScrollArea, QFrame, QSizePolicy
|
||
)
|
||
from PyQt6.QtCore import Qt, pyqtSignal, QSize
|
||
|
||
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,
|
||
GROUP_PADDING_BOTTOM, GROUP_SPACING, FORM_LABEL_WIDTH, FORM_H_SPACING,
|
||
INPUT_HEIGHT, INPUT_MIN_WIDTH, BUTTON_HEIGHT, BUTTON_HEIGHT_NORMAL,
|
||
BUTTON_MIN_WIDTH, BUTTON_MIN_WIDTH_NORMAL, BUTTON_SPACING,
|
||
LIST_MIN_HEIGHT, LIST_ITEM_HEIGHT, PROGRESS_HEIGHT,
|
||
get_title_style, get_help_text_style
|
||
)
|
||
|
||
|
||
class BrowseWidget(QWidget):
|
||
"""Browse task panel"""
|
||
|
||
log_signal = pyqtSignal(str)
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._worker = None
|
||
self._is_running = False
|
||
self._setup_ui()
|
||
self._load_task_config()
|
||
self._refresh_accounts()
|
||
|
||
def _setup_ui(self):
|
||
main_layout = QVBoxLayout(self)
|
||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||
main_layout.setSpacing(0)
|
||
|
||
# Scroll area
|
||
scroll = QScrollArea()
|
||
scroll.setWidgetResizable(True)
|
||
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||
|
||
content = QWidget()
|
||
layout = QVBoxLayout(content)
|
||
layout.setContentsMargins(16, 12, 16, 12)
|
||
layout.setSpacing(10)
|
||
|
||
# ==================== Title + Control buttons ====================
|
||
header_layout = QHBoxLayout()
|
||
header_layout.setSpacing(SECTION_SPACING)
|
||
|
||
title = QLabel("🔄 浏览任务")
|
||
title.setStyleSheet(get_title_style())
|
||
header_layout.addWidget(title)
|
||
|
||
header_layout.addStretch()
|
||
|
||
self.stop_btn = QPushButton("⏹ 停止")
|
||
self.stop_btn.setMinimumWidth(BUTTON_MIN_WIDTH_NORMAL)
|
||
self.stop_btn.setMinimumHeight(BUTTON_HEIGHT)
|
||
self.stop_btn.setEnabled(False)
|
||
self.stop_btn.clicked.connect(self._stop_task)
|
||
header_layout.addWidget(self.stop_btn)
|
||
|
||
self.start_btn = QPushButton("▶ 开始任务")
|
||
self.start_btn.setObjectName("primary")
|
||
self.start_btn.setMinimumWidth(BUTTON_MIN_WIDTH)
|
||
self.start_btn.setMinimumHeight(BUTTON_HEIGHT)
|
||
self.start_btn.clicked.connect(self._start_task)
|
||
header_layout.addWidget(self.start_btn)
|
||
|
||
layout.addLayout(header_layout)
|
||
|
||
# ==================== Progress area (compact) ====================
|
||
progress_group = QGroupBox("任务进度")
|
||
progress_layout = QVBoxLayout(progress_group)
|
||
progress_layout.setContentsMargins(12, 8, 12, 8)
|
||
progress_layout.setSpacing(6)
|
||
|
||
# Current task + stats in one row
|
||
task_row = QHBoxLayout()
|
||
self.current_task_label = QLabel("当前任务: 无")
|
||
task_row.addWidget(self.current_task_label)
|
||
task_row.addStretch()
|
||
self.stats_label = QLabel("已处理: 0 条内容,0 个附件")
|
||
self.stats_label.setStyleSheet(get_help_text_style())
|
||
task_row.addWidget(self.stats_label)
|
||
progress_layout.addLayout(task_row)
|
||
|
||
# Progress bars in one row
|
||
prog_row = QHBoxLayout()
|
||
prog_row.setSpacing(12)
|
||
|
||
prog_row.addWidget(QLabel("总体:"))
|
||
self.total_progress = QProgressBar()
|
||
self.total_progress.setValue(0)
|
||
self.total_progress.setFixedHeight(18)
|
||
prog_row.addWidget(self.total_progress, 1)
|
||
|
||
prog_row.addWidget(QLabel("当前:"))
|
||
self.account_progress = QProgressBar()
|
||
self.account_progress.setValue(0)
|
||
self.account_progress.setFixedHeight(18)
|
||
prog_row.addWidget(self.account_progress, 1)
|
||
|
||
progress_layout.addLayout(prog_row)
|
||
|
||
layout.addWidget(progress_group)
|
||
|
||
# ==================== Browse options (compact, one row) ====================
|
||
options_group = QGroupBox("浏览选项")
|
||
options_layout = QHBoxLayout(options_group)
|
||
options_layout.setContentsMargins(12, 8, 12, 8)
|
||
options_layout.setSpacing(16)
|
||
|
||
options_layout.addWidget(QLabel("浏览类型:"))
|
||
self.browse_type_combo = QComboBox()
|
||
self.browse_type_combo.addItems(["应读", "注册前未读"])
|
||
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()
|
||
|
||
layout.addWidget(options_group)
|
||
|
||
# ==================== Account selection (compact) ====================
|
||
account_group = QGroupBox("选择账号")
|
||
account_group.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
account_layout = QVBoxLayout(account_group)
|
||
account_layout.setContentsMargins(12, 8, 12, 8)
|
||
account_layout.setSpacing(6)
|
||
|
||
# Toolbar - compact buttons with proper width
|
||
toolbar = QHBoxLayout()
|
||
toolbar.setSpacing(8)
|
||
|
||
self.select_all_btn = QPushButton("全选")
|
||
self.select_all_btn.setFixedSize(70, 28)
|
||
self.select_all_btn.setStyleSheet("QPushButton { padding: 2px 8px; }")
|
||
self.select_all_btn.clicked.connect(self._select_all)
|
||
toolbar.addWidget(self.select_all_btn)
|
||
|
||
self.deselect_all_btn = QPushButton("取消全选")
|
||
self.deselect_all_btn.setFixedSize(90, 28)
|
||
self.deselect_all_btn.setStyleSheet("QPushButton { padding: 2px 8px; }")
|
||
self.deselect_all_btn.clicked.connect(self._deselect_all)
|
||
toolbar.addWidget(self.deselect_all_btn)
|
||
|
||
toolbar.addStretch()
|
||
|
||
self.refresh_btn = QPushButton("刷新")
|
||
self.refresh_btn.setFixedSize(70, 28)
|
||
self.refresh_btn.setStyleSheet("QPushButton { padding: 2px 8px; }")
|
||
self.refresh_btn.clicked.connect(self._refresh_accounts)
|
||
toolbar.addWidget(self.refresh_btn)
|
||
|
||
account_layout.addLayout(toolbar)
|
||
|
||
# Account list - compact
|
||
self.account_list = QListWidget()
|
||
self.account_list.setMinimumHeight(120)
|
||
self.account_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||
self.account_list.setSpacing(1)
|
||
account_layout.addWidget(self.account_list, 1)
|
||
|
||
layout.addWidget(account_group, 1)
|
||
|
||
scroll.setWidget(content)
|
||
main_layout.addWidget(scroll)
|
||
|
||
def _refresh_accounts(self):
|
||
"""Refresh account list"""
|
||
self.account_list.clear()
|
||
config = get_config()
|
||
|
||
for account in config.accounts:
|
||
if account.enabled:
|
||
item = QListWidgetItem()
|
||
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||
item.setCheckState(Qt.CheckState.Checked)
|
||
display_name = account.remark if account.remark else account.username
|
||
item.setText(f"{display_name} ({account.username})")
|
||
item.setData(Qt.ItemDataRole.UserRole, account)
|
||
item.setSizeHint(item.sizeHint().expandedTo(QSize(0, LIST_ITEM_HEIGHT)))
|
||
self.account_list.addItem(item)
|
||
|
||
def _select_all(self):
|
||
"""Select all"""
|
||
for i in range(self.account_list.count()):
|
||
self.account_list.item(i).setCheckState(Qt.CheckState.Checked)
|
||
|
||
def _deselect_all(self):
|
||
"""Deselect all"""
|
||
for i in range(self.account_list.count()):
|
||
self.account_list.item(i).setCheckState(Qt.CheckState.Unchecked)
|
||
|
||
def _get_selected_accounts(self) -> list:
|
||
"""Get selected accounts"""
|
||
accounts = []
|
||
for i in range(self.account_list.count()):
|
||
item = self.account_list.item(i)
|
||
if item.checkState() == Qt.CheckState.Checked:
|
||
account = item.data(Qt.ItemDataRole.UserRole)
|
||
accounts.append(account)
|
||
return accounts
|
||
|
||
def _start_task(self):
|
||
"""Start task"""
|
||
accounts = self._get_selected_accounts()
|
||
if not accounts:
|
||
QMessageBox.information(self, "提示", "请选择至少一个账号")
|
||
return
|
||
|
||
self._is_running = True
|
||
self.start_btn.setEnabled(False)
|
||
self.stop_btn.setEnabled(True)
|
||
self._stop_requested = False
|
||
|
||
browse_type = self.browse_type_combo.currentText()
|
||
auto_screenshot = self.auto_screenshot_check.isChecked()
|
||
auto_upload = self.auto_upload_check.isChecked()
|
||
|
||
self.total_progress.setMaximum(len(accounts))
|
||
self.total_progress.setValue(0)
|
||
|
||
self.log_signal.emit(f"🚀 开始任务: {len(accounts)}个账号")
|
||
|
||
from utils.worker import Worker
|
||
|
||
def run_tasks(_signals=None, _should_stop=None):
|
||
results = []
|
||
for i, account in enumerate(accounts):
|
||
if _should_stop and _should_stop():
|
||
break
|
||
|
||
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
|
||
|
||
from core.api_browser import APIBrowser
|
||
from config import get_config as _get_config
|
||
|
||
proxy_config = None
|
||
cfg = _get_config()
|
||
if cfg.proxy.enabled and cfg.proxy.server:
|
||
proxy_config = {"server": cfg.proxy.server}
|
||
|
||
browse_result = None
|
||
# 静默模式浏览,不输出详细日志
|
||
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,
|
||
)
|
||
browse_result = {
|
||
"success": result.success,
|
||
"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
|
||
# 静默截图
|
||
ss_result = take_screenshot(
|
||
account.username,
|
||
password,
|
||
browse_type,
|
||
remark=account.remark,
|
||
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
|
||
from config import get_config as _get_config2
|
||
|
||
cfg2 = _get_config2()
|
||
if cfg2.kdocs.enabled:
|
||
uploader = get_kdocs_uploader()
|
||
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(f"📤 {display_name} 已上传文档")
|
||
else:
|
||
_signals.log.emit(f"⚠️ {display_name} 上传失败")
|
||
|
||
results.append({
|
||
"account": account.username,
|
||
"browse": browse_result,
|
||
"screenshot": screenshot_path,
|
||
})
|
||
|
||
_signals.progress.emit(i + 1, f"完成: {display_name}")
|
||
|
||
return results
|
||
|
||
def on_progress(current, message):
|
||
self.total_progress.setValue(current)
|
||
self.current_task_label.setText(f"当前任务: {message}")
|
||
|
||
def on_finished(success, message):
|
||
self._is_running = False
|
||
self.start_btn.setEnabled(True)
|
||
self.stop_btn.setEnabled(False)
|
||
self.current_task_label.setText("当前任务: 无")
|
||
self.log_signal.emit("🎉 全部任务完成")
|
||
|
||
def on_result(results):
|
||
if results:
|
||
total_items = sum(r.get("browse", {}).get("total_items", 0) for r in results if r.get("browse"))
|
||
total_attachments = sum(r.get("browse", {}).get("total_attachments", 0) for r in results if r.get("browse"))
|
||
self.stats_label.setText(f"已处理: {total_items} 条内容,{total_attachments} 个附件")
|
||
|
||
self._worker = Worker(run_tasks)
|
||
self._worker.signals.log.connect(self.log_signal.emit)
|
||
self._worker.signals.progress.connect(on_progress)
|
||
self._worker.signals.finished.connect(on_finished)
|
||
self._worker.signals.result.connect(on_result)
|
||
self._worker.start()
|
||
|
||
def _stop_task(self):
|
||
"""Stop task"""
|
||
if self._worker:
|
||
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)
|