🏅 Sync 2026-06-22 01:15:34

This commit is contained in:
github-actions[bot] committed 2026-06-22 01:15:34 +08:00
1 parent 6d4cdd8118
commit 4b85448aee
41 files changed
+3203 -1407

No files matched your search

@@ -1,5 +1,6 @@
#!/usr/bin/lua
require "nixio"
require "nixio.fs"
require "luci.model.uci"
@@ -582,6 +583,376 @@ local function string_or_nil(value)
return tostring(value)
end
local build_shadowsocks_plugin
local function first_nonempty(...)
for i = 1, select("#", ...) do
local value = select(i, ...)
if value ~= nil and value ~= "" then
return value
end
end
return nil
end
local function split_alpn(value)
local items = {}
for part in tostring(value or ""):gmatch("[^,;|%s]+") do
items[#items + 1] = part
end
return items
end
local function parse_wireguard_reserved(sid)
local raw = get_server_field(sid, "reserved", nil)
local values = {}
local bytes = {}
if raw == nil or raw == "" then
return nil
end
if type(raw) == "table" then
values = raw
else
values = { raw }
end
for _, item in ipairs(values) do
local text = tostring(item or "")
if text ~= "" then
if not text:match("[^%d,]+") then
for byte in text:gmatch("%d+") do
bytes[#bytes + 1] = tonumber(byte)
end
else
local decoded = nixio.bin.b64decode(text)
if decoded then
for i = 1, #decoded do
bytes[#bytes + 1] = decoded:byte(i)
end
end
end
end
end
return #bytes > 0 and bytes or nil
end
local function split_local_addresses(value)
local items = {}
if type(value) == "table" then
for _, item in ipairs(value) do
if item and item ~= "" then
items[#items + 1] = tostring(item)
end
end
elseif value and value ~= "" then
for item in tostring(value):gmatch("[^,%s]+") do
items[#items + 1] = item
end
end
return items
end
local function split_wireguard_addresses(value)
local ip, ipv6
for _, item in ipairs(split_local_addresses(value)) do
if item:find(":", 1, true) then
ipv6 = ipv6 or item
else
ip = ip or item
end
end
return ip, ipv6
end
local function apply_v2ray_tls_options(proxy, sid)
local tls = get_server_field(sid, "tls", "0")
local reality = get_server_field(sid, "reality", "0")
if tls ~= "1" and reality ~= "1" then
return
end
proxy.tls = true
proxy.servername = string_or_nil(get_server_field(sid, "tls_host", ""))
proxy["client-fingerprint"] = string_or_nil(get_server_field(sid, "fingerprint", ""))
proxy.fingerprint = string_or_nil(get_server_field(sid, "tls_CertSha", ""))
proxy["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0"))
local alpn = split_alpn(get_server_field(sid, "tls_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
if reality == "1" then
proxy["reality-opts"] = {
["public-key"] = string_or_nil(get_server_field(sid, "reality_publickey", "")),
["short-id"] = string_or_nil(get_server_field(sid, "reality_shortid", "")),
["support-x25519mlkem768"] = bool_enabled(get_server_field(sid, "enable_mldsa65verify", "0"))
}
end
if get_server_field(sid, "enable_ech", "0") == "1" then
proxy["ech-opts"] = {
enable = true,
config = string_or_nil(get_server_field(sid, "ech_config", ""))
}
end
end
local function apply_trojan_tls_options(proxy, sid)
proxy.sni = string_or_nil(get_server_field(sid, "tls_host", ""))
proxy["client-fingerprint"] = string_or_nil(get_server_field(sid, "fingerprint", ""))
proxy.fingerprint = string_or_nil(get_server_field(sid, "tls_CertSha", ""))
proxy["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0"))
local alpn = split_alpn(get_server_field(sid, "tls_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
if get_server_field(sid, "reality", "0") == "1" then
proxy["reality-opts"] = {
["public-key"] = string_or_nil(get_server_field(sid, "reality_publickey", "")),
["short-id"] = string_or_nil(get_server_field(sid, "reality_shortid", ""))
}
end
end
local function apply_v2ray_transport_options(proxy, sid)
local transport = get_server_field(sid, "transport", "raw")
if transport == "raw" or transport == "tcp" or transport == "" then
return
end
if transport == "ws" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_or_nil(get_server_field(sid, "ws_path", "")),
headers = first_nonempty(get_server_field(sid, "ws_host", ""), get_server_field(sid, "tls_host", "")) and {
Host = first_nonempty(get_server_field(sid, "ws_host", ""), get_server_field(sid, "tls_host", ""))
} or nil
}
elseif transport == "httpupgrade" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_or_nil(get_server_field(sid, "httpupgrade_path", "")),
headers = first_nonempty(get_server_field(sid, "httpupgrade_host", ""), get_server_field(sid, "tls_host", "")) and {
Host = first_nonempty(get_server_field(sid, "httpupgrade_host", ""), get_server_field(sid, "tls_host", ""))
} or nil,
["v2ray-http-upgrade"] = true
}
elseif transport == "h2" then
local host = first_nonempty(get_server_field(sid, "h2_host", ""), get_server_field(sid, "tls_host", ""))
proxy.network = "h2"
proxy["h2-opts"] = {
host = host and split_alpn(host) or nil,
path = string_or_nil(get_server_field(sid, "h2_path", ""))
}
elseif transport == "grpc" then
proxy.network = "grpc"
proxy["grpc-opts"] = {
["grpc-service-name"] = string_or_nil(get_server_field(sid, "serviceName", ""))
}
elseif transport == "xhttp" and proxy.type == "vless" then
proxy.network = "xhttp"
proxy["xhttp-opts"] = {
path = string_or_nil(get_server_field(sid, "xhttp_path", "")),
host = string_or_nil(get_server_field(sid, "xhttp_host", "")),
mode = string_or_nil(get_server_field(sid, "xhttp_mode", ""))
}
end
end
local function can_mihomo_handle_v2ray_transport(protocol, sid)
local transport = get_server_field(sid, "transport", "raw")
if transport == "" or transport == "raw" or transport == "tcp" then
if get_server_field(sid, "tcp_guise", "none") == "http" then
return false
end
return true
end
if protocol == "socks" or protocol == "http" then
return false
end
if transport == "ws" or transport == "httpupgrade" or transport == "h2" or transport == "grpc" then
return true
end
return protocol == "vless" and transport == "xhttp"
end
local function build_v2ray_mihomo_proxy(sid)
local node_type = get_server_field(sid, "type", "")
local protocol = get_server_field(sid, "v2ray_protocol", "vmess")
local proxy = {
name = sid,
server = get_server_field(sid, "server", ""),
port = tonumber(get_server_field(sid, "server_port", "0")) or 0,
udp = true,
tfo = bool_enabled(get_server_field(sid, "fast_open", "0"))
}
if node_type == "socks5" then
proxy.type = "socks5"
if get_server_field(sid, "auth_enable", "0") == "1" then
proxy.username = string_or_nil(get_server_field(sid, "username", ""))
proxy.password = string_or_nil(get_server_field(sid, "password", ""))
end
elseif protocol == "vmess" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "vmess"
proxy.uuid = first_nonempty(get_server_field(sid, "vmess_id", ""), get_server_field(sid, "vmess_uuid", "")) or ""
proxy.alterId = tonumber(get_server_field(sid, "alter_id", "0")) or 0
proxy.cipher = first_nonempty(get_server_field(sid, "security", ""), get_server_field(sid, "vmess_method", ""), "auto")
apply_v2ray_tls_options(proxy, sid)
apply_v2ray_transport_options(proxy, sid)
elseif protocol == "vless" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "vless"
proxy.uuid = get_server_field(sid, "vmess_id", "")
proxy.flow = string_or_nil(get_server_field(sid, "tls_flow", ""))
proxy.encryption = get_server_field(sid, "vless_encryption", "")
apply_v2ray_tls_options(proxy, sid)
apply_v2ray_transport_options(proxy, sid)
elseif protocol == "trojan" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "trojan"
proxy.password = get_server_field(sid, "password", "")
apply_trojan_tls_options(proxy, sid)
apply_v2ray_transport_options(proxy, sid)
elseif protocol == "shadowsocks" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "ss"
proxy.cipher = get_server_field(sid, "encrypt_method_ss", "none")
proxy.password = get_server_field(sid, "password", "")
build_shadowsocks_plugin(proxy, sid)
elseif protocol == "hysteria2" then
proxy.type = "hysteria2"
proxy.password = get_server_field(sid, "hy2_auth", "")
proxy.ports = string_or_nil(get_server_field(sid, "port_range", ""))
proxy.up = string_or_nil(get_server_field(sid, "uplink_capacity", "")) and (get_server_field(sid, "uplink_capacity", "") .. " Mbps") or nil
proxy.down = string_or_nil(get_server_field(sid, "downlink_capacity", "")) and (get_server_field(sid, "downlink_capacity", "") .. " Mbps") or nil
proxy.sni = string_or_nil(get_server_field(sid, "tls_host", ""))
proxy.fingerprint = string_or_nil(get_server_field(sid, "tls_CertSha", ""))
proxy["skip-cert-verify"] = bool_enabled(get_server_field(sid, "insecure", "0"))
local alpn = split_alpn(get_server_field(sid, "tls_alpn", ""))
if #alpn > 0 then
proxy.alpn = alpn
end
if get_server_field(sid, "flag_obfs", "0") == "1" then
proxy.obfs = string_or_nil(get_server_field(sid, "obfs_type", ""))
proxy["obfs-password"] = string_or_nil(get_server_field(sid, "salamander", ""))
end
elseif protocol == "socks" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "socks5"
if get_server_field(sid, "socks_ver", "5") ~= "5" then
return nil
end
if get_server_field(sid, "auth_enable", "0") == "1" then
proxy.username = string_or_nil(get_server_field(sid, "username", ""))
proxy.password = string_or_nil(get_server_field(sid, "password", ""))
end
apply_v2ray_tls_options(proxy, sid)
elseif protocol == "http" then
if not can_mihomo_handle_v2ray_transport(protocol, sid) then
return nil
end
proxy.type = "http"
if get_server_field(sid, "auth_enable", "0") == "1" then
proxy.username = string_or_nil(get_server_field(sid, "username", ""))
proxy.password = string_or_nil(get_server_field(sid, "password", ""))
end
apply_v2ray_tls_options(proxy, sid)
elseif protocol == "wireguard" then
local ip, ipv6 = split_wireguard_addresses(get_server_field(sid, "local_addresses", ""))
proxy.type = "wireguard"
proxy["private-key"] = get_server_field(sid, "private_key", "")
proxy["public-key"] = get_server_field(sid, "peer_pubkey", "")
proxy["pre-shared-key"] = string_or_nil(get_server_field(sid, "preshared_key", ""))
proxy.ip = ip
proxy.ipv6 = ipv6
proxy["allowed-ips"] = split_local_addresses(get_server_field(sid, "allowedips", "0.0.0.0/0"))
proxy.reserved = parse_wireguard_reserved(sid)
proxy["persistent-keepalive"] = number_or_nil(get_server_field(sid, "keepaliveperiod", ""))
proxy.mtu = number_or_nil(get_server_field(sid, "mtu", ""))
elseif protocol == "snell" then
proxy.type = "snell"
proxy.psk = get_server_field(sid, "snell_psk", "")
proxy.version = number_or_nil(get_server_field(sid, "snell_version", ""))
local obfs_mode = string_or_nil(get_server_field(sid, "snell_obfs", ""))
local obfs_host = string_or_nil(get_server_field(sid, "snell_obfs_host", ""))
if obfs_mode or obfs_host then
proxy["obfs-opts"] = {
mode = obfs_mode,
host = obfs_host
}
end
else
return nil
end
if not proxy.server or proxy.server == "" or not proxy.port or proxy.port == 0 then
return nil
end
if proxy.type == "snell" and (not proxy.psk or proxy.psk == "") then
return nil
end
return proxy
end
local function build_single_proxy_runtime_doc(proxy, local_port, socks_port, mode)
local dns_mode = uci:get_first("shadowsocksr", "global", "pdnsd_enable", "0")
local listen_port = tonumber(local_port)
local socks_listen = tonumber(socks_port)
local doc = {
["allow-lan"] = true,
["bind-address"] = "0.0.0.0",
mode = "rule",
["log-level"] = "silent",
["find-process-mode"] = "off",
["unified-delay"] = true,
["tcp-concurrent"] = true,
["routing-mark"] = 255,
proxies = { proxy },
["proxy-groups"] = {
{
name = "PROXY",
type = "select",
proxies = { proxy.name }
}
},
rules = { "MATCH,PROXY" },
tun = { enable = false },
profile = { ["store-selected"] = true },
dns = {
enable = dns_mode == "7",
["enhanced-mode"] = "redir-host",
listen = "127.0.0.1:5335",
ipv6 = get_filter_aaaa() ~= "1"
}
}
if mode == "socks" then
doc["socks-port"] = listen_port
else
doc["redir-port"] = listen_port
doc["tproxy-port"] = listen_port
if socks_listen and socks_listen > 0 then
doc["socks-port"] = socks_listen
end
end
return doc
end
local function pick_plugin_opt(plugin_opts, ...)
for i = 1, select("#", ...) do
local key = select(i, ...)
@@ -661,7 +1032,7 @@ local function normalize_plugin_name(plugin)
return value
end
local function build_shadowsocks_plugin(proxy, sid)
function build_shadowsocks_plugin(proxy, sid)
local plugin = normalize_plugin_name(get_server_field(sid, "plugin", ""))
local plugin_opts = parse_plugin_opts(get_server_field(sid, "plugin_opts", ""))
@@ -871,6 +1242,22 @@ local function generate_shadowsocks_runtime(sid, output_path, local_port, socks_
return true
end
local function generate_v2ray_runtime(sid, output_path, local_port, socks_port, mode)
local proxy = build_v2ray_mihomo_proxy(sid)
if not proxy then
io.stderr:write("unsupported_or_invalid_v2ray_node\n")
return false
end
local doc = build_single_proxy_runtime_doc(proxy, local_port, socks_port, mode)
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local function build_shadowsocks_server_doc(sid)
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local method = get_server_field(sid, "encrypt_method_ss", "aes-128-gcm")
@@ -916,6 +1303,86 @@ local function generate_shadowsocks_server(sid, output_path)
return true
end
local function build_mihomo_listener_doc(sid)
local ltype = get_server_field(sid, "type", "")
local server_port = tonumber(get_server_field(sid, "server_port", "0")) or 0
local listener = {
name = sid,
type = ltype,
listen = "::",
port = server_port
}
if ltype == "vmess" then
listener.users = {
{
username = "1",
uuid = get_server_field(sid, "uuid", ""),
alterId = tonumber(get_server_field(sid, "alter_id", "0")) or 0
}
}
elseif ltype == "vless" then
listener.users = {
{
username = "1",
uuid = get_server_field(sid, "uuid", ""),
flow = string_or_nil(get_server_field(sid, "flow", ""))
}
}
elseif ltype == "trojan" then
listener.users = {
{
username = "1",
password = get_server_field(sid, "trojan_password", "")
}
}
elseif ltype == "shadowsocks" then
listener.type = "shadowsocks"
listener.cipher = get_server_field(sid, "security", "chacha20-ietf-poly1305")
listener.password = get_server_field(sid, "ss_password", "")
listener.udp = true
else
return nil
end
local network = get_server_field(sid, "network", "tcp")
if network == "ws" then
listener["ws-path"] = string_or_nil(get_server_field(sid, "ws_path", "/"))
elseif network == "grpc" then
listener["grpc-service-name"] = string_or_nil(get_server_field(sid, "grpc_service", ""))
end
local cert = get_server_field(sid, "certpath", "")
local key = get_server_field(sid, "keypath", "")
if cert ~= "" and key ~= "" then
listener.certificate = cert
listener["private-key"] = key
end
return {
["allow-lan"] = true,
["bind-address"] = "*",
["log-level"] = "silent",
["find-process-mode"] = "off",
listeners = { listener }
}
end
local function generate_mihomo_listener(sid, output_path)
local doc = build_mihomo_listener_doc(sid)
if not doc then
io.stderr:write("unsupported_listener\n")
return false
end
local ok, rendered = pcall(lyaml.dump, { doc })
if not ok or not rendered then
io.stderr:write("dump_failed\n")
return false
end
write_file(output_path, rendered)
return true
end
local action = arg[1]
if action == "validate" then
os.exit(validate(arg[2]) and 0 or 1)
@@ -931,9 +1398,13 @@ elseif action == "tuic" then
os.exit(generate_tuic_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss" then
os.exit(generate_shadowsocks_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "v2ray" then
os.exit(generate_v2ray_runtime(arg[2], arg[3], arg[4], arg[5], arg[6]) and 0 or 1)
elseif action == "ss_server" then
os.exit(generate_shadowsocks_server(arg[2], arg[3]) and 0 or 1)
elseif action == "v2ray_server" then
os.exit(generate_mihomo_listener(arg[2], arg[3]) and 0 or 1)
else
io.stderr:write("usage: clash_yaml.lua validate <yaml> | filter <yaml> <words> | prepare <input> <output> | merge <raw> <overlay> <output> | append_client_policy_rules <runtime_yaml> <sid> | tuic <sid> <output> <local_port> [socks_port] [mode] | ss <sid> <output> <local_port> [socks_port] [mode] | ss_server <sid> <output>\n")
io.stderr:write("usage: clash_yaml.lua validate <yaml> | filter <yaml> <words> | prepare <input> <output> | merge <raw> <overlay> <output> | append_client_policy_rules <runtime_yaml> <sid> | tuic <sid> <output> <local_port> [socks_port] [mode] | ss <sid> <output> <local_port> [socks_port] [mode] | v2ray <sid> <output> <local_port> [socks_port] [mode] | ss_server <sid> <output> | v2ray_server <sid> <output>\n")
os.exit(1)
end
@@ -50,32 +50,28 @@ local b64encode = nixio.bin.b64encode
local effective_node_local_port = tonumber(server.local_port) or tonumber(default_node_local_port) or 1234
if server.type == "ss-rust" then
server.type = "ss"
server.type = "ss"
end
local function parse_realm_uri(uri)
if type(uri) ~= "string" then return nil end
-- realm://token@server/realm_id?query
local token, server_url, realm_id, query = trim(uri):match("^realm://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not token or not server_url or not realm_id then return nil end
-- realm[+http]://token@server/realm_id?query
local scheme, token, server_url, realm_id, query = trim(uri):match("^(realm%+http|realm)://([^@]+)@([^/]+)/([^?]*)%??(.*)$")
if not scheme or not token or not server_url or not realm_id then return nil end
realm_id = realm_id:gsub("/+$", "")
local realm = {
scheme = scheme,
token = token,
server_url = server_url,
realm_id = realm_id
}
-- 解析 query 中的 stun=
if query and query ~= "" then
local stun_servers = {}
for key, value in query:gmatch("([^&=?]+)=([^&]+)") do
if key == "stun" and value ~= "" then
stun_servers[#stun_servers + 1] = value
end
end
if #stun_servers > 0 then
realm.stun_servers = stun_servers
end
local stun_servers
for value in (query or ""):gmatch("[?&]?[Ss][Tt][Uu][Nn]=([^&]+)") do
if not stun_servers then stun_servers = {} end
stun_servers[#stun_servers + 1] = value
end
realm.stun_servers = stun_servers
return realm
end
@@ -428,22 +424,24 @@ end
-- 开启 socks 代理
-- 检查是否启用 socks 代理
if proto and proto:find("tcp") and socks_port ~= "0" then
table.insert(Xray.inbounds, {
-- socks
protocol = "socks",
port = tonumber(socks_port),
local auth = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth")
and {{
user = socks_server.socks5_user,
pass = socks_server.socks5_pass
}} or nil
table.insert(Xray.inbounds, {
-- socks
protocol = "socks",
port = tonumber(socks_port),
settings = {
auth = socks_server.socks5_auth or "noauth",
udp = true,
mixed = ((socks_server.socks5_mixed == '1') and true or false) or nil,
accounts = (socks_server.socks5_auth and socks_server.socks5_auth ~= "noauth") and {
{
user = socks_server.socks5_user,
pass = socks_server.socks5_pass
}
} or nil
} or nil
})
mixed = socks_server.socks5_mixed == "1" or nil,
accounts = (xray_version_val <= 260503) and auth or nil,
users = (xray_version_val > 260503) and auth or nil
}
})
end
-- 传出连接
@@ -645,9 +643,7 @@ Xray.outbounds = {
local realm = parse_realm_uri(server.hysteria2_realm_url)
local url, stun
if realm then
if realm.token and realm.server_url and realm.realm_id then
url = "realm://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
end
url = realm.scheme .. "://" .. realm.token .. "@" .. realm.server_url .. "/" .. realm.realm_id
stun = realm.stun_servers or server.hysteria2_realm_stun
end
local r = {
@@ -29,7 +29,7 @@ config server_subscribe
option auto_update_min_time '0'
option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99'
option filter_words '过期/套餐/剩余/网址/QQ群/官网/防失联/回国'
option filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国'
config access_control
option lan_ac_mode '0'
@@ -26,9 +26,12 @@ local ucic = require "luci.model.uci".cursor()
local proxy = ucic:get_first(name, 'server_subscribe', 'proxy', '0')
local switch = ucic:get_first(name, 'server_subscribe', 'switch', '1')
local allow_insecure = ucic:get_first(name, 'server_subscribe', 'allow_insecure', '0')
local filter_words = ucic:get_first(name, 'server_subscribe', 'filter_words', '过期/套餐/剩余/网址/QQ群/官网/防失联/回国')
local filter_words = ucic:get_first(name, 'server_subscribe', 'filter_words', '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国')
local save_words = ucic:get_first(name, 'server_subscribe', 'save_words', '')
local user_agent = ucic:get_first(name, 'server_subscribe', 'user_agent', 'v2rayN/9.99')
local sub_convert = ucic:get_first(name, 'server_subscribe', 'sub_convert', '0')
local convert_address = ucic:get_first(name, 'server_subscribe', 'convert_address', 'https://api.asailor.org/sub')
local template_url = ucic:get_first(name, 'server_subscribe', 'template_url', '')
local local_clash_dir = "/etc/ssrplus/clash"
local target_subscribe_sid = tostring(arg and arg[1] or ""):gsub("^%s*(.-)%s*$", "%1")
@@ -125,6 +128,11 @@ local function urlEncode(szText)
return str
end
local function urlEncodeRFC3986(szText)
local str = tostring(szText or ""):gsub("([^0-9a-zA-Z%-_%.~])", get_urlencode)
return str
end
local function get_urldecode(h)
return schar(tonumber(h, 16))
end
@@ -397,6 +405,7 @@ local function parseAnytlsShare(content)
if not userinfo or not hostinfo then
return nil
end
hostinfo = hostinfo:gsub("/+$", "")
local password = UrlDecode(userinfo)
local server, port = hostinfo:match("^(.+):(%d+)$")
@@ -468,6 +477,389 @@ local function buildAnytlsClashYaml(entries, group_name)
return table.concat(lines, "\n") .. "\n"
end
local function anytls_to_mihomo_node(entry)
if not entry then
return nil
end
return {
type = "anytls",
alias = entry.name,
raw_alias = entry.name,
server = entry.server,
server_port = entry.port,
password = entry.password,
tls_host = entry.sni,
insecure = entry.allow_insecure and "1" or "0",
fingerprint = entry.client_fingerprint
}
end
local function split_csv_values(value)
local items = {}
for v in tostring(value or ""):gmatch("[^,;|%s]+") do
items[#items + 1] = v
end
return items
end
local function bool_from_flag(value)
return is_true_value(value) and true or nil
end
local function number_from_value(value)
if value == nil or value == "" then
return nil
end
return tonumber(value)
end
local function string_from_value(value)
if value == nil or value == "" then
return nil
end
return tostring(value)
end
local function split_plugin_opts(value)
local opts = {}
for part in tostring(value or ""):gmatch("[^;]+") do
local key, val = part:match("^%s*([^=]+)=?(.*)%s*$")
if key and key ~= "" then
opts[key] = val or ""
end
end
return opts
end
local function pick_plugin_opt(opts, ...)
for i = 1, select("#", ...) do
local key = select(i, ...)
if opts[key] ~= nil and opts[key] ~= "" then
return opts[key]
end
end
return nil
end
local function normalize_mihomo_plugin_name(plugin)
local value = tostring(plugin or ""):lower()
if value == "" or value == "none" then
return ""
end
if value == "simple-obfs" or value == "obfs" or value == "obfs-local" then
return "obfs"
end
if value == "xray-plugin" then
return "v2ray-plugin"
end
if value == "shadowtls" then
return "shadow-tls"
end
return value
end
local function apply_mihomo_ss_plugin(proxy, node)
local plugin = normalize_mihomo_plugin_name(node.plugin)
if plugin == "" then
return
end
local opts = split_plugin_opts(node.plugin_opts)
proxy.plugin = plugin
if plugin == "obfs" then
proxy["plugin-opts"] = {
mode = pick_plugin_opt(opts, "obfs", "mode") or "http",
host = string_from_value(pick_plugin_opt(opts, "obfs-host", "obfs_host", "host"))
}
elseif plugin == "shadow-tls" then
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["plugin-opts"] = {
host = string_from_value(pick_plugin_opt(opts, "host")),
password = string_from_value(pick_plugin_opt(opts, "passwd", "password")),
version = number_from_value(pick_plugin_opt(opts, "version")) or (opts.v3 == "1" and 3) or (opts.v2 == "1" and 2) or (opts.v1 == "1" and 1) or nil
}
elseif plugin == "v2ray-plugin" then
proxy["plugin-opts"] = {
mode = pick_plugin_opt(opts, "mode") or "websocket",
tls = bool_from_flag(pick_plugin_opt(opts, "tls")),
host = string_from_value(pick_plugin_opt(opts, "host")),
path = string_from_value(pick_plugin_opt(opts, "path")),
mux = bool_from_flag(pick_plugin_opt(opts, "mux")),
["skip-cert-verify"] = bool_from_flag(pick_plugin_opt(opts, "skip-cert-verify", "skip_cert_verify", "insecure")),
["v2ray-http-upgrade"] = bool_from_flag(pick_plugin_opt(opts, "v2ray-http-upgrade", "v2ray_http_upgrade"))
}
else
proxy["plugin-opts"] = next(opts) and opts or nil
end
end
local function apply_mihomo_tls_options(proxy, node)
local tls_enabled = node.tls == "1" or node.reality == "1"
if not tls_enabled then
return
end
proxy.tls = true
proxy.servername = string_from_value(node.tls_host)
proxy.fingerprint = string_from_value(node.tls_CertSha)
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
local alpn = split_csv_values(node.tls_alpn)
if #alpn > 0 then
proxy.alpn = alpn
end
if node.reality == "1" then
proxy["reality-opts"] = {
["public-key"] = string_from_value(node.reality_publickey),
["short-id"] = string_from_value(node.reality_shortid),
["support-x25519mlkem768"] = bool_from_flag(node.enable_mldsa65verify)
}
end
if node.enable_ech == "1" and node.ech_config and node.ech_config ~= "" then
proxy["ech-opts"] = {
enable = true,
config = node.ech_config
}
end
end
local function apply_mihomo_trojan_tls_options(proxy, node)
proxy.sni = string_from_value(node.tls_host)
proxy.fingerprint = string_from_value(node.tls_CertSha)
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
local alpn = split_csv_values(node.tls_alpn)
if #alpn > 0 then
proxy.alpn = alpn
end
if node.reality == "1" then
proxy["reality-opts"] = {
["public-key"] = string_from_value(node.reality_publickey),
["short-id"] = string_from_value(node.reality_shortid)
}
end
end
local function apply_mihomo_transport_options(proxy, node)
local transport = node.transport or "raw"
if transport == "raw" or transport == "tcp" or transport == "" then
return
end
if transport == "httpupgrade" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_from_value(node.httpupgrade_path),
headers = node.httpupgrade_host and node.httpupgrade_host ~= "" and { Host = node.httpupgrade_host } or nil,
["v2ray-http-upgrade"] = true
}
elseif transport == "ws" then
proxy.network = "ws"
proxy["ws-opts"] = {
path = string_from_value(node.ws_path),
headers = node.ws_host and node.ws_host ~= "" and { Host = node.ws_host } or nil
}
elseif transport == "h2" then
proxy.network = "h2"
proxy["h2-opts"] = {
host = node.h2_host and node.h2_host ~= "" and split_csv_values(node.h2_host) or nil,
path = string_from_value(node.h2_path)
}
elseif transport == "grpc" then
proxy.network = "grpc"
proxy["grpc-opts"] = {
["grpc-service-name"] = string_from_value(node.serviceName)
}
elseif transport == "xhttp" and proxy.type == "vless" then
proxy.network = "xhttp"
proxy["xhttp-opts"] = {
path = string_from_value(node.xhttp_path),
host = string_from_value(node.xhttp_host),
mode = string_from_value(node.xhttp_mode)
}
end
end
local function to_mihomo_proxy(node)
local proxy = {
name = node.alias,
server = node.server,
port = tonumber(node.server_port) or node.server_port,
udp = true
}
if node.type == "v2ray" and node.v2ray_protocol == "vmess" then
proxy.type = "vmess"
proxy.uuid = node.vmess_id
proxy.alterId = tonumber(node.alter_id) or 0
proxy.cipher = node.security or "auto"
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_tls_options(proxy, node)
apply_mihomo_transport_options(proxy, node)
elseif node.type == "v2ray" and node.v2ray_protocol == "vless" then
proxy.type = "vless"
proxy.uuid = node.vmess_id
proxy.flow = string_from_value(node.tls_flow)
proxy.encryption = node.vless_encryption or ""
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_tls_options(proxy, node)
apply_mihomo_transport_options(proxy, node)
elseif node.type == "ss" or node.type == "ss-rust" or (node.type == "v2ray" and node.v2ray_protocol == "shadowsocks") then
proxy.type = "ss"
proxy.cipher = node.encrypt_method_ss or node.encrypt_method
proxy.password = node.password
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_ss_plugin(proxy, node)
elseif node.type == "ssr" then
proxy.type = "ssr"
proxy.cipher = node.encrypt_method
proxy.password = node.password
proxy.obfs = node.obfs
proxy.protocol = node.protocol
proxy["obfs-param"] = string_from_value(node.obfs_param)
proxy["protocol-param"] = string_from_value(node.protocol_param)
proxy.tfo = bool_from_flag(node.fast_open)
elseif node.type == "v2ray" and node.v2ray_protocol == "trojan" then
proxy.type = "trojan"
proxy.password = node.password
proxy.tfo = bool_from_flag(node.fast_open)
apply_mihomo_trojan_tls_options(proxy, node)
apply_mihomo_transport_options(proxy, node)
elseif node.type == "tuic" then
local alpn = split_csv_values(node.tls_alpn)
local heartbeat = number_from_value(node.heartbeat)
local timeout = number_from_value(node.timeout)
proxy.type = "tuic"
proxy.uuid = node.tuic_uuid
proxy.password = node.tuic_passwd
proxy.ip = string_from_value(node.tuic_ip)
proxy.sni = string_from_value(node.tls_host)
proxy.alpn = (#alpn > 0) and alpn or nil
proxy["udp-relay-mode"] = string_from_value(node.udp_relay_mode)
proxy["congestion-controller"] = string_from_value(node.congestion_control)
proxy["heartbeat-interval"] = heartbeat and (heartbeat * 1000) or nil
proxy["request-timeout"] = timeout and (timeout * 1000) or nil
proxy["max-udp-relay-packet-size"] = number_from_value(node.tuic_max_package_size)
proxy["disable-sni"] = bool_from_flag(node.disable_sni)
proxy["reduce-rtt"] = bool_from_flag(node.zero_rtt_handshake)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
elseif node.type == "v2ray" and (node.v2ray_protocol == "hysteria2" or node.v2ray_protocol == "hy2") then
local alpn = split_csv_values(node.tls_alpn)
proxy.type = "hysteria2"
proxy.password = node.hy2_auth
proxy.ports = string_from_value(node.port_range)
proxy.up = node.uplink_capacity and (tostring(node.uplink_capacity) .. " Mbps") or nil
proxy.down = node.downlink_capacity and (tostring(node.downlink_capacity) .. " Mbps") or nil
proxy.sni = string_from_value(node.tls_host)
proxy.fingerprint = string_from_value(node.tls_CertSha)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
proxy.alpn = (#alpn > 0) and alpn or nil
if node.flag_obfs == "1" then
proxy.obfs = string_from_value(node.obfs_type)
proxy["obfs-password"] = string_from_value(node.salamander)
end
elseif node.type == "anytls" then
proxy.type = "anytls"
proxy.password = node.password
proxy.sni = string_from_value(node.tls_host)
proxy["client-fingerprint"] = string_from_value(node.fingerprint)
proxy["skip-cert-verify"] = bool_from_flag(node.insecure)
elseif node.type == "v2ray" and node.v2ray_protocol == "snell" then
proxy.type = "snell"
proxy.psk = node.snell_psk
proxy.version = tonumber(node.snell_version) or nil
local obfs_mode = string_from_value(node.snell_obfs)
local obfs_host = string_from_value(node.snell_obfs_host)
if obfs_mode or obfs_host then
proxy["obfs-opts"] = {
mode = obfs_mode,
host = obfs_host
}
end
else
return nil
end
if not proxy.name or proxy.name == "" or not proxy.server or not proxy.port then
return nil
end
return proxy
end
local function can_group_into_mihomo(node)
if not node then
return false
end
if node.type == "ssr" or node.type == "ss" or node.type == "ss-rust" then
return true
end
if node.type == "v2ray" and (
node.v2ray_protocol == "vmess"
or node.v2ray_protocol == "vless"
or node.v2ray_protocol == "shadowsocks"
or node.v2ray_protocol == "trojan"
or node.v2ray_protocol == "hysteria2"
or node.v2ray_protocol == "hy2"
or node.v2ray_protocol == "snell"
) then
return true
end
if node.type == "tuic" then
return true
end
if node.type == "anytls" then
return true
end
return false
end
local function buildMihomoSubscribeYaml(nodes, group_name)
local proxies = {}
local names = {}
for _, node in ipairs(nodes) do
local proxy = to_mihomo_proxy(node)
if proxy then
proxies[#proxies + 1] = proxy
names[#names + 1] = proxy.name
end
end
if #proxies == 0 then
return nil, 0
end
local doc = {
mode = "rule",
["log-level"] = "silent",
proxies = proxies,
["proxy-groups"] = {
{
name = group_name,
type = "select",
proxies = names
}
},
rules = { "MATCH," .. group_name }
}
local ok_lyaml, lyaml = pcall(require, "lyaml")
if ok_lyaml then
local ok_dump, dumped = pcall(lyaml.dump, { doc })
if ok_dump and dumped then
return dumped, #proxies
end
end
return nil, 0
end
local function processLocalClashSubscription(path, alias)
local result = {
type = "clash",
@@ -499,7 +891,7 @@ local function processData(szType, content, cfgid)
local url = URL.parse("http://" .. content)
local params = url.query
if not has_xray then
if not has_xray and not has_mihomo then
return nil
end
@@ -562,6 +954,30 @@ local function processData(szType, content, cfgid)
-- 处理 fast open 参数
result.fast_open = params.tfo
end
elseif szType == "snell" then
if not has_mihomo then
log("跳过 Snell 节点:本地未安装 mihomo。")
return nil
end
local url = URL.parse("http://" .. content)
local params = url.query or {}
local raw_alias = url.fragment and UrlDecode(url.fragment) or nil
local psk = url.user and UrlDecode(url.user) or first_nonempty(params, {"psk", "password"})
result.type = "v2ray"
result.v2ray_protocol = "snell"
result.raw_alias = raw_alias
result.alias = raw_alias
result.server = normalize_host(url.host)
result.server_port = url.port
result.snell_psk = psk
result.snell_version = first_nonempty(params, {"version", "v"}) or "4"
result.snell_obfs = first_nonempty(params, {"obfs", "obfs-mode", "obfs_mode"})
result.snell_obfs_host = first_nonempty(params, {"obfs-host", "obfs_host", "host"})
if not result.snell_psk or result.snell_psk == "" then
result.server = nil
end
elseif szType == 'ssr' then
-- 去掉前后空白和#注释
local link = trim(content:gsub("#.*$", ""))
@@ -1168,7 +1584,7 @@ local function processData(szType, content, cfgid)
end
-- 自动决定模式(true=Xray, false=普通 Trojan)
if not has_xray then
if not has_xray and not has_mihomo then
return nil
end
@@ -1575,6 +1991,56 @@ local function curl(url, user_agent)
return stdout, md5
end
local function build_convert_url(url)
if sub_convert ~= "1" then
return url, false
end
local tpl = trim(template_url or "")
local endpoint = trim(convert_address or "")
local exclude = trim(filter_words or "")
if tpl == "" or endpoint == "" then
return url, false
end
endpoint = endpoint:gsub("%s+$", ""):gsub("^%s+", ""):gsub("%z", ""):gsub("[\r\n]", "")
local sep = endpoint:find("?", 1, true) and "&" or "?"
local query = "target=clash"
.. "&new_name=true"
.. "&url=" .. urlEncodeRFC3986(url)
.. "&config=" .. urlEncodeRFC3986(tpl)
.. "&exclude=" .. urlEncodeRFC3986(exclude)
.. "&emoji=false"
.. "&list=false"
.. "&sort=false"
.. "&udp=true"
.. "&scv=" .. (allow_insecure == "1" and "true" or "false")
.. "&fdn=true"
return endpoint .. sep .. query, true
end
local function filterClashYamlRaw(raw)
if not raw or raw == "" then
return raw, 0
end
local tmp = string.format("/tmp/ssrplus-subscribe-filter-%d-%d.yaml", nixio.getpid(), os.time())
local ok = nixio.fs.writefile(tmp, raw)
if not ok then
return raw, 0
end
local removed = tonumber(trim(luci.sys.exec(string.format(
"/usr/bin/lua /usr/share/shadowsocksr/clash_yaml.lua filter %s %s 2>/dev/null",
shell_quote(tmp),
shell_quote(filter_words)
)))) or 0
local filtered = nixio.fs.readfile(tmp) or raw
nixio.fs.remove(tmp)
return filtered, removed
end
local function collect_wan_interfaces()
local ifaces = {}
local seen = {}
@@ -1845,6 +2311,7 @@ end
-- 加载订阅未变化的节点用于防止被误删
local function loadOldNodes(groupHash)
local nodes = {}
local count = 0
cache[groupHash] = {}
nodeResult[#nodeResult + 1] = nodes
local index = #nodeResult
@@ -1854,8 +2321,22 @@ local function loadOldNodes(groupHash)
local section = setmetatable({}, {__index = s})
nodes[s.hashkey] = section
cache[groupHash][s.hashkey] = section
count = count + 1
end
end)
return count
end
local function group_has_saved_nodes(groupHash)
local found = false
ucic:foreach(name, uciType, function(s)
if s.grouphashkey == groupHash and s.hashkey then
found = true
return false
end
end)
return found
end
local function get_section_ss_backend(section)
@@ -1901,6 +2382,58 @@ local function group_needs_ss_backend_refresh(groupHash)
return has_ss_node and needs_refresh
end
local function is_mihomo_subscribe_node(section)
if not section then
return false
end
if section.type == "clash" and section.clash_path and tostring(section.clash_path):match("%.mihomo%.yaml$") then
return true
end
if section.type == "ssr" then
return true
end
if section.type == "ss" or section.type == "ss-rust" then
return true
end
if section.type == "v2ray" and (
section.v2ray_protocol == "vmess"
or section.v2ray_protocol == "vless"
or section.v2ray_protocol == "shadowsocks"
or section.v2ray_protocol == "trojan"
or section.v2ray_protocol == "hysteria2"
or section.v2ray_protocol == "hy2"
or section.v2ray_protocol == "snell"
) then
return true
end
if section.type == "tuic" then
return true
end
return false
end
local function group_needs_mihomo_subscribe_refresh(groupHash)
if not has_mihomo then
return false
end
local needs_refresh = false
ucic:foreach(name, uciType, function(s)
if s.grouphashkey == groupHash then
if is_mihomo_subscribe_node(s) and s.type ~= "clash" then
needs_refresh = true
return false
end
if s.type == "clash" and s.clash_path and tostring(s.clash_path):match("%.anytls%.yaml$") then
needs_refresh = true
return false
end
end
end)
return needs_refresh
end
local function preserve_unselected_groups(selected_hashes)
local preserved = {}
@@ -1918,7 +2451,8 @@ local execute = function()
local selected_hashes = {}
for _, item in ipairs(subscribe_items) do
selected_hashes[md5(item.url)] = true
local fetch_url = build_convert_url(item.url)
selected_hashes[md5(fetch_url)] = true
end
if target_subscribe_sid ~= "" then
@@ -1927,21 +2461,38 @@ local execute = function()
for _, item in ipairs(subscribe_items) do
local url = item.url
local raw, new_md5 = curl(url, user_agent)
local fetch_url, converted = build_convert_url(url)
local raw, new_md5 = curl(fetch_url, user_agent)
local yaml_removed_count = 0
if isClashYAML(raw) then
raw, yaml_removed_count = filterClashYamlRaw(raw)
if yaml_removed_count and yaml_removed_count > 0 then
new_md5 = md5_string(raw)
end
end
log("raw 长度: "..#raw)
local groupHash = md5(url)
local groupHash = md5(fetch_url)
local old_md5 = read_old_md5(groupHash)
log("处理订阅: " .. url)
if converted then
log("使用订阅转换模板: " .. template_url)
log("转换服务: " .. convert_address)
end
if yaml_removed_count and yaml_removed_count > 0 then
log("Clash/Mihomo YAML 已按订阅过滤关键词移除节点数量: " .. tostring(yaml_removed_count))
end
log("groupHash: " .. groupHash)
log("old_md5: " .. tostring(old_md5))
log("new_md5: " .. tostring(new_md5))
local backend_refresh = group_needs_ss_backend_refresh(groupHash)
local mihomo_subscribe_refresh = group_needs_mihomo_subscribe_refresh(groupHash)
local missing_saved_nodes = old_md5 and new_md5 == old_md5 and not group_has_saved_nodes(groupHash)
if #raw == 0 then
log(url .. ': 获取内容为空')
loadOldNodes(groupHash)
elseif old_md5 and new_md5 == old_md5 and not backend_refresh then
elseif old_md5 and new_md5 == old_md5 and not backend_refresh and not mihomo_subscribe_refresh and not missing_saved_nodes then
log("订阅未变化, 跳过无需更新的订阅: " .. url)
-- 防止 diff 阶段误删未更新订阅节点
loadOldNodes(groupHash)
@@ -1955,6 +2506,12 @@ local execute = function()
if backend_refresh and old_md5 and new_md5 == old_md5 then
log("检测到 SS 后端偏好变化,强制重建订阅节点: " .. url)
end
if mihomo_subscribe_refresh and old_md5 and new_md5 == old_md5 then
log("检测到可迁移订阅节点,强制重建为 Mihomo 总节点: " .. url)
end
if missing_saved_nodes then
log("订阅内容未变化但本地节点不存在,强制重建订阅节点: " .. url)
end
updated = true
-- 保存更新后的 MD5 值到以 groupHash 为标识的临时文件中,用于下次订阅更新时进行对比
write_new_md5(groupHash, new_md5)
@@ -1967,7 +2524,7 @@ local execute = function()
if isClashYAML(raw) then
is_clash_subscription = true
local result = processClashSubscription(url)
local result = processClashSubscription(fetch_url)
if result and not check_filer(result) and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
@@ -2014,7 +2571,7 @@ local execute = function()
if line:match("^anytls://") then
local parsed = parseAnytlsShare(line:gsub("^anytls://", ""))
if parsed then
table.insert(anytls_nodes, parsed)
table.insert(anytls_nodes, anytls_to_mihomo_node(parsed))
end
elseif line ~= "" then
table.insert(normal_nodes, node)
@@ -2022,19 +2579,8 @@ local execute = function()
end
if #anytls_nodes > 0 then
local parsed_url = URL.parse(url)
local alias = "Clash_" .. (parsed_url.host or groupHash)
local local_path = string.format("%s/%s.anytls.yaml", local_clash_dir, groupHash)
local yaml = buildAnytlsClashYaml(anytls_nodes, "Proxy")
nixio.fs.mkdirr(local_clash_dir)
nixio.fs.writefile(local_path, yaml)
local result = processLocalClashSubscription(local_path, alias)
if result and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
cache[groupHash][result.hashkey] = result
log('成功导入 AnyTLS 转 Clash 总节点: ' .. result.alias)
for _, parsed in ipairs(anytls_nodes) do
table.insert(normal_nodes, parsed)
end
end
@@ -2046,10 +2592,12 @@ local execute = function()
if not is_clash_subscription then
for _, v in ipairs(nodes) do
if v and not string.match(v, "^%s*$") then
if v and (type(v) == "table" or not string.match(v, "^%s*$")) then
xpcall(function()
local result
if szType then
if type(v) == "table" then
result = v
elseif szType then
result = processData(szType, v)
elseif not szType then
local node = trim(v)
@@ -2061,7 +2609,7 @@ local execute = function()
if dat[3] then
dat3 = "://" .. dat[3]
end
if dat[1] == 'ss' or dat[1] == 'trojan' or dat[1] == 'tuic' then
if dat[1] == 'ss' or dat[1] == 'trojan' or dat[1] == 'tuic' or dat[1] == 'snell' then
result = processData(dat[1], dat[2] .. dat3)
else
result = processData(dat[1], base64Decode(dat[2]))
@@ -2110,7 +2658,44 @@ local execute = function()
end
-- 清理临时字段
node.raw_alias = nil
-- 存入 nodeResult
end
local mihomo_nodes = {}
local legacy_nodes = {}
for _, node in ipairs(groupRawNodes) do
if has_mihomo and can_group_into_mihomo(node) then
mihomo_nodes[#mihomo_nodes + 1] = node
else
legacy_nodes[#legacy_nodes + 1] = node
end
end
if #mihomo_nodes > 0 then
local parsed_url = URL.parse(url)
local alias = item.alias ~= "" and item.alias or ("Mihomo_" .. ((parsed_url and parsed_url.host) or groupHash))
local local_path = string.format("%s/%s.mihomo.yaml", local_clash_dir, groupHash)
local yaml, proxy_count = buildMihomoSubscribeYaml(mihomo_nodes, "Proxy")
if yaml and proxy_count > 0 then
nixio.fs.mkdirr(local_clash_dir)
nixio.fs.writefile(local_path, yaml)
local result = processLocalClashSubscription(local_path, alias)
if result and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
cache[groupHash][result.hashkey] = result
log('成功导入 Mihomo 总节点: ' .. result.alias .. ',包含节点数量: ' .. proxy_count)
end
else
log('Mihomo 总节点生成失败,回退为普通订阅节点。')
for _, node in ipairs(mihomo_nodes) do
legacy_nodes[#legacy_nodes + 1] = node
end
end
end
for _, node in ipairs(legacy_nodes) do
node.grouphashkey = groupHash
table.insert(nodeResult[index], node)
cache[groupHash][node.hashkey] = node