Compare commits

..
Author SHA1 Message Date
RoyirinRosmontis f8eba465a9 修复: - 日志不再打印用户数据目录路径:节点配置加载与首启创建提示中的 %LOCALAPPDATA% 路径已移除 2026-08-31 16:36:44 +08:00
RoyirinRosmontis 53e9d1e31a 修复: - 检查更新不再内置回退地址:只读取 config.json 的 update_url,未配置时检查更新直接失败并记录日志 2026-08-31 16:25:48 +08:00
RoyirinRosmontis 59393b9ae9 修复: - 检查更新地址改用 config.json 的 update_url 配置,内置回退地址域名从 git.bu4.ren 更换为 git.rrrosmontis.icu 2026-08-31 16:24:15 +08:00
RoyirinRosmontis 89b5c09e95 修复: - 设置页代理转发端口输入框横向过长:宽度 72px 缩到 54px(够放下 5 位端口号) 2026-08-27 13:19:51 +08:00
RoyirinRosmontis b15beb97b4 修复: - 设置页代理转发端口输入框过大:内边距从 5px 缩到 2px,并固定为 72×22 与右侧开关同高,和其他设置项同一水平 2026-08-27 13:19:33 +08:00
RoyirinRosmontis 567d395d6f 新增功能: - 代理白名单:设置页新增第一个设置项「代理白名单+配置」,点配置打开编辑弹窗(Windows 环境变量风格列表),支持添加/删除强制直连的域名、IP、CIDR(IPv4/IPv6 均支持),每条规则带复选框可单独启用/禁用,保存后立即生效,白名单优先于域名表强制隧道与固定代理模式 2026-08-27 13:18:07 +08:00
RoyirinRosmontis c2c3bd1426 修复:
- 打包路径兼容 Nuitka:识别 __compiled__,无 sys._MEIPASS 时回退 __file__ 所在目录(onefile 解压临时目录);新增 build_nuitka.bat(PyQt5 插件、内置 config.json、输出名不变)
2026-08-26 14:56:06 +08:00
RoyirinRosmontis 2aeb327638 新增功能:
- 切换节点后把当前节点域名记录到用户设置(settings.json 的 current_node),下次启动自动继续使用上次选中的节点(按域名匹配,节点表变化时安全回退第一条)
2026-08-26 14:39:57 +08:00
RoyirinRosmontis 717b7569de 更新 server_config/nginx/nginx.conf 2026-08-26 11:32:44 +08:00
2 changed files with 336 additions and 20 deletions
+333 -16
View File
@@ -7,6 +7,7 @@ from PyQt5.QtWidgets import (
QApplication, QWidget, QLabel, QPushButton, QApplication, QWidget, QLabel, QPushButton,
QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit, QVBoxLayout, QHBoxLayout, QFrame, QMessageBox, QTextEdit,
QCheckBox, QMenu, QDialog, QLineEdit, QCheckBox, QMenu, QDialog, QLineEdit,
QListWidget, QListWidgetItem,
) )
from PyQt5.QtCore import Qt, QTimer, QRectF, pyqtSignal from PyQt5.QtCore import Qt, QTimer, QRectF, pyqtSignal
from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor, QPainter from PyQt5.QtGui import QFont, QIcon, QPixmap, QImage, QColor, QPainter
@@ -28,11 +29,12 @@ C_TEXT = "#333333" # 正文
C_SUBTEXT = "#889099" # 次要文字 C_SUBTEXT = "#889099" # 次要文字
# ── 路径 ────────────────────────────────────────────── # ── 路径 ──────────────────────────────────────────────
# frozen=True 表示已被 PyInstaller 打包为 EXE: # 已被打包为 EXE(PyInstaller 的 sys.frozen / Nuitka 的 __compiled__):
# BASE_DIR -> EXE 解压出的临时目录(_MEIPASS) # BASE_DIR -> 内置数据目录:PyInstaller 用 _MEIPASS,Nuitka onefile 用
# __file__ 所在解压临时目录(无 _MEIPASS 时回退)
# PERSIST_DIR -> EXE 所在目录,服务安装等需要持久化的操作都放这里 # PERSIST_DIR -> EXE 所在目录,服务安装等需要持久化的操作都放这里
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False) or "__compiled__" in globals():
BASE_DIR = Path(sys._MEIPASS) BASE_DIR = Path(getattr(sys, "_MEIPASS", None) or Path(__file__).resolve().parent)
PERSIST_DIR = Path(sys.executable).resolve().parent PERSIST_DIR = Path(sys.executable).resolve().parent
else: else:
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
@@ -88,7 +90,9 @@ def _load_nodes():
if not isinstance(data, list): if not isinstance(data, list):
raise ValueError("nodes.json 顶层应为节点数组") raise ValueError("nodes.json 顶层应为节点数组")
_NODES = data _NODES = data
log(f"已加载节点配置: {len(_NODES)} 个 ({path})") if not _SELECTED_RESTORED:
_restore_selected_node(_NODES) # 恢复上次使用的节点
log(f"已加载节点配置: {len(_NODES)} 个")
if data and not str(data[0].get("domain") or "").strip(): if data and not str(data[0].get("domain") or "").strip():
log_red("节点未填写域名:请配置 config.json 的 nodes_url 在线更新," log_red("节点未填写域名:请配置 config.json 的 nodes_url 在线更新,"
"或使用 others/nodes_tool 导入节点配置") "或使用 others/nodes_tool 导入节点配置")
@@ -97,6 +101,7 @@ def _load_nodes():
return _NODES return _NODES
_SELECTED_NODE = 0 # 当前选中节点下标(默认第一条) _SELECTED_NODE = 0 # 当前选中节点下标(默认第一条)
_SELECTED_RESTORED = False # 启动时是否已按设置恢复过上次选中的节点
def _current_node(): def _current_node():
"""当前使用的节点:默认取列表第一个;无节点时返回 None。""" """当前使用的节点:默认取列表第一个;无节点时返回 None。"""
@@ -121,8 +126,26 @@ def _switch_node(index):
log("已在当前节点") log("已在当前节点")
return False return False
_SELECTED_NODE = index _SELECTED_NODE = index
_save_current_node(nodes[index].get("domain") or "") # 记录到设置,下次启动恢复
return True return True
def _restore_selected_node(nodes):
"""首次加载时按设置里记录的 current_node(域名)恢复上次选中的节点。"""
global _SELECTED_NODE, _SELECTED_RESTORED
_SELECTED_RESTORED = True
saved = str(_settings_load().get("current_node") or "")
for i, n in enumerate(nodes):
if saved and str(n.get("domain") or "") == saved:
_SELECTED_NODE = i
return
_SELECTED_NODE = 0
def _save_current_node(domain):
"""把当前选中节点域名写入设置文件(下次启动自动继续使用)。"""
data = _settings_load()
data["current_node"] = str(domain or "")
_settings_save(data)
def _reload_nodes(): def _reload_nodes():
"""强制重新读取 nodes.json(菜单打开时调用,外部改动即时生效)。""" """强制重新读取 nodes.json(菜单打开时调用,外部改动即时生效)。"""
global _NODES global _NODES
@@ -613,6 +636,74 @@ def _set_skip_update_check(enabled):
_settings_save(data) _settings_save(data)
log(f"检查更新已{'跳过' if enabled else '恢复'}(PAC 与程序版本;节点更新不受影响)") log(f"检查更新已{'跳过' if enabled else '恢复'}(PAC 与程序版本;节点更新不受影响)")
# ── 代理白名单(用户可配置,强制直连,settings.json 的 whitelist 键) ─────────
# 每条规则 {"value": 域名/IP/CIDR, "enabled": 是否启用},命中即直连(不经过代理)。
# 规则保存在内存缓存 _WHITELIST,连接建立时直接读取,不落盘回读;
# _WHITELIST_VERSION 在保存时 +1,使按 (host, version) 键控的匹配缓存失效。
_WHITELIST = None # 内存缓存:规范化后的规则列表
_WHITELIST_VERSION = 0 # 规则版本号(保存时 +1,用于匹配缓存失效)
def _whitelist_rules():
"""白名单规则列表(内存缓存,首次读取 settings.json;保存时同步更新)。"""
global _WHITELIST
if _WHITELIST is None:
raw = _settings_load().get("whitelist")
if not isinstance(raw, list):
raw = []
_WHITELIST = [{"value": str(r.get("value") or "").strip().lower(),
"enabled": bool(r.get("enabled", True))}
for r in raw if isinstance(r, dict) and str(r.get("value") or "").strip()]
return _WHITELIST
def _whitelist_save(rules):
"""保存白名单规则(规范化 + 去空白条目),更新内存缓存并记录日志。"""
global _WHITELIST, _WHITELIST_VERSION
clean = [{"value": _whitelist_normalize(r.get("value") or ""),
"enabled": bool(r.get("enabled", True))}
for r in rules if _whitelist_normalize(r.get("value") or "")]
data = _settings_load()
data["whitelist"] = clean
_settings_save(data)
_WHITELIST = clean
_WHITELIST_VERSION += 1
log(f"代理白名单已保存: {len(clean)} 条(立即生效)")
def _whitelist_normalize(entry: str) -> str:
"""条目规范化:去协议头/通配符/末尾点,统一小写。"""
s = str(entry).strip().lower()
if "://" in s:
s = s.split("://", 1)[1]
return s.rstrip("/").rstrip(".").lstrip("*.")
def _whitelist_validate_entry(entry: str):
"""校验并规范化白名单条目;返回 (规范值, 错误),错误非空表示非法。"""
s = _whitelist_normalize(entry)
if not s:
return "", "内容不能为空"
if any(ch.isspace() for ch in s):
return "", "不能包含空格"
import ipaddress
if "/" in s:
# CIDR(IPv4/IPv6 网段或单地址)
try:
ipaddress.ip_network(s, strict=False)
except ValueError:
return "", "IP/CIDR 格式不正确(如 10.0.0.0/8)"
elif s.count(".") == 3 and all(p.isdigit() for p in s.split(".")):
# 形如 x.x.x.x:必须是合法 IPv4,防止 300.1.2.3 这类误当域名
try:
ipaddress.ip_address(s)
except ValueError:
return "", "IP 格式不正确"
elif ":" in s:
# IPv6 字面量(域名不含冒号)
try:
ipaddress.ip_address(s)
except ValueError:
return "", "IP 格式不正确"
return s, ""
def _ver_tuple(v): def _ver_tuple(v):
"""版本号转数字元组:'v0.1.0' -> (0, 1, 0),无法解析的段按 0 处理。""" """版本号转数字元组:'v0.1.0' -> (0, 1, 0),无法解析的段按 0 处理。"""
parts = [] parts = []
@@ -825,8 +916,8 @@ def _user_init_local():
"username": "", "password": "", "remark": ""}] "username": "", "password": "", "remark": ""}]
(_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt( (_USER_DIR / "nodes.json").write_bytes(_nodes_encrypt(
json.dumps(template, ensure_ascii=False, indent=2) + "\n")) json.dumps(template, ensure_ascii=False, indent=2) + "\n"))
log_red(f"已创建 {_USER_DIR / 'nodes.json'}(加密存储),请配置 config.json 的 " log_red("已创建节点配置文件(加密存储),请配置 config.json 的 "
f"nodes_url 在线更新,或使用 others/nodes_tool 导入节点配置") "nodes_url 在线更新,或使用 others/nodes_tool 导入节点配置")
def _user_init_remote(base_url=None): def _user_init_remote(base_url=None):
"""检查并更新 PAC 数据(后台线程): """检查并更新 PAC 数据(后台线程):
@@ -981,11 +1072,55 @@ def _is_force_proxy(host: str) -> bool:
h = host.rstrip(".").lower() h = host.rstrip(".").lower()
return any(h == s or h.endswith("." + s) for s in suffixes) return any(h == s or h.endswith("." + s) for s in suffixes)
def _resolve_host_ips(host: str):
"""解析域名的 IPv4/IPv6 地址集合(字面量 IP 返回自身);解析失败返回空集合。"""
import ipaddress
try:
return {ipaddress.ip_address(host)}
except ValueError:
pass
try:
import socket
# 双栈解析(AF_UNSPEC):IPv6 白名单条目也能命中域名解析出的 IPv6 地址
return {ipaddress.ip_address(a[4][0])
for a in socket.getaddrinfo(host, None, socket.AF_UNSPEC, socket.SOCK_STREAM)}
except Exception:
return set()
def _whitelist_entry_match(host: str, entry: str) -> bool:
"""单条白名单规则匹配:IP/CIDR 条目按解析地址比对,域名条目按后缀匹配。"""
h = host.rstrip(".").lower()
entry = entry.rstrip(".").lower()
import ipaddress
net = None
# 只有含点/冒号才按 IP 解析:纯数字串(如 8080)会被 ip_network 误解析为
# 整型地址,应视为域名条目
if "." in entry or ":" in entry:
try:
net = ipaddress.ip_network(entry, strict=False)
except ValueError:
net = None
if net is not None:
return any(addr in net for addr in _resolve_host_ips(h))
# 域名条目:精确匹配或子域后缀匹配(与 pac_domains 同一规则)
return h == entry or h.endswith("." + entry)
@functools.lru_cache(maxsize=8192)
def _is_whitelisted(host: str, version: int) -> bool:
"""host 是否命中已启用的白名单规则(按版本键控缓存,规则变更即失效)。"""
entries = [r["value"] for r in _whitelist_rules() if r.get("enabled")]
if not entries:
return False
return any(_whitelist_entry_match(host, e) for e in entries)
def _decide_direct(host: str, bypass_lan: bool, smart: bool) -> bool: def _decide_direct(host: str, bypass_lan: bool, smart: bool) -> bool:
"""判断连接是否直连。判定顺序(域名表优先于 IP): """判断连接是否直连。判定顺序(白名单最优先):
域名表命中 -> 隧道;内网 + bypass_lan -> 直连;中国 IP -> 直连,其余 -> 隧道。 白名单命中 -> 直连;域名表命中 -> 隧道;内网 + bypass_lan -> 直连;
仅 PAC(智能分流)模式生效,固定代理模式全部走隧道。 中国 IP -> 直连,其余 -> 隧道。
白名单在 PAC 与固定代理模式下均生效(固定模式除白名单外全部走隧道)。
""" """
if _is_whitelisted(host, _WHITELIST_VERSION):
return True
if smart and _is_force_proxy(host): if smart and _is_force_proxy(host):
return False return False
if bypass_lan and _is_private_ip(host): if bypass_lan and _is_private_ip(host):
@@ -1406,6 +1541,164 @@ class LoginDialog(QDialog):
self.lb_err.setText(info) self.lb_err.setText(info)
self.lb_err.show() self.lb_err.show()
# ── 代理白名单弹窗 ─────────────────────────────────────
class WhitelistDialog(QDialog):
"""代理白名单编辑弹窗:可编辑列表(Windows 环境变量风格),
每条规则带复选框启用/禁用,支持添加/删除,保存后立即生效。"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("代理白名单")
self.setWindowIcon(load_icon())
self.setFixedSize(380, 400)
self.setStyleSheet(f"""
QDialog {{ background-color: {C_BG}; }}
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
QLineEdit {{
background-color: {C_CARD};
border: 1px solid {C_BORDER};
border-radius: 6px;
padding: 6px 8px;
color: {C_TEXT};
font-size: 12px;
}}
QListWidget {{
background-color: {C_CARD};
border: 1px solid {C_BORDER};
border-radius: 6px;
padding: 4px;
color: {C_TEXT};
font-size: 12px;
}}
QListWidget::item {{ padding: 3px 6px; border-radius: 4px; }}
QListWidget::item:selected {{ background-color: #e3e8ee; color: {C_TEXT}; }}
QPushButton#btnOk {{
background-color: {C_GREEN}; color: #fff; border: none;
border-radius: 6px; padding: 4px 12px;
font-size: 12px; font-weight: bold;
}}
QPushButton#btnOk:hover {{ background-color: #5ed978; }}
QPushButton#btnCancel {{
background-color: {C_CARD}; color: {C_SUBTEXT};
border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 4px 12px; font-size: 12px;
}}
QPushButton#btnSmall {{
background-color: {C_CARD}; color: {C_SUBTEXT};
border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 3px 10px; font-size: 11px;
}}
QPushButton#btnSmall:hover {{ background-color: #e3e8ee; color: {C_TEXT}; }}
""")
layout = QVBoxLayout(self)
layout.setSpacing(10)
layout.setContentsMargins(20, 16, 20, 16)
title = QLabel("代理白名单")
title.setStyleSheet(f"color: {C_TITLE}; font-size: 15px; font-weight: bold;")
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
hint = QLabel("命中以下规则的连接将强制直连(不经过代理)")
hint.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 11px;")
hint.setAlignment(Qt.AlignCenter)
layout.addWidget(hint)
self.lb_err = QLabel("")
self.lb_err.setStyleSheet(f"color: {C_RED}; font-size: 11px;")
self.lb_err.setFixedHeight(14)
self.lb_err.hide() # 无错误时不占空间
layout.addWidget(self.lb_err)
# 规则列表:每条前面带复选框(勾选启用 / 取消禁用)
self.list_widget = QListWidget()
self.list_widget.setToolTip("勾选启用规则;可多选后删除")
for r in _whitelist_rules():
self._add_item(r["value"], r.get("enabled", True))
layout.addWidget(self.list_widget, 1)
# 添加行:输入框 + 添加按钮
add_row = QHBoxLayout()
add_row.setSpacing(8)
self.ed_add = QLineEdit()
self.ed_add.setPlaceholderText("输入域名 / IP / CIDR,如 example.com")
self.ed_add.returnPressed.connect(self._add_rule)
add_row.addWidget(self.ed_add, 1)
self.btn_add = QPushButton("添加")
self.btn_add.setObjectName("btnOk")
self.btn_add.setStyleSheet(f"""
QPushButton {{
background-color: {C_GREEN}; color: #fff; border: none;
border-radius: 6px; padding: 6px 14px;
font-size: 12px; font-weight: bold;
}}
QPushButton:hover {{ background-color: #5ed978; }}
""")
self.btn_add.clicked.connect(self._add_rule)
add_row.addWidget(self.btn_add)
layout.addLayout(add_row)
# 删除选中 + 底部按钮
mid_row = QHBoxLayout()
self.btn_delete = QPushButton("删除选中")
self.btn_delete.setObjectName("btnSmall")
self.btn_delete.clicked.connect(self._delete_selected)
mid_row.addWidget(self.btn_delete)
mid_row.addStretch()
layout.addLayout(mid_row)
btns = QHBoxLayout()
btns.setSpacing(10)
btns.addStretch()
self.btn_cancel = QPushButton("取消")
self.btn_cancel.setObjectName("btnCancel")
self.btn_cancel.clicked.connect(self.reject)
btns.addWidget(self.btn_cancel)
self.btn_ok = QPushButton("保存")
self.btn_ok.setObjectName("btnOk")
self.btn_ok.clicked.connect(self._save)
btns.addWidget(self.btn_ok)
layout.addLayout(btns)
def _add_item(self, value, enabled):
item = QListWidgetItem(value)
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked if enabled else Qt.Unchecked)
self.list_widget.addItem(item)
def _add_rule(self):
value, err = _whitelist_validate_entry(self.ed_add.text())
if err:
self.lb_err.setText(err)
self.lb_err.show()
return
# 去重(不区分大小写)
for i in range(self.list_widget.count()):
if self.list_widget.item(i).text().lower() == value.lower():
self.lb_err.setText("该规则已存在")
self.lb_err.show()
return
self.lb_err.hide()
self._add_item(value, True)
self.ed_add.clear()
def _delete_selected(self):
for item in self.list_widget.selectedItems():
self.list_widget.takeItem(self.list_widget.row(item))
def _save(self):
"""收集列表内容保存(未点保存前所有改动均可取消)。"""
rules = []
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
value = str(item.text()).strip()
if value:
rules.append({"value": value, "enabled": item.checkState() == Qt.Checked})
_whitelist_save(rules)
self.accept()
# ── 设置弹窗 ───────────────────────────────────────── # ── 设置弹窗 ─────────────────────────────────────────
class SettingsDialog(QDialog): class SettingsDialog(QDialog):
@@ -1415,7 +1708,7 @@ class SettingsDialog(QDialog):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("设置") self.setWindowTitle("设置")
self.setWindowIcon(load_icon()) self.setWindowIcon(load_icon())
self.setFixedSize(300, 316) self.setFixedSize(300, 354)
self.setStyleSheet(f""" self.setStyleSheet(f"""
QDialog {{ background-color: {C_BG}; }} QDialog {{ background-color: {C_BG}; }}
QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }} QLabel {{ color: {C_TEXT}; font-family: "Microsoft YaHei", "Segoe UI", sans-serif; font-size: 12px; }}
@@ -1423,7 +1716,7 @@ class SettingsDialog(QDialog):
background-color: {C_CARD}; background-color: {C_CARD};
border: 1px solid {C_BORDER}; border: 1px solid {C_BORDER};
border-radius: 6px; border-radius: 6px;
padding: 5px 8px; padding: 2px 8px;
color: {C_TEXT}; color: {C_TEXT};
font-size: 12px; font-size: 12px;
}} }}
@@ -1463,6 +1756,27 @@ class SettingsDialog(QDialog):
layout.addStretch() # 设置项在空白部分纵向居中(上下均分) layout.addStretch() # 设置项在空白部分纵向居中(上下均分)
# 代理白名单:第一个设置项,点「配置」打开编辑弹窗
wl_row = QHBoxLayout()
wl_lab = QLabel("代理白名单")
wl_lab.setStyleSheet(f"color: {C_SUBTEXT}; font-size: 12px;")
wl_lab.setToolTip("命中列表的域名 / IP 将强制直连(不经过代理),每条规则可单独启用")
wl_row.addWidget(wl_lab)
wl_row.addStretch()
self.btn_whitelist = QPushButton("配置")
self.btn_whitelist.setCursor(Qt.PointingHandCursor)
self.btn_whitelist.setStyleSheet(f"""
QPushButton {{
background-color: {C_CARD}; color: {C_SUBTEXT};
border: 1px solid {C_BORDER}; border-radius: 6px;
padding: 2px 10px; font-size: 11px;
}}
QPushButton:hover {{ background-color: #e3e8ee; color: {C_TEXT}; }}
""")
self.btn_whitelist.clicked.connect(self._open_whitelist)
wl_row.addWidget(self.btn_whitelist)
layout.addLayout(wl_row)
# 代理转发端口 # 代理转发端口
port_row = QHBoxLayout() port_row = QHBoxLayout()
port_lab = QLabel("代理转发端口") port_lab = QLabel("代理转发端口")
@@ -1470,7 +1784,7 @@ class SettingsDialog(QDialog):
port_row.addWidget(port_lab) port_row.addWidget(port_lab)
port_row.addStretch() port_row.addStretch()
self.ed_port = QLineEdit(str(_TLS_LOCAL_PORT)) self.ed_port = QLineEdit(str(_TLS_LOCAL_PORT))
self.ed_port.setFixedWidth(80) self.ed_port.setFixedSize(54, 22) # 与右侧开关同高,和其他设置项同一水平
self.ed_port.setEnabled(not port_locked) self.ed_port.setEnabled(not port_locked)
port_row.addWidget(self.ed_port) port_row.addWidget(self.ed_port)
layout.addLayout(port_row) layout.addLayout(port_row)
@@ -1551,6 +1865,10 @@ class SettingsDialog(QDialog):
except Exception: except Exception:
pass pass
def _open_whitelist(self):
"""打开代理白名单编辑弹窗(保存后立即生效,无需重启代理)。"""
WhitelistDialog(self).exec_()
def _save_settings(self): def _save_settings(self):
"""保存更改:统一校验并应用全部设置项(后续新增设置项在此扩展)。""" """保存更改:统一校验并应用全部设置项(后续新增设置项在此扩展)。"""
port = _parse_port(self.ed_port.text()) port = _parse_port(self.ed_port.text())
@@ -2154,11 +2472,10 @@ class MainWindow(QWidget):
if _skip_update_check() and not manual: if _skip_update_check() and not manual:
return # 设置跳过(仅自动检查):不检查也不打日志 return # 设置跳过(仅自动检查):不检查也不打日志
import json, urllib.request import json, urllib.request
# 基于 Gitea 仓库 releases API 检查更新;config.json 无 update_url 时用默认地址 # 基于 Gitea 仓库 releases API 检查更新;地址只取 config.json 的 update_url
url = str(_config_val("update_url", "") or "").strip() url = str(_config_val("update_url", "") or "").strip()
if not url: if not url:
url = ("https://git.bu4.ren/api/v1/repos/RoyirinRosmontis/" raise RuntimeError("config.json 未配置 update_url")
"Private-Proxy/releases/latest")
log("正在检查更新...") log("正在检查更新...")
if _update_via_proxy(): if _update_via_proxy():
# 走节点隧道(CONNECT,设置「使用代理更新」开启时);显式代理避免残留干扰 # 走节点隧道(CONNECT,设置「使用代理更新」开启时);显式代理避免残留干扰
+3 -4
View File
@@ -56,15 +56,14 @@ http {
} }
} }
# 443 端口:iam.aeolianchi.me HTTPS 站点 + 认证 CONNECT 代理
server { server {
listen 443 ssl; listen 443 ssl;
http2 on; http2 on;
server_name iam.aeolianchi.me; server_name XXX;
root /var/www/html; root /var/www/html;
ssl_certificate /etc/nginx/ssl/iam.aeolianchi.me/fullchain.pem; ssl_certificate /etc/nginx/ssl/XXX/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/iam.aeolianchi.me/privkey.pem; ssl_certificate_key /etc/nginx/ssl/XXX/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5; ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m; ssl_session_cache shared:SSL:10m;