This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""Remote client-policy parsing and non-sensitive local caching."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from dataclasses import dataclass, replace
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from . import appconfig
|
||||
|
||||
|
||||
POLICY_VERSION = 1
|
||||
CACHE_VERSION = 1
|
||||
CACHE_FILENAME = "client_policy.json"
|
||||
|
||||
|
||||
class ClientPolicyError(ValueError):
|
||||
"""Raised when a remote or cached client policy is malformed."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClientPolicy:
|
||||
policy_version: int
|
||||
subscription_check_enabled: bool
|
||||
subscription_enforcement_enabled: bool
|
||||
updated_at: str
|
||||
source: str = "remote"
|
||||
warning: str = ""
|
||||
|
||||
@property
|
||||
def mode(self) -> str:
|
||||
if not self.subscription_check_enabled:
|
||||
return "off"
|
||||
if self.subscription_enforcement_enabled:
|
||||
return "enforce"
|
||||
return "observe"
|
||||
|
||||
def to_contract_dict(self) -> dict:
|
||||
return {
|
||||
"policy_version": self.policy_version,
|
||||
"subscription_check_enabled": self.subscription_check_enabled,
|
||||
"subscription_enforcement_enabled": (
|
||||
self.subscription_enforcement_enabled
|
||||
),
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def observation_policy(*, source="fallback", warning="") -> ClientPolicy:
|
||||
"""Return the fail-open client fallback that still observes entitlement."""
|
||||
|
||||
return ClientPolicy(
|
||||
policy_version=POLICY_VERSION,
|
||||
subscription_check_enabled=True,
|
||||
subscription_enforcement_enabled=False,
|
||||
updated_at="",
|
||||
source=source,
|
||||
warning=warning,
|
||||
)
|
||||
|
||||
|
||||
def parse_client_policy(value, *, source="remote") -> ClientPolicy:
|
||||
if not isinstance(value, dict):
|
||||
raise ClientPolicyError("客户端策略不是 JSON 对象")
|
||||
|
||||
policy_version = value.get("policy_version")
|
||||
if type(policy_version) is not int or policy_version != POLICY_VERSION:
|
||||
raise ClientPolicyError("客户端策略版本不受支持")
|
||||
|
||||
check_enabled = value.get("subscription_check_enabled")
|
||||
enforcement_enabled = value.get("subscription_enforcement_enabled")
|
||||
if type(check_enabled) is not bool or type(enforcement_enabled) is not bool:
|
||||
raise ClientPolicyError("客户端订阅策略开关必须是布尔值")
|
||||
|
||||
updated_at = _parse_timezone_timestamp(
|
||||
value.get("updated_at"),
|
||||
field_name="客户端策略更新时间",
|
||||
)
|
||||
warning = ""
|
||||
if not check_enabled and enforcement_enabled:
|
||||
enforcement_enabled = False
|
||||
warning = "客户端订阅策略组合非法,已按关闭模式处理"
|
||||
|
||||
return ClientPolicy(
|
||||
policy_version=policy_version,
|
||||
subscription_check_enabled=check_enabled,
|
||||
subscription_enforcement_enabled=enforcement_enabled,
|
||||
updated_at=updated_at,
|
||||
source=source,
|
||||
warning=warning,
|
||||
)
|
||||
|
||||
|
||||
def policy_cache_path(config=None) -> str:
|
||||
return appconfig.data_path("config", CACHE_FILENAME, config=config)
|
||||
|
||||
|
||||
def save_cached_policy(
|
||||
policy: ClientPolicy,
|
||||
*,
|
||||
config=None,
|
||||
path=None,
|
||||
cached_at=None,
|
||||
) -> str:
|
||||
destination = os.path.abspath(path or policy_cache_path(config))
|
||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
||||
acquired_at = _parse_timezone_timestamp(
|
||||
cached_at or _now_iso(),
|
||||
field_name="客户端策略缓存时间",
|
||||
)
|
||||
payload = {
|
||||
"cache_version": CACHE_VERSION,
|
||||
"cached_at": acquired_at,
|
||||
"client_policy": policy.to_contract_dict(),
|
||||
}
|
||||
temporary_path = ""
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
dir=os.path.dirname(destination),
|
||||
prefix="client-policy-",
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
) as handle:
|
||||
temporary_path = handle.name
|
||||
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary_path, destination)
|
||||
finally:
|
||||
if temporary_path and os.path.exists(temporary_path):
|
||||
try:
|
||||
os.remove(temporary_path)
|
||||
except OSError:
|
||||
pass
|
||||
return destination
|
||||
|
||||
|
||||
def load_cached_policy(*, config=None, path=None) -> ClientPolicy:
|
||||
source_path = os.path.abspath(path or policy_cache_path(config))
|
||||
try:
|
||||
with open(source_path, "r", encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise ClientPolicyError("客户端策略缓存不可用") from exc
|
||||
cache_version = payload.get("cache_version") if isinstance(payload, dict) else None
|
||||
if type(cache_version) is not int or cache_version != CACHE_VERSION:
|
||||
raise ClientPolicyError("客户端策略缓存版本不受支持")
|
||||
_parse_timezone_timestamp(
|
||||
payload.get("cached_at"),
|
||||
field_name="客户端策略缓存时间",
|
||||
)
|
||||
return parse_client_policy(payload.get("client_policy"), source="cache")
|
||||
|
||||
|
||||
def resolve_client_policy(
|
||||
remote_policy,
|
||||
*,
|
||||
config=None,
|
||||
path=None,
|
||||
) -> ClientPolicy:
|
||||
"""Prefer a valid live policy, then cache, then observation mode."""
|
||||
|
||||
if isinstance(remote_policy, ClientPolicy):
|
||||
try:
|
||||
save_cached_policy(remote_policy, config=config, path=path)
|
||||
except (OSError, ClientPolicyError) as exc:
|
||||
warning = _join_warning(
|
||||
remote_policy.warning,
|
||||
"客户端策略缓存写入失败:%s" % exc,
|
||||
)
|
||||
return replace(remote_policy, source="remote", warning=warning)
|
||||
return replace(remote_policy, source="remote")
|
||||
|
||||
try:
|
||||
return load_cached_policy(config=config, path=path)
|
||||
except ClientPolicyError as exc:
|
||||
return observation_policy(
|
||||
warning="未取得有效远程策略或本地缓存,已使用观察模式:%s" % exc
|
||||
)
|
||||
|
||||
|
||||
def _parse_timezone_timestamp(value, *, field_name) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
raise ClientPolicyError("%s不能为空" % field_name)
|
||||
try:
|
||||
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
||||
except ValueError as exc:
|
||||
raise ClientPolicyError("%s格式不正确" % field_name) from exc
|
||||
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||
raise ClientPolicyError("%s必须包含时区" % field_name)
|
||||
return text
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def _join_warning(*messages) -> str:
|
||||
return ";".join(str(message).strip() for message in messages if str(message).strip())
|
||||
+45
-3
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
+13
-1
@@ -51,6 +51,7 @@ class SubscriptionStatus:
|
||||
grace_expires_at: str = ""
|
||||
manage_url: str = ""
|
||||
notice_id: str = ""
|
||||
real_entitlement_allowed: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
if self.state not in _ALLOWED_STATES:
|
||||
@@ -58,7 +59,7 @@ class SubscriptionStatus:
|
||||
|
||||
@property
|
||||
def allows_product_workflows(self) -> bool:
|
||||
return self.state in {STATUS_ACTIVE, STATUS_GRACE, STATUS_LEGACY}
|
||||
return self.real_entitlement_allowed
|
||||
|
||||
@property
|
||||
def credentials_accepted(self) -> bool:
|
||||
@@ -184,6 +185,16 @@ def _parse_status_response(data, base_url: str) -> SubscriptionStatus:
|
||||
normalized = _normalize_remote_state(data.get("status"))
|
||||
if not normalized:
|
||||
return SubscriptionStatus(STATUS_UNAVAILABLE)
|
||||
real_entitlement_allowed = data.get("real_entitlement_allowed")
|
||||
if type(real_entitlement_allowed) is not bool:
|
||||
return SubscriptionStatus(STATUS_UNAVAILABLE)
|
||||
if real_entitlement_allowed and normalized not in {
|
||||
STATUS_ACTIVE,
|
||||
STATUS_GRACE,
|
||||
}:
|
||||
return SubscriptionStatus(STATUS_UNAVAILABLE)
|
||||
if normalized in {STATUS_ACTIVE, STATUS_GRACE} and not real_entitlement_allowed:
|
||||
return SubscriptionStatus(STATUS_UNAVAILABLE)
|
||||
account = data.get("account") if isinstance(data.get("account"), dict) else {}
|
||||
plan = data.get("plan") if isinstance(data.get("plan"), dict) else {}
|
||||
account_name = str(account.get("display_name") or "").strip()
|
||||
@@ -211,6 +222,7 @@ def _parse_status_response(data, base_url: str) -> SubscriptionStatus:
|
||||
grace_expires_at=grace_expires_at,
|
||||
manage_url=manage_url,
|
||||
notice_id=notice_id,
|
||||
real_entitlement_allowed=real_entitlement_allowed,
|
||||
)
|
||||
|
||||
|
||||
|
||||
+32
-2
@@ -11,6 +11,7 @@ import re
|
||||
import urllib.request
|
||||
from dataclasses import dataclass
|
||||
|
||||
from . import client_policy
|
||||
from .version import APP_CODE_NAME, APP_UPDATE_CHECK_URL, APP_VERSION
|
||||
|
||||
DEFAULT_TIMEOUT_SECONDS = 3.0
|
||||
@@ -54,6 +55,8 @@ class UpdateCheckResult:
|
||||
automatic_update_error: str = ""
|
||||
message: str = ""
|
||||
error: str = ""
|
||||
client_policy: client_policy.ClientPolicy | None = None
|
||||
client_policy_error: str = ""
|
||||
|
||||
@property
|
||||
def can_enter(self) -> bool:
|
||||
@@ -202,8 +205,31 @@ def check_for_update(
|
||||
return UpdateCheckResult(current_version=current_version, checked=False)
|
||||
|
||||
try:
|
||||
payload = (fetcher or fetch_update_payload)(url, timeout)
|
||||
info = parse_update_info(_decode_payload(payload))
|
||||
payload = _decode_payload((fetcher or fetch_update_payload)(url, timeout))
|
||||
except Exception as exc:
|
||||
return UpdateCheckResult(
|
||||
current_version=current_version,
|
||||
checked=True,
|
||||
forced=False,
|
||||
error=f"启动版本检查失败,已允许继续使用:{exc}",
|
||||
)
|
||||
|
||||
resolved_policy = None
|
||||
policy_error = ""
|
||||
if isinstance(payload, dict) and "client_policy" in payload:
|
||||
try:
|
||||
resolved_policy = client_policy.parse_client_policy(
|
||||
payload.get("client_policy")
|
||||
)
|
||||
policy_error = resolved_policy.warning
|
||||
except client_policy.ClientPolicyError as exc:
|
||||
policy_error = str(exc)
|
||||
|
||||
try:
|
||||
if isinstance(payload, dict) and payload.get("release", object()) is None:
|
||||
info = UpdateInfo()
|
||||
else:
|
||||
info = parse_update_info(payload)
|
||||
forced = is_forced_update(info, current_version)
|
||||
return UpdateCheckResult(
|
||||
current_version=current_version,
|
||||
@@ -220,6 +246,8 @@ def check_for_update(
|
||||
signature_algorithm=info.signature_algorithm,
|
||||
manifest_signature=info.manifest_signature,
|
||||
message=info.message,
|
||||
client_policy=resolved_policy,
|
||||
client_policy_error=policy_error,
|
||||
)
|
||||
except Exception as exc:
|
||||
return UpdateCheckResult(
|
||||
@@ -227,4 +255,6 @@ def check_for_update(
|
||||
checked=True,
|
||||
forced=False,
|
||||
error=f"启动版本检查失败,已允许继续使用:{exc}",
|
||||
client_policy=resolved_policy,
|
||||
client_policy_error=policy_error,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user