新增
This commit is contained in:
+175
-232
@@ -59,6 +59,9 @@ from PySide6.QtWebEngineCore import (
|
||||
)
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
from app_version import APP_VERSION, release_status
|
||||
from runtime_paths import application_data_dir, resource_path
|
||||
|
||||
|
||||
try:
|
||||
import __main__ as _legacy
|
||||
@@ -71,10 +74,11 @@ except ImportError:
|
||||
|
||||
BotThread = _legacy.BotThread
|
||||
LogQueue = _legacy.LogQueue
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
SCRIPT_DIR = application_data_dir()
|
||||
APP_SETTINGS_FILE = SCRIPT_DIR / "app_settings.json"
|
||||
CUSTOMER_SERVICE_URL = "http://kf.zhenyangtang.com.cn/"
|
||||
WIN_TITLE = "甄养堂 · 企微客服助手"
|
||||
WIN_TITLE = f"甄养堂 · 企微客服助手 v{APP_VERSION}"
|
||||
APP_ICON_PATH = resource_path("assets", "brand", "zhenyangtang-icon.png")
|
||||
WECOM_WAITING_MESSAGE = (
|
||||
"企业微信主界面未显示、未在前台或已最小化,正在尝试切到前台;"
|
||||
"本轮失败时下一轮将自动重试。"
|
||||
@@ -409,7 +413,7 @@ class PortalPage(QWidget):
|
||||
)
|
||||
|
||||
def _install_light_theme(self) -> None:
|
||||
css_path = SCRIPT_DIR / "edge_light_theme" / "light-theme.css"
|
||||
css_path = resource_path("edge_light_theme", "light-theme.css")
|
||||
try:
|
||||
css = css_path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
@@ -561,6 +565,7 @@ class DashboardPage(QScrollArea):
|
||||
"waiting": COLORS["warning"],
|
||||
"connecting": COLORS["warning"],
|
||||
"error": COLORS["danger"],
|
||||
"verification": COLORS["danger"],
|
||||
"stopping": COLORS["warning"],
|
||||
"stopped": COLORS["ink"],
|
||||
}
|
||||
@@ -639,7 +644,7 @@ class SettingsPage(QScrollArea):
|
||||
for index, text in enumerate(
|
||||
(
|
||||
"企业微信 PC 端已登录,且账号状态正常。",
|
||||
"如启用 AI,API 地址、密钥与模型配置已经保存。",
|
||||
"软件启动时会自动获取云端 AI 配置,无需本机填写模型参数。",
|
||||
"双屏环境可以使用不同缩放比例,Qt 会跟随当前屏幕自动缩放。",
|
||||
),
|
||||
start=1,
|
||||
@@ -664,7 +669,7 @@ class SettingsPage(QScrollArea):
|
||||
|
||||
def values(self) -> dict:
|
||||
return {
|
||||
"auto_reply_text": self.reply.text().strip() or "你好",
|
||||
"auto_reply_text": self.reply.text().strip() or "在的,您慢慢说,我这边看着呢。",
|
||||
"poll_interval": self.poll.value(),
|
||||
"mouse_idle_enabled": self.mouse_idle.isChecked(),
|
||||
"mouse_idle_seconds": self.idle_seconds.value(),
|
||||
@@ -1125,7 +1130,7 @@ class BackendLoginDialog(QDialog):
|
||||
|
||||
class PersonaPage(QScrollArea):
|
||||
saved = Signal(bool, str)
|
||||
backendSyncFinished = Signal(bool, str)
|
||||
cloudSyncFinished = Signal(bool, str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -1138,63 +1143,35 @@ class PersonaPage(QScrollArea):
|
||||
layout = QVBoxLayout(root)
|
||||
layout.setContentsMargins(28, 25, 28, 28)
|
||||
layout.setSpacing(18)
|
||||
layout.addLayout(_page_header("05", "AI 人格与能力", "统一管理模型、客服身份、上下文和 MCP 工具。"))
|
||||
layout.addLayout(
|
||||
_page_header(
|
||||
"05",
|
||||
"AI 人格与能力",
|
||||
"模型与能力由云端统一管理,本机仅保留客服身份偏好。",
|
||||
)
|
||||
)
|
||||
|
||||
import ai_config
|
||||
|
||||
remote, remote_layout = _card(
|
||||
"后台同步",
|
||||
"在管理后台配置模型后,本机启动及运行期间会自动获取并应用。",
|
||||
identity, identity_layout = _card(
|
||||
"客服与回复",
|
||||
"客服名称留空时自动采用云端名称;填写后优先使用本机名称。",
|
||||
)
|
||||
remote_row = QHBoxLayout()
|
||||
remote_copy = QVBoxLayout()
|
||||
self.remote_status = _label("正在读取后台连接…", "WarningText")
|
||||
self.remote_meta = _label("", "CardSubtitle")
|
||||
remote_copy.addWidget(self.remote_status)
|
||||
remote_copy.addWidget(self.remote_meta)
|
||||
remote_row.addLayout(remote_copy, 1)
|
||||
self.logout_backend_button = _button("退出后台")
|
||||
self.logout_backend_button.clicked.connect(self.logout_backend)
|
||||
remote_row.addWidget(self.logout_backend_button)
|
||||
self.manage_backend_button = _button("打开管理后台")
|
||||
self.manage_backend_button.clicked.connect(self.open_backend_page)
|
||||
remote_row.addWidget(self.manage_backend_button)
|
||||
self.sync_backend_button = _button("立即同步")
|
||||
self.sync_backend_button.clicked.connect(lambda: self.sync_from_backend(silent=False))
|
||||
remote_row.addWidget(self.sync_backend_button)
|
||||
self.login_backend_button = _button("登录后台", "primary")
|
||||
self.login_backend_button.clicked.connect(self.open_backend_login)
|
||||
remote_row.addWidget(self.login_backend_button)
|
||||
remote_layout.addLayout(remote_row)
|
||||
layout.addWidget(remote)
|
||||
|
||||
overview, overview_layout = _card("能力开关", "关闭 AI 后将使用通用设置中的固定回复。")
|
||||
switch_grid = QGridLayout()
|
||||
self.ai_enabled = QCheckBox("启用 AI 回复")
|
||||
self.ai_enabled.setChecked(bool(ai_config.AI_ENABLED))
|
||||
self.context_enabled = QCheckBox("启用会话上下文")
|
||||
self.context_enabled.setChecked(bool(ai_config.AI_CONTEXT_ENABLED))
|
||||
self.counter_enabled = QCheckBox("启用反辱骂策略")
|
||||
self.counter_enabled.setChecked(bool(ai_config.AI_COUNTER_INSULT_ENABLED))
|
||||
self.mcp_enabled = QCheckBox("启用 MCP 工具")
|
||||
self.mcp_enabled.setChecked(bool(ai_config.AI_MCP_ENABLED))
|
||||
for index, switch in enumerate(
|
||||
(self.ai_enabled, self.context_enabled, self.counter_enabled, self.mcp_enabled)
|
||||
):
|
||||
switch_grid.addWidget(switch, index // 2, index % 2)
|
||||
overview_layout.addLayout(switch_grid)
|
||||
layout.addWidget(overview)
|
||||
|
||||
identity, identity_layout = _card("模型与身份", "保存后立即写入 ai_settings.json。")
|
||||
form = QGridLayout()
|
||||
form.setHorizontalSpacing(18)
|
||||
form.setVerticalSpacing(8)
|
||||
self.api_base = self._field(form, 0, 0, "API 地址", ai_config.AI_API_BASE)
|
||||
self.model = self._field(form, 0, 1, "模型名称", ai_config.AI_MODEL)
|
||||
self.api_key = self._field(form, 2, 0, "API Key", ai_config.AI_API_KEY)
|
||||
self.api_key.setEchoMode(QLineEdit.Password)
|
||||
self.agent_name = self._field(form, 2, 1, "客服名称", ai_config.AI_AGENT_NAME)
|
||||
self.hospital = self._field(form, 4, 0, "机构名称", ai_config.AI_HOSPITAL_NAME, 1, 2)
|
||||
self.agent_name = self._field(
|
||||
form,
|
||||
0,
|
||||
0,
|
||||
"本机客服名称(可留空)",
|
||||
ai_config.local_agent_name_override(),
|
||||
1,
|
||||
2,
|
||||
)
|
||||
self.agent_name.setPlaceholderText(
|
||||
f"云端客服名称:{ai_config.AI_CLOUD_AGENT_NAME}"
|
||||
)
|
||||
self.rounds = QSpinBox()
|
||||
self.rounds.setRange(1, 50)
|
||||
self.rounds.setValue(int(ai_config.AI_CONTEXT_MAX_ROUNDS))
|
||||
@@ -1205,12 +1182,12 @@ class PersonaPage(QScrollArea):
|
||||
self.temperature.setRange(0.0, 2.0)
|
||||
self.temperature.setSingleStep(0.05)
|
||||
self.temperature.setValue(float(ai_config.AI_TEMPERATURE))
|
||||
form.addWidget(_label("上下文轮数", "CardSubtitle"), 6, 0)
|
||||
form.addWidget(_label("最大回复 tokens", "CardSubtitle"), 6, 1)
|
||||
form.addWidget(self.rounds, 7, 0)
|
||||
form.addWidget(self.max_tokens, 7, 1)
|
||||
form.addWidget(_label("温度", "CardSubtitle"), 8, 0)
|
||||
form.addWidget(self.temperature, 9, 0)
|
||||
form.addWidget(_label("上下文轮数", "CardSubtitle"), 2, 0)
|
||||
form.addWidget(_label("最大回复 tokens", "CardSubtitle"), 2, 1)
|
||||
form.addWidget(self.rounds, 3, 0)
|
||||
form.addWidget(self.max_tokens, 3, 1)
|
||||
form.addWidget(_label("温度", "CardSubtitle"), 4, 0)
|
||||
form.addWidget(self.temperature, 5, 0)
|
||||
form.setColumnStretch(0, 1)
|
||||
form.setColumnStretch(1, 1)
|
||||
identity_layout.addLayout(form)
|
||||
@@ -1236,32 +1213,14 @@ class PersonaPage(QScrollArea):
|
||||
actions = QHBoxLayout()
|
||||
self.save_status = _label("", "SuccessText")
|
||||
actions.addWidget(self.save_status, 1)
|
||||
self.save_button = _button("保存 AI 配置", "primary")
|
||||
self.save_button = _button("保存本地偏好", "primary")
|
||||
self.save_button.clicked.connect(self.save_config)
|
||||
actions.addWidget(self.save_button)
|
||||
layout.addLayout(actions)
|
||||
layout.addStretch(1)
|
||||
self._backend_sync_running = False
|
||||
self._backend_sync_silent = False
|
||||
self.backendSyncFinished.connect(self._backend_sync_finished)
|
||||
self._managed_widgets = (
|
||||
self.ai_enabled,
|
||||
self.context_enabled,
|
||||
self.counter_enabled,
|
||||
self.mcp_enabled,
|
||||
self.api_base,
|
||||
self.model,
|
||||
self.api_key,
|
||||
self.agent_name,
|
||||
self.hospital,
|
||||
self.rounds,
|
||||
self.max_tokens,
|
||||
self.temperature,
|
||||
self.mcp_json,
|
||||
self.mcp_rounds,
|
||||
self.save_button,
|
||||
)
|
||||
self.refresh_backend_status()
|
||||
self._cloud_sync_running = False
|
||||
self._cloud_sync_silent = False
|
||||
self.cloudSyncFinished.connect(self._cloud_sync_finished)
|
||||
|
||||
@staticmethod
|
||||
def _field(
|
||||
@@ -1279,35 +1238,13 @@ class PersonaPage(QScrollArea):
|
||||
return field
|
||||
|
||||
def save_config(self) -> None:
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
remote_managed = backend_client.is_configured() and bool(
|
||||
backend_client.load_settings().get("auto_sync", True)
|
||||
)
|
||||
except Exception:
|
||||
remote_managed = False
|
||||
if remote_managed:
|
||||
self.save_status.setText("当前由后台统一管理,请在后台修改后点击“立即同步”")
|
||||
self.save_status.setObjectName("WarningText")
|
||||
self.save_status.style().unpolish(self.save_status)
|
||||
self.save_status.style().polish(self.save_status)
|
||||
return
|
||||
try:
|
||||
servers = json.loads(self.mcp_json.toPlainText().strip() or "[]")
|
||||
if not isinstance(servers, list):
|
||||
raise ValueError("MCP JSON 根节点必须是数组")
|
||||
import ai_config
|
||||
|
||||
ai_config.AI_ENABLED = self.ai_enabled.isChecked()
|
||||
ai_config.AI_CONTEXT_ENABLED = self.context_enabled.isChecked()
|
||||
ai_config.AI_COUNTER_INSULT_ENABLED = self.counter_enabled.isChecked()
|
||||
ai_config.AI_MCP_ENABLED = self.mcp_enabled.isChecked()
|
||||
ai_config.AI_API_BASE = self.api_base.text().strip()
|
||||
ai_config.AI_API_KEY = self.api_key.text().strip()
|
||||
ai_config.AI_MODEL = self.model.text().strip()
|
||||
ai_config.AI_AGENT_NAME = self.agent_name.text().strip()
|
||||
ai_config.AI_HOSPITAL_NAME = self.hospital.text().strip()
|
||||
ai_config.set_local_agent_name_override(self.agent_name.text().strip())
|
||||
ai_config.AI_CONTEXT_MAX_ROUNDS = self.rounds.value()
|
||||
ai_config.AI_MAX_TOKENS = self.max_tokens.value()
|
||||
ai_config.AI_TEMPERATURE = self.temperature.value()
|
||||
@@ -1320,144 +1257,46 @@ class PersonaPage(QScrollArea):
|
||||
self.save_status.setObjectName("DangerText")
|
||||
self.saved.emit(False, str(exc))
|
||||
else:
|
||||
self.save_status.setText("配置已保存并立即生效")
|
||||
self.save_status.setText("本地偏好已保存并立即生效")
|
||||
self.save_status.setObjectName("SuccessText")
|
||||
self.saved.emit(True, "AI 配置已保存并生效")
|
||||
self.saved.emit(True, "AI 本地偏好已保存并生效")
|
||||
self.save_status.style().unpolish(self.save_status)
|
||||
self.save_status.style().polish(self.save_status)
|
||||
|
||||
def open_backend_login(self) -> None:
|
||||
dialog = BackendLoginDialog(self)
|
||||
if dialog.exec() == QDialog.Accepted:
|
||||
self.reload_from_ai_config()
|
||||
self.refresh_backend_status()
|
||||
self.saved.emit(True, "后台已登录,模型配置已自动同步")
|
||||
|
||||
def open_backend_page(self) -> None:
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
url = backend_client.load_settings().get(
|
||||
"server_url", backend_client.DEFAULT_SERVER_URL
|
||||
)
|
||||
QDesktopServices.openUrl(QUrl(str(url)))
|
||||
except Exception as exc:
|
||||
self.saved.emit(False, f"无法打开管理后台:{exc}")
|
||||
|
||||
def logout_backend(self) -> None:
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"退出配置后台",
|
||||
"退出后将停止自动同步,但已下发到本机的配置会继续保留。确定退出吗?",
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
def sync_from_cloud(self, *, silent: bool = True) -> None:
|
||||
if self._cloud_sync_running:
|
||||
return
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
backend_client.logout()
|
||||
except Exception as exc:
|
||||
self.saved.emit(False, f"退出后台失败:{exc}")
|
||||
return
|
||||
self.refresh_backend_status()
|
||||
self.saved.emit(True, "已退出配置后台,当前配置保留在本机")
|
||||
|
||||
def refresh_backend_status(self) -> None:
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
summary = backend_client.connection_summary()
|
||||
except Exception as exc:
|
||||
self.remote_status.setObjectName("DangerText")
|
||||
self.remote_status.setText(f"后台连接配置读取失败:{exc}")
|
||||
return
|
||||
configured = bool(summary["configured"])
|
||||
authenticated = bool(summary.get("authenticated"))
|
||||
managed = configured and bool(summary["auto_sync"])
|
||||
if configured:
|
||||
last_error = str(summary.get("last_error") or "")
|
||||
self.remote_status.setObjectName("DangerText" if last_error else "SuccessText")
|
||||
self.remote_status.setText(
|
||||
f"最近同步失败 · {last_error}"
|
||||
if last_error
|
||||
else (
|
||||
f"已连接 · {summary['username']} · 配置 v{summary['last_version']}"
|
||||
if authenticated
|
||||
else f"已发现本机后台 · 配置 v{summary['last_version']}"
|
||||
)
|
||||
)
|
||||
sync_at = summary.get("last_sync_at") or "尚未同步"
|
||||
self.remote_meta.setText(f"{summary['server_url']} · 最后同步 {sync_at}")
|
||||
else:
|
||||
self.remote_status.setObjectName("WarningText")
|
||||
self.remote_status.setText("尚未登录配置后台")
|
||||
self.remote_meta.setText(f"默认地址 {summary['server_url']} · 当前使用本机配置")
|
||||
self.remote_status.style().unpolish(self.remote_status)
|
||||
self.remote_status.style().polish(self.remote_status)
|
||||
self.login_backend_button.setVisible(not authenticated)
|
||||
self.login_backend_button.setText("账号登录" if configured else "登录后台")
|
||||
self.logout_backend_button.setVisible(authenticated)
|
||||
self.sync_backend_button.setVisible(configured)
|
||||
for widget in self._managed_widgets:
|
||||
widget.setEnabled(not managed)
|
||||
self.save_status.setText("后台统一管理中,本地字段为只读" if managed else "")
|
||||
self.save_status.setObjectName("SuccessText" if managed else "CardSubtitle")
|
||||
self.save_status.style().unpolish(self.save_status)
|
||||
self.save_status.style().polish(self.save_status)
|
||||
|
||||
def sync_from_backend(self, *, silent: bool = True) -> None:
|
||||
if self._backend_sync_running:
|
||||
return
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
if not backend_client.is_configured():
|
||||
return
|
||||
except Exception as exc:
|
||||
if not silent:
|
||||
self.saved.emit(False, f"后台连接配置读取失败:{exc}")
|
||||
return
|
||||
self._backend_sync_running = True
|
||||
self._backend_sync_silent = silent
|
||||
self.sync_backend_button.setEnabled(False)
|
||||
self.remote_status.setObjectName("WarningText")
|
||||
self.remote_status.setText("正在从后台同步模型配置…")
|
||||
self.remote_status.style().unpolish(self.remote_status)
|
||||
self.remote_status.style().polish(self.remote_status)
|
||||
self._cloud_sync_running = True
|
||||
self._cloud_sync_silent = silent
|
||||
|
||||
def worker() -> None:
|
||||
try:
|
||||
result = backend_client.sync_config(force=not silent)
|
||||
message = str(result.get("message") or "后台配置同步完成")
|
||||
import backend_client
|
||||
|
||||
result = backend_client.sync_cloud_config()
|
||||
message = str(result.get("message") or "云端配置同步完成")
|
||||
except Exception as exc:
|
||||
self.backendSyncFinished.emit(False, str(exc))
|
||||
self.cloudSyncFinished.emit(False, str(exc))
|
||||
else:
|
||||
self.backendSyncFinished.emit(True, message)
|
||||
self.cloudSyncFinished.emit(True, message)
|
||||
|
||||
threading.Thread(target=worker, daemon=True).start()
|
||||
|
||||
def _backend_sync_finished(self, ok: bool, message: str) -> None:
|
||||
silent = self._backend_sync_silent
|
||||
self._backend_sync_running = False
|
||||
self.sync_backend_button.setEnabled(True)
|
||||
def _cloud_sync_finished(self, ok: bool, message: str) -> None:
|
||||
silent = self._cloud_sync_silent
|
||||
self._cloud_sync_running = False
|
||||
if ok:
|
||||
self.reload_from_ai_config()
|
||||
self.refresh_backend_status()
|
||||
if not silent or not ok:
|
||||
self.saved.emit(ok, message if ok else f"后台同步失败:{message}")
|
||||
self.saved.emit(ok, message if ok else f"云端配置同步失败:{message}")
|
||||
|
||||
def reload_from_ai_config(self) -> None:
|
||||
import ai_config
|
||||
|
||||
self.ai_enabled.setChecked(bool(ai_config.AI_ENABLED))
|
||||
self.context_enabled.setChecked(bool(ai_config.AI_CONTEXT_ENABLED))
|
||||
self.counter_enabled.setChecked(bool(ai_config.AI_COUNTER_INSULT_ENABLED))
|
||||
self.mcp_enabled.setChecked(bool(ai_config.AI_MCP_ENABLED))
|
||||
self.api_base.setText(str(ai_config.AI_API_BASE or ""))
|
||||
self.model.setText(str(ai_config.AI_MODEL or ""))
|
||||
self.api_key.setText(str(ai_config.AI_API_KEY or ""))
|
||||
self.agent_name.setText(str(ai_config.AI_AGENT_NAME or ""))
|
||||
self.hospital.setText(str(ai_config.AI_HOSPITAL_NAME or ""))
|
||||
self.agent_name.setText(ai_config.local_agent_name_override())
|
||||
self.agent_name.setPlaceholderText(
|
||||
f"云端客服名称:{ai_config.AI_CLOUD_AGENT_NAME}"
|
||||
)
|
||||
self.rounds.setValue(int(ai_config.AI_CONTEXT_MAX_ROUNDS))
|
||||
self.max_tokens.setValue(int(ai_config.AI_MAX_TOKENS))
|
||||
self.temperature.setValue(float(ai_config.AI_TEMPERATURE))
|
||||
@@ -1630,6 +1469,7 @@ class CapsuleWindow(QWidget):
|
||||
"connecting": "#B67820",
|
||||
"stopping": "#B67820",
|
||||
"error": "#D85260",
|
||||
"verification": "#D85260",
|
||||
}
|
||||
hints = {
|
||||
"running": "安全运行中",
|
||||
@@ -1637,6 +1477,7 @@ class CapsuleWindow(QWidget):
|
||||
"connecting": "正在连接窗口",
|
||||
"stopping": "正在结束任务",
|
||||
"error": "请查看运行日志",
|
||||
"verification": "请先用手机扫码验证",
|
||||
"stopped": "监听已停止",
|
||||
}
|
||||
color = colors.get(state, "#7B8D84")
|
||||
@@ -1707,6 +1548,7 @@ class Sidebar(QFrame):
|
||||
"connecting": "#F2BC64",
|
||||
"stopping": "#F2BC64",
|
||||
"error": "#FF818E",
|
||||
"verification": "#FF818E",
|
||||
"stopped": "#A5B9B0",
|
||||
}
|
||||
self.status_title.setText(f"● {text}")
|
||||
@@ -1797,10 +1639,9 @@ class MainWindow(QMainWindow):
|
||||
self.backend_sync_timer = QTimer(self)
|
||||
self.backend_sync_timer.setInterval(max(60, sync_seconds) * 1000)
|
||||
self.backend_sync_timer.timeout.connect(
|
||||
lambda: self.persona_page.sync_from_backend(silent=True)
|
||||
lambda: self.persona_page.sync_from_cloud(silent=True)
|
||||
)
|
||||
self.backend_sync_timer.start()
|
||||
QTimer.singleShot(350, lambda: self.persona_page.sync_from_backend(silent=True))
|
||||
|
||||
def _set_initial_geometry(self) -> None:
|
||||
screen = QApplication.primaryScreen()
|
||||
@@ -1826,7 +1667,7 @@ class MainWindow(QMainWindow):
|
||||
@staticmethod
|
||||
def _runtime_defaults() -> dict:
|
||||
return {
|
||||
"auto_reply_text": "你好",
|
||||
"auto_reply_text": "在的,您慢慢说,我这边看着呢。",
|
||||
"poll_interval": 2.0,
|
||||
"mouse_idle_enabled": True,
|
||||
"mouse_idle_seconds": 20.0,
|
||||
@@ -1844,7 +1685,10 @@ 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["mouse_idle_enabled"] = bool(settings["mouse_idle_enabled"])
|
||||
settings["auto_reply_text"] = str(settings["auto_reply_text"]).strip() or "你好"
|
||||
settings["auto_reply_text"] = (
|
||||
str(settings["auto_reply_text"]).strip()
|
||||
or "在的,您慢慢说,我这边看着呢。"
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
return self._runtime_defaults()
|
||||
return settings
|
||||
@@ -1910,6 +1754,7 @@ class MainWindow(QMainWindow):
|
||||
"running": "安全运行中,检测到未读消息后自动处理",
|
||||
"stopping": "等待后台任务退出",
|
||||
"error": "连接失败,请查看运行日志",
|
||||
"verification": "请用手机企业微信扫码验证,完成后重新开始监听",
|
||||
"stopped": "等待连接企业微信",
|
||||
}
|
||||
hint = hints.get(state, text)
|
||||
@@ -1922,6 +1767,14 @@ class MainWindow(QMainWindow):
|
||||
return
|
||||
self._saved_geometry = self.saveGeometry()
|
||||
self._was_maximized = self.isMaximized()
|
||||
# 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()
|
||||
if self._portal_was_visible:
|
||||
self.portal_page.view.hide()
|
||||
QApplication.processEvents()
|
||||
self.capsule.show_near(self)
|
||||
self.hide()
|
||||
|
||||
@@ -1935,6 +1788,13 @@ class MainWindow(QMainWindow):
|
||||
self.showNormal()
|
||||
self.raise_()
|
||||
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)
|
||||
|
||||
QTimer.singleShot(80, restore_portal)
|
||||
self._portal_was_visible = False
|
||||
|
||||
def _finish_thread(self, final_state: str) -> None:
|
||||
self._running = False
|
||||
@@ -1999,6 +1859,10 @@ class MainWindow(QMainWindow):
|
||||
self.set_status("waiting", "等待企业微信")
|
||||
elif data == "error":
|
||||
self._finish_thread("error")
|
||||
elif data == "verification":
|
||||
self._finish_thread("verification")
|
||||
self.set_status("verification", "需要扫码验证")
|
||||
self.show_page(5)
|
||||
elif data == "stopped":
|
||||
self._finish_thread("stopped")
|
||||
elif kind == "stats":
|
||||
@@ -2029,6 +1893,10 @@ class MainWindow(QMainWindow):
|
||||
|
||||
|
||||
def _app_icon() -> QIcon:
|
||||
icon = QIcon(str(APP_ICON_PATH))
|
||||
if not icon.isNull():
|
||||
return icon
|
||||
|
||||
pixmap = QPixmap(64, 64)
|
||||
pixmap.fill(Qt.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
@@ -2066,6 +1934,74 @@ def install_console_shutdown_handler(app: QApplication, window: MainWindow):
|
||||
return request_shutdown
|
||||
|
||||
|
||||
def handle_startup_update(release: object) -> bool:
|
||||
"""显示启动升级提示;返回 False 表示本次不能进入软件。"""
|
||||
status = release_status(release)
|
||||
if not status["update_available"]:
|
||||
return True
|
||||
|
||||
forced = status["force_upgrade"]
|
||||
message = QMessageBox()
|
||||
message.setIcon(QMessageBox.Icon.Critical if forced else QMessageBox.Icon.Information)
|
||||
message.setWindowTitle("必须升级" if forced else "发现新版本")
|
||||
message.setText(
|
||||
f"当前版本:v{status['local_version']}\n"
|
||||
f"云端版本:v{status['latest_version']}"
|
||||
)
|
||||
message.setInformativeText(
|
||||
"管理员已设置强制升级,升级前无法继续使用。"
|
||||
if forced
|
||||
else "是否现在前往下载新版本?也可以稍后继续使用。"
|
||||
)
|
||||
if status["release_notes"]:
|
||||
message.setDetailedText(status["release_notes"][:4000])
|
||||
upgrade_button = message.addButton("立即升级", QMessageBox.ButtonRole.AcceptRole)
|
||||
message.addButton(
|
||||
"退出软件" if forced else "稍后使用", QMessageBox.ButtonRole.RejectRole
|
||||
)
|
||||
message.setDefaultButton(upgrade_button)
|
||||
message.exec()
|
||||
|
||||
if message.clickedButton() is upgrade_button:
|
||||
download_url = status["download_url"]
|
||||
if not download_url or not QDesktopServices.openUrl(QUrl(download_url)):
|
||||
QMessageBox.warning(
|
||||
None,
|
||||
"无法打开下载地址",
|
||||
"升级下载地址暂时无法打开,请联系管理员。",
|
||||
)
|
||||
return not forced
|
||||
return not forced
|
||||
|
||||
|
||||
def run_packaging_self_check(app: QApplication) -> int:
|
||||
"""离线验证随 EXE 打包的 Qt WebEngine 能否真正创建并加载页面。"""
|
||||
probe = QWebEngineView()
|
||||
state = {"finished": False}
|
||||
|
||||
def finish(ok: bool) -> None:
|
||||
if state["finished"]:
|
||||
return
|
||||
state["finished"] = True
|
||||
probe.close()
|
||||
app.exit(0 if ok else 3)
|
||||
|
||||
def timeout() -> None:
|
||||
if not state["finished"]:
|
||||
state["finished"] = True
|
||||
probe.close()
|
||||
app.exit(4)
|
||||
|
||||
probe.loadFinished.connect(finish)
|
||||
probe.setGeometry(-32000, -32000, 320, 180)
|
||||
probe.show()
|
||||
probe.setHtml(
|
||||
"<html><meta charset='utf-8'><body>Qt WebEngine packaging check</body></html>"
|
||||
)
|
||||
QTimer.singleShot(30_000, timeout)
|
||||
return app.exec()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||
@@ -2080,13 +2016,20 @@ def main() -> None:
|
||||
font.setHintingPreference(QFont.PreferFullHinting)
|
||||
app.setFont(font)
|
||||
|
||||
if "--packaging-self-check" in sys.argv:
|
||||
raise SystemExit(run_packaging_self_check(app))
|
||||
|
||||
startup_result = {}
|
||||
try:
|
||||
import backend_client
|
||||
|
||||
backend_client.startup_sync_config(timeout=3.0)
|
||||
startup_result = backend_client.startup_sync_config(timeout=3.0)
|
||||
except Exception:
|
||||
# 启动检测失败不阻止软件打开,具体原因会显示在后台同步卡片中。
|
||||
pass
|
||||
# 云端暂时不可用时仍执行上次成功同步的强制升级策略。
|
||||
startup_result = {"release": backend_client.cached_release_status()}
|
||||
|
||||
if not handle_startup_update(startup_result.get("release")):
|
||||
return
|
||||
|
||||
window = MainWindow()
|
||||
console_shutdown = install_console_shutdown_handler(app, window)
|
||||
|
||||
Reference in New Issue
Block a user