feat: 接入远程订阅策略门禁 T-705
Tests / Python 3.11 / Windows (push) Has been cancelled

This commit is contained in:
chengma
2026-07-28 15:42:19 +08:00
parent 593c6f77cb
commit df9da63841
15 changed files with 925 additions and 73 deletions
+45 -3
View File
@@ -6,7 +6,16 @@ import os
import sys
from dataclasses import replace
from .. import appconfig, chrome, db, diagnostics, image_studio_generation, update_check, update_health
from .. import (
appconfig,
chrome,
client_policy,
db,
diagnostics,
image_studio_generation,
update_check,
update_health,
)
from ..version import APP_NAME, APP_VERSION, display_name
from . import widgets as _widgets
from .widgets import *
@@ -70,6 +79,10 @@ def _write_update_check_diagnostic(message, *, result=None, exc=None):
"min_supported_version": result.min_supported_version,
"download_url": result.download_url,
"error": result.error,
"client_policy_mode": (
result.client_policy.mode if result.client_policy is not None else ""
),
"client_policy_error": result.client_policy_error,
}
try:
diagnostics.write_diagnostic_log(
@@ -91,15 +104,24 @@ def _show_forced_update_dialog(result, *, parent=None, dialog_factory=None) -> b
return False
def _run_startup_update_gate(*, checker=None, dialog_factory=None) -> bool:
def _run_startup_update_gate(
*,
checker=None,
dialog_factory=None,
result_callback=None,
) -> bool:
try:
result = (checker or update_check.check_for_update)()
except Exception as exc:
_write_update_check_diagnostic("启动版本检查异常,已允许继续使用", exc=exc)
return True
if result_callback is not None:
result_callback(result)
if result.error:
_write_update_check_diagnostic("启动版本检查失败,已允许继续使用", result=result)
elif result.client_policy_error:
_write_update_check_diagnostic("客户端订阅策略已按安全兼容规则处理", result=result)
if result.forced:
failed = update_health.get_failed_release(
appconfig.app_base_dir(),
@@ -148,7 +170,8 @@ def main() -> int:
update_health.write_health(health_context, "environment_blocked", str(exc))
QMessageBox.critical(None, "启动配置错误", str(exc))
return 1
if not _run_startup_update_gate():
startup_update_results = []
if not _run_startup_update_gate(result_callback=startup_update_results.append):
return 1
try:
startup = chrome.ensure_configured_chrome_path()
@@ -183,9 +206,28 @@ def main() -> int:
)
except Exception as exc:
_write_update_check_diagnostic("自定义网关套图任务恢复检查失败", exc=exc)
remote_policy = (
startup_update_results[-1].client_policy
if startup_update_results
else None
)
resolved_policy = client_policy.resolve_client_policy(
remote_policy,
config=startup["config"],
)
if resolved_policy.warning:
_write_update_check_diagnostic(
"客户端订阅策略使用兼容模式",
result=(
startup_update_results[-1]
if startup_update_results
else None
),
)
window = MainWindow(
config=startup["config"],
startup_status=startup["message"],
subscription_policy=resolved_policy,
)
if runtime_lease is not None:
app.aboutToQuit.connect(runtime_lease.release)
+132 -37
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from PySide6.QtCore import QUrl
from PySide6.QtGui import QDesktopServices
from .. import subscription
from .. import client_policy, subscription
from ..version import display_name
from .activation_dialog import MembershipActivationDialog
from .tabs.accounts import AccountsTab
@@ -21,7 +21,7 @@ from .workers import SubscriptionCheckWorker
PREFERRED_WINDOW_SIZE = (1180, 760)
MIN_WINDOW_SIZE = (960, 640)
WINDOW_SCREEN_MARGIN = 40
# 当前开发版本启用真实查询和客户端强制门禁。
# 仅供直接构造 MainWindow 的兼容入口使用;正式启动以远程策略为准。
SUBSCRIPTION_CHECK_ENABLED = True
SUBSCRIPTION_ENFORCEMENT_ENABLED = True
@@ -91,6 +91,7 @@ class MainWindow(QMainWindow):
config_path=None,
ai_models_path=None,
startup_status="",
subscription_policy=None,
):
super().__init__()
self._initial_window_fit_applied_after_show = False
@@ -106,6 +107,17 @@ class MainWindow(QMainWindow):
or self.config.get("ai_models_path")
or appconfig.ai_models_config_path(self.config)
)
self._client_policy = (
subscription_policy
if isinstance(subscription_policy, client_policy.ClientPolicy)
else client_policy.ClientPolicy(
policy_version=client_policy.POLICY_VERSION,
subscription_check_enabled=SUBSCRIPTION_CHECK_ENABLED,
subscription_enforcement_enabled=SUBSCRIPTION_ENFORCEMENT_ENABLED,
updated_at="",
source="compatibility",
)
)
self.setWindowTitle(display_name())
window_icon = app_icon()
if window_icon is not None:
@@ -123,6 +135,8 @@ class MainWindow(QMainWindow):
self._subscription_request_token = 0
self._subscription_closed = False
self._expired_subscription_notice_shown = False
self._subscription_notice_state = None
self._subscription_notice_states_seen = set()
self._activation_dialog = None
self._first_use_guide_shown = False
self.tabs = QTabWidget()
@@ -196,12 +210,17 @@ class MainWindow(QMainWindow):
status_callback=self.show_status,
)
if title == "设置":
return SettingsTab(
tab = SettingsTab(
config=self.config,
config_path=self.config_path,
ai_models_path=self.ai_models_path,
status_callback=self.show_status,
)
if hasattr(tab, "set_subscription_policy_enabled"):
tab.set_subscription_policy_enabled(
self._client_policy.subscription_check_enabled
)
return tab
if title == "商品套图":
return ProductSuiteTab(
db_path=self.db_path,
@@ -231,20 +250,26 @@ class MainWindow(QMainWindow):
def subscription_status(self):
return self._subscription_status
@property
def subscription_policy(self):
return self._client_policy
def begin_subscription_check(self):
"""Refresh membership state without blocking the Qt GUI thread."""
self._set_membership_window_title()
if not SUBSCRIPTION_CHECK_ENABLED:
if not self._client_policy.subscription_check_enabled:
self._set_product_access(True)
self._set_subscription_check_running(False)
self.show_status("会员订阅检测已暂停", level="muted")
self.show_status("服务端暂未启用会员检测", level="muted")
return
self._subscription_request_token += 1
token = self._subscription_request_token
if self._subscription_worker is not None:
self._subscription_worker.cancel()
self._set_product_access(not SUBSCRIPTION_ENFORCEMENT_ENABLED)
self._set_product_access(
not self._client_policy.subscription_enforcement_enabled
)
self._set_subscription_check_running(True)
self.show_status("正在验证会员状态", level="info")
worker = SubscriptionCheckWorker(
@@ -272,7 +297,10 @@ class MainWindow(QMainWindow):
def ensure_subscription_for_new_submit(self, action_name="生成"):
"""Return whether a new product request may start from the current UI."""
if not SUBSCRIPTION_CHECK_ENABLED or not SUBSCRIPTION_ENFORCEMENT_ENABLED:
if (
not self._client_policy.subscription_check_enabled
or not self._client_policy.subscription_enforcement_enabled
):
return True
status = self._subscription_status
if status.allows_product_workflows:
@@ -309,6 +337,8 @@ class MainWindow(QMainWindow):
self._subscription_status = status
if status.allows_product_workflows:
self._expired_subscription_notice_shown = False
self._subscription_notice_state = None
self._subscription_notice_states_seen.clear()
if status.state == subscription.STATUS_ACTIVE:
expiry = subscription.format_expiry(status.expires_at)
self._set_membership_window_title(
@@ -330,25 +360,25 @@ class MainWindow(QMainWindow):
access_allowed = (
status.allows_product_workflows
or not SUBSCRIPTION_ENFORCEMENT_ENABLED
or not self._client_policy.subscription_enforcement_enabled
)
self._set_product_access(access_allowed)
if status.allows_product_workflows:
if status.state == subscription.STATUS_LEGACY:
self.show_status(status.user_message, level="muted")
else:
message = (
"会员状态已验证"
if SUBSCRIPTION_ENFORCEMENT_ENABLED
else "会员状态已验证,当前处于观察模式"
)
self.show_status(message, level="success")
if SUBSCRIPTION_ENFORCEMENT_ENABLED and show_notice:
self._show_subscription_notice_once(status)
message = (
"会员状态已验证"
if self._client_policy.subscription_enforcement_enabled
else "会员状态已验证,当前处于观察模式"
)
self.show_status(message, level="success")
if (
self._client_policy.subscription_enforcement_enabled
and show_notice
):
self._show_subscription_notice_once(status)
if show_first_use_guide:
QTimer.singleShot(0, self._show_first_use_guide_if_pending)
return
if not SUBSCRIPTION_ENFORCEMENT_ENABLED:
if not self._client_policy.subscription_enforcement_enabled:
self.show_status(
"订阅观察:%s,当前不影响使用" % status.user_message,
level=self._subscription_level(status),
@@ -359,9 +389,17 @@ class MainWindow(QMainWindow):
self.show_status("请先绑定会员账号", level="warning")
self._show_membership_activation()
return
if status.state == subscription.STATUS_KEY_INVALID:
self.open_settings_tab()
self._show_membership_activation()
if self._activation_dialog is not None:
self._activation_dialog.focus_api_key()
return
self.open_settings_tab()
if status.state == subscription.STATUS_EXPIRED:
self._show_expired_subscription_notice_once(status)
return
self._show_subscription_state_notice_once(status)
def _show_membership_activation(self):
if self._activation_dialog is not None:
@@ -489,6 +527,7 @@ class MainWindow(QMainWindow):
if (
result != QDialog.Accepted
and not self._subscription_closed
and self._client_policy.subscription_enforcement_enabled
and not self._subscription_status.allows_product_workflows
):
self.close()
@@ -605,38 +644,94 @@ class MainWindow(QMainWindow):
settings_tab.set_subscription_check_running(running)
def _show_expired_subscription_notice_once(self, status):
if self._expired_subscription_notice_shown:
return
self._expired_subscription_notice_shown = True
self._show_subscription_state_notice_once(status)
def _show_subscription_state_notice_once(self, status):
state = status.state
notice_state = (
subscription.STATUS_UNAVAILABLE
if state == subscription.STATUS_LEGACY
else state
)
if notice_state in self._subscription_notice_states_seen:
return
self._subscription_notice_states_seen.add(notice_state)
self._subscription_notice_state = notice_state
self._expired_subscription_notice_shown = (
notice_state == subscription.STATUS_EXPIRED
)
titles = {
subscription.STATUS_REQUIRED: "尚未开通会员套餐",
subscription.STATUS_EXPIRED: "会员套餐已过期",
subscription.STATUS_REVOKED: "会员套餐已失效",
subscription.STATUS_ACCOUNT_DISABLED: "会员账号当前不可用",
subscription.STATUS_UNAVAILABLE: "暂时无法验证会员状态",
}
messages = {
subscription.STATUS_REQUIRED: (
"当前账号尚未开通蝦皮圈会员套餐,业务功能已暂停。"
"请前往会员中心选择套餐,完成后重新检测会员状态。"
),
subscription.STATUS_EXPIRED: (
"当前账号的蝦皮圈会员已到期,业务功能已暂停。"
"请前往会员中心续费或更换套餐,完成后重新检测会员状态。"
),
subscription.STATUS_REVOKED: (
"当前账号的蝦皮圈会员套餐已失效,业务功能已暂停。"
"请前往会员中心查看套餐状态,处理后重新检测。"
),
subscription.STATUS_ACCOUNT_DISABLED: (
"当前会员账号暂时不可用,业务功能已暂停。"
"请前往会员中心查看账号状态或联系管理员处理。"
),
subscription.STATUS_UNAVAILABLE: (
"当前暂时无法验证会员状态,业务功能已暂停。"
"这不代表账号未付费,请检查网络或服务状态后重新检测。"
),
}
box = QMessageBox(self)
box.setIcon(QMessageBox.Warning)
box.setWindowTitle("会员套餐已过期")
text = (
"当前账号的蝦皮圈会员已到期,业务功能已暂停。"
"请前往会员中心续费或更换套餐,完成后返回设置重新检测会员状态。"
box.setWindowTitle(titles.get(notice_state, "暂时无法验证会员状态"))
text = messages.get(
notice_state,
messages[subscription.STATUS_UNAVAILABLE],
)
manage_button = None
settings_button = None
manage_url = str(status.manage_url or "").strip()
if not manage_url:
text += "\n\n会员中心地址当前不可用,请检查默认网关配置或稍后重试。"
box.setText(text)
manage_button = box.addButton("前往会员中心", QMessageBox.ActionRole)
if notice_state != subscription.STATUS_UNAVAILABLE:
manage_button = box.addButton(
"前往会员中心",
QMessageBox.ActionRole,
)
manage_button.setEnabled(bool(manage_url))
if manage_url:
box.setDefaultButton(manage_button)
else:
manage_button.setToolTip("会员中心地址当前不可用")
text += "\n\n会员中心地址当前不可用,请稍后重试。"
retry_button = box.addButton("重新检测", QMessageBox.AcceptRole)
if notice_state == subscription.STATUS_UNAVAILABLE:
settings_button = box.addButton("打开设置", QMessageBox.ActionRole)
box.setDefaultButton(retry_button)
exit_button = box.addButton("退出程序", QMessageBox.DestructiveRole)
manage_button.setEnabled(bool(manage_url))
if manage_url:
box.setDefaultButton(manage_button)
else:
manage_button.setToolTip("会员中心地址当前不可用")
box.setText(text)
box.exec()
clicked = box.clickedButton()
if clicked is manage_button and manage_url:
if manage_button is not None and clicked is manage_button and manage_url:
opened = QDesktopServices.openUrl(QUrl(manage_url))
if opened is False:
self.show_status(
"无法打开会员中心,请检查系统默认浏览器后重试",
level="warning",
)
elif clicked is retry_button:
self.begin_subscription_check()
elif settings_button is not None and clicked is settings_button:
self.open_settings_tab()
elif clicked is exit_button:
self.close()
+30 -2
View File
@@ -283,6 +283,7 @@ class SettingsTab(QWidget):
self.subscription_check_button = QPushButton("重新检测会员状态")
self.subscription_check_button.setObjectName("subscriptionCheckButton")
self.subscription_check_button.setToolTip("重新查询当前 cmhub 账号的会员套餐状态")
self._subscription_policy_enabled = True
self.unsaved_changes_label = QLabel("● 未保存更改")
self.unsaved_changes_label.setObjectName("settingsUnsavedChangesLabel")
self.unsaved_changes_label.setStyleSheet("color: #bc4c00; font-weight: 600;")
@@ -557,13 +558,40 @@ class SettingsTab(QWidget):
_emit_status(self.status_callback, message, level=level)
def _request_subscription_check(self, checked=False):
if not self._subscription_policy_enabled:
return
self.subscriptionCheckRequested.emit()
def set_subscription_policy_enabled(self, enabled):
self._subscription_policy_enabled = bool(enabled)
self.subscription_check_button.setVisible(True)
self.subscription_check_button.setEnabled(self._subscription_policy_enabled)
if self._subscription_policy_enabled:
self.subscription_check_button.setText("重新检测会员状态")
self.subscription_check_button.setToolTip(
"重新查询当前 cmhub 账号的会员套餐状态"
)
return
self.subscription_check_button.setText("会员检测暂未启用")
self.subscription_check_button.setToolTip(
"服务端当前未启用客户端会员检测,本次运行无需手动检测"
)
def set_subscription_check_running(self, running):
running = bool(running)
self.subscription_check_button.setEnabled(not running)
self.subscription_check_button.setEnabled(
self._subscription_policy_enabled and not running
)
self.subscription_check_button.setText(
"正在检测会员状态..." if running else "重新检测会员状态"
(
"正在检测会员状态..."
if running
else (
"重新检测会员状态"
if self._subscription_policy_enabled
else "会员检测暂未启用"
)
)
)
def refresh_default_gateway_credentials(self):