"""PySide6 GUI entry point.""" from __future__ import annotations import os import sys import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed try: from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap from PySide6.QtWidgets import ( QAbstractItemView, QApplication, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QGridLayout, QHBoxLayout, QHeaderView, QInputDialog, QLabel, QLineEdit, QMainWindow, QMenu, QMessageBox, QPlainTextEdit, QProgressBar, QPushButton, QScrollArea, QSplitter, QTableView, QSpinBox, QTableWidget, QTableWidgetItem, QTabWidget, QVBoxLayout, QWidget, ) QT_IMPORT_ERROR = None except ModuleNotFoundError as exc: QApplication = None QMainWindow = object QTabWidget = None QVBoxLayout = None QWidget = object QT_IMPORT_ERROR = exc TAB_TITLES = [ "① 导入采集", "② AI生成", "③ 更新蝦皮", "④ 账号管理", "⑤ 设置", ] TAB_STYLE = """ QTabWidget::pane { border-top: 1px solid #c9d1d9; } QTabBar::tab { min-width: 128px; min-height: 34px; padding: 8px 18px; margin-right: 8px; border: 1px solid #c9d1d9; border-bottom-color: #b8c0ca; background: #f4f6f8; color: #24292f; } QTabBar::tab:selected { background: #ffffff; border-color: #687785; border-bottom-color: #ffffff; font-weight: 600; } QTabBar::tab:hover:!selected { background: #eaf2ff; } """ COLOR_SUCCESS = "#1a7f37" COLOR_DANGER = "#cf222e" COLOR_INFO = "#0969da" COLOR_PENDING = "#9a6700" COLOR_MUTED = "#6e7781" COLOR_WARNING = "#bc4c00" COLOR_DANGER_BG = "#ffebe9" STATUS_LEVEL_COLORS = { "muted": COLOR_MUTED, "info": COLOR_INFO, "success": COLOR_SUCCESS, "warning": COLOR_WARNING, "danger": COLOR_DANGER, "pending": COLOR_PENDING, } BUTTON_RADIUS_PX = 4 BUTTON_BASE_STYLE = f""" QPushButton {{ min-height: 24px; min-width: 72px; padding: 4px 10px; border: 1px solid #d0d7de; border-radius: {BUTTON_RADIUS_PX}px; background-color: #f6f8fa; color: #24292f; }} QPushButton:hover {{ background-color: #eef4ff; border-color: #8c959f; }} QPushButton:pressed {{ background-color: #d8dee4; border-color: #6e7781; }} QPushButton:focus {{ border-color: #0969da; }} QPushButton:disabled {{ background-color: #f6f8fa; border-color: #d8dee4; color: #8c959f; }} """ _RawQMessageBox = QMessageBox if QT_IMPORT_ERROR is None else None class _GuiAttributeProxy: def __init__(self, name, fallback): self._name = name self._fallback = fallback def _target(self): package = sys.modules.get("app.gui") if package is not None: value = getattr(package, self._name, self._fallback) if value is not self: return value return self._fallback def __getattr__(self, item): return getattr(self._target(), item) def __call__(self, *args, **kwargs): return self._target()(*args, **kwargs) def _package_attr(name, fallback): package = sys.modules.get("app.gui") if package is not None: value = getattr(package, name, fallback) if value is not fallback: return value return fallback def _call_package_attr(name, fallback, *args, **kwargs): return _package_attr(name, fallback)(*args, **kwargs) QMessageBox = _GuiAttributeProxy("QMessageBox", _RawQMessageBox) from .. import accounts, ai, appconfig, chrome, db, diagnostics, editor, excel, image_paths, prompts from .. import config as account_config from ..workers import BaseWorker, run_worker as _raw_run_worker def run_worker(*args, **kwargs): return _package_attr("run_worker", _raw_run_worker)(*args, **kwargs) PLAINTEXT_SECRET_TITLE = "本地明文保存提示" PLAINTEXT_API_KEY_WARNING = ( "API Key 会以本地明文保存到 data/config/ai_models.json,仅供本机调用 AI 使用。" "该文件已 gitignore,UI 打码显示,日志/导出不记录明文。" ) PLAINTEXT_PASSWORD_WARNING = ( "密码会以本地明文保存到本地 SQLite,仅供人工参考,不会自动登录/自动填。" "数据库文件已 gitignore,请勿提交或分享。" ) def _qcolor(color): return QColor(color) def _status_base_color(status): if status == "running": return COLOR_INFO if status == "failed": return COLOR_DANGER if status in {"skipped", "cancelled"}: return COLOR_MUTED return None def _status_level_color(level): return STATUS_LEVEL_COLORS.get(str(level or "muted"), COLOR_MUTED) def _status_level_for_message(message, default="muted"): text = str(message or "") if any(keyword in text for keyword in ("失败", "错误", "异常", "未登录", "不可写", "点数不足", "阻止")): return "danger" if any(keyword in text for keyword in ("没有", "请先", "未配置", "不完整", "已取消", "不能", "无法")): return "warning" if any( keyword in text for keyword in ( "完成", "成功", "已保存", "已回写", "已新增", "已删除", "已软删除", "已重置", "已刷新", "已修改", "已启动", "已复用", "已生成", "已新建", "已另存为", "已重命名", ) ): return "success" if any(keyword in text for keyword in ("正在", "开始", "进度", "检测中")): return "info" return default def _emit_status(callback, message, level=None): if callback is None: return actual_level = level or _status_level_for_message(message) try: callback(str(message), level=actual_level) except TypeError: callback(str(message)) def _danger_metric_text(text, active): if not active: return text return f'{text}' def _outline_button_style(object_name, color, hover_bg="#f6f8fa", pressed_bg="#f6f8fa"): return ( f"QPushButton#{object_name} {{ " f"color: {color}; border-color: {color}; font-weight: 600; " "}" f"QPushButton#{object_name}:hover {{ " f"background-color: {hover_bg}; border-color: {color}; " "}" f"QPushButton#{object_name}:pressed {{ " f"background-color: {pressed_bg}; border-color: {color}; " "}" f"QPushButton#{object_name}:disabled {{ " "color: #8c959f; border-color: #d8dee4; " "}" ) def _warning_outline_button_style(object_name): return _outline_button_style( object_name, COLOR_WARNING, hover_bg="#fff8f0", pressed_bg="#ffedd5", ) def _danger_outline_button_style(object_name): return _outline_button_style( object_name, COLOR_DANGER, hover_bg="#fff1f0", pressed_bg=COLOR_DANGER_BG, ) def _login_status_display(status): return f"● {status or '未知'}" def _login_status_color(status): status_text = str(status or "") if status_text == "已登录": return _qcolor(COLOR_SUCCESS) if status_text == "检测中": return _qcolor(COLOR_INFO) if "未登录" in status_text or "失败" in status_text: return _qcolor(COLOR_DANGER) return _qcolor(COLOR_MUTED) def _warning_dot_icon(size=12): pixmap = QPixmap(size, size) pixmap.fill(Qt.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.Antialiasing) painter.setPen(Qt.NoPen) painter.setBrush(QColor(COLOR_WARNING)) margin = max(1, size // 6) painter.drawEllipse(margin, margin, size - margin * 2, size - margin * 2) painter.end() return QIcon(pixmap) def _build_empty_state_card(object_name, button_text="前往账号管理"): card = QWidget() card.setObjectName(object_name) card.setStyleSheet( f"QWidget#{object_name} {{ " "background: #f6f8fa; border: 1px solid #d0d7de; " "border-radius: 6px; padding: 10px; " "}" ) layout = QHBoxLayout(card) layout.setContentsMargins(12, 10, 12, 10) label = QLabel("") label.setWordWrap(True) button = QPushButton(button_text) button.setObjectName(f"{object_name}Button") button.setVisible(False) layout.addWidget(label, 1) layout.addWidget(button) card.setVisible(False) return card, label, button def _set_empty_state(card, label, button, message=None, show_button=False): active = bool(message) card.setVisible(active) label.setText(message or "") button.setVisible(active and show_button) BATCH_PROGRESS_FIELDS = [ ("total", "总数"), ("imported", "导入"), ("collected", "已采集"), ("generated", "已生成"), ("applied", "已更新"), ("failed", "失败"), ("skipped", "略过"), ] def _summarize_batch_progress(tasks): summary = {key: 0 for key, _label in BATCH_PROGRESS_FIELDS} task_rows = list(tasks or []) summary["total"] = len(task_rows) for task in task_rows: status = getattr(task, "status", None) stage = getattr(task, "stage", None) if status == "failed": summary["failed"] += 1 elif status == "skipped": summary["skipped"] += 1 elif stage == "applied": summary["applied"] += 1 elif stage == "generated": summary["generated"] += 1 elif stage == "collected": summary["collected"] += 1 else: summary["imported"] += 1 return summary def _format_batch_progress(summary): parts = [f"{label}{summary.get(key, 0)}" for key, label in BATCH_PROGRESS_FIELDS] return "批次进度:" + " · ".join(parts) def _build_batch_progress_overview(object_name): label = QLabel("") label.setObjectName(object_name) label.setWordWrap(True) label.setStyleSheet( f"QLabel#{object_name} {{ " "background: #f6f8fa; border: 1px solid #d0d7de; " "border-radius: 6px; padding: 8px 10px; color: #24292f; " "}" ) label.setVisible(False) return label def _set_batch_progress_overview(label, tasks): summary = _summarize_batch_progress(tasks) active = summary["total"] > 0 label.setVisible(active) label.setText(_format_batch_progress(summary) if active else "") def _database_path(db_path=None, config=None) -> str: return db_path or appconfig.db_path(config) def _write_reset_run_log(db_path, task, action, message): run_id = db.create_run_log( "reset", total=1, options={ "action": action, "task_id": getattr(task, "id", None), "alias": getattr(task, "alias", None), "item_id": getattr(task, "item_id", None), }, path=db_path, ) db.add_run_log_event( run_id, message, task_id=getattr(task, "id", None), alias=getattr(task, "alias", None), item_id=getattr(task, "item_id", None), path=db_path, ) db.finish_run_log( run_id, status="done", done=1, success_count=1, failed_count=0, summary_json={ "action": action, "task_id": getattr(task, "id", None), "alias": getattr(task, "alias", None), "item_id": getattr(task, "item_id", None), }, path=db_path, ) return run_id def _safe_create_run_log(run_type, db_path=None, dry_run=False, total=0, options=None): try: return db.create_run_log( run_type, dry_run=dry_run, total=total, options=options or {}, path=db_path, ) except Exception: return None def _safe_finish_run_log(run_id, db_path=None, **fields): if run_id is None: return try: db.finish_run_log(run_id, path=db_path, **fields) except Exception: return def _safe_add_run_log_event( run_id, message, db_path=None, task=None, account=None, level="info", task_id=None, alias=None, item_id=None, ): safe_message = diagnostics.redact_log_text(message) if task is not None: task_id = getattr(task, "id", task_id) alias = getattr(task, "alias", alias) item_id = getattr(task, "item_id", item_id) if account is not None: alias = getattr(account, "alias", alias) if run_id is None: return safe_message try: db.add_run_log_event( run_id, safe_message, task_id=task_id, alias=alias, item_id=item_id, level=level, path=db_path, ) except Exception: pass return safe_message def _safe_write_diagnostic_log( message, level="INFO", step=None, task=None, account=None, elapsed_ms=None, payload=None, exc=None, log_dir=None, ): try: diagnostics.write_diagnostic_log( message, level=level, step=step, task_id=getattr(task, "id", None), alias=getattr(task, "alias", None) or getattr(account, "alias", None), item_id=getattr(task, "item_id", None), elapsed_ms=elapsed_ms, payload=payload, exc=exc, log_dir=log_dir, ) except Exception: return def _elapsed_ms(started): return int((time.monotonic() - started) * 1000) __all__ = [name for name in globals() if not name.startswith("__")]