main_qml.py 6.52 KB
"""QML 主入口 — 装载业务桥层 + Apple 风格 QtQuick UI。

启动序列:
1. 加载 config.json(API key / db_config / 历史最大数)
2. 实例化 core 业务(HistoryManager / JewelryLibraryManager / TaskQueueManager)
3. 实例化 audit_logger(落盘 + 后台 worker,task #18 全量启用)
4. 实例化 5 个桥(auth / imageGen / history / taskQueue / jewelry)+ AppState
5. 把它们注入 QQmlApplicationEngine context
6. load Main.qml

跑:.venv/Scripts/pythonw.exe qml_poc/main_qml.py
"""
import logging
import os
import sys
from pathlib import Path

from PySide6.QtCore import Property, QObject, QUrl, Signal, Slot
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle

# 确保 import 路径包含项目根(qml_poc/ 跑 main_qml.py 时也能 import core/bridges)
ROOT = Path(__file__).resolve().parent.parent
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from audit_logger import AuditLogger  # noqa: E402
from bridges.auth import AuthBridge  # noqa: E402
from bridges.history import HistoryBridge  # noqa: E402
from bridges.imagegen import ImageGenBridge  # noqa: E402
from bridges.jewelry import JewelryBridge  # noqa: E402
from bridges.taskqueue import TaskQueueBridge  # noqa: E402
from config_util import get_config_dir, get_config_path, load_config_safe  # noqa: E402
from core.history import HistoryManager  # noqa: E402
from core.jewelry import JewelryLibraryManager  # noqa: E402
from core.paths import get_app_data_path  # noqa: E402
from task_queue import TaskQueueManager  # noqa: E402

logger = logging.getLogger(__name__)


class AppState(QObject):
    """全局轻量状态。登录态委托给 AuthBridge,本类只管当前 tab 索引等纯 UI 状态。

    PoC 调试:QML_AUTO_LOGIN=1 时跳过登录直接进主窗口;task #13 后 QML 改用
    auth.loggedIn 替代 appState.loggedIn,本类的 loggedIn 字段也会被删掉。
    """
    loggedInChanged = Signal()
    currentTabChanged = Signal()

    def __init__(self, auth_bridge: AuthBridge):
        super().__init__()
        self._auth = auth_bridge
        # 兼容现有 QML:env QML_AUTO_LOGIN=1 时强制 loggedIn=True
        self._poc_force_login = os.environ.get("QML_AUTO_LOGIN", "") == "1"
        # 调试便捷:env QML_DEBUG_TAB=0/1/2 控制启动时默认 tab
        try:
            self._current_tab = int(os.environ.get("QML_DEBUG_TAB", "0"))
        except ValueError:
            self._current_tab = 0
        self._auth.loggedInChanged.connect(self.loggedInChanged.emit)

    @Property(bool, notify=loggedInChanged)
    def loggedIn(self) -> bool:
        return self._poc_force_login or self._auth.loggedIn

    @Property(int, notify=currentTabChanged)
    def currentTab(self) -> int:
        return self._current_tab

    @Slot(str, str, result=bool)
    def login(self, username: str, password: str) -> bool:
        """兼容旧 QML PoC:转发到 AuthBridge。task #13 后 QML 直接调 auth.login。"""
        return self._auth.login(username, password)

    @Slot()
    def logout(self) -> None:
        self._auth.logout()

    @Slot(int)
    def setTab(self, idx: int) -> None:
        self._current_tab = idx
        self.currentTabChanged.emit()


def _setup_logging():
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    )


def main():
    _setup_logging()
    QQuickStyle.setStyle("Basic")

    app = QGuiApplication(sys.argv)
    app.setApplicationName("珠宝壹佰图像生成器")
    app.setOrganizationName("ZB100")

    # ---- 配置 + 业务核心 ------------------------------------------------
    config_path = get_config_path()
    config, err = load_config_safe(config_path)
    if err:
        logger.warning(f"config 加载警告: {err}")

    api_key = config.get("api_key", "") or ""
    db_config = config.get("db_config")  # None 时桥层走 PoC 模式
    saved_prompts = config.get("saved_prompts", []) or []
    config_dir = get_config_dir()

    history_manager = HistoryManager()
    jewelry_manager = JewelryLibraryManager(config_dir)
    task_queue_manager = TaskQueueManager()  # 单例

    audit_logger = None
    if db_config:
        try:
            app_data = get_app_data_path()
            audit_logger = AuditLogger(
                db_config=db_config,
                queue_path=app_data / "audit_queue.ndjson",
                logs_dir=app_data / "logs",
            )
            audit_logger.start()
        except Exception as e:
            logger.warning(f"audit_logger 启动失败(不影响 UI): {e}")
            audit_logger = None

    # ---- 桥层 ----------------------------------------------------------
    auth_bridge = AuthBridge(db_config=db_config, audit_logger=audit_logger)
    image_gen_bridge = ImageGenBridge(
        task_queue_manager=task_queue_manager,
        history_manager=history_manager,
        auth_bridge=auth_bridge,
        api_key=api_key,
        saved_prompts=saved_prompts,
        config_path=config_path,
    )
    history_bridge = HistoryBridge(history_manager=history_manager)
    task_queue_bridge = TaskQueueBridge(task_queue_manager=task_queue_manager)
    jewelry_bridge = JewelryBridge(library_manager=jewelry_manager)
    app_state = AppState(auth_bridge=auth_bridge)

    # 新生成完图后让 history 列表加新行(绕过手动 refresh)
    image_gen_bridge.taskCompleted.connect(
        lambda task_id, result_path, prompt, model:
        history_bridge.addNew(Path(result_path).parent.name)
    )

    # 启动期预填一次历史
    history_bridge.refresh()

    # ---- QML 装载 ------------------------------------------------------
    engine = QQmlApplicationEngine()
    ctx = engine.rootContext()
    ctx.setContextProperty("appState", app_state)
    ctx.setContextProperty("auth", auth_bridge)
    ctx.setContextProperty("imageGen", image_gen_bridge)
    ctx.setContextProperty("history", history_bridge)
    ctx.setContextProperty("taskQueue", task_queue_bridge)
    ctx.setContextProperty("jewelry", jewelry_bridge)

    qml_dir = Path(__file__).parent / "qml"
    engine.addImportPath(str(qml_dir))
    engine.load(QUrl.fromLocalFile(str(qml_dir / "Main.qml")))

    if not engine.rootObjects():
        print("QML load failed", file=sys.stderr)
        sys.exit(1)

    rc = app.exec()

    # 退出前 flush audit 日志
    if audit_logger:
        try:
            audit_logger.shutdown(timeout=5.0)
        except Exception:
            pass

    sys.exit(rc)


if __name__ == "__main__":
    main()