From 41513d566eaecdc1a0500cbbf047afb90388c231 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Tue, 21 Jul 2026 14:02:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B8=B8=E8=A7=84=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + CMakeLists.txt | 72 +- config/config.yaml | 18 +- src/base/Async_Blocking.h | 54 + src/base/Check_Ip.cpp | 205 +- src/base/Proxy_Ping0_Labeler.cpp | 122 +- src/base/export.cpp | 143 +- src/base/export.h | 6 +- src/base/global.h | 9 + src/cpp_filter/main.cpp | 29 +- src/node_service/Node_Service.h | 264 ++ src/node_service/Node_Service_Auto_Switch.cpp | 147 + src/node_service/Node_Service_Config.cpp | 136 + src/node_service/Node_Service_Controller.cpp | 72 + src/node_service/Node_Service_Monitor.cpp | 168 ++ src/node_service/Node_Service_State.cpp | 314 +++ src/node_service/Node_Service_Util.cpp | 320 +++ src/node_service/Node_Service_Web.cpp | 205 ++ src/node_service/main.cpp | 43 + src/node_service/web/README.md | 9 + src/node_service/web/build_frontend.ps1 | 9 + src/node_service/web/index.html | 12 + src/node_service/web/package-lock.json | 2368 +++++++++++++++++ src/node_service/web/package.json | 21 + src/node_service/web/src/App.jsx | 178 ++ src/node_service/web/src/api.js | 23 + src/node_service/web/src/main.jsx | 13 + src/node_service/web/src/theme.js | 22 + src/node_service/web/vite.config.js | 10 + 29 files changed, 4776 insertions(+), 219 deletions(-) create mode 100644 src/base/Async_Blocking.h create mode 100644 src/node_service/Node_Service.h create mode 100644 src/node_service/Node_Service_Auto_Switch.cpp create mode 100644 src/node_service/Node_Service_Config.cpp create mode 100644 src/node_service/Node_Service_Controller.cpp create mode 100644 src/node_service/Node_Service_Monitor.cpp create mode 100644 src/node_service/Node_Service_State.cpp create mode 100644 src/node_service/Node_Service_Util.cpp create mode 100644 src/node_service/Node_Service_Web.cpp create mode 100644 src/node_service/main.cpp create mode 100644 src/node_service/web/README.md create mode 100644 src/node_service/web/build_frontend.ps1 create mode 100644 src/node_service/web/index.html create mode 100644 src/node_service/web/package-lock.json create mode 100644 src/node_service/web/package.json create mode 100644 src/node_service/web/src/App.jsx create mode 100644 src/node_service/web/src/api.js create mode 100644 src/node_service/web/src/main.jsx create mode 100644 src/node_service/web/src/theme.js create mode 100644 src/node_service/web/vite.config.js diff --git a/.gitignore b/.gitignore index 1f6b77d..831ecf3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ /ping0_cc_cache/ /Ping0_Html_Cache/ /Node_Exit_Ip_Cache/ +/src/web/dist/ +/src/node_service/web/dist/ +/src/node_service/web/node_modules/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 22fe859..07e23ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,8 @@ cmake_minimum_required(VERSION 3.20) - project(tizi LANGUAGES CXX) - set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) - - -# 第三方库引入 set(third_party "${CMAKE_CURRENT_LIST_DIR}/third_party") set_property(GLOBAL PROPERTY USE_FOLDERS ON) set(CMAKE_FOLDER "yaml-cpp-master") @@ -19,35 +14,62 @@ add_subdirectory("${third_party}/json-3.12.0") set(CMAKE_FOLDER "asio") include("${third_party}/asio/main.cmake") unset(CMAKE_FOLDER) - find_package(OpenSSL REQUIRED) - -# 基础库 +if (WIN32 AND TARGET asio_object) + target_compile_definitions(asio_object PUBLIC + _WIN32_WINNT=0x0A00 + WINVER=0x0A00 + NOMINMAX + WIN32_LEAN_AND_MEAN + ) +endif () set(src "${CMAKE_CURRENT_LIST_DIR}/src") -set(base_src ${src}/base) -file(GLOB_RECURSE base_srcs ${base_src}/*.h ${base_src}/*.cpp) +set(base_src "${src}/base") +file(GLOB_RECURSE base_srcs CONFIGURE_DEPENDS + "${base_src}/*.hpp" "${base_src}/*.h" "${base_src}/*.cpp" + "${src}/*.hpp" "${src}/*.h" "${src}/*.cpp" +) add_library(Base ${base_srcs}) -target_compile_definitions(Base PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT) -target_link_libraries(Base PUBLIC OpenSSL::SSL OpenSSL::Crypto) -target_link_libraries(Base PUBLIC yaml-cpp::yaml-cpp) -target_compile_definitions(Base PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT) -target_link_libraries(Base PUBLIC lexbor_static) -target_link_libraries(Base PUBLIC nlohmann_json::nlohmann_json) -target_link_libraries(Base PUBLIC asio_object) - - +target_include_directories(Base PUBLIC "${src}") +target_compile_definitions(Base PUBLIC + CPPHTTPLIB_OPENSSL_SUPPORT + ASIO_STANDALONE + ASIO_SEPARATE_COMPILATION + ASIO_NO_TS_EXECUTORS + ASIO_NO_DYNAMIC_BUFFER_V1 +) +if (WIN32) + target_compile_definitions(Base PUBLIC + _WIN32_WINNT=0x0A00 + WINVER=0x0A00 + NOMINMAX + WIN32_LEAN_AND_MEAN + ) +endif () +target_link_libraries(Base PUBLIC + OpenSSL::SSL + OpenSSL::Crypto + yaml-cpp::yaml-cpp + lexbor_static + nlohmann_json::nlohmann_json + asio_object +) if (MSVC) target_compile_options(Base PUBLIC /utf-8) endif () -target_include_directories(Base PUBLIC "${src}") - - -file(GLOB list CONFIGURE_DEPENDS LIST_DIRECTORIES true "${src}/*") +file(GLOB list CONFIGURE_DEPENDS LIST_DIRECTORIES true "${src}/*" ) list(REMOVE_ITEM list "${src}/base") foreach (cur_dir ${list}) get_filename_component(target "${cur_dir}" NAME) - file(GLOB_RECURSE cur_srcs ${cur_dir}/*.h ${cur_dir}/*.cpp) + file(GLOB_RECURSE cur_srcs CONFIGURE_DEPENDS "${cur_dir}/*.h" "${cur_dir}/*.cpp") add_executable(${target} ${cur_srcs}) target_link_libraries(${target} PRIVATE Base) + if (target STREQUAL "node_service") + add_custom_command(TARGET ${target} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_directory + "${cur_dir}/web" + "$/../node_service_web" + VERBATIM + ) + endif () endforeach () - diff --git a/config/config.yaml b/config/config.yaml index 096b67f..144dd96 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -131,20 +131,10 @@ proxies: - {name: 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, type: vless, server: 134.195.101.187, port: 443, uuid: a9680250-7325-4253-a8d9-b9835e8781f9, udp: true, flow: xtls-rprx-vision, tls: true, skip-cert-verify: false, client-fingerprint: ios, servername: www.mi.com, reality-opts: {public-key: CQqGVFQ65FCLJsqx2xb0RkfjZ6veJNaCsqazSYVvfgQ, short-id: d574526d}} - {name: 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, type: vless, server: 203.10.96.138, port: 443, uuid: a9680250-7325-4253-a8d9-b9835e8781f9, udp: true, flow: xtls-rprx-vision, tls: true, skip-cert-verify: false, client-fingerprint: ios, servername: cdn-dynmedia-1.microsoft.com, reality-opts: {public-key: VFG2L-3i30fQZg3wd0DF6dEGdaT27S-PKpuYu7Hp8xU, short-id: fd26d49a}} proxy-groups: - - {name: 🔥新加坡, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2], hidden: true} - - {name: 🔥新加坡0.1倍, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无], hidden: true} - - {name: 🔥日本, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐], hidden: true} - - {name: 🔥日本0.1倍, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无], hidden: true} - - {name: 🔥日本东京, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无], hidden: true} - - {name: 🔥美国, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无], hidden: true} - - {name: 🔥美国0.01倍, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无], hidden: true} - - {name: 🔥美国0.1倍, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无], hidden: true} - - {name: 🔥美国圣何塞, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b], hidden: true} - - {name: 🔥美国电信联通移动推荐, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无], hidden: true} - - {name: 📥下载, type: fallback, proxies: [🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无], url: http://cp.cloudflare.com/generate_204, interval: 30, timeout: 3000, lazy: false, tolerance: 150, max-failed-times: 2} - - {name: 🤖AI网站, type: fallback, proxies: [🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无], url: http://cp.cloudflare.com/generate_204, interval: 60, timeout: 1000, lazy: false} - - {name: ️♻️自动选择, type: url-test, url: http://cp.cloudflare.com/generate_204, proxies: [📥下载, DIRECT, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾2 | 避免晚高峰使用, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无]} - - {name: 🚀节点选择, type: select, proxies: [📥下载, 🔥新加坡, 🔥新加坡0.1倍, 🔥日本, 🔥日本0.1倍, 🔥日本东京, 🔥美国, 🔥美国0.01倍, 🔥美国0.1倍, 🔥美国圣何塞, 🔥美国电信联通移动推荐, 📥下载, DIRECT, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾2 | 避免晚高峰使用, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无]} + - {name: 📥下载, type: select, proxies: [🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无]} + - {name: 🤖AI网站, type: select, proxies: [🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无]} + - {name: ️♻️自动选择, type: select, proxies: [📥下载, DIRECT, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾2 | 避免晚高峰使用, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无]} + - {name: 🚀节点选择, type: select, proxies: [📥下载, DIRECT, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾2 | 避免晚高峰使用, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无]} - {name: 🎬媒体解锁, type: select, proxies: [🤖AI网站, 🚀节点选择, 📥下载, DIRECT, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾2 | 避免晚高峰使用, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无]} - {name: 🐟漏网之鱼, type: select, proxies: [🤖AI网站, 🚀节点选择, 📥下载, DIRECT, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾2 | 避免晚高峰使用, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无]} - {name: ▶️油管视频, type: select, proxies: [🤖AI网站, 🚀节点选择, 📥下载, DIRECT, 🇯🇵日本东京01-0.1倍 | 电信联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵日本东京02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京03-0.1倍 | 电信联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京04-0.1倍 | 电信联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京05-0.1倍 | 电信联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵日本东京06-0.1倍 | 高速专线推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵日本东京07-0.1倍 | 高速专线推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵日本东京08-0.1倍 | 高速专线推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵日本东京09-0.1倍 | 高速专线推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇸🇬新加坡01 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡02 | 电信联通推荐_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇯🇵AWS日本01 | 电信移动联通推荐_AI3星_风控38中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.138_ipv6无, 🇯🇵AWS日本02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本03 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本04 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本05 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本06 | 电信移动联通推荐_AI3星_风控27中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.154_ipv6无, 🇯🇵AWS日本07 | 电信移动联通推荐_AI1星_风控67稍高风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.178_ipv6无, 🇯🇵AWS日本08 | 电信移动联通推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.170_ipv6无, 🇯🇵AWS日本09 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv4103.62.49.162_ipv6无, 🇯🇵AWS日本10 | 电信移动联通推荐, 🇸🇬新加坡01-0.1倍 | 电信联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡02-0.1倍 | 电信联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬新加坡03-0.1倍 | 电信联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬新加坡04-0.1倍 | 电信联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬新加坡05-0.1倍 | 电信联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇸🇬AWS新加坡01 | 电信移动联通推荐_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬AWS新加坡02 | 电信移动联通推荐_AI3星_风控26中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.147_ipv6无, 🇸🇬AWS新加坡03 | 电信移动联通推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.148_ipv6无, 🇸🇬AWS新加坡04 | 电信移动联通推荐_AI1星_风控73极度风险_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.149_ipv6无, 🇸🇬AWS新加坡05 | 电信移动联通推荐_AI3星_风控29中性_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.150_ipv6无, 🇺🇸美国01-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f39b-2aa8-712f-fa9f-8b1e-e57, 🇺🇸美国02-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f39d-e4d5-f1e3-102e-a77b-2349, 🇺🇸美国03-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f395-fc3c-5df5-b392-c36a-8cf7, 🇺🇸美国04-0.1倍 | 电信联通移动推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-d2a8-39e5-647a-b949-cd64, 🇺🇸美国05-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f399-3482-1ef5-da10-a002-cf58, 🇺🇸美国06-0.1倍 | 电信联通移动推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-da05-369a-e728-e327-2914, 🇺🇸美国07-0.1倍 | 电信联通移动推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f390-c2ea-d430-7bd5-7a06-a439, 🇺🇸美国08-0.1倍 | 电信联通移动推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇸🇬新加坡 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡2 | 高速专线-hy2_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv467.159.48.146_ipv6无, 🇸🇬新加坡3 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡4 | 高速专线-hy2_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4203.27.106.146_ipv6无, 🇸🇬新加坡5 | 高速专线-hy2_AI1星_风控76极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4146.235.17.47_ipv6无, 🇸🇬新加坡6 | 高速专线-hy2, 🇺🇸美国圣何塞01-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f392-d804-1396-1c34-2b3b-5b9a, 🇺🇸美国圣何塞02-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-83a4-5723-c2e1-d589-a253, 🇺🇸美国圣何塞03-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39b-4401-de6f-8516-17c8-b20e, 🇺🇸美国圣何塞04-0.1倍_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f397-99de-8796-ee83-4afd-ee69, 🇺🇸美国圣何塞05-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f392-c639-102e-dc8b-f841-144e, 🇺🇸美国圣何塞06-0.1倍_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f398-ada-918f-3637-21ac-2f99, 🇺🇸美国圣何塞07-0.1倍_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f391-9ff-bb3-3469-73be-8c00, 🇺🇸美国洛杉矶08-0.1倍_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无, 🇹🇼台湾 | 避免晚高峰使用_AI5星_风控6极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv4114.37.199.26_ipv6无, 🇹🇼台湾2 | 避免晚高峰使用, 🇹🇼台湾3 | 避免晚高峰使用_AI5星_风控5极度纯净_共享1-10极好_家庭宽带IP_原生IP_ipv436.231.97.241_ipv6无, 🇺🇸美国01 | 合适下载使用-0.01倍_AI3星_风控16纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.43_ipv6无, 🇺🇸美国02 | 合适下载使用-0.01倍_AI3星_风控25中性_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.23.235_ipv6无, 🇺🇸美国03 | 合适下载使用-0.01倍_AI4星_风控16纯净_共享1-10极好_IDC机房IP_原生IP_ipv4208.87.242.97_ipv6无, 🇺🇸美国04 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.47_ipv6无, 🇺🇸美国05 | 合适下载使用-0.01倍_AI3星_风控24纯净_共享1-10极好_IDC机房IP_广播IP_ipv4108.181.24.41_ipv6无, 🇺🇸美国圣何塞01 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.193_ipv62602-feda-f393-5162-25fc-91f-700e-4f1a, 🇺🇸美国圣何塞02 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.194_ipv62602-feda-f399-2c38-e3d3-d70c-33c9-21b3, 🇺🇸美国圣何塞03 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.195_ipv62602-feda-f39a-13e5-d88e-29e8-5379-7486, 🇺🇸美国圣何塞04 | 三网推荐_AI1星_风控82极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.197_ipv62602-feda-f39a-c712-e32-2b7-76eb-3172, 🇺🇸美国圣何塞05 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.120_ipv62602-feda-f393-cb92-828b-312d-85e0-d1e5, 🇺🇸美国圣何塞06 | 三网推荐_AI1星_风控92极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.180_ipv62602-feda-f39d-dc3c-3361-70b2-1845-239d, 🇺🇸美国圣何塞07 | 三网推荐_AI1星_风控86极度风险_共享1-10极好_IDC机房IP_原生IP_ipv4134.195.101.188_ipv62602-feda-f396-24f5-6a11-a635-1005-a67b, 🇺🇸美国洛杉矶08 | 三网推荐_AI1星_风控87极度风险_共享1-10极好_IDC机房IP_广播IP_ipv4203.10.96.139_ipv6无]} diff --git a/src/base/Async_Blocking.h b/src/base/Async_Blocking.h new file mode 100644 index 0000000..5ac8ab7 --- /dev/null +++ b/src/base/Async_Blocking.h @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +namespace WyC { +template +asio::awaitable&>> async_blocking(asio::thread_pool& pool, Function&& function) { + using Stored_Function = std::decay_t; + using Result = std::invoke_result_t; + auto executor = co_await asio::this_coro::executor; + Stored_Function stored_function(std::forward(function)); + co_await asio::post(pool.get_executor(), asio::use_awaitable); + std::exception_ptr exception; + if constexpr (std::is_void_v) { + try { + stored_function(); + } + catch (...) { + exception = std::current_exception(); + } + co_await asio::post(executor, asio::use_awaitable); + if (exception) { + std::rethrow_exception(exception); + } + co_return; + } + else { + std::optional>> result; + try { + result.emplace(stored_function()); + } + catch (...) { + exception = std::current_exception(); + } + co_await asio::post(executor, asio::use_awaitable); + if (exception) { + std::rethrow_exception(exception); + } + co_return std::move(*result); + } +} +} diff --git a/src/base/Check_Ip.cpp b/src/base/Check_Ip.cpp index 269924e..4897c62 100644 --- a/src/base/Check_Ip.cpp +++ b/src/base/Check_Ip.cpp @@ -1,9 +1,14 @@ +#ifndef NOMINMAX #define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN +#endif #ifndef CPPHTTPLIB_OPENSSL_SUPPORT #define CPPHTTPLIB_OPENSSL_SUPPORT #endif #include "Check_Ip.h" +#include "Async_Blocking.h" #include "export.h" #include #include @@ -18,12 +23,13 @@ #include #include #include +#include #include #include +#include #include #include #include -#include #include #include #include @@ -91,7 +97,6 @@ struct Service_Check { std::string_view cache_name; std::string_view extension; }; - Ip_Check_Api_Config g_ip_check_api_config; std::vector make_ip_check_routes(const YAML::Node& proxies) { std::vector routes; @@ -107,24 +112,10 @@ std::vector make_ip_check_routes(const YAML::Node& proxies) { return routes; } fs::path path_from_utf8(std::string_view value) { -#if defined(__cpp_char8_t) - std::u8string utf8; - utf8.reserve(value.size()); - for (const unsigned char ch : value) { - utf8.push_back(static_cast(ch)); - } - return fs::path(utf8); -#else - return fs::u8path(value.begin(), value.end()); -#endif + return ::path_from_utf8(value); } std::string path_to_utf8(const fs::path& path) { -#if defined(__cpp_char8_t) - const auto utf8 = path.generic_u8string(); - return std::string(reinterpret_cast(utf8.data()), utf8.size()); -#else - return path.generic_u8string(); -#endif + return ::path_to_utf8(path); } std::string trim_ascii(std::string value) { const auto first = value.find_first_not_of(" \t\r\n"); @@ -154,7 +145,7 @@ std::string read_environment_variable(std::string_view name) { return {}; } value.resize(written); - return trim_ascii(value); + return trim_ascii(native_to_utf8(value)); #else const auto* value = std::getenv(key.c_str()); return value ? trim_ascii(value) : std::string{}; @@ -167,16 +158,15 @@ std::string config_value_or_env(std::string configured_value, std::string_view e } return read_environment_variable(env_name); } - std::string url_encode_query(std::string_view value) { static constexpr char hex[] = "0123456789ABCDEF"; std::string result; result.reserve(value.size() * 3); for (const unsigned char ch : value) { const bool safe = (ch >= 'A' && ch <= 'Z') || - (ch >= 'a' && ch <= 'z') || - (ch >= '0' && ch <= '9') || - ch == '-' || ch == '_' || ch == '.' || ch == '~'; + (ch >= 'a' && ch <= 'z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '_' || ch == '.' || ch == '~'; if (safe) { result.push_back(static_cast(ch)); } @@ -188,7 +178,6 @@ std::string url_encode_query(std::string_view value) { } return result; } - YAML::Node load_yaml_file(const fs::path& path) { std::ifstream stream(path, std::ios::binary); if (!stream) { @@ -236,7 +225,7 @@ void write_binary_file_atomic(const fs::path& path, std::string_view data) { fs::rename(temporary_path, path, error); if (error) { fs::remove(temporary_path); - throw std::runtime_error(std::format("保存缓存文件失败:{},{}", path_to_utf8(path), error.message())); + throw std::runtime_error(std::format("保存缓存文件失败:{},{}", path_to_utf8(path), error_text_to_utf8(error.message()))); } } } @@ -438,16 +427,13 @@ Http_Response http_get_cached(const Proxy_Check_Task& task, const auto cache_stem = make_valid_file_name(cache_name); const auto body_path = raw_dir / path_from_utf8(std::format("{}.{}", cache_stem, extension)); const auto meta_path = raw_dir / path_from_utf8(std::format("{}.meta.yaml", cache_stem)); - Http_Response cached; if (load_http_response_cache(body_path, meta_path, cached)) { return cached; } - auto response = http_get(task, url, timeout_ms, response_limit, extra_headers); response.cache_body_path = path_to_utf8(body_path); response.cache_meta_path = path_to_utf8(meta_path); - save_http_response_cache(task, url_for_meta.empty() ? url : url_for_meta, cache_name, body_path, meta_path, response); return response; } @@ -465,7 +451,7 @@ YAML::Node parse_json_response(const Http_Response& response, std::string_view s return YAML::Load(response.body); } catch (const YAML::Exception& error) { - throw std::runtime_error(std::format("{} 返回内容不是有效 JSON:{}", source, error.what())); + throw std::runtime_error(std::format("{} 返回内容不是有效 JSON:{}", source, error_text_to_utf8(error.what()))); } } std::string detect_public_ipv4(const Proxy_Check_Task& task, long long& latency_ms) { @@ -502,7 +488,7 @@ YAML::Node make_json_provider_result(const Http_Response& response, std::string_ } catch (const YAML::Exception& error) { result["accessible"] = false; - result["error"] = std::format("{} 返回内容不是有效 JSON:{}", source, error.what()); + result["error"] = std::format("{} 返回内容不是有效 JSON:{}", source, error_text_to_utf8(error.what())); } return result; } @@ -644,7 +630,6 @@ YAML::Node fetch_ping0_detail(const Proxy_Check_Task& task, std::string_view ip) const auto safe_url = std::format("{}***)/ip({})", ping0_detail_api_url_prefix, ip); return make_json_provider_result(http_get_cached(task, url, "ping0_detail", "json", main_request_timeout_ms, normal_response_limit, safe_url), "ping0_detail"); } - YAML::Node fetch_getipintel(const Proxy_Check_Task& task, std::string_view ip) { const auto contact = config_value_or_env(g_ip_check_api_config.getipintel_contact, getipintel_contact_env); if (contact.empty()) { @@ -654,7 +639,6 @@ YAML::Node fetch_getipintel(const Proxy_Check_Task& task, std::string_view ip) { const auto safe_url = std::format("{}?ip={}&contact=***&format=json&flags=f", getipintel_check_url, ip); return make_json_provider_result(http_get_cached(task, url, "getipintel", "json", main_request_timeout_ms, normal_response_limit, safe_url), "getipintel"); } - YAML::Node fetch_abuseipdb(const Proxy_Check_Task& task, std::string_view ip) { const auto api_key = config_value_or_env(g_ip_check_api_config.abuseipdb_api_key, abuseipdb_api_key_env); if (api_key.empty()) { @@ -667,7 +651,6 @@ YAML::Node fetch_abuseipdb(const Proxy_Check_Task& task, std::string_view ip) { }; return make_json_provider_result(http_get_cached(task, url, "abuseipdb", "json", main_request_timeout_ms, normal_response_limit, url, headers), "abuseipdb"); } - YAML::Node fetch_vpnapi_io(const Proxy_Check_Task& task, std::string_view ip) { const auto api_key = config_value_or_env(g_ip_check_api_config.vpnapi_io_key, vpnapi_io_key_env); if (api_key.empty()) { @@ -677,7 +660,6 @@ YAML::Node fetch_vpnapi_io(const Proxy_Check_Task& task, std::string_view ip) { const auto safe_url = std::format("{}{}?key=***", vpnapi_io_url_prefix, ip); return make_json_provider_result(http_get_cached(task, url, "vpnapi_io", "json", main_request_timeout_ms, normal_response_limit, safe_url), "vpnapi_io"); } - YAML::Node fetch_ipqualityscore(const Proxy_Check_Task& task, std::string_view ip) { const auto api_key = config_value_or_env(g_ip_check_api_config.ipqualityscore_key, ipqualityscore_key_env); if (api_key.empty()) { @@ -690,8 +672,7 @@ YAML::Node fetch_ipqualityscore(const Proxy_Check_Task& task, std::string_view i YAML::Node check_services(const Proxy_Check_Task& task) { static constexpr Service_Check services[] = { {"google", "https://www.gstatic.com/generate_204", "google_204", "txt"}, - {"github", "https://github.com/favicon.ico", "github_favicon", "bin"}, - {"chatgpt", "https://chatgpt.com/favicon.ico", "chatgpt_favicon", "bin"} + {"github", "https://github.com/favicon.ico", "github_favicon", "bin"} }; YAML::Node result(YAML::NodeType::Map); for (const auto& service : services) { @@ -733,7 +714,6 @@ int scalar_to_int(const YAML::Node& value, int fallback = 0) { } } } - double scalar_to_double(const YAML::Node& value, double fallback = 0.0) { if (!value || !value.IsScalar()) { return fallback; @@ -812,7 +792,6 @@ int calculate_risk_score(const YAML::Node& ipapi_is, if (read_bool(ipapi_is, "is_crawler")) { add_score(score, reasons, 20, "ipapi.is 标记为 crawler"); } - if (read_nested_bool(ipwho_is, {"security", "tor"})) { add_score(score, reasons, 100, "ipwho.is 标记为 Tor"); } @@ -831,14 +810,12 @@ int calculate_risk_score(const YAML::Node& ipapi_is, if (read_nested_bool(ipwho_is, {"security", "hosting"})) { add_score(score, reasons, 8, "ipwho.is 标记为 hosting"); } - if (read_bool(ip_api_com, "proxy")) { add_score(score, reasons, 25, "ip-api.com 标记为 proxy"); } if (read_bool(ip_api_com, "hosting")) { add_score(score, reasons, 8, "ip-api.com 标记为 hosting"); } - const auto pc_ip_node = find_yaml_value(proxycheck, ip); if (pc_ip_node) { const auto pc_proxy = lower_ascii(scalar_to_string(find_yaml_value(pc_ip_node, "proxy"))); @@ -860,7 +837,6 @@ int calculate_risk_score(const YAML::Node& ipapi_is, add_score(score, reasons, std::min(pc_risk, 60), std::format("proxycheck.io risk={}", pc_risk)); } } - const auto ping0_risk = scalar_to_int(find_yaml_value(ping0_detail, "iprisk"), -1); if (ping0_risk >= 0) { add_score(score, reasons, std::min(std::max(ping0_risk, 0), 70), std::format("ping0.cc iprisk={}", ping0_risk)); @@ -873,13 +849,11 @@ int calculate_risk_score(const YAML::Node& ipapi_is, if (ping0_isnative && ping0_isnative.IsScalar() && !ping0_isnative.as()) { add_score(score, reasons, 8, "ping0.cc 标记为非原生 IP / 广播 IP"); } - const auto getipintel_probability = scalar_to_double(find_yaml_value(getipintel, "result"), -1.0); if (getipintel_probability >= 0.0) { const auto getipintel_score = std::min(80, std::max(0, static_cast(getipintel_probability * 100.0 + 0.5))); add_score(score, reasons, getipintel_score, std::format("GetIPIntel result={:.3f}", getipintel_probability)); } - const auto abuse_confidence = scalar_to_int(find_nested_yaml_value(abuseipdb, {"data", "abuseConfidenceScore"}), -1); if (abuse_confidence > 0) { add_score(score, reasons, std::min(abuse_confidence, 70), std::format("AbuseIPDB abuseConfidenceScore={}", abuse_confidence)); @@ -887,7 +861,6 @@ int calculate_risk_score(const YAML::Node& ipapi_is, if (read_nested_bool(abuseipdb, {"data", "isTor"})) { add_score(score, reasons, 100, "AbuseIPDB 标记为 Tor"); } - if (read_nested_bool(vpnapi_io, {"security", "tor"})) { add_score(score, reasons, 100, "VPNAPI.io 标记为 Tor"); } @@ -900,7 +873,6 @@ int calculate_risk_score(const YAML::Node& ipapi_is, if (read_nested_bool(vpnapi_io, {"security", "relay"})) { add_score(score, reasons, 15, "VPNAPI.io 标记为 relay/private relay"); } - const auto ipqs_fraud_score = scalar_to_int(find_yaml_value(ipqualityscore, "fraud_score"), -1); if (ipqs_fraud_score > 0) { add_score(score, reasons, std::min(ipqs_fraud_score, 80), std::format("IPQualityScore fraud_score={}", ipqs_fraud_score)); @@ -920,7 +892,6 @@ int calculate_risk_score(const YAML::Node& ipapi_is, if (read_bool(ipqualityscore, "bot_status")) { add_score(score, reasons, 25, "IPQualityScore 标记为 bot_status"); } - return std::min(score, 100); } std::string_view risk_grade(int score) { @@ -935,7 +906,6 @@ std::string_view risk_grade(int score) { } return "very_high"; } - std::string_view chatgpt_score_grade(int score) { if (score >= 85) { return "excellent"; @@ -1018,7 +988,6 @@ YAML::Node calculate_chatgpt_suitability_score(const YAML::Node& ipapi_is, std::string_view ip) { int running_score = 100; YAML::Node factors(YAML::NodeType::Sequence); - add_chatgpt_score_factor_if(factors, running_score, read_bool(ipapi_is, "is_bogon"), "ipapi.is", "is_bogon", "true", -90, "私有/保留/异常地址,不适合作为公网 ChatGPT 出口"); add_chatgpt_score_factor_if(factors, running_score, read_bool(ipapi_is, "is_tor"), "ipapi.is", "is_tor", "true", -90, "Tor 出口属于强匿名网络,ChatGPT 登录环境风险极高"); add_chatgpt_score_factor_if(factors, running_score, read_bool(ipapi_is, "is_abuser"), "ipapi.is", "is_abuser", "true", -30, "存在滥用画像,降低账号环境稳定性"); @@ -1026,17 +995,14 @@ YAML::Node calculate_chatgpt_suitability_score(const YAML::Node& ipapi_is, add_chatgpt_score_factor_if(factors, running_score, read_bool(ipapi_is, "is_vpn"), "ipapi.is", "is_vpn", "true", -14, "被识别为 VPN 出口"); add_chatgpt_score_factor_if(factors, running_score, read_bool(ipapi_is, "is_datacenter"), "ipapi.is", "is_datacenter", "true", -6, "机房/托管 IP 通常比住宅/企业固定宽带更容易被风控识别"); add_chatgpt_score_factor_if(factors, running_score, read_bool(ipapi_is, "is_crawler"), "ipapi.is", "is_crawler", "true", -12, "爬虫画像会降低可信度"); - add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(ipwho_is, {"security", "tor"}), "ipwho.is", "security.tor", "true", -90, "Tor 标记是强风险信号"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(ipwho_is, {"security", "proxy"}), "ipwho.is", "security.proxy", "true", -12, "被识别为代理出口"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(ipwho_is, {"security", "vpn"}), "ipwho.is", "security.vpn", "true", -10, "被识别为 VPN 出口"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(ipwho_is, {"security", "anonymous"}), "ipwho.is", "security.anonymous", "true", -8, "匿名网络画像会增加登录环境风险"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(ipwho_is, {"security", "threat"}), "ipwho.is", "security.threat", "true", -35, "威胁标记是强风险信号"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(ipwho_is, {"security", "hosting"}), "ipwho.is", "security.hosting", "true", -5, "托管/机房画像轻度扣分"); - add_chatgpt_score_factor_if(factors, running_score, read_bool(ip_api_com, "proxy"), "ip-api.com", "proxy", "true", -18, "ip-api.com 认为该 IP 是代理出口"); add_chatgpt_score_factor_if(factors, running_score, read_bool(ip_api_com, "hosting"), "ip-api.com", "hosting", "true", -5, "hosting 标记说明该 IP 更像机房/托管环境"); - const auto pc_ip_node = find_yaml_value(proxycheck, ip); if (pc_ip_node) { const auto pc_proxy = lower_ascii(scalar_to_string(find_yaml_value(pc_ip_node, "proxy"))); @@ -1057,7 +1023,6 @@ YAML::Node calculate_chatgpt_suitability_score(const YAML::Node& ipapi_is, add_chatgpt_score_factor(factors, running_score, "proxycheck.io", "risk", std::to_string(pc_risk), delta, "proxycheck 风险分按 30% 权重折算,最高扣 25 分,避免单源直接打死"); } } - const auto ping0_risk = scalar_to_int(find_yaml_value(ping0_detail, "iprisk"), -1); if (ping0_risk >= 0) { const auto delta = -std::min(35, std::max(0, static_cast(ping0_risk * 0.50 + 0.5))); @@ -1078,7 +1043,6 @@ YAML::Node calculate_chatgpt_suitability_score(const YAML::Node& ipapi_is, add_chatgpt_score_factor_if(factors, running_score, !ping0_isnative.as(), "ping0.cc", "isnative", bool_text(ping0_isnative.as()), -6, "非原生/广播 IP 轻度扣分"); add_chatgpt_score_factor_if(factors, running_score, ping0_isnative.as(), "ping0.cc", "isnative", bool_text(ping0_isnative.as()), 3, "原生 IP 给少量加分"); } - const auto getipintel_probability = scalar_to_double(find_yaml_value(getipintel, "result"), -1.0); if (getipintel_probability >= 0.0) { if (getipintel_probability >= 0.99) { @@ -1094,19 +1058,16 @@ YAML::Node calculate_chatgpt_suitability_score(const YAML::Node& ipapi_is, add_chatgpt_score_factor(factors, running_score, "GetIPIntel", "result", std::format("{:.3f}", getipintel_probability), 5, "GetIPIntel 概率较低,给少量加分"); } } - const auto abuse_confidence = scalar_to_int(find_nested_yaml_value(abuseipdb, {"data", "abuseConfidenceScore"}), -1); if (abuse_confidence > 0) { const auto delta = -std::min(40, std::max(1, static_cast(abuse_confidence * 0.50 + 0.5))); add_chatgpt_score_factor(factors, running_score, "AbuseIPDB", "abuseConfidenceScore", std::to_string(abuse_confidence), delta, "恶意行为举报置信度按 50% 权重折算,最高扣 40 分"); } add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(abuseipdb, {"data", "isTor"}), "AbuseIPDB", "isTor", "true", -90, "AbuseIPDB 标记为 Tor"); - add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(vpnapi_io, {"security", "tor"}), "VPNAPI.io", "security.tor", "true", -90, "Tor 标记是强风险信号"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(vpnapi_io, {"security", "proxy"}), "VPNAPI.io", "security.proxy", "true", -14, "VPNAPI.io 标记为 proxy"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(vpnapi_io, {"security", "vpn"}), "VPNAPI.io", "security.vpn", "true", -12, "VPNAPI.io 标记为 VPN"); add_chatgpt_score_factor_if(factors, running_score, read_nested_bool(vpnapi_io, {"security", "relay"}), "VPNAPI.io", "security.relay", "true", -8, "relay/private relay 轻中度扣分"); - const auto ipqs_fraud_score = scalar_to_int(find_yaml_value(ipqualityscore, "fraud_score"), -1); if (ipqs_fraud_score > 0) { const auto delta = -std::min(35, std::max(1, static_cast(ipqs_fraud_score * 0.40 + 0.5))); @@ -1117,7 +1078,6 @@ YAML::Node calculate_chatgpt_suitability_score(const YAML::Node& ipapi_is, add_chatgpt_score_factor_if(factors, running_score, read_bool(ipqualityscore, "vpn"), "IPQualityScore", "vpn", "true", -12, "IPQS 标记为 VPN"); add_chatgpt_score_factor_if(factors, running_score, read_bool(ipqualityscore, "recent_abuse"), "IPQualityScore", "recent_abuse", "true", -25, "IPQS 标记 recent_abuse"); add_chatgpt_score_factor_if(factors, running_score, read_bool(ipqualityscore, "bot_status"), "IPQualityScore", "bot_status", "true", -12, "IPQS 标记 bot_status"); - const auto google_accessible = read_nested_bool(services, {"google", "accessible"}); const auto github_accessible = read_nested_bool(services, {"github", "accessible"}); const auto chatgpt_accessible = read_nested_bool(services, {"chatgpt", "accessible"}); @@ -1136,7 +1096,6 @@ YAML::Node calculate_chatgpt_suitability_score(const YAML::Node& ipapi_is, add_chatgpt_score_factor(factors, running_score, "connectivity", "chatgpt.latency_ms", std::to_string(latency), -8, "ChatGPT 延迟超过 5000ms,体验一般"); } } - const auto final_score = clamp_chatgpt_score(running_score); YAML::Node result(YAML::NodeType::Map); result["base_score"] = 100; @@ -1177,37 +1136,37 @@ std::string make_chatgpt_score_console_report(const YAML::Node& result) { const auto ip_api_com_provider = find_yaml_value(providers, "ip_api_com"); const auto ip_api_com = provider_data(ip_api_com_provider); stream << " ip-api.com: proxy=" << yaml_node_to_inline_text(find_yaml_value(ip_api_com, "proxy")) - << ", hosting=" << yaml_node_to_inline_text(find_yaml_value(ip_api_com, "hosting")) - << ", isp=" << yaml_node_to_inline_text(find_yaml_value(ip_api_com, "isp")) - << ", org=" << yaml_node_to_inline_text(find_yaml_value(ip_api_com, "org")) << "\n"; + << ", hosting=" << yaml_node_to_inline_text(find_yaml_value(ip_api_com, "hosting")) + << ", isp=" << yaml_node_to_inline_text(find_yaml_value(ip_api_com, "isp")) + << ", org=" << yaml_node_to_inline_text(find_yaml_value(ip_api_com, "org")) << "\n"; const auto proxycheck_provider = find_yaml_value(providers, "proxycheck_io"); const auto proxycheck = provider_data(proxycheck_provider); const auto public_ip = scalar_to_string(find_yaml_value(network, "ipv4")); const auto pc_ip_node = find_yaml_value(proxycheck, public_ip); stream << " proxycheck.io: proxy=" << yaml_node_to_inline_text(find_yaml_value(pc_ip_node, "proxy")) - << ", type=" << yaml_node_to_inline_text(find_yaml_value(pc_ip_node, "type")) - << ", risk=" << yaml_node_to_inline_text(find_yaml_value(pc_ip_node, "risk")) << "\n"; + << ", type=" << yaml_node_to_inline_text(find_yaml_value(pc_ip_node, "type")) + << ", risk=" << yaml_node_to_inline_text(find_yaml_value(pc_ip_node, "risk")) << "\n"; const auto ping0_detail = provider_data(find_yaml_value(providers, "ping0_detail")); stream << " ping0.detail: iprisk=" << yaml_node_to_inline_text(find_yaml_value(ping0_detail, "iprisk")) - << ", isidc=" << yaml_node_to_inline_text(find_yaml_value(ping0_detail, "isidc")) - << ", isnative=" << yaml_node_to_inline_text(find_yaml_value(ping0_detail, "isnative")) << "\n"; + << ", isidc=" << yaml_node_to_inline_text(find_yaml_value(ping0_detail, "isidc")) + << ", isnative=" << yaml_node_to_inline_text(find_yaml_value(ping0_detail, "isnative")) << "\n"; const auto getipintel = provider_data(find_yaml_value(providers, "getipintel")); stream << " GetIPIntel: result=" << yaml_node_to_inline_text(find_yaml_value(getipintel, "result")) << "\n"; const auto abuse_data = find_yaml_value(provider_data(find_yaml_value(providers, "abuseipdb")), "data"); stream << " AbuseIPDB: abuseConfidenceScore=" << yaml_node_to_inline_text(find_yaml_value(abuse_data, "abuseConfidenceScore")) - << ", isTor=" << yaml_node_to_inline_text(find_yaml_value(abuse_data, "isTor")) << "\n"; + << ", isTor=" << yaml_node_to_inline_text(find_yaml_value(abuse_data, "isTor")) << "\n"; const auto vpnapi_io = provider_data(find_yaml_value(providers, "vpnapi_io")); stream << " VPNAPI.io security: " << yaml_node_to_inline_text(find_yaml_value(vpnapi_io, "security")) << "\n"; const auto ipqualityscore = provider_data(find_yaml_value(providers, "ipqualityscore")); stream << " IPQualityScore: fraud_score=" << yaml_node_to_inline_text(find_yaml_value(ipqualityscore, "fraud_score")) - << ", proxy=" << yaml_node_to_inline_text(find_yaml_value(ipqualityscore, "proxy")) - << ", vpn=" << yaml_node_to_inline_text(find_yaml_value(ipqualityscore, "vpn")) - << ", recent_abuse=" << yaml_node_to_inline_text(find_yaml_value(ipqualityscore, "recent_abuse")) << "\n"; + << ", proxy=" << yaml_node_to_inline_text(find_yaml_value(ipqualityscore, "proxy")) + << ", vpn=" << yaml_node_to_inline_text(find_yaml_value(ipqualityscore, "vpn")) + << ", recent_abuse=" << yaml_node_to_inline_text(find_yaml_value(ipqualityscore, "recent_abuse")) << "\n"; const auto connectivity = find_yaml_value(result, "connectivity"); const auto chatgpt = find_yaml_value(connectivity, "chatgpt"); stream << " ChatGPT 连通性: accessible=" << yaml_node_to_inline_text(find_yaml_value(chatgpt, "accessible")) - << ", http_status=" << yaml_node_to_inline_text(find_yaml_value(chatgpt, "http_status")) - << ", latency_ms=" << yaml_node_to_inline_text(find_yaml_value(chatgpt, "latency_ms")) << "\n"; + << ", http_status=" << yaml_node_to_inline_text(find_yaml_value(chatgpt, "http_status")) + << ", latency_ms=" << yaml_node_to_inline_text(find_yaml_value(chatgpt, "latency_ms")) << "\n"; stream << "基础分: " << scalar_to_int(find_yaml_value(score_node, "base_score"), 100) << "\n"; stream << "计算过程:\n"; const auto factors = find_yaml_value(score_node, "factors"); @@ -1215,12 +1174,12 @@ std::string make_chatgpt_score_console_report(const YAML::Node& result) { for (const auto& factor : factors) { const auto delta = scalar_to_int(find_yaml_value(factor, "delta"), 0); stream << " [" << scalar_to_string(find_yaml_value(factor, "source")) << "] " - << scalar_to_string(find_yaml_value(factor, "parameter")) - << "=" << scalar_to_string(find_yaml_value(factor, "observed")) - << ",delta=" << (delta > 0 ? "+" : "") << delta - << ",分数 " << scalar_to_int(find_yaml_value(factor, "score_before"), 0) - << " -> " << scalar_to_int(find_yaml_value(factor, "score_after_unclamped"), 0) - << ",原因: " << scalar_to_string(find_yaml_value(factor, "explanation")) << "\n"; + << scalar_to_string(find_yaml_value(factor, "parameter")) + << "=" << scalar_to_string(find_yaml_value(factor, "observed")) + << ",delta=" << (delta > 0 ? "+" : "") << delta + << ",分数 " << scalar_to_int(find_yaml_value(factor, "score_before"), 0) + << " -> " << scalar_to_int(find_yaml_value(factor, "score_after_unclamped"), 0) + << ",原因: " << scalar_to_string(find_yaml_value(factor, "explanation")) << "\n"; } } else { @@ -1243,7 +1202,6 @@ void copy_if_present(YAML::Node& destination, std::string_view destination_key, YAML::Node make_quality_result(const Proxy_Check_Task& task) { long long ipify_latency_ms = 0; const auto public_ip = detect_public_ipv4(task, ipify_latency_ms); - const auto ipapi_is_provider = fetch_ipapi_is(task, public_ip); const auto ipwho_is_provider = fetch_ipwho_is(task, public_ip); const auto ip_api_com_provider = fetch_ip_api_com(task, public_ip); @@ -1256,7 +1214,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { const auto vpnapi_io_provider = fetch_vpnapi_io(task, public_ip); const auto ipqualityscore_provider = fetch_ipqualityscore(task, public_ip); const auto services = check_services(task); - const auto ipapi_is = provider_data(ipapi_is_provider); const auto ipwho_is = provider_data(ipwho_is_provider); const auto ip_api_com = provider_data(ip_api_com_provider); @@ -1267,7 +1224,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { const auto abuseipdb = provider_data(abuseipdb_provider); const auto vpnapi_io = provider_data(vpnapi_io_provider); const auto ipqualityscore = provider_data(ipqualityscore_provider); - YAML::Node result(YAML::NodeType::Map); result["name"] = task.name; result["username"] = task.username; @@ -1286,7 +1242,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { optional_api_envs["ipqualityscore_key"] = std::string(ipqualityscore_key_env); cache_info["optional_api_envs"] = optional_api_envs; result["cache"] = cache_info; - YAML::Node network(YAML::NodeType::Map); network["ipv4"] = public_ip; network["ipify_latency_ms"] = ipify_latency_ms; @@ -1306,12 +1261,11 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { network["ping0_detail_ip_consistent"] = ping0_detail_ip.as() == public_ip; } const auto all_seen_ip_consistent = (!cloudflare_ip || !cloudflare_ip.IsScalar() || cloudflare_ip.as() == public_ip) - && (!ping0_ip || !ping0_ip.IsScalar() || ping0_ip.as() == public_ip) - && (!ping0_detail_ip || !ping0_detail_ip.IsScalar() || ping0_detail_ip.as() == public_ip); + && (!ping0_ip || !ping0_ip.IsScalar() || ping0_ip.as() == public_ip) + && (!ping0_detail_ip || !ping0_detail_ip.IsScalar() || ping0_detail_ip.as() == public_ip); network["all_seen_ip_consistent"] = all_seen_ip_consistent; network["ip_consistent"] = all_seen_ip_consistent; result["network"] = network; - YAML::Node providers(YAML::NodeType::Map); providers["ipapi_is"] = ipapi_is_provider; providers["ipwho_is"] = ipwho_is_provider; @@ -1324,7 +1278,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { providers["vpnapi_io"] = vpnapi_io_provider; providers["ipqualityscore"] = ipqualityscore_provider; result["providers"] = providers; - YAML::Node quality(YAML::NodeType::Map); static constexpr std::array ipapi_quality_fields = { "is_bogon", @@ -1383,7 +1336,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { copy_if_present(ping0_detail_summary, "orgtype", ping0_detail, "orgtype"); quality["ping0_detail"] = ping0_detail_summary; } - if (getipintel) { YAML::Node getipintel_summary(YAML::NodeType::Map); copy_if_present(getipintel_summary, "result", getipintel, "result"); @@ -1421,7 +1373,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { copy_if_present(ipqs_summary, "organization", ipqualityscore, "organization"); quality["ipqualityscore"] = ipqs_summary; } - YAML::Node reasons(YAML::NodeType::Sequence); const auto legacy_risk_score = calculate_risk_score(ipapi_is, ipwho_is, ip_api_com, proxycheck, ping0_detail, getipintel, abuseipdb, vpnapi_io, ipqualityscore, public_ip, reasons); const auto chatgpt_score = calculate_chatgpt_suitability_score(ipapi_is, ipwho_is, ip_api_com, proxycheck, ping0_detail, getipintel, abuseipdb, vpnapi_io, ipqualityscore, services, public_ip); @@ -1437,7 +1388,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { quality["legacy_strict_risk_reasons"] = reasons; quality["score_note"] = "chatgpt_node_score 是本程序用于判断是否适合作为 ChatGPT 节点的加权分,0-100 越高越适合;legacy_strict_* 保留旧版严格累加算法作为参考,不代表 OpenAI 官方风控分。"; result["quality"] = quality; - YAML::Node summary(YAML::NodeType::Map); copy_if_present(summary, "ipapi_rir", ipapi_is, "rir"); copy_if_present(summary, "ipapi_location", ipapi_is, "location"); @@ -1474,7 +1424,6 @@ YAML::Node make_quality_result(const Proxy_Check_Task& task) { copy_if_present(summary, "ping0_detail_asntype", ping0_detail, "asntype"); copy_if_present(summary, "ping0_detail_orgtype", ping0_detail, "orgtype"); result["summary"] = summary; - YAML::Node connectivity(YAML::NodeType::Map); connectivity["cloudflare"] = cloudflare; for (const auto& service : services) { @@ -1511,7 +1460,7 @@ void save_yaml_file(const fs::path& path, const YAML::Node& result) { fs::rename(temporary_path, path, error); if (error) { fs::remove(temporary_path); - throw std::runtime_error(std::format("保存结果文件失败:{},{}", path_to_utf8(path), error.message())); + throw std::runtime_error(std::format("保存结果文件失败:{},{}", path_to_utf8(path), error_text_to_utf8(error.message()))); } } } @@ -1538,12 +1487,27 @@ bool execute_proxy_check(const Proxy_Check_Task& task, std::mutex& output_mutex) temporary_path += L".tmp"; fs::remove(temporary_path, remove_error); std::lock_guard lock(output_mutex); - std::cerr << std::format("{} 检测失败:{}\n", task.name, error.what()); + std::cerr << std::format("{} 检测失败:{}\n", task.name, error_text_to_utf8(error.what())); return false; } } - -void check_proxies_impl() { +struct Proxy_Check_Task_State { + bool success = false; +}; +asio::awaitable execute_proxy_check_task(asio::thread_pool& pool, + const Proxy_Check_Task& task, + std::mutex& output_mutex, + std::vector& states, + size_t index, + std::shared_ptr pending, + asio::steady_timer& signal) { + states[index].success = co_await WyC::async_blocking(pool, [&task, &output_mutex] { + return execute_proxy_check(task, output_mutex); + }); + --*pending; + signal.cancel(); +} +asio::awaitable check_proxies_async() { const fs::path root = path_from_utf8(get_executable_dir()); const fs::path test_result = (root / L"..\\IP_Check_Place").lexically_normal(); const fs::path current_config = (root / L"..\\config\\config.yaml").lexically_normal(); @@ -1555,35 +1519,46 @@ void check_proxies_impl() { const auto tasks = make_proxy_check_tasks(config, test_result); if (tasks.empty()) { std::cout << "没有需要检测的代理\n"; - return; + co_return; } const auto logical_cpu_count = std::max(1u, std::thread::hardware_concurrency()); const auto thread_count = std::min(tasks.size(), static_cast(std::min(logical_cpu_count, 8u))); std::mutex output_mutex; - std::atomic_size_t next_task{0}; - std::atomic_size_t failed_count{0}; - std::vector threads; - threads.reserve(thread_count); - std::cout << std::format("需要检测 {} 个代理,CPU 逻辑处理器 {} 个,使用 {} 个并发线程,raw 缓存命中时不重新请求网站\n", tasks.size(), logical_cpu_count, thread_count); - for (size_t i = 0; i < thread_count; ++i) { - threads.emplace_back([&] { - while (true) { - const auto index = next_task.fetch_add(1, std::memory_order_relaxed); - if (index >= tasks.size()) { - return; - } - if (!execute_proxy_check(tasks[index], output_mutex)) { - failed_count.fetch_add(1, std::memory_order_relaxed); - } - } - }); + std::vector states(tasks.size()); + auto pending = std::make_shared(tasks.size()); + auto executor = co_await asio::this_coro::executor; + asio::thread_pool pool(thread_count); + asio::steady_timer signal(executor); + std::cout << std::format("需要检测 {} 个代理,CPU 逻辑处理器 {} 个,使用 {} 个 Asio 池线程,raw 缓存命中时不重新请求网站\n", tasks.size(), logical_cpu_count, thread_count); + for (size_t i = 0; i < tasks.size(); ++i) { + asio::co_spawn(executor, execute_proxy_check_task(pool, tasks[i], output_mutex, states, i, pending, signal), asio::detached); } - for (auto& thread : threads) { - thread.join(); + while (*pending != 0) { + signal.expires_after(std::chrono::hours(24)); + std::error_code error; + co_await signal.async_wait(asio::redirect_error(asio::use_awaitable, error)); + } + pool.stop(); + pool.join(); + size_t failed = 0; + for (const auto& state : states) { + if (!state.success) { + ++failed; + } } - const auto failed = failed_count.load(std::memory_order_relaxed); std::cout << std::format("检测结束,成功 {} 个,失败 {} 个\n", tasks.size() - failed, failed); } +void check_proxies_impl() { + asio::io_context io(1); + std::exception_ptr error; + asio::co_spawn(io, check_proxies_async(), [&](std::exception_ptr current_error) { + error = current_error; + }); + io.run(); + if (error) { + std::rethrow_exception(error); + } +} } void set_ip_check_api_config(const Ip_Check_Api_Config& config) { g_ip_check_api_config = config; @@ -1660,6 +1635,6 @@ void check_proxies() { check_proxies_impl(); } catch (const std::exception& error) { - std::cerr << std::format("IP 检测初始化失败:{}\n", error.what()); + std::cerr << std::format("IP 检测初始化失败:{}\n", error_text_to_utf8(error.what())); } } diff --git a/src/base/Proxy_Ping0_Labeler.cpp b/src/base/Proxy_Ping0_Labeler.cpp index 902865c..599a125 100644 --- a/src/base/Proxy_Ping0_Labeler.cpp +++ b/src/base/Proxy_Ping0_Labeler.cpp @@ -2,18 +2,24 @@ #define CPPHTTPLIB_OPENSSL_SUPPORT #endif #include "Proxy_Ping0_Labeler.h" +#include "Async_Blocking.h" #include "Ping0_Html_Parser.h" #include "export.h" #include +#include #include #include #include #include #include +#include #include #include +#include #include #include +#include +#include #include #include #include @@ -57,25 +63,12 @@ struct Ai_Node_Info { size_t order = 0; }; std::vector ai_node_infos; +std::array public_ip_cache_locks; fs::path path_from_utf8(std::string_view value) { -#if defined(__cpp_char8_t) - std::u8string utf8; - utf8.reserve(value.size()); - for (const unsigned char ch : value) { - utf8.push_back(static_cast(ch)); - } - return fs::path(utf8); -#else - return fs::u8path(value.begin(), value.end()); -#endif + return ::path_from_utf8(value); } std::string path_to_utf8(const fs::path& path) { -#if defined(__cpp_char8_t) - const auto utf8 = path.generic_u8string(); - return std::string(reinterpret_cast(utf8.data()), utf8.size()); -#else - return path.generic_u8string(); -#endif + return ::path_to_utf8(path); } std::string trim_ascii(std::string value) { const auto first = value.find_first_not_of(" \t\r\n"); @@ -131,7 +124,7 @@ void write_file_atomic(const fs::path& path, std::string_view content) { fs::rename(temporary_path, path, error); if (error) { fs::remove(temporary_path); - throw std::runtime_error(std::format("保存文件失败:{},{}", path_to_utf8(path), error.message())); + throw std::runtime_error(std::format("保存文件失败:{},{}", path_to_utf8(path), error_text_to_utf8(error.message()))); } } } @@ -180,7 +173,7 @@ std::optional read_current_mixed_port(const fs::path& root) { return port.as(); } catch (const std::exception& error) { - std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:读取当前 Mihomo 配置失败:{},{}\n", path_to_utf8(current_config), error.what()); + std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:读取当前 Mihomo 配置失败:{},{}\n", path_to_utf8(current_config), error_text_to_utf8(error.what())); return std::nullopt; } } @@ -200,6 +193,9 @@ std::vector make_node_routes(const YAML::Node& proxies) { fs::path make_ip_cache_path(const fs::path& cache_dir, std::string_view proxy_name) { return cache_dir / path_from_utf8(make_safe_file_name(proxy_name)) / L"public_ip.yaml"; } +std::mutex& public_ip_cache_lock(const fs::path& path) { + return public_ip_cache_locks[std::hash{}(path_to_utf8(path)) % public_ip_cache_locks.size()]; +} Public_Ip_Cache load_cached_public_ips(const fs::path& path) { Public_Ip_Cache result; if (!fs::is_regular_file(path)) { @@ -269,6 +265,7 @@ void save_public_ip_cache(const fs::path& path, const Node_Route& route, const P } Public_Ip_Cache get_public_ips(const Node_Route& route, const fs::path& cache_dir, std::optional mixed_port) { const auto cache_path = make_ip_cache_path(cache_dir, route.proxy_name); + std::lock_guard lock(public_ip_cache_lock(cache_path)); auto cache = load_cached_public_ips(cache_path); bool changed = false; if (!mixed_port) { @@ -279,7 +276,7 @@ Public_Ip_Cache get_public_ips(const Node_Route& route, const fs::path& cache_di cache.ipv4 = query_public_ip_by_user(route.username, *mixed_port, ipv4_query_url_host); } catch (const std::exception& error) { - cache.ipv4_error = error.what(); + cache.ipv4_error = error_text_to_utf8(error.what()); } changed = true; } @@ -288,7 +285,7 @@ Public_Ip_Cache get_public_ips(const Node_Route& route, const fs::path& cache_di cache.ipv6 = query_public_ip_by_user(route.username, *mixed_port, ipv6_query_url_host); } catch (const std::exception& error) { - cache.ipv6_error = error.what(); + cache.ipv6_error = error_text_to_utf8(error.what()); } changed = true; } @@ -425,8 +422,32 @@ void record_ai_node_info(std::string name, const Node_Ping0_Result& result, size } ai_node_infos.push_back({std::move(name), country_from_location(result.info.location), ai_score, result.info.risk, order}); } +struct Node_Ping0_Task_Result { + Node_Route route; + std::optional result; + std::string error; +}; +asio::awaitable load_node_ping0_result_task(asio::thread_pool& pool, + std::vector& results, + size_t index, + fs::path ip_cache_dir, + fs::path html_cache_dir, + std::optional mixed_port, + std::shared_ptr pending, + asio::steady_timer& signal) { + try { + const auto route = results[index].route; + results[index].result = co_await WyC::async_blocking(pool, [route, ip_cache_dir, html_cache_dir, mixed_port] { + return load_node_ping0_result(route, ip_cache_dir, html_cache_dir, mixed_port); + }); + } + catch (const std::exception& error) { + results[index].error = error_text_to_utf8(error.what()); + } + --*pending; + signal.cancel(); } -void append_ping0_info_to_proxy_names(YAML::Node& proxies) { +asio::awaitable append_ping0_info_to_proxy_names_async(YAML::Node& proxies) { const fs::path root = path_from_utf8(get_executable_dir()); const auto ip_cache_dir = (root / L".." / L"Node_Exit_Ip_Cache").lexically_normal(); const auto html_cache_dir = (root / L".." / L"Ping0_Html_Cache").lexically_normal(); @@ -434,20 +455,55 @@ void append_ping0_info_to_proxy_names(YAML::Node& proxies) { fs::create_directories(html_cache_dir); ai_node_infos.clear(); const auto mixed_port = read_current_mixed_port(root); - for (const auto& route : make_node_routes(proxies)) { - try { - const auto result = load_node_ping0_result(route, ip_cache_dir, html_cache_dir, mixed_port); - if (!result) { - continue; - } - print_ping0_info(route, *result); - const auto new_name = route.proxy_name + make_proxy_name_suffix(*result); - proxies[route.index]["name"] = new_name; - record_ai_node_info(new_name, *result, route.index); + const auto routes = make_node_routes(proxies); + const auto logical_cpu_count = std::max(1u, std::thread::hardware_concurrency()); + const auto thread_count = std::min(routes.size(), static_cast(std::min(logical_cpu_count, 16u))); + if (routes.empty()) { + co_return; + } + asio::thread_pool pool(thread_count); + std::vector results(routes.size()); + for (size_t i = 0; i < routes.size(); ++i) { + results[i].route = routes[i]; + } + auto pending = std::make_shared(routes.size()); + auto executor = co_await asio::this_coro::executor; + asio::steady_timer signal(executor); + std::cout << std::format("Ping0 标注需要处理 {} 个节点,使用 {} 个 Asio 池线程\n", routes.size(), thread_count); + for (size_t i = 0; i < routes.size(); ++i) { + asio::co_spawn(executor, load_node_ping0_result_task(pool, results, i, ip_cache_dir, html_cache_dir, mixed_port, pending, signal), asio::detached); + } + while (*pending != 0) { + signal.expires_after(std::chrono::hours(24)); + std::error_code error; + co_await signal.async_wait(asio::redirect_error(asio::use_awaitable, error)); + } + pool.stop(); + pool.join(); + for (auto& item : results) { + if (!item.error.empty()) { + std::cerr << std::format("Ping0 标注失败:{},{}\n", item.route.proxy_name, item.error); + continue; } - catch (const std::exception& error) { - std::cerr << std::format("Ping0 标注失败:{},{}\n", route.proxy_name, error.what()); + if (!item.result) { + continue; } + print_ping0_info(item.route, *item.result); + const auto new_name = item.route.proxy_name + make_proxy_name_suffix(*item.result); + proxies[item.route.index]["name"] = new_name; + record_ai_node_info(new_name, *item.result, item.route.index); + } +} +} +void append_ping0_info_to_proxy_names(YAML::Node& proxies) { + asio::io_context io(1); + std::exception_ptr error; + asio::co_spawn(io, append_ping0_info_to_proxy_names_async(proxies), [&](std::exception_ptr current_error) { + error = current_error; + }); + io.run(); + if (error) { + std::rethrow_exception(error); } } std::vector make_ai_site_proxy_list() { diff --git a/src/base/export.cpp b/src/base/export.cpp index 5429a47..3256a06 100644 --- a/src/base/export.cpp +++ b/src/base/export.cpp @@ -15,24 +15,151 @@ std::string fetch_config_yml(const std::string& token) { } return res->body; } +#include #include #include +#include #include #include #ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif #include #else #include #include #endif +#ifdef _WIN32 +std::string utf16_to_utf8(std::wstring_view text) { + if (text.empty()) { + return {}; + } + const int required = WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast(text.size()), nullptr, 0, nullptr, nullptr); + if (required <= 0) { + return {}; + } + std::string result(static_cast(required), '\0'); + WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast(text.size()), result.data(), required, nullptr, nullptr); + return result; +} +std::wstring utf8_to_utf16(std::string_view text) { + if (text.empty()) { + return {}; + } + const int required = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), nullptr, 0); + if (required <= 0) { + return {}; + } + std::wstring result(static_cast(required), L'\0'); + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), static_cast(text.size()), result.data(), required); + return result; +} +#endif +bool is_valid_utf8(std::string_view text) { + size_t i = 0; + while (i < text.size()) { + const auto ch = static_cast(text[i]); + if (ch < 0x80) { + ++i; + continue; + } + size_t count = 0; + uint32_t code = 0; + if ((ch & 0xE0) == 0xC0) { + count = 2; + code = ch & 0x1F; + } + else if ((ch & 0xF0) == 0xE0) { + count = 3; + code = ch & 0x0F; + } + else if ((ch & 0xF8) == 0xF0) { + count = 4; + code = ch & 0x07; + } + else { + return false; + } + if (i + count > text.size()) { + return false; + } + for (size_t j = 1; j < count; ++j) { + const auto next = static_cast(text[i + j]); + if ((next & 0xC0) != 0x80) { + return false; + } + code = (code << 6) | (next & 0x3F); + } + if ((count == 2 && code < 0x80) || (count == 3 && code < 0x800) || (count == 4 && code < 0x10000) || code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF)) { + return false; + } + i += count; + } + return true; +} +std::string gbk_to_utf8(std::string_view text) { +#ifndef _WIN32 + return std::string(text); +#else + if (text.empty()) { + return {}; + } + const int wide_size = MultiByteToWideChar(936, 0, text.data(), static_cast(text.size()), nullptr, 0); + if (wide_size <= 0) { + return std::string(text); + } + std::wstring wide(static_cast(wide_size), L'\0'); + MultiByteToWideChar(936, 0, text.data(), static_cast(text.size()), wide.data(), wide_size); + return utf16_to_utf8(wide); +#endif +} +std::string native_to_utf8(std::string_view text) { +#ifndef _WIN32 + return std::string(text); +#else + if (is_valid_utf8(text)) { + return std::string(text); + } + return gbk_to_utf8(text); +#endif +} +std::string error_text_to_utf8(std::string_view text) { + return native_to_utf8(text); +} +std::filesystem::path path_from_utf8(std::string_view text) { +#ifdef _WIN32 + return std::filesystem::path(utf8_to_utf16(text)); +#elif defined(__cpp_char8_t) + std::u8string utf8; + utf8.reserve(text.size()); + for (const unsigned char ch : text) { + utf8.push_back(static_cast(ch)); + } + return std::filesystem::path(utf8); +#else + return std::filesystem::u8path(text.begin(), text.end()); +#endif +} +std::string path_to_utf8(const std::filesystem::path& path) { +#ifdef _WIN32 + return utf16_to_utf8(path.generic_wstring()); +#elif defined(__cpp_char8_t) + const auto text = path.generic_u8string(); + return std::string(reinterpret_cast(text.data()), text.size()); +#else + return path.generic_u8string(); +#endif +} std::string get_executable_dir() { #ifdef _WIN32 - char path[MAX_PATH]; - DWORD size = GetModuleFileNameA(nullptr, path, MAX_PATH); - if (size == 0 || size == MAX_PATH) { - throw std::runtime_error("GetModuleFileNameA failed"); + std::wstring buffer(32768, L'\0'); + const DWORD size = GetModuleFileNameW(nullptr, buffer.data(), static_cast(buffer.size())); + if (size == 0 || size >= buffer.size()) { + throw std::runtime_error("GetModuleFileNameW failed"); } - return std::filesystem::path(path).parent_path().string(); + buffer.resize(size); + return path_to_utf8(std::filesystem::path(buffer).parent_path()); #else char path[PATH_MAX]; ssize_t size = readlink("/proc/self/exe", path, sizeof(path) - 1); @@ -40,18 +167,18 @@ std::string get_executable_dir() { throw std::runtime_error("readlink /proc/self/exe failed"); } path[size] = '\0'; - return std::filesystem::path(path).parent_path().string(); + return path_to_utf8(std::filesystem::path(path).parent_path()); #endif } void write_file_content(const std::string& path, const std::string& content) { - std::ofstream ofs(path, std::ios::binary); + std::ofstream ofs(path_from_utf8(path), std::ios::binary); if (!ofs) { throw std::runtime_error("open file for write failed: " + path); } ofs.write(content.data(), static_cast(content.size())); } std::string read_file_content(const std::string& path) { - std::ifstream ifs(path, std::ios::binary); + std::ifstream ifs(path_from_utf8(path), std::ios::binary); if (!ifs) { throw std::runtime_error("open file for read failed: " + path); } diff --git a/src/base/export.h b/src/base/export.h index 3f7063a..d28191a 100644 --- a/src/base/export.h +++ b/src/base/export.h @@ -2,6 +2,8 @@ #include "global.h" std::string fetch_config_yml(const std::string& token); std::string get_executable_dir(); +std::filesystem::path path_from_utf8(std::string_view text); +std::string path_to_utf8(const std::filesystem::path& path); void write_file_content(const std::string& path, const std::string& content); std::string read_file_content(const std::string& path); // 工具函数 @@ -15,4 +17,6 @@ struct Atomic_Unit_Proxy_Group_Map { std::vector get_from_groups(const std::vector& group_list); std::vector proxy_list; }; - +std::string gbk_to_utf8(std::string_view text); +std::string native_to_utf8(std::string_view text); +std::string error_text_to_utf8(std::string_view text); diff --git a/src/base/global.h b/src/base/global.h index e93788a..bf9ce0c 100644 --- a/src/base/global.h +++ b/src/base/global.h @@ -1,7 +1,16 @@ #pragma once +#ifdef _WIN32 +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0A00 +#endif +#ifndef WINVER +#define WINVER 0x0A00 +#endif +#endif #include #include #include +#include #include "../3rd/httplib.h" #include #include diff --git a/src/cpp_filter/main.cpp b/src/cpp_filter/main.cpp index 0f93897..8730fc3 100644 --- a/src/cpp_filter/main.cpp +++ b/src/cpp_filter/main.cpp @@ -12,14 +12,14 @@ #include "base/Check_Ip.h" #include "base/groups.h" #include "base/Proxy_Ping0_Labeler.h" +#include "asio.hpp" std::string yaml_to_string(const YAML::Node& node) { YAML::Emitter out; out << YAML::Block << node; return out.c_str(); } void write_yaml_file(const std::string& path, const YAML::Node& node) { - std::ofstream out(path, std::ios::binary); - out << yaml_to_string(node); + write_file_content(path, yaml_to_string(node)); } #define Key_Word(kw) bool kw = contain(name, #kw); \ if (kw) map.push(#kw, name); @@ -59,28 +59,11 @@ void out_config_yml() { // check_proxies(proxies); auto base_props = props.atomic_unit_proxy_group_list(); for (auto& prop : base_props) { - auto cur = make_url_test_group(prop, props.data[prop]); - if ( - prop == "美国0.01倍" - || prop == "美国圣何塞" - || prop == "美国电信联通移动推荐" - ) { - cur["interval"] = 30; - cur["timeout"] = 3000; - cur["lazy"] = false; - cur["tolerance"] = 150; - cur["max-failed-times"] = 2; - } - //groups_node.push_back(make_smart_group(prop, props.data[prop])); + auto cur = make_select_group(prop, props.data[prop]); groups_node.push_back(cur); } auto download = "📥下载"; - auto download_yaml = make_fallback_group(download, props.get_from_groups({"🔥美国0.01倍", "🔥美国0.1倍", "🔥日本0.1倍", "🔥新加坡0.1倍"})); - download_yaml["interval"] = 30; - download_yaml["timeout"] = 3000; - download_yaml["lazy"] = false; - download_yaml["tolerance"] = 150; - download_yaml["max-failed-times"] = 2; + auto download_yaml = make_select_group(download, props.get_from_groups({"🔥美国0.01倍", "🔥美国0.1倍", "🔥日本0.1倍", "🔥新加坡0.1倍"})); groups_node.push_back(download_yaml); for (const auto& name : name_set) { if (name == "DIRECT") continue; @@ -94,12 +77,12 @@ void out_config_yml() { if (ai_site_list.empty()) { ai_site_list = props.get_from_groups({"🔥美国圣何塞"}); } - groups_node.push_back(make_fallback_group("🤖AI网站", ai_site_list)); + groups_node.push_back(make_select_group("🤖AI网站", ai_site_list)); std::vector proxy_group_list = {download, "DIRECT"}; for (const auto& proxy_group : props.proxy_list) { proxy_group_list.push_back(proxy_group); } - groups_node.push_back(make_url_test_group("️♻️自动选择", proxy_group_list, false)); + groups_node.push_back(make_select_group("️♻️自动选择", proxy_group_list)); groups_node.push_back(make_select_group("🚀节点选择", download + props.atomic_unit_proxy_group_list() + proxy_group_list)); std::vector list2{"🤖AI网站", "🚀节点选择"}; for (const auto& li : proxy_group_list) { diff --git a/src/node_service/Node_Service.h b/src/node_service/Node_Service.h new file mode 100644 index 0000000..9ca4005 --- /dev/null +++ b/src/node_service/Node_Service.h @@ -0,0 +1,264 @@ +#pragma once +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef CPPHTTPLIB_OPENSSL_SUPPORT +#define CPPHTTPLIB_OPENSSL_SUPPORT +#endif +#include "base/Async_Blocking.h" +#include "base/export.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif +namespace fs = std::filesystem; +using json = nlohmann::json; +inline constexpr double rto_alpha = 0.125; +inline constexpr double rto_beta = 0.25; +inline constexpr double rto_k = 4.0; +inline constexpr double rto_min_ms = 1000.0; +inline constexpr double rto_max_ms = 60000.0; +inline constexpr long mihomo_timeout_ms = 5000; +inline constexpr long mihomo_request_timeout_ms = 8000; +inline constexpr int min_check_interval_seconds = 5; +inline constexpr int max_check_interval_seconds = 3600; +struct Test_Target { + std::string name; + std::string url; + long timeout_ms = mihomo_timeout_ms; +}; +struct Auto_Switch_Config { + bool enabled = true; + bool skip_direct = true; + int switch_cooldown_seconds = 120; + int fail_switch_count = 2; + int better_confirm_rounds = 3; + double keep_score_threshold = 75.0; + double weak_score_threshold = 60.0; + double weak_improve_margin = 15.0; + double better_improve_margin = 10.0; + std::vector managed_groups; +}; +struct Service_Config { + std::string listen_host = "127.0.0.1"; + int listen_port = 18088; + std::string controller_origin = "http://127.0.0.1:7898"; + std::string controller_secret; + fs::path config_path; + fs::path state_dir; + fs::path web_dir; + fs::path service_config_path; + int check_interval_seconds = 60; + int first_delay_ms = 1000; + size_t worker_threads = 16; + Auto_Switch_Config auto_switch; + std::vector targets; +}; +struct Delay_Result { + bool success = false; + int delay_ms = 0; + int http_status = 0; + std::string error; + json raw = json::object(); +}; +struct Probe_Result { + std::string target; + std::string url; + Delay_Result result; +}; +struct Node_State { + std::string name; + std::string type; + std::string provider; + std::string exit_ipv4; + std::string exit_ipv6; + int ai_score = -1; + int risk = -1; + std::string risk_level; + std::string shared_users; + std::string ip_type; + std::string native_type; + std::string country; + bool alive = false; + bool initialized = false; + double srtt_ms = 0.0; + double rttvar_ms = 0.0; + double rto_ms = rto_min_ms; + double success_ewma = 0.0; + uint64_t samples = 0; + uint64_t success_count = 0; + uint64_t failure_count = 0; + uint64_t consecutive_failures = 0; + int last_delay_ms = 0; + int last_success_delay_ms = 0; + double stability_score = 0.0; + std::string last_error; + std::string last_check_time; + std::string updated_time; + std::vector probes; +}; +struct Proxy_Group_Def { + std::string name; + std::string type; + std::vector proxies; +}; +struct Group_Switch_State { + std::string name; + std::string type; + std::vector candidates; + std::string current; + std::string best_candidate; + int better_rounds = 0; + uint64_t switch_count = 0; + int64_t last_switch_unix = 0; + std::string last_switch_time; + std::string last_decision; + std::string last_error; + bool switchable = false; +}; +struct Controller_Proxy_Info { + bool found = false; + bool alive = false; + std::string type; + std::string provider; + json raw = json::object(); +}; +struct Service_State { + Service_Config config; + std::mutex mutex; + std::unordered_map nodes; + std::vector node_order; + std::unordered_map groups; + std::vector group_order; + std::atomic_bool running = true; + std::atomic_bool checking = false; + std::atomic_bool force_refresh = false; + std::atomic_size_t refresh_done_nodes = 0; + uint64_t round = 0; + uint64_t refresh_request_count = 0; + size_t refresh_total_nodes = 0; + std::string started_time; + std::string status_text; + std::string last_refresh_request_time; + std::string last_refresh_begin_time; + std::string last_refresh_end_time; +}; +fs::path path_from_utf8_string(std::string_view value); +std::string path_to_utf8_string(const fs::path& path); +std::string now_string(); +int64_t unix_seconds(); +std::string trim(std::string value); +std::string read_env(std::string_view key); +std::string url_encode(std::string_view value); +std::string make_safe_file_name(std::string_view value); +std::vector split_parts(std::string_view value, char delimiter); +bool starts_with(std::string_view value, std::string_view prefix); +bool ends_with(std::string_view value, std::string_view suffix); +int parse_int_prefix(std::string_view value, size_t& offset); +std::string to_string(std::string_view value); +std::string compact_country_from_name(std::string_view name); +void parse_node_metadata(Node_State& node); +bool is_label_metadata_part(std::string_view part); +std::string original_proxy_name_from_label(std::string_view name); +std::optional yaml_string_value(const YAML::Node& node, const char* key); +void load_node_ip_cache(Node_State& node); +void enrich_node_metadata(Node_State& node); +void write_text_atomic(const fs::path& path, std::string_view content); +json read_json_file(const fs::path& path); +void save_json_file(const fs::path& path, const json& value); +std::string read_binary_text(const fs::path& path); +std::string mime_type(const fs::path& path); +void set_file_response(const fs::path& file, httplib::Response& res); +void service_log(std::string_view message); +int normalize_check_interval(int value); +double normalize_score_threshold(double value); +int normalize_positive_int(int value, int min_value, int max_value); +json auto_switch_config_to_json(const Auto_Switch_Config& config); +void load_auto_switch_config(Auto_Switch_Config& config, const json& data); +json runtime_config_to_json(const Service_Config& config); +void load_runtime_config(Service_Config& config); +void save_runtime_config(const Service_Config& config); +fs::path default_config_path(); +fs::path default_web_dir(); +Service_Config make_config(int argc, char** argv); +fs::path node_state_path(const Service_State& service, std::string_view name); +json target_to_json(const Test_Target& target); +json probe_to_json(const Probe_Result& probe); +json group_to_json(const Group_Switch_State& group); +json node_to_json(const Node_State& node); +void load_node_state(Node_State& node, const fs::path& path); +void save_service_index(Service_State& service); +double clamp_score(double value); +void update_rto(Node_State& node, std::optional delay_ms); +void update_score(Node_State& node); +std::vector read_proxy_names(const fs::path& config_path); +std::vector read_proxy_groups(const fs::path& config_path); +bool contains_name(const std::vector& list, const std::string& name); +bool should_manage_group(const Auto_Switch_Config& config, const Proxy_Group_Def& group); +void reload_switch_groups_locked(Service_State& service); +void reload_switch_groups(Service_State& service); +httplib::Headers controller_headers(const Service_Config& config); +json controller_get_json(const Service_Config& config, const std::string& path); +Controller_Proxy_Info request_proxy_info(const Service_Config& config, const std::string& name); +Delay_Result request_proxy_delay(const Service_Config& config, const std::string& name, const Test_Target& target); +void controller_put_json(const Service_Config& config, const std::string& path, const json& body); +std::string request_group_current(const Service_Config& config, const std::string& group_name); +void switch_group_to_node(const Service_Config& config, const std::string& group_name, const std::string& node_name); +std::optional find_best_candidate_node(const std::unordered_map& nodes, const Group_Switch_State& group); +void reset_better_tracking(Group_Switch_State& group); +std::string score_text(const Node_State& node); +bool group_switch_cooldown_finished(const Auto_Switch_Config& config, const Group_Switch_State& group, int64_t now); +void note_group_decision(Service_State& service, const Group_Switch_State& group); +void perform_auto_switch(Service_State& service); +Service_Config config_snapshot(Service_State& service); +asio::awaitable>> check_node(Service_State& service, asio::thread_pool& pool, std::string name); +std::optional best_delay(const std::vector& probes); +std::string join_probe_errors(const std::vector& probes); +void update_node_state(Service_State& service, const std::string& name, const Controller_Proxy_Info& info, std::vector probes); +asio::awaitable check_node_task(Service_State& service, asio::thread_pool& pool, std::string name, std::shared_ptr pending, asio::steady_timer& signal); +int current_check_interval_seconds(Service_State& service); +asio::awaitable check_all_nodes_once(Service_State& service, asio::thread_pool& pool); +asio::awaitable run_auto_switch_once(Service_State& service, asio::thread_pool& pool); +asio::awaitable monitor_refresh_loop(Service_State& service, asio::thread_pool& pool); +json state_snapshot(Service_State& service); +std::string html_page(); +void start_web_server(Service_State& service, asio::io_context& io, httplib::Server& server); +void initialize_nodes(Service_State& service); diff --git a/src/node_service/Node_Service_Auto_Switch.cpp b/src/node_service/Node_Service_Auto_Switch.cpp new file mode 100644 index 0000000..2f9c843 --- /dev/null +++ b/src/node_service/Node_Service_Auto_Switch.cpp @@ -0,0 +1,147 @@ +#include "Node_Service.h" +std::optional find_best_candidate_node(const std::unordered_map& nodes, const Group_Switch_State& group) { + std::optional best; + for (const auto& name : group.candidates) { + const auto item = nodes.find(name); + if (item == nodes.end() || !item->second.alive) { + continue; + } + if (!best || item->second.stability_score > best->stability_score) { + best = item->second; + } + } + return best; +} +void reset_better_tracking(Group_Switch_State& group) { + group.best_candidate.clear(); + group.better_rounds = 0; +} +std::string score_text(const Node_State& node) { + return std::format("{} {:.1f}分 连续失败{}", node.name, node.stability_score, node.consecutive_failures); +} +bool group_switch_cooldown_finished(const Auto_Switch_Config& config, const Group_Switch_State& group, int64_t now) { + return config.switch_cooldown_seconds <= 0 || group.last_switch_unix == 0 || now - group.last_switch_unix >= config.switch_cooldown_seconds; +} +void note_group_decision(Service_State& service, const Group_Switch_State& group) { + auto& stored = service.groups[group.name]; + stored = group; +} +void perform_auto_switch(Service_State& service) { + Service_Config config; + std::unordered_map nodes; + std::vector groups; + { + std::lock_guard lock(service.mutex); + config = service.config; + nodes = service.nodes; + for (const auto& name : service.group_order) { + const auto item = service.groups.find(name); + if (item != service.groups.end()) { + groups.push_back(item->second); + } + } + } + service_log(std::format("自动切换判定:{} 个代理组", groups.size())); + if (!config.auto_switch.enabled) { + service_log("自动切换判定跳过:未启用"); + std::lock_guard lock(service.mutex); + for (auto& group : groups) { + group.last_decision = "自动切换未启用"; + note_group_decision(service, group); + } + save_service_index(service); + return; + } + const auto now = unix_seconds(); + for (auto& group : groups) { + group.last_error.clear(); + if (!group.switchable) { + group.last_decision = "没有可切换的直接节点候选"; + group.last_error = "代理组 proxies 里没有直接节点名"; + std::lock_guard lock(service.mutex); + note_group_decision(service, group); + service_log(std::format("代理组 {}:{},{}", group.name, group.last_decision, group.last_error)); + continue; + } + try { + group.current = request_group_current(config, group.name); + } + catch (const std::exception& error) { + group.last_error = error_text_to_utf8(error.what()); + group.last_decision = "读取当前选择失败"; + std::lock_guard lock(service.mutex); + note_group_decision(service, group); + service_log(std::format("代理组 {}:{},{}", group.name, group.last_decision, group.last_error)); + continue; + } + const auto best = find_best_candidate_node(nodes, group); + if (!best) { + group.last_decision = "没有 alive 候选节点"; + group.last_error = "候选节点全部不可用"; + std::lock_guard lock(service.mutex); + note_group_decision(service, group); + service_log(std::format("代理组 {}:{},{}", group.name, group.last_decision, group.last_error)); + continue; + } + const auto current_item = nodes.find(group.current); + const bool current_is_node = current_item != nodes.end(); + const bool can_switch_now = group_switch_cooldown_finished(config.auto_switch, group, now); + auto switch_to_best = [&](std::string_view reason) { + if (group.current == best->name) { + group.last_decision = std::format("{},当前已经是最佳节点 {}", reason, score_text(*best)); + reset_better_tracking(group); + return; + } + if (!can_switch_now) { + group.last_decision = std::format("{},但冷却中,当前 {},最佳 {}", reason, group.current, score_text(*best)); + return; + } + switch_group_to_node(config, group.name, best->name); + group.current = best->name; + ++group.switch_count; + group.last_switch_unix = now; + group.last_switch_time = now_string(); + group.last_decision = std::format("{},切换到 {}", reason, score_text(*best)); + reset_better_tracking(group); + }; + if (!current_is_node) { + switch_to_best("当前选择不是受监控直接节点"); + } + else { + const auto& current = current_item->second; + if (current.alive && current.stability_score >= config.auto_switch.keep_score_threshold) { + group.last_decision = std::format("当前节点稳定,不切换:{}", score_text(current)); + reset_better_tracking(group); + } + else if (current.consecutive_failures >= static_cast(config.auto_switch.fail_switch_count)) { + switch_to_best(std::format("当前节点连续失败达到 {} 次", current.consecutive_failures)); + } + else if (current.stability_score < config.auto_switch.weak_score_threshold && best->stability_score >= current.stability_score + config.auto_switch.weak_improve_margin) { + switch_to_best(std::format("当前节点低于弱分阈值:当前 {},最佳 {}", score_text(current), score_text(*best))); + } + else if (best->stability_score >= current.stability_score + config.auto_switch.better_improve_margin) { + if (group.best_candidate == best->name) { + ++group.better_rounds; + } + else { + group.best_candidate = best->name; + group.better_rounds = 1; + } + if (group.better_rounds >= config.auto_switch.better_confirm_rounds) { + switch_to_best(std::format("最佳节点连续 {} 轮明显更好:当前 {},最佳 {}", group.better_rounds, score_text(current), score_text(*best))); + } + else { + group.last_decision = std::format("最佳节点更好但未达到确认轮数:{}/{},当前 {},最佳 {}", group.better_rounds, config.auto_switch.better_confirm_rounds, score_text(current), score_text(*best)); + } + } + else { + group.last_decision = std::format("不满足切换阈值:当前 {},最佳 {}", score_text(current), score_text(*best)); + reset_better_tracking(group); + } + } + service_log(std::format("代理组 {}:{}", group.name, group.last_decision)); + std::lock_guard lock(service.mutex); + note_group_decision(service, group); + save_service_index(service); + } +} diff --git a/src/node_service/Node_Service_Config.cpp b/src/node_service/Node_Service_Config.cpp new file mode 100644 index 0000000..90fa4c2 --- /dev/null +++ b/src/node_service/Node_Service_Config.cpp @@ -0,0 +1,136 @@ +#include "Node_Service.h" +int normalize_check_interval(int value) { + return std::clamp(value, min_check_interval_seconds, max_check_interval_seconds); +} +double normalize_score_threshold(double value) { + return std::clamp(value, 0.0, 100.0); +} +int normalize_positive_int(int value, int min_value, int max_value) { + return std::clamp(value, min_value, max_value); +} +json auto_switch_config_to_json(const Auto_Switch_Config& config) { + json result; + result["enabled"] = config.enabled; + result["skip_direct"] = config.skip_direct; + result["switch_cooldown_seconds"] = config.switch_cooldown_seconds; + result["fail_switch_count"] = config.fail_switch_count; + result["better_confirm_rounds"] = config.better_confirm_rounds; + result["keep_score_threshold"] = config.keep_score_threshold; + result["weak_score_threshold"] = config.weak_score_threshold; + result["weak_improve_margin"] = config.weak_improve_margin; + result["better_improve_margin"] = config.better_improve_margin; + result["managed_groups"] = config.managed_groups; + return result; +} +void load_auto_switch_config(Auto_Switch_Config& config, const json& data) { + if (!data.is_object()) { + return; + } + config.enabled = data.value("enabled", config.enabled); + config.skip_direct = data.value("skip_direct", config.skip_direct); + config.switch_cooldown_seconds = normalize_positive_int(data.value("switch_cooldown_seconds", config.switch_cooldown_seconds), 0, 3600); + config.fail_switch_count = normalize_positive_int(data.value("fail_switch_count", config.fail_switch_count), 1, 100); + config.better_confirm_rounds = normalize_positive_int(data.value("better_confirm_rounds", config.better_confirm_rounds), 1, 100); + config.keep_score_threshold = normalize_score_threshold(data.value("keep_score_threshold", config.keep_score_threshold)); + config.weak_score_threshold = normalize_score_threshold(data.value("weak_score_threshold", config.weak_score_threshold)); + config.weak_improve_margin = normalize_score_threshold(data.value("weak_improve_margin", config.weak_improve_margin)); + config.better_improve_margin = normalize_score_threshold(data.value("better_improve_margin", config.better_improve_margin)); + if (data.contains("managed_groups") && data["managed_groups"].is_array()) { + config.managed_groups.clear(); + for (const auto& item : data["managed_groups"]) { + if (item.is_string()) { + auto name = trim(item.get()); + if (!name.empty()) { + config.managed_groups.push_back(std::move(name)); + } + } + } + } +} +json runtime_config_to_json(const Service_Config& config) { + json result; + result["listen_host"] = config.listen_host; + result["listen_port"] = config.listen_port; + result["controller_origin"] = config.controller_origin; + result["config_path"] = path_to_utf8_string(config.config_path); + result["state_dir"] = path_to_utf8_string(config.state_dir); + result["web_dir"] = path_to_utf8_string(config.web_dir); + result["service_config_path"] = path_to_utf8_string(config.service_config_path); + result["check_interval_seconds"] = config.check_interval_seconds; + result["delay_refresh_interval_seconds"] = config.check_interval_seconds; + result["first_delay_ms"] = config.first_delay_ms; + result["worker_threads"] = config.worker_threads; + result["auto_switch"] = auto_switch_config_to_json(config.auto_switch); + result["targets"] = json::array(); + for (const auto& target : config.targets) { + result["targets"].push_back(target_to_json(target)); + } + return result; +} +void load_runtime_config(Service_Config& config) { + const auto data = read_json_file(config.service_config_path); + if (!data.is_object()) { + return; + } + if (data.contains("check_interval_seconds") && data["check_interval_seconds"].is_number_integer()) { + config.check_interval_seconds = normalize_check_interval(data["check_interval_seconds"].get()); + } + else if (data.contains("delay_refresh_interval_seconds") && data["delay_refresh_interval_seconds"].is_number_integer()) { + config.check_interval_seconds = normalize_check_interval(data["delay_refresh_interval_seconds"].get()); + } + if (data.contains("auto_switch")) { + load_auto_switch_config(config.auto_switch, data["auto_switch"]); + } +} +void save_runtime_config(const Service_Config& config) { + save_json_file(config.service_config_path, runtime_config_to_json(config)); +} +fs::path default_config_path() { + const fs::path root = path_from_utf8_string(get_executable_dir()); + const fs::path out_config = (root / "../out_yml/config.yaml").lexically_normal(); + if (fs::is_regular_file(out_config)) { + return out_config; + } + return (root / "../yml/origin.yaml").lexically_normal(); +} +fs::path default_web_dir() { + const fs::path exe_dir = path_from_utf8_string(get_executable_dir()); + const std::vector candidates{ + (exe_dir / "../node_service_web/dist").lexically_normal(), + (exe_dir / "../node_service_web").lexically_normal(), + (exe_dir / "../src/node_service/web/dist").lexically_normal(), + (fs::current_path() / "../src/node_service/web/dist").lexically_normal(), + (fs::current_path() / "src/node_service/web/dist").lexically_normal(), + (fs::current_path() / "web/dist").lexically_normal() + }; + for (const auto& candidate : candidates) { + if (fs::is_regular_file(candidate / "index.html")) { + return candidate; + } + } + return candidates.front(); +} +Service_Config make_config(int argc, char** argv) { + Service_Config config; + const fs::path root = path_from_utf8_string(get_executable_dir()); + config.config_path = argc > 1 ? path_from_utf8_string(argv[1]) : default_config_path(); + config.state_dir = argc > 2 ? path_from_utf8_string(argv[2]) : (root / "../node_state").lexically_normal(); + config.service_config_path = (root / "../node_service_config" / "config.json").lexically_normal(); + config.web_dir = argc > 5 ? path_from_utf8_string(argv[5]) : default_web_dir(); + if (argc > 3) { + config.controller_origin = argv[3]; + } + if (argc > 4) { + config.listen_port = std::stoi(argv[4]); + } + const auto secret = read_env("MIHOMO_SECRET"); + if (!secret.empty()) { + config.controller_secret = secret; + } + config.targets.push_back({"gstatic", "https://www.gstatic.com/generate_204", 5000}); + config.targets.push_back({"cloudflare", "https://cp.cloudflare.com/generate_204", 5000}); + config.targets.push_back({"connectivity", "https://www.google.com/generate_204", 5000}); + load_runtime_config(config); + save_runtime_config(config); + return config; +} diff --git a/src/node_service/Node_Service_Controller.cpp b/src/node_service/Node_Service_Controller.cpp new file mode 100644 index 0000000..7191c42 --- /dev/null +++ b/src/node_service/Node_Service_Controller.cpp @@ -0,0 +1,72 @@ +#include "Node_Service.h" +httplib::Headers controller_headers(const Service_Config& config) { + httplib::Headers headers; + if (!config.controller_secret.empty()) { + headers.emplace("Authorization", "Bearer " + config.controller_secret); + } + return headers; +} +json controller_get_json(const Service_Config& config, const std::string& path) { + httplib::Client client(config.controller_origin); + client.set_follow_location(true); + client.set_connection_timeout(std::chrono::milliseconds(3000)); + client.set_read_timeout(std::chrono::milliseconds(mihomo_request_timeout_ms)); + const auto response = client.Get(path, controller_headers(config)); + if (!response) { + throw std::runtime_error(httplib::to_string(response.error())); + } + if (response->status < 200 || response->status >= 300) { + throw std::runtime_error(std::format("mihomo http status {}", response->status)); + } + return json::parse(response->body, nullptr, true, true); +} +Controller_Proxy_Info request_proxy_info(const Service_Config& config, const std::string& name) { + Controller_Proxy_Info info; + auto data = controller_get_json(config, "/proxies/" + url_encode(name)); + info.found = true; + info.raw = std::move(data); + info.alive = info.raw.value("alive", false); + info.type = info.raw.value("type", std::string{}); + info.provider = info.raw.value("provider", std::string{}); + return info; +} +Delay_Result request_proxy_delay(const Service_Config& config, const std::string& name, const Test_Target& target) { + Delay_Result result; + try { + const std::string path = "/proxies/" + url_encode(name) + "/delay?timeout=" + std::to_string(target.timeout_ms) + "&url=" + url_encode(target.url); + result.raw = controller_get_json(config, path); + result.delay_ms = result.raw.value("delay", 0); + result.success = result.delay_ms > 0; + result.http_status = 200; + if (!result.success) { + result.error = "delay is zero"; + } + } + catch (const std::exception& error) { + result.success = false; + result.error = error_text_to_utf8(error.what()); + } + return result; +} +void controller_put_json(const Service_Config& config, const std::string& path, const json& body) { + httplib::Client client(config.controller_origin); + client.set_follow_location(true); + client.set_connection_timeout(std::chrono::milliseconds(3000)); + client.set_read_timeout(std::chrono::milliseconds(mihomo_request_timeout_ms)); + auto headers = controller_headers(config); + headers.emplace("Content-Type", "application/json"); + const auto response = client.Put(path, headers, body.dump(), "application/json"); + if (!response) { + throw std::runtime_error(httplib::to_string(response.error())); + } + if (response->status < 200 || response->status >= 300) { + throw std::runtime_error(std::format("mihomo http status {}", response->status)); + } +} +std::string request_group_current(const Service_Config& config, const std::string& group_name) { + const auto data = controller_get_json(config, "/proxies/" + url_encode(group_name)); + return data.value("now", std::string{}); +} +void switch_group_to_node(const Service_Config& config, const std::string& group_name, const std::string& node_name) { + controller_put_json(config, "/proxies/" + url_encode(group_name), json{{"name", node_name}}); +} diff --git a/src/node_service/Node_Service_Monitor.cpp b/src/node_service/Node_Service_Monitor.cpp new file mode 100644 index 0000000..bc65b0e --- /dev/null +++ b/src/node_service/Node_Service_Monitor.cpp @@ -0,0 +1,168 @@ +#include "Node_Service.h" +Service_Config config_snapshot(Service_State& service) { + std::lock_guard lock(service.mutex); + return service.config; +} +asio::awaitable>> check_node(Service_State& service, asio::thread_pool& pool, std::string name) { + const auto config = config_snapshot(service); + co_return co_await WyC::async_blocking(pool, [config, name = std::move(name)] { + Controller_Proxy_Info info; + try { + info = request_proxy_info(config, name); + } + catch (...) { + info.found = false; + info.alive = false; + } + std::vector probes; + probes.reserve(config.targets.size()); + for (const auto& target : config.targets) { + probes.push_back({target.name, target.url, request_proxy_delay(config, name, target)}); + } + return std::make_pair(std::move(info), std::move(probes)); + }); +} +std::optional best_delay(const std::vector& probes) { + std::optional value; + for (const auto& probe : probes) { + if (!probe.result.success) { + continue; + } + if (!value || probe.result.delay_ms < *value) { + value = probe.result.delay_ms; + } + } + return value; +} +std::string join_probe_errors(const std::vector& probes) { + std::string result; + for (const auto& probe : probes) { + if (probe.result.success) { + continue; + } + if (!result.empty()) { + result += "; "; + } + result += probe.target + ":" + probe.result.error; + } + return result; +} +void update_node_state(Service_State& service, const std::string& name, const Controller_Proxy_Info& info, std::vector probes) { + std::string log_text; + { + std::lock_guard lock(service.mutex); + auto& node = service.nodes[name]; + node.name = name; + enrich_node_metadata(node); + node.alive = info.alive; + node.type = info.type; + node.provider = info.provider; + node.probes = std::move(probes); + node.last_check_time = now_string(); + node.updated_time = node.last_check_time; + const auto delay = best_delay(node.probes); + node.last_delay_ms = delay.value_or(0); + node.last_error = delay ? std::string{} : join_probe_errors(node.probes); + update_rto(node, delay); + update_score(node); + log_text = std::format("节点 delay 完成:{} alive={} delay={}ms score={:.1f} fail={} {}", node.name, node.alive, node.last_delay_ms, node.stability_score, node.consecutive_failures, node.last_error.empty() ? "" : node.last_error); + save_json_file(node_state_path(service, name), node_to_json(node)); + save_service_index(service); + } + service_log(log_text); +} +asio::awaitable check_node_task(Service_State& service, asio::thread_pool& pool, std::string name, std::shared_ptr pending, asio::steady_timer& signal) { + try { + auto [info, probes] = co_await check_node(service, pool, name); + update_node_state(service, name, info, std::move(probes)); + } + catch (const std::exception& error) { + Controller_Proxy_Info info; + info.found = false; + info.alive = false; + std::vector probes; + Probe_Result probe; + probe.target = "node_service"; + probe.result.success = false; + probe.result.error = error_text_to_utf8(error.what()); + probes.push_back(std::move(probe)); + update_node_state(service, name, info, std::move(probes)); + } + const auto done = service.refresh_done_nodes.fetch_add(1) + 1; + --*pending; + if (done == service.refresh_total_nodes || done % 5 == 0) { + service_log(std::format("全量 delay 进度:{}/{}", done, service.refresh_total_nodes)); + } + signal.cancel(); +} +int current_check_interval_seconds(Service_State& service) { + std::lock_guard lock(service.mutex); + return service.config.check_interval_seconds; +} +asio::awaitable check_all_nodes_once(Service_State& service, asio::thread_pool& pool) { + std::vector names; + { + std::lock_guard lock(service.mutex); + names = service.node_order; + service.refresh_total_nodes = names.size(); + service.refresh_done_nodes.store(0); + service.last_refresh_begin_time = now_string(); + service.status_text = names.empty() ? "没有节点需要刷新" : std::format("全量 delay 开始:{} 个节点", names.size()); + save_service_index(service); + } + service_log(names.empty() ? "全量 delay 跳过:没有节点" : std::format("全量 delay 开始:{} 个节点", names.size())); + if (names.empty()) { + co_return; + } + auto executor = co_await asio::this_coro::executor; + asio::steady_timer signal(executor); + auto pending = std::make_shared(names.size()); + for (auto& name : names) { + asio::co_spawn(executor, check_node_task(service, pool, std::move(name), pending, signal), asio::detached); + } + while (*pending != 0) { + signal.expires_after(std::chrono::hours(24)); + std::error_code error; + co_await signal.async_wait(asio::redirect_error(asio::use_awaitable, error)); + } + { + std::lock_guard lock(service.mutex); + ++service.round; + service.last_refresh_end_time = now_string(); + service.status_text = std::format("全量 delay 完成:轮次 {},节点 {}", service.round, names.size()); + save_service_index(service); + } + service_log(std::format("全量 delay 完成:轮次 {}", service.round)); +} +asio::awaitable run_auto_switch_once(Service_State& service, asio::thread_pool& pool) { + co_await WyC::async_blocking(pool, [&service] { + perform_auto_switch(service); + }); +} +asio::awaitable monitor_refresh_loop(Service_State& service, asio::thread_pool& pool) { + auto executor = co_await asio::this_coro::executor; + asio::steady_timer timer(executor); + timer.expires_after(std::chrono::milliseconds(service.config.first_delay_ms)); + std::error_code error; + co_await timer.async_wait(asio::redirect_error(asio::use_awaitable, error)); + while (service.running.load()) { + service.force_refresh.store(false); + service.checking.store(true); + co_await check_all_nodes_once(service, pool); + service_log("自动切换判定开始"); + co_await run_auto_switch_once(service, pool); + service_log("自动切换判定结束"); + service.checking.store(false); + int elapsed = 0; + while (service.running.load() && elapsed < current_check_interval_seconds(service)) { + if (service.force_refresh.exchange(false)) { + service_log("收到立即刷新请求,提前进入下一轮全量 delay"); + break; + } + timer.expires_after(std::chrono::seconds(1)); + error.clear(); + co_await timer.async_wait(asio::redirect_error(asio::use_awaitable, error)); + ++elapsed; + } + } +} diff --git a/src/node_service/Node_Service_State.cpp b/src/node_service/Node_Service_State.cpp new file mode 100644 index 0000000..8be27e2 --- /dev/null +++ b/src/node_service/Node_Service_State.cpp @@ -0,0 +1,314 @@ +#include "Node_Service.h" +fs::path node_state_path(const Service_State& service, std::string_view name) { + return service.config.state_dir / path_from_utf8_string(make_safe_file_name(name) + ".json"); +} +json target_to_json(const Test_Target& target) { + json result; + result["name"] = target.name; + result["url"] = target.url; + result["timeout_ms"] = target.timeout_ms; + return result; +} +json probe_to_json(const Probe_Result& probe) { + json result; + result["target"] = probe.target; + result["url"] = probe.url; + result["success"] = probe.result.success; + result["delay_ms"] = probe.result.delay_ms; + result["http_status"] = probe.result.http_status; + result["error"] = probe.result.error; + result["raw"] = probe.result.raw; + return result; +} +json group_to_json(const Group_Switch_State& group) { + json result; + result["name"] = group.name; + result["type"] = group.type; + result["candidates"] = group.candidates; + result["current"] = group.current; + result["best_candidate"] = group.best_candidate; + result["better_rounds"] = group.better_rounds; + result["switch_count"] = group.switch_count; + result["last_switch_unix"] = group.last_switch_unix; + result["last_switch_time"] = group.last_switch_time; + result["last_decision"] = group.last_decision; + result["last_error"] = group.last_error; + result["switchable"] = group.switchable; + return result; +} +json node_to_json(const Node_State& node) { + json result; + result["name"] = node.name; + result["type"] = node.type; + result["provider"] = node.provider; + result["exit_ipv4"] = node.exit_ipv4; + result["exit_ipv6"] = node.exit_ipv6; + result["ai_score"] = node.ai_score; + result["risk"] = node.risk; + result["risk_level"] = node.risk_level; + result["shared_users"] = node.shared_users; + result["ip_type"] = node.ip_type; + result["native_type"] = node.native_type; + result["country"] = node.country; + result["alive"] = node.alive; + result["initialized"] = node.initialized; + result["srtt_ms"] = node.srtt_ms; + result["rttvar_ms"] = node.rttvar_ms; + result["rto_ms"] = node.rto_ms; + result["success_ewma"] = node.success_ewma; + result["samples"] = node.samples; + result["success_count"] = node.success_count; + result["failure_count"] = node.failure_count; + result["consecutive_failures"] = node.consecutive_failures; + result["last_delay_ms"] = node.last_delay_ms; + result["last_success_delay_ms"] = node.last_success_delay_ms; + result["stability_score"] = node.stability_score; + result["last_error"] = node.last_error; + result["last_check_time"] = node.last_check_time; + result["updated_time"] = node.updated_time; + result["probes"] = json::array(); + for (const auto& probe : node.probes) { + result["probes"].push_back(probe_to_json(probe)); + } + return result; +} +void load_node_state(Node_State& node, const fs::path& path) { + const auto data = read_json_file(path); + if (!data.is_object()) { + return; + } + node.initialized = data.value("initialized", false); + node.srtt_ms = data.value("srtt_ms", 0.0); + node.rttvar_ms = data.value("rttvar_ms", 0.0); + node.rto_ms = data.value("rto_ms", rto_min_ms); + node.success_ewma = data.value("success_ewma", 0.0); + node.samples = data.value("samples", uint64_t{}); + node.success_count = data.value("success_count", uint64_t{}); + node.failure_count = data.value("failure_count", uint64_t{}); + node.consecutive_failures = data.value("consecutive_failures", uint64_t{}); + node.last_delay_ms = data.value("last_delay_ms", 0); + node.last_success_delay_ms = data.value("last_success_delay_ms", 0); + node.stability_score = data.value("stability_score", 0.0); + node.last_error = data.value("last_error", std::string{}); + node.last_check_time = data.value("last_check_time", std::string{}); + node.updated_time = data.value("updated_time", std::string{}); + node.alive = data.value("alive", false); + if (data.contains("probes") && data["probes"].is_array()) { + node.probes.clear(); + for (const auto& item : data["probes"]) { + Probe_Result probe; + probe.target = item.value("target", std::string{}); + probe.url = item.value("url", std::string{}); + probe.result.success = item.value("success", false); + probe.result.delay_ms = item.value("delay_ms", 0); + probe.result.http_status = item.value("http_status", 0); + probe.result.error = item.value("error", std::string{}); + node.probes.push_back(std::move(probe)); + } + } +} +void save_service_index(Service_State& service) { + json result; + result["started_time"] = service.started_time; + result["updated_time"] = now_string(); + result["controller_origin"] = service.config.controller_origin; + result["check_interval_seconds"] = service.config.check_interval_seconds; + result["delay_refresh_interval_seconds"] = service.config.check_interval_seconds; + result["status_text"] = service.status_text; + result["checking"] = service.checking.load(); + result["force_refresh"] = service.force_refresh.load(); + result["refresh_done_nodes"] = service.refresh_done_nodes.load(); + result["refresh_total_nodes"] = service.refresh_total_nodes; + result["refresh_request_count"] = service.refresh_request_count; + result["last_refresh_request_time"] = service.last_refresh_request_time; + result["last_refresh_begin_time"] = service.last_refresh_begin_time; + result["last_refresh_end_time"] = service.last_refresh_end_time; + result["state_dir"] = path_to_utf8_string(service.config.state_dir); + result["web_dir"] = path_to_utf8_string(service.config.web_dir); + result["service_config_path"] = path_to_utf8_string(service.config.service_config_path); + result["targets"] = json::array(); + for (const auto& target : service.config.targets) { + result["targets"].push_back(target_to_json(target)); + } + result["round"] = service.round; + result["auto_switch"] = auto_switch_config_to_json(service.config.auto_switch); + result["groups"] = json::array(); + for (const auto& name : service.group_order) { + const auto item = service.groups.find(name); + if (item != service.groups.end()) { + result["groups"].push_back(group_to_json(item->second)); + } + } + result["nodes"] = json::array(); + for (const auto& name : service.node_order) { + const auto item = service.nodes.find(name); + if (item != service.nodes.end()) { + result["nodes"].push_back(node_to_json(item->second)); + } + } + save_json_file(service.config.state_dir / "index.json", result); +} +double clamp_score(double value) { + return std::clamp(value, 0.0, 100.0); +} +void update_rto(Node_State& node, std::optional delay_ms) { + ++node.samples; + if (!delay_ms) { + ++node.failure_count; + ++node.consecutive_failures; + node.success_ewma = node.samples == 1 ? 0.0 : node.success_ewma * 0.95; + node.rto_ms = std::clamp(node.rto_ms * 2.0, rto_min_ms, rto_max_ms); + return; + } + const double rtt = static_cast(*delay_ms); + ++node.success_count; + node.consecutive_failures = 0; + node.last_success_delay_ms = *delay_ms; + node.success_ewma = node.samples == 1 ? 1.0 : node.success_ewma * 0.95 + 0.05; + if (!node.initialized) { + node.srtt_ms = rtt; + node.rttvar_ms = rtt / 2.0; + node.initialized = true; + } + else { + node.rttvar_ms = (1.0 - rto_beta) * node.rttvar_ms + rto_beta * std::abs(node.srtt_ms - rtt); + node.srtt_ms = (1.0 - rto_alpha) * node.srtt_ms + rto_alpha * rtt; + } + node.rto_ms = std::clamp(node.srtt_ms + rto_k * node.rttvar_ms, rto_min_ms, rto_max_ms); +} +void update_score(Node_State& node) { + const double rto_penalty = std::min(30.0, node.rto_ms / 1000.0 * 2.0); + const double jitter_penalty = std::min(25.0, node.rttvar_ms / 50.0); + const double failure_penalty = std::min(40.0, static_cast(node.consecutive_failures) * 15.0); + node.stability_score = clamp_score(node.success_ewma * 100.0 - rto_penalty - jitter_penalty - failure_penalty); +} +std::vector read_proxy_names(const fs::path& config_path) { + std::ifstream stream(config_path, std::ios::binary); + if (!stream) { + throw std::runtime_error(std::format("无法打开配置文件:{}", path_to_utf8_string(config_path))); + } + YAML::Node config = YAML::Load(stream); + const auto proxies = config["proxies"]; + std::vector names; + std::set seen; + if (!proxies || !proxies.IsSequence()) { + return names; + } + for (const auto& proxy : proxies) { + if (!proxy["name"]) { + continue; + } + const auto name = proxy["name"].as(); + if (contain(name, "剩余流量") || contain(name, "套餐到期")) { + continue; + } + if (seen.insert(name).second) { + names.push_back(name); + } + } + return names; +} +std::vector read_proxy_groups(const fs::path& config_path) { + std::ifstream stream(config_path, std::ios::binary); + if (!stream) { + throw std::runtime_error(std::format("无法打开配置文件:{}", path_to_utf8_string(config_path))); + } + YAML::Node config = YAML::Load(stream); + const auto groups = config["proxy-groups"]; + std::vector result; + if (!groups || !groups.IsSequence()) { + return result; + } + for (const auto& item : groups) { + if (!item["name"] || !item["type"]) { + continue; + } + Proxy_Group_Def group; + group.name = item["name"].as(); + group.type = item["type"].as(); + const auto proxies = item["proxies"]; + if (proxies && proxies.IsSequence()) { + std::set seen; + for (const auto& proxy : proxies) { + const auto name = proxy.as(); + if (seen.insert(name).second) { + group.proxies.push_back(name); + } + } + } + result.push_back(std::move(group)); + } + return result; +} +bool contains_name(const std::vector& list, const std::string& name) { + return std::find(list.begin(), list.end(), name) != list.end(); +} +bool should_manage_group(const Auto_Switch_Config& config, const Proxy_Group_Def& group) { + if (group.type != "select") { + return false; + } + return config.managed_groups.empty() || contains_name(config.managed_groups, group.name); +} +void reload_switch_groups_locked(Service_State& service) { + const auto groups = read_proxy_groups(service.config.config_path); + std::set node_names(service.node_order.begin(), service.node_order.end()); + std::unordered_map next_groups; + std::vector next_order; + for (const auto& def : groups) { + if (!should_manage_group(service.config.auto_switch, def)) { + continue; + } + Group_Switch_State group; + const auto old = service.groups.find(def.name); + if (old != service.groups.end()) { + group = old->second; + } + group.name = def.name; + group.type = def.type; + group.candidates.clear(); + for (const auto& proxy : def.proxies) { + if (service.config.auto_switch.skip_direct && proxy == "DIRECT") { + continue; + } + if (node_names.contains(proxy)) { + group.candidates.push_back(proxy); + } + } + group.switchable = !group.candidates.empty(); + if (group.switchable) { + group.last_error.clear(); + if (group.last_decision.empty()) { + group.last_decision = "等待自动切换判定"; + } + } + else { + group.last_error = "代理组没有直接节点候选,不能由稳定性服务切换"; + } + next_order.push_back(group.name); + next_groups.emplace(group.name, std::move(group)); + } + service.group_order = std::move(next_order); + service.groups = std::move(next_groups); +} +void reload_switch_groups(Service_State& service) { + std::lock_guard lock(service.mutex); + reload_switch_groups_locked(service); + save_service_index(service); +} +void initialize_nodes(Service_State& service) { + fs::create_directories(service.config.state_dir); + const auto names = read_proxy_names(service.config.config_path); + std::lock_guard lock(service.mutex); + service.status_text = "服务初始化完成,等待第一轮全量 delay"; + service.node_order = names; + for (const auto& name : names) { + Node_State node; + node.name = name; + enrich_node_metadata(node); + load_node_state(node, node_state_path(service, name)); + service.nodes.emplace(name, std::move(node)); + } + reload_switch_groups_locked(service); + save_service_index(service); + service_log(std::format("节点初始化完成:{} 个节点,{} 个受控代理组", service.node_order.size(), service.group_order.size())); +} diff --git a/src/node_service/Node_Service_Util.cpp b/src/node_service/Node_Service_Util.cpp new file mode 100644 index 0000000..b30fe78 --- /dev/null +++ b/src/node_service/Node_Service_Util.cpp @@ -0,0 +1,320 @@ +#include "Node_Service.h" +fs::path path_from_utf8_string(std::string_view value) { + return path_from_utf8(value); +} +std::string path_to_utf8_string(const fs::path& path) { + return path_to_utf8(path); +} +std::string now_string() { + const auto now = std::chrono::system_clock::now(); + const auto value = std::chrono::system_clock::to_time_t(now); + std::tm utc{}; +#ifdef _WIN32 + gmtime_s(&utc, &value); +#else + gmtime_r(&value, &utc); +#endif + return std::format("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday, utc.tm_hour, utc.tm_min, utc.tm_sec); +} +int64_t unix_seconds() { + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} +std::string trim(std::string value) { + const auto first = value.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) { + return {}; + } + const auto last = value.find_last_not_of(" \t\r\n"); + return value.substr(first, last - first + 1); +} +std::string read_env(std::string_view key) { +#ifdef _WIN32 + const std::string name(key); + const DWORD required = GetEnvironmentVariableA(name.c_str(), nullptr, 0); + if (required == 0) { + return {}; + } + std::string value(required, '\0'); + const DWORD written = GetEnvironmentVariableA(name.c_str(), value.data(), required); + if (written == 0) { + return {}; + } + value.resize(written); + return trim(native_to_utf8(value)); +#else + const auto* value = std::getenv(std::string(key).c_str()); + return value ? trim(value) : std::string{}; +#endif +} +std::string url_encode(std::string_view value) { + static constexpr char hex[] = "0123456789ABCDEF"; + std::string out; + out.reserve(value.size() * 3); + for (const unsigned char ch : value) { + const bool safe = (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' || ch == '~'; + if (safe) { + out.push_back(static_cast(ch)); + } + else { + out.push_back('%'); + out.push_back(hex[ch >> 4]); + out.push_back(hex[ch & 0x0F]); + } + } + return out; +} +std::string make_safe_file_name(std::string_view value) { + std::string out; + out.reserve(value.size()); + for (const unsigned char ch : value) { + if (ch < 32 || ch == '"' || ch == '*' || ch == '/' || ch == ':' || ch == '<' || ch == '>' || ch == '?' || ch == '\\' || ch == '|') { + out.push_back('_'); + } + else { + out.push_back(static_cast(ch)); + } + } + while (!out.empty() && (out.back() == ' ' || out.back() == '.')) { + out.pop_back(); + } + if (out.empty()) { + return "unnamed"; + } + return out; +} +std::vector split_parts(std::string_view value, char delimiter) { + std::vector result; + size_t start = 0; + while (start <= value.size()) { + const auto pos = value.find(delimiter, start); + if (pos == std::string_view::npos) { + result.push_back(value.substr(start)); + break; + } + result.push_back(value.substr(start, pos - start)); + start = pos + 1; + } + return result; +} +bool starts_with(std::string_view value, std::string_view prefix) { + return value.size() >= prefix.size() && value.substr(0, prefix.size()) == prefix; +} +bool ends_with(std::string_view value, std::string_view suffix) { + return value.size() >= suffix.size() && value.substr(value.size() - suffix.size()) == suffix; +} +int parse_int_prefix(std::string_view value, size_t& offset) { + int result = 0; + bool found = false; + while (offset < value.size() && std::isdigit(static_cast(value[offset]))) { + found = true; + result = result * 10 + value[offset] - '0'; + ++offset; + } + return found ? result : -1; +} +std::string to_string(std::string_view value) { + return std::string(value.data(), value.size()); +} +std::string compact_country_from_name(std::string_view name) { + static constexpr std::array countries{ + "日本", "新加坡", "美国", "香港", "台湾", "韩国", "英国", "德国", "法国", "加拿大", "澳大利亚", "印度", "泰国", "越南" + }; + for (const auto country : countries) { + if (name.find(country) != std::string_view::npos) { + return to_string(country); + } + } + return {}; +} +void parse_node_metadata(Node_State& node) { + node.exit_ipv4.clear(); + node.exit_ipv6.clear(); + node.ai_score = -1; + node.risk = -1; + node.risk_level.clear(); + node.shared_users.clear(); + node.ip_type.clear(); + node.native_type.clear(); + node.country = compact_country_from_name(node.name); + for (const auto part : split_parts(node.name, '_')) { + if (starts_with(part, "ipv4")) { + node.exit_ipv4 = to_string(part.substr(4)); + continue; + } + if (starts_with(part, "ipv6")) { + const auto value = part.substr(4); + if (value != "无") { + node.exit_ipv6 = to_string(value); + } + continue; + } + if (starts_with(part, "AI") && ends_with(part, "星")) { + size_t offset = 2; + node.ai_score = parse_int_prefix(part, offset); + continue; + } + if (starts_with(part, "风控")) { + size_t offset = 6; + node.risk = parse_int_prefix(part, offset); + node.risk_level = to_string(part.substr(offset)); + continue; + } + if (starts_with(part, "共享")) { + node.shared_users = to_string(part.substr(6)); + continue; + } + if (part.find("原生") != std::string_view::npos || part.find("广播") != std::string_view::npos) { + node.native_type = to_string(part); + continue; + } + if (part.find("IDC") != std::string_view::npos || part.find("机房") != std::string_view::npos || part.find("家庭宽带") != std::string_view::npos || part.find("住宅") != std::string_view::npos) { + node.ip_type = to_string(part); + continue; + } + } +} +bool is_label_metadata_part(std::string_view part) { + return starts_with(part, "ipv4") || starts_with(part, "ipv6") || starts_with(part, "AI") && ends_with(part, "星") || starts_with(part, "风控") || starts_with(part, "共享") || part.find("原生") != std::string_view::npos || part.find("广播") != std::string_view::npos || part.find("IDC") != std::string_view::npos || part.find("机房") != std::string_view::npos || part.find("家庭宽带") != std::string_view::npos || part.find("住宅") != std::string_view::npos; +} +std::string original_proxy_name_from_label(std::string_view name) { + const auto parts = split_parts(name, '_'); + for (size_t i = 0; i < parts.size(); ++i) { + if (!is_label_metadata_part(parts[i])) { + continue; + } + std::string result; + for (size_t j = 0; j < i; ++j) { + if (j != 0) { + result.push_back('_'); + } + result += to_string(parts[j]); + } + return result.empty() ? to_string(name) : result; + } + return to_string(name); +} +std::optional yaml_string_value(const YAML::Node& node, const char* key) { + if (!node || !node.IsMap()) { + return std::nullopt; + } + const auto value = node[key]; + if (!value || !value.IsScalar()) { + return std::nullopt; + } + const auto text = trim(value.as()); + return text.empty() ? std::nullopt : std::optional{text}; +} +void load_node_ip_cache(Node_State& node) { + const fs::path root = path_from_utf8_string(get_executable_dir()); + const auto cache_dir = (root / "../Node_Exit_Ip_Cache").lexically_normal(); + const auto original_name = original_proxy_name_from_label(node.name); + const std::array candidates{original_name, node.name}; + for (const auto& name : candidates) { + const auto path = cache_dir / path_from_utf8_string(make_safe_file_name(name)) / "public_ip.yaml"; + if (!fs::is_regular_file(path)) { + continue; + } + try { + std::ifstream stream(path, std::ios::binary); + const auto data = YAML::Load(stream); + auto ipv4 = yaml_string_value(data, "public_ipv4"); + if (!ipv4) { + ipv4 = yaml_string_value(data, "public_ip"); + } + const auto ipv6 = yaml_string_value(data, "public_ipv6"); + if (ipv4) { + node.exit_ipv4 = *ipv4; + } + if (ipv6) { + node.exit_ipv6 = *ipv6; + } + return; + } + catch (...) { + return; + } + } +} +void enrich_node_metadata(Node_State& node) { + parse_node_metadata(node); + load_node_ip_cache(node); +} +void write_text_atomic(const fs::path& path, std::string_view content) { + fs::create_directories(path.parent_path()); + auto temp = path; + temp += L".tmp"; + { + std::ofstream stream(temp, std::ios::binary | std::ios::trunc); + if (!stream) { + throw std::runtime_error(std::format("无法写入文件:{}", path_to_utf8_string(temp))); + } + stream.write(content.data(), static_cast(content.size())); + } + std::error_code ec; + fs::rename(temp, path, ec); + if (ec) { + fs::remove(path, ec); + ec.clear(); + fs::rename(temp, path, ec); + if (ec) { + fs::remove(temp); + throw std::runtime_error(std::format("无法替换文件:{},{}", path_to_utf8_string(path), error_text_to_utf8(ec.message()))); + } + } +} +json read_json_file(const fs::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) { + return json::object(); + } + try { + return json::parse(stream, nullptr, true, true); + } + catch (...) { + return json::object(); + } +} +void save_json_file(const fs::path& path, const json& value) { + write_text_atomic(path, value.dump(2)); +} +std::string read_binary_text(const fs::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) { + return {}; + } + return std::string((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); +} +std::string mime_type(const fs::path& path) { + const auto ext = path_to_utf8_string(path.extension()); + if (ext == ".html") { + return "text/html; charset=utf-8"; + } + if (ext == ".css") { + return "text/css; charset=utf-8"; + } + if (ext == ".js") { + return "application/javascript; charset=utf-8"; + } + if (ext == ".json") { + return "application/json; charset=utf-8"; + } + if (ext == ".svg") { + return "image/svg+xml"; + } + return "application/octet-stream"; +} +void set_file_response(const fs::path& file, httplib::Response& res) { + const auto content = read_binary_text(file); + if (content.empty() && !fs::is_regular_file(file)) { + res.status = 404; + res.set_content("not found", "text/plain; charset=utf-8"); + return; + } + res.set_header("Cache-Control", "no-store"); + res.set_content(content, mime_type(file)); +} +void service_log(std::string_view message) { + static std::mutex log_mutex; + std::lock_guard lock(log_mutex); + std::cout << std::format("[{}] {}\n", now_string(), message); +} diff --git a/src/node_service/Node_Service_Web.cpp b/src/node_service/Node_Service_Web.cpp new file mode 100644 index 0000000..3a51b84 --- /dev/null +++ b/src/node_service/Node_Service_Web.cpp @@ -0,0 +1,205 @@ +#include "Node_Service.h" +json state_snapshot(Service_State& service) { + std::lock_guard lock(service.mutex); + json result; + result["started_time"] = service.started_time; + result["updated_time"] = now_string(); + result["controller_origin"] = service.config.controller_origin; + result["config_path"] = path_to_utf8_string(service.config.config_path); + result["state_dir"] = path_to_utf8_string(service.config.state_dir); + result["web_dir"] = path_to_utf8_string(service.config.web_dir); + result["service_config_path"] = path_to_utf8_string(service.config.service_config_path); + result["check_interval_seconds"] = service.config.check_interval_seconds; + result["delay_refresh_interval_seconds"] = service.config.check_interval_seconds; + result["status_text"] = service.status_text; + result["refresh_done_nodes"] = service.refresh_done_nodes.load(); + result["refresh_total_nodes"] = service.refresh_total_nodes; + result["refresh_request_count"] = service.refresh_request_count; + result["last_refresh_request_time"] = service.last_refresh_request_time; + result["last_refresh_begin_time"] = service.last_refresh_begin_time; + result["last_refresh_end_time"] = service.last_refresh_end_time; + result["checking"] = service.checking.load(); + result["force_refresh"] = service.force_refresh.load(); + result["targets"] = json::array(); + for (const auto& target : service.config.targets) { + result["targets"].push_back(target_to_json(target)); + } + result["round"] = service.round; + result["auto_switch"] = auto_switch_config_to_json(service.config.auto_switch); + result["groups"] = json::array(); + for (const auto& name : service.group_order) { + const auto item = service.groups.find(name); + if (item != service.groups.end()) { + result["groups"].push_back(group_to_json(item->second)); + } + } + result["nodes"] = json::array(); + for (const auto& name : service.node_order) { + const auto item = service.nodes.find(name); + if (item != service.nodes.end()) { + result["nodes"].push_back(node_to_json(item->second)); + } + } + return result; +} +std::string html_page() { + return R"HTML( + + + + +节点稳定性监控 + + + +
节点稳定性监控
loading
+
+ + +)HTML"; +} +void start_web_server(Service_State& service, asio::io_context& io, httplib::Server& server) { + server.Get("/", [&service](const httplib::Request&, httplib::Response& res) { + const auto index = service.config.web_dir / "index.html"; + if (fs::is_regular_file(index)) { + set_file_response(index, res); + return; + } + res.set_content(html_page(), "text/html; charset=utf-8"); + }); + server.Get(R"(/assets/(.+))", [&service](const httplib::Request& req, httplib::Response& res) { + const auto name = req.matches[1].str(); + if (name.find("..") != std::string::npos || name.find('/') != std::string::npos || name.find('\\') != std::string::npos) { + res.status = 400; + res.set_content("bad asset path", "text/plain; charset=utf-8"); + return; + } + set_file_response(service.config.web_dir / "assets" / name, res); + }); + server.Get("/api/state", [&service](const httplib::Request&, httplib::Response& res) { + res.set_content(state_snapshot(service).dump(), "application/json; charset=utf-8"); + }); + server.Get("/api/config", [&service](const httplib::Request&, httplib::Response& res) { + std::lock_guard lock(service.mutex); + res.set_content(runtime_config_to_json(service.config).dump(), "application/json; charset=utf-8"); + }); + server.Post("/api/config", [&service](const httplib::Request& req, httplib::Response& res) { + try { + const auto body = json::parse(req.body, nullptr, true, true); + std::lock_guard lock(service.mutex); + if (body.contains("check_interval_seconds") && body["check_interval_seconds"].is_number_integer()) { + service.config.check_interval_seconds = normalize_check_interval(body["check_interval_seconds"].get()); + } + else if (body.contains("delay_refresh_interval_seconds") && body["delay_refresh_interval_seconds"].is_number_integer()) { + service.config.check_interval_seconds = normalize_check_interval(body["delay_refresh_interval_seconds"].get()); + } + if (body.contains("auto_switch")) { + load_auto_switch_config(service.config.auto_switch, body["auto_switch"]); + reload_switch_groups_locked(service); + } + service.status_text = "服务配置已保存"; + save_runtime_config(service.config); + save_service_index(service); + service_log(std::format("服务配置已保存:全量 delay 间隔 {} 秒,自动切换 {}", service.config.check_interval_seconds, service.config.auto_switch.enabled ? "启用" : "关闭")); + res.set_content(json{{"success", true}, {"message", "配置已保存"}, {"config", runtime_config_to_json(service.config)}}.dump(), "application/json; charset=utf-8"); + } + catch (const std::exception& error) { + res.status = 400; + res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); + } + }); + server.Post("/api/refresh", [&service](const httplib::Request&, httplib::Response& res) { + std::string message; + { + std::lock_guard lock(service.mutex); + service.force_refresh.store(true); + ++service.refresh_request_count; + service.last_refresh_request_time = now_string(); + message = service.checking.load() ? "全量 delay 正在执行,本次请求已记录" : "已请求立即全量 delay"; + service.status_text = message; + save_service_index(service); + } + service_log(message); + res.set_content(json{{"success", true}, {"message", message}, {"checking", service.checking.load()}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); + }); + server.Post("/api/switch", [&service](const httplib::Request&, httplib::Response& res) { + try { + service_log("收到手动自动切换判定请求"); + perform_auto_switch(service); + service_log("手动自动切换判定完成"); + res.set_content(json{{"success", true}, {"message", "切换判定完成"}, {"state", state_snapshot(service)}}.dump(), "application/json; charset=utf-8"); + } + catch (const std::exception& error) { + res.status = 400; + res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); + } + }); + server.Post("/api/report", [&service](const httplib::Request& req, httplib::Response& res) { + try { + const auto body = json::parse(req.body, nullptr, true, true); + const auto name = body.at("name").get(); + std::lock_guard lock(service.mutex); + if (service.nodes.find(name) == service.nodes.end()) { + service.node_order.push_back(name); + } + auto& node = service.nodes[name]; + node.name = name; + enrich_node_metadata(node); + node.updated_time = now_string(); + node.last_check_time = node.updated_time; + if (body.contains("type") && body["type"].is_string()) { + node.type = body["type"].get(); + } + if (body.contains("provider") && body["provider"].is_string()) { + node.provider = body["provider"].get(); + } + if (body.contains("alive")) { + node.alive = body["alive"].get(); + } + if (body.contains("probes") && body["probes"].is_array()) { + node.probes.clear(); + for (const auto& item : body["probes"]) { + Probe_Result probe; + probe.target = item.value("target", std::string{}); + probe.url = item.value("url", std::string{}); + probe.result.success = item.value("success", false); + probe.result.delay_ms = item.value("delay_ms", 0); + probe.result.http_status = item.value("http_status", 0); + probe.result.error = item.value("error", std::string{}); + node.probes.push_back(std::move(probe)); + } + } + if (body.contains("delay_ms") && body["delay_ms"].is_number_integer()) { + node.last_delay_ms = body["delay_ms"].get(); + update_rto(node, node.last_delay_ms); + } + else if (body.contains("alive") && !node.alive) { + update_rto(node, std::nullopt); + } + if (body.contains("error") && body["error"].is_string()) { + node.last_error = body["error"].get(); + } + update_score(node); + save_json_file(node_state_path(service, name), node_to_json(node)); + save_service_index(service); + service_log(std::format("收到节点上报:{} alive={} delay={}ms score={:.1f}", node.name, node.alive, node.last_delay_ms, node.stability_score)); + res.set_content(R"({"success":true})", "application/json; charset=utf-8"); + } + catch (const std::exception& error) { + res.status = 400; + res.set_content(json{{"success", false}, {"error", error_text_to_utf8(error.what())}}.dump(), "application/json; charset=utf-8"); + } + }); + server.Post("/api/stop", [&service, &server, &io](const httplib::Request&, httplib::Response& res) { + service.running.store(false); + service_log("收到停止服务请求"); + res.set_content(R"({"success":true})", "application/json; charset=utf-8"); + server.stop(); + asio::post(io, [&io] { + io.stop(); + }); + }); +} diff --git a/src/node_service/main.cpp b/src/node_service/main.cpp new file mode 100644 index 0000000..7b9121a --- /dev/null +++ b/src/node_service/main.cpp @@ -0,0 +1,43 @@ +#include "Node_Service.h" +int main(int argc, char** argv) { +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); +#endif + try { + Service_State service; + service.config = make_config(argc, argv); + service.started_time = now_string(); + initialize_nodes(service); + asio::io_context io(1); + asio::thread_pool pool(service.config.worker_threads); + httplib::Server server; + start_web_server(service, io, server); + std::thread web_thread([&service, &server] { + service_log(std::format("HTTP 服务监听:{}:{}", service.config.listen_host, service.config.listen_port)); + if (!server.listen(service.config.listen_host, service.config.listen_port)) { + service_log("HTTP 服务监听失败"); + } + }); + asio::co_spawn(io, monitor_refresh_loop(service, pool), asio::detached); + service_log(std::format("节点服务启动:http://{}:{}", service.config.listen_host, service.config.listen_port)); + service_log(std::format("配置文件:{}", path_to_utf8_string(service.config.config_path))); + service_log(std::format("状态目录:{}", path_to_utf8_string(service.config.state_dir))); + service_log(std::format("前端目录:{}", path_to_utf8_string(service.config.web_dir))); + service_log(std::format("服务配置:{}", path_to_utf8_string(service.config.service_config_path))); + io.run(); + service.running.store(false); + server.stop(); + if (web_thread.joinable()) { + web_thread.join(); + } + pool.stop(); + pool.join(); + service_log("节点服务退出"); + return 0; + } + catch (const std::exception& error) { + std::cerr << std::format("node_service 启动失败:{}\n", error_text_to_utf8(error.what())); + return 1; + } +} diff --git a/src/node_service/web/README.md b/src/node_service/web/README.md new file mode 100644 index 0000000..3b2825c --- /dev/null +++ b/src/node_service/web/README.md @@ -0,0 +1,9 @@ +# node_service Web + +这是 node_service 的 React + Vite + Material Design 前端。 + +```powershell +pwsh ./build_frontend.ps1 +``` + +构建产物在 `dist` 目录,node_service 会优先加载 `web/dist/index.html`。 diff --git a/src/node_service/web/build_frontend.ps1 b/src/node_service/web/build_frontend.ps1 new file mode 100644 index 0000000..56350f8 --- /dev/null +++ b/src/node_service/web/build_frontend.ps1 @@ -0,0 +1,9 @@ +$ErrorActionPreference = "Stop" +Push-Location $PSScriptRoot +try { + npm install + npm run build +} +finally { + Pop-Location +} diff --git a/src/node_service/web/index.html b/src/node_service/web/index.html new file mode 100644 index 0000000..467284b --- /dev/null +++ b/src/node_service/web/index.html @@ -0,0 +1,12 @@ + + + + + +节点稳定性监控 + + +
+ + + diff --git a/src/node_service/web/package-lock.json b/src/node_service/web/package-lock.json new file mode 100644 index 0000000..86e4cfd --- /dev/null +++ b/src/node_service/web/package-lock.json @@ -0,0 +1,2368 @@ +{ + "name": "tizi-node-service-web", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tizi-node-service-web", + "version": "1.0.0", + "dependencies": { + "@emotion/react": "^11.13.5", + "@emotion/styled": "^11.13.5", + "@mui/material": "^6.1.8", + "@vitejs/plugin-react": "^4.3.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "vite": "^5.4.11" + }, + "devDependencies": {} + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz", + "integrity": "sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz", + "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.5.0", + "@mui/system": "^6.5.0", + "@mui/types": "~7.2.24", + "@mui/utils": "^6.4.9", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^6.5.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz", + "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.4.9", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz", + "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz", + "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.4.9", + "@mui/styled-engine": "^6.5.0", + "@mui/types": "~7.2.24", + "@mui/utils": "^6.4.9", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.24", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", + "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz", + "integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/types": "~7.2.24", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.44", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.44.tgz", + "integrity": "sha512-T3ghW+sl/ZJ8w1v/yQx3qvJ9040DWoLBz8JT/CILbAKcFyG9b2MRe75v6W5uXjv6uH1lumK2Kv46y2zSkcej0Q==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.394", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.394.tgz", + "integrity": "sha512-Wmt2Gm0o8JWBuGgmc4XZ0u9s1RaCRqhxP47phplmfg04+qypTUurpeJGP45A7Fhv7jdrrVH44PLlR9qXo37cVQ==", + "license": "ISC" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/src/node_service/web/package.json b/src/node_service/web/package.json new file mode 100644 index 0000000..526d493 --- /dev/null +++ b/src/node_service/web/package.json @@ -0,0 +1,21 @@ +{ + "name": "tizi-node-service-web", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1 --port 5173", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1 --port 4173" + }, + "dependencies": { + "@emotion/react": "^11.13.5", + "@emotion/styled": "^11.13.5", + "@mui/material": "^6.1.8", + "@vitejs/plugin-react": "^4.3.3", + "vite": "^5.4.11", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": {} +} diff --git a/src/node_service/web/src/App.jsx b/src/node_service/web/src/App.jsx new file mode 100644 index 0000000..911f5c9 --- /dev/null +++ b/src/node_service/web/src/App.jsx @@ -0,0 +1,178 @@ +import React,{useEffect,useMemo,useState} from 'react'; +import {Alert, AppBar, Box, Button, Card, CardContent, Checkbox, Chip, CircularProgress, Container, Divider, FormControl, FormControlLabel, Grid, InputLabel, LinearProgress, MenuItem, Paper, Select, Snackbar, Stack, Switch, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Toolbar, Tooltip, Typography} from '@mui/material'; +import {readState, requestAutoSwitch, requestFullDelay, saveConfig} from './api.js'; +const emptySwitchConfig={enabled:true,skip_direct:true,switch_cooldown_seconds:120,fail_switch_count:2,better_confirm_rounds:3,keep_score_threshold:75,weak_score_threshold:60,weak_improve_margin:15,better_improve_margin:10,managed_groups:[]}; +function scoreLevel(score) { + if (score >= 80) return 'success'; + if (score >= 50) return 'warning'; + return 'error'; +} +function scoreText(value) { + return Number(value || 0).toFixed(1); +} +function msText(value) { + return `${Number(value || 0).toFixed(0)}ms`; +} +function valueText(value) { + const text = String(value ?? '').trim(); + return text || '-'; +} +function riskText(node) { + if (node.risk === undefined || node.risk === null || Number(node.risk) < 0) return valueText(node.risk_level); + return `${node.risk}${node.risk_level ? ` ${node.risk_level}` : ''}`; +} +function aiText(node) { + return Number(node.ai_score) >= 0 ? `${node.ai_score} 星` : '-'; +} +function timeText(value) { + return valueText(value); +} +function normalizeAutoSwitch(input) { + return {...emptySwitchConfig,...(input || {}),managed_groups:Array.isArray(input?.managed_groups) ? input.managed_groups : []}; +} +function matchStatus(node,status) { + const score = Number(node.stability_score || 0); + if (status === 'alive') return !!node.alive; + if (status === 'dead') return !node.alive; + if (status === 'good') return score >= 80; + if (status === 'warn') return score >= 50 && score < 80; + if (status === 'bad') return score < 50; + return true; +} +function matchKeyword(node,keyword) { + const text = [node.name,node.exit_ipv4,node.exit_ipv6,node.ip_type,node.risk_level,node.native_type,node.shared_users,node.provider,node.country].join(' ').toLowerCase(); + return text.includes(keyword.toLowerCase()); +} +function sortNodes(nodes,sortBy) { + const list = [...nodes]; + if (sortBy === 'rto') list.sort((a,b)=>Number(a.rto_ms || 0) - Number(b.rto_ms || 0)); + else if (sortBy === 'failure') list.sort((a,b)=>Number(b.consecutive_failures || 0) - Number(a.consecutive_failures || 0)); + else if (sortBy === 'delay') list.sort((a,b)=>Number(a.last_delay_ms || 0) - Number(b.last_delay_ms || 0)); + else if (sortBy === 'name') list.sort((a,b)=>String(a.name || '').localeCompare(String(b.name || ''),'zh-Hans-CN')); + else list.sort((a,b)=>Number(b.stability_score || 0) - Number(a.stability_score || 0)); + return list; +} +function metricValues(state) { + const nodes = state?.nodes || []; + const alive = nodes.filter(node=>node.alive).length; + const good = nodes.filter(node=>Number(node.stability_score || 0) >= 80).length; + const bad = nodes.filter(node=>Number(node.stability_score || 0) < 50).length; + const avg = nodes.length ? nodes.reduce((sum,node)=>sum + Number(node.stability_score || 0),0) / nodes.length : 0; + return {nodes:nodes.length,alive,good,bad,avg}; +} +function StatCard({label,value,hint,color}) { + return {label}{value}{hint}; +} +function TopBar({state,interval,setIntervalValue,onSaveInterval,onRefresh,onReload,autoRefresh,setAutoRefresh,busy,status}) { + const done = Number(state?.refresh_done_nodes || 0); + const total = Number(state?.refresh_total_nodes || 0); + const progress = total > 0 ? Math.min(100,done * 100 / total) : 0; + return 节点稳定性监控轮次 {state?.round ?? 0} | {state?.status_text || '等待服务状态'} | 配置 {state?.config_path || '-'}{state?.checking ? : null}setIntervalValue(event.target.value)} sx={{width:180}} inputProps={{min:5,max:3600,step:1}}/>setAutoRefresh(event.target.checked)}/>} label="自动刷新"/>; +} +function Summary({state}) { + const metrics = metricValues(state); + return ; +} +function AutoSwitchPanel({state,onSave,onRun,busy}) { + const [form,setForm] = useState(emptySwitchConfig); + const [groups,setGroups] = useState(''); + useEffect(()=>{const config = normalizeAutoSwitch(state?.auto_switch);setForm(config);setGroups(config.managed_groups.join('\n'));},[state?.auto_switch]); + const setValue = (key,value)=>setForm(prev=>({...prev,[key]:value})); + const save = ()=>onSave({...form,managed_groups:groups.split(/\r?\n/).map(item=>item.trim()).filter(Boolean)}); + return 代理组自动切换使用稳定性评分主动 PUT /proxies/group 切换 select 组。setValue('enabled',event.target.checked)}/>} label="启用自动切换"/>setValue('skip_direct',event.target.checked)}/>} label="跳过 DIRECT"/>setValue('keep_score_threshold',Number(event.target.value))}/>setValue('weak_score_threshold',Number(event.target.value))}/>setValue('weak_improve_margin',Number(event.target.value))}/>setValue('better_improve_margin',Number(event.target.value))}/>setValue('better_confirm_rounds',Number(event.target.value))}/>setValue('fail_switch_count',Number(event.target.value))}/>setValue('switch_cooldown_seconds',Number(event.target.value))}/>setGroups(event.target.value)}/>; +} +function GroupTable({groups}) { + return 代理组状态代理组当前选择候选更好确认切换次数上次切换决策{(groups || []).map(group=>{group.name}{valueText(group.current)}{group.candidates?.length || 0}{group.better_rounds || 0}{group.switch_count || 0}{timeText(group.last_switch_time)}{group.last_error ? : valueText(group.last_decision)})}
; +} +function Filters({keyword,setKeyword,statusFilter,setStatusFilter,sortBy,setSortBy}) { + return setKeyword(event.target.value)}/>状态过滤排序; +} +function ScoreCell({score}) { + const value = Number(score || 0); + return {scoreText(value)}; +} +function ProbeIcons({probes}) { + const list = probes || []; + return {list.map((probe,index)=>)}; +} +function NodeTable({nodes,selected,setSelected}) { + return 节点aliveIPv4IPv6AIIDC属性风控共享原生稳定性RTOSRTTRTTVAR最新延迟连续失败探测更新时间{nodes.map(node=>setSelected(node)} sx={{cursor:'pointer'}}>{node.name}{valueText(node.exit_ipv4)}{valueText(node.exit_ipv6)}{aiText(node)}{valueText(node.ip_type)}= 80 ? 'error.main' : Number(node.risk || -1) >= 50 ? 'warning.main' : 'success.main'} sx={{fontWeight:800}}>{riskText(node)}{valueText(node.shared_users)}{valueText(node.native_type)}{msText(node.rto_ms)}{msText(node.srtt_ms)}{msText(node.rttvar_ms)}{msText(node.last_delay_ms)}{node.consecutive_failures || 0}{timeText(node.updated_time)})}
; +} +function DetailItem({label,value}) { + return {label}{valueText(value)}; +} +function DetailPanel({node}) { + if (!node) return 节点详情点击左侧节点查看探测详情。; + return {node.name}探测详情{(node.probes || []).map((probe,index)=>{probe.target}{probe.url}{probe.result?.error ? {probe.result.error} : null})}; +} +export default function App() { + const [state,setState] = useState(null); + const [selected,setSelected] = useState(null); + const [keyword,setKeyword] = useState(''); + const [statusFilter,setStatusFilter] = useState('all'); + const [sortBy,setSortBy] = useState('score'); + const [intervalValue,setIntervalValue] = useState('60'); + const [autoRefresh,setAutoRefresh] = useState(true); + const [busy,setBusy] = useState(false); + const [notice,setNotice] = useState('等待操作'); + const [error,setError] = useState(''); + const reload = async()=>{ + const data = await readState(); + setState(data); + setIntervalValue(String(data.delay_refresh_interval_seconds || data.check_interval_seconds || 60)); + if (selected) { + const next = (data.nodes || []).find(node=>node.name === selected.name); + setSelected(next || null); + } + }; + useEffect(()=>{reload().catch(err=>setError(err.message));},[]); + useEffect(()=>{const timer=setInterval(()=>{if(autoRefresh)reload().catch(err=>setError(err.message));},5000);return()=>clearInterval(timer);},[autoRefresh,selected]); + const nodes = useMemo(()=>sortNodes((state?.nodes || []).filter(node=>matchStatus(node,statusFilter)).filter(node=>matchKeyword(node,keyword)),sortBy),[state,keyword,statusFilter,sortBy]); + const saveInterval = async()=>{ + setBusy(true); + try { + const seconds = Number(intervalValue); + const result = await saveConfig({check_interval_seconds:seconds,delay_refresh_interval_seconds:seconds}); + setNotice(result.message || '间隔已保存'); + await reload(); + } + finally { + setBusy(false); + } + }; + const runRefresh = async()=>{ + setBusy(true); + try { + const result = await requestFullDelay(); + setNotice(result.message || '已请求全量 delay'); + if (result.state) setState(result.state); + } + finally { + setBusy(false); + } + }; + const saveSwitch = async(autoSwitch)=>{ + setBusy(true); + try { + const result = await saveConfig({check_interval_seconds:Number(intervalValue),delay_refresh_interval_seconds:Number(intervalValue),auto_switch:autoSwitch}); + setNotice(result.message || '切换策略已保存'); + await reload(); + } + finally { + setBusy(false); + } + }; + const runSwitch = async()=>{ + setBusy(true); + try { + const result = await requestAutoSwitch(); + setNotice(result.message || '切换判定完成'); + if (result.state) setState(result.state); + } + finally { + setBusy(false); + } + }; + const safeRun = fn=>fn().catch(err=>setError(err.message)); + return safeRun(saveInterval)} onRefresh={()=>safeRun(runRefresh)} onReload={()=>safeRun(reload)} autoRefresh={autoRefresh} setAutoRefresh={setAutoRefresh} busy={busy} status={notice}/>{state ? <>safeRun(()=>saveSwitch(autoSwitch))} onRun={()=>safeRun(runSwitch)} busy={busy}/> : 正在读取服务状态}setError('')}>setError('')}>{error}; +} diff --git a/src/node_service/web/src/api.js b/src/node_service/web/src/api.js new file mode 100644 index 0000000..da14b14 --- /dev/null +++ b/src/node_service/web/src/api.js @@ -0,0 +1,23 @@ +export async function readState() { + const response = await fetch('/api/state',{cache:'no-store'}); + return await readJsonResponse(response); +} +export async function saveConfig(config) { + const response = await fetch('/api/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(config)}); + return await readJsonResponse(response); +} +export async function requestFullDelay() { + const response = await fetch('/api/refresh',{method:'POST'}); + return await readJsonResponse(response); +} +export async function requestAutoSwitch() { + const response = await fetch('/api/switch',{method:'POST'}); + return await readJsonResponse(response); +} +async function readJsonResponse(response) { + const data = await response.json(); + if (!response.ok || data.success === false) { + throw new Error(data.error || data.message || `HTTP ${response.status}`); + } + return data; +} diff --git a/src/node_service/web/src/main.jsx b/src/node_service/web/src/main.jsx new file mode 100644 index 0000000..a807452 --- /dev/null +++ b/src/node_service/web/src/main.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import {CssBaseline, ThemeProvider} from '@mui/material'; +import App from './App.jsx'; +import {theme} from './theme.js'; +createRoot(document.getElementById('root')).render( + + + + + + +); diff --git a/src/node_service/web/src/theme.js b/src/node_service/web/src/theme.js new file mode 100644 index 0000000..02ebd00 --- /dev/null +++ b/src/node_service/web/src/theme.js @@ -0,0 +1,22 @@ +import {createTheme} from '@mui/material/styles'; +export const theme = createTheme({ + palette:{ + mode:'light', + primary:{main:'#2563eb'}, + success:{main:'#059669'}, + warning:{main:'#d97706'}, + error:{main:'#dc2626'}, + background:{default:'#f3f6fb',paper:'#ffffff'} + }, + shape:{borderRadius:14}, + typography:{ + fontFamily:'Segoe UI,Microsoft YaHei,Arial,sans-serif', + h5:{fontWeight:800}, + h6:{fontWeight:800} + }, + components:{ + MuiPaper:{styleOverrides:{root:{backgroundImage:'none'}}}, + MuiButton:{styleOverrides:{root:{textTransform:'none',fontWeight:700}}}, + MuiTableCell:{styleOverrides:{head:{fontWeight:800,color:'#475569',background:'#f8fafc'},root:{whiteSpace:'nowrap'}}} + } +}); diff --git a/src/node_service/web/vite.config.js b/src/node_service/web/vite.config.js new file mode 100644 index 0000000..089f086 --- /dev/null +++ b/src/node_service/web/vite.config.js @@ -0,0 +1,10 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ + plugins:[react()], + base:'/', + build:{ + outDir:'dist', + emptyOutDir:true + } +});