refactor: migrate from bulit-in C++ clash parser to clash2singbox

This commit is contained in:
parhelia512 2025-11-30 21:17:56 +08:00
parent 2b5bde651b
commit 046acde181
11 changed files with 83 additions and 15192 deletions

14730
3rdparty/fkYAML/node.hpp vendored

File diff suppressed because it is too large Load Diff

View File

@ -50,9 +50,9 @@ Various formats are supported, including share links, JSON array of outbounds an
- [Qv2ray](https://github.com/Qv2ray/Qv2ray)
- [Qt](https://www.qt.io/)
- [simple-protobuf](https://github.com/tonda-kriz/simple-protobuf)
- [fkYAML](https://github.com/fktn-k/fkYAML)
- [quirc](https://github.com/dlbeer/quirc)
- [QHotkey](https://github.com/Skycoder42/QHotkey)
- [clash2singbox](https://github.com/xmdhs/clash2singbox)
## FAQ
**How does this project differ from the original Nekoray?** <br/>

View File

@ -44,9 +44,9 @@
- [Qv2ray](https://github.com/Qv2ray/Qv2ray)
- [Qt](https://www.qt.io/)
- [simple-protobuf](https://github.com/tonda-kriz/simple-protobuf)
- [fkYAML](https://github.com/fktn-k/fkYAML)
- [quirc](https://github.com/dlbeer/quirc)
- [QHotkey](https://github.com/Skycoder42/QHotkey)
- [clash2singbox](https://github.com/xmdhs/clash2singbox)
## FAQ
**这个项目与原始的 Nekoray 有什么不同?** <br/>

View File

@ -20,6 +20,8 @@ service LibcoreService {
rpc SpeedTest(SpeedTestRequest) returns(SpeedTestResponse);
rpc QuerySpeedTest(EmptyReq) returns(QuerySpeedTestResponse);
rpc QueryCountryTest(EmptyReq) returns(QueryCountryTestResponse);
//
rpc Clash2Singbox(Clash2SingboxRequest) returns(Clash2SingboxResponse);
}
message EmptyReq {}
@ -132,3 +134,12 @@ message QueryCountryTestResponse {
message QueryURLTestResponse {
repeated URLTestResp results = 1;
}
message Clash2SingboxRequest {
optional string clash_config = 1 [default = ""];
}
message Clash2SingboxResponse {
optional string singbox_config = 1 [default = ""];
optional string error = 2 [default = ""];
}

View File

@ -15,14 +15,18 @@ require (
github.com/sagernet/sing-box v1.12.12
github.com/sagernet/sing-tun v0.7.3
github.com/spf13/cobra v1.10.1
github.com/xmdhs/clash2singbox v0.1.5-0.20251129070952-d3d41337d2c1
golang.org/x/sys v0.37.0
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
)
replace github.com/sagernet/sing-box => github.com/throneproj/sing-box v1.11.16-0.20251027170654-efe4b5f5b1e4
replace github.com/sagernet/wireguard-go => github.com/throneproj/wireguard-go v0.0.1-beta.7.0.20250728063157-408bba78ad26
replace github.com/xmdhs/clash2singbox => github.com/throneproj/clash2singbox v0.1.5-0.20251130124907-02ead6993794
replace github.com/chai2010/protorpc => ../protorpc
require (

View File

@ -216,6 +216,8 @@ github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c=
github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY=
github.com/throneproj/clash2singbox v0.1.5-0.20251130124907-02ead6993794 h1:U0zcOKETHWz2N/V721V498cpRgHwm1EBrutxmRSAjjc=
github.com/throneproj/clash2singbox v0.1.5-0.20251130124907-02ead6993794/go.mod h1:v5Kl3ZsY7KkoK7uY9oC56uEdXRp0upRxNEWX9Us8siA=
github.com/throneproj/sing-box v1.11.16-0.20251027170654-efe4b5f5b1e4 h1:tQumipJlxqgMXvbuOJR8qMU4HoM0qbcU6RPA0DeAV9M=
github.com/throneproj/sing-box v1.11.16-0.20251027170654-efe4b5f5b1e4/go.mod h1:4hUwNgXeaqRWAuYxixxVBOEGRFIamyw12lrpx8hbZBc=
github.com/throneproj/wireguard-go v0.0.1-beta.7.0.20250728063157-408bba78ad26 h1:bBzqh7xTshvPjTFz4URNj/xbPA/d0BOwUM2R83FEMGU=
@ -305,6 +307,7 @@ google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -21,6 +21,11 @@ import (
"runtime"
"strings"
"time"
"encoding/json"
"github.com/xmdhs/clash2singbox/convert"
"github.com/xmdhs/clash2singbox/model"
"github.com/xmdhs/clash2singbox/model/clash"
"gopkg.in/yaml.v3"
)
var boxInstance *boxbox.Box
@ -403,3 +408,29 @@ func (s *server) QueryCountryTest(in *gen.EmptyReq, out *gen.QueryCountryTestRes
}
return nil
}
func (s *server) Clash2Singbox(in *gen.Clash2SingboxRequest, out *gen.Clash2SingboxResponse) (_ error) {
var convErr error
defer func() {
if convErr != nil {
out.Error = To(convErr.Error())
}
}()
c := clash.Clash{}
err := yaml.Unmarshal([]byte(*in.ClashConfig), &c)
if err != nil {
return
}
sing, convErr := convert.Clash2sing(c, model.SINGLATEST)
outb, err := json.Marshal(map[string]any{"outbounds": sing})
if err != nil {
return
}
out.SingboxConfig = To(string(outb))
return
}

View File

@ -39,6 +39,8 @@ namespace API {
libcore::QueryCountryTestResponse QueryCountryTestResults(bool *rpcOK);
QString Clash2Singbox(bool *rpcOK, const QString& config) const;
private:
std::function<std::unique_ptr<protorpc::Client>()> make_rpc_client;
std::function<void(const QString &)> onError;

View File

@ -5,8 +5,6 @@
namespace Subscription {
class RawUpdater {
public:
void updateClash(const QString &str);
void update(const QString &str, bool needParse);
void updateSingBox(const QString &str);

View File

@ -240,4 +240,27 @@ if (!Configs::dataStore->core_running) MW_show_log("Cannot invoke method " + QSt
}
}
QString Client::Clash2Singbox(bool* rpcOK, const QString& config) const
{
CHECK("Clash2Singbox")
libcore::Clash2SingboxRequest request;
libcore::Clash2SingboxResponse reply;
request.clash_config = config.toStdString();
std::string resp, req = spb::pb::serialize<std::string>(request);
auto err = make_rpc_client()->CallMethod("LibcoreService.Clash2Singbox", &req, &resp);
if(err.IsNil()) {
reply = spb::pb::deserialize< libcore::Clash2SingboxResponse >( resp );
*rpcOK = true;
QString error = QString::fromStdString(reply.error.value());
if (!error.isEmpty()) {
MW_show_log(QString("Failed to convert Clash config:\n") + error);
}
return QString::fromStdString(reply.singbox_config.value());
} else {
NOT_OK
return "";
}
}
} // namespace API

View File

@ -1,6 +1,7 @@
#include "include/dataStore/ProfileFilter.hpp"
#include "include/configs/proxy/includes.h"
#include "include/global/HTTPRequestHelper.hpp"
#include "include/api/RPC.h"
#include "include/configs/sub/GroupUpdater.hpp"
@ -8,8 +9,6 @@
#include <QUrlQuery>
#include <QJsonDocument>
#include "3rdparty/fkYAML/node.hpp"
namespace Subscription {
GroupUpdater *groupUpdater = new GroupUpdater;
@ -58,7 +57,12 @@ namespace Subscription {
// Clash
if (str.contains("proxies:")) {
updateClash(str);
bool ok;
QString resp = API::defaultClient->Clash2Singbox(&ok, str);
if (ok && !resp.isEmpty())
{
updateSingBox(resp);
}
return;
}
@ -323,461 +327,6 @@ namespace Subscription {
updated_order += ent;
}
QString Node2QString(const fkyaml::node &n, const QString &def = "") {
try {
return n.as_str().c_str();
} catch (const fkyaml::exception &ex) {
qDebug() << ex.what();
return def;
}
}
QStringList Node2QStringList(const fkyaml::node &n) {
try {
if (n.is_sequence()) {
QStringList list;
for (auto item: n) {
list << item.as_str().c_str();
}
return list;
} else {
return {};
}
} catch (const fkyaml::exception &ex) {
qDebug() << ex.what();
return {};
}
}
int Node2Int(const fkyaml::node &n, const int &def = 0) {
try {
if (n.is_integer())
return n.as_int();
else if (n.is_string())
return atoi(n.as_str().c_str());
return def;
} catch (const fkyaml::exception &ex) {
qDebug() << ex.what();
return def;
}
}
bool Node2Bool(const fkyaml::node &n, const bool &def = false) {
try {
return n.as_bool();
} catch (const fkyaml::exception &ex) {
try {
return n.as_int();
} catch (const fkyaml::exception &ex2) {
ex2.what();
}
qDebug() << ex.what();
return def;
}
}
// NodeChild returns the first defined children or Null Node
fkyaml::node NodeChild(const fkyaml::node &n, const std::list<std::string> &keys) {
for (const auto &key: keys) {
if (n.contains(key)) return n[key];
}
return {};
}
// https://github.com/Dreamacro/clash/wiki/configuration
void RawUpdater::updateClash(const QString &str) {
try {
auto proxies = fkyaml::node::deserialize(str.toStdString())["proxies"];
for (auto proxy: proxies) {
auto type = Node2QString(proxy["type"]).toLower();
auto type_clash = type;
if (type == "ss" || type == "ssr") type = "shadowsocks";
if (type == "socks5") type = "socks";
auto ent = Configs::ProfileManager::NewProxyEntity(type);
if (ent->outbound->DisplayType().isEmpty()) continue;
bool needFix = false;
// common
ent->outbound->name = Node2QString(proxy["name"]);
ent->outbound->server = Node2QString(proxy["server"]);
ent->outbound->server_port = Node2Int(proxy["port"]);
if (type_clash == "ss") {
auto bean = ent->ShadowSocks();
bean->method = Node2QString(proxy["cipher"]).replace("dummy", "none");
bean->password = Node2QString(proxy["password"]);
// UDP over TCP
if (Node2Bool(proxy["udp-over-tcp"])) {
bean->uot = Node2Int(proxy["udp-over-tcp-version"]);
if (bean->uot == 0) bean->uot = true;
}
if (proxy.contains("plugin") && proxy.contains("plugin-opts")) {
auto plugin_n = proxy["plugin"];
auto pluginOpts_n = proxy["plugin-opts"];
QStringList ssPlugin;
auto plugin = Node2QString(plugin_n);
if (plugin == "obfs") {
ssPlugin << "obfs-local";
ssPlugin << "obfs=" + Node2QString(pluginOpts_n["mode"]);
ssPlugin << "obfs-host=" + Node2QString(pluginOpts_n["host"]);
} else if (plugin == "v2ray-plugin") {
auto mode = Node2QString(pluginOpts_n["mode"]);
auto host = Node2QString(pluginOpts_n["host"]);
auto path = Node2QString(pluginOpts_n["path"]);
ssPlugin << "v2ray-plugin";
if (!mode.isEmpty() && mode != "websocket") ssPlugin << "mode=" + mode;
if (Node2Bool(pluginOpts_n["tls"])) ssPlugin << "tls";
if (!host.isEmpty()) ssPlugin << "host=" + host;
if (!path.isEmpty()) ssPlugin << "path=" + path;
// clash only: skip-cert-verify
// clash only: headers
// clash: mux=?
}
bean->plugin = ssPlugin.join(";");
}
// sing-mux
auto smux = NodeChild(proxy, {"smux"});
if (!smux.is_null() && Node2Bool(smux["enabled"])) bean->multiplex->enabled = true;
} else if (type == "http") {
auto bean = ent->Http();
bean->username = Node2QString(proxy["username"]);
bean->password = Node2QString(proxy["password"]);
if (type == "http" && Node2Bool(proxy["tls"])) {
bean->tls->enabled = true;
if (Node2Bool(proxy["skip-cert-verify"])) bean->tls->insecure = true;
bean->tls->server_name = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"]));
bean->tls->alpn = Node2QStringList(proxy["alpn"]);
bean->tls->utls->fingerPrint = Node2QString(proxy["client-fingerprint"]);
bean->tls->utls->enabled = true;
if (bean->tls->utls->fingerPrint.isEmpty()) {
bean->tls->utls->fingerPrint = Configs::dataStore->utlsFingerprint;
}
auto reality = NodeChild(proxy, {"reality-opts"});
if (reality.is_mapping()) {
bean->tls->reality->enabled = true;
bean->tls->reality->public_key = Node2QString(reality["public-key"]);
bean->tls->reality->short_id = Node2QString(reality["short-id"]);
}
}
} else if (type == "socks") {
auto bean = ent->Socks();
bean->username = Node2QString(proxy["username"]);
bean->password = Node2QString(proxy["password"]);
} else if (type == "trojan") {
needFix = true;
auto bean = ent->Trojan();
bean->password = Node2QString(proxy["password"]);
bean->tls->enabled = true;
bean->transport->type = Node2QString(proxy["network"], "tcp");
bean->tls->server_name = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"]));
bean->tls->alpn = Node2QStringList(proxy["alpn"]);
bean->tls->insecure = Node2Bool(proxy["skip-cert-verify"]);
bean->tls->utls->fingerPrint = Node2QString(proxy["client-fingerprint"]);
bean->tls->utls->enabled = true;
if (bean->tls->utls->fingerPrint.isEmpty()) {
bean->tls->utls->fingerPrint = Configs::dataStore->utlsFingerprint;
}
// sing-mux
auto smux = NodeChild(proxy, {"smux"});
if (!smux.is_null() && Node2Bool(smux["enabled"])) bean->multiplex->enabled = true;
// opts
auto ws = NodeChild(proxy, {"ws-opts", "ws-opt"});
if (ws.is_mapping()) {
auto headers = ws["headers"];
if (headers.is_mapping()) {
for (auto header: headers.as_map()) {
if (Node2QString(header.first).toLower() == "host") {
if (header.second.is_string())
bean->transport->host = Node2QString(header.second);
else if (header.second.is_sequence() && header.second[0].is_string())
bean->transport->host = Node2QString(header.second[0]);
break;
}
}
}
bean->transport->path = Node2QString(ws["path"]);
bean->transport->max_early_data = Node2Int(ws["max-early-data"]);
bean->transport->early_data_header_name = Node2QString(ws["early-data-header-name"]);
if (Node2Bool(ws["v2ray-http-upgrade"])) {
bean->transport->type = "httpupgrade";
}
}
auto grpc = NodeChild(proxy, {"grpc-opts", "grpc-opt"});
if (grpc.is_mapping()) {
bean->transport->path = Node2QString(grpc["grpc-service-name"]);
}
auto reality = NodeChild(proxy, {"reality-opts"});
if (reality.is_mapping()) {
bean->tls->reality->enabled = true;
bean->tls->reality->public_key = Node2QString(reality["public-key"]);
bean->tls->reality->short_id = Node2QString(reality["short-id"]);
}
} else if (type == "vless") {
needFix = true;
auto bean = ent->VLESS();
if (type == "vless") {
bean->flow = Node2QString(proxy["flow"]);
bean->uuid = Node2QString(proxy["uuid"]);
// meta packet encoding
if (Node2Bool(proxy["packet-addr"])) {
bean->packet_encoding = "packetaddr";
} else {
// For VLESS, default to use xudp
bean->packet_encoding = "xudp";
}
}
bean->tls->enabled = true;
bean->transport->type = Node2QString(proxy["network"], "tcp");
bean->tls->server_name = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"]));
bean->tls->alpn = Node2QStringList(proxy["alpn"]);
bean->tls->insecure = Node2Bool(proxy["skip-cert-verify"]);
bean->tls->utls->enabled = true;
bean->tls->utls->fingerPrint = Node2QString(proxy["client-fingerprint"]);
if (bean->tls->utls->fingerPrint.isEmpty()) {
bean->tls->utls->fingerPrint = Configs::dataStore->utlsFingerprint;
}
// sing-mux
auto smux = NodeChild(proxy, {"smux"});
if (!smux.is_null() && Node2Bool(smux["enabled"])) bean->multiplex->enabled = 1;
// opts
auto ws = NodeChild(proxy, {"ws-opts", "ws-opt"});
if (ws.is_mapping()) {
auto headers = ws["headers"];
if (headers.is_mapping()) {
for (auto header: headers.as_map()) {
if (Node2QString(header.first).toLower() == "host") {
if (header.second.is_string())
bean->transport->host = Node2QString(header.second);
else if (header.second.is_sequence() && header.second[0].is_string())
bean->transport->host = Node2QString(header.second[0]);
break;
}
}
}
bean->transport->path = Node2QString(ws["path"]);
bean->transport->max_early_data = Node2Int(ws["max-early-data"]);
bean->transport->early_data_header_name = Node2QString(ws["early-data-header-name"]);
if (Node2Bool(ws["v2ray-http-upgrade"])) {
bean->transport->type = "httpupgrade";
}
}
auto grpc = NodeChild(proxy, {"grpc-opts", "grpc-opt"});
if (grpc.is_mapping()) {
bean->transport->path = Node2QString(grpc["grpc-service-name"]);
}
auto reality = NodeChild(proxy, {"reality-opts"});
if (reality.is_mapping()) {
bean->tls->reality->enabled = true;
bean->tls->reality->public_key = Node2QString(reality["public-key"]);
bean->tls->reality->short_id = Node2QString(reality["short-id"]);
}
} else if (type == "vmess") {
needFix = true;
auto bean = ent->VMess();
bean->uuid = Node2QString(proxy["uuid"]);
bean->alter_id = Node2Int(proxy["alterId"]);
bean->security = Node2QString(proxy["cipher"], bean->security);
bean->transport->type = Node2QString(proxy["network"], "tcp").replace("h2", "http");
bean->tls->server_name = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"]));
bean->tls->alpn = Node2QStringList(proxy["alpn"]);
if (Node2Bool(proxy["tls"])) bean->tls->enabled = true;
if (Node2Bool(proxy["skip-cert-verify"])) bean->tls->insecure = true;
bean->tls->utls->fingerPrint = Node2QString(proxy["client-fingerprint"]);
bean->tls->utls->enabled = true;
if (bean->tls->utls->fingerPrint.isEmpty()) {
bean->tls->utls->fingerPrint = Configs::dataStore->utlsFingerprint;
}
// sing-mux
auto smux = NodeChild(proxy, {"smux"});
if (!smux.is_null() && Node2Bool(smux["enabled"])) bean->multiplex->enabled = true;
// meta packet encoding
if (Node2Bool(proxy["xudp"])) bean->packet_encoding = "xudp";
if (Node2Bool(proxy["packet-addr"])) bean->packet_encoding = "packetaddr";
// opts
auto ws = NodeChild(proxy, {"ws-opts", "ws-opt"});
if (ws.is_mapping()) {
auto headers = ws["headers"];
if (headers.is_mapping()) {
for (auto header: headers.as_map()) {
if (Node2QString(header.first).toLower() == "host") {
bean->transport->host = Node2QString(header.second);
break;
}
}
}
bean->transport->path = Node2QString(ws["path"]);
bean->transport->max_early_data = Node2Int(ws["max-early-data"]);
bean->transport->early_data_header_name = Node2QString(ws["early-data-header-name"]);
if (Node2Bool(ws["v2ray-http-upgrade"])) {
bean->transport->type = "httpupgrade";
}
// for Xray
if (Node2QString(ws["early-data-header-name"]) == "Sec-WebSocket-Protocol") {
bean->transport->path += "?ed=" + Node2QString(ws["max-early-data"]);
}
}
auto grpc = NodeChild(proxy, {"grpc-opts", "grpc-opt"});
if (grpc.is_mapping()) {
bean->transport->path = Node2QString(grpc["grpc-service-name"]);
}
auto h2 = NodeChild(proxy, {"h2-opts", "h2-opt"});
if (h2.is_mapping()) {
auto hosts = h2["host"];
for (auto host: hosts) {
bean->transport->host = Node2QString(host);
break;
}
bean->transport->path = Node2QString(h2["path"]);
}
auto tcp_http = NodeChild(proxy, {"http-opts", "http-opt"});
if (tcp_http.is_mapping()) {
bean->transport->type = "http";
auto headers = tcp_http["headers"];
if (headers.is_mapping()) {
for (auto header: headers.as_map()) {
if (Node2QString(header.first).toLower() == "host") {
bean->transport->host = Node2QString(header.second);
break;
}
}
}
auto paths = tcp_http["path"];
if (paths.is_string())
bean->transport->path = Node2QString(paths);
else if (paths.is_sequence() && paths[0].is_string())
bean->transport->path = Node2QString(paths[0]);
}
} else if (type == "anytls") {
needFix = true;
auto bean = ent->AnyTLS();
bean->password = Node2QString(proxy["password"]);
bean->tls->enabled = true;
if (Node2Bool(proxy["skip-cert-verify"])) bean->tls->insecure = true;
bean->tls->server_name = FIRST_OR_SECOND(Node2QString(proxy["sni"]), Node2QString(proxy["servername"]));
bean->tls->alpn = Node2QStringList(proxy["alpn"]);
bean->tls->utls->fingerPrint = Node2QString(proxy["client-fingerprint"]);
bean->tls->utls->enabled = true;
if (bean->tls->utls->fingerPrint.isEmpty()) {
bean->tls->utls->fingerPrint = Configs::dataStore->utlsFingerprint;
}
auto reality = NodeChild(proxy, {"reality-opts"});
if (reality.is_mapping()) {
bean->tls->reality->enabled = true;
bean->tls->reality->public_key = Node2QString(reality["public-key"]);
bean->tls->reality->short_id = Node2QString(reality["short-id"]);
}
} else if (type == "hysteria" || type == "hysteria2") {
auto bean = ent->Hysteria();
bean->tls->enabled = true;
bean->tls->insecure = Node2Bool(proxy["skip-cert-verify"]);
auto alpn = Node2QStringList(proxy["alpn"]);
bean->tls->certificate = Node2QString(proxy["ca-str"]).split("\n", Qt::SkipEmptyParts);
if (!alpn.isEmpty()) bean->tls->alpn = {alpn[0]};
bean->tls->server_name = Node2QString(proxy["sni"]);
if (type == "hysteria") {
auto auth_str = FIRST_OR_SECOND(Node2QString(proxy["auth_str"]), Node2QString(proxy["auth-str"]));
auto auth = Node2QString(proxy["auth"]);
if (!auth_str.isEmpty()) {
bean->auth_type = "STRING";
bean->auth = auth_str;
}
if (!auth.isEmpty()) {
bean->auth_type = "BASE64";
bean->auth = auth;
}
bean->obfs = Node2QString(proxy["obfs"]);
if (Node2Bool(proxy["disable_mtu_discovery"]) || Node2Bool(proxy["disable-mtu-discovery"])) bean->disable_mtu_discovery = true;
bean->recv_window = Node2Int(proxy["recv-window"]);
bean->recv_window_conn = Node2Int(proxy["recv-window-conn"]);
} else {
bean->obfs = Node2QString(proxy["obfs-password"]);
bean->password = Node2QString(proxy["password"]);
}
auto upMbps = Node2QString(proxy["up"]).split(" ")[0].toInt();
auto downMbps = Node2QString(proxy["down"]).split(" ")[0].toInt();
if (upMbps > 0) bean->up_mbps = upMbps;
if (downMbps > 0) bean->down_mbps = downMbps;
auto ports = Node2QString(proxy["ports"]);
if (!ports.isEmpty()) {
QStringList serverPorts;
ports.replace("/", ",");
for (const QString& port : ports.split(",", Qt::SkipEmptyParts)) {
if (port.isEmpty()) {
continue;
}
QString modifiedPort = port;
modifiedPort.replace("-", ":");
serverPorts.append(modifiedPort);
}
bean->server_ports = serverPorts;
}
} else if (type == "tuic") {
auto bean = ent->TUIC();
bean->uuid = Node2QString(proxy["uuid"]);
bean->password = Node2QString(proxy["password"]);
if (Node2Int(proxy["heartbeat-interval"]) != 0) {
bean->heartbeat = Int2String(Node2Int(proxy["heartbeat-interval"])) + "ms";
}
bean->udp_relay_mode = Node2QString(proxy["udp-relay-mode"], "native");
bean->congestion_control = Node2QString(proxy["congestion-controller"], "bbr");
bean->tls->enabled = true;
bean->tls->disable_sni = Node2Bool(proxy["disable-sni"]);
bean->zero_rtt_handshake = Node2Bool(proxy["reduce-rtt"]);
bean->tls->insecure = Node2Bool(proxy["skip-cert-verify"]);
bean->tls->alpn = Node2QStringList(proxy["alpn"]);
bean->tls->certificate = Node2QString(proxy["ca-str"]).split("\n", Qt::SkipEmptyParts);
bean->tls->server_name = Node2QString(proxy["sni"]);
if (Node2Bool(proxy["udp-over-stream"])) bean->udp_over_stream = true;
if (!Node2QString(proxy["ip"]).isEmpty()) {
if (bean->tls->server_name.isEmpty()) bean->tls->server_name = bean->server;
bean->server = Node2QString(proxy["ip"]);
}
} else {
continue;
}
// if (needFix) RawUpdater_FixEnt(ent); TODO
updated_order += ent;
}
} catch (const fkyaml::exception &ex) {
runOnUiThread([=] {
MessageBoxWarning("YAML Exception", ex.what());
});
}
}
// 在新的 thread 运行
void GroupUpdater::AsyncUpdate(const QString &str, int _sub_gid, const std::function<void()> &finish) {
auto content = str.trimmed();