main_qml.py
6.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""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"
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()