#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["proxies"] = group.proxies; 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; result["managed"] = group.managed; result["strategy"] = group.strategy; return result; } namespace { json enum_item(std::string_view value, std::string_view label) { return json{{"value", std::string(value)}, {"label", std::string(label)}}; } bool text_has(std::string_view text, std::string_view key) { return text.find(key) != std::string_view::npos; } std::string score_category(double value) { if (value >= 80.0) { return "excellent"; } if (value >= 50.0) { return "normal"; } return "poor"; } std::string rate_category(double value, uint64_t count) { if (count == 0) { return "unknown"; } if (value >= 95.0) { return "excellent"; } if (value >= 80.0) { return "usable"; } return "poor"; } std::string ai_category(int value) { if (value < 0) { return "unknown"; } if (value >= 4) { return "high"; } if (value >= 3) { return "usable"; } return "weak"; } std::string ip_type_category(std::string_view value) { if (value.empty()) { return "unknown"; } if (text_has(value, "IDC") || text_has(value, "机房") || text_has(value, "数据中心")) { return "idc"; } if (text_has(value, "家庭") || text_has(value, "住宅") || text_has(value, "家宽") || text_has(value, "宽带")) { return "residential"; } if (text_has(value, "移动") || text_has(value, "手机") || text_has(value, "蜂窝")) { return "mobile"; } if (text_has(value, "教育") || text_has(value, "学校")) { return "education"; } return "other"; } std::string risk_category(int risk, std::string_view level) { if (text_has(level, "纯净")) { return "clean"; } if (text_has(level, "高风险") || text_has(level, "危险")) { return "high"; } if (text_has(level, "风险")) { return "risk"; } if (risk < 0) { return "unknown"; } if (risk >= 80) { return "high"; } if (risk >= 50) { return "risk"; } if (risk >= 25) { return "normal"; } return "clean"; } std::string shared_category(std::string_view value) { if (value.empty()) { return "unknown"; } if (text_has(value, "极好") || text_has(value, "1 - 10") || text_has(value, "1-10")) { return "excellent"; } if (text_has(value, "良好") || text_has(value, "10 - 50") || text_has(value, "10-50")) { return "good"; } if (text_has(value, "差") || text_has(value, "多") || text_has(value, "100") || text_has(value, "1000")) { return "high"; } return "normal"; } std::string native_category(std::string_view value) { if (value.empty()) { return "unknown"; } if (text_has(value, "原生") && !text_has(value, "非原生")) { return "native"; } if (text_has(value, "广播")) { return "broadcast"; } if (text_has(value, "非原生")) { return "non_native"; } return "other"; } std::string ip_version_category(const Node_State& node) { const bool has_ipv4 = !node.exit_ipv4.empty(); const bool has_ipv6 = !node.exit_ipv6.empty() && node.exit_ipv6 != "无"; if (has_ipv4 && has_ipv6) { return "dual"; } if (has_ipv4) { return "ipv4_only"; } if (has_ipv6) { return "ipv6_only"; } return "none"; } std::string enum_label(const json& options, std::string_view name, std::string_view value) { const auto item = options.find(std::string(name)); if (item == options.end() || !item->is_array()) { return std::string(value); } for (const auto& option : *item) { if (option.value("value", std::string{}) == std::string(value)) { return option.value("label", std::string(value)); } } return std::string(value); } } json enum_options_to_json() { json result; result["alive_category"] = json::array({enum_item("alive", "alive"), enum_item("dead", "dead"), enum_item("special", "特殊")}); result["stability_category"] = json::array({enum_item("excellent", "稳定 ≥ 80"), enum_item("normal", "稳定 50~80"), enum_item("poor", "稳定 < 50"), enum_item("unknown", "特殊/未知")}); result["rate_category"] = json::array({enum_item("excellent", "通过率 ≥ 95%"), enum_item("usable", "通过率 80~95%"), enum_item("poor", "通过率 < 80%"), enum_item("unknown", "未检测")}); result["ai_category"] = json::array({enum_item("high", "AI 4~5 星"), enum_item("usable", "AI 3 星"), enum_item("weak", "AI < 3 星"), enum_item("unknown", "AI 未知")}); result["ip_type_category"] = json::array({enum_item("idc", "IDC/机房"), enum_item("residential", "家庭宽带"), enum_item("mobile", "移动网络"), enum_item("education", "教育网络"), enum_item("other", "其他"), enum_item("unknown", "未知")}); result["risk_category"] = json::array({enum_item("clean", "纯净/低风险"), enum_item("normal", "普通"), enum_item("risk", "风险"), enum_item("high", "高风险"), enum_item("unknown", "未知")}); result["shared_category"] = json::array({enum_item("excellent", "极好"), enum_item("good", "良好"), enum_item("normal", "普通"), enum_item("high", "共享偏高"), enum_item("unknown", "未知")}); result["native_category"] = json::array({enum_item("native", "原生"), enum_item("broadcast", "广播"), enum_item("non_native", "非原生"), enum_item("other", "其他"), enum_item("unknown", "未知")}); result["ip_version_category"] = json::array({enum_item("dual", "IPv4 + IPv6"), enum_item("ipv4_only", "仅 IPv4"), enum_item("ipv6_only", "仅 IPv6"), enum_item("none", "无 IP 缓存")}); result["switch_strategy"] = switch_strategy_options_to_json(); return result; } json dynamic_filter_options_to_json(const Service_State& service) { std::set countries; std::set providers; for (const auto& name : service.node_order) { const auto item = service.nodes.find(name); if (item == service.nodes.end()) { continue; } if (!item->second.country.empty()) { countries.insert(item->second.country); } if (!item->second.provider.empty()) { providers.insert(item->second.provider); } } json result; result["country"] = json::array(); result["provider"] = json::array(); for (const auto& country : countries) { result["country"].push_back(enum_item(country, country)); } for (const auto& provider : providers) { result["provider"].push_back(enum_item(provider, provider)); } 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["multiplier"] = node.multiplier; result["multiplier_text"] = node.multiplier_text; result["shared_users"] = node.shared_users; result["ip_type"] = node.ip_type; result["native_type"] = node.native_type; result["country"] = node.country; const auto options = enum_options_to_json(); const auto recent_pass_for_category = static_cast(std::count(node.recent_results.begin(), node.recent_results.end(), 1)); const double total_rate_for_category = node.samples == 0 ? 0.0 : static_cast(node.success_count) * 100.0 / static_cast(node.samples); const double recent_rate_for_category = node.recent_results.empty() ? 0.0 : static_cast(recent_pass_for_category) * 100.0 / static_cast(node.recent_results.size()); const auto alive_enum = node.alive ? std::string("alive") : std::string("dead"); const auto stability_enum = score_category(node.stability_score); const auto total_rate_enum = rate_category(total_rate_for_category, node.samples); const auto recent_rate_enum = rate_category(recent_rate_for_category, node.recent_results.size()); const auto ai_enum = ai_category(node.ai_score); const auto ip_type_enum = ip_type_category(node.ip_type); const auto risk_enum = risk_category(node.risk, node.risk_level); const auto shared_enum = shared_category(node.shared_users); const auto native_enum = native_category(node.native_type); const auto ip_version_enum = ip_version_category(node); result["alive"] = node.alive; result["alive_category"] = alive_enum; result["alive_category_label"] = enum_label(options, "alive_category", alive_enum); result["stability_category"] = stability_enum; result["stability_category_label"] = enum_label(options, "stability_category", stability_enum); result["total_rate_category"] = total_rate_enum; result["total_rate_category_label"] = enum_label(options, "rate_category", total_rate_enum); result["recent_rate_category"] = recent_rate_enum; result["recent_rate_category_label"] = enum_label(options, "rate_category", recent_rate_enum); result["ai_category"] = ai_enum; result["ai_category_label"] = enum_label(options, "ai_category", ai_enum); result["ip_type_category"] = ip_type_enum; result["ip_type_category_label"] = enum_label(options, "ip_type_category", ip_type_enum); result["risk_category"] = risk_enum; result["risk_category_label"] = enum_label(options, "risk_category", risk_enum); result["shared_category"] = shared_enum; result["shared_category_label"] = enum_label(options, "shared_category", shared_enum); result["native_category"] = native_enum; result["native_category_label"] = enum_label(options, "native_category", native_enum); result["ip_version_category"] = ip_version_enum; result["ip_version_category_label"] = enum_label(options, "ip_version_category", ip_version_enum); result["initialized"] = node.initialized; result["srtt_ms"] = node.srtt_ms; result["rttvar_ms"] = node.rttvar_ms; result["success_ewma"] = node.success_ewma; result["samples"] = node.samples; result["check_count"] = node.samples; result["success_count"] = node.success_count; result["pass_count"] = node.success_count; result["failure_count"] = node.failure_count; result["consecutive_failures"] = node.consecutive_failures; const auto recent_pass = static_cast(std::count(node.recent_results.begin(), node.recent_results.end(), 1)); result["success_rate"] = node.samples == 0 ? 0.0 : static_cast(node.success_count) * 100.0 / static_cast(node.samples); result["recent_check_count"] = node.recent_results.size(); result["recent_pass_count"] = recent_pass; result["recent_success_rate"] = node.recent_results.empty() ? 0.0 : static_cast(recent_pass) * 100.0 / static_cast(node.recent_results.size()); 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["recent_results"] = node.recent_results; 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.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.recent_results.clear(); if (data.contains("recent_results") && data["recent_results"].is_array()) { for (const auto& item : data["recent_results"]) { node.recent_results.push_back(item.get() == 0 ? 0 : 1); } if (node.recent_results.size() > recent_result_window) { node.recent_results.erase(node.recent_results.begin(), node.recent_results.end() - static_cast(recent_result_window)); } } 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["enum_options"] = enum_options_to_json(); result["dynamic_filter_options"] = dynamic_filter_options_to_json(service); result["auto_switch"] = auto_switch_config_to_json(service.config.auto_switch); result["table_protocol"] = Table_Protocol_Registry::instance().protocol(service); result["table_rows"] = Table_Protocol_Registry::instance().rows(service); 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_node_statistics(Node_State& node, std::optional delay_ms) { ++node.samples; node.recent_results.push_back(delay_ms ? 1 : 0); if (node.recent_results.size() > recent_result_window) { node.recent_results.erase(node.recent_results.begin()); } if (!delay_ms) { ++node.failure_count; ++node.consecutive_failures; node.success_ewma = node.samples == 1 ? 0.0 : node.success_ewma * 0.95; 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 - ewma_rttvar_beta) * node.rttvar_ms + ewma_rttvar_beta * std::abs(node.srtt_ms - rtt); node.srtt_ms = (1.0 - ewma_rtt_alpha) * node.srtt_ms + ewma_rtt_alpha * rtt; } } void update_score(Node_State& node) { const auto recent_pass = static_cast(std::count(node.recent_results.begin(), node.recent_results.end(), 1)); const double recent_rate = node.recent_results.empty() ? node.success_ewma : static_cast(recent_pass) / static_cast(node.recent_results.size()); const double latency_penalty = node.initialized ? std::min(30.0, node.srtt_ms / 50.0) : 30.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(recent_rate * 100.0 - latency_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) { 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.proxies = def.proxies; group.managed = should_manage_group(service.config.auto_switch, def); group.strategy = group_switch_strategy(service.config.auto_switch, group.name); 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.managed && !group.candidates.empty(); if (!group.managed) { group.last_error.clear(); if (group.last_decision.empty()) { group.last_decision = "不受自动切换管理"; } } else 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())); }