更新bug

This commit is contained in:
Your Name
2026-07-31 11:48:16 +08:00
parent f913a57529
commit f22cc1a70d
109 changed files with 37586 additions and 927 deletions
+288 -70
View File
@@ -14,7 +14,9 @@ from pathlib import Path
from PySide6.QtCore import (
QEasingCurve,
QEvent,
QPoint,
QPointF,
QPropertyAnimation,
QRect,
QTimer,
@@ -22,7 +24,15 @@ from PySide6.QtCore import (
QUrl,
Signal,
)
from PySide6.QtGui import QColor, QDesktopServices, QFont, QIcon, QPainter, QPixmap
from PySide6.QtGui import (
QColor,
QDesktopServices,
QFont,
QIcon,
QPainter,
QPen,
QPixmap,
)
from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
@@ -74,6 +84,15 @@ except ImportError:
BotThread = _legacy.BotThread
LogQueue = _legacy.LogQueue
MESSAGE_BATCH_WINDOW_SECONDS = getattr(
_legacy, "MESSAGE_BATCH_WINDOW_SECONDS", 20.0
)
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
))
SCRIPT_DIR = application_data_dir()
APP_SETTINGS_FILE = SCRIPT_DIR / "app_settings.json"
CUSTOMER_SERVICE_URL = "http://kf.zhenyangtang.com.cn/"
@@ -307,7 +326,7 @@ def _shadow(widget: QWidget, blur: int = 28, y: int = 8, alpha: int = 24) -> Non
effect = QGraphicsDropShadowEffect(widget)
effect.setBlurRadius(blur)
effect.setOffset(0, y)
effect.setColor(QColor(22, 58, 43, alpha))
effect.setColor(QColor(45, 52, 49, alpha))
widget.setGraphicsEffect(effect)
@@ -593,7 +612,11 @@ class SettingsPage(QScrollArea):
layout = QVBoxLayout(root)
layout.setContentsMargins(28, 25, 28, 28)
layout.setSpacing(18)
layout.addLayout(_page_header("03", "通用设置", "修改后自动保存,并在下次监听时生效。"))
layout.addLayout(_page_header(
"03",
"通用设置",
"修改后自动保存;连续消息合并等待从下一轮回复生效。",
))
runtime, runtime_layout = _card(
"运行参数", "参数直接参与企业微信监听,不是静态展示。"
@@ -601,12 +624,13 @@ class SettingsPage(QScrollArea):
runtime_grid = QGridLayout()
runtime_grid.setHorizontalSpacing(18)
runtime_grid.setVerticalSpacing(8)
runtime_grid.addWidget(_label("固定回复(关闭 AI 时使用)", "CardSubtitle"), 0, 0, 1, 2)
runtime_grid.addWidget(_label("固定回复(关闭 AI 时使用)", "CardSubtitle"), 0, 0, 1, 3)
self.reply = QLineEdit(str(settings["auto_reply_text"]))
self.reply.setPlaceholderText("请输入固定回复")
runtime_grid.addWidget(self.reply, 1, 0, 1, 2)
runtime_grid.addWidget(self.reply, 1, 0, 1, 3)
runtime_grid.addWidget(_label("轮询间隔(秒)", "CardSubtitle"), 2, 0)
runtime_grid.addWidget(_label("鼠标静止等待(秒)", "CardSubtitle"), 2, 1)
runtime_grid.addWidget(_label("连续消息合并等待(秒)", "CardSubtitle"), 2, 2)
self.poll = QDoubleSpinBox()
self.poll.setRange(0.2, 300.0)
self.poll.setDecimals(1)
@@ -615,10 +639,33 @@ class SettingsPage(QScrollArea):
self.idle_seconds.setRange(0.0, 3600.0)
self.idle_seconds.setDecimals(1)
self.idle_seconds.setValue(float(settings["mouse_idle_seconds"]))
self.batch_window = QDoubleSpinBox()
self.batch_window.setRange(
float(MESSAGE_BATCH_WINDOW_MIN_SECONDS),
float(MESSAGE_BATCH_WINDOW_MAX_SECONDS),
)
self.batch_window.setDecimals(1)
self.batch_window.setSingleStep(1.0)
self.batch_window.setValue(float(settings["message_batch_window_seconds"]))
self.batch_window.setToolTip(
"同一客户连续发送多条消息时,等待后合并为一次回复;默认 20 秒。"
)
runtime_grid.addWidget(self.poll, 3, 0)
runtime_grid.addWidget(self.idle_seconds, 3, 1)
runtime_grid.addWidget(self.batch_window, 3, 2)
runtime_grid.setColumnStretch(0, 1)
runtime_grid.setColumnStretch(1, 1)
runtime_grid.setColumnStretch(2, 1)
runtime_grid.addWidget(
_label(
"同一客户的连续消息会在该窗口内合并为一次回复(1–120 秒,默认 20 秒)。",
"CardSubtitle",
),
4,
0,
1,
3,
)
runtime_layout.addLayout(runtime_grid)
self.save_status = _label("已加载保存配置", "SuccessText")
self.save_status.setAlignment(Qt.AlignRight)
@@ -665,6 +712,7 @@ class SettingsPage(QScrollArea):
self.reply.textChanged.connect(self._schedule_save)
self.poll.valueChanged.connect(self._schedule_save)
self.idle_seconds.valueChanged.connect(self._schedule_save)
self.batch_window.valueChanged.connect(self._schedule_save)
self.mouse_idle.toggled.connect(self._schedule_save)
def values(self) -> dict:
@@ -673,6 +721,7 @@ class SettingsPage(QScrollArea):
"poll_interval": self.poll.value(),
"mouse_idle_enabled": self.mouse_idle.isChecked(),
"mouse_idle_seconds": self.idle_seconds.value(),
"message_batch_window_seconds": float(self.batch_window.value()),
}
def _schedule_save(self, *_args) -> None:
@@ -1275,6 +1324,9 @@ class PersonaPage(QScrollArea):
result = backend_client.sync_cloud_config()
message = str(result.get("message") or "云端配置同步完成")
diagnostics = [str(item) for item in result.get("diagnostics") or []]
if diagnostics:
message += "\n" + "\n".join(diagnostics)
except Exception as exc:
self.cloudSyncFinished.emit(False, str(exc))
else:
@@ -1287,7 +1339,7 @@ class PersonaPage(QScrollArea):
self._cloud_sync_running = False
if ok:
self.reload_from_ai_config()
if not silent or not ok:
if not silent or not ok or "[开发模式]" in message:
self.saved.emit(ok, message if ok else f"云端配置同步失败:{message}")
def reload_from_ai_config(self) -> None:
@@ -1354,6 +1406,62 @@ class LogPage(QWidget):
self.editor.clear()
class CapsuleActionButton(QPushButton):
"""不依赖字体字形的胶囊浮窗矢量操作按钮。"""
def __init__(self, icon_kind: str, parent: QWidget | None = None):
super().__init__(parent)
self.icon_kind = icon_kind
self.setCursor(Qt.PointingHandCursor)
self.setFixedSize(30, 34)
self.setFocusPolicy(Qt.NoFocus)
self.setStyleSheet(
"QPushButton{background:transparent;border:none;border-radius:9px;}"
"QPushButton:hover{background:#F1F5F3;}"
"QPushButton:pressed{background:#E4ECE8;}"
)
def paintEvent(self, event) -> None:
super().paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
color = "#006C51" if self.underMouse() or self.isDown() else "#5A5F5D"
pen = QPen(QColor(color))
center_x = self.width() / 2.0
center_y = self.height() / 2.0
if self.icon_kind == "pause":
pen.setWidthF(3.0)
pen.setCapStyle(Qt.PenCapStyle.FlatCap)
painter.setPen(pen)
painter.drawLine(
QPointF(center_x - 3.5, center_y - 6.0),
QPointF(center_x - 3.5, center_y + 6.0),
)
painter.drawLine(
QPointF(center_x + 3.5, center_y - 6.0),
QPointF(center_x + 3.5, center_y + 6.0),
)
else:
pen.setWidthF(1.8)
pen.setCapStyle(Qt.PenCapStyle.SquareCap)
painter.setPen(pen)
left, right = center_x - 7.0, center_x + 7.0
top, bottom = center_y - 7.0, center_y + 7.0
arm = 4.5
for start, end in (
(QPointF(left, top + arm), QPointF(left, top)),
(QPointF(left, top), QPointF(left + arm, top)),
(QPointF(right - arm, top), QPointF(right, top)),
(QPointF(right, top), QPointF(right, top + arm)),
(QPointF(left, bottom - arm), QPointF(left, bottom)),
(QPointF(left, bottom), QPointF(left + arm, bottom)),
(QPointF(right - arm, bottom), QPointF(right, bottom)),
(QPointF(right, bottom), QPointF(right, bottom - arm)),
):
painter.drawLine(start, end)
class CapsuleWindow(QWidget):
expandRequested = Signal()
stopRequested = Signal()
@@ -1368,76 +1476,108 @@ class CapsuleWindow(QWidget):
)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setAttribute(Qt.WA_ShowWithoutActivating)
self.setFixedSize(430, 82)
self.setFixedSize(436, 112)
self._drag_origin: QPoint | None = None
self._window_origin = QPoint()
outer = QVBoxLayout(self)
outer.setContentsMargins(12, 8, 12, 14)
panel = QFrame()
panel.setObjectName("CapsulePanel")
panel.setStyleSheet(
"QFrame#CapsulePanel{background:#FFFFFF;border:1px solid #CFE0D8;border-radius:16px;}"
outer.setContentsMargins(8, 7, 8, 11)
self.panel = QFrame()
self.panel.setObjectName("CapsulePanel")
self.panel.setAccessibleName("监听状态浮窗")
self.panel.setToolTip("按住空白区域可拖动浮窗")
self.panel.setCursor(Qt.SizeAllCursor)
self.panel.setStyleSheet(
"QFrame#CapsulePanel{background:#FEFEFE;"
"border:1px solid #EFF2F0;border-radius:47px;}"
"QFrame#CapsulePanel QLabel{background:transparent;border:none;}"
)
_shadow(panel, 28, 7, 38)
row = QHBoxLayout(panel)
row.setContentsMargins(17, 10, 10, 10)
row.setSpacing(11)
grip = QLabel("•••")
grip.setAlignment(Qt.AlignCenter)
grip.setFixedSize(38, 38)
grip.setStyleSheet(
"QLabel{background:#F0F6F3;border:1px solid #D8E6DF;border-radius:19px;"
"color:#789087;font-family:'Cascadia Mono';font-weight:700;}"
_shadow(self.panel, 20, 4, 18)
row = QHBoxLayout(self.panel)
row.setContentsMargins(44, 0, 10, 0)
row.setSpacing(0)
self.status_group = QWidget()
self.status_group.setObjectName("CapsuleStatusGroup")
self.status_group.setFixedSize(108, 46)
status_row = QHBoxLayout(self.status_group)
status_row.setContentsMargins(0, 0, 0, 0)
status_row.setSpacing(4)
self.dot = QFrame()
self.dot.setObjectName("CapsuleStatusDot")
self.dot.setAccessibleName("监听状态")
self.dot.setFixedSize(10, 10)
self.dot.setStyleSheet(
"QFrame#CapsuleStatusDot{background:#7AB2A4;"
"border:none;border-radius:5px;}"
)
row.addWidget(grip)
self.dot = QLabel("")
self.dot.setStyleSheet("QLabel{color:#10A57A;font-size:17px;}")
row.addWidget(self.dot)
status_row.addWidget(self.dot, 0, Qt.AlignVCenter)
copy = QVBoxLayout()
copy.setSpacing(1)
copy.setContentsMargins(0, 0, 0, 0)
copy.setSpacing(0)
copy.addStretch(1)
self.status = QLabel("监听中")
self.status.setMinimumWidth(94)
self.status.setStyleSheet(
"QLabel{color:#087A59;font-size:14px;font-weight:700;}"
"QLabel{color:#006C51;font-size:13px;font-weight:600;}"
)
self.hint = QLabel("安全运行中")
self.hint.setStyleSheet("QLabel{color:#7B8D84;font-size:11px;}")
self.hint.setStyleSheet(
"QLabel{color:#666A68;font-size:10px;}"
)
copy.addWidget(self.status)
copy.addWidget(self.hint)
row.addLayout(copy, 1)
copy.addStretch(1)
status_row.addLayout(copy)
row.addWidget(self.status_group, 0, Qt.AlignVCenter)
row.addStretch(14)
self.timer = QLabel("00:00:00")
self.timer.setObjectName("CapsuleTimer")
self.timer.setAccessibleName("监听运行时长")
self.timer.setAlignment(Qt.AlignCenter)
self.timer.setMinimumWidth(82)
self.timer.setFixedSize(84, 28)
self.timer.setStyleSheet(
"QLabel{background:#F2F7F4;border:1px solid #DCE8E2;border-radius:9px;"
"padding:7px 9px;font-family:'Cascadia Mono';font-size:12px;color:#30473D;}"
"QLabel#CapsuleTimer{background:#F5F5F5;border:1px solid #E8E8E8;"
"border-radius:8px;font-family:'Cascadia Mono','Consolas';"
"font-size:10px;font-weight:600;color:#1A1C1C;}"
)
row.addWidget(self.timer)
expand = QPushButton("")
expand.setToolTip("展开控制台")
expand.setCursor(Qt.PointingHandCursor)
expand.setFixedSize(34, 34)
expand.setStyleSheet(
"QPushButton{background:#E7F4EE;color:#087A59;border:1px solid #CFE8DD;"
"border-radius:8px;font-size:17px;font-weight:700;}"
"QPushButton:hover{background:#D4EFE4;border-color:#AEDCC9;}"
"QPushButton:pressed{background:#C5E8D9;}"
row.addWidget(self.timer, 0, Qt.AlignVCenter)
row.addStretch(39)
self.divider = QFrame()
self.divider.setObjectName("CapsuleDivider")
self.divider.setFixedSize(1, 22)
self.divider.setStyleSheet(
"QFrame#CapsuleDivider{background:#EFEFEF;border:none;}"
)
expand.clicked.connect(self.expandRequested)
row.addWidget(expand)
stop = QPushButton("×")
stop.setToolTip("停止监听")
stop.setCursor(Qt.PointingHandCursor)
stop.setFixedSize(34, 34)
stop.setStyleSheet(
"QPushButton{background:#FBEAEC;color:#C74452;border:1px solid #F2CDD2;"
"border-radius:8px;font-size:18px;}"
"QPushButton:hover{background:#D85260;color:#FFFFFF;border-color:#D85260;}"
)
stop.clicked.connect(self.stopRequested)
row.addWidget(stop)
outer.addWidget(panel)
row.addWidget(self.divider, 0, Qt.AlignVCenter)
row.addSpacing(5)
actions = QWidget()
actions.setObjectName("CapsuleActions")
actions.setFixedSize(62, 34)
action_row = QHBoxLayout(actions)
action_row.setContentsMargins(0, 0, 0, 0)
action_row.setSpacing(2)
self.stop_button = CapsuleActionButton("pause")
self.stop_button.setObjectName("CapsuleStopButton")
self.stop_button.setAccessibleName("停止监听")
self.stop_button.setToolTip("停止监听")
self.stop_button.clicked.connect(self.stopRequested)
action_row.addWidget(self.stop_button)
self.expand_button = CapsuleActionButton("fullscreen")
self.expand_button.setObjectName("CapsuleExpandButton")
self.expand_button.setAccessibleName("展开控制台")
self.expand_button.setToolTip("展开控制台")
self.expand_button.clicked.connect(self.expandRequested)
action_row.addWidget(self.expand_button)
row.addWidget(actions, 0, Qt.AlignVCenter)
outer.addWidget(self.panel)
def show_near(self, window: QWidget) -> None:
screen = window.screen() or QApplication.primaryScreen()
@@ -1464,12 +1604,12 @@ class CapsuleWindow(QWidget):
def set_status(self, state: str, text: str) -> None:
colors = {
"running": "#10A57A",
"waiting": "#B67820",
"connecting": "#B67820",
"stopping": "#B67820",
"error": "#D85260",
"verification": "#D85260",
"running": ("#006C51", "#7AB2A4"),
"waiting": ("#9B681D", "#D2A85F"),
"connecting": ("#9B681D", "#D2A85F"),
"stopping": ("#9B681D", "#D2A85F"),
"error": ("#B84552", "#DF8E98"),
"verification": ("#B84552", "#DF8E98"),
}
hints = {
"running": "安全运行中",
@@ -1480,11 +1620,13 @@ class CapsuleWindow(QWidget):
"verification": "请先用手机扫码验证",
"stopped": "监听已停止",
}
color = colors.get(state, "#7B8D84")
self.dot.setStyleSheet(f"QLabel{{color:{color};font-size:17px;}}")
title_color, dot_color = colors.get(state, ("#66736D", "#A8B2AD"))
self.dot.setStyleSheet(
f"QFrame#CapsuleStatusDot{{background:{dot_color};border:none;border-radius:5px;}}"
)
self.status.setText(text)
self.status.setStyleSheet(
f"QLabel{{color:{color};font-size:14px;font-weight:700;}}"
f"QLabel{{color:{title_color};font-size:13px;font-weight:600;}}"
)
self.hint.setText(hints.get(state, text))
@@ -1583,6 +1725,7 @@ class MainWindow(QMainWindow):
self._stdout_proxy = None
self._saved_geometry = None
self._was_maximized = False
self._capsule_minimize_pending = False
self.runtime_settings = self._load_runtime_settings()
root = QWidget()
@@ -1671,6 +1814,7 @@ class MainWindow(QMainWindow):
"poll_interval": 2.0,
"mouse_idle_enabled": True,
"mouse_idle_seconds": 20.0,
"message_batch_window_seconds": MESSAGE_BATCH_WINDOW_SECONDS,
}
def _load_runtime_settings(self) -> dict:
@@ -1684,6 +1828,11 @@ class MainWindow(QMainWindow):
try:
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(
settings["message_batch_window_seconds"]
)
)
settings["mouse_idle_enabled"] = bool(settings["mouse_idle_enabled"])
settings["auto_reply_text"] = (
str(settings["auto_reply_text"]).strip()
@@ -1706,6 +1855,11 @@ class MainWindow(QMainWindow):
self.settings_page.mark_saved(False, f"保存失败:{exc}")
self.append_log(f"运行参数保存失败:{exc}", "err")
else:
thread = getattr(self, "_thread", None)
if thread is not None and thread.is_alive():
thread.set_message_batch_window_seconds(float(
self.runtime_settings["message_batch_window_seconds"]
))
self.settings_page.mark_saved(True, "已自动保存")
def show_page(self, index: int) -> None:
@@ -1730,6 +1884,9 @@ class MainWindow(QMainWindow):
float(self.runtime_settings["poll_interval"]),
mouse_idle_enabled=bool(self.runtime_settings["mouse_idle_enabled"]),
mouse_idle_seconds=float(self.runtime_settings["mouse_idle_seconds"]),
message_batch_window_seconds=float(
self.runtime_settings["message_batch_window_seconds"]
),
)
self._running = True
self._start_time = time.time()
@@ -1762,16 +1919,75 @@ class MainWindow(QMainWindow):
self.dashboard_page.set_status(state, text, hint)
self.capsule.set_status(state, text)
def enter_capsule_mode(self) -> None:
def changeEvent(self, event) -> None:
if (
event.type() == QEvent.Type.WindowStateChange
and self.isMinimized()
and getattr(self, "_running", False)
and not self.capsule.isVisible()
and not self._capsule_minimize_pending
):
self._capsule_minimize_pending = True
previous_state = event.oldState()
saved_geometry = self.saveGeometry()
was_maximized = bool(previous_state & Qt.WindowMaximized)
portal_was_visible = bool(
self.stack.currentIndex() == 0
and not self.portal_page.view.isHidden()
)
QTimer.singleShot(
0,
lambda: self._enter_capsule_after_minimize(
saved_geometry,
was_maximized,
portal_was_visible,
),
)
super().changeEvent(event)
def _enter_capsule_after_minimize(
self,
saved_geometry,
was_maximized: bool,
portal_was_visible: bool,
) -> None:
self._capsule_minimize_pending = False
if (
not self._running
or not self.isMinimized()
or self.capsule.isVisible()
):
return
self.enter_capsule_mode(
saved_geometry=saved_geometry,
was_maximized=was_maximized,
portal_was_visible=portal_was_visible,
)
def enter_capsule_mode(
self,
*,
saved_geometry=None,
was_maximized: bool | None = None,
portal_was_visible: bool | None = None,
) -> None:
if self.capsule.isVisible():
return
self._saved_geometry = self.saveGeometry()
self._was_maximized = self.isMaximized()
self._saved_geometry = (
self.saveGeometry() if saved_geometry is None else saved_geometry
)
self._was_maximized = (
self.isMaximized() if was_maximized is None else bool(was_maximized)
)
# QWebEngineView owns a native Chromium surface. Hiding the top-level
# 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.
self._portal_was_visible = self.portal_page.view.isVisible()
self._portal_was_visible = (
self.portal_page.view.isVisible()
if portal_was_visible is None
else bool(portal_was_visible)
)
if self._portal_was_visible:
self.portal_page.view.hide()
QApplication.processEvents()
@@ -2032,6 +2248,8 @@ def main() -> None:
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()