import ast
import logging
import os
import shutil
import subprocess
import sys
import time
import zipfile
from logging.handlers import RotatingFileHandler
import requests
from PyQt6.QtCore import *
from PyQt6.QtWidgets import *
from PyQt6.QtGui import *
import socket
import threading
import json
from mcstatus import JavaServer, BedrockServer
from requests import RequestException
# 设置全局日志
logger = logging.getLogger('dynamic_tunneling')
logger.setLevel(logging.DEBUG)
file_handler = RotatingFileHandler('dynamic_tunneling.log', maxBytes=5 * 1024 * 1024, backupCount=5)
file_handler.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
def get_pos(event):
"""获取事件的全局位置"""
if hasattr(event, 'globalPos'):
return event.globalPos()
else:
return event.globalPosition().toPoint()
def get_config_folder():
base_path = os.path.dirname(os.path.abspath(__file__))
config_folder = os.path.join(base_path, "config")
if not os.path.exists(config_folder):
os.makedirs(config_folder)
return config_folder
def get_nodes(max_retries=3, retry_delay=1):
"""获取节点数据"""
logger.info("开始获取节点数据")
url = "http://cf-v2.uapis.cn/node"
headers = {'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'}
for attempt in range(max_retries):
try:
response = requests.post(url, headers=headers)
response.raise_for_status() # 如果响应状态不是200,将引发HTTPError异常
data = response.json()
if data['code'] == 200:
return data['data']
else:
logger.error(f"获取节点数据失败: {data['msg']}")
return []
except RequestException as e:
logger.warning(f"获取节点数据时发生网络错误 (尝试 {attempt + 1}/{max_retries}): {str(e)}")
if attempt < max_retries - 1:
time.sleep(retry_delay)
else:
logger.error("获取节点数据失败,已达到最大重试次数")
return []
except Exception as e:
logger.exception("获取节点数据时发生未知错误")
return []
def login(username, password):
"""用户登录返回token"""
logger.info(f"尝试登录用户: {username}")
url = f"http://cf-v2.uapis.cn/login?username={username}&password={password}"
headers = {'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'}
try:
response = requests.get(url, headers=headers)
response_data = response.json()
token = response_data.get("data", {}).get("usertoken")
if token:
logger.info("登录成功")
else:
logger.warning("登录失败")
return token
except Exception as e:
logger.exception("登录时发生错误")
logger.exception(e)
return None
def get_user_tunnels(token):
"""获取用户隧道列表"""
url = f"http://cf-v2.uapis.cn/tunnel?token={token}"
headers = {'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'}
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # 这会在HTTP错误时抛出异常
data = response.json()
if data['code'] == 200:
tunnels = data.get("data", [])
return tunnels
else:
logger.error(f"获取隧道列表失败: {data.get('msg', '未知错误')}")
return None
except requests.RequestException as e:
logger.exception("获取用户隧道列表时发生网络错误")
return None
except Exception as e:
logger.exception("获取用户隧道列表时发生未知错误")
return None
def get_node_ip(token, node):
"""获取节点IP"""
logger.info(f"获取节点 {node} 的IP")
url = f"http://cf-v2.uapis.cn/nodeinfo?token={token}&node={node}"
headers = {'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'}
try:
response = requests.get(url, headers=headers)
ip = response.json()["data"]["realIp"]
logger.info(f"节点 {node} 的IP为 {ip}")
return ip
except Exception as e:
logger.exception(f"获取节点 {node} 的IP时发生错误")
logger.exception(e)
return None
def update_subdomain(token, domain, record, target, record_type):
"""更新子域名"""
logger.info(f"更新子域名 {record}.{domain} 到 {target}")
url = "http://cf-v2.uapis.cn/update_free_subdomain"
payload = {
"token": token,
"domain": domain,
"record": record,
"type": record_type,
"target": target,
"ttl": "1分钟",
"remarks": ""
}
headers = {
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
'Content-Type': 'application/json'
}
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
logger.info("子域名更新成功")
else:
logger.warning(f"子域名更新失败: {response.text}")
except Exception as e:
logger.exception("更新子域名时发生错误")
logger.exception(e)
def get_tunnel_info(tunnels, tunnel_name):
"""获取特定隧道信息"""
logger.info(f"获取隧道 {tunnel_name} 的信息")
for tunnel in tunnels:
if tunnel.get("name") == tunnel_name:
logger.debug(f"找到隧道 {tunnel_name} 的信息")
return tunnel
logger.warning(f"未找到隧道 {tunnel_name} 的信息")
return None
def update_tunnel(token, tunnel_info, node):
"""更新隧道信息"""
logger.info(f"更新隧道 {tunnel_info['name']} 到节点 {node}")
url = "http://cf-v2.uapis.cn/update_tunnel"
payload = {
"tunnelid": int(tunnel_info["id"]),
"token": token,
"tunnelname": tunnel_info["name"],
"node": str(node),
"localip": tunnel_info["localip"],
"porttype": tunnel_info["type"],
"localport": tunnel_info["nport"],
"remoteport": tunnel_info["dorp"],
"banddomain": "",
"encryption": tunnel_info["encryption"],
"compression": tunnel_info["compression"],
"extraparams": ""
}
headers = {
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
'Content-Type': 'application/json'
}
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
logger.info("隧道更新成功")
else:
logger.warning(f"隧道更新失败: {response.text}")
except Exception as e:
logger.exception("更新隧道时发生错误")
logger.exception(e)
def get_tunnel_config(token, node, tunnel_name):
"""获取隧道配置"""
url = f"http://cf-v2.uapis.cn/tunnel_config?token={token}&node={node}&tunnel_names={tunnel_name}"
headers = {'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'}
try:
response = requests.get(url, headers=headers)
response.raise_for_status() # 这会在HTTP错误时抛出异常
data = response.json()
if data['code'] == 200:
config = data.get("data")
if config:
return config
else:
logger.warning(f"获取隧道配置失败: 返回数据中没有 'data' 字段")
else:
logger.warning(f"获取隧道配置失败: {data.get('msg', '未知错误')}")
return None
except requests.RequestException as e:
logger.exception(f"获取隧道配置时发生网络错误: {str(e)}")
return None
except Exception as e:
logger.exception(f"获取隧道配置时发生未知错误: {str(e)}")
return None
def is_node_online(node_name):
"""检查节点是否在线"""
url = "http://cf-v2.uapis.cn/node_stats"
headers = {'User-Agent': 'Apifox/1.0.0 (https://apifox.com)'}
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
stats = response.json()
if stats and 'data' in stats:
for node in stats['data']:
if node['node_name'] == node_name:
return node['state'] == "online"
return False
except Exception as e:
logger.exception("检查节点在线状态时发生错误")
logger.exception(e)
return False
def read_config():
file_path = get_absolute_path("Dynamic_tunneling.json")
try:
with open(file_path, 'r') as file:
content = file.read()
if content.strip():
return json.loads(content)
else:
return []
except FileNotFoundError:
return []
except json.JSONDecodeError as e:
print(f"JSON解析错误: {e}")
return []
def get_absolute_path(relative_path):
"""获取文件的绝对路径"""
if relative_path == "dynamic_tunneling.log":
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
else:
return os.path.join(get_config_folder(), relative_path)
def write_config(configs):
file_path = get_absolute_path("Dynamic_tunneling.json")
with open(file_path, 'w') as file:
json.dump(configs, file, indent=2)
def parse_domain(domain):
"""解析域名"""
logger.info(f"解析域名: {domain}")
parts = domain.split('.')
if len(parts) >= 3:
subdomain = '.'.join(parts[:-2])
main_domain = '.'.join(parts[-2:])
else:
subdomain = parts[0]
main_domain = '.'.join(parts[-2:])
logger.debug(f"解析结果 - 子域名: {subdomain}, 主域名: {main_domain}")
return subdomain, main_domain
class QtHandler(QObject, logging.Handler):
"""Qt日志处理器"""
new_record = pyqtSignal(str)
def __init__(self, parent):
super().__init__(parent)
super(logging.Handler).__init__()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
self.setFormatter(formatter)
def emit(self, record):
msg = self.format(record)
self.new_record.emit(msg)
def setup_logging(parent):
"""设置日志系统"""
logger = logging.getLogger('dynamic_tunneling')
logger.setLevel(logging.DEBUG)
# 文件处理器
file_handler = RotatingFileHandler('dynamic_tunneling.log', maxBytes=5 * 1024 * 1024, backupCount=5)
file_handler.setLevel(logging.DEBUG)
file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(file_formatter)
logger.addHandler(file_handler)
# Qt处理器
qt_handler = QtHandler(parent)
qt_handler.setLevel(logging.INFO)
logger.addHandler(qt_handler)
return logger, qt_handler
class CustomListItem(QListWidgetItem):
def __init__(self, widget):
super().__init__()
self.widget = widget
self.setSizeHint(widget.sizeHint())
# 添加 CustomItemDelegate 类
class CustomItemDelegate(QStyledItemDelegate):
def paint(self, painter: QPainter, option, index):
try:
if option.state & QStyle.StateFlag.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
widget = index.data(Qt.ItemDataRole.UserRole)
if widget:
painter.save()
painter.translate(option.rect.topLeft())
widget.render(painter, QPoint(), QRegion(), QWidget.RenderFlag.DrawChildren)
painter.restore()
except Exception as e:
print(f"Error in CustomItemDelegate.paint: {str(e)}")
def sizeHint(self, option, index):
widget = index.data(Qt.ItemDataRole.UserRole)
if widget:
return widget.sizeHint()
return super().sizeHint(option, index)
class PingThread(QThread):
"""Ping线程"""
update_signal = pyqtSignal(str, object)
def __init__(self, target, ping_type):
super().__init__()
self.target = target
self.ping_type = ping_type
def run(self):
if self.ping_type == "ICMP":
result = self.icmp_ping()
elif self.ping_type == "TCP":
result = self.tcp_ping()
elif self.ping_type == "HTTP":
result = self.http_ping()
elif self.ping_type == "HTTPS":
result = self.https_ping()
elif self.ping_type == "JavaMC":
result = self.java_mc_ping()
elif self.ping_type == "BedrockMC":
result = self.bedrock_mc_ping()
else:
result = None
if result is not None:
self.update_signal.emit(self.target, result)
def icmp_ping(self):
try:
output = subprocess.check_output(["ping", "-n", "4", self.target], universal_newlines=True)
lines = output.split('\n')
result = {
'min': None,
'max': None,
'avg': None,
'loss': None
}
for line in lines:
if "最小 = " in line:
parts = line.split(', ')
result['min'] = float(parts[0].split('=')[1].strip().replace('ms', ''))
result['max'] = float(parts[1].split('=')[1].strip().replace('ms', ''))
result['avg'] = float(parts[2].split('=')[1].strip().replace('ms', ''))
elif "丢失 = " in line:
result['loss'] = int(line.split('(')[1].split('%')[0])
return result
except subprocess.CalledProcessError:
return None
def tcp_ping(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
start_time = time.time()
try:
sock.connect((self.target, 80))
return (time.time() - start_time) * 1000
except socket.error:
return None
finally:
sock.close()
def http_ping(self):
try:
start_time = time.time()
requests.get(f"http://{self.target}", timeout=5)
return (time.time() - start_time) * 1000
except requests.RequestException:
return None
def https_ping(self):
try:
start_time = time.time()
requests.get(f"https://{self.target}", timeout=5, verify=False)
return (time.time() - start_time) * 1000
except requests.RequestException:
return None
def java_mc_ping(self):
try:
server = JavaServer.lookup(self.target)
status = server.status()
return {
'延迟': status.latency,
'版本': status.version.name,
'协议': status.version.protocol,
'在线玩家': status.players.online,
'最大玩家': status.players.max,
'描述': status.description
}
except Exception as e:
return f"错误: {str(e)}"
def bedrock_mc_ping(self):
try:
server = BedrockServer.lookup(self.target)
status = server.status()
return {
'延迟': status.latency,
'版本': status.version.name,
'协议': status.version.protocol,
'在线玩家': status.players_online,
'最大玩家': status.players_max,
'游戏模式': status.gamemode,
'地图': status.map
}
except Exception as e:
return f"错误: {str(e)}"
class NodeSelectionDialog(QDialog):
"""节点选择对话框"""
def __init__(self, nodes, parent=None):
super().__init__(parent)
self.nodes = nodes
self.selected_nodes = []
self.initUI()
def initUI(self):
self.setWindowTitle("选择节点")
layout = QVBoxLayout()
splitter = QSplitter(Qt.Orientation.Horizontal)
self.available_list = QListWidget()
self.selected_list = QListWidget()
for node in self.nodes:
self.available_list.addItem(node['name'])
splitter.addWidget(self.available_list)
splitter.addWidget(self.selected_list)
layout.addWidget(splitter)
button_layout = QHBoxLayout()
ok_button = QPushButton("确定")
ok_button.clicked.connect(self.accept)
cancel_button = QPushButton("取消")
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
self.setLayout(layout)
self.available_list.itemClicked.connect(self.add_node)
self.selected_list.itemDoubleClicked.connect(self.remove_node)
def add_node(self, item):
self.available_list.takeItem(self.available_list.row(item))
self.selected_list.addItem(item.text())
def remove_node(self, item):
self.selected_list.takeItem(self.selected_list.row(item))
self.available_list.addItem(item.text())
def accept(self):
self.selected_nodes = [self.selected_list.item(i).text() for i in range(self.selected_list.count())]
if not self.selected_nodes:
QMessageBox.warning(self, "警告", "请至少选择一个节点")
else:
super().accept()
class WorkerThread(QThread):
"""工作线程"""
update_signal = pyqtSignal(str)
def __init__(self, function, *args, **kwargs):
super().__init__()
self.function = function
self.args = args
self.kwargs = kwargs
def run(self):
result = self.function(*self.args, **self.kwargs)
self.update_signal.emit(str(result))
class BaseCard(QFrame):
clicked = pyqtSignal()
def __init__(self):
super().__init__()
self.setFrameShape(QFrame.Shape.StyledPanel)
self.setFrameShadow(QFrame.Shadow.Raised)
self.setAttribute(Qt.WidgetAttribute.WA_Hover)
self.setStyleSheet("""
BaseCard {
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
border-radius: 5px;
}
BaseCard:hover {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
""")
class TunnelCard(QFrame):
clicked = pyqtSignal(object, bool)
start_stop_signal = pyqtSignal(object, bool)
def __init__(self, tunnel_info):
super().__init__()
self.tunnel_info = tunnel_info
self.is_running = False
self.is_selected = False
self.initUI()
self.updateStyle()
def initUI(self):
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
name_label = QLabel(f"<b>{self.tunnel_info.get('name', 'Unknown')}</b>")
name_label.setObjectName("nameLabel")
type_label = QLabel(f"类型: {self.tunnel_info.get('type', 'Unknown')}")
local_label = QLabel(
f"本地: {self.tunnel_info.get('localip', 'Unknown')}:{self.tunnel_info.get('nport', 'Unknown')}")
remote_label = QLabel(f"远程端口: {self.tunnel_info.get('dorp', 'Unknown')}")
node_label = QLabel(f"节点: {self.tunnel_info.get('node', 'Unknown')}")
self.status_label = QLabel("状态: 未启动")
self.start_stop_button = QPushButton("启动")
self.start_stop_button.clicked.connect(self.toggle_start_stop)
layout.addWidget(name_label)
layout.addWidget(type_label)
layout.addWidget(local_label)
layout.addWidget(remote_label)
layout.addWidget(node_label)
layout.addWidget(self.status_label)
layout.addWidget(self.start_stop_button)
self.setLayout(layout)
self.setFixedSize(250, 220)
def toggle_start_stop(self):
self.is_running = not self.is_running
self.update_status()
self.start_stop_signal.emit(self.tunnel_info, self.is_running)
def update_status(self):
if self.is_running:
self.status_label.setText("状态: 运行中")
self.start_stop_button.setText("停止")
else:
self.status_label.setText("状态: 未启动")
self.start_stop_button.setText("启动")
self.update()
def paintEvent(self, event):
super().paintEvent(event)
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
if self.is_running:
color = QColor(0, 255, 0) # 绿色
else:
color = QColor(255, 0, 0) # 红色
painter.setPen(QPen(color, 2))
painter.setBrush(color)
painter.drawEllipse(self.width() - 20, 10, 10, 10)
def updateStyle(self):
self.setStyleSheet("""
TunnelCard {
border: 1px solid #d0d0d0;
border-radius: 5px;
padding: 10px;
margin: 5px;
}
TunnelCard:hover {
background-color: rgba(240, 240, 240, 50);
}
#nameLabel {
font-size: 16px;
font-weight: bold;
}
""")
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.is_selected = not self.is_selected
self.setSelected(self.is_selected)
self.clicked.emit(self.tunnel_info, self.is_selected)
super().mousePressEvent(event)
def setSelected(self, selected):
self.is_selected = selected
if selected:
self.setStyleSheet(self.styleSheet() + "TunnelCard { border: 2px solid #0066cc; background-color: rgba(224, 224, 224, 50); }")
else:
self.setStyleSheet(self.styleSheet().replace("TunnelCard { border: 2px solid #0066cc; background-color: rgba(224, 224, 224, 50); }", ""))
class BatchEditDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("批量编辑隧道")
self.layout = QVBoxLayout(self)
self.node_combo = QComboBox()
self.node_combo.addItem("不修改")
self.node_combo.addItems([node['name'] for node in get_nodes()])
self.type_combo = QComboBox()
self.type_combo.addItem("不修改")
self.type_combo.addItems(["tcp", "udp", "http", "https"])
self.local_ip_input = QLineEdit()
self.local_ip_input.setPlaceholderText("不修改")
self.local_port_input = QLineEdit()
self.local_port_input.setPlaceholderText("不修改")
self.remote_port_input = QLineEdit()
self.remote_port_input.setPlaceholderText("不修改")
form_layout = QFormLayout()
form_layout.addRow("节点:", self.node_combo)
form_layout.addRow("类型:", self.type_combo)
form_layout.addRow("本地IP:", self.local_ip_input)
form_layout.addRow("本地端口:", self.local_port_input)
form_layout.addRow("远程端口:", self.remote_port_input)
self.layout.addLayout(form_layout)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
self.layout.addWidget(buttons)
def get_changes(self):
changes = {}
if self.node_combo.currentIndex() != 0:
changes['node'] = self.node_combo.currentText()
if self.type_combo.currentIndex() != 0:
changes['type'] = self.type_combo.currentText()
if self.local_ip_input.text():
changes['localip'] = self.local_ip_input.text()
if self.local_port_input.text():
changes['nport'] = int(self.local_port_input.text())
if self.remote_port_input.text():
changes['dorp'] = int(self.remote_port_input.text())
return changes
class DomainCard(QFrame):
clicked = pyqtSignal(object)
def __init__(self, domain_info):
super().__init__()
self.domain_info = domain_info
self.initUI()
self.updateStyle()
def initUI(self):
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
domain_label = QLabel(f"<b>{self.domain_info['record']}.{self.domain_info['domain']}</b>")
domain_label.setObjectName("nameLabel")
type_label = QLabel(f"类型: {self.domain_info['type']}")
target_label = QLabel(f"目标: {self.domain_info['target']}")
ttl_label = QLabel(f"TTL: {self.domain_info['ttl']}")
remarks_label = QLabel(f"备注: {self.domain_info.get('remarks', '无')}")
layout.addWidget(domain_label)
layout.addWidget(type_label)
layout.addWidget(target_label)
layout.addWidget(ttl_label)
layout.addWidget(remarks_label)
self.setLayout(layout)
self.setFixedSize(250, 180)
def updateStyle(self):
self.setStyleSheet("""
DomainCard {
border: 1px solid #d0d0d0;
border-radius: 5px;
padding: 10px;
margin: 5px;
}
DomainCard:hover {
background-color: rgba(240, 240, 240, 50);
}
#nameLabel {
font-size: 16px;
font-weight: bold;
}
""")
def setSelected(self, selected):
if selected:
self.setStyleSheet(self.styleSheet() + "DomainCard { border: 2px solid #0066cc; background-color: rgba(224, 224, 224, 50); }")
else:
self.setStyleSheet(self.styleSheet().replace("DomainCard { border: 2px solid #0066cc; background-color: rgba(224, 224, 224, 50); }", ""))
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.clicked.emit(self.domain_info)
super().mousePressEvent(event)
class ConfigEditorDialog(QDialog):
def __init__(self, config_file_path, dark_theme, parent=None):
super().__init__(parent)
self.config_file_path = config_file_path
self.dark_theme = dark_theme
self.initUI()
self.apply_theme()
def initUI(self):
self.setWindowTitle("编辑配置文件")
self.setGeometry(100, 100, 600, 400)
layout = QVBoxLayout(self)
# 创建选项卡小部件
self.tab_widget = QTabWidget()
layout.addWidget(self.tab_widget)
# 文本编辑选项卡
self.text_edit = QTextEdit()
self.tab_widget.addTab(self.text_edit, "文本编辑")
# 可视化编辑选项卡
visual_edit_widget = QWidget()
visual_edit_layout = QFormLayout(visual_edit_widget)
self.tab_widget.addTab(visual_edit_widget, "可视化编辑")
# 添加可视化编辑的输入字段
self.token_input = QLineEdit()
self.nodes_input = QLineEdit()
self.tunnel_name_input = QLineEdit()
self.domain_input = QLineEdit()
self.subdomain_input = QLineEdit()
self.record_type_combo = QComboBox()
self.record_type_combo.addItems(["A", "SRV"])
visual_edit_layout.addRow("Token:", self.token_input)
visual_edit_layout.addRow("节点 (逗号分隔):", self.nodes_input)
visual_edit_layout.addRow("隧道名称:", self.tunnel_name_input)
visual_edit_layout.addRow("域名:", self.domain_input)
visual_edit_layout.addRow("子域名:", self.subdomain_input)
visual_edit_layout.addRow("记录类型:", self.record_type_combo)
# SRV 特定输入
self.srv_widget = QWidget()
srv_layout = QFormLayout(self.srv_widget)
self.priority_input = QLineEdit("10")
self.weight_input = QLineEdit("10")
self.port_input = QLineEdit()
srv_layout.addRow("优先级:", self.priority_input)
srv_layout.addRow("权重:", self.weight_input)
srv_layout.addRow("端口:", self.port_input)
visual_edit_layout.addRow(self.srv_widget)
self.record_type_combo.currentTextChanged.connect(self.on_record_type_changed)
# 添加保存和取消按钮
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.load_config()
def apply_theme(self):
if self.dark_theme:
self.setStyleSheet("""
QDialog, QTabWidget, QTextEdit, QLineEdit, QComboBox {
background-color: #2D2D2D;
color: #FFFFFF;
}
QTabWidget::pane {
border: 1px solid #555555;
}
QTabBar::tab {
background-color: #3D3D3D;
color: #FFFFFF;
padding: 5px;
}
QTabBar::tab:selected {
background-color: #4D4D4D;
}
QPushButton {
background-color: #0D47A1;
color: white;
border: none;
padding: 5px 10px;
text-align: center;
text-decoration: none;
font-size: 14px;
margin: 4px 2px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #1565C0;
}
QLabel {
color: #FFFFFF;
}
""")
else:
self.setStyleSheet("") # 使用默认浅色主题
def on_record_type_changed(self):
self.srv_widget.setVisible(self.record_type_combo.currentText() == "SRV")
def load_config(self):
with open(self.config_file_path, 'r') as f:
config_content = f.read()
self.text_edit.setPlainText(config_content)
try:
config = ast.literal_eval(config_content)
if isinstance(config, list) and len(config) >= 6:
self.token_input.setText(config[0])
self.nodes_input.setText(", ".join(config[2:2+int(config[1])]))
self.tunnel_name_input.setText(config[2+int(config[1])])
self.domain_input.setText(config[3+int(config[1])])
self.subdomain_input.setText(config[4+int(config[1])])
self.record_type_combo.setCurrentText(config[5+int(config[1])])
if len(config) > 6+int(config[1]) and config[5+int(config[1])] == "SRV":
self.priority_input.setText(config[6+int(config[1])])
self.weight_input.setText(config[7+int(config[1])])
self.port_input.setText(config[8+int(config[1])])
except:
pass
def get_config(self):
if self.tab_widget.currentIndex() == 0:
# 文本编辑模式
return self.text_edit.toPlainText()
else:
# 可视化编辑模式
nodes = [node.strip() for node in self.nodes_input.text().split(',')]
config = [
self.token_input.text(),
len(nodes),
*nodes,
self.tunnel_name_input.text(),
self.domain_input.text(),
self.subdomain_input.text(),
self.record_type_combo.currentText()
]
if self.record_type_combo.currentText() == "SRV":
config.extend([
self.priority_input.text(),
self.weight_input.text(),
self.port_input.text()
])
return str(config)
class ConfigListItem(QListWidgetItem):
def __init__(self, config):
super().__init__()
self.config = config
self.update_display()
def update_display(self):
if isinstance(self.config, dict):
display_text = (f"隧道: {self.config.get('tunnel_name', 'N/A')}\n"
f"域名: {self.config.get('subdomain', 'N/A')}.{self.config.get('domain', 'N/A')}")
else:
display_text = str(self.config)
self.setText(display_text)
07-08
1483
09-26
1235