diff --git a/account_switcher.py b/account_switcher.py
index ed2784e..49b2f14 100644
--- a/account_switcher.py
+++ b/account_switcher.py
@@ -50,9 +50,41 @@ class AccountSwitcher:
self.package_json = self.app_path / "package.json"
self.auth_manager = CursorAuthManager()
self.config = Config()
- self.hardware_id = get_hardware_id()
+ self.hardware_id = self.get_hardware_id() # 先获取硬件ID
self.registry = CursorRegistry() # 添加注册表操作工具类
+ logging.info(f"初始化硬件ID: {self.hardware_id}")
+
+ def get_hardware_id(self) -> str:
+ """获取硬件唯一标识"""
+ try:
+ # 创建startupinfo对象来隐藏命令行窗口
+ startupinfo = None
+ if sys.platform == "win32":
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ startupinfo.wShowWindow = subprocess.SW_HIDE
+
+ # 获取CPU信息
+ cpu_info = subprocess.check_output('wmic cpu get ProcessorId', startupinfo=startupinfo).decode()
+ cpu_id = cpu_info.split('\n')[1].strip()
+
+ # 获取主板序列号
+ board_info = subprocess.check_output('wmic baseboard get SerialNumber', startupinfo=startupinfo).decode()
+ board_id = board_info.split('\n')[1].strip()
+
+ # 获取BIOS序列号
+ bios_info = subprocess.check_output('wmic bios get SerialNumber', startupinfo=startupinfo).decode()
+ bios_id = bios_info.split('\n')[1].strip()
+
+ # 组合信息并生成哈希
+ combined = f"{cpu_id}:{board_id}:{bios_id}"
+ return hashlib.md5(combined.encode()).hexdigest()
+ except Exception as e:
+ logging.error(f"获取硬件ID失败: {str(e)}")
+ # 如果获取失败,使用UUID作为备选方案
+ return str(uuid.uuid4())
+
def get_cursor_version(self) -> str:
"""获取Cursor版本号"""
try:
@@ -71,199 +103,103 @@ class AccountSwitcher:
import platform
import socket
import requests
+ import subprocess
# 获取操作系统信息
- os_info = f"{platform.system()} {platform.version()}"
+ os_info = f"{platform.system()} {platform.release()}"
# 获取设备名称
- device_name = platform.node()
-
- # 获取地理位置(可选)
try:
- ip_info = requests.get('https://ipapi.co/json/', timeout=5).json()
+ # 在Windows上使用wmic获取更详细的计算机名称
+ if platform.system() == "Windows":
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ computer_info = subprocess.check_output('wmic computersystem get model', startupinfo=startupinfo).decode()
+ device_name = computer_info.split('\n')[1].strip()
+ else:
+ device_name = platform.node()
+ except:
+ device_name = platform.node()
+
+ # 获取IP地址
+ try:
+ ip_response = requests.get('https://api.ipify.org?format=json', timeout=5)
+ ip_address = ip_response.json()['ip']
+ except:
+ ip_address = "未知"
+
+ # 获取地理位置
+ try:
+ ip_info = requests.get(f'https://ipapi.co/{ip_address}/json/', timeout=5).json()
location = f"{ip_info.get('country_name', '')}-{ip_info.get('region', '')}-{ip_info.get('city', '')}"
except:
- location = ""
+ location = "未知"
return {
"os": os_info,
"device_name": device_name,
+ "ip": ip_address,
"location": location
}
except Exception as e:
logging.error(f"获取设备信息失败: {str(e)}")
return {
- "os": "Windows 10",
+ "os": "Windows",
"device_name": "未知设备",
- "location": ""
+ "ip": "未知",
+ "location": "未知"
}
- def check_activation_code(self, code: str) -> Tuple[bool, str, Optional[Dict]]:
- """验证激活码
+ def check_activation_code(self, code: str) -> tuple:
+ """检查激活码
+ Args:
+ code: 激活码
+
Returns:
- Tuple[bool, str, Optional[Dict]]: (是否成功, 提示消息, 账号信息)
+ tuple: (成功标志, 消息, 账号信息)
"""
try:
- # 获取当前状态和历史记录
- member_info = self.config.load_member_info()
- activation_history = member_info.get("activation_records", []) if member_info else []
+ data = {
+ "machine_id": self.hardware_id,
+ "code": code
+ }
- # 分割多个激活码
- codes = [c.strip() for c in code.split(',')]
- success_codes = []
- failed_codes = []
- activation_results = []
-
- # 获取设备信息
- device_info = self.get_device_info()
-
- # 逐个验证激活码
- for single_code in codes:
- if not single_code:
- continue
-
- # 验证激活码
- endpoint = "https://cursorapi.nosqli.com/admin/api.member/activate"
- data = {
- "code": single_code,
- "machine_id": self.hardware_id,
- "os": device_info["os"],
- "device_name": device_info["device_name"],
- "location": device_info["location"]
+ response = requests.post(
+ self.config.get_api_url("activate"),
+ json=data,
+ headers={"Content-Type": "application/json"},
+ timeout=10
+ )
+
+ result = response.json()
+ if result["code"] == 200:
+ api_data = result["data"]
+ # 构造标准的返回数据结构
+ account_info = {
+ "status": "active",
+ "expire_time": api_data.get("expire_time", ""),
+ "total_days": api_data.get("total_days", 0),
+ "days_left": api_data.get("days_left", 0),
+ "device_info": self.get_device_info()
}
-
- headers = {
- "Content-Type": "application/json"
- }
-
- try:
- response = requests.post(
- endpoint,
- json=data,
- headers=headers,
- timeout=10
- )
-
- response_data = response.json()
- if response_data.get("code") == 200:
- result_data = response_data.get("data", {})
- logging.info(f"激活码 {single_code} 验证成功: {response_data.get('msg', '')}")
- activation_results.append(result_data)
- success_codes.append(single_code)
- elif response_data.get("code") == 400:
- error_msg = response_data.get("msg", "参数错误")
- if "已被使用" in error_msg or "已激活" in error_msg:
- logging.warning(f"激活码 {single_code} 已被使用")
- failed_codes.append(f"{single_code} (已被使用)")
- else:
- logging.error(f"激活码 {single_code} 验证失败: {error_msg}")
- failed_codes.append(f"{single_code} ({error_msg})")
- elif response_data.get("code") == 500:
- error_msg = response_data.get("msg", "系统错误")
- logging.error(f"激活码 {single_code} 验证失败: {error_msg}")
- failed_codes.append(f"{single_code} ({error_msg})")
- else:
- error_msg = response_data.get("msg", "未知错误")
- logging.error(f"激活码 {single_code} 验证失败: {error_msg}")
- failed_codes.append(f"{single_code} ({error_msg})")
-
- except requests.RequestException as e:
- logging.error(f"激活码 {single_code} 请求失败: {str(e)}")
- failed_codes.append(f"{single_code} (网络请求失败)")
- except Exception as e:
- logging.error(f"激活码 {single_code} 处理失败: {str(e)}")
- failed_codes.append(f"{single_code} (处理失败)")
-
- if not success_codes:
- failed_msg = "\n".join(failed_codes)
- return False, f"激活失败:\n{failed_msg}", None
-
- try:
- # 使用最后一次激活的结果作为最终状态
- final_result = activation_results[-1]
-
- # 合并历史记录
- new_activation_records = final_result.get("activation_records", [])
- if activation_history:
- # 保留旧的激活记录,避免重复
- existing_codes = {record.get("code") for record in activation_history}
- for record in new_activation_records:
- if record.get("code") not in existing_codes:
- activation_history.append(record)
- else:
- activation_history = new_activation_records
-
- # 保存会员信息,包含完整的历史记录
- member_info = {
- "hardware_id": final_result.get("machine_id", self.hardware_id),
- "expire_time": final_result.get("expire_time", ""),
- "days_left": final_result.get("days_left", 0),
- "total_days": final_result.get("total_days", 0),
- "status": final_result.get("status", "inactive"),
- "device_info": final_result.get("device_info", device_info),
- "activation_time": final_result.get("activation_time", ""),
- "activation_records": activation_history # 使用合并后的历史记录
- }
- self.config.save_member_info(member_info)
-
- # 生成结果消息
- message = f"激活成功\n"
-
- # 显示每个成功激活码的信息
- for i, result in enumerate(activation_results, 1):
- message += f"\n第{i}个激活码:\n"
- message += f"- 新增天数: {result.get('added_days', 0)}天\n"
- activation_time = result.get('activation_time', '')
- if activation_time:
- try:
- from datetime import datetime
- dt = datetime.strptime(activation_time, "%Y-%m-%d %H:%M:%S")
- activation_time = dt.strftime("%Y-%m-%d %H:%M:%S")
- except:
- pass
- message += f"- 激活时间: {activation_time}\n"
-
- message += f"\n最终状态:"
- message += f"\n- 总天数: {final_result.get('total_days', 0)}天"
- message += f"\n- 剩余天数: {final_result.get('days_left', 0)}天"
-
- # 格式化到期时间显示
- expire_time = final_result.get('expire_time', '')
- if expire_time:
- try:
- dt = datetime.strptime(expire_time, "%Y-%m-%d %H:%M:%S")
- expire_time = dt.strftime("%Y-%m-%d %H:%M:%S")
- except:
- pass
- message += f"\n- 到期时间: {expire_time}"
-
- # 显示完整的激活记录历史
- message += "\n\n历史激活记录:"
- for record in activation_history:
- activation_time = record.get('activation_time', '')
- if activation_time:
- try:
- dt = datetime.strptime(activation_time, "%Y-%m-%d %H:%M:%S")
- activation_time = dt.strftime("%Y-%m-%d %H:%M:%S")
- except:
- pass
- message += f"\n- 激活码: {record.get('code', '')}"
- message += f"\n 天数: {record.get('days', 0)}天"
- message += f"\n 时间: {activation_time}\n"
-
- if failed_codes:
- message += f"\n\n以下激活码验证失败:\n" + "\n".join(failed_codes)
-
- return True, message, member_info
-
- except Exception as e:
- logging.error(f"处理激活结果时出错: {str(e)}")
- return False, f"处理激活结果时出错: {str(e)}", None
-
+ return True, result["msg"], account_info
+ return False, result["msg"], {
+ "status": "inactive",
+ "expire_time": "",
+ "total_days": 0,
+ "days_left": 0,
+ "device_info": self.get_device_info()
+ }
+
except Exception as e:
- logging.error(f"验证激活码时出错: {str(e)}")
- return False, f"验证激活码时出错: {str(e)}", None
+ return False, f"激活失败: {str(e)}", {
+ "status": "inactive",
+ "expire_time": "",
+ "total_days": 0,
+ "days_left": 0,
+ "device_info": self.get_device_info()
+ }
def reset_machine_id(self) -> bool:
"""重置机器码"""
@@ -356,93 +292,76 @@ class AccountSwitcher:
logging.error(f"激活过程出错: {str(e)}")
return False, f"激活失败: {str(e)}"
- def get_member_status(self) -> Optional[Dict]:
+ def get_member_status(self) -> dict:
"""获取会员状态
Returns:
- Optional[Dict]: 会员状态信息
+ dict: 会员状态信息,包含:
+ - status: 状态(active/inactive/expired)
+ - expire_time: 到期时间
+ - total_days: 总天数
+ - days_left: 剩余天数
+ - device_info: 设备信息
"""
try:
- # 读取保存的会员信息
- member_info = self.config.load_member_info()
-
- # 构造状态检查请求
- endpoint = "https://cursorapi.nosqli.com/admin/api.member/status"
data = {
"machine_id": self.hardware_id
}
- headers = {
- "Content-Type": "application/json"
+
+ api_url = self.config.get_api_url("status")
+ logging.info(f"正在检查会员状态...")
+ logging.info(f"API URL: {api_url}")
+ logging.info(f"请求数据: {data}")
+
+ response = requests.post(
+ api_url,
+ json=data,
+ headers={"Content-Type": "application/json"},
+ timeout=10
+ )
+
+ result = response.json()
+ logging.info(f"状态检查响应: {result}")
+
+ if result.get("code") in [1, 200]: # 兼容两种响应码
+ api_data = result.get("data", {})
+ # 构造标准的返回数据结构
+ return {
+ "status": api_data.get("status", "inactive"),
+ "expire_time": api_data.get("expire_time", ""),
+ "total_days": api_data.get("total_days", 0),
+ "days_left": api_data.get("days_left", 0),
+ "device_info": self.get_device_info() # 添加设备信息
+ }
+ else:
+ error_msg = result.get("msg", "未知错误")
+ logging.error(f"获取状态失败: {error_msg}")
+ return {
+ "status": "inactive",
+ "expire_time": "",
+ "total_days": 0,
+ "days_left": 0,
+ "device_info": self.get_device_info()
+ }
+
+ except requests.exceptions.RequestException as e:
+ logging.error(f"API请求失败: {str(e)}")
+ return {
+ "status": "inactive",
+ "expire_time": "",
+ "total_days": 0,
+ "days_left": 0,
+ "device_info": self.get_device_info()
}
-
- try:
- response = requests.post(endpoint, json=data, headers=headers, timeout=10)
- response_data = response.json()
-
- if response_data.get("code") == 200:
- # 正常状态
- data = response_data.get("data", {})
- status = data.get("status", "inactive")
-
- # 构造会员信息
- member_info = {
- "hardware_id": data.get("machine_id", self.hardware_id),
- "expire_time": data.get("expire_time", ""),
- "days_left": data.get("days_left", 0), # 使用days_left
- "total_days": data.get("total_days", 0), # 使用total_days
- "status": status,
- "activation_records": data.get("activation_records", []) # 保存激活记录
- }
-
- # 打印调试信息
- logging.info(f"API返回数据: {data}")
- logging.info(f"处理后的会员信息: {member_info}")
-
- self.config.save_member_info(member_info)
- return member_info
-
- elif response_data.get("code") == 401:
- # 未激活或已过期
- logging.warning("会员未激活或已过期")
- return self._get_inactive_status()
-
- elif response_data.get("code") == 400:
- # 参数错误
- error_msg = response_data.get("msg", "参数错误")
- logging.error(f"获取会员状态失败: {error_msg}")
- return self._get_inactive_status()
-
- elif response_data.get("code") == 500:
- # 系统错误
- error_msg = response_data.get("msg", "系统错误")
- logging.error(f"获取会员状态失败: {error_msg}")
- return self._get_inactive_status()
-
- else:
- # 其他未知错误
- error_msg = response_data.get("msg", "未知错误")
- logging.error(f"获取会员状态失败: {error_msg}")
- return self._get_inactive_status()
-
- except requests.RequestException as e:
- logging.error(f"请求会员状态失败: {str(e)}")
- return self._get_inactive_status()
-
except Exception as e:
- logging.error(f"获取会员状态出错: {str(e)}")
- return self._get_inactive_status()
-
- def _get_inactive_status(self) -> Dict:
- """获取未激活状态的默认信息"""
- return {
- "hardware_id": self.hardware_id,
- "expire_time": "",
- "days": 0,
- "total_days": 0,
- "status": "inactive",
- "last_activation": {},
- "activation_records": []
- }
+ logging.error(f"获取会员状态失败: {str(e)}")
+ return {
+ "status": "inactive",
+ "expire_time": "",
+ "total_days": 0,
+ "days_left": 0,
+ "device_info": self.get_device_info()
+ }
def restart_cursor(self) -> bool:
"""重启Cursor编辑器
diff --git a/build_nezha.spec b/build_nezha.spec
index d7b6f66..02d02d6 100644
--- a/build_nezha.spec
+++ b/build_nezha.spec
@@ -1,5 +1,7 @@
# -*- mode: python ; coding: utf-8 -*-
import os
+import sys
+from PyInstaller.utils.hooks import collect_all
def get_version():
with open('version.txt', 'r', encoding='utf-8') as f:
@@ -8,26 +10,43 @@ def get_version():
version = get_version()
+# 收集所有需要的依赖
+datas = [('icon', 'icon'), ('version.txt', '.')]
+binaries = []
+hiddenimports = [
+ 'win32gui', 'win32con', 'win32process', 'psutil', # Windows API 相关
+ 'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', # GUI 相关
+ 'PyQt5.sip', # PyQt5 必需
+ 'PyQt5.QtNetwork', # 网络相关
+ 'PIL', # Pillow 相关
+ 'PIL._imaging', # Pillow 核心
+ 'requests', 'urllib3', 'certifi', # 网络请求相关
+ 'json', 'uuid', 'hashlib', 'logging', # 基础功能相关
+ 'importlib', # 导入相关
+ 'pkg_resources', # 包资源
+]
+
+# 主要的分析对象
a = Analysis(
['main.py'],
pathex=[],
- binaries=[],
- datas=[('icon', 'icon'), ('version.txt', '.')],
- hiddenimports=[
- 'win32gui', 'win32con', 'win32process', 'psutil', # Windows API 相关
- 'PyQt5', 'PyQt5.QtCore', 'PyQt5.QtGui', 'PyQt5.QtWidgets', # GUI 相关
- 'requests', 'urllib3', 'certifi', # 网络请求相关
- 'json', 'uuid', 'hashlib', 'logging' # 基础功能相关
- ],
+ binaries=binaries,
+ datas=datas,
+ hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
- excludes=['_tkinter', 'tkinter', 'Tkinter'], # 排除 tkinter 相关模块
+ excludes=['_tkinter', 'tkinter', 'Tkinter'],
+ win_no_prefer_redirects=False,
+ win_private_assemblies=False,
noarchive=False,
- optimize=0,
+ module_collection_mode={'PyQt5': 'pyz+py'},
)
+
+# 创建PYZ
pyz = PYZ(a.pure)
+# 创建EXE
exe = EXE(
pyz,
a.scripts,
diff --git a/gui/main_window.py b/gui/main_window.py
index 52cefb6..737e7e6 100644
--- a/gui/main_window.py
+++ b/gui/main_window.py
@@ -9,6 +9,7 @@ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QDialog, QProgressBar, QStyle)
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt5.QtGui import QIcon, QPixmap
+import time
sys.path.append(str(Path(__file__).parent.parent))
@@ -53,8 +54,9 @@ class LoadingDialog(QDialog):
background-color: #f8f9fa;
}
QLabel {
- color: #333333;
+ color: #0d6efd;
font-size: 14px;
+ font-weight: bold;
padding: 10px;
}
QProgressBar {
@@ -96,6 +98,11 @@ class MainWindow(QMainWindow):
self._activation_status = None # 缓存的激活状态
self._status_timer = None # 状态更新定时器
+ # 添加请求锁,防止重复提交
+ self._is_requesting = False
+ self._last_request_time = 0
+ self._request_cooldown = 2 # 请求冷却时间(秒)
+
version = get_version()
cursor_version = self.switcher.get_cursor_version()
self.setWindowTitle(f"听泉Cursor助手 v{version} (本机Cursor版本: {cursor_version})")
@@ -108,6 +115,8 @@ class MainWindow(QMainWindow):
if not window_icon.isNull():
self.setWindowIcon(window_icon)
logging.info(f"成功设置窗口图标: {icon_path}")
+ else:
+ logging.warning("图标文件加载失败")
# 创建系统托盘图标
self.tray_icon = QSystemTrayIcon(self)
@@ -163,21 +172,6 @@ class MainWindow(QMainWindow):
activation_frame = QFrame()
activation_layout = QVBoxLayout(activation_frame)
- # 使用说明
- usage_label = QLabel()
- usage_label.setText("使用说明:\n\n1. 输入激活码并点击激活\n2. 激活成功后点击\"刷新Cursor编辑器授权\"即可正常使用\n3. 如果刷新无效,请点击\"突破Cursor0.45.x限制\"\n4. 建议点击\"禁用Cursor版本更新\"保持长期稳定")
- usage_label.setStyleSheet("""
- QLabel {
- color: #6c757d;
- font-size: 13px;
- padding: 10px;
- background-color: #f8f9fa;
- border-radius: 4px;
- border: 1px solid #dee2e6;
- }
- """)
- activation_layout.addWidget(usage_label)
-
# 激活码输入区域
activation_layout.addWidget(QLabel("激活(叠加)会员,多个激活码可叠加整体时长"))
input_frame = QFrame()
@@ -186,11 +180,54 @@ class MainWindow(QMainWindow):
self.activation_edit = QLineEdit()
input_layout.addWidget(self.activation_edit)
activate_btn = QPushButton("激活")
+ activate_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #0d6efd;
+ color: white;
+ border: none;
+ padding: 8px 25px;
+ border-radius: 4px;
+ font-size: 13px;
+ min-width: 80px;
+ }
+ QPushButton:hover {
+ background-color: #0b5ed7;
+ }
+ QPushButton:pressed {
+ background-color: #0a58ca;
+ }
+ """)
activate_btn.clicked.connect(self.activate_account)
input_layout.addWidget(activate_btn)
activation_layout.addWidget(input_frame)
main_layout.addWidget(activation_frame)
+ # 使用说明
+ usage_label = QLabel()
+ usage_text = (
+ "
使用步骤:
"
+ ""
+ "1. 第一步:输入激活码并点击【激活】按钮
"
+ "2. 第二步:激活成功后点击【刷新Cursor编辑器授权】即可正常使用
"
+ "3. 如果刷新无效:请先点击【突破Cursor0.45.x限制】,然后再点击刷新
"
+ "4. 建议操作:点击【禁用Cursor版本更新】保持长期稳定"
+ "
"
+ )
+ usage_label.setText(usage_text)
+ usage_label.setStyleSheet("""
+ QLabel {
+ color: #333333;
+ font-size: 13px;
+ padding: 15px;
+ background-color: #f8f9fa;
+ border-radius: 4px;
+ border: 1px solid #dee2e6;
+ }
+ """)
+ usage_label.setTextFormat(Qt.RichText)
+ usage_label.setWordWrap(True)
+ activation_layout.addWidget(usage_label)
+
# 操作按钮区域
btn_frame = QFrame()
btn_layout = QVBoxLayout(btn_frame)
@@ -203,8 +240,8 @@ class MainWindow(QMainWindow):
border: none;
padding: 15px;
border-radius: 6px;
- font-size: 14px;
- min-width: 200px;
+ font-size: 13px;
+ min-width: 300px;
margin: 5px;
}
QPushButton:hover {
@@ -216,21 +253,21 @@ class MainWindow(QMainWindow):
"""
# 刷新授权按钮
- refresh_btn = QPushButton("刷新Cursor编辑器授权")
+ refresh_btn = QPushButton("刷新 Cursor 编辑器授权")
refresh_btn.setStyleSheet(button_style)
refresh_btn.clicked.connect(self.refresh_cursor_auth)
refresh_btn.setMinimumHeight(50)
btn_layout.addWidget(refresh_btn)
# 突破限制按钮
- bypass_btn = QPushButton("突破Cursor0.45.x限制")
+ bypass_btn = QPushButton("突破 Cursor 0.45.x 限制")
bypass_btn.setStyleSheet(button_style.replace("#0d6efd", "#198754").replace("#0b5ed7", "#157347").replace("#0a58ca", "#146c43"))
- bypass_btn.clicked.connect(self.dummy_function)
+ bypass_btn.clicked.connect(self.bypass_cursor_limit)
bypass_btn.setMinimumHeight(50)
btn_layout.addWidget(bypass_btn)
# 禁用更新按钮
- disable_update_btn = QPushButton("禁用Cursor版本更新")
+ disable_update_btn = QPushButton("禁用 Cursor 版本更新")
disable_update_btn.setStyleSheet(button_style.replace("#0d6efd", "#dc3545").replace("#0b5ed7", "#bb2d3b").replace("#0a58ca", "#b02a37"))
disable_update_btn.clicked.connect(self.disable_cursor_update)
disable_update_btn.setMinimumHeight(50)
@@ -271,14 +308,33 @@ class MainWindow(QMainWindow):
def copy_device_id(self):
"""复制设备ID到剪贴板"""
- if not self.check_status():
- return
QApplication.clipboard().setText(self.hardware_id_edit.text())
QMessageBox.information(self, "提示", "设备ID已复制到剪贴板")
def show_loading_dialog(self, message="请稍候..."):
"""显示加载对话框"""
self.loading_dialog = LoadingDialog(self, message)
+ self.loading_dialog.setStyleSheet("""
+ QDialog {
+ background-color: #f8f9fa;
+ }
+ QLabel {
+ color: #0d6efd;
+ font-size: 14px;
+ font-weight: bold;
+ padding: 10px;
+ }
+ QProgressBar {
+ border: 2px solid #e9ecef;
+ border-radius: 5px;
+ text-align: center;
+ }
+ QProgressBar::chunk {
+ background-color: #0d6efd;
+ width: 10px;
+ margin: 0.5px;
+ }
+ """)
self.loading_dialog.show()
def hide_loading_dialog(self):
@@ -286,7 +342,7 @@ class MainWindow(QMainWindow):
if hasattr(self, 'loading_dialog'):
self.loading_dialog.hide()
self.loading_dialog.deleteLater()
-
+
def activate_account(self):
"""激活账号"""
code = self.activation_edit.text().strip()
@@ -460,19 +516,17 @@ class MainWindow(QMainWindow):
# 更新会员信息显示
self.update_status_display(account_info)
# 更新激活状态缓存
- self._activation_status = True
+ self._activation_status = account_info.get('status') == 'active'
# 更新状态定时器
- days_left = account_info.get('days_left', 0)
- seconds_left = days_left * 24 * 60 * 60
- if self._status_timer:
- self._status_timer.stop()
- self._status_timer = QTimer(self)
- self._status_timer.setSingleShot(True)
- self._status_timer.timeout.connect(self.check_status)
- update_interval = min(seconds_left - 60, 24 * 60 * 60) # 最长1天更新一次
- if update_interval > 0:
- self._status_timer.start(update_interval * 1000) # 转换为毫秒
+ if self._activation_status:
+ if self._status_timer:
+ self._status_timer.stop()
+ self._status_timer = QTimer(self)
+ self._status_timer.setSingleShot(False)
+ self._status_timer.timeout.connect(self.check_status)
+ self._status_timer.start(60 * 1000) # 每60秒检测一次
+ logging.info("已设置每分钟检测会员状态")
msg = QMessageBox(self)
msg.setWindowTitle("激活成功")
@@ -549,21 +603,26 @@ class MainWindow(QMainWindow):
}
""")
msg.exec_()
+ # 更新显示为未激活状态
+ self.update_status_display(account_info)
else:
QMessageBox.critical(self, "错误", f"激活失败: {data}")
+ # 更新为未激活状态
+ device_info = self.switcher.get_device_info()
+ inactive_status = {
+ "status": "inactive",
+ "expire_time": "",
+ "total_days": 0,
+ "days_left": 0,
+ "device_info": device_info
+ }
+ self.update_status_display(inactive_status)
- # 激活后检查一次状态
- self.check_status()
-
def update_status_display(self, status_info: dict):
"""更新状态显示"""
# 打印API返回的原始数据
logging.info("=== API返回数据 ===")
logging.info(f"状态信息: {status_info}")
- if 'activation_records' in status_info:
- logging.info("激活记录:")
- for record in status_info['activation_records']:
- logging.info(f"- 记录: {record}")
# 更新状态文本
status_map = {
@@ -581,18 +640,10 @@ class MainWindow(QMainWindow):
f"剩余天数:{status_info.get('days_left', 0)}天"
]
- # 如果有激活记录,显示最近一次激活信息
- activation_records = status_info.get('activation_records', [])
- if activation_records:
- latest_record = activation_records[-1] # 获取最新的激活记录
- device_info = latest_record.get('device_info', {})
-
+ # 添加设备信息
+ device_info = status_info.get('device_info', {})
+ if device_info:
status_lines.extend([
- "",
- "最近激活信息:",
- f"激活码:{latest_record.get('code', '')}",
- f"激活时间:{latest_record.get('activation_time', '')}",
- f"增加天数:{latest_record.get('days', 0)}天",
"",
"设备信息:",
f"系统:{device_info.get('os', '')}",
@@ -603,12 +654,13 @@ class MainWindow(QMainWindow):
# 更新状态文本
self.status_text.setPlainText("\n".join(status_lines))
-
+
def check_status(self):
"""检查会员状态(从API获取)"""
try:
- # 显示加载对话框
- self.show_loading_dialog("正在检查会员状态,请稍候...")
+ # 只在首次检查时显示加载对话框
+ if self._activation_status is None:
+ self.show_loading_dialog("正在检查会员状态,请稍候...")
# 创建工作线程
self.worker = ApiWorker(self.switcher.get_member_status)
@@ -616,175 +668,199 @@ class MainWindow(QMainWindow):
self.worker.start()
except Exception as e:
- self.hide_loading_dialog()
+ if self._activation_status is None:
+ self.hide_loading_dialog()
QMessageBox.critical(self, "错误", f"检查状态失败: {str(e)}")
self._activation_status = False
+ logging.error(f"检查状态时发生错误: {str(e)}")
return False
-
+
def on_status_check_complete(self, result):
"""状态检查完成回调"""
success, data = result
- self.hide_loading_dialog()
+
+ # 只在首次检查时隐藏加载对话框
+ if self._activation_status is None:
+ self.hide_loading_dialog()
if success and data:
- self.update_status_display(data)
- # 更新缓存的激活状态
+ # 更新激活状态和定时器
self._activation_status = data.get('status') == 'active'
-
- # 设置状态更新定时器
if self._activation_status:
- # 获取剩余时间(秒)
- days_left = data.get('days_left', 0)
- seconds_left = days_left * 24 * 60 * 60
-
- # 创建定时器,在到期前1分钟更新状态
+ # 设置定时器
if self._status_timer:
self._status_timer.stop()
self._status_timer = QTimer(self)
- self._status_timer.setSingleShot(True)
+ self._status_timer.setSingleShot(False)
self._status_timer.timeout.connect(self.check_status)
- update_interval = min(seconds_left - 60, 24 * 60 * 60) # 最长1天更新一次
- if update_interval > 0:
- self._status_timer.start(update_interval * 1000) # 转换为毫秒
+ self._status_timer.start(60 * 1000) # 每60秒检测一次
+ logging.info("已设置每分钟检测会员状态")
+ else:
+ if self._status_timer:
+ self._status_timer.stop()
- return self._activation_status
+ # 更新显示
+ self.update_status_display(data)
else:
+ # 停止定时器
+ if self._status_timer:
+ self._status_timer.stop()
+ self._activation_status = False
+
# 更新为未激活状态
+ device_info = self.switcher.get_device_info()
inactive_status = {
- "hardware_id": self.switcher.hardware_id,
- "expire_time": "",
- "days_left": 0,
- "total_days": 0,
"status": "inactive",
- "activation_records": []
+ "expire_time": "",
+ "total_days": 0,
+ "days_left": 0,
+ "device_info": device_info
}
self.update_status_display(inactive_status)
- self._activation_status = False
- return False
+ logging.warning("会员状态检查失败或未激活")
+ # 清除会员信息文件
+ try:
+ member_file = self.switcher.config.member_file
+ if member_file.exists():
+ member_file.unlink()
+ logging.info("已清除会员信息文件")
+ except Exception as e:
+ logging.error(f"清除会员信息文件时出错: {str(e)}")
+
+ return self._activation_status
+
def check_activation_status(self) -> bool:
"""检查是否已激活(使用缓存)
Returns:
bool: 是否已激活
"""
+ # 如果缓存的状态是None,从API获取
if self._activation_status is None:
- # 首次检查,从API获取
return self.check_status()
+ # 如果未激活,显示购买信息
if not self._activation_status:
- # 创建自定义消息框
- msg = QDialog(self)
- msg.setWindowTitle("会员未激活")
- msg.setFixedWidth(400)
- msg.setWindowFlags(msg.windowFlags() & ~Qt.WindowContextHelpButtonHint)
-
- # 创建布局
- layout = QVBoxLayout()
-
- # 添加图标和文本
- icon_label = QLabel()
- icon_label.setPixmap(self.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(32, 32))
- icon_label.setAlignment(Qt.AlignCenter)
- layout.addWidget(icon_label)
-
- text_label = QLabel("您还未激活会员或会员已过期")
- text_label.setAlignment(Qt.AlignCenter)
- text_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #333333; padding: 10px;")
- layout.addWidget(text_label)
-
- # 添加购买信息
- info_text = "获取会员激活码,请通过以下方式:\n\n" \
- "• 官方自助网站:cursor.nosqli.com\n" \
- "• 微信客服:behikcigar\n" \
- "• 闲鱼店铺:xxx\n\n" \
- "————————————————————\n" \
- "诚招代理商,欢迎加盟合作!"
-
- info_label = QLabel(info_text)
- info_label.setAlignment(Qt.AlignLeft)
- info_label.setStyleSheet("""
- QLabel {
- color: #333333;
- font-size: 14px;
- padding: 15px;
- background-color: #f8f9fa;
- border-radius: 4px;
- border: 1px solid #dee2e6;
- margin: 10px;
- }
- """)
- layout.addWidget(info_label)
-
- # 添加复制按钮区域
- btn_layout = QHBoxLayout()
-
- # 复制网站按钮
- copy_web_btn = QPushButton("复制网站")
- copy_web_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "cursor.nosqli.com", "网站地址已复制到剪贴板"))
- copy_web_btn.setStyleSheet("""
- QPushButton {
- background-color: #0d6efd;
- color: white;
- border: none;
- padding: 8px 15px;
- border-radius: 4px;
- font-size: 13px;
- }
- QPushButton:hover {
- background-color: #0b5ed7;
- }
- """)
- btn_layout.addWidget(copy_web_btn)
-
- # 复制微信按钮
- copy_wx_btn = QPushButton("复制微信")
- copy_wx_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "behikcigar", "微信号已复制到剪贴板"))
- copy_wx_btn.setStyleSheet("""
- QPushButton {
- background-color: #198754;
- color: white;
- border: none;
- padding: 8px 15px;
- border-radius: 4px;
- font-size: 13px;
- }
- QPushButton:hover {
- background-color: #157347;
- }
- """)
- btn_layout.addWidget(copy_wx_btn)
-
- # 确定按钮
- ok_btn = QPushButton("确定")
- ok_btn.clicked.connect(msg.accept)
- ok_btn.setStyleSheet("""
- QPushButton {
- background-color: #0d6efd;
- color: white;
- border: none;
- padding: 8px 25px;
- border-radius: 4px;
- font-size: 13px;
- min-width: 100px;
- }
- QPushButton:hover {
- background-color: #0b5ed7;
- }
- """)
- btn_layout.addWidget(ok_btn)
-
- layout.addLayout(btn_layout)
- msg.setLayout(layout)
- msg.exec_()
+ self.show_purchase_info()
return self._activation_status
-
+
+ def show_purchase_info(self):
+ """显示购买信息对话框"""
+ # 创建自定义消息框
+ msg = QDialog(self)
+ msg.setWindowTitle("会员未激活")
+ msg.setFixedWidth(400)
+ msg.setWindowFlags(msg.windowFlags() & ~Qt.WindowContextHelpButtonHint)
+
+ # 创建布局
+ layout = QVBoxLayout()
+
+ # 添加图标和文本
+ icon_label = QLabel()
+ icon_label.setPixmap(self.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(32, 32))
+ icon_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(icon_label)
+
+ text_label = QLabel("您还未激活会员或会员已过期")
+ text_label.setAlignment(Qt.AlignCenter)
+ text_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #333333; padding: 10px;")
+ layout.addWidget(text_label)
+
+ # 添加购买信息
+ info_text = "获取会员激活码,请通过以下方式:\n\n" \
+ "• 官方自助网站:cursor.nosqli.com\n" \
+ "• 微信客服:behikcigar\n" \
+ "• 闲鱼店铺:xxx\n\n" \
+ "————————————————————\n" \
+ "诚招代理商,欢迎加盟合作!"
+
+ info_label = QLabel(info_text)
+ info_label.setAlignment(Qt.AlignLeft)
+ info_label.setStyleSheet("""
+ QLabel {
+ color: #333333;
+ font-size: 14px;
+ padding: 15px;
+ background-color: #f8f9fa;
+ border-radius: 4px;
+ border: 1px solid #dee2e6;
+ margin: 10px;
+ }
+ """)
+ layout.addWidget(info_label)
+
+ # 添加复制按钮区域
+ btn_layout = QHBoxLayout()
+
+ # 复制网站按钮
+ copy_web_btn = QPushButton("复制网站")
+ copy_web_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "cursor.nosqli.com", "网站地址已复制到剪贴板"))
+ copy_web_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #0d6efd;
+ color: white;
+ border: none;
+ padding: 8px 15px;
+ border-radius: 4px;
+ font-size: 13px;
+ }
+ QPushButton:hover {
+ background-color: #0b5ed7;
+ }
+ """)
+ btn_layout.addWidget(copy_web_btn)
+
+ # 复制微信按钮
+ copy_wx_btn = QPushButton("复制微信")
+ copy_wx_btn.clicked.connect(lambda: self.copy_and_show_tip(msg, "behikcigar", "微信号已复制到剪贴板"))
+ copy_wx_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #198754;
+ color: white;
+ border: none;
+ padding: 8px 15px;
+ border-radius: 4px;
+ font-size: 13px;
+ }
+ QPushButton:hover {
+ background-color: #157347;
+ }
+ """)
+ btn_layout.addWidget(copy_wx_btn)
+
+ # 确定按钮
+ ok_btn = QPushButton("确定")
+ ok_btn.clicked.connect(msg.accept)
+ ok_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #0d6efd;
+ color: white;
+ border: none;
+ padding: 8px 25px;
+ border-radius: 4px;
+ font-size: 13px;
+ min-width: 100px;
+ }
+ QPushButton:hover {
+ background-color: #0b5ed7;
+ }
+ """)
+ btn_layout.addWidget(ok_btn)
+
+ layout.addLayout(btn_layout)
+ msg.setLayout(layout)
+ msg.exec_()
+
def refresh_cursor_auth(self):
"""刷新Cursor授权"""
if not self.check_activation_status():
return
+ if not self._check_request_throttle():
+ return
+
try:
# 显示加载对话框
self.show_loading_dialog("正在刷新授权,请稍候...")
@@ -795,6 +871,7 @@ class MainWindow(QMainWindow):
self.worker.start()
except Exception as e:
+ self._request_complete()
self.hide_loading_dialog()
self.show_custom_error("刷新授权失败", str(e))
@@ -802,6 +879,7 @@ class MainWindow(QMainWindow):
"""刷新授权完成回调"""
success, data = result
self.hide_loading_dialog()
+ self._request_complete()
if isinstance(data, tuple):
success, message = data
@@ -811,34 +889,185 @@ class MainWindow(QMainWindow):
self.show_custom_error("刷新授权失败", message)
else:
self.show_custom_error("刷新授权失败", str(data))
-
+
def disable_cursor_update(self):
"""禁用Cursor更新"""
if not self.check_activation_status():
return
+ if not self._check_request_throttle():
+ return
+
try:
- success, message = self.switcher.disable_cursor_update()
- if success:
- self.show_custom_message("成功", "禁用更新成功", message, QStyle.SP_DialogApplyButton, "#198754")
- else:
- self.show_custom_error("禁用更新失败", message)
+ # 显示加载对话框
+ self.show_loading_dialog("正在禁用更新,请稍候...")
+
+ # 创建工作线程
+ from utils.cursor_registry import CursorRegistry
+ registry = CursorRegistry()
+
+ def disable_func():
+ try:
+ # 1. 先关闭所有Cursor进程
+ if sys.platform == "win32":
+ os.system("taskkill /f /im Cursor.exe >nul 2>&1")
+ time.sleep(2)
+
+ # 2. 处理updater文件
+ updater_path = Path(os.getenv('LOCALAPPDATA')) / "cursor-updater"
+ try:
+ # 如果是目录,则删除
+ if updater_path.is_dir():
+ import shutil
+ shutil.rmtree(str(updater_path))
+ logging.info("删除updater目录成功")
+ # 如果是文件,则删除
+ if updater_path.is_file():
+ updater_path.unlink()
+ logging.info("删除updater文件成功")
+
+ # 创建阻止文件
+ updater_path.touch()
+ logging.info("创建updater空文件成功")
+
+ # 设置文件权限
+ import subprocess
+ import stat
+
+ # 设置只读属性
+ os.chmod(str(updater_path), stat.S_IREAD)
+ logging.info("设置只读属性成功")
+
+ # 使用icacls设置权限(只读)
+ username = os.getenv('USERNAME')
+ cmd = f'icacls "{str(updater_path)}" /inheritance:r /grant:r "{username}:(R)"'
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
+ if result.returncode != 0:
+ logging.error(f"设置文件权限失败: {result.stderr}")
+ return False, "设置文件权限失败"
+ logging.info("设置文件权限成功")
+
+ # 验证设置
+ if not os.path.exists(updater_path):
+ return False, "文件创建失败"
+ if os.access(str(updater_path), os.W_OK):
+ return False, "文件权限设置失败"
+
+ except Exception as e:
+ logging.error(f"处理updater文件失败: {str(e)}")
+ return False, "处理updater文件失败"
+
+ # 3. 修改package.json配置
+ if not registry.fix_cursor_startup():
+ return False, "修改配置失败"
+
+ # 4. 重启Cursor
+ cursor_exe = registry.cursor_path / "Cursor.exe"
+ if cursor_exe.exists():
+ os.startfile(str(cursor_exe))
+ logging.info("Cursor重启成功")
+ return True, "Cursor更新已禁用,程序已重启"
+ else:
+ return False, "未找到Cursor程序"
+ except Exception as e:
+ logging.error(f"禁用更新时发生错误: {str(e)}")
+ return False, str(e)
+
+ self.worker = ApiWorker(disable_func)
+ self.worker.finished.connect(lambda result: self.on_disable_update_complete(result))
+ self.worker.start()
+
except Exception as e:
+ self._request_complete()
+ self.hide_loading_dialog()
self.show_custom_error("禁用更新失败", str(e))
- def dummy_function(self):
- """突破版本限制"""
+ def on_disable_update_complete(self, result):
+ """禁用更新完成回调"""
+ success, data = result
+ self.hide_loading_dialog()
+ self._request_complete()
+
+ if success:
+ self.show_custom_message(
+ "成功",
+ "禁用更新成功",
+ data,
+ QStyle.SP_DialogApplyButton,
+ "#198754"
+ )
+ else:
+ self.show_custom_error("禁用更新失败", str(data))
+
+ def bypass_cursor_limit(self):
+ """突破Cursor版本限制"""
if not self.check_activation_status():
return
- self.show_custom_message(
- "提示",
- "功能未实现",
- "此功能暂未实现,请等待后续更新。",
- QStyle.SP_MessageBoxInformation,
- "#0d6efd"
- )
+ if not self._check_request_throttle():
+ return
+
+ try:
+ # 显示加载对话框
+ self.show_loading_dialog("正在突破版本限制,请稍候...")
+
+ # 创建工作线程
+ from utils.cursor_registry import CursorRegistry
+ registry = CursorRegistry()
+
+ def reset_func():
+ try:
+ # 1. 先关闭所有Cursor进程
+ if sys.platform == "win32":
+ os.system("taskkill /f /im Cursor.exe >nul 2>&1")
+ time.sleep(2)
+
+ # 2. 清理注册表
+ if not registry.clean_registry():
+ return False, "清理注册表失败"
+
+ # 3. 清理文件
+ if not registry.clean_cursor_files():
+ return False, "清理文件失败"
+
+ # 4. 重启Cursor
+ cursor_exe = self.cursor_path / "Cursor.exe"
+ if cursor_exe.exists():
+ os.startfile(str(cursor_exe))
+ logging.info("Cursor重启成功")
+ return True, "突破限制成功"
+ else:
+ return False, "未找到Cursor程序"
+ except Exception as e:
+ logging.error(f"突破限制时发生错误: {str(e)}")
+ return False, str(e)
+
+ self.worker = ApiWorker(reset_func)
+ self.worker.finished.connect(self.on_bypass_complete)
+ self.worker.start()
+
+ except Exception as e:
+ self._request_complete()
+ self.hide_loading_dialog()
+ self.show_custom_error("突破限制失败", str(e))
+
+ def on_bypass_complete(self, result):
+ """突破限制完成回调"""
+ success, data = result
+ self.hide_loading_dialog()
+ self._request_complete()
+ if success:
+ self.show_custom_message(
+ "成功",
+ "突破限制成功",
+ "Cursor版本限制已突破,编辑器已重启。",
+ QStyle.SP_DialogApplyButton,
+ "#198754"
+ )
+ else:
+ self.show_custom_error("突破限制失败", str(data))
+
def show_custom_message(self, title, header, message, icon_type, color):
"""显示自定义消息框"""
msg = QDialog(self)
@@ -914,4 +1143,29 @@ class MainWindow(QMainWindow):
message,
QStyle.SP_MessageBoxCritical,
"#dc3545"
- )
\ No newline at end of file
+ )
+
+ def _check_request_throttle(self) -> bool:
+ """检查是否可以发送请求(防重复提交)
+
+ Returns:
+ bool: 是否可以发送请求
+ """
+ current_time = time.time()
+
+ # 如果正在请求中,返回False
+ if self._is_requesting:
+ return False
+
+ # 如果距离上次请求时间小于冷却时间,返回False
+ if current_time - self._last_request_time < self._request_cooldown:
+ return False
+
+ # 更新状态和时间
+ self._is_requesting = True
+ self._last_request_time = current_time
+ return True
+
+ def _request_complete(self):
+ """请求完成,重置状态"""
+ self._is_requesting = False
\ No newline at end of file
diff --git a/main.py b/main.py
index 6ce8a4c..57d20a8 100644
--- a/main.py
+++ b/main.py
@@ -52,6 +52,9 @@ def main():
# 注册退出时的清理函数
atexit.register(cleanup_temp)
+ # 创建QApplication实例
+ app = QApplication(sys.argv)
+
setup_logging()
# 检查Python版本
@@ -66,7 +69,6 @@ def main():
logging.info(f" - {p}")
logging.info("正在初始化主窗口...")
- app = QApplication(sys.argv)
# 设置应用程序ID (在设置图标之前)
if sys.platform == "win32":
@@ -102,7 +104,8 @@ def main():
error_msg = f"程序运行出错: {str(e)}\n{traceback.format_exc()}"
logging.error(error_msg)
# 使用 QMessageBox 显示错误
- app = QApplication(sys.argv)
+ if QApplication.instance() is None:
+ app = QApplication(sys.argv)
QMessageBox.critical(None, "错误", error_msg)
sys.exit(1)
diff --git a/requirements.txt b/requirements.txt
index ab393db..742fcca 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,8 +2,9 @@
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
requests==2.31.0
+setuptools>=68.0.0
+altgraph>=0.17.4
pyinstaller==6.3.0
-pillow==10.2.0 # For icon processing
-setuptools==65.5.1 # Fix pkg_resources.extern issue
-PyQt5==5.15.10 # GUI framework
-pywin32==306 # Windows API support
\ No newline at end of file
+pillow==10.2.0
+PyQt5==5.15.10
+pywin32==306
\ No newline at end of file
diff --git a/utils/config.py b/utils/config.py
index f896cde..5e90f94 100644
--- a/utils/config.py
+++ b/utils/config.py
@@ -4,8 +4,15 @@ import logging
from pathlib import Path
class Config:
+ """配置类"""
+
def __init__(self):
- self.api_base_url = "https://cursorapi.nosqli.com/admin"
+ self.base_url = "https://cursorapi.nosqli.com"
+ self.api_endpoints = {
+ "activate": f"{self.base_url}/admin/api.member/activate",
+ "status": f"{self.base_url}/admin/api.member/status",
+ "get_unused": f"{self.base_url}/admin/api.account/getUnused"
+ }
self.config_dir = Path(os.path.expanduser("~")) / ".cursor_switcher"
self.config_file = self.config_dir / "config.json"
self.member_file = self.config_dir / "member.json"
@@ -64,4 +71,15 @@ class Config:
}
with open(self.config_file, "w", encoding="utf-8") as f:
json.dump(config, f, indent=2, ensure_ascii=False)
- self.api_token = api_token
\ No newline at end of file
+ self.api_token = api_token
+
+ def get_api_url(self, endpoint_name: str) -> str:
+ """获取API端点URL
+
+ Args:
+ endpoint_name: 端点名称
+
+ Returns:
+ str: 完整的API URL
+ """
+ return self.api_endpoints.get(endpoint_name, "")
\ No newline at end of file
diff --git a/version.txt b/version.txt
index 2c6109e..aa00c92 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-3.3.4
\ No newline at end of file
+3.3.9
\ No newline at end of file