常规更新
This commit is contained in:
@@ -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<int> 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<double>(*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<double>(node.consecutive_failures) * 15.0);
|
||||
node.stability_score = clamp_score(node.success_ewma * 100.0 - rto_penalty - jitter_penalty - failure_penalty);
|
||||
}
|
||||
std::vector<std::string> 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<std::string> names;
|
||||
std::set<std::string> seen;
|
||||
if (!proxies || !proxies.IsSequence()) {
|
||||
return names;
|
||||
}
|
||||
for (const auto& proxy : proxies) {
|
||||
if (!proxy["name"]) {
|
||||
continue;
|
||||
}
|
||||
const auto name = proxy["name"].as<std::string>();
|
||||
if (contain(name, "剩余流量") || contain(name, "套餐到期")) {
|
||||
continue;
|
||||
}
|
||||
if (seen.insert(name).second) {
|
||||
names.push_back(name);
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
std::vector<Proxy_Group_Def> 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<Proxy_Group_Def> 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<std::string>();
|
||||
group.type = item["type"].as<std::string>();
|
||||
const auto proxies = item["proxies"];
|
||||
if (proxies && proxies.IsSequence()) {
|
||||
std::set<std::string> seen;
|
||||
for (const auto& proxy : proxies) {
|
||||
const auto name = proxy.as<std::string>();
|
||||
if (seen.insert(name).second) {
|
||||
group.proxies.push_back(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push_back(std::move(group));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
bool contains_name(const std::vector<std::string>& 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<std::string> node_names(service.node_order.begin(), service.node_order.end());
|
||||
std::unordered_map<std::string, Group_Switch_State> next_groups;
|
||||
std::vector<std::string> 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()));
|
||||
}
|
||||
Reference in New Issue
Block a user