mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-09 01:36:52 +08:00
feat: implement favorite nodes toolbar/banner, favorites routing mode, and detailed periodic check diagnostics
This commit is contained in:
+114
-7
@@ -176,7 +176,8 @@ def load_ui_config() -> dict[str, Any]:
|
|||||||
"force_country": "",
|
"force_country": "",
|
||||||
"routing_ip_type": "all",
|
"routing_ip_type": "all",
|
||||||
"connection_enabled": True,
|
"connection_enabled": True,
|
||||||
"fixed_node_id": ""
|
"fixed_node_id": "",
|
||||||
|
"favorite_node_ids": []
|
||||||
}
|
}
|
||||||
updated = False
|
updated = False
|
||||||
if auth_file.exists():
|
if auth_file.exists():
|
||||||
@@ -184,7 +185,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"]:
|
for key in ["routing_mode", "force_country", "routing_ip_type", "connection_enabled", "fixed_node_id", "favorite_node_ids"]:
|
||||||
if key not in data:
|
if key not in data:
|
||||||
updated = True
|
updated = True
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -303,6 +304,7 @@ def get_state() -> dict[str, Any]:
|
|||||||
state["routing_ip_type"] = ui_cfg.get("routing_ip_type", "all")
|
state["routing_ip_type"] = ui_cfg.get("routing_ip_type", "all")
|
||||||
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", [])
|
||||||
|
|
||||||
return state
|
return state
|
||||||
|
|
||||||
@@ -1116,6 +1118,9 @@ def auto_switch_node(attempt: int = 0) -> None:
|
|||||||
if n.get("country") == target_country
|
if n.get("country") == target_country
|
||||||
or vpn_utils.COUNTRY_TRANSLATIONS.get(n.get("country", ""), 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", []))
|
||||||
|
candidates = [n for n in candidates if n.get("id") in fav_ids]
|
||||||
|
|
||||||
# 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")
|
||||||
@@ -1363,14 +1368,36 @@ def maintain_valid_nodes(force: bool = False) -> str:
|
|||||||
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")]
|
||||||
to_test_ids = [n["id"] for n in to_test]
|
to_test_ids = [n["id"] for n in to_test]
|
||||||
|
|
||||||
print(f"[维护线程] 正在并发检测列表中所有节点,共 {len(to_test_ids)} 个...", flush=True)
|
msg = f"开始对列表中所有候选节点进行周期连通性与延迟测试,待检测节点共 {len(to_test_ids)} 个"
|
||||||
|
print(f"[周期检测] {msg}", flush=True)
|
||||||
|
log_to_json("INFO", "Main", msg)
|
||||||
|
|
||||||
set_state(is_connecting=True, last_check_message="正在并发检测所有节点可用性...")
|
set_state(is_connecting=True, last_check_message="正在并发检测所有节点可用性...")
|
||||||
test_multiple_nodes(to_test_ids)
|
test_multiple_nodes(to_test_ids)
|
||||||
|
|
||||||
is_connecting = False
|
is_connecting = False
|
||||||
|
|
||||||
with lock:
|
with lock:
|
||||||
merged = read_json(NODES_FILE, [])
|
merged = read_json(NODES_FILE, [])
|
||||||
|
|
||||||
|
# Identify available, unavailable, and active nodes
|
||||||
|
available_nodes = [n["id"] for n in merged if n.get("probe_status") == "available"]
|
||||||
|
unavailable_nodes = [n["id"] for n in merged if n.get("probe_status") == "unavailable"]
|
||||||
|
active_node = next((n["id"] for n in merged if n.get("active")), "无")
|
||||||
|
|
||||||
|
status_report = (
|
||||||
|
f"周期节点检测完成。实时同步状态: 获取到候选节点共 {len(merged)} 个。 "
|
||||||
|
f"其中【可用节点】{len(available_nodes)} 个: {available_nodes[:15]}...; "
|
||||||
|
f"【不可用节点】{len(unavailable_nodes)} 个; "
|
||||||
|
f"当前【正在正常运行的活动连接节点】为: {active_node}。"
|
||||||
|
)
|
||||||
|
print(f"[周期检测] {status_report}", flush=True)
|
||||||
|
log_to_json("INFO", "Main", status_report)
|
||||||
|
|
||||||
|
if active_node != "无" and not active_openvpn_running():
|
||||||
|
warn_msg = f"[诊断警告] 活动节点 {active_node} 被标记为活动状态,但 OpenVPN 进程实际并未正常运行!"
|
||||||
|
print(warn_msg, flush=True)
|
||||||
|
log_to_json("WARNING", "Main", warn_msg)
|
||||||
|
|
||||||
if not active_openvpn_running():
|
if not active_openvpn_running():
|
||||||
ui_cfg = load_ui_config()
|
ui_cfg = load_ui_config()
|
||||||
connection_enabled = ui_cfg.get("connection_enabled", True)
|
connection_enabled = ui_cfg.get("connection_enabled", True)
|
||||||
@@ -1386,6 +1413,9 @@ def maintain_valid_nodes(force: bool = False) -> str:
|
|||||||
if n.get("country") == target_country
|
if n.get("country") == target_country
|
||||||
or vpn_utils.COUNTRY_TRANSLATIONS.get(n.get("country", ""), 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", []))
|
||||||
|
available_candidates = [n for n in available_candidates if n.get("id") in fav_ids]
|
||||||
|
|
||||||
# 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")
|
||||||
@@ -1417,10 +1447,16 @@ def collector_loop() -> None:
|
|||||||
last_collector_heartbeat = time.time()
|
last_collector_heartbeat = time.time()
|
||||||
success = False
|
success = False
|
||||||
try:
|
try:
|
||||||
|
print("[守护线程] 开始执行节点拉取与可用性检测周期任务...", flush=True)
|
||||||
|
log_to_json("INFO", "Main", "开始执行节点拉取与可用性检测周期任务...")
|
||||||
res = maintain_valid_nodes(force=False)
|
res = maintain_valid_nodes(force=False)
|
||||||
if "没有拉取到新节点" not in res:
|
if "没有拉取到新节点" not in res:
|
||||||
success = True
|
success = True
|
||||||
|
log_to_json("INFO", "Main", f"周期同步与检测任务完成,结果: {res}")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
err_msg = f"周期节点同步任务执行异常: {exc}"
|
||||||
|
print(f"[错误] {err_msg}", flush=True)
|
||||||
|
log_to_json("ERROR", "Main", err_msg)
|
||||||
set_state(last_check_at=time.time(), last_check_message=f"check error: {exc}")
|
set_state(last_check_at=time.time(), last_check_message=f"check error: {exc}")
|
||||||
|
|
||||||
if not active_openvpn_running() and not success:
|
if not active_openvpn_running() and not success:
|
||||||
@@ -2660,6 +2696,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
<select id="status_filter">
|
<select id="status_filter">
|
||||||
<option value="available">可用节点</option>
|
<option value="available">可用节点</option>
|
||||||
<option value="all">全部节点</option>
|
<option value="all">全部节点</option>
|
||||||
|
<option value="favorites">收藏节点</option>
|
||||||
<option value="unavailable">失效节点</option>
|
<option value="unavailable">失效节点</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="country_filter">
|
<select id="country_filter">
|
||||||
@@ -2672,6 +2709,10 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
</select>
|
</select>
|
||||||
<input id="search" placeholder="输入国家、位置、IP、ASN、运营主体等过滤节点..." />
|
<input id="search" placeholder="输入国家、位置、IP、ASN、运营主体等过滤节点..." />
|
||||||
</section>
|
</section>
|
||||||
|
<div id="favorites_banner" style="display: none; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.25); border-radius: 12px; padding: 12px 16px; margin-bottom: 20px; font-size: 13px; color: var(--warning); line-height: 1.5;">
|
||||||
|
ℹ️ <strong>温馨提示</strong>:您可以通过点击节点列表右侧的「☆ 收藏」按钮将心仪节点加入此列表。如果您只希望在收藏列表中自动切换和使用节点,请点击右上角「管理员 -> 代理设置」,将「IP 出站路由模式」设置为「仅用收藏」。
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table>
|
<table>
|
||||||
@@ -2682,7 +2723,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
<th>物理位置</th>
|
<th>物理位置</th>
|
||||||
<th>运营主体 / ISP</th>
|
<th>运营主体 / ISP</th>
|
||||||
<th style="width: 110px;">IP 类型</th>
|
<th style="width: 110px;">IP 类型</th>
|
||||||
<th style="width: 100px;">操作</th>
|
<th style="width: 180px;">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="rows"></tbody>
|
<tbody id="rows"></tbody>
|
||||||
@@ -2780,6 +2821,7 @@ INDEX_HTML = r"""<!doctype html>
|
|||||||
<option value="auto">自动配置 (智能切换,最稳定)</option>
|
<option value="auto">自动配置 (智能切换,最稳定)</option>
|
||||||
<option value="fixed_ip">固定 IP (永不自动换 IP)</option>
|
<option value="fixed_ip">固定 IP (永不自动换 IP)</option>
|
||||||
<option value="fixed_region">固定地区 (锁定特定国家节点)</option>
|
<option value="fixed_region">固定地区 (锁定特定国家节点)</option>
|
||||||
|
<option value="favorites">仅用收藏 (只在收藏的节点中切换)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -3118,6 +3160,9 @@ function getFilteredNodes() {
|
|||||||
if (selectedStatus === "unavailable" && (n.probe_status !== "unavailable" || n.active)) {
|
if (selectedStatus === "unavailable" && (n.probe_status !== "unavailable" || n.active)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (selectedStatus === "favorites" && (!state.favorite_node_ids || !state.favorite_node_ids.includes(n.id))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const searchStr = [
|
const searchStr = [
|
||||||
n.country || "", n.country_short || "", n.ip || "", n.remote_host || "", n.proto || "",
|
n.country || "", n.country_short || "", n.ip || "", n.remote_host || "", n.proto || "",
|
||||||
translateQuality(n.quality), translateIpType(n.ip_type), n.location || "", n.owner || "", n.as_name || ""
|
translateQuality(n.quality), translateIpType(n.ip_type), n.location || "", n.owner || "", n.as_name || ""
|
||||||
@@ -3277,6 +3322,10 @@ function render(){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isFavoritesFilter = $("status_filter").value === "favorites";
|
||||||
|
const banner = $("favorites_banner");
|
||||||
|
if (banner) banner.style.display = isFavoritesFilter ? "block" : "none";
|
||||||
|
|
||||||
// Pagination calculation
|
// Pagination calculation
|
||||||
const totalPages = Math.ceil(shown.length / pageSize) || 1;
|
const totalPages = Math.ceil(shown.length / pageSize) || 1;
|
||||||
if (currentPage > totalPages) currentPage = totalPages;
|
if (currentPage > totalPages) currentPage = totalPages;
|
||||||
@@ -3306,12 +3355,18 @@ function render(){
|
|||||||
const testBtnText = isTesting ? `${testSpinner}检测中` : '检测';
|
const testBtnText = isTesting ? `${testSpinner}检测中` : '检测';
|
||||||
const testBtn = `<button class="test-btn" data-node-id="${esc(n.id)}" ${isTesting ? 'disabled' : ''} onclick="testNode(this, '${esc(n.id)}', event)">${testBtnText}</button>`;
|
const testBtn = `<button class="test-btn" data-node-id="${esc(n.id)}" ${isTesting ? 'disabled' : ''} onclick="testNode(this, '${esc(n.id)}', event)">${testBtnText}</button>`;
|
||||||
|
|
||||||
|
// Connect button is disabled if probe status is "unavailable" and not already active, or if we are already connecting
|
||||||
// Connect button is disabled if probe status is "unavailable" and not already active, or if we are already connecting
|
// Connect button is disabled if probe status is "unavailable" and not already active, or if we are already connecting
|
||||||
const isUnavailable = n.probe_status === "unavailable";
|
const isUnavailable = n.probe_status === "unavailable";
|
||||||
const connectBtn = isCurrentlyActive
|
const connectBtn = isCurrentlyActive
|
||||||
? `<button class="connect-btn" disabled style="background: var(--success-gradient); color: white; cursor: default; opacity: 1;">已连接</button>`
|
? `<button class="connect-btn" disabled style="background: var(--success-gradient); color: white; cursor: default; opacity: 1;">已连接</button>`
|
||||||
: `<button class="connect-btn" ${(isUnavailable || state.is_connecting) ? 'disabled style="opacity:0.3; cursor:not-allowed;"' : ''} onclick="connectNode('${esc(n.id)}')">切换</button>`;
|
: `<button class="connect-btn" ${(isUnavailable || state.is_connecting) ? 'disabled style="opacity:0.3; cursor:not-allowed;"' : ''} onclick="connectNode('${esc(n.id)}')">切换</button>`;
|
||||||
|
|
||||||
|
const isFav = state.favorite_node_ids && state.favorite_node_ids.includes(n.id);
|
||||||
|
const favBtn = isFav
|
||||||
|
? `<button class="test-btn" style="color: var(--warning); border-color: rgba(245, 158, 11, 0.4); padding: 0 8px; height: 30px;" onclick="toggleFavorite('${esc(n.id)}', event)">★ 已收藏</button>`
|
||||||
|
: `<button class="test-btn" style="color: var(--text-secondary); border-color: var(--border-color); padding: 0 8px; height: 30px;" onclick="toggleFavorite('${esc(n.id)}', event)">☆ 收藏</button>`;
|
||||||
|
|
||||||
return `<tr ${rowClass}>
|
return `<tr ${rowClass}>
|
||||||
<td><span class="badge ${badgeClass}">${badgeText}</span></td>
|
<td><span class="badge ${badgeClass}">${badgeText}</span></td>
|
||||||
<td class="mono" style="white-space: nowrap; max-width: 220px; overflow: hidden; text-overflow: ellipsis;" title="${esc(n.ip||n.remote_host)}:${n.remote_port||""}">${esc(n.ip||n.remote_host)}:${n.remote_port||""}</td>
|
<td class="mono" style="white-space: nowrap; max-width: 220px; overflow: hidden; text-overflow: ellipsis;" title="${esc(n.ip||n.remote_host)}:${n.remote_port||""}">${esc(n.ip||n.remote_host)}:${n.remote_port||""}</td>
|
||||||
@@ -3320,6 +3375,7 @@ function render(){
|
|||||||
<td style="white-space: nowrap; max-width: 110px; overflow: hidden; text-overflow: ellipsis;" title="${esc(translateIpType(n.ip_type))}">${esc(translateIpType(n.ip_type))}</td>
|
<td style="white-space: nowrap; max-width: 110px; overflow: hidden; text-overflow: ellipsis;" title="${esc(translateIpType(n.ip_type))}">${esc(translateIpType(n.ip_type))}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
|
${favBtn}
|
||||||
${connectBtn}
|
${connectBtn}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -3380,6 +3436,24 @@ async function testNode(btn, id, event){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleFavorite(id, event) {
|
||||||
|
if (event) event.stopPropagation();
|
||||||
|
try {
|
||||||
|
const response = await fetch("./api/toggle_favorite", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ id })
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.ok) {
|
||||||
|
state.favorite_node_ids = result.favorite_node_ids || [];
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("切换收藏失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let pollInterval = null;
|
let pollInterval = null;
|
||||||
|
|
||||||
function startConnectionPolling() {
|
function startConnectionPolling() {
|
||||||
@@ -3578,6 +3652,12 @@ function handleRoutingModeChange(mode) {
|
|||||||
warningDiv.style.background = "rgba(245, 158, 11, 0.1)";
|
warningDiv.style.background = "rgba(245, 158, 11, 0.1)";
|
||||||
warningDiv.style.border = "1px solid rgba(245, 158, 11, 0.2)";
|
warningDiv.style.border = "1px solid rgba(245, 158, 11, 0.2)";
|
||||||
warningDiv.innerHTML = `⚠️ <strong>固定地区</strong>:限制仅连接选定国家的节点,且后台仅并发测速该国家的节点。如果该国的所有可用节点都失效,会造成代理中断且<strong>绝不自动切换到其他国家</strong>的节点。`;
|
warningDiv.innerHTML = `⚠️ <strong>固定地区</strong>:限制仅连接选定国家的节点,且后台仅并发测速该国家的节点。如果该国的所有可用节点都失效,会造成代理中断且<strong>绝不自动切换到其他国家</strong>的节点。`;
|
||||||
|
} else if (mode === "favorites") {
|
||||||
|
countryGroup.style.display = "none";
|
||||||
|
warningDiv.style.color = "var(--warning)";
|
||||||
|
warningDiv.style.background = "rgba(245, 158, 11, 0.1)";
|
||||||
|
warningDiv.style.border = "1px solid rgba(245, 158, 11, 0.2)";
|
||||||
|
warningDiv.innerHTML = `⚠️ <strong>仅用收藏</strong>:只连接和切换您收藏的节点。如果所有收藏的节点均失效,系统不会自动切换到未收藏的节点。请确保收藏列表中有足够多且可用的节点。`;
|
||||||
} else if (mode === "fixed_ip") {
|
} else if (mode === "fixed_ip") {
|
||||||
countryGroup.style.display = "none";
|
countryGroup.style.display = "none";
|
||||||
warningDiv.style.color = "var(--warning)";
|
warningDiv.style.color = "var(--warning)";
|
||||||
@@ -4589,7 +4669,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_json({"ok": False, "error": "安全后缀仅能由英文字母和数字组成"}, HTTPStatus.BAD_REQUEST)
|
self.send_json({"ok": False, "error": "安全后缀仅能由英文字母和数字组成"}, HTTPStatus.BAD_REQUEST)
|
||||||
return
|
return
|
||||||
|
|
||||||
if routing_mode not in ("auto", "fixed_ip", "fixed_region"):
|
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"):
|
||||||
@@ -4637,7 +4717,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
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()
|
||||||
|
|
||||||
if routing_mode not in ("auto", "fixed_ip", "fixed_region"):
|
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"):
|
||||||
@@ -4660,6 +4740,33 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
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
|
||||||
|
|
||||||
|
elif effective_path == "/api/toggle_favorite":
|
||||||
|
try:
|
||||||
|
length = parse_int(self.headers.get("Content-Length"))
|
||||||
|
payload = json.loads(self.rfile.read(length).decode("utf-8") or "{}")
|
||||||
|
node_id = str(payload.get("id") or "").strip()
|
||||||
|
|
||||||
|
ui_cfg = load_ui_config()
|
||||||
|
fav_ids = ui_cfg.get("favorite_node_ids", [])
|
||||||
|
if not isinstance(fav_ids, list):
|
||||||
|
fav_ids = []
|
||||||
|
|
||||||
|
if node_id in fav_ids:
|
||||||
|
fav_ids.remove(node_id)
|
||||||
|
else:
|
||||||
|
fav_ids.append(node_id)
|
||||||
|
|
||||||
|
ui_cfg["favorite_node_ids"] = fav_ids
|
||||||
|
auth_file = DATA_DIR / "ui_auth.json"
|
||||||
|
with lock:
|
||||||
|
DATA_DIR.mkdir(exist_ok=True, parents=True)
|
||||||
|
auth_file.write_text(json.dumps(ui_cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
self.send_json({"ok": True, "favorite_node_ids": fav_ids})
|
||||||
|
except Exception as exc:
|
||||||
|
self.send_json({"ok": False, "error": str(exc)}, HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
if effective_path == "/api/check":
|
if effective_path == "/api/check":
|
||||||
try:
|
try:
|
||||||
self.send_json({"ok": True, "message": maintain_valid_nodes(force=True)})
|
self.send_json({"ok": True, "message": maintain_valid_nodes(force=True)})
|
||||||
|
|||||||
Reference in New Issue
Block a user