update 2026-06-29 23:37:40

This commit is contained in:
action committed 2026-06-29 23:37:40 +08:00
1 parent c8ceebd509
commit f769dc4727
15 files changed
+1645 -1217

No files matched your search

@@ -29,6 +29,8 @@ local allow_insecure = ucic:get_first(name, 'server_subscribe', 'allow_insecure'
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')
-- 读取 enable_mihomo 配置
local enable_mihomo = ucic:get_first(name, 'server_subscribe', 'enable_mihomo', '')
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', '')
@@ -44,8 +46,30 @@ local log = function(...)
print(os.date("%Y-%m-%d %H:%M:%S ") .. table.concat({...}, " "))
end
-- 检查是否需要使用 Mihomo 订阅模式
local function should_use_mihomo_mode()
-- 如果 enable_mihomo == 1,使用 Mihomo 模式
if enable_mihomo == "1" and has_mihomo then
return true
end
-- 如果 enable_mihomo ~= 1,检查是否有其他可用核心
if enable_mihomo ~= "1" then
-- 有其他核心可用,使用传统模式
if has_xray or has_ss_rust then
return false
end
-- 没有其他核心,回退到 Mihomo 模式
if has_mihomo then
log("回退: Xray/ss-rust 不存在,使用 Mihomo 订阅模式")
return true
end
end
-- 默认:如果有 Mihomo 则使用 Mihomo 模式
return has_mihomo
end
local function preferred_ss_backend()
if has_mihomo then
if should_use_mihomo_mode() and has_mihomo then
return "ss"
end
if has_ss_rust then
@@ -54,6 +78,9 @@ local function preferred_ss_backend()
if has_xray then
return "v2ray"
end
if has_mihomo then
return "ss"
end
return nil
end
@@ -89,6 +116,7 @@ local encrypt_methods_ss = {
"chacha20",
"chacha20-ietf" ]]--
}
-- 分割字符串
local function split(full, sep)
if full == nil or type(full) ~= "string" then
@@ -117,6 +145,7 @@ local function split(full, sep)
end
return result
end
-- urlencode
local function get_urlencode(c)
return sformat("%%%02X", sbyte(c))
@@ -136,6 +165,7 @@ end
local function get_urldecode(h)
return schar(tonumber(h, 16))
end
local function UrlDecode(szText)
return szText:gsub("+", " "):gsub("%%(%x%x)", get_urldecode)
end
@@ -269,12 +299,14 @@ local function parse_host_port(value, default_port)
return normalize_host(value), default_port
end
-- md5
local function md5(content)
local stdout = luci.sys.exec('echo \"' .. urlEncode(content) .. '\" | md5sum | cut -d \" \" -f1')
-- assert(nixio.errno() == 0)
return trim(stdout)
end
-- base64 解码
local function base64Decode(text)
local raw = text
@@ -296,6 +328,7 @@ local function base64Decode(text)
return raw
end
end
-- base64 编码
local function base64Encode(text)
if not text or text == "" then
@@ -309,6 +342,7 @@ local function base64Encode(text)
return text
end
end
-- 检查数组(table)中是否存在某个字符值
-- https://www.04007.cn/article/135.html
local function checkTabValue(tab)
@@ -318,6 +352,7 @@ local function checkTabValue(tab)
end
return revtab
end
-- JSON完整性检查
local function isCompleteJSON(str)
-- 检查JSON格式
@@ -349,7 +384,8 @@ local function processClashSubscription(url)
return nil
end
local alias = "Clash_" .. parsed.host
local url_hash = string.sub(md5(url), 1, 8)
local alias = "Clash_" .. parsed.host .. "_" .. url_hash
local server_port = parsed.port or ((parsed.scheme == "http") and "80" or "443")
local result = {
type = "clash",
@@ -796,6 +832,10 @@ local function can_group_into_mihomo(node)
if not node then
return false
end
-- 如果不使用 Mihomo 模式,返回 false
if not should_use_mihomo_mode() then
return false
end
if node.type == "ssr" or node.type == "ss" or node.type == "ss-rust" then
return true
end
@@ -820,6 +860,11 @@ local function can_group_into_mihomo(node)
end
local function buildMihomoSubscribeYaml(nodes, group_name)
-- 如果不使用 Mihomo 模式,返回 nil
if not should_use_mihomo_mode() then
return nil, 0
end
local proxies = {}
local names = {}
@@ -943,6 +988,10 @@ local function processData(szType, content, cfgid)
result.flag_obfs = "1"
result.obfs_type = params.obfs
result.salamander = params["obfs-password"] or params["obfs_password"]
if params.obfs == "gecko" then
result.obfs_MinPacketSize = params.minPacketSize or "512"
result.obfs_MaxPacketSize = params.maxPacketSize or "1200"
end
end
if params.allowInsecure or params.insecure then
local insecure = params.allowInsecure or params.insecure
@@ -2386,6 +2435,10 @@ local function is_mihomo_subscribe_node(section)
if not section then
return false
end
-- 如果不使用 Mihomo 模式,返回 false
if not should_use_mihomo_mode() then
return false
end
if section.type == "clash" and section.clash_path and tostring(section.clash_path):match("%.mihomo%.yaml$") then
return true
end
@@ -2413,7 +2466,7 @@ local function is_mihomo_subscribe_node(section)
end
local function group_needs_mihomo_subscribe_refresh(groupHash)
if not has_mihomo then
if not should_use_mihomo_mode() then
return false
end
@@ -2446,13 +2499,26 @@ local function preserve_unselected_groups(selected_hashes)
end)
end
-- 获取 groupHash 的公共函数(预留函数)
local function get_group_hash(url, fetch_url)
if should_use_mihomo_mode() then
return md5(fetch_url)
else
return md5(url)
end
end
local execute = function()
local updated = false
local selected_hashes = {}
for _, item in ipairs(subscribe_items) do
local fetch_url = build_convert_url(item.url)
selected_hashes[md5(fetch_url)] = true
if should_use_mihomo_mode() then
selected_hashes[md5(fetch_url)] = true
else
selected_hashes[md5(item.url)] = true
end
end
if target_subscribe_sid ~= "" then
@@ -2462,24 +2528,33 @@ local execute = function()
for _, item in ipairs(subscribe_items) do
local url = item.url
local fetch_url, converted = build_convert_url(url)
local raw, new_md5 = curl(fetch_url, user_agent)
local raw, new_md5
local groupHash
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)
if should_use_mihomo_mode() then
raw, new_md5 = curl(fetch_url, user_agent)
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
groupHash = md5(fetch_url)
else
raw, new_md5 = curl(url, user_agent)
groupHash = md5(url)
end
log("raw 长度: "..#raw)
local groupHash = md5(fetch_url)
local old_md5 = read_old_md5(groupHash)
log("处理订阅: " .. url)
if converted then
if should_use_mihomo_mode() and converted then
log("使用订阅转换模板: " .. template_url)
log("转换服务: " .. convert_address)
end
if yaml_removed_count and yaml_removed_count > 0 then
if should_use_mihomo_mode() and yaml_removed_count and yaml_removed_count > 0 then
log("Clash/Mihomo YAML 已按订阅过滤关键词移除节点数量: " .. tostring(yaml_removed_count))
end
log("groupHash: " .. groupHash)
@@ -2489,110 +2564,111 @@ local execute = function()
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 and not mihomo_subscribe_refresh and not missing_saved_nodes then
log("订阅未变化, 跳过无需更新的订阅: " .. url)
-- 防止 diff 阶段误删未更新订阅节点
loadOldNodes(groupHash)
--ucic:foreach(name, uciType, function(s)
-- if s.grouphashkey == groupHash and s.hashkey then
-- cache[groupHash][s.hashkey] = s
-- tinsert(nodeResult[index], s)
-- end
--end)
log("订阅未变化, 跳过无需更新的订阅: " .. url)
-- 防止 diff 阶段误删未更新订阅节点
loadOldNodes(groupHash)
--ucic:foreach(name, uciType, function(s)
-- if s.grouphashkey == groupHash and s.hashkey then
-- cache[groupHash][s.hashkey] = s
-- tinsert(nodeResult[index], s)
-- end
--end)
else
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)
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)
cache[groupHash] = {}
tinsert(nodeResult, {})
local index = #nodeResult
local nodes, szType
local is_clash_subscription = false
cache[groupHash] = {}
tinsert(nodeResult, {})
local index = #nodeResult
local nodes, szType
local is_clash_subscription = false
if isClashYAML(raw) then
is_clash_subscription = true
local result = processClashSubscription(fetch_url)
if result and check_filer(result) then
log('过滤 Clash 总节点: ' .. result.alias)
elseif result and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
cache[groupHash][result.hashkey] = result
log('成功导入 Clash 总节点: ' .. result.alias)
else
log('丢弃无效 Clash 总节点: ' .. url)
end
-- SSD 似乎是这种格式 ssd:// 开头的
elseif raw:find('ssd://') then
szType = 'ssd'
local nEnd = select(2, raw:find('ssd://'))
nodes = base64Decode(raw:sub(nEnd + 1, #raw))
nodes = jsonParse(nodes)
local extra = {
airport = nodes.airport,
port = nodes.port,
encryption = nodes.encryption,
password = nodes.password
}
local servers = {}
-- SS里面包着 干脆直接这样
for _, server in ipairs(nodes.servers or {}) do
tinsert(servers, setmetatable(server, {__index = extra}))
end
nodes = servers
-- SS SIP008 直接使用 Json 格式
elseif jsonParse(raw) then
nodes = jsonParse(raw).servers or jsonParse(raw)
if nodes[1] and nodes[1].server and nodes[1].method then
szType = 'sip008'
end
-- 其他 base64 格式
if isClashYAML(raw) then
is_clash_subscription = true
local result = processClashSubscription(fetch_url)
if result and check_filer(result) then
log('过滤 Clash 总节点: ' .. result.alias)
elseif result and not cache[groupHash][result.hashkey] then
result.grouphashkey = groupHash
table.insert(nodeResult[index], result)
cache[groupHash][result.hashkey] = result
log('成功导入 Clash 总节点: ' .. result.alias)
else
-- ssd 外的格式
nodes = split(base64Decode(raw):gsub("\r\n", "\n"), "\n")
log('丢弃无效 Clash 总节点: ' .. url)
end
-- SSD 似乎是这种格式 ssd:// 开头的
elseif raw:find('ssd://') then
szType = 'ssd'
local nEnd = select(2, raw:find('ssd://'))
nodes = base64Decode(raw:sub(nEnd + 1, #raw))
nodes = jsonParse(nodes)
local extra = {
airport = nodes.airport,
port = nodes.port,
encryption = nodes.encryption,
password = nodes.password
}
local servers = {}
-- SS里面包着 干脆直接这样
for _, server in ipairs(nodes.servers or {}) do
tinsert(servers, setmetatable(server, {__index = extra}))
end
nodes = servers
-- SS SIP008 直接使用 Json 格式
elseif jsonParse(raw) then
nodes = jsonParse(raw).servers or jsonParse(raw)
if nodes[1] and nodes[1].server and nodes[1].method then
szType = 'sip008'
end
-- 其他 base64 格式
else
-- ssd 外的格式
nodes = split(base64Decode(raw):gsub("\r\n", "\n"), "\n")
end
if not is_clash_subscription and not szType and type(nodes) == "table" then
local anytls_nodes = {}
local normal_nodes = {}
for _, node in ipairs(nodes) do
local line = trim(node or "")
if line:match("^anytls://") then
local parsed = parseAnytlsShare(line:gsub("^anytls://", ""))
if parsed then
table.insert(anytls_nodes, anytls_to_mihomo_node(parsed))
end
elseif line ~= "" then
table.insert(normal_nodes, node)
end
end
if not is_clash_subscription and not szType and type(nodes) == "table" then
local anytls_nodes = {}
local normal_nodes = {}
for _, node in ipairs(nodes) do
local line = trim(node or "")
if line:match("^anytls://") then
local parsed = parseAnytlsShare(line:gsub("^anytls://", ""))
if parsed then
table.insert(anytls_nodes, anytls_to_mihomo_node(parsed))
end
elseif line ~= "" then
table.insert(normal_nodes, node)
end
if #anytls_nodes > 0 then
for _, parsed in ipairs(anytls_nodes) do
table.insert(normal_nodes, parsed)
end
if #anytls_nodes > 0 then
for _, parsed in ipairs(anytls_nodes) do
table.insert(normal_nodes, parsed)
end
end
nodes = normal_nodes
end
-- 临时存储该订阅解析出的节点(带原始别名)
local groupRawNodes = {}
nodes = normal_nodes
end
if not is_clash_subscription then
-- 临时存储该订阅解析出的节点(带原始别名)
local groupRawNodes = {}
if not is_clash_subscription then
for _, v in ipairs(nodes) do
if v and (type(v) == "table" or not string.match(v, "^%s*$")) then
xpcall(function()
@@ -2621,52 +2697,61 @@ local execute = function()
log('跳过未知类型: ' .. szType)
end
-- log(result)
if result then
local filtered = check_filer(result)
-- 中文做地址的 也没有人拿中文域名搞,就算中文域也有Puny Code SB 机场
local invalid = not result.server or not result.server_port
or (result.type ~= "clash" and result.server == "127.0.0.1")
or result.alias == "NULL"
or (result.type ~= "clash" and result.server:match("[^0-9a-zA-Z%-_%.%s]"))
or cache[groupHash][result.hashkey]
if filtered then
log('过滤节点: ' .. result.alias)
elseif invalid then
log('丢弃无效节点: ' .. result.alias)
else
-- 暂存节点
table.insert(groupRawNodes, result)
end
if result then
local filtered = check_filer(result)
-- 中文做地址的 也没有人拿中文域名搞,就算中文域也有Puny Code SB 机场
local invalid = not result.server or not result.server_port
or (result.type ~= "clash" and result.server == "127.0.0.1")
or result.alias == "NULL"
or (result.type ~= "clash" and result.server:match("[^0-9a-zA-Z%-_%.%s]"))
or cache[groupHash][result.hashkey]
if filtered then
log('过滤节点: ' .. result.alias)
elseif invalid then
log('丢弃无效节点: ' .. result.alias)
else
-- 暂存节点
table.insert(groupRawNodes, result)
end
end
end, function(err)
log(string.format("解析节点出错: %s\n原始数据: %s", tostring(err), tostring(v)))
end)
end
end
end
end
-- 对该组节点进行别名编号:重复节点加后缀,唯一节点不加
-- 传统模式:对 groupRawNodes 进行组内编号
if not should_use_mihomo_mode() then
local freq = {}
for _, node in ipairs(groupRawNodes) do
local raw = node.raw_alias or ""
freq[raw] = (freq[raw] or 0) + 1
if raw ~= "" then
freq[raw] = (freq[raw] or 0) + 1
end
end
local aliasCount = {}
for _, node in ipairs(groupRawNodes) do
local raw = node.raw_alias or ""
if freq[raw] > 1 then
local count = (aliasCount[raw] or 0) + 1
aliasCount[raw] = count
node.alias = raw .. "_" .. count
else
node.alias = raw
if raw ~= "" then
if freq[raw] and freq[raw] > 1 then
local count = (aliasCount[raw] or 0) + 1
aliasCount[raw] = count
node.alias = raw .. "_" .. count
else
node.alias = raw
end
end
-- 清理临时字段
node.raw_alias = nil
end
end
local mihomo_nodes = {}
local legacy_nodes = {}
local mihomo_nodes = {}
local legacy_nodes = {}
-- 只有当 should_use_mihomo_mode() 为 true 时才尝试分组
if should_use_mihomo_mode() then
for _, node in ipairs(groupRawNodes) do
if has_mihomo and can_group_into_mihomo(node) then
mihomo_nodes[#mihomo_nodes + 1] = node
@@ -2674,41 +2759,50 @@ local execute = function()
legacy_nodes[#legacy_nodes + 1] = node
end
end
else
-- 传统模式:所有节点作为普通节点
for _, node in ipairs(groupRawNodes) do
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 #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)
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
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
end
for _, node in ipairs(legacy_nodes) do
node.grouphashkey = groupHash
table.insert(nodeResult[index], node)
cache[groupHash][node.hashkey] = node
end
if not should_use_mihomo_mode() then
log('成功解析节点数量: ' .. #groupRawNodes)
end
end
end
-- 输出日志并判断是否需要进行 diff
if not updated then
log("订阅未变化,无需更新节点信息。")
@@ -2729,8 +2823,8 @@ local execute = function()
del = del + 1
else
local dat = nodeResult[old.grouphashkey][old.hashkey]
ucic:tset(name, old['.name'], dat)
-- 标记一下
ucic:tset(name, old['.name'], dat)
setmetatable(nodeResult[old.grouphashkey][old.hashkey], {__index = {_ignore = true}})
end
else
@@ -2746,11 +2840,12 @@ local execute = function()
end
end
end)
-- 1615-1653 行为生成 sid
-- 生成 sid
-- 记录已使用编号
local used_sid = {}
local next_sid = 1
-- 扫描已有 section
-- 检查已有 section
ucic:foreach(name, uciType, function(s)
local num = s[".name"]:match("^cfg(%x%x)") -- 提取两位十六进制序号
if num then
@@ -2820,6 +2915,14 @@ if subscribe_items and #subscribe_items > 0 then
else
log("当前订阅模式: 不通过代理订阅")
end
-- 记录当前使用的订阅模式
if should_use_mihomo_mode() then
log("订阅模式: Mihomo 总节点模式")
else
log("订阅模式: 传统节点模式")
end
local direct_bypass = create_direct_subscribe_bypass()
if direct_bypass then
direct_bypass:apply()