初始提交:识流 AI 助手项目
微信自动回复机器人,基于截图+OCR识别消息,支持关键词规则和 AI(OpenAI/DeepSeek/Dify)自动回复。 技术栈:PySide6 + Flask + Vue3 + RapidOCR + SQLite 注:OCR大模型文件(.onnx / .pdiparams)不纳入版本控制,需单独下载。 🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
238
app/presentation/main_window.py
Normal file
238
app/presentation/main_window.py
Normal file
@@ -0,0 +1,238 @@
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from urllib import parse, request
|
||||
|
||||
import requests
|
||||
from PySide6.QtCore import QTimer, Qt, QObject, Slot
|
||||
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QMessageBox
|
||||
|
||||
from app.application.services import AppConfig, BackendRuntime, FrontendRuntime
|
||||
from app.presentation.shell import LoadingWindowController, WebViewShellController
|
||||
|
||||
print(f"[GUI] Python executable: {sys.executable}")
|
||||
|
||||
try:
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
from PySide6.QtWebChannel import QWebChannel
|
||||
WEB_ENGINE_AVAILABLE = True
|
||||
except Exception:
|
||||
WEB_ENGINE_AVAILABLE = False
|
||||
QWebEngineView = None
|
||||
QWebChannel = None
|
||||
|
||||
|
||||
class WindowBridge(QObject):
|
||||
def __init__(self, main_window):
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
|
||||
@Slot()
|
||||
def open_devtools(self):
|
||||
self.main_window.open_devtools()
|
||||
|
||||
@Slot()
|
||||
def minimize(self):
|
||||
self.main_window.showMinimized()
|
||||
|
||||
@Slot()
|
||||
def maximize_or_restore(self):
|
||||
if self.main_window.isMaximized():
|
||||
self.main_window.showNormal()
|
||||
else:
|
||||
self.main_window.showMaximized()
|
||||
|
||||
@Slot()
|
||||
def close_window(self):
|
||||
self.main_window.close()
|
||||
|
||||
@Slot()
|
||||
def start_move(self):
|
||||
wh = self.main_window.windowHandle()
|
||||
if wh is not None:
|
||||
wh.startSystemMove()
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def post_settings(self, **kwargs):
|
||||
if not self.base_url:
|
||||
return
|
||||
try:
|
||||
payload = {"action": "settings_set"}
|
||||
for k, v in kwargs.items():
|
||||
payload[k] = "1" if v is True else "0" if v is False else str(v)
|
||||
data = parse.urlencode(payload).encode("utf-8")
|
||||
req = request.Request(self.base_url + "/api/rules", data=data, method="POST")
|
||||
with request.urlopen(req, timeout=2):
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("识流 AI 助手")
|
||||
self.resize(1280, 920)
|
||||
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Window)
|
||||
self.hide()
|
||||
|
||||
self.config = AppConfig()
|
||||
self.backend_runtime = BackendRuntime(self.config)
|
||||
self.frontend_runtime = FrontendRuntime(self.config, self.backend_runtime)
|
||||
self.base_url = os.getenv("APP_BASE_URL", self.backend_runtime.base_url)
|
||||
self.backend_process = None
|
||||
self.backend_started_by_gui = False
|
||||
self.backend_boot_thread = None
|
||||
self.web_channel = None
|
||||
self.window_bridge = None
|
||||
self.loading = LoadingWindowController()
|
||||
self.web_shell = None
|
||||
self.frontend_entry_url = ""
|
||||
|
||||
central = QWidget()
|
||||
central.setStyleSheet("background:#eef2f7;")
|
||||
self.setCentralWidget(central)
|
||||
root = QVBoxLayout(central)
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
root.setSpacing(0)
|
||||
|
||||
self.web = None
|
||||
self.devtools_view = None
|
||||
if WEB_ENGINE_AVAILABLE:
|
||||
self.web = QWebEngineView()
|
||||
self.web.setStyleSheet("background:#eef2f7;")
|
||||
root.addWidget(self.web, 1)
|
||||
self.web_channel = QWebChannel(self.web.page())
|
||||
self.window_bridge = WindowBridge(self)
|
||||
self.web_channel.registerObject("windowBridge", self.window_bridge)
|
||||
self.web.page().setWebChannel(self.web_channel)
|
||||
self.web.loadFinished.connect(self.on_main_page_loaded)
|
||||
self.web_shell = WebViewShellController(self.web)
|
||||
|
||||
self.listener_sync_timer = QTimer(self)
|
||||
self.listener_sync_timer.setInterval(30000)
|
||||
self.listener_sync_timer.timeout.connect(self.sync_listener_state)
|
||||
self._syncing_listener_state = False
|
||||
|
||||
self.backend_ready_timer = QTimer(self)
|
||||
self.backend_ready_timer.setInterval(100)
|
||||
self.backend_ready_timer.timeout.connect(self.consume_backend_boot_result)
|
||||
self._backend_boot_result = None
|
||||
|
||||
self.loading.show()
|
||||
self.start_backend()
|
||||
|
||||
def get_settings(self):
|
||||
if not self.base_url:
|
||||
return {}
|
||||
try:
|
||||
url = self.base_url + "/api/rules?action=settings_get"
|
||||
with request.urlopen(url, timeout=2) as resp:
|
||||
data = resp.read().decode("utf-8")
|
||||
import json
|
||||
payload = json.loads(data)
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def sync_listener_state(self):
|
||||
if self._syncing_listener_state:
|
||||
return
|
||||
self._syncing_listener_state = True
|
||||
try:
|
||||
if not self.base_url:
|
||||
return
|
||||
requests.get(self.base_url + "/api/bot/status", timeout=2)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._syncing_listener_state = False
|
||||
|
||||
def start_backend(self):
|
||||
self.base_url = self.backend_runtime.base_url
|
||||
self._backend_boot_result = None
|
||||
self.backend_boot_thread = threading.Thread(target=self._boot_backend_worker, daemon=True)
|
||||
self.backend_boot_thread.start()
|
||||
self.backend_ready_timer.start()
|
||||
|
||||
def _boot_backend_worker(self):
|
||||
try:
|
||||
if self.backend_runtime.is_backend_alive():
|
||||
self.backend_started_by_gui = False
|
||||
self._backend_boot_result = {"ok": True, "url": self.base_url}
|
||||
return
|
||||
process = self.backend_runtime.launch_backend_process()
|
||||
self.backend_process = process
|
||||
self.backend_started_by_gui = True
|
||||
ok, message = self.frontend_runtime.wait_for_backend(process=process)
|
||||
if ok:
|
||||
self._backend_boot_result = {"ok": True, "url": self.base_url}
|
||||
else:
|
||||
self._backend_boot_result = {"ok": False, "message": message}
|
||||
except Exception as exc:
|
||||
self._backend_boot_result = {"ok": False, "message": str(exc)}
|
||||
|
||||
def consume_backend_boot_result(self):
|
||||
if self._backend_boot_result is None:
|
||||
return
|
||||
result = self._backend_boot_result
|
||||
self._backend_boot_result = None
|
||||
self.backend_ready_timer.stop()
|
||||
if result.get("ok"):
|
||||
self.on_backend_started(result["url"])
|
||||
return
|
||||
self.on_backend_failed(result.get("message") or "后台启动失败")
|
||||
|
||||
def on_backend_started(self, url):
|
||||
self.base_url = url
|
||||
self.frontend_entry_url = self.frontend_runtime.resolve_frontend_entry_url(url)
|
||||
self.reload_admin_page()
|
||||
self.listener_sync_timer.start()
|
||||
|
||||
def on_backend_failed(self, msg):
|
||||
self.loading.close()
|
||||
QMessageBox.critical(self, "后台错误", msg)
|
||||
|
||||
def reload_admin_page(self):
|
||||
self.web_shell.load(self.frontend_entry_url)
|
||||
|
||||
def on_main_page_loaded(self, ok):
|
||||
if not self.web_shell.mark_loaded(ok):
|
||||
return
|
||||
self.loading.finish_with(self._finish_show_main)
|
||||
|
||||
def _finish_show_main(self):
|
||||
if not self.web_shell.can_finish():
|
||||
return
|
||||
self.web_shell.finish()
|
||||
self.show()
|
||||
self.raise_()
|
||||
self.activateWindow()
|
||||
QTimer.singleShot(60, self.loading.close)
|
||||
|
||||
def open_devtools(self):
|
||||
if self.web is None:
|
||||
return
|
||||
if self.devtools_view is None:
|
||||
self.devtools_view = QWebEngineView()
|
||||
self.devtools_view.setWindowTitle("页面调试控制台")
|
||||
self.devtools_view.resize(1100, 760)
|
||||
self.web.page().setDevToolsPage(self.devtools_view.page())
|
||||
self.devtools_view.show()
|
||||
self.devtools_view.raise_()
|
||||
self.devtools_view.activateWindow()
|
||||
|
||||
def closeEvent(self, event):
|
||||
try:
|
||||
self.listener_sync_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.backend_ready_timer.stop()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if self.backend_started_by_gui:
|
||||
self.backend_runtime.terminate_backend_process(self.backend_process)
|
||||
except Exception:
|
||||
pass
|
||||
super().closeEvent(event)
|
||||
Reference in New Issue
Block a user