mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-08 09:17:03 +08:00
Tighten routing filters and speed up initial node connect
This commit is contained in:
+318
-134
@@ -110,6 +110,8 @@ CHECK_INTERVAL_SECONDS = env_int("CHECK_INTERVAL_SECONDS", 1260, 1)
|
|||||||
TARGET_VALID_NODES = env_int("TARGET_VALID_NODES", 3, 1)
|
TARGET_VALID_NODES = env_int("TARGET_VALID_NODES", 3, 1)
|
||||||
MAX_SCAN_ROWS = env_int("MAX_SCAN_ROWS", 300, 1)
|
MAX_SCAN_ROWS = env_int("MAX_SCAN_ROWS", 300, 1)
|
||||||
OPENVPN_TEST_TIMEOUT_SECONDS = env_int("OPENVPN_TEST_TIMEOUT_SECONDS", 35, 1)
|
OPENVPN_TEST_TIMEOUT_SECONDS = env_int("OPENVPN_TEST_TIMEOUT_SECONDS", 35, 1)
|
||||||
|
MANUAL_TEST_NODE_LIMIT = env_int("MANUAL_TEST_NODE_LIMIT", 5, 1, 20)
|
||||||
|
INITIAL_CONNECT_TEST_LIMIT = env_int("INITIAL_CONNECT_TEST_LIMIT", 10, 1, 50)
|
||||||
OPENVPN_CMD = os.environ.get("OPENVPN_CMD", "openvpn")
|
OPENVPN_CMD = os.environ.get("OPENVPN_CMD", "openvpn")
|
||||||
OPENVPN_AUTH_USER = os.environ.get("OPENVPN_AUTH_USER", "vpn")
|
OPENVPN_AUTH_USER = os.environ.get("OPENVPN_AUTH_USER", "vpn")
|
||||||
OPENVPN_AUTH_PASS = os.environ.get("OPENVPN_AUTH_PASS", "vpn")
|
OPENVPN_AUTH_PASS = os.environ.get("OPENVPN_AUTH_PASS", "vpn")
|
||||||
@@ -133,7 +135,7 @@ maintenance_lock = threading.Lock()
|
|||||||
active_sessions: dict[str, float] = {}
|
active_sessions: dict[str, float] = {}
|
||||||
active_openvpn_process: subprocess.Popen[str] | None = None
|
active_openvpn_process: subprocess.Popen[str] | None = None
|
||||||
active_openvpn_node_id = ""
|
active_openvpn_node_id = ""
|
||||||
is_connecting = True
|
is_connecting = False
|
||||||
last_active_ping_time = 0.0
|
last_active_ping_time = 0.0
|
||||||
last_active_latency = 0
|
last_active_latency = 0
|
||||||
|
|
||||||
@@ -225,7 +227,7 @@ def load_ui_config() -> dict[str, Any]:
|
|||||||
"connection_enabled": True,
|
"connection_enabled": True,
|
||||||
"fixed_node_id": "",
|
"fixed_node_id": "",
|
||||||
"favorite_node_ids": [],
|
"favorite_node_ids": [],
|
||||||
"fav_fail_fallback": True
|
"fav_fail_fallback": False
|
||||||
}
|
}
|
||||||
updated = False
|
updated = False
|
||||||
if auth_file.exists():
|
if auth_file.exists():
|
||||||
@@ -265,7 +267,7 @@ def load_ui_config() -> dict[str, Any]:
|
|||||||
if not auth_file.exists() or updated:
|
if not auth_file.exists() or updated:
|
||||||
try:
|
try:
|
||||||
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
auth_file.write_text(json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8")
|
write_json(auth_file, config)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -376,7 +378,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)
|
state["fav_fail_fallback"] = False
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@@ -1169,6 +1171,146 @@ def sort_all_nodes(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
)
|
)
|
||||||
return available_nodes + untested_nodes + unavailable_nodes
|
return available_nodes + untested_nodes + unavailable_nodes
|
||||||
|
|
||||||
|
def apply_routing_filters(
|
||||||
|
nodes: list[dict[str, Any]],
|
||||||
|
ui_cfg: dict[str, Any],
|
||||||
|
include_unknown_ip_type: bool = False,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
candidates = list(nodes)
|
||||||
|
routing_mode = ui_cfg.get("routing_mode", "auto")
|
||||||
|
target_country = ui_cfg.get("force_country", "")
|
||||||
|
|
||||||
|
if routing_mode == "fixed_region" and target_country:
|
||||||
|
candidates = [
|
||||||
|
n for n in candidates
|
||||||
|
if country_matches(n.get("country"), target_country)
|
||||||
|
]
|
||||||
|
elif routing_mode == "favorites":
|
||||||
|
fav_ids = set(ui_cfg.get("favorite_node_ids", []))
|
||||||
|
candidates = [n for n in candidates if n.get("id") in fav_ids]
|
||||||
|
|
||||||
|
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
||||||
|
if routing_ip_type == "residential":
|
||||||
|
candidates = [
|
||||||
|
n for n in candidates
|
||||||
|
if n.get("ip_type") in ("residential", "mobile")
|
||||||
|
or (include_unknown_ip_type and not n.get("ip_type"))
|
||||||
|
]
|
||||||
|
elif routing_ip_type == "hosting":
|
||||||
|
candidates = [
|
||||||
|
n for n in candidates
|
||||||
|
if n.get("ip_type") == "hosting"
|
||||||
|
or (include_unknown_ip_type and not n.get("ip_type"))
|
||||||
|
]
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
def normalized_country_name(country: Any) -> str:
|
||||||
|
value = str(country or "").strip()
|
||||||
|
return vpn_utils.COUNTRY_TRANSLATIONS.get(value, value)
|
||||||
|
|
||||||
|
def country_matches(node_country: Any, target_country: Any) -> bool:
|
||||||
|
return bool(target_country) and normalized_country_name(node_country) == normalized_country_name(target_country)
|
||||||
|
|
||||||
|
def probe_priority_key(node: dict[str, Any]) -> tuple[int, int, int, int]:
|
||||||
|
ping = parse_int(node.get("ping")) or 999999
|
||||||
|
return (
|
||||||
|
ping,
|
||||||
|
-parse_int(node.get("score")),
|
||||||
|
-parse_int(node.get("speed")),
|
||||||
|
parse_int(node.get("sessions")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def current_fixed_node_id(ui_cfg: dict[str, Any]) -> str:
|
||||||
|
if active_openvpn_node_id:
|
||||||
|
return active_openvpn_node_id
|
||||||
|
nodes = read_nodes()
|
||||||
|
active_node = next((n for n in nodes if n.get("active") and n.get("id")), None)
|
||||||
|
if active_node:
|
||||||
|
return str(active_node.get("id") or "")
|
||||||
|
return str(ui_cfg.get("fixed_node_id") or "").strip()
|
||||||
|
|
||||||
|
def validate_node_allowed_by_routing(node: dict[str, Any], ui_cfg: dict[str, Any]) -> None:
|
||||||
|
routing_mode = ui_cfg.get("routing_mode", "auto")
|
||||||
|
node_id = str(node.get("id") or "")
|
||||||
|
|
||||||
|
if routing_mode == "fixed_region":
|
||||||
|
target_country = ui_cfg.get("force_country", "")
|
||||||
|
if target_country and not country_matches(node.get("country"), target_country):
|
||||||
|
raise RuntimeError(f"当前已锁定国家【{target_country}】,不能连接其他国家节点")
|
||||||
|
elif routing_mode == "favorites":
|
||||||
|
fav_ids = set(ui_cfg.get("favorite_node_ids", []))
|
||||||
|
if node_id not in fav_ids:
|
||||||
|
raise RuntimeError("当前处于仅用收藏模式,不能连接未收藏节点")
|
||||||
|
|
||||||
|
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
||||||
|
node_ip_type = node.get("ip_type")
|
||||||
|
if routing_ip_type == "residential" and node_ip_type not in ("residential", "mobile"):
|
||||||
|
raise RuntimeError("当前已锁定住宅 IP 出站,不能连接非住宅节点")
|
||||||
|
if routing_ip_type == "hosting" and node_ip_type != "hosting":
|
||||||
|
raise RuntimeError("当前已锁定机房 IP 出站,不能连接非机房节点")
|
||||||
|
|
||||||
|
def enforce_active_node_allowed_by_routing(ui_cfg: dict[str, Any], reason: str = "路由规则已更新") -> str | None:
|
||||||
|
active_id = active_openvpn_node_id
|
||||||
|
if not active_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
nodes = read_nodes()
|
||||||
|
active_node = next((item for item in nodes if item.get("id") == active_id), None)
|
||||||
|
if not active_node:
|
||||||
|
clear_active_connection_state(f"{reason},当前活动节点已不在节点列表中,已断开连接")
|
||||||
|
return "当前活动节点已不在节点列表中,已断开连接"
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_node_allowed_by_routing(active_node, ui_cfg)
|
||||||
|
return None
|
||||||
|
except Exception as exc:
|
||||||
|
msg = f"{reason},当前活动节点 {active_id} 不符合新规则,已断开连接: {exc}"
|
||||||
|
print(f"[路由规则] {msg}", flush=True)
|
||||||
|
log_to_json("WARNING", "Routing", msg)
|
||||||
|
stop_active_openvpn()
|
||||||
|
with lock:
|
||||||
|
nodes = read_nodes()
|
||||||
|
for item in nodes:
|
||||||
|
item["active"] = False
|
||||||
|
write_json(NODES_FILE, nodes)
|
||||||
|
set_state(
|
||||||
|
active_openvpn_node_id="",
|
||||||
|
active_node_latency="无活动连接",
|
||||||
|
proxy_ok=False,
|
||||||
|
proxy_ip="-",
|
||||||
|
proxy_latency_ms=0,
|
||||||
|
proxy_error=msg,
|
||||||
|
last_check_message=msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ui_cfg.get("connection_enabled", True) and ui_cfg.get("routing_mode") != "fixed_ip":
|
||||||
|
threading.Thread(target=auto_switch_node, daemon=True).start()
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def reconnect_fixed_node_if_needed(ui_cfg: dict[str, Any]) -> bool:
|
||||||
|
global is_connecting
|
||||||
|
if ui_cfg.get("routing_mode") != "fixed_ip" or active_openvpn_running():
|
||||||
|
return False
|
||||||
|
target_id = current_fixed_node_id(ui_cfg)
|
||||||
|
if not target_id:
|
||||||
|
return False
|
||||||
|
nodes = read_nodes()
|
||||||
|
if not any(n.get("id") == target_id for n in nodes):
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"[维护线程] 固定 IP 模式下 OpenVPN 未运行,正在重新拉起同一节点: {target_id}", flush=True)
|
||||||
|
previous_connecting = is_connecting
|
||||||
|
is_connecting = False
|
||||||
|
try:
|
||||||
|
connect_node(target_id)
|
||||||
|
return active_openvpn_running()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[维护线程] 重新拉起固定节点 {target_id} 失败: {e}", flush=True)
|
||||||
|
return False
|
||||||
|
finally:
|
||||||
|
is_connecting = previous_connecting
|
||||||
|
|
||||||
active_test_indexes = set()
|
active_test_indexes = set()
|
||||||
test_indexes_lock = threading.Lock()
|
test_indexes_lock = threading.Lock()
|
||||||
|
|
||||||
@@ -1386,29 +1528,7 @@ def auto_switch_node(attempt: int = 0) -> None:
|
|||||||
if n.get("probe_status") == "available"
|
if n.get("probe_status") == "available"
|
||||||
and not n.get("active")
|
and not n.get("active")
|
||||||
]
|
]
|
||||||
|
candidates = apply_routing_filters(candidates, ui_cfg)
|
||||||
if routing_mode == "fixed_region" and target_country:
|
|
||||||
candidates = [
|
|
||||||
n for n in candidates
|
|
||||||
if n.get("country") == target_country
|
|
||||||
or vpn_utils.COUNTRY_TRANSLATIONS.get(n.get("country", ""), n.get("country", "")) == target_country
|
|
||||||
]
|
|
||||||
if routing_mode == "favorites":
|
|
||||||
fav_ids = set(ui_cfg.get("favorite_node_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
|
|
||||||
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
|
||||||
if routing_ip_type == "residential":
|
|
||||||
candidates = [n for n in candidates if n.get("ip_type") in ("residential", "mobile")]
|
|
||||||
elif routing_ip_type == "hosting":
|
|
||||||
candidates = [n for n in candidates if n.get("ip_type") == "hosting"]
|
|
||||||
|
|
||||||
candidates.sort(key=lambda n: (parse_int(n.get("latency_ms")) or 999999, -parse_int(n.get("score"))))
|
candidates.sort(key=lambda n: (parse_int(n.get("latency_ms")) or 999999, -parse_int(n.get("score"))))
|
||||||
|
|
||||||
@@ -1440,8 +1560,10 @@ def auto_switch_node(attempt: int = 0) -> None:
|
|||||||
|
|
||||||
def bg_fetch_and_switch():
|
def bg_fetch_and_switch():
|
||||||
try:
|
try:
|
||||||
|
# 避免所有节点不可用时连续拉取/测试导致 CPU 与 tun 网卡风暴。
|
||||||
|
time.sleep(60)
|
||||||
maintain_valid_nodes(force=False)
|
maintain_valid_nodes(force=False)
|
||||||
auto_switch_node()
|
auto_switch_node(attempt + 1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[自动切换后台补齐] 获取并测试节点失败: {e}", flush=True)
|
print(f"[自动切换后台补齐] 获取并测试节点失败: {e}", flush=True)
|
||||||
|
|
||||||
@@ -1469,13 +1591,14 @@ def connect_node(node_id: str) -> str:
|
|||||||
raise ValueError(f"Node not found: {node_id}")
|
raise ValueError(f"Node not found: {node_id}")
|
||||||
|
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
|
validate_node_allowed_by_routing(node, ui_cfg)
|
||||||
ui_cfg["connection_enabled"] = True
|
ui_cfg["connection_enabled"] = True
|
||||||
if ui_cfg.get("routing_mode") == "fixed_ip":
|
if ui_cfg.get("routing_mode") == "fixed_ip":
|
||||||
ui_cfg["fixed_node_id"] = node_id
|
ui_cfg["fixed_node_id"] = node_id
|
||||||
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")
|
write_json(auth_file, ui_cfg)
|
||||||
|
|
||||||
set_state(active_node_latency="清理连接", last_check_message="正在关闭与清理旧的 VPN 连接及网卡...")
|
set_state(active_node_latency="清理连接", last_check_message="正在关闭与清理旧的 VPN 连接及网卡...")
|
||||||
stop_active_openvpn()
|
stop_active_openvpn()
|
||||||
@@ -1576,28 +1699,25 @@ def maintain_valid_nodes(force: bool = False) -> str:
|
|||||||
msg = "节点维护任务正在运行,请稍后再试"
|
msg = "节点维护任务正在运行,请稍后再试"
|
||||||
set_state(last_check_message=msg)
|
set_state(last_check_message=msg)
|
||||||
return msg
|
return msg
|
||||||
is_connecting = True
|
with lock:
|
||||||
|
if is_connecting:
|
||||||
|
maintenance_lock.release()
|
||||||
|
msg = "当前已有连接或节点测试任务正在运行,请稍后再试"
|
||||||
|
set_state(last_check_message=msg)
|
||||||
|
return msg
|
||||||
|
is_connecting = True
|
||||||
try:
|
try:
|
||||||
if force:
|
if force:
|
||||||
with lock:
|
with lock:
|
||||||
stop_active_openvpn()
|
stop_active_openvpn()
|
||||||
|
reconnect_fixed_node_if_needed(load_ui_config())
|
||||||
elif not active_openvpn_running():
|
elif not active_openvpn_running():
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
routing_mode = ui_cfg.get("routing_mode", "auto")
|
routing_mode = ui_cfg.get("routing_mode", "auto")
|
||||||
connection_enabled = ui_cfg.get("connection_enabled", True)
|
connection_enabled = ui_cfg.get("connection_enabled", True)
|
||||||
if connection_enabled:
|
if connection_enabled:
|
||||||
if routing_mode == "fixed_ip":
|
if routing_mode == "fixed_ip":
|
||||||
target_id = active_openvpn_node_id or ui_cfg.get("fixed_node_id", "")
|
reconnect_fixed_node_if_needed(ui_cfg)
|
||||||
if target_id:
|
|
||||||
nodes = read_nodes()
|
|
||||||
if any(n.get("id") == target_id for n in nodes):
|
|
||||||
print(f"[维护线程] 检测到固定 IP 模式下 OpenVPN 未运行,正在重新拉起同一节点: {target_id}", flush=True)
|
|
||||||
is_connecting = False
|
|
||||||
try:
|
|
||||||
connect_node(target_id)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[维护线程] 重新拉起固定节点 {target_id} 失败: {e}", flush=True)
|
|
||||||
is_connecting = True
|
|
||||||
else:
|
else:
|
||||||
has_active_id = False
|
has_active_id = False
|
||||||
with lock:
|
with lock:
|
||||||
@@ -1656,10 +1776,66 @@ def maintain_valid_nodes(force: bool = False) -> str:
|
|||||||
|
|
||||||
write_json(NODES_FILE, merged)
|
write_json(NODES_FILE, merged)
|
||||||
|
|
||||||
# Test all non-active nodes from the list
|
initial_tested_ids: set[str] = set()
|
||||||
|
ui_cfg = load_ui_config()
|
||||||
|
should_fast_connect = (
|
||||||
|
ui_cfg.get("connection_enabled", True)
|
||||||
|
and ui_cfg.get("routing_mode", "auto") != "fixed_ip"
|
||||||
|
and not active_openvpn_running()
|
||||||
|
)
|
||||||
|
if should_fast_connect:
|
||||||
|
with lock:
|
||||||
|
current_nodes = read_nodes()
|
||||||
|
fast_candidates = [
|
||||||
|
n for n in current_nodes
|
||||||
|
if not n.get("active") and n.get("probe_status") != "unavailable"
|
||||||
|
]
|
||||||
|
fast_candidates = apply_routing_filters(fast_candidates, ui_cfg, include_unknown_ip_type=True)
|
||||||
|
fast_candidates.sort(key=probe_priority_key)
|
||||||
|
fast_test_ids = [
|
||||||
|
n["id"] for n in fast_candidates
|
||||||
|
if n.get("id")
|
||||||
|
][:INITIAL_CONNECT_TEST_LIMIT]
|
||||||
|
|
||||||
|
if fast_test_ids:
|
||||||
|
initial_tested_ids = set(fast_test_ids)
|
||||||
|
msg = f"首次快速连接模式:优先测试 {len(fast_test_ids)} 个高优先级节点,发现可用节点后立即连接"
|
||||||
|
print(f"[快速首连] {msg}", flush=True)
|
||||||
|
log_to_json("INFO", "Main", msg)
|
||||||
|
set_state(is_connecting=True, last_check_message=msg)
|
||||||
|
test_multiple_nodes(fast_test_ids)
|
||||||
|
|
||||||
|
with lock:
|
||||||
|
fast_nodes = read_nodes()
|
||||||
|
available_candidates = [
|
||||||
|
n for n in fast_nodes
|
||||||
|
if n.get("probe_status") == "available" and not n.get("active")
|
||||||
|
]
|
||||||
|
available_candidates = apply_routing_filters(available_candidates, ui_cfg)
|
||||||
|
|
||||||
|
if available_candidates:
|
||||||
|
is_connecting = False
|
||||||
|
set_state(is_connecting=False, last_check_message="快速首连已找到可用节点,正在建立连接...")
|
||||||
|
auto_switch_node()
|
||||||
|
if active_openvpn_running():
|
||||||
|
valid_nodes_count = len([n for n in read_nodes() if n.get("probe_status") == "available"])
|
||||||
|
message = f"Fetched {len(candidates)} nodes. Fast-tested {len(fast_test_ids)} nodes and connected."
|
||||||
|
set_state(
|
||||||
|
last_check_at=time.time(),
|
||||||
|
last_check_message=message,
|
||||||
|
active_openvpn_node_id=active_openvpn_node_id,
|
||||||
|
valid_nodes=valid_nodes_count,
|
||||||
|
)
|
||||||
|
return message
|
||||||
|
is_connecting = True
|
||||||
|
|
||||||
|
# Test remaining non-active nodes from the list
|
||||||
with lock:
|
with lock:
|
||||||
current_nodes = read_nodes()
|
current_nodes = read_nodes()
|
||||||
to_test = [n for n in current_nodes if not n.get("active")]
|
to_test = [
|
||||||
|
n for n in current_nodes
|
||||||
|
if not n.get("active") and n.get("id") not in initial_tested_ids
|
||||||
|
]
|
||||||
to_test_ids = [n["id"] for n in to_test]
|
to_test_ids = [n["id"] for n in to_test]
|
||||||
|
|
||||||
msg = f"开始对列表中所有候选节点进行周期连通性与延迟测试,待检测节点共 {len(to_test_ids)} 个"
|
msg = f"开始对列表中所有候选节点进行周期连通性与延迟测试,待检测节点共 {len(to_test_ids)} 个"
|
||||||
@@ -1697,32 +1873,10 @@ def maintain_valid_nodes(force: bool = False) -> str:
|
|||||||
connection_enabled = ui_cfg.get("connection_enabled", True)
|
connection_enabled = ui_cfg.get("connection_enabled", True)
|
||||||
if connection_enabled:
|
if connection_enabled:
|
||||||
routing_mode = ui_cfg.get("routing_mode", "auto")
|
routing_mode = ui_cfg.get("routing_mode", "auto")
|
||||||
target_country = ui_cfg.get("force_country", "")
|
|
||||||
|
|
||||||
if routing_mode != "fixed_ip":
|
if routing_mode != "fixed_ip":
|
||||||
available_candidates = [n for n in merged if n.get("probe_status") == "available"]
|
available_candidates = [n for n in merged if n.get("probe_status") == "available"]
|
||||||
if routing_mode == "fixed_region" and target_country:
|
available_candidates = apply_routing_filters(available_candidates, ui_cfg)
|
||||||
available_candidates = [
|
|
||||||
n for n in available_candidates
|
|
||||||
if n.get("country") == target_country
|
|
||||||
or vpn_utils.COUNTRY_TRANSLATIONS.get(n.get("country", ""), n.get("country", "")) == target_country
|
|
||||||
]
|
|
||||||
elif routing_mode == "favorites":
|
|
||||||
fav_ids = set(ui_cfg.get("favorite_node_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
|
|
||||||
routing_ip_type = ui_cfg.get("routing_ip_type", "all")
|
|
||||||
if routing_ip_type == "residential":
|
|
||||||
available_candidates = [n for n in available_candidates if n.get("ip_type") in ("residential", "mobile")]
|
|
||||||
elif routing_ip_type == "hosting":
|
|
||||||
available_candidates = [n for n in available_candidates if n.get("ip_type") == "hosting"]
|
|
||||||
|
|
||||||
if available_candidates:
|
if available_candidates:
|
||||||
auto_switch_node()
|
auto_switch_node()
|
||||||
@@ -3053,15 +3207,8 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="border-top: 1px solid rgba(255,255,255,0.06); padding-top: 16px;">
|
<div style="border-top: 1px solid rgba(255,255,255,0.06); padding-top: 16px;">
|
||||||
<label style="display: flex; align-items: flex-start; gap: 10px; cursor: pointer; user-select: none;">
|
<div style="padding: 10px 14px; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.25); border-radius: 8px; font-size: 12px; color: var(--warning); line-height: 1.5;">
|
||||||
<input type="checkbox" id="fav_fail_fallback_checkbox" style="margin-top: 3px; cursor: pointer;" onchange="handleFavFallbackChange(this.checked)" checked />
|
<strong>仅用收藏是强锁定模式。</strong>开启后只会连接收藏节点;如果收藏节点全部不可用,系统不会切换到非收藏节点。
|
||||||
<div style="display: flex; flex-direction: column; gap: 2px;">
|
|
||||||
<span style="font-size: 14px; font-weight: 500; color: var(--text-primary);">收藏节点失效后自动切换其他(非收藏)可用节点</span>
|
|
||||||
<span style="font-size: 12px; color: var(--text-secondary);">勾选此项,当所有收藏节点不可用时,系统将自动使用其他最快的非收藏可用节点,保障网络连接不中断。</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<div id="fav_fallback_warning" style="display: none; margin-top: 12px; padding: 10px 14px; background: rgba(244, 63, 94, 0.1); border: 1px solid rgba(244, 63, 94, 0.25); border-radius: 8px; font-size: 12px; color: var(--danger); line-height: 1.4; animation: modalFadeIn 0.2s ease-out;">
|
|
||||||
⚠️ <strong>警告</strong>:您已取消勾选此项。如果当前收藏的节点均不可用,系统将<strong>无法切换</strong>到其他可用节点,可能导致网络<strong>彻底断开连接</strong>!
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4027,16 +4174,6 @@ function updateFavPanelUI() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showFavoritesOnly && state) {
|
if (showFavoritesOnly && state) {
|
||||||
const fallbackCheckbox = $("fav_fail_fallback_checkbox");
|
|
||||||
if (fallbackCheckbox) {
|
|
||||||
fallbackCheckbox.checked = !!state.fav_fail_fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
const warningDiv = $("fav_fallback_warning");
|
|
||||||
if (warningDiv) {
|
|
||||||
warningDiv.style.display = state.fav_fail_fallback ? "none" : "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
const favRoutingBtn = $("btn_toggle_fav_routing");
|
const favRoutingBtn = $("btn_toggle_fav_routing");
|
||||||
if (favRoutingBtn) {
|
if (favRoutingBtn) {
|
||||||
if (state.routing_mode === "favorites") {
|
if (state.routing_mode === "favorites") {
|
||||||
@@ -4070,8 +4207,7 @@ async function toggleFavRouting() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
routing_mode: newMode,
|
routing_mode: newMode,
|
||||||
force_country: state.force_country || "",
|
force_country: state.force_country || "",
|
||||||
routing_ip_type: state.routing_ip_type || "all",
|
routing_ip_type: state.routing_ip_type || "all"
|
||||||
fav_fail_fallback: state.fav_fail_fallback !== false
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -4087,40 +4223,6 @@ async function toggleFavRouting() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFavFallbackChange(checked) {
|
|
||||||
if (!state) return;
|
|
||||||
|
|
||||||
if (!checked) {
|
|
||||||
alert("⚠️ 警告:不勾选此项可能在所有收藏节点失效时造成网络彻底断开连接,无法自动切换到其他非收藏的可用节点!");
|
|
||||||
}
|
|
||||||
|
|
||||||
state.fav_fail_fallback = checked;
|
|
||||||
updateFavPanelUI();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch("./api/update_routing", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({
|
|
||||||
routing_mode: state.routing_mode || "auto",
|
|
||||||
force_country: state.force_country || "",
|
|
||||||
routing_ip_type: state.routing_ip_type || "all",
|
|
||||||
fav_fail_fallback: checked
|
|
||||||
})
|
|
||||||
});
|
|
||||||
const data = await res.json();
|
|
||||||
if (res.ok && data.ok) {
|
|
||||||
load();
|
|
||||||
} else {
|
|
||||||
alert("更新失败: " + (data.error || "未知错误"));
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert("连接服务器失败,请稍后重试");
|
|
||||||
load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectOptionCard(groupName, value) {
|
function selectOptionCard(groupName, value) {
|
||||||
if (groupName === 'routing_mode') {
|
if (groupName === 'routing_mode') {
|
||||||
const input = $("net_routing_mode");
|
const input = $("net_routing_mode");
|
||||||
@@ -4378,6 +4480,11 @@ async function saveNetwork(e) {
|
|||||||
errorDivEl.style.display = "block";
|
errorDivEl.style.display = "block";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (routingMode === "fixed_ip" && !(state && (state.active_openvpn_node_id || state.fixed_node_id))) {
|
||||||
|
errorDivEl.textContent = "启用固定 IP 前,请先连接一个要锁定的节点";
|
||||||
|
errorDivEl.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
submitBtn.disabled = true;
|
submitBtn.disabled = true;
|
||||||
submitBtn.textContent = "正在保存...";
|
submitBtn.textContent = "正在保存...";
|
||||||
@@ -5115,6 +5222,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_json({"error": "not found"}, HTTPStatus.NOT_FOUND)
|
self.send_json({"error": "not found"}, HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
|
global is_connecting
|
||||||
effective_path = self.validate_path()
|
effective_path = self.validate_path()
|
||||||
if effective_path == "": return
|
if effective_path == "": return
|
||||||
|
|
||||||
@@ -5220,7 +5328,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
reauth_required = new_username != expected_username or (new_password and new_password != expected_password)
|
reauth_required = new_username != expected_username or (new_password and new_password != expected_password)
|
||||||
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")
|
write_json(auth_file, ui_cfg)
|
||||||
if reauth_required:
|
if reauth_required:
|
||||||
active_sessions.clear()
|
active_sessions.clear()
|
||||||
|
|
||||||
@@ -5260,26 +5368,39 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
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)
|
||||||
return
|
return
|
||||||
|
if routing_mode == "fixed_region" and not force_country:
|
||||||
|
self.send_json({"ok": False, "error": "启用固定地区前,请先选择一个要锁定的国家"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
if routing_ip_type not in ("all", "residential", "hosting"):
|
if routing_ip_type not in ("all", "residential", "hosting"):
|
||||||
self.send_json({"ok": False, "error": "无效的IP出站类型过滤"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "无效的IP出站类型过滤"}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
expected_proxy_port = ui_cfg.get("proxy_port", 7928)
|
expected_proxy_port = ui_cfg.get("proxy_port", 7928)
|
||||||
|
fixed_node_id = current_fixed_node_id(ui_cfg) if routing_mode == "fixed_ip" else ""
|
||||||
|
|
||||||
if new_proxy_port_int == ui_cfg.get("port", 8787):
|
if new_proxy_port_int == ui_cfg.get("port", 8787):
|
||||||
self.send_json({"ok": False, "error": "代理出站端口不能与网页管理端口相同"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "代理出站端口不能与网页管理端口相同"}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
if routing_mode == "fixed_ip" and not fixed_node_id:
|
||||||
|
self.send_json({"ok": False, "error": "启用固定 IP 前,请先连接一个要锁定的节点"}, 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
|
||||||
ui_cfg["routing_ip_type"] = routing_ip_type
|
ui_cfg["routing_ip_type"] = routing_ip_type
|
||||||
|
if routing_mode == "favorites":
|
||||||
|
ui_cfg["fav_fail_fallback"] = False
|
||||||
|
if routing_mode == "fixed_ip":
|
||||||
|
ui_cfg["fixed_node_id"] = fixed_node_id
|
||||||
|
|
||||||
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")
|
write_json(auth_file, ui_cfg)
|
||||||
|
|
||||||
|
policy_message = enforce_active_node_allowed_by_routing(ui_cfg, "路由设置已更新")
|
||||||
|
|
||||||
restart_needed = (new_proxy_port_int != expected_proxy_port)
|
restart_needed = (new_proxy_port_int != expected_proxy_port)
|
||||||
if restart_needed:
|
if restart_needed:
|
||||||
@@ -5292,7 +5413,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
threading.Thread(target=restart_server, daemon=True).start()
|
threading.Thread(target=restart_server, daemon=True).start()
|
||||||
else:
|
else:
|
||||||
self.send_json({"ok": True, "restart_needed": False, "message": "配置更新成功,已即时生效!"})
|
message = policy_message or "配置更新成功,已即时生效!"
|
||||||
|
self.send_json({"ok": True, "restart_needed": False, "message": 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
|
||||||
@@ -5303,28 +5425,40 @@ 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))
|
fav_fail_fallback = False
|
||||||
|
|
||||||
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)
|
||||||
return
|
return
|
||||||
|
if routing_mode == "fixed_region" and not force_country:
|
||||||
|
self.send_json({"ok": False, "error": "启用固定地区前,请先选择一个要锁定的国家"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
if routing_ip_type not in ("all", "residential", "hosting"):
|
if routing_ip_type not in ("all", "residential", "hosting"):
|
||||||
self.send_json({"ok": False, "error": "无效的IP出站类型过滤"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "无效的IP出站类型过滤"}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
|
fixed_node_id = current_fixed_node_id(ui_cfg) if routing_mode == "fixed_ip" else ""
|
||||||
|
if routing_mode == "fixed_ip" and not fixed_node_id:
|
||||||
|
self.send_json({"ok": False, "error": "启用固定 IP 前,请先连接一个要锁定的节点"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
|
||||||
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["fav_fail_fallback"] = fav_fail_fallback
|
||||||
|
if routing_mode == "fixed_ip":
|
||||||
|
ui_cfg["fixed_node_id"] = fixed_node_id
|
||||||
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"
|
||||||
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")
|
write_json(auth_file, ui_cfg)
|
||||||
|
|
||||||
|
policy_message = enforce_active_node_allowed_by_routing(ui_cfg, "出站路由配置已更新")
|
||||||
|
|
||||||
self.send_json({"ok": True, "message": "出站路由配置更新成功,已即时生效!"})
|
self.send_json({"ok": True, "message": policy_message or "出站路由配置更新成功,已即时生效!"})
|
||||||
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
|
||||||
@@ -5333,6 +5467,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
payload = self.read_json_body()
|
payload = self.read_json_body()
|
||||||
node_id = str(payload.get("id") or "").strip()
|
node_id = str(payload.get("id") or "").strip()
|
||||||
|
if not node_id:
|
||||||
|
self.send_json({"ok": False, "error": "节点 ID 不能为空"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
fav_ids = ui_cfg.get("favorite_node_ids", [])
|
fav_ids = ui_cfg.get("favorite_node_ids", [])
|
||||||
@@ -5348,9 +5485,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
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")
|
write_json(auth_file, ui_cfg)
|
||||||
|
|
||||||
|
policy_message = None
|
||||||
|
if ui_cfg.get("routing_mode") == "favorites":
|
||||||
|
policy_message = enforce_active_node_allowed_by_routing(ui_cfg, "收藏列表已更新")
|
||||||
|
|
||||||
self.send_json({"ok": True, "favorite_node_ids": fav_ids})
|
self.send_json({"ok": True, "favorite_node_ids": fav_ids, "message": policy_message or ""})
|
||||||
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
|
||||||
@@ -5373,8 +5514,32 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
payload = self.read_json_body(max_bytes=262144)
|
payload = self.read_json_body(max_bytes=262144)
|
||||||
node_ids = payload.get("ids", [])
|
node_ids = payload.get("ids", [])
|
||||||
tested_nodes = test_multiple_nodes(node_ids)
|
if not isinstance(node_ids, list):
|
||||||
self.send_json({"ok": True, "nodes": tested_nodes})
|
self.send_json({"ok": False, "error": "节点 ID 列表无效"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
node_ids = [str(node_id or "").strip() for node_id in node_ids]
|
||||||
|
node_ids = [node_id for node_id in node_ids if node_id]
|
||||||
|
if len(node_ids) > MANUAL_TEST_NODE_LIMIT:
|
||||||
|
self.send_json({"ok": False, "error": f"单次最多测试 {MANUAL_TEST_NODE_LIMIT} 个节点"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
if not maintenance_lock.acquire(blocking=False):
|
||||||
|
self.send_json({"ok": False, "error": "当前已有连接或节点维护任务正在运行,请稍后再试"}, HTTPStatus.CONFLICT)
|
||||||
|
return
|
||||||
|
with lock:
|
||||||
|
if is_connecting:
|
||||||
|
maintenance_lock.release()
|
||||||
|
self.send_json({"ok": False, "error": "当前已有连接或节点维护任务正在运行,请稍后再试"}, HTTPStatus.CONFLICT)
|
||||||
|
return
|
||||||
|
is_connecting = True
|
||||||
|
try:
|
||||||
|
set_state(is_connecting=True, last_check_message="正在手动测试节点可用性...")
|
||||||
|
tested_nodes = test_multiple_nodes(node_ids)
|
||||||
|
self.send_json({"ok": True, "nodes": tested_nodes})
|
||||||
|
finally:
|
||||||
|
with lock:
|
||||||
|
is_connecting = False
|
||||||
|
set_state(is_connecting=False)
|
||||||
|
maintenance_lock.release()
|
||||||
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)
|
||||||
elif effective_path == "/api/disconnect":
|
elif effective_path == "/api/disconnect":
|
||||||
@@ -5384,7 +5549,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
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")
|
write_json(auth_file, ui_cfg)
|
||||||
|
|
||||||
stop_active_openvpn()
|
stop_active_openvpn()
|
||||||
with lock:
|
with lock:
|
||||||
@@ -5409,8 +5574,27 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
try:
|
try:
|
||||||
payload = self.read_json_body()
|
payload = self.read_json_body()
|
||||||
node_id = str(payload.get("id") or "")
|
node_id = str(payload.get("id") or "")
|
||||||
updated_node = test_node_by_id(node_id)
|
if not node_id.strip():
|
||||||
self.send_json({"ok": True, "node": updated_node})
|
self.send_json({"ok": False, "error": "节点 ID 不能为空"}, HTTPStatus.BAD_REQUEST)
|
||||||
|
return
|
||||||
|
if not maintenance_lock.acquire(blocking=False):
|
||||||
|
self.send_json({"ok": False, "error": "当前已有连接或节点维护任务正在运行,请稍后再试"}, HTTPStatus.CONFLICT)
|
||||||
|
return
|
||||||
|
with lock:
|
||||||
|
if is_connecting:
|
||||||
|
maintenance_lock.release()
|
||||||
|
self.send_json({"ok": False, "error": "当前已有连接或节点维护任务正在运行,请稍后再试"}, HTTPStatus.CONFLICT)
|
||||||
|
return
|
||||||
|
is_connecting = True
|
||||||
|
try:
|
||||||
|
set_state(is_connecting=True, last_check_message="正在手动测试节点可用性...")
|
||||||
|
updated_node = test_node_by_id(node_id)
|
||||||
|
self.send_json({"ok": True, "node": updated_node})
|
||||||
|
finally:
|
||||||
|
with lock:
|
||||||
|
is_connecting = False
|
||||||
|
set_state(is_connecting=False)
|
||||||
|
maintenance_lock.release()
|
||||||
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)
|
||||||
elif effective_path == "/api/test_proxy":
|
elif effective_path == "/api/test_proxy":
|
||||||
|
|||||||
Reference in New Issue
Block a user