mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-08 17:26:46 +08:00
feat: add fav_fail_fallback configuration, implement UI option card styling, and refactor server settings update logic to handle dynamic restarts.
This commit is contained in:
+113
-32
@@ -177,7 +177,8 @@ def load_ui_config() -> dict[str, Any]:
|
|||||||
"routing_ip_type": "all",
|
"routing_ip_type": "all",
|
||||||
"connection_enabled": True,
|
"connection_enabled": True,
|
||||||
"fixed_node_id": "",
|
"fixed_node_id": "",
|
||||||
"favorite_node_ids": []
|
"favorite_node_ids": [],
|
||||||
|
"fav_fail_fallback": True
|
||||||
}
|
}
|
||||||
updated = False
|
updated = False
|
||||||
if auth_file.exists():
|
if auth_file.exists():
|
||||||
@@ -185,7 +186,7 @@ def load_ui_config() -> dict[str, Any]:
|
|||||||
data = json.loads(auth_file.read_text(encoding="utf-8"))
|
data = json.loads(auth_file.read_text(encoding="utf-8"))
|
||||||
for key, val in data.items():
|
for key, val in data.items():
|
||||||
config[key] = val
|
config[key] = val
|
||||||
for key in ["routing_mode", "force_country", "routing_ip_type", "connection_enabled", "fixed_node_id", "favorite_node_ids"]:
|
for key in ["routing_mode", "force_country", "routing_ip_type", "connection_enabled", "fixed_node_id", "favorite_node_ids", "fav_fail_fallback"]:
|
||||||
if key not in data:
|
if key not in data:
|
||||||
updated = True
|
updated = True
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -305,6 +306,7 @@ def get_state() -> dict[str, Any]:
|
|||||||
state["connection_enabled"] = ui_cfg.get("connection_enabled", True)
|
state["connection_enabled"] = ui_cfg.get("connection_enabled", True)
|
||||||
state["fixed_node_id"] = ui_cfg.get("fixed_node_id", "")
|
state["fixed_node_id"] = ui_cfg.get("fixed_node_id", "")
|
||||||
state["favorite_node_ids"] = ui_cfg.get("favorite_node_ids", [])
|
state["favorite_node_ids"] = ui_cfg.get("favorite_node_ids", [])
|
||||||
|
state["fav_fail_fallback"] = ui_cfg.get("fav_fail_fallback", True)
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@@ -1120,7 +1122,13 @@ def auto_switch_node(attempt: int = 0) -> None:
|
|||||||
]
|
]
|
||||||
if routing_mode == "favorites":
|
if routing_mode == "favorites":
|
||||||
fav_ids = set(ui_cfg.get("favorite_node_ids", []))
|
fav_ids = set(ui_cfg.get("favorite_node_ids", []))
|
||||||
candidates = [n for n in candidates if n.get("id") in fav_ids]
|
fav_candidates = [n for n in candidates if n.get("id") in fav_ids]
|
||||||
|
if fav_candidates:
|
||||||
|
candidates = fav_candidates
|
||||||
|
else:
|
||||||
|
fav_fail_fallback = ui_cfg.get("fav_fail_fallback", True)
|
||||||
|
if not fav_fail_fallback:
|
||||||
|
candidates = []
|
||||||
|
|
||||||
# Apply routing_ip_type filter
|
# Apply routing_ip_type filter
|
||||||
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
||||||
@@ -1415,7 +1423,13 @@ def maintain_valid_nodes(force: bool = False) -> str:
|
|||||||
]
|
]
|
||||||
elif routing_mode == "favorites":
|
elif routing_mode == "favorites":
|
||||||
fav_ids = set(ui_cfg.get("favorite_node_ids", []))
|
fav_ids = set(ui_cfg.get("favorite_node_ids", []))
|
||||||
available_candidates = [n for n in available_candidates if n.get("id") in fav_ids]
|
fav_candidates = [n for n in available_candidates if n.get("id") in fav_ids]
|
||||||
|
if fav_candidates:
|
||||||
|
available_candidates = fav_candidates
|
||||||
|
else:
|
||||||
|
fav_fail_fallback = ui_cfg.get("fav_fail_fallback", True)
|
||||||
|
if not fav_fail_fallback:
|
||||||
|
available_candidates = []
|
||||||
|
|
||||||
# Apply routing_ip_type filter for auto-connect
|
# Apply routing_ip_type filter for auto-connect
|
||||||
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
||||||
@@ -2620,6 +2634,57 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
background-color: #0f172a;
|
background-color: #0f172a;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Option Card Styles for Proxy/Routing Settings */
|
||||||
|
.option-group {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.option-group {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card {
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(99, 102, 241, 0.25);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card.active {
|
||||||
|
background: rgba(99, 102, 241, 0.08);
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 12px rgba(99, 102, 241, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-card-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -3899,7 +3964,7 @@ function populateRoutingCountries() {
|
|||||||
select.innerHTML = html;
|
select.innerHTML = html;
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
select.value = translateCountry(state.force_country) || "";
|
select.value = state.force_country ? translateCountry(state.force_country) : "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4845,21 +4910,53 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
|
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
|
||||||
new_username = str(payload.get("username") or "").strip()
|
new_username = str(payload.get("username") or "").strip()
|
||||||
new_password = str(payload.get("password") or "").strip()
|
new_password = str(payload.get("password") or "").strip()
|
||||||
|
new_port = payload.get("port")
|
||||||
|
new_suffix = str(payload.get("secret_path") or "").strip()
|
||||||
|
|
||||||
if not new_username or not new_password:
|
if not new_username or not new_password:
|
||||||
self.send_json({"ok": False, "error": "用户名和密码不能为空"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "用户名和密码不能为空"}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_port_int = int(new_port)
|
||||||
|
if not (1 <= new_port_int <= 65535):
|
||||||
|
raise ValueError()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
self.send_json({"ok": False, "error": "网页管理端口范围必须是 1 至 65535"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not new_suffix or not re.match(r"^[A-Za-z0-9]+$", new_suffix):
|
||||||
|
self.send_json({"ok": False, "error": "安全后缀仅能由英文字母和数字组成"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
|
expected_username = ui_cfg.get("username", "")
|
||||||
|
expected_password = ui_cfg.get("password", "")
|
||||||
|
expected_port = ui_cfg.get("port", 8787)
|
||||||
|
expected_suffix = ui_cfg.get("secret_path", "EJsW2EeBo9lY")
|
||||||
|
|
||||||
ui_cfg["username"] = new_username
|
ui_cfg["username"] = new_username
|
||||||
ui_cfg["password"] = new_password
|
ui_cfg["password"] = new_password
|
||||||
|
ui_cfg["port"] = new_port_int
|
||||||
|
ui_cfg["secret_path"] = new_suffix
|
||||||
|
|
||||||
auth_file = DATA_DIR / "ui_auth.json"
|
auth_file = DATA_DIR / "ui_auth.json"
|
||||||
with lock:
|
with lock:
|
||||||
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
auth_file.write_text(json.dumps(ui_cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
auth_file.write_text(json.dumps(ui_cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
self.send_json({"ok": True, "message": "账号密码配置更新成功,已即时生效!"})
|
restart_needed = (new_port_int != expected_port or new_suffix != expected_suffix)
|
||||||
|
if restart_needed:
|
||||||
|
self.send_json({"ok": True, "restart_needed": True, "message": "配置更新成功,网页管理端口或路径已变更,将在 2 秒内重启..."})
|
||||||
|
|
||||||
|
def restart_server():
|
||||||
|
time.sleep(2)
|
||||||
|
print("[系统] 管理后台安全配置更新,进程即将退出以触发自动重启...", flush=True)
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
threading.Thread(target=restart_server, daemon=True).start()
|
||||||
|
else:
|
||||||
|
self.send_json({"ok": True, "restart_needed": False, "message": "账号密码配置更新成功,已即时生效!"})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.send_json({"ok": False, "error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
|
self.send_json({"ok": False, "error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
return
|
return
|
||||||
@@ -4869,21 +4966,11 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
length = parse_int(self.headers.get("Content-Length"))
|
length = parse_int(self.headers.get("Content-Length"))
|
||||||
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
|
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
|
||||||
|
|
||||||
new_port = payload.get("port")
|
|
||||||
new_suffix = str(payload.get("secret_path") or "").strip()
|
|
||||||
new_proxy_port = payload.get("proxy_port")
|
new_proxy_port = payload.get("proxy_port")
|
||||||
routing_mode = str(payload.get("routing_mode") or "auto").strip()
|
routing_mode = str(payload.get("routing_mode") or "auto").strip()
|
||||||
force_country = str(payload.get("force_country") or "").strip()
|
force_country = str(payload.get("force_country") or "").strip()
|
||||||
routing_ip_type = str(payload.get("routing_ip_type") or "all").strip()
|
routing_ip_type = str(payload.get("routing_ip_type") or "all").strip()
|
||||||
|
|
||||||
try:
|
|
||||||
new_port_int = int(new_port)
|
|
||||||
if not (1 <= new_port_int <= 65535):
|
|
||||||
raise ValueError()
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
self.send_json({"ok": False, "error": "端口范围必须是 1 至 65535"}, HTTPStatus.BAD_REQUEST)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_proxy_port_int = int(new_proxy_port)
|
new_proxy_port_int = int(new_proxy_port)
|
||||||
if not (1024 <= new_proxy_port_int <= 65535):
|
if not (1024 <= new_proxy_port_int <= 65535):
|
||||||
@@ -4892,15 +4979,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_json({"ok": False, "error": "代理出站端口范围必须是 1024 至 65535"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "代理出站端口范围必须是 1024 至 65535"}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_proxy_port_int == new_port_int:
|
if routing_mode not in ("auto", "fixed_ip", "fixed_region", "favorites"):
|
||||||
self.send_json({"ok": False, "error": "代理出站端口不能与网页管理端口相同"}, HTTPStatus.BAD_REQUEST)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not new_suffix or not re.match(r"^[A-Za-z0-9]+$", new_suffix):
|
|
||||||
self.send_json({"ok": False, "error": "安全后缀仅能由英文字母和数字组成"}, HTTPStatus.BAD_REQUEST)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (routing_mode not in ("auto", "fixed_ip", "fixed_region", "favorites")):
|
|
||||||
self.send_json({"ok": False, "error": "无效的路由配置模式"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "无效的路由配置模式"}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
if routing_ip_type not in ("all", "residential", "hosting"):
|
if routing_ip_type not in ("all", "residential", "hosting"):
|
||||||
@@ -4908,12 +4987,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
expected_port = ui_cfg.get("port", 8787)
|
|
||||||
expected_suffix = ui_cfg.get("secret_path", "EJsW2EeBo9lY")
|
|
||||||
expected_proxy_port = ui_cfg.get("proxy_port", 7928)
|
expected_proxy_port = ui_cfg.get("proxy_port", 7928)
|
||||||
|
|
||||||
ui_cfg["port"] = new_port_int
|
if new_proxy_port_int == ui_cfg.get("port", 8787):
|
||||||
ui_cfg["secret_path"] = new_suffix
|
self.send_json({"ok": False, "error": "代理出站端口不能与网页管理端口相同"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
|
||||||
ui_cfg["proxy_port"] = new_proxy_port_int
|
ui_cfg["proxy_port"] = new_proxy_port_int
|
||||||
ui_cfg["routing_mode"] = routing_mode
|
ui_cfg["routing_mode"] = routing_mode
|
||||||
ui_cfg["force_country"] = force_country
|
ui_cfg["force_country"] = force_country
|
||||||
@@ -4924,13 +5003,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
auth_file.write_text(json.dumps(ui_cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
auth_file.write_text(json.dumps(ui_cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
restart_needed = (new_port_int != expected_port or new_suffix != expected_suffix or new_proxy_port_int != expected_proxy_port)
|
restart_needed = (new_proxy_port_int != expected_proxy_port)
|
||||||
if restart_needed:
|
if restart_needed:
|
||||||
self.send_json({"ok": True, "restart_needed": True, "message": "配置更新成功,系统及网页端口或后缀变更,将在 2 秒内重启..."})
|
self.send_json({"ok": True, "restart_needed": True, "message": "配置更新成功,代理出站端口变更,将在 2 秒内重启..."})
|
||||||
|
|
||||||
def restart_server():
|
def restart_server():
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
print("[系统] 管理后台配置更新,进程即将退出以触发自动重启...", flush=True)
|
print("[系统] 代理出站端口变更,进程即将退出以触发自动重启...", flush=True)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
threading.Thread(target=restart_server, daemon=True).start()
|
threading.Thread(target=restart_server, daemon=True).start()
|
||||||
@@ -4947,6 +5026,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
routing_mode = str(payload.get("routing_mode") or "auto").strip()
|
routing_mode = str(payload.get("routing_mode") or "auto").strip()
|
||||||
force_country = str(payload.get("force_country") or "").strip()
|
force_country = str(payload.get("force_country") or "").strip()
|
||||||
routing_ip_type = str(payload.get("routing_ip_type") or "all").strip()
|
routing_ip_type = str(payload.get("routing_ip_type") or "all").strip()
|
||||||
|
fav_fail_fallback = bool(payload.get("fav_fail_fallback", True))
|
||||||
|
|
||||||
if routing_mode not in ("auto", "fixed_ip", "fixed_region", "favorites"):
|
if routing_mode not in ("auto", "fixed_ip", "fixed_region", "favorites"):
|
||||||
self.send_json({"ok": False, "error": "无效的路由配置模式"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "无效的路由配置模式"}, HTTPStatus.BAD_REQUEST)
|
||||||
@@ -4959,6 +5039,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
ui_cfg["routing_mode"] = routing_mode
|
ui_cfg["routing_mode"] = routing_mode
|
||||||
ui_cfg["force_country"] = force_country
|
ui_cfg["force_country"] = force_country
|
||||||
ui_cfg["routing_ip_type"] = routing_ip_type
|
ui_cfg["routing_ip_type"] = routing_ip_type
|
||||||
|
ui_cfg["fav_fail_fallback"] = fav_fail_fallback
|
||||||
ui_cfg.pop("enable_force_country", None)
|
ui_cfg.pop("enable_force_country", None)
|
||||||
|
|
||||||
auth_file = DATA_DIR / "ui_auth.json"
|
auth_file = DATA_DIR / "ui_auth.json"
|
||||||
|
|||||||
Reference in New Issue
Block a user