fix: unblock add-node API and improve online status detection
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+18
-4
@@ -97,7 +97,7 @@ def login_required(view):
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def apply_singbox() -> tuple[bool, str]:
|
def render_singbox_config() -> tuple[bool, str]:
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["JIEDIAN_ROOT"] = str(ROOT)
|
env["JIEDIAN_ROOT"] = str(ROOT)
|
||||||
proc = subprocess.run(
|
proc = subprocess.run(
|
||||||
@@ -108,9 +108,23 @@ def apply_singbox() -> tuple[bool, str]:
|
|||||||
)
|
)
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
return False, proc.stderr or proc.stdout or "配置生成失败"
|
return False, proc.stderr or proc.stdout or "配置生成失败"
|
||||||
restart = subprocess.run(["systemctl", "restart", "sing-box"], capture_output=True, text=True)
|
return True, "ok"
|
||||||
if restart.returncode != 0:
|
|
||||||
return False, restart.stderr or restart.stdout or "sing-box 重启失败"
|
|
||||||
|
def restart_singbox_async() -> None:
|
||||||
|
"""后台重启 sing-box,避免添加/删除节点 API 长时间阻塞。"""
|
||||||
|
subprocess.Popen(
|
||||||
|
["systemctl", "restart", "sing-box"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_singbox() -> tuple[bool, str]:
|
||||||
|
ok, msg = render_singbox_config()
|
||||||
|
if not ok:
|
||||||
|
return False, msg
|
||||||
|
restart_singbox_async()
|
||||||
return True, "ok"
|
return True, "ok"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+29
-10
@@ -90,23 +90,38 @@ if (cancelBtn) {
|
|||||||
cancelBtn.addEventListener("click", () => modal.classList.add("hidden"));
|
cancelBtn.addEventListener("click", () => modal.classList.add("hidden"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setButtonBusy(btn, busy, busyText) {
|
||||||
|
if (!btn) return;
|
||||||
|
if (busy) {
|
||||||
|
if (!btn.dataset.label) btn.dataset.label = btn.textContent;
|
||||||
|
btn.textContent = busyText;
|
||||||
|
btn.disabled = true;
|
||||||
|
} else {
|
||||||
|
btn.textContent = btn.dataset.label || btn.textContent;
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (confirmAddBtn) {
|
if (confirmAddBtn) {
|
||||||
confirmAddBtn.addEventListener("click", async () => {
|
confirmAddBtn.addEventListener("click", async () => {
|
||||||
const name = nodeName.value.trim() || "新节点";
|
const name = nodeName.value.trim() || "新节点";
|
||||||
confirmAddBtn.disabled = true;
|
setButtonBusy(confirmAddBtn, true, "创建中…");
|
||||||
|
if (cancelBtn) cancelBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(apiUrl("/api/nodes"), {
|
const res = await fetch(apiUrl("/api/nodes"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ name }),
|
body: JSON.stringify({ name }),
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || "创建失败");
|
if (!res.ok) throw new Error(data.error || "创建失败");
|
||||||
location.reload();
|
toast("节点已创建,配置生效中…");
|
||||||
|
setTimeout(() => location.reload(), 600);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast(err.message);
|
toast(err.message || "创建失败");
|
||||||
} finally {
|
setButtonBusy(confirmAddBtn, false);
|
||||||
confirmAddBtn.disabled = false;
|
if (cancelBtn) cancelBtn.disabled = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -115,15 +130,19 @@ document.querySelectorAll(".delete-btn").forEach((btn) => {
|
|||||||
btn.addEventListener("click", async () => {
|
btn.addEventListener("click", async () => {
|
||||||
const id = btn.dataset.id;
|
const id = btn.dataset.id;
|
||||||
if (!confirm("确定删除该节点?删除后对应链接将失效。")) return;
|
if (!confirm("确定删除该节点?删除后对应链接将失效。")) return;
|
||||||
btn.disabled = true;
|
setButtonBusy(btn, true, "删除中…");
|
||||||
try {
|
try {
|
||||||
const res = await fetch(apiUrl(`/api/nodes/${id}`), { method: "DELETE" });
|
const res = await fetch(apiUrl(`/api/nodes/${id}`), {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "same-origin",
|
||||||
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || "删除失败");
|
if (!res.ok) throw new Error(data.error || "删除失败");
|
||||||
location.reload();
|
toast("已删除,配置生效中…");
|
||||||
|
setTimeout(() => location.reload(), 600);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast(err.message);
|
toast(err.message || "删除失败");
|
||||||
btn.disabled = false;
|
setButtonBusy(btn, false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-1
@@ -279,14 +279,17 @@ def collect_node_stats() -> dict:
|
|||||||
matched = [c for c in connections if _match_connection(c, node)]
|
matched = [c for c in connections if _match_connection(c, node)]
|
||||||
if not matched and single_node and has_connections:
|
if not matched and single_node and has_connections:
|
||||||
matched = connections
|
matched = connections
|
||||||
|
if not matched and (session_up + session_down) > 0:
|
||||||
|
matched = [None] # 有活跃会话但 Clash 未返回连接详情
|
||||||
if not matched and single_node and global_active:
|
if not matched and single_node and global_active:
|
||||||
up_speed = global_up_speed
|
up_speed = global_up_speed
|
||||||
down_speed = global_down_speed
|
down_speed = global_down_speed
|
||||||
|
|
||||||
online = (
|
online = (
|
||||||
len(matched) > 0
|
len(matched) > 0
|
||||||
|
or (session_up + session_down) > 0
|
||||||
or (up_speed + down_speed) > 512
|
or (up_speed + down_speed) > 512
|
||||||
or (single_node and global_active)
|
or (single_node and (global_active or has_connections))
|
||||||
)
|
)
|
||||||
|
|
||||||
if online:
|
if online:
|
||||||
|
|||||||
Reference in New Issue
Block a user