🔥 Sync 2026-09-29 06:58:20

This commit is contained in:
github-actions[bot] committed 2026-09-29 06:58:20 +08:00
1 parent 498fa20885
commit 37d63c09a5
22 files changed
+459 -121

No files matched your search

@@ -1,62 +1,23 @@
-- output.lua — 订阅输出统一分发(所有 13 种目标格式)
-- output.lua — 订阅输出统一分发(所有目标格式的唯一注册点)
-- luci-app-substore
local util = require("substore.util")
local node = require("substore.node")
local clash_meta = require("substore.output_clash_meta")
local output_uri = require("substore.output_uri")
local output_singbox = require("substore.output_singbox")
local output_v2ray = require("substore.output_v2ray")
local output_formats = require("substore.output_formats")
local output_wgconf = require("substore.output_wireguard_conf")
local M = {}
local function esc_yaml(s)
s = tostring(s or "")
s = s:gsub("\\", "\\\\"):gsub("\"", "\\\""):gsub("\n", "\\n")
if s:find("[ :#{}[\],&*?|>'\"%@`]", 1, true) or s:match("^[-?]*:") then
return '"' .. s .. '"'
end
return s
end
function M.to_clash_yaml(nodes)
local out = {}
out[#out + 1] = "proxies:"
for _, n in ipairs(nodes) do
local proto = n.proto or "vmess"
local name = esc_yaml(n.name or "")
out[#out + 1] = "- name: " .. name
out[#out + 1] = " type: " .. proto
out[#out + 1] = " server: " .. esc_yaml(n.server or "")
out[#out + 1] = " port: " .. tostring(n.port or 0)
if n.uuid then out[#out + 1] = " uuid: " .. esc_yaml(n.uuid) end
if n.password then out[#out + 1] = " password: " .. esc_yaml(n.password) end
if n.method then out[#out + 1] = " cipher: " .. esc_yaml(n.method) end
if n.net then out[#out + 1] = " network: " .. esc_yaml(n.net) end
if n.security then out[#out + 1] = " tls: " .. tostring(n.security ~= "none") end
if n.sni then out[#out + 1] = " sni: " .. esc_yaml(n.sni) end
end
out[#out + 1] = ""
out[#out + 1] = "proxy-groups:"
out[#out + 1] = "- name: ALL"
out[#out + 1] = " type: select"
out[#out + 1] = " proxies: [REJECT]"
return table.concat(out, "\n")
end
function M.to_json(nodes)
return util.json_encode(nodes)
end
function M.to_base64(nodes)
local yaml = M.to_clash_yaml(nodes)
return util.base64_encode(yaml)
end
-- 目标格式别名映射(兼容 ?target=X 的各类写法)
M.FORMAT_ALIASES = {
clash = "clashmeta",
-- Clash 原版:vless/hysteria2/tuic/wireguard 等原版不支持的协议会被过滤
clash = "clash",
clashclassic = "clash",
clashpremium = "clash",
-- Clash.Meta / Mihomo
yaml = "clashmeta",
clashmeta = "clashmeta",
mihomo = "clashmeta",
@@ -77,6 +38,13 @@ M.FORMAT_ALIASES = {
v2rayuri = "v2rayuri",
v2ray_uri = "v2rayuri",
uri = "v2rayuri",
-- wg-quick / AmneziaWG .conf
wgconf = "wgconf",
wg = "wgconf",
wireguard = "wgconf",
amneziawg = "wgconf",
amnezia = "wgconf",
conf = "wgconf",
plain = "plain",
plainjson = "plain",
json = "plain",
@@ -85,6 +53,7 @@ M.FORMAT_ALIASES = {
-- 目标格式的 HTTP Content-Type
local CONTENT_TYPES = {
clash = "text/plain; charset=utf-8",
clashmeta = "text/plain; charset=utf-8",
stash = "text/plain; charset=utf-8",
surge = "text/plain; charset=utf-8",
@@ -97,12 +66,16 @@ local CONTENT_TYPES = {
singbox = "application/json; charset=utf-8",
v2ray = "application/json; charset=utf-8",
v2rayuri = "text/plain; charset=utf-8",
wgconf = "text/plain; charset=utf-8",
plain = "application/json; charset=utf-8",
}
-- 未指定 target 时的默认格式(保持历史行为:Clash.Meta)
local DEFAULT_FORMAT = "clashmeta"
function M.content_type_for(format)
format = (format or "clash") or "clash"
if type(format) ~= "string" then format = "clash" end
format = (format or DEFAULT_FORMAT) or DEFAULT_FORMAT
if type(format) ~= "string" then format = DEFAULT_FORMAT end
local norm = M.FORMAT_ALIASES[format:lower():gsub("[-_%s]", "")]
if not norm then norm = M.FORMAT_ALIASES[format:lower()] end
return CONTENT_TYPES[norm]
@@ -110,6 +83,7 @@ end
-- 目标格式的下载文件名后缀
local FILENAME_EXT = {
clash = "yaml",
clashmeta = "yaml",
stash = "yaml",
surge = "conf",
@@ -122,26 +96,47 @@ local FILENAME_EXT = {
singbox = "json",
v2ray = "json",
v2rayuri = "txt",
wgconf = "conf",
plain = "json",
}
function M.extension_for(format)
format = (format or "clash") or "clash"
if type(format) ~= "string" then format = "clash" end
format = (format or DEFAULT_FORMAT) or DEFAULT_FORMAT
if type(format) ~= "string" then format = DEFAULT_FORMAT end
local norm = M.FORMAT_ALIASES[format:lower():gsub("[-_%s]", "")]
if not norm then norm = M.FORMAT_ALIASES[format:lower()] end
return FILENAME_EXT[norm] or "txt"
end
-- UI 下拉使用的有序格式列表(单一数据源;模板由此渲染,避免多处硬编码不同步)
M.FORMAT_OPTIONS = {
{ "clashmeta", "Clash.Meta / Mihomo" },
{ "clash", "Clash" },
{ "stash", "Stash" },
{ "surge", "Surge" },
{ "surfboard", "Surfboard" },
{ "surgemac", "SurgeMac" },
{ "loon", "Loon" },
{ "egern", "Egern" },
{ "qx", "Quantumult X" },
{ "shadowrocket", "Shadowrocket" },
{ "singbox", "sing-box" },
{ "v2ray", "V2Ray" },
{ "v2rayuri", "V2Ray URI" },
{ "wgconf", "WireGuard / AmneziaWG .conf" },
{ "plain", "Plain JSON" },
}
-- 统一分发:nodes → 目标格式字符串
function M.generate(nodes, format, options)
format = (format or "clash") or "clash"
if type(format) ~= "string" then format = "clash" end
format = (format or DEFAULT_FORMAT) or DEFAULT_FORMAT
if type(format) ~= "string" then format = DEFAULT_FORMAT end
local norm = M.FORMAT_ALIASES[format:lower():gsub("[-_%s]", "")]
if not norm then norm = M.FORMAT_ALIASES[format:lower()] end
if not norm then return nil, "unsupported format: " .. tostring(format) end
if norm == "clashmeta" then return clash_meta.generate(nodes, options) end
if norm == "clash" then return output_formats.to_clash(nodes, options) end
if norm == "stash" then return output_formats.to_stash(nodes, options) end
if norm == "surge" then return output_formats.to_surge(nodes, options) end
if norm == "surfboard" then return output_formats.to_surfboard(nodes, options) end
@@ -153,6 +148,7 @@ function M.generate(nodes, format, options)
if norm == "singbox" then return output_singbox.generate(nodes) end
if norm == "v2ray" then return output_v2ray.generate(nodes) end
if norm == "v2rayuri" then return output_uri.to_v2ray_uri(nodes) end
if norm == "wgconf" then return output_wgconf.generate(nodes, options) end
if norm == "plain" then return output_formats.to_plain(nodes) end
return nil, "unsupported format: " .. tostring(format)