大重构前
This commit is contained in:
@@ -34,6 +34,188 @@ json group_to_json(const Group_Switch_State& group) {
|
||||
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")});
|
||||
result["stability_category"] = json::array({enum_item("excellent", "稳定 ≥ 80"), enum_item("normal", "稳定 50~80"), enum_item("poor", "稳定 < 50")});
|
||||
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<std::string> countries;
|
||||
std::set<std::string> 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) {
|
||||
@@ -46,26 +228,69 @@ json node_to_json(const Node_State& node) {
|
||||
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<uint64_t>(std::count(node.recent_results.begin(), node.recent_results.end(), 1));
|
||||
const double total_rate_for_category = node.samples == 0 ? 0.0 : static_cast<double>(node.success_count) * 100.0 / static_cast<double>(node.samples);
|
||||
const double recent_rate_for_category = node.recent_results.empty() ? 0.0 : static_cast<double>(recent_pass_for_category) * 100.0 / static_cast<double>(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["rto_ms"] = node.rto_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<uint64_t>(std::count(node.recent_results.begin(), node.recent_results.end(), 1));
|
||||
result["success_rate"] = node.samples == 0 ? 0.0 : static_cast<double>(node.success_count) * 100.0 / static_cast<double>(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<double>(recent_pass) * 100.0 / static_cast<double>(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));
|
||||
@@ -80,12 +305,20 @@ void load_node_state(Node_State& node, const fs::path& path) {
|
||||
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.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<int>() == 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<std::ptrdiff_t>(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);
|
||||
@@ -131,6 +364,8 @@ void save_service_index(Service_State& service) {
|
||||
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["groups"] = json::array();
|
||||
for (const auto& name : service.group_order) {
|
||||
@@ -151,13 +386,16 @@ void save_service_index(Service_State& service) {
|
||||
double clamp_score(double value) {
|
||||
return std::clamp(value, 0.0, 100.0);
|
||||
}
|
||||
void update_rto(Node_State& node, std::optional<int> delay_ms) {
|
||||
void update_node_statistics(Node_State& node, std::optional<int> 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;
|
||||
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);
|
||||
@@ -171,16 +409,17 @@ void update_rto(Node_State& node, std::optional<int> delay_ms) {
|
||||
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.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;
|
||||
}
|
||||
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 auto recent_pass = static_cast<uint64_t>(std::count(node.recent_results.begin(), node.recent_results.end(), 1));
|
||||
const double recent_rate = node.recent_results.empty() ? node.success_ewma : static_cast<double>(recent_pass) / static_cast<double>(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<double>(node.consecutive_failures) * 15.0);
|
||||
node.stability_score = clamp_score(node.success_ewma * 100.0 - rto_penalty - jitter_penalty - failure_penalty);
|
||||
node.stability_score = clamp_score(recent_rate * 100.0 - latency_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);
|
||||
@@ -255,9 +494,6 @@ void reload_switch_groups_locked(Service_State& service) {
|
||||
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()) {
|
||||
@@ -265,6 +501,8 @@ void reload_switch_groups_locked(Service_State& service) {
|
||||
}
|
||||
group.name = def.name;
|
||||
group.type = def.type;
|
||||
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") {
|
||||
@@ -274,8 +512,14 @@ void reload_switch_groups_locked(Service_State& service) {
|
||||
group.candidates.push_back(proxy);
|
||||
}
|
||||
}
|
||||
group.switchable = !group.candidates.empty();
|
||||
if (group.switchable) {
|
||||
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 = "等待自动切换判定";
|
||||
|
||||
Reference in New Issue
Block a user