更新
This commit is contained in:
+109
-47
@@ -15,6 +15,7 @@ from pathlib import Path
|
||||
from PySide6.QtCore import (
|
||||
QEasingCurve,
|
||||
QEvent,
|
||||
QObject,
|
||||
QPoint,
|
||||
QPointF,
|
||||
QPropertyAnimation,
|
||||
@@ -74,26 +75,21 @@ from app_version import APP_VERSION, release_status
|
||||
from runtime_paths import application_data_dir, resource_path
|
||||
|
||||
|
||||
try:
|
||||
import __main__ as _legacy
|
||||
|
||||
if not hasattr(_legacy, "BotThread"):
|
||||
raise ImportError
|
||||
except ImportError:
|
||||
import wechat_gui as _legacy
|
||||
|
||||
|
||||
BotThread = _legacy.BotThread
|
||||
LogQueue = _legacy.LogQueue
|
||||
MESSAGE_BATCH_WINDOW_SECONDS = getattr(
|
||||
_legacy, "MESSAGE_BATCH_WINDOW_SECONDS", 20.0
|
||||
# 直接用 GUI 无关的共享运行时;过去 import wechat_gui 会连带把整个
|
||||
# tkinter/tcl 拖进 Qt 进程,冷启动白付一笔加载费。
|
||||
from gui_runtime import (
|
||||
BotThread,
|
||||
LogQueue,
|
||||
normalize_message_batch_window_seconds,
|
||||
)
|
||||
MESSAGE_BATCH_WINDOW_MIN_SECONDS = int(getattr(
|
||||
_legacy, "MESSAGE_BATCH_WINDOW_MIN_SECONDS", 1.0
|
||||
))
|
||||
MESSAGE_BATCH_WINDOW_MAX_SECONDS = int(getattr(
|
||||
_legacy, "MESSAGE_BATCH_WINDOW_MAX_SECONDS", 120.0
|
||||
))
|
||||
from gui_runtime import (
|
||||
MESSAGE_BATCH_WINDOW_SECONDS,
|
||||
MESSAGE_BATCH_WINDOW_MAX_SECONDS as _BATCH_MAX,
|
||||
MESSAGE_BATCH_WINDOW_MIN_SECONDS as _BATCH_MIN,
|
||||
)
|
||||
|
||||
MESSAGE_BATCH_WINDOW_MIN_SECONDS = int(_BATCH_MIN)
|
||||
MESSAGE_BATCH_WINDOW_MAX_SECONDS = int(_BATCH_MAX)
|
||||
SCRIPT_DIR = application_data_dir()
|
||||
APP_SETTINGS_FILE = SCRIPT_DIR / "app_settings.json"
|
||||
CUSTOMER_SERVICE_URL = "http://kf.zhenyangtang.com.cn/"
|
||||
@@ -385,18 +381,37 @@ class PortalWebView(QWebEngineView):
|
||||
|
||||
|
||||
class PortalPage(QWidget):
|
||||
"""AI 客服网页页。
|
||||
|
||||
WebEngine 视图按需创建:Chromium 子进程初始化在低配机上要好几秒、常驻
|
||||
两三百 MB 内存,构造期就拉起会拖慢整个窗口的首帧。
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("PageRoot")
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
self._layout = QVBoxLayout(self)
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.setSpacing(0)
|
||||
|
||||
self.progress = QProgressBar()
|
||||
self.progress.setRange(0, 100)
|
||||
self.progress.hide()
|
||||
layout.addWidget(self.progress)
|
||||
self._layout.addWidget(self.progress)
|
||||
|
||||
self._placeholder = QLabel("正在准备 AI 客服页面…")
|
||||
self._placeholder.setAlignment(Qt.AlignCenter)
|
||||
self._placeholder.setStyleSheet("color:#617269;font-size:14px;background:#f4f7f5;")
|
||||
self._layout.addWidget(self._placeholder, 1)
|
||||
|
||||
self.view: PortalWebView | None = None
|
||||
self.page: QWebEnginePage | None = None
|
||||
self.profile: QWebEngineProfile | None = None
|
||||
|
||||
def ensure_view(self) -> None:
|
||||
"""真正创建 WebEngine 视图;重复调用无副作用。"""
|
||||
if self.view is not None:
|
||||
return
|
||||
self.view = PortalWebView()
|
||||
self.view.setFocusPolicy(Qt.StrongFocus)
|
||||
self.view.settings().setAttribute(
|
||||
@@ -421,7 +436,8 @@ class PortalPage(QWidget):
|
||||
self.view.loadStarted.connect(self._load_started)
|
||||
self.view.loadProgress.connect(self.progress.setValue)
|
||||
self.view.loadFinished.connect(self._load_finished)
|
||||
layout.addWidget(self.view, 1)
|
||||
self._placeholder.hide()
|
||||
self._layout.addWidget(self.view, 1)
|
||||
|
||||
if os.environ.get("WECOM_RPA_DISABLE_PORTAL") != "1":
|
||||
self.view.setUrl(QUrl(CUSTOMER_SERVICE_URL))
|
||||
@@ -432,6 +448,10 @@ class PortalPage(QWidget):
|
||||
"AI 客服网页在测试模式下未加载</div></body></html>"
|
||||
)
|
||||
|
||||
def focus_view(self) -> None:
|
||||
if self.view is not None:
|
||||
self.view.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def _install_light_theme(self) -> None:
|
||||
css_path = resource_path("edge_light_theme", "light-theme.css")
|
||||
try:
|
||||
@@ -471,10 +491,14 @@ class PortalPage(QWidget):
|
||||
self.view.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
def reload(self) -> None:
|
||||
self.view.reload()
|
||||
if self.view is None:
|
||||
self.ensure_view()
|
||||
else:
|
||||
self.view.reload()
|
||||
|
||||
def open_external(self) -> None:
|
||||
QDesktopServices.openUrl(self.view.url() or QUrl(CUSTOMER_SERVICE_URL))
|
||||
url = self.view.url() if self.view is not None else QUrl()
|
||||
QDesktopServices.openUrl(url if url and not url.isEmpty() else QUrl(CUSTOMER_SERVICE_URL))
|
||||
|
||||
|
||||
class MetricCard(QFrame):
|
||||
@@ -1689,6 +1713,9 @@ class LogPage(QWidget):
|
||||
self.editor = QTextEdit()
|
||||
self.editor.setReadOnly(True)
|
||||
self.editor.setAcceptRichText(True)
|
||||
# 机器人每轮轮询都在打日志,跑一天就是几万块富文本;不封顶的话文档
|
||||
# 越长每次追加越慢,整个界面跟着卡。磁盘副本是全量的,界面只留近况。
|
||||
self.editor.document().setMaximumBlockCount(2000)
|
||||
self.editor.setStyleSheet(
|
||||
"QTextEdit{font-family:'Cascadia Mono','Microsoft YaHei UI';font-size:13px;line-height:1.5;}"
|
||||
)
|
||||
@@ -2193,7 +2220,7 @@ class MainWindow(QMainWindow):
|
||||
settings["poll_interval"] = max(0.2, float(settings["poll_interval"]))
|
||||
settings["mouse_idle_seconds"] = max(0.0, float(settings["mouse_idle_seconds"]))
|
||||
settings["message_batch_window_seconds"] = (
|
||||
_legacy.normalize_message_batch_window_seconds(
|
||||
normalize_message_batch_window_seconds(
|
||||
settings["message_batch_window_seconds"]
|
||||
)
|
||||
)
|
||||
@@ -2234,7 +2261,8 @@ class MainWindow(QMainWindow):
|
||||
if index == 5:
|
||||
self.queue_page.refresh_data()
|
||||
if index == 0:
|
||||
QTimer.singleShot(80, lambda: self.portal_page.view.setFocus(Qt.OtherFocusReason))
|
||||
self.portal_page.ensure_view()
|
||||
QTimer.singleShot(80, self.portal_page.focus_view)
|
||||
|
||||
def _refresh_queue_page(self) -> None:
|
||||
"""队列页开着的时候,让它跟着机器人一起动。"""
|
||||
@@ -2303,9 +2331,11 @@ class MainWindow(QMainWindow):
|
||||
previous_state = event.oldState()
|
||||
saved_geometry = self.saveGeometry()
|
||||
was_maximized = bool(previous_state & Qt.WindowMaximized)
|
||||
portal_view = self.portal_page.view
|
||||
portal_was_visible = bool(
|
||||
self.stack.currentIndex() == 0
|
||||
and not self.portal_page.view.isHidden()
|
||||
and portal_view is not None
|
||||
and not portal_view.isHidden()
|
||||
)
|
||||
QTimer.singleShot(
|
||||
0,
|
||||
@@ -2355,12 +2385,13 @@ class MainWindow(QMainWindow):
|
||||
# window while that surface is still visible can leave a large black DWM
|
||||
# window above WeCom on some Windows/GPU combinations. Tear down the
|
||||
# visible surface first and let Qt flush that state before hiding us.
|
||||
portal_view = self.portal_page.view
|
||||
self._portal_was_visible = (
|
||||
self.portal_page.view.isVisible()
|
||||
portal_view is not None and portal_view.isVisible()
|
||||
if portal_was_visible is None
|
||||
else bool(portal_was_visible)
|
||||
)
|
||||
if self._portal_was_visible:
|
||||
if self._portal_was_visible and self.portal_page.view is not None:
|
||||
self.portal_page.view.hide()
|
||||
QApplication.processEvents()
|
||||
self.capsule.show_near(self)
|
||||
@@ -2378,8 +2409,9 @@ class MainWindow(QMainWindow):
|
||||
self.activateWindow()
|
||||
if getattr(self, "_portal_was_visible", False) and self.stack.currentIndex() == 0:
|
||||
def restore_portal() -> None:
|
||||
self.portal_page.view.show()
|
||||
self.portal_page.view.setFocus(Qt.OtherFocusReason)
|
||||
if self.portal_page.view is not None:
|
||||
self.portal_page.view.show()
|
||||
self.portal_page.view.setFocus(Qt.OtherFocusReason)
|
||||
|
||||
QTimer.singleShot(80, restore_portal)
|
||||
self._portal_was_visible = False
|
||||
@@ -2564,6 +2596,45 @@ def handle_startup_update(release: object) -> bool:
|
||||
return not forced
|
||||
|
||||
|
||||
class _StartupSyncBridge(QObject):
|
||||
finished = Signal(object)
|
||||
|
||||
|
||||
def _start_background_startup_sync(window: "MainWindow") -> None:
|
||||
"""云端配置同步放到后台线程执行。
|
||||
|
||||
过去它在窗口出现之前同步跑,网络一慢冷启动就跟着慢(超时 3 秒起步、
|
||||
DNS 卡住时更久)。现在窗口先出来,结果回来后再补日志和升级提示。
|
||||
"""
|
||||
bridge = _StartupSyncBridge(window)
|
||||
|
||||
def deliver(result: object) -> None:
|
||||
payload = result if isinstance(result, dict) else {}
|
||||
for diagnostic in payload.get("diagnostics") or []:
|
||||
window.append_log(str(diagnostic), "notify")
|
||||
if not handle_startup_update(payload.get("release")):
|
||||
window.close()
|
||||
|
||||
bridge.finished.connect(deliver)
|
||||
|
||||
def worker() -> None:
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
result = backend_client.startup_sync_config(timeout=3.0)
|
||||
except Exception:
|
||||
# 云端暂时不可用时仍执行上次成功同步的强制升级策略。
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
result = {"release": backend_client.cached_release_status()}
|
||||
except Exception:
|
||||
result = {}
|
||||
bridge.finished.emit(result or {})
|
||||
|
||||
threading.Thread(target=worker, daemon=True, name="startup-sync").start()
|
||||
|
||||
|
||||
def run_packaging_self_check(app: QApplication) -> int:
|
||||
"""离线验证随 EXE 打包的 Qt WebEngine 能否真正创建并加载页面。"""
|
||||
probe = QWebEngineView()
|
||||
@@ -2609,23 +2680,14 @@ def main() -> None:
|
||||
if "--packaging-self-check" in sys.argv:
|
||||
raise SystemExit(run_packaging_self_check(app))
|
||||
|
||||
startup_result = {}
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
startup_result = backend_client.startup_sync_config(timeout=3.0)
|
||||
except Exception:
|
||||
# 云端暂时不可用时仍执行上次成功同步的强制升级策略。
|
||||
startup_result = {"release": backend_client.cached_release_status()}
|
||||
|
||||
if not handle_startup_update(startup_result.get("release")):
|
||||
return
|
||||
|
||||
window = MainWindow()
|
||||
for diagnostic in startup_result.get("diagnostics") or []:
|
||||
window.append_log(str(diagnostic), "notify")
|
||||
console_shutdown = install_console_shutdown_handler(app, window)
|
||||
window.show()
|
||||
# WebEngine(Chromium 子进程)挪到首帧之后再拉起:低配机上它初始化要
|
||||
# 好几秒,放在构造期会让用户对着白屏等。
|
||||
QTimer.singleShot(120, window.portal_page.ensure_view)
|
||||
if "--qt-smoke-test" not in sys.argv:
|
||||
_start_background_startup_sync(window)
|
||||
|
||||
if "--qt-smoke-test" in sys.argv:
|
||||
for index in range(window.stack.count()):
|
||||
|
||||
Reference in New Issue
Block a user