591 lines
24 KiB
C++
591 lines
24 KiB
C++
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
|
||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||
#endif
|
||
#include "Proxy_Ping0_Labeler.h"
|
||
#include "Async_Blocking.h"
|
||
#include "Ping0_Html_Parser.h"
|
||
#include "export.h"
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <chrono>
|
||
#include <filesystem>
|
||
#include <format>
|
||
#include <fstream>
|
||
#include <iostream>
|
||
#include <memory>
|
||
#include <iterator>
|
||
#include <limits>
|
||
#include <map>
|
||
#include <mutex>
|
||
#include <optional>
|
||
#include <sstream>
|
||
#include <system_error>
|
||
#include <thread>
|
||
#include <string>
|
||
#include <string_view>
|
||
#include <vector>
|
||
#include <nlohmann/json.hpp>
|
||
namespace fs = std::filesystem;
|
||
namespace {
|
||
constexpr std::string_view ipv4_query_url_host = "https://api4.ipify.org";
|
||
constexpr std::string_view ipv6_query_url_host = "https://api6.ipify.org";
|
||
constexpr std::string_view ip_query_url_path = "/?format=json";
|
||
constexpr std::string_view ip_check_password = "ip-check";
|
||
constexpr long ip_query_timeout_ms = 15000;
|
||
struct Node_Route {
|
||
size_t index = 0;
|
||
std::string username;
|
||
std::string proxy_name;
|
||
};
|
||
struct Public_Ip_Cache {
|
||
std::optional<std::string> ipv4;
|
||
std::optional<std::string> ipv6;
|
||
std::optional<std::string> ipv4_error;
|
||
std::optional<std::string> ipv6_error;
|
||
bool has_ipv4_state() const {
|
||
return ipv4.has_value() || ipv4_error.has_value();
|
||
}
|
||
bool has_ipv6_state() const {
|
||
return ipv6.has_value() || ipv6_error.has_value();
|
||
}
|
||
};
|
||
struct Node_Ping0_Result {
|
||
std::string name;
|
||
std::string public_ipv4;
|
||
std::optional<std::string> public_ipv6;
|
||
fs::path ip_cache_path;
|
||
fs::path ping0_html_path;
|
||
Ping0::Ip_Info info;
|
||
size_t order = 0;
|
||
std::optional<int> last_delay_ms;
|
||
};
|
||
std::vector<Node_Ping0_Result> ai_node_infos;
|
||
std::array<std::mutex, 256> public_ip_cache_locks;
|
||
fs::path path_from_utf8(std::string_view value) {
|
||
return ::path_from_utf8(value);
|
||
}
|
||
std::string path_to_utf8(const fs::path& path) {
|
||
return ::path_to_utf8(path);
|
||
}
|
||
std::string trim_ascii(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 make_utc_time() {
|
||
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);
|
||
}
|
||
std::string make_safe_file_name(std::string_view name) {
|
||
std::string result;
|
||
result.reserve(name.size());
|
||
for (const unsigned char ch : name) {
|
||
if (ch < 32 || ch == '"' || ch == '*' || ch == '/' || ch == ':' || ch == '<' || ch == '>' || ch == '?' || ch == '\\' || ch == '|') {
|
||
result.push_back('_');
|
||
}
|
||
else {
|
||
result.push_back(static_cast<char>(ch));
|
||
}
|
||
}
|
||
while (!result.empty() && (result.back() == ' ' || result.back() == '.')) {
|
||
result.pop_back();
|
||
}
|
||
return result.empty() ? std::string{"_"} : result;
|
||
}
|
||
void write_file_atomic(const fs::path& path, std::string_view content) {
|
||
fs::create_directories(path.parent_path());
|
||
auto temporary_path = path;
|
||
temporary_path += L".tmp";
|
||
{
|
||
std::ofstream stream(temporary_path, std::ios::binary | std::ios::trunc);
|
||
if (!stream) {
|
||
throw std::runtime_error(std::format("无法创建文件:{}", path_to_utf8(temporary_path)));
|
||
}
|
||
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
|
||
}
|
||
std::error_code error;
|
||
fs::rename(temporary_path, path, error);
|
||
if (error) {
|
||
fs::remove(path, error);
|
||
error.clear();
|
||
fs::rename(temporary_path, path, error);
|
||
if (error) {
|
||
fs::remove(temporary_path);
|
||
throw std::runtime_error(std::format("保存文件失败:{},{}", path_to_utf8(path), error_text_to_utf8(error.message())));
|
||
}
|
||
}
|
||
}
|
||
YAML::Node load_yaml_file(const fs::path& path) {
|
||
std::ifstream stream(path, std::ios::binary);
|
||
if (!stream) {
|
||
throw std::runtime_error(std::format("无法打开 YAML:{}", path_to_utf8(path)));
|
||
}
|
||
return YAML::Load(stream);
|
||
}
|
||
YAML::Node find_yaml_value(const YAML::Node& node, std::string_view key) {
|
||
if (!node.IsMap()) {
|
||
return {};
|
||
}
|
||
for (const auto& item : node) {
|
||
if (item.first.IsScalar() && item.first.as<std::string>() == key) {
|
||
return item.second;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
std::optional<std::string> read_optional_string(const YAML::Node& node, std::string_view key) {
|
||
const auto value = find_yaml_value(node, key);
|
||
if (!value || !value.IsScalar()) {
|
||
return std::nullopt;
|
||
}
|
||
const auto text = trim_ascii(value.as<std::string>());
|
||
if (text.empty()) {
|
||
return std::nullopt;
|
||
}
|
||
return text;
|
||
}
|
||
std::optional<uint16_t> read_current_mixed_port(const fs::path& root) {
|
||
const auto current_config = (root / L".." / L"config" / L"config.yaml").lexically_normal();
|
||
if (!fs::is_regular_file(current_config)) {
|
||
std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:找不到当前 Mihomo 配置 {}\n", path_to_utf8(current_config));
|
||
return std::nullopt;
|
||
}
|
||
try {
|
||
const auto config = load_yaml_file(current_config);
|
||
const auto port = find_yaml_value(config, "mixed-port");
|
||
if (!port || !port.IsScalar()) {
|
||
std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:当前 Mihomo 配置没有 mixed-port:{}\n", path_to_utf8(current_config));
|
||
return std::nullopt;
|
||
}
|
||
return port.as<uint16_t>();
|
||
}
|
||
catch (const std::exception& error) {
|
||
std::cerr << std::format("Ping0 标注跳过未缓存出口 IP:读取当前 Mihomo 配置失败:{},{}\n", path_to_utf8(current_config), error_text_to_utf8(error.what()));
|
||
return std::nullopt;
|
||
}
|
||
}
|
||
std::vector<Node_Route> make_node_routes(const YAML::Node& proxies) {
|
||
std::vector<Node_Route> routes;
|
||
routes.reserve(proxies.size());
|
||
size_t username_index = 1;
|
||
for (size_t i = 0; i < proxies.size(); ++i) {
|
||
const auto name = proxies[i]["name"].as<std::string>();
|
||
if (contain(name, "剩余流量") || contain(name, "套餐到期")) {
|
||
continue;
|
||
}
|
||
routes.push_back({i, std::format("n{:04}", username_index++), name});
|
||
}
|
||
return routes;
|
||
}
|
||
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<std::string>{}(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)) {
|
||
return result;
|
||
}
|
||
const auto cache = load_yaml_file(path);
|
||
result.ipv4 = read_optional_string(cache, "public_ipv4");
|
||
if (!result.ipv4) {
|
||
result.ipv4 = read_optional_string(cache, "public_ip");
|
||
}
|
||
result.ipv6 = read_optional_string(cache, "public_ipv6");
|
||
result.ipv4_error = read_optional_string(cache, "public_ipv4_error");
|
||
result.ipv6_error = read_optional_string(cache, "public_ipv6_error");
|
||
return result;
|
||
}
|
||
std::string query_public_ip_by_user(std::string_view username, uint16_t mixed_port, std::string_view host) {
|
||
httplib::Client client{std::string(host)};
|
||
client.set_proxy("127.0.0.1", mixed_port);
|
||
client.set_proxy_basic_auth(std::string(username), std::string(ip_check_password));
|
||
client.set_follow_location(true);
|
||
client.set_connection_timeout(std::chrono::milliseconds(std::min(ip_query_timeout_ms, 6000L)));
|
||
client.set_read_timeout(std::chrono::milliseconds(ip_query_timeout_ms));
|
||
client.set_write_timeout(std::chrono::milliseconds(ip_query_timeout_ms));
|
||
client.set_max_timeout(std::chrono::milliseconds(ip_query_timeout_ms));
|
||
httplib::Headers headers{{"User-Agent", "wyc-node-ip-cache/1.0"}, {"Accept-Encoding", ""}};
|
||
const auto response = client.Get(std::string(ip_query_url_path), headers);
|
||
if (!response) {
|
||
throw std::runtime_error(std::format("ipify 请求失败:{}", httplib::to_string(response.error())));
|
||
}
|
||
if (response->status < 200 || response->status >= 300) {
|
||
throw std::runtime_error(std::format("ipify 返回 HTTP {}", response->status));
|
||
}
|
||
const auto data = nlohmann::json::parse(response->body);
|
||
return trim_ascii(data.at("ip").get<std::string>());
|
||
}
|
||
std::string source_url(std::string_view host) {
|
||
return std::string(host) + std::string(ip_query_url_path);
|
||
}
|
||
void save_public_ip_cache(const fs::path& path, const Node_Route& route, const Public_Ip_Cache& cache, uint16_t mixed_port) {
|
||
YAML::Node node(YAML::NodeType::Map);
|
||
node["proxy_name"] = route.proxy_name;
|
||
node["username"] = route.username;
|
||
node["mixed_port"] = mixed_port;
|
||
if (cache.ipv4) {
|
||
node["public_ip"] = *cache.ipv4;
|
||
node["public_ipv4"] = *cache.ipv4;
|
||
}
|
||
if (cache.ipv6) {
|
||
node["public_ipv6"] = *cache.ipv6;
|
||
}
|
||
if (cache.ipv4_error) {
|
||
node["public_ipv4_error"] = *cache.ipv4_error;
|
||
}
|
||
if (cache.ipv6_error) {
|
||
node["public_ipv6_error"] = *cache.ipv6_error;
|
||
}
|
||
node["cached_at_utc"] = make_utc_time();
|
||
node["ipv4_source"] = source_url(ipv4_query_url_host);
|
||
node["ipv6_source"] = source_url(ipv6_query_url_host);
|
||
YAML::Emitter emitter;
|
||
emitter.SetIndent(2);
|
||
emitter << node;
|
||
if (!emitter.good()) {
|
||
throw std::runtime_error(std::format("生成出口 IP 缓存失败:{}", emitter.GetLastError()));
|
||
}
|
||
write_file_atomic(path, std::string_view(emitter.c_str(), emitter.size()));
|
||
}
|
||
Public_Ip_Cache get_public_ips(const Node_Route& route, const fs::path& cache_dir, std::optional<uint16_t> 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) {
|
||
return cache;
|
||
}
|
||
if (!cache.has_ipv4_state()) {
|
||
try {
|
||
cache.ipv4 = query_public_ip_by_user(route.username, *mixed_port, ipv4_query_url_host);
|
||
}
|
||
catch (const std::exception& error) {
|
||
cache.ipv4_error = error_text_to_utf8(error.what());
|
||
}
|
||
changed = true;
|
||
}
|
||
if (!cache.has_ipv6_state()) {
|
||
try {
|
||
cache.ipv6 = query_public_ip_by_user(route.username, *mixed_port, ipv6_query_url_host);
|
||
}
|
||
catch (const std::exception& error) {
|
||
cache.ipv6_error = error_text_to_utf8(error.what());
|
||
}
|
||
changed = true;
|
||
}
|
||
if (changed) {
|
||
save_public_ip_cache(cache_path, route, cache, *mixed_port);
|
||
}
|
||
return cache;
|
||
}
|
||
fs::path make_ping0_html_file_name(std::string_view ip) {
|
||
return path_from_utf8(std::format("{}-高精度IP地址归属地查询-IP风控值查询-原生IP查询-IP类型查询-家庭宽带IP查询-全球小鸡监控平台.html", ip));
|
||
}
|
||
std::optional<fs::path> find_ping0_html_path(const fs::path& cache_dir, std::string_view ip) {
|
||
const auto exact = cache_dir / make_ping0_html_file_name(ip);
|
||
if (fs::is_regular_file(exact)) {
|
||
return exact;
|
||
}
|
||
if (!fs::is_directory(cache_dir)) {
|
||
return std::nullopt;
|
||
}
|
||
const auto prefix = std::string(ip) + "-";
|
||
for (const auto& item : fs::directory_iterator(cache_dir)) {
|
||
if (!item.is_regular_file()) {
|
||
continue;
|
||
}
|
||
const auto name = path_to_utf8(item.path().filename());
|
||
if (name.starts_with(prefix) && path_to_utf8(item.path().extension()) == ".html") {
|
||
return item.path();
|
||
}
|
||
}
|
||
return std::nullopt;
|
||
}
|
||
int ai_scene_score(const Ping0::Ip_Info& info) {
|
||
for (const auto& scene : info.scenes) {
|
||
if (scene.name.find("AI") != std::string::npos || scene.name.find("ai") != std::string::npos) {
|
||
return scene.score;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
std::string ai_scene_advice(const Ping0::Ip_Info& info) {
|
||
for (const auto& scene : info.scenes) {
|
||
if (scene.name.find("AI") != std::string::npos || scene.name.find("ai") != std::string::npos) {
|
||
return scene.advice;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
int first_number_or_max(std::string_view value) {
|
||
int result = 0;
|
||
bool has_number = false;
|
||
for (const unsigned char ch : value) {
|
||
if (ch >= '0' && ch <= '9') {
|
||
result = result * 10 + ch - '0';
|
||
has_number = true;
|
||
}
|
||
else if (has_number) {
|
||
return result;
|
||
}
|
||
}
|
||
return has_number ? result : std::numeric_limits<int>::max();
|
||
}
|
||
int risk_sort_value(int value) {
|
||
return value >= 0 ? value : std::numeric_limits<int>::max();
|
||
}
|
||
int delay_sort_value(const Node_Ping0_Result& result) {
|
||
return result.last_delay_ms.value_or(std::numeric_limits<int>::max());
|
||
}
|
||
std::optional<int> read_node_last_delay_ms(const fs::path& node_state_dir, std::string_view name) {
|
||
const auto path = node_state_dir / path_from_utf8(make_safe_file_name(name) + ".json");
|
||
if (!fs::is_regular_file(path)) {
|
||
return std::nullopt;
|
||
}
|
||
std::ifstream stream(path, std::ios::binary);
|
||
const auto data = nlohmann::json::parse(stream);
|
||
const auto delay = data.value("last_delay_ms", 0);
|
||
return delay > 0 ? std::optional<int>{delay} : std::nullopt;
|
||
}
|
||
std::string country_from_location(std::string_view location) {
|
||
const auto first = location.find_first_not_of(" \t\r\n");
|
||
if (first == std::string_view::npos) {
|
||
return "未知";
|
||
}
|
||
const auto last = location.find_first_of(" \t\r\n", first);
|
||
return std::string(location.substr(first, last == std::string_view::npos ? location.size() - first : last - first));
|
||
}
|
||
std::string compact_text(std::string value) {
|
||
value.erase(std::remove_if(value.begin(), value.end(), [](unsigned char ch) {
|
||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' || ch == '(' || ch == ')' || ch == '%' || ch == '/' || ch == '\\' || ch == ':' || ch == '*' || ch == '?' || ch == '"' || ch == '<' || ch == '>' || ch == '|';
|
||
}), value.end());
|
||
return value;
|
||
}
|
||
std::string compact_ip_text(std::string value) {
|
||
for (char& ch : value) {
|
||
if (ch == ':') {
|
||
ch = '-';
|
||
}
|
||
}
|
||
return compact_text(std::move(value));
|
||
}
|
||
std::string make_proxy_name_suffix(const Node_Ping0_Result& result) {
|
||
const auto ai_score = ai_scene_score(result.info);
|
||
std::vector<std::string> parts;
|
||
if (ai_score >= 0) {
|
||
parts.push_back(std::format("AI:{}星", ai_score));
|
||
}
|
||
if (result.info.risk >= 0) {
|
||
parts.push_back(std::format("风控:{}:{}", result.info.risk, compact_text(result.info.risk_level)));
|
||
}
|
||
if (!result.info.shared_users.empty()) {
|
||
parts.push_back("共享:" + compact_text(result.info.shared_users));
|
||
}
|
||
if (!result.info.ip_type.empty()) {
|
||
parts.push_back(compact_text(result.info.ip_type));
|
||
}
|
||
if (!result.info.native_type.empty()) {
|
||
parts.push_back(compact_text(result.info.native_type));
|
||
}
|
||
parts.push_back("ip:" + compact_ip_text(result.public_ipv4));
|
||
if (result.public_ipv6) {
|
||
parts.push_back("_" + compact_ip_text(*result.public_ipv6));
|
||
}
|
||
|
||
std::string suffix;
|
||
for (const auto& part : parts) {
|
||
if (!part.empty()) {
|
||
suffix += " " + part;
|
||
}
|
||
}
|
||
return suffix;
|
||
}
|
||
void print_ping0_info(const Node_Route& route, const Node_Ping0_Result& result) {
|
||
const auto ai_score = ai_scene_score(result.info);
|
||
const auto advice = ai_scene_advice(result.info);
|
||
std::cout << std::format("Ping0 节点信息:{}\n", route.proxy_name);
|
||
std::cout << std::format(" 用户: {}\n", route.username);
|
||
std::cout << std::format(" 出口 IPv4: {}\n", result.public_ipv4);
|
||
std::cout << std::format(" 出口 IPv6: {}\n", result.public_ipv6.value_or("无"));
|
||
std::cout << std::format(" HTML 缓存: {}\n", path_to_utf8(result.ping0_html_path));
|
||
std::cout << std::format(" 位置: {}\n", result.info.location);
|
||
std::cout << std::format(" ASN: {} {}\n", result.info.asn, result.info.asn_owner.name);
|
||
std::cout << std::format(" IP 类型: {}\n", result.info.ip_type);
|
||
std::cout << std::format(" 原生属性: {}\n", result.info.native_type);
|
||
std::cout << std::format(" 风控: {}% {}\n", result.info.risk, result.info.risk_level);
|
||
std::cout << std::format(" 共享人数: {}\n", result.info.shared_users);
|
||
if (ai_score >= 0) {
|
||
std::cout << std::format(" AI 应用: {} 星 {}\n", ai_score, advice);
|
||
}
|
||
}
|
||
std::optional<Node_Ping0_Result> load_node_ping0_result(const Node_Route& route, const fs::path& ip_cache_dir, const fs::path& html_cache_dir, std::optional<uint16_t> mixed_port) {
|
||
const auto public_ips = get_public_ips(route, ip_cache_dir, mixed_port);
|
||
if (!public_ips.ipv4) {
|
||
std::cerr << std::format("Ping0 标注跳过:节点 {} 没有 IPv4 出口缓存\n", route.proxy_name);
|
||
return std::nullopt;
|
||
}
|
||
const auto html_path = find_ping0_html_path(html_cache_dir, *public_ips.ipv4);
|
||
if (!html_path) {
|
||
std::cerr << std::format("【注意添加缓存数据】================================ Ping0 HTML 缓存不存在,节点 {},出口 IPv4 {},目录 {}\n", route.proxy_name, *public_ips.ipv4, path_to_utf8(html_cache_dir));
|
||
return std::nullopt;
|
||
}
|
||
auto info = Ping0::parse_file(*html_path);
|
||
return Node_Ping0_Result{route.proxy_name, *public_ips.ipv4, public_ips.ipv6, make_ip_cache_path(ip_cache_dir, route.proxy_name), *html_path, std::move(info), route.index, std::nullopt};
|
||
}
|
||
void record_ai_node_info(Node_Ping0_Result result, const fs::path& node_state_dir) {
|
||
const auto ai_score = ai_scene_score(result.info);
|
||
if (ai_score < 3) {
|
||
return;
|
||
}
|
||
result.last_delay_ms = read_node_last_delay_ms(node_state_dir, result.name);
|
||
ai_node_infos.push_back(std::move(result));
|
||
}
|
||
struct Node_Ping0_Task_Result {
|
||
Node_Route route;
|
||
std::optional<Node_Ping0_Result> result;
|
||
std::string error;
|
||
};
|
||
asio::awaitable<void> load_node_ping0_result_task(asio::thread_pool& pool,
|
||
std::vector<Node_Ping0_Task_Result>& results,
|
||
size_t index,
|
||
fs::path ip_cache_dir,
|
||
fs::path html_cache_dir,
|
||
std::optional<uint16_t> mixed_port,
|
||
std::shared_ptr<size_t> 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();
|
||
}
|
||
asio::awaitable<void> 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();
|
||
const auto node_state_dir = (root / L".." / L"serer_state" / L"node_state").lexically_normal();
|
||
fs::create_directories(ip_cache_dir);
|
||
fs::create_directories(html_cache_dir);
|
||
ai_node_infos.clear();
|
||
const auto mixed_port = read_current_mixed_port(root);
|
||
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<size_t>(std::min(logical_cpu_count, 16u)));
|
||
if (routes.empty()) {
|
||
co_return;
|
||
}
|
||
asio::thread_pool pool(thread_count);
|
||
std::vector<Node_Ping0_Task_Result> results(routes.size());
|
||
for (size_t i = 0; i < routes.size(); ++i) {
|
||
results[i].route = routes[i];
|
||
}
|
||
auto pending = std::make_shared<size_t>(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;
|
||
}
|
||
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;
|
||
item.result->name = new_name;
|
||
record_ai_node_info(std::move(*item.result), node_state_dir);
|
||
}
|
||
}
|
||
}
|
||
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<std::string> make_ai_site_proxy_list() {
|
||
std::map<std::string, size_t> country_count;
|
||
for (const auto& info : ai_node_infos) {
|
||
++country_count[country_from_location(info.info.location)];
|
||
}
|
||
std::vector<Node_Ping0_Result> list = ai_node_infos;
|
||
std::stable_sort(list.begin(), list.end(), [&](const Node_Ping0_Result& left, const Node_Ping0_Result& right) {
|
||
const auto left_country = country_from_location(left.info.location);
|
||
const auto right_country = country_from_location(right.info.location);
|
||
const auto left_count = country_count[left_country];
|
||
const auto right_count = country_count[right_country];
|
||
if (left_count != right_count) {
|
||
return left_count > right_count;
|
||
}
|
||
const auto left_ai_score = ai_scene_score(left.info);
|
||
const auto right_ai_score = ai_scene_score(right.info);
|
||
if (left_ai_score != right_ai_score) {
|
||
return left_ai_score > right_ai_score;
|
||
}
|
||
const auto left_risk = risk_sort_value(left.info.risk);
|
||
const auto right_risk = risk_sort_value(right.info.risk);
|
||
if (left_risk != right_risk) {
|
||
return left_risk < right_risk;
|
||
}
|
||
const auto left_shared_users = first_number_or_max(left.info.shared_users);
|
||
const auto right_shared_users = first_number_or_max(right.info.shared_users);
|
||
if (left_shared_users != right_shared_users) {
|
||
return left_shared_users < right_shared_users;
|
||
}
|
||
const auto left_delay = delay_sort_value(left);
|
||
const auto right_delay = delay_sort_value(right);
|
||
if (left_delay != right_delay) {
|
||
return left_delay < right_delay;
|
||
}
|
||
return left.order < right.order;
|
||
});
|
||
std::vector<std::string> result;
|
||
result.reserve(list.size());
|
||
for (const auto& info : list) {
|
||
result.push_back(info.name);
|
||
}
|
||
std::cout << "AI 网站节点:按 AI>=3 星过滤,并按国家节点数量、AI 星级、风控、共享人数、延迟排序\n";
|
||
for (const auto& info : list) {
|
||
const auto country = country_from_location(info.info.location);
|
||
const auto delay = info.last_delay_ms ? std::format("{}ms", *info.last_delay_ms) : std::string{"未知"};
|
||
std::cout << std::format(" {},国家{}({}),AI{}星,风控{},共享{},延迟{}\n", info.name, country, country_count[country], ai_scene_score(info.info), info.info.risk, info.info.shared_users, delay);
|
||
}
|
||
return result;
|
||
}
|