🦄 Sync 2026-06-05 01:27:09

This commit is contained in:
github-actions[bot] committed 2026-06-05 01:27:09 +08:00
1 parent 4c613c876b
commit 23b721b464
63 files changed
+10393 -11351

No files matched your search

+6 -2
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=beszel-agent
PKG_VERSION:=0.18.7
PKG_RELEASE:=4
PKG_RELEASE:=5
ifeq ($(ARCH),aarch64)
PKG_ARCH:=arm64
@@ -45,7 +45,11 @@ endef
define Build/Compile
if command -v upx >/dev/null 2>&1; then \
upx --best --lzma $(PKG_BUILD_DIR)/beszel-agent || true; \
if [ "$(BESZEL_GOARCH)" = "mips" ] || [ "$(BESZEL_GOARCH)" = "mipsle" ]; then \
echo "Skipping UPX compression for MIPS architectures (prevents execution hang)..."; \
else \
upx --best --lzma $(PKG_BUILD_DIR)/beszel-agent || true; \
fi; \
fi
endef
+35 -5
View File
@@ -1,21 +1,50 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclash
PKG_VERSION:=0.47.075
PKG_RELEASE:=3
PKG_VERSION:=0.47.097
PKG_RELEASE:=4
PKG_MAINTAINER:=vernesong <https://github.com/vernesong/OpenClash>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
include $(INCLUDE_DIR)/package.mk
define Package/$(PKG_NAME)/config
config PACKAGE_kmod-inet-diag
default y if PACKAGE_$(PKG_NAME)
config PACKAGE_luci-compat
default y if PACKAGE_$(PKG_NAME)
config PACKAGE_kmod-nft-tproxy
default y if PACKAGE_firewall4
config PACKAGE_kmod-ipt-nat
default y if ! PACKAGE_firewall4
config PACKAGE_iptables-mod-tproxy
default y if ! PACKAGE_firewall4
config PACKAGE_iptables-mod-extra
default y if ! PACKAGE_firewall4
config PACKAGE_dnsmasq_full_ipset
default y if ! PACKAGE_firewall4
config PACKAGE_dnsmasq_full_nftset
default y if PACKAGE_firewall4
config PACKAGE_ipset
default y if ! PACKAGE_firewall4
endef
define Package/$(PKG_NAME)
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI support for clash
PKGARCH:=all
DEPENDS:=+luci-compat +dnsmasq-full +bash +curl +ca-bundle +ip-full +ruby \
+ruby-yaml +kmod-inet-diag +kmod-nft-tproxy +kmod-tun +unzip
DEPENDS:=+dnsmasq-full +bash +curl +ca-bundle +ip-full \
+ruby +ruby-yaml +kmod-tun +unzip
MAINTAINER:=vernesong
endef
@@ -124,7 +153,8 @@ define Package/$(PKG_NAME)/postrm
uci -q delete ucitrack.@openclash[-1]
uci -q commit ucitrack
}
rm -rf /tmp/luci-*
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /var/luci-indexcache /var/luci-indexcache.* >/dev/null 2>&1
rm -rf /tmp/luci-modulecache /var/luci-modulecache >/dev/null 2>&1
exit 0
endef
+175 -116
View File
@@ -665,8 +665,12 @@ function get_sub_url(filename)
string.gsub(s.url, '[^\n]+', function(w) table.insert(info_tb, w) end)
end
if #info_tb == 1 then
local url, _ = parse_url_with_name(info_tb[1], filename)
sub_url = url
local url, name = parse_url_with_name(info_tb[1], filename)
if url ~= info_tb[1] then
table.insert(providers, {name = name, url = url})
else
sub_url = url
end
elseif #info_tb > 1 then
for _, raw in ipairs(info_tb) do
local url, name = parse_url_with_name(raw, filename)
@@ -798,7 +802,7 @@ function sub_info_get()
if #providers_data == 0 then
if not url_result then
luci.http.status(400, "Subscription information not found")
luci.http.status(500, "Subscription information not found")
return
end
end
@@ -841,7 +845,7 @@ function action_switch_rule_mode()
mode = luci.http.formvalue("rule_mode")
if not mode then
luci.http.status(400, "Missing parameters")
luci.http.status(500, "Missing parameters")
return
end
@@ -1098,7 +1102,7 @@ end
function action_default_dashboard()
local default_dashboard = luci.http.formvalue("name")
if not default_dashboard or (default_dashboard ~= "Dashboard" and default_dashboard ~= "Yacd" and default_dashboard ~= "Metacubexd" and default_dashboard ~= "Zashboard") then
luci.http.status(400, "Set Failed")
luci.http.status(500, "Set Failed")
return
end
if not fs.isdirectory("/usr/share/openclash/ui/" .. string.lower(default_dashboard)) then
@@ -1644,6 +1648,13 @@ function rename_file()
uci:set("openclash", s[".name"], "name", fs.filename(new_file_name))
end
end)
uci:foreach("openclash", "subscribe_info",
function(s)
if s.name == fs.filename(old_file_name) and fs.filename(new_file_name) ~= new_file_name then
uci:set("openclash", s[".name"], "name", fs.filename(new_file_name))
end
end)
uci:foreach("openclash", "groups",
function(s)
@@ -2073,114 +2084,101 @@ function action_myip_check()
luci.http.write_json(result)
end
function action_website_check()
local domain = luci.http.formvalue("domain")
local result = {
success = false,
response_time = 0,
error = ""
}
function latency_test(addr)
local result = { success = false, response_time = 0, error = "" }
if not domain then
if not addr then
result.error = "Missing domain parameter"
luci.http.prepare_content("application/json")
luci.http.write_json(result)
return
return result
end
local test_domain = domain
local test_url
if test_domain:match("^https?://") then
test_domain = test_domain:gsub("^https?://([^/]+)/?.*$", "%1")
if addr:match("^https?://") then
addr = addr:gsub("^https?://([^/]+)/?.*$", "%1")
end
if domain == "https://raw.githubusercontent.com/" or test_domain == "raw.githubusercontent.com" then
test_url = "https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png"
local urls = {}
if addr == "raw.githubusercontent.com" then
table.insert(urls, "https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png")
else
test_url = "https://" .. test_domain .. "/favicon.ico"
table.insert(urls, "https://" .. addr .. "/favicon.ico")
end
local cmd = string.format(
'curl -sL -m 5 --connect-timeout 3 --retry 2 -w "%%{http_code},%%{time_total},%%{time_connect},%%{time_appconnect}" "%s" -o /dev/null 2>/dev/null',
test_url
)
table.insert(urls, "https://" .. addr)
local output = luci.sys.exec(cmd)
for i, test_url in ipairs(urls) do
local cmd = string.format(
'curl -sI -m 5 --connect-timeout 3 --retry 2 -w "%%{http_code},%%{time_total},%%{time_connect},%%{time_appconnect}" "%s" -o /dev/null 2>/dev/null',
test_url
)
if output and output ~= "" then
local http_code, time_total, time_connect, time_appconnect = output:match("(%d+),([%d%.]+),([%d%.]+),([%d%.]+)")
local output = luci.sys.exec(cmd)
if output and output ~= "" then
local http_code, time_total, time_connect, time_appconnect =
output:match("(%d+),([%d%.]+),([%d%.]+),([%d%.]+)")
if http_code and tonumber(http_code) then
local code = tonumber(http_code)
local response_time = 0
if time_appconnect and tonumber(time_appconnect) and tonumber(time_appconnect) > 0 then
response_time = math.floor(tonumber(time_appconnect) * 1000)
elseif time_connect and tonumber(time_connect) then
response_time = math.floor(tonumber(time_connect) * 1000)
else
response_time = math.floor((tonumber(time_total) or 0) * 1000)
if not http_code then
http_code, time_total, time_appconnect = output:match("(%d+),([%d%.]+),([%d%.]+)")
time_connect = nil
end
if code >= 200 and code < 400 then
result.success = true
result.response_time = response_time
elseif code == 403 or code == 404 then
result.success = true
result.response_time = response_time
else
local fallback_url
if domain == "https://raw.githubusercontent.com/" or test_domain == "raw.githubusercontent.com" then
fallback_url = "https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png"
if http_code and tonumber(http_code) then
local code = tonumber(http_code)
local response_time = 0
if time_appconnect and tonumber(time_appconnect) and tonumber(time_appconnect) > 0 then
response_time = math.floor(tonumber(time_appconnect) * 1000)
elseif time_connect and tonumber(time_connect) and tonumber(time_connect) > 0 then
response_time = math.floor(tonumber(time_connect) * 1000)
else
fallback_url = "https://" .. test_domain .. "/"
response_time = math.floor((tonumber(time_total) or 0) * 1000)
end
local fallback_cmd = string.format(
'curl -sI -m 5 --connect-timeout 3 -w "%%{http_code},%%{time_total},%%{time_appconnect}" "%s" -o /dev/null 2>/dev/null',
fallback_url
)
local fallback_output = luci.sys.exec(fallback_cmd)
if fallback_output and fallback_output ~= "" then
local fb_code, fb_total, fb_appconnect = fallback_output:match("(%d+),([%d%.]+),([%d%.]+)")
if fb_code and tonumber(fb_code) then
local fb_code_num = tonumber(fb_code)
local fb_response_time = 0
if fb_appconnect and tonumber(fb_appconnect) and tonumber(fb_appconnect) > 0 then
fb_response_time = math.floor(tonumber(fb_appconnect) * 1000)
else
fb_response_time = math.floor((tonumber(fb_total) or 0) * 1000)
end
if fb_code_num >= 200 and fb_code_num < 400 then
result.success = true
result.response_time = fb_response_time
elseif fb_code_num == 403 or fb_code_num == 404 then
result.success = true
result.response_time = fb_response_time
else
result.success = false
result.error = "HTTP " .. fb_code_num
result.response_time = fb_response_time
end
else
result.success = false
result.error = "Connection failed"
end
if (code >= 200 and code < 400) or code == 403 or code == 404 then
result.success = true
result.response_time = response_time
return result
else
if i == #urls then
result.success = false
result.error = "HTTP " .. code
result.response_time = response_time
return result
end
end
else
if i == #urls then
result.success = false
result.error = "Connection failed"
result.error = "Invalid response"
return result
end
end
else
result.success = false
result.error = "Invalid response"
if i == #urls then
result.success = false
result.error = "No response"
return result
end
end
else
result.success = false
result.error = "No response"
end
return result
end
function action_website_check()
local domain = luci.http.formvalue("domain")
if not domain then
luci.http.prepare_content("application/json")
luci.http.write_json({
success = false,
response_time = 0,
error = "Missing domain parameter"
})
return
end
local result = latency_test(domain)
luci.http.prepare_content("application/json")
luci.http.write_json(result)
end
@@ -2257,7 +2255,7 @@ function action_switch_oc_setting()
local value = luci.http.formvalue("value")
if not setting or not value then
luci.http.status(400, "Missing parameters")
luci.http.status(500, "Missing parameters")
return
end
@@ -2455,9 +2453,16 @@ function action_switch_oc_setting()
uci:set("openclash", "@overwrite[0]", "enable_respect_rules", tonumber(value))
uci:commit("openclash")
elseif setting == "oversea" then
oversea_v6_setting = fs.uci_get_config("config", "ipv6_enable") or "0"
if oversea_v6_setting ~= "0" then
uci:set("openclash", "config", "china_ip6_route", value)
end
uci:set("openclash", "config", "china_ip_route", value)
uci:commit("openclash")
if is_running() then
if oversea_v6_setting ~= "0" then
uci:set("openclash", "@overwrite[0]", "china_ip6_route", value)
end
uci:set("openclash", "@overwrite[0]", "china_ip_route", value)
uci:commit("openclash")
luci.sys.exec("/etc/init.d/openclash restart >/dev/null 2>&1 &")
@@ -2490,7 +2495,7 @@ function action_switch_oc_setting()
end
uci:commit("openclash")
else
luci.http.status(400, "Invalid setting")
luci.http.status(500, "Invalid setting")
return
end
@@ -2910,14 +2915,14 @@ function action_oc_action()
local config_file = luci.http.formvalue("config_file")
if not action then
luci.http.status(400, "Missing action parameter")
luci.http.status(500, "Missing action parameter")
return
end
if config_file and config_file ~= "" then
local config_path = "/etc/openclash/config/" .. config_file
if not fs.access(config_path) then
luci.http.status(404, "Config file not found")
luci.http.status(500, "Config file not found")
return
end
@@ -2952,7 +2957,7 @@ function action_oc_action()
luci.sys.call("ps | grep openclash | grep -v grep | awk '{print $1}' | xargs -r kill -9 >/dev/null 2>&1")
luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1")
else
luci.http.status(400, "Invalid action parameter")
luci.http.status(500, "Invalid action parameter")
return
end
@@ -3111,7 +3116,7 @@ function action_config_file_read()
local config_file = luci.http.formvalue("config_file")
if not config_file then
luci.http.status(400, "Missing config_file parameter")
luci.http.status(500, "Missing config_file parameter")
return
end
@@ -3202,12 +3207,12 @@ function action_config_file_save()
end
if not config_file then
luci.http.status(400, "Missing config_file parameter")
luci.http.status(500, "Missing config_file parameter")
return
end
if not content then
luci.http.status(400, "Missing content parameter")
luci.http.status(500, "Missing content parameter")
return
end
@@ -3323,7 +3328,7 @@ end
function action_add_subscription()
local name = luci.http.formvalue("name")
local address = luci.http.formvalue("address")
local sub_ua = luci.http.formvalue("sub_ua") or "clash.meta"
local sub_ua = luci.http.formvalue("sub_ua") or "clash-verge/v2.4.5"
local sub_convert = luci.http.formvalue("sub_convert") or "0"
local convert_address = luci.http.formvalue("convert_address") or ""
local template = luci.http.formvalue("template") or ""
@@ -3351,9 +3356,10 @@ function action_add_subscription()
local is_valid_url = false
if sub_convert == "1" then
if string.find(address, "^https?://") and not string.find(address, "\n") and not string.find(address, "|") then
is_valid_url = true
elseif string.find(address, "\n") or string.find(address, "|") then
local prefixed_http_pattern = "^[^,%s]+,https?://.+"
local encoded_prefixed_http_pattern = "^[^%%%s]+%%2[Cc]https?%%3[Aa]%%2[Ff]%%2[Ff].+"
if string.find(address, "\n") or string.find(address, "|") then
local links = {}
if string.find(address, "\n") then
for line in address:gmatch("[^\n]+") do
@@ -3367,15 +3373,20 @@ function action_add_subscription()
for _, link in ipairs(links) do
if link and link ~= "" then
if string.find(link, "^https?://") or string.find(link, "^[a-zA-Z]+://") then
if string.find(link, "^https?://")
or string.find(link, "^[a-zA-Z]+://")
or string.find(link, prefixed_http_pattern)
or string.find(link, encoded_prefixed_http_pattern) then
is_valid_url = true
break
end
end
end
else
if string.find(address, "^[a-zA-Z]+://") and
not string.find(address, "\n") and not string.find(address, "|") then
if string.find(address, "^https?://")
or string.find(address, "^[a-zA-Z]+://")
or string.find(address, prefixed_http_pattern)
or string.find(address, encoded_prefixed_http_pattern) then
is_valid_url = true
end
end
@@ -3467,9 +3478,7 @@ function action_add_subscription()
end
end
if #params > 0 then
for i, param in ipairs(params) do
uci:set_list("openclash", section_id, "custom_params", param)
end
uci:set_list("openclash", section_id, "custom_params", params)
end
end
@@ -3483,9 +3492,7 @@ function action_add_subscription()
end
end
if #keywords > 0 then
for i, kw in ipairs(keywords) do
uci:set_list("openclash", section_id, "keyword", kw)
end
uci:set_list("openclash", section_id, "keyword", keywords)
end
end
@@ -3499,9 +3506,7 @@ function action_add_subscription()
end
end
if #ex_keywords > 0 then
for i, ex_kw in ipairs(ex_keywords) do
uci:set_list("openclash", section_id, "ex_keyword", ex_kw)
end
uci:set_list("openclash", section_id, "ex_keyword", ex_keywords)
end
end
@@ -3530,6 +3535,16 @@ end
function action_upload_overwrite()
local upload = luci.http.formvalue("config_file")
local filename = luci.http.formvalue("filename")
local config_values = {}
local raw_config = luci.http.formvalue("config") or ""
if raw_config ~= "" then
for line in raw_config:gmatch("[^\n]+") do
local config_value = line:match("^%s*(.-)%s*$")
if config_value and config_value ~= "" then
table.insert(config_values, config_value)
end
end
end
local enable = luci.http.formvalue("enable")
local order = luci.http.formvalue("order")
luci.http.prepare_content("application/json")
@@ -3575,6 +3590,10 @@ function action_upload_overwrite()
uci:foreach("openclash", "config_overwrite", function(s)
if s.name == section_name then
found = true
uci:delete("openclash", s[".name"], "config")
if #config_values > 0 then
uci:set_list("openclash", s[".name"], "config", config_values)
end
if s.enable == nil or (s.enable ~= nil and enable ~= nil) then
if enable == nil then
enable = 0
@@ -3600,6 +3619,10 @@ function action_upload_overwrite()
local sid = uci:add("openclash", "config_overwrite")
uci:set("openclash", sid, "name", section_name)
uci:set("openclash", sid, "type", "file")
uci:delete("openclash", sid, "config")
if #config_values > 0 then
uci:set_list("openclash", sid, "config", config_values)
end
if enable ~= nil then
uci:set("openclash", sid, "enable", tostring(enable))
else
@@ -3657,8 +3680,22 @@ function action_overwrite_subscribe_info()
local result = {}
uci:foreach("openclash", "config_overwrite", function(s)
if s.name then
local config_value = ""
if s.config then
local config_list = {}
for _, item in ipairs(s.config) do
if item and item ~= "" then
table.insert(config_list, tostring(item))
end
end
if #config_list > 0 then
config_value = config_list
end
end
result[s.name] = {
url = s.url or "",
config = config_value,
update_days = s.update_days or "",
update_hour = s.update_hour or "",
order = tonumber(s.order) or 0,
@@ -3673,7 +3710,7 @@ function action_overwrite_subscribe_info()
return
elseif method == "POST" then
if not section_name then
luci.http.status(400, "Missing filename")
luci.http.status(500, "Missing filename")
return
end
local url = luci.http.formvalue("url") or ""
@@ -3681,6 +3718,16 @@ function action_overwrite_subscribe_info()
local update_hour = luci.http.formvalue("update_hour") or ""
local order = luci.http.formvalue("order")
local param = luci.http.formvalue("param") or ""
local config_values = {}
local raw_config = luci.http.formvalue("config") or ""
if raw_config ~= "" then
for line in raw_config:gmatch("[^\n]+") do
local config_value = line:match("^%s*(.-)%s*$")
if config_value and config_value ~= "" then
table.insert(config_values, config_value)
end
end
end
typ = luci.http.formvalue("type") or typ or "file"
local enable = luci.http.formvalue("enable")
@@ -3713,6 +3760,10 @@ function action_overwrite_subscribe_info()
if s.name == old_section_name then
uci:set("openclash", s[".name"], "name", section_name)
uci:set("openclash", s[".name"], "url", url)
uci:delete("openclash", s[".name"], "config")
if #config_values > 0 then
uci:set_list("openclash", s[".name"], "config", config_values)
end
uci:set("openclash", s[".name"], "update_days", update_days)
uci:set("openclash", s[".name"], "update_hour", update_hour)
uci:set("openclash", s[".name"], "type", typ)
@@ -3754,6 +3805,10 @@ function action_overwrite_subscribe_info()
uci:foreach("openclash", "config_overwrite", function(s)
if s.name == section_name then
uci:set("openclash", s[".name"], "url", url)
uci:delete("openclash", s[".name"], "config")
if #config_values > 0 then
uci:set_list("openclash", s[".name"], "config", config_values)
end
uci:set("openclash", s[".name"], "update_days", update_days)
uci:set("openclash", s[".name"], "update_hour", update_hour)
uci:set("openclash", s[".name"], "type", typ)
@@ -3785,6 +3840,10 @@ function action_overwrite_subscribe_info()
local sid = uci:add("openclash", "config_overwrite")
uci:set("openclash", sid, "name", section_name)
uci:set("openclash", sid, "url", url)
uci:delete("openclash", sid, "config")
if #config_values > 0 then
uci:set_list("openclash", sid, "config", config_values)
end
uci:set("openclash", sid, "update_days", update_days)
uci:set("openclash", sid, "update_hour", update_hour)
uci:set("openclash", sid, "type", typ)
@@ -3835,7 +3894,7 @@ function action_overwrite_subscribe_info()
luci.http.write_json({status="success"})
return
else
luci.http.status(405, "Method Not Allowed")
luci.http.status(500, "Method Not Allowed")
end
end
@@ -3923,7 +3982,7 @@ end
function action_get_subscribe_data()
local filename = luci.http.formvalue("filename")
if not filename then
luci.http.status(400, "Bad Request")
luci.http.status(500, "Bad Request")
return
end
@@ -3941,7 +4000,7 @@ end
function action_get_subscribe_info_data()
local filename = luci.http.formvalue("filename")
if not filename then
luci.http.status(400, "Bad Request")
luci.http.status(500, "Bad Request")
return
end
luci.http.prepare_content("application/json")
@@ -11,7 +11,7 @@ local datatype = require "luci.cbi.datatypes"
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
luci.http.write([[
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
@@ -328,6 +328,13 @@ o = s:taboption("meta", Flag, "enable_unified_delay", font_red..bold_on..transla
o.description = font_red..bold_on..translate("Change The Delay Calculation Method To Remove Extra Delays Such as Handshaking")..bold_off..font_off
o.default = "0"
o = s:taboption("meta", Value, "global_ua", translate("Global User-Agent"))
o:value("0", translate("Disable"))
o:value("clash-verge/v2.4.5")
o:value("clash.meta/1.19.20")
o:value("Clash")
o.default = "0"
o = s:taboption("meta", ListValue, "find_process_mode", translate("Enable Process Rule"))
o.description = translate("Whether to Enable Process Rules, Only Works on Routerself, If You Are Not Sure, Please Choose off Which Useful in Router Environment, Depend on kmod-inet-diag")
o:value("0", translate("Disable"))
@@ -497,15 +504,15 @@ ds.anonymous = true
ds.addremove = true
ds.sortable = true
ds.template = "openclash/tblsection"
ds.extedit = luci.dispatcher.build_url("admin/services/openclash/custom-dns-edit/%s")
ds.extedit = DISP.build_url("admin/services/openclash/custom-dns-edit/%s")
function ds.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "group", name)
end
luci.http.redirect(ds.extedit % sid)
HTTP.redirect(ds.extedit % sid)
return
end
end
@@ -5,6 +5,8 @@ local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local json = require "luci.jsonc"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
font_red = [[<b style=color:red>]]
@@ -22,9 +24,9 @@ m.description=translate("Convert Subscribe function of Online is Supported By su
"<br/>"..translate("If you need to customize the external configuration file (subscription conversion template), please write it according to the instructions, upload it to the accessible location of the external network, and fill in the address correctly when using it")..
"<br/>"..
"<br/>"..translate("If you have a recommended external configuration file (subscription conversion template), you can modify by following The file format of /usr/share/openclash/res/sub_ini.list and pr")
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-subscribe")
m.redirect = DISP.build_url("admin/services/openclash/config-subscribe")
if m.uci:get(openclash, sid) ~= "config_subscribe" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -58,10 +60,10 @@ end
---- UA
o = s:option(Value, "sub_ua", "User-Agent")
o.description = font_red..bold_on..translate("Used for Downloading Subscriptions, Defaults to Clash")..bold_off..font_off
o:value("clash.meta")
o:value("clash-verge/v1.5.1")
o:value("clash-verge/v2.4.5")
o:value("clash.meta/1.19.20")
o:value("Clash")
o.default = "clash.meta"
o.default = "clash-verge/v2.4.5"
o.rmempty = true
---- subconverter
@@ -74,10 +76,10 @@ o = s:option(Value, "convert_address", translate("Convert Address"))
o.rmempty = true
o.description = font_red..bold_on..translate("Note: There is A Risk of Privacy Leakage in Online Convert")..bold_off..font_off
o:depends("sub_convert", "1")
o:value("https://api.wcc.best/sub", translate("api.wcc.best"))
o:value("https://api.asailor.org/sub", translate("api.asailor.org"))
o.default = "https://api.wcc.best/sub"
o.placeholder = "https://api.wcc.best/sub"
o:value("https://api.wcc.best/sub", translate("api.wcc.best"))
o.default = "https://api.asailor.org/sub"
o.placeholder = "https://api.asailor.org/sub"
---- Template
o = s:option(ListValue, "template", translate("Template Name"))
@@ -182,7 +184,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -190,7 +192,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -11,7 +11,7 @@ local uci = require "luci.model.uci".cursor()
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
luci.http.write([[
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
@@ -90,11 +90,11 @@ s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/config-subscribe-edit/%s")
s.extedit = DISP.build_url("admin/services/openclash/config-subscribe-edit/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
HTTP.redirect(s.extedit % sid)
return
end
end
@@ -204,14 +204,14 @@ end
btnis.write=function(a,t)
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..e[t].name)
uci:commit("openclash")
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
btned=tb:option(Button,"edit",translate("Edit"))
btned.inputstyle="apply"
btned.write=function(a,t)
local file_path = "etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "config", "%s") % file_path)
local file_path = "/etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "config") .. "?file=" .. HTTP.urlencode(file_path))
end
btnrn=tb:option(DummyValue,"/etc/openclash/config/",translate("Rename"))
@@ -262,8 +262,8 @@ btnapply.write = function(self, t)
local action = self.map:formvalue("cbid." .. self.map.config .. "." .. t .. ".actions")
if action == "servers_manage" then
local file_path = "etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers", "%s") % file_path)
local file_path = "/etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers") .. "?file=" .. HTTP.urlencode(file_path))
elseif action == "copy" then
local num = 1
while true do
@@ -273,7 +273,7 @@ btnapply.write = function(self, t)
break
end
end
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
elseif action == "download" then
local sPath, sFile, fd, block
sPath = "/etc/openclash/config/"..e[t].name
@@ -3,6 +3,8 @@ local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local SYS = require "luci.sys"
local DISP = require "luci.dispatcher"
local HTTP = require "luci.http"
local sid = arg[1]
font_red = [[<b style=color:red>]]
@@ -13,9 +15,9 @@ bold_off = [[</strong>]]
m = Map(openclash, translate("Add Custom DNS Servers"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-overwrite")
m.redirect = DISP.build_url("admin/services/openclash/config-overwrite")
if m.uci:get(openclash, sid) ~= "dns_servers" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -167,7 +169,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -175,7 +177,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -4,15 +4,14 @@ local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local file_path = ""
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
@@ -33,9 +32,9 @@ end
m = Map(openclash, translate("Edit Group"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers%s" % file_path)
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "groups" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -314,11 +313,11 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
local old_name = m.uci:get(openclash, sid, "old_name") or ""
local new_name = luci.http.formvalue("cbid.openclash." .. sid .. ".name") or m.uci:get(openclash, sid, "name")
local new_name = HTTP.formvalue("cbid.openclash." .. sid .. ".name") or m.uci:get(openclash, sid, "name")
sync_group_name(sid, old_name, new_name)
m.uci:set(openclash, sid, "old_name", new_name)
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -326,7 +325,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -2,19 +2,17 @@ local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local fs = require "luci.openclash"
local file_path = ""
local DISP = require "luci.dispatcher"
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "%s") % arg[1])
return
end
m = Map("openclash", translate("File Edit"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/"..arg[1])
m.redirect = DISP.build_url("admin", "services", "openclash", "other-file-edit", "%s") % arg[1].."?file="..HTTP.urlencode(file_path)
s = m:section(TypedSection, "openclash")
s.anonymous = true
s.addremove=false
@@ -47,14 +45,14 @@ o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
luci.http.redirect(m.redirect)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "%s") % arg[1])
end
m:append(Template("openclash/config_editor"))
@@ -3,16 +3,15 @@ local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local fs = require "luci.openclash"
local file_path = ""
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
@@ -33,9 +32,9 @@ end
m = Map(openclash, translate("Edit Proxy-Provider"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers%s" % file_path)
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "proxy-provider" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -182,7 +181,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -190,7 +189,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -38,8 +38,8 @@ p.inputstyle="apply"
Button.render(p,x,r)
end
btned1.write=function(r,x)
local file_path = "etc/openclash/proxy_provider/" .. fs.basename(p[x].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "proxy-provider-file-manage", "%s") %file_path)
local file_path = "/etc/openclash/proxy_provider/" .. fs.basename(p[x].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "proxy-provider-file-manage") .. "?file=" .. HTTP.urlencode(file_path))
end
btndl1 = tb1:option(Button,"download1",translate("Download Config"))
@@ -38,8 +38,8 @@ g.inputstyle="apply"
Button.render(g,n,h)
end
btned1.write=function(h,n)
local file_path = "etc/openclash/rule_provider/" .. fs.basename(g[n].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "rule-providers-file-manage", "%s") %file_path)
local file_path = "/etc/openclash/rule_provider/" .. fs.basename(g[n].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "rule-providers-file-manage") .. "?file=" .. HTTP.urlencode(file_path))
end
btndl2 = tb2:option(Button,"download2",translate("Download Config"))
@@ -4,16 +4,15 @@ local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local uuid = luci.sys.exec("cat /proc/sys/kernel/random/uuid")
local file_path = ""
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
@@ -108,10 +107,10 @@ local obfs = {
m = Map(openclash, translate("Edit Server"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers%s" % file_path)
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "servers" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -158,6 +157,8 @@ o:value("http", translate("HTTP(S)"))
o:value("direct", translate("DIRECT"))
o:value("dns", translate("DNS"))
o:value("ssh", translate("SSH"))
o:value("masque", translate("MASQUE"))
o:value("trusttunnel", translate("TrustTunnel"))
o.description = translate("Using incorrect encryption mothod may causes service fail to start")
@@ -184,6 +185,8 @@ o:depends("type", "snell")
o:depends("type", "socks5")
o:depends("type", "http")
o:depends("type", "ssh")
o:depends("type", "masque")
o:depends("type", "trusttunnel")
o = s:option(Value, "port", translate("Server Port"))
o.datatype = "port"
@@ -205,6 +208,8 @@ o:depends("type", "snell")
o:depends("type", "socks5")
o:depends("type", "http")
o:depends("type", "ssh")
o:depends("type", "masque")
o:depends("type", "trusttunnel")
o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping"))
o:depends("type", "hysteria")
@@ -508,6 +513,8 @@ o:depends({type = "snell", snell_version = "3"})
o:depends("type", "wireguard")
o:depends("type", "direct")
o:depends("type", "anytls")
o:depends("type", "masque")
o:depends("type", "trusttunnel")
o = s:option(ListValue, "udp_over_tcp", translate("udp-over-tcp"))
o.rmempty = true
@@ -549,6 +556,7 @@ o.default = "tcp"
o:value("tcp", translate("tcp"))
o:value("ws", translate("websocket (ws)"))
o:value("grpc", translate("grpc"))
o:value("xhttp", translate("xhttp"))
o:depends("type", "vless")
o = s:option(ListValue, "obfs_vmess", translate("obfs-mode"))
@@ -637,6 +645,21 @@ o.placeholder = translate("Host: v2ray.com")
o:depends("obfs_vmess", "websocket")
o:depends("obfs_vless", "ws")
o = s:option(Value, "xhttp_opts_path", translate("xhttp-opts-path"))
o.rmempty = true
o.placeholder = translate("/path")
o:depends("obfs_vless", "xhttp")
o = s:option(Value, "xhttp_opts_host", translate("xhttp-opts-host"))
o.rmempty = true
o.placeholder = translate("xxx.com")
o:depends("obfs_vless", "xhttp")
o = s:option(Value, "vless_encryption", translate("encryption"))
o.rmempty = true
o.placeholder = translate("mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(padding len).(padding gap).(X25519 Password).(ML-KEM-768 Client)...")
o:depends("obfs_vless", "tcp")
o = s:option(Value, "vless_flow", translate("flow"))
o.rmempty = true
o.default = "xtls-rprx-direct"
@@ -696,6 +719,7 @@ o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o:depends("type", "tuic")
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ TLS ]]--
o = s:option(ListValue, "tls", translate("TLS"))
@@ -743,6 +767,7 @@ o:depends("type", "http")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ headers ]]--
o = s:option(DynamicList, "http_headers", translate("headers"))
@@ -787,6 +812,7 @@ o:value("h2")
o:value("http/1.1")
o:depends("type", "trojan")
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ alpn ]]--
o = s:option(DynamicList, "hysteria_alpn", translate("alpn"))
@@ -936,7 +962,7 @@ o:value("true")
o:value("false")
o:depends("type", "vmess")
-- [[ AnyTLS ]]--
-- [[ AnyTLS ]] --
o = s:option(Value, "idle_session_check_interval", translate("idle-session-check-interval"))
o.rmempty = true
o.default = "30"
@@ -952,6 +978,70 @@ o.rmempty = true
o.default = "0"
o:depends("type", "anytls")
-- [[ MASQUE ]] --
o = s:option(Value, "masque_private_key", translate("private-key"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_public_key", translate("public-key"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_ip", translate("IP"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_ipv6", translate("IPv6"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_mtu", translate("MTU"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(ListValue, "masque_remote_dns_resolve", translate("Remote DNS Resolve"))
o.rmempty = true
o.default = "true"
o:value("true")
o:value("false")
o:depends("type", "masque")
o = s:option(DynamicList, "masque_dns", translate("DNS"))
o.rmempty = true
o.placeholder = translate("8.8.8.8")
o:depends("type", "masque")
-- [[ TrustTunnel ]] --
o = s:option(Value, "trusttunnel_username", translate("Username"))
o.rmempty = false
o.placeholder = "user"
o:depends("type", "trusttunnel")
o = s:option(Value, "trusttunnel_password", translate("Password"))
o.password = true
o.rmempty = false
o:depends("type", "trusttunnel")
o = s:option(ListValue, "trusttunnel_health_check", translate("Health Check"))
o.rmempty = true
o.default = "true"
o:value("true")
o:value("false")
o:depends("type", "trusttunnel")
o = s:option(ListValue, "trusttunnel_quic", translate("QUIC"))
o.rmempty = true
o.default = "false"
o:value("true")
o:value("false")
o:depends("type", "trusttunnel")
o = s:option(ListValue, "trusttunnel_congestion_controller", translate("Congestion Controller"))
o.rmempty = true
o.default = "bbr"
o:value("bbr")
o:depends("type", "trusttunnel")
-- [[ Fast Open ]]--
o = s:option(ListValue, "fast_open", translate("Fast Open"))
o.rmempty = true
@@ -1010,6 +1100,7 @@ o:depends({type = "vmess", obfs_vmess = "http"})
o:depends({type = "vmess", obfs_vmess = "h2"})
o:depends({type = "vmess", obfs_vmess = "grpc"})
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ ip version ]]--
o = s:option(ListValue, "ip_version", translate("IP Version"))
@@ -1204,7 +1295,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -1212,7 +1303,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -3,18 +3,18 @@ local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local file_path = ""
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local file_path = fs.get_file_path_from_request()
for i = 1, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
return
end
m = Map(openclash, translate("Servers & Groups manage"))
m.pageaction = false
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
m.description=translate("Attention:")..
"<br/>"..translate("1. Before modifying the configuration file, please click the button below to read the configuration file")..
"<br/>"..translate("2. Proxy-providers address can be directly filled in the subscription link")..
@@ -28,15 +28,15 @@ gs.anonymous = true
gs.addremove = true
gs.sortable = true
gs.template = "openclash/tblsection"
gs.extedit = luci.dispatcher.build_url("admin/services/openclash/groups-config/%s"..file_path)
gs.extedit = DISP.build_url("admin/services/openclash/groups-config/%s").."?file="..file_path
function gs.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
luci.http.redirect(gs.extedit % sid)
HTTP.redirect(gs.extedit % sid)
return
end
end
@@ -70,15 +70,15 @@ ps.anonymous = true
ps.addremove = true
ps.sortable = true
ps.template = "openclash/tblsection"
ps.extedit = luci.dispatcher.build_url("admin/services/openclash/proxy-provider-config/%s"..file_path)
ps.extedit = DISP.build_url("admin/services/openclash/proxy-provider-config/%s").."?file="..file_path
function ps.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
luci.http.redirect(ps.extedit % sid)
HTTP.redirect(ps.extedit % sid)
return
end
end
@@ -111,15 +111,15 @@ ss.anonymous = true
ss.addremove = true
ss.sortable = true
ss.template = "openclash/tblsection"
ss.extedit = luci.dispatcher.build_url("admin/services/openclash/servers-config/%s"..file_path)
ss.extedit = DISP.build_url("admin/services/openclash/servers-config/%s").."?file="..file_path
function ss.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
luci.http.redirect(ss.extedit % sid)
HTTP.redirect(ss.extedit % sid)
return
end
end
@@ -223,6 +223,7 @@ o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_get.sh \"%s\" 2>/dev/null" % file_path)
HTTP.redirect(m.redirect)
end
o = a:option(Button, "Commit", " ")
@@ -230,6 +231,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
o = a:option(Button, "Apply", " ")
@@ -238,13 +240,14 @@ o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_set.sh \"%s\" >/dev/null 2>&1 &" % file_path)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "apply"
o.write = function()
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
m:append(Template("openclash/toolbar_show"))
@@ -18,7 +18,7 @@ end
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
luci.http.write([[
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
@@ -265,17 +265,17 @@ o.placeholder = translate("5000 or 1234-2345")
o.rmempty = true
o = s2:option(ListValue, "proto", translate("Proto"))
o:value("both", translate("Both"))
o:value("udp", translate("UDP"))
o:value("tcp", translate("TCP"))
o:value("both", translate("Both"))
o.default = "tcp"
o.default = "both"
o.rmempty = false
o = s2:option(ListValue, "family", translate("Family"))
o:value("both", translate("Both"))
o:value("ipv4", translate("IPv4"))
o:value("ipv6", translate("IPv6"))
o:value("both", translate("Both"))
o.default = "tcp"
o.default = "both"
o.rmempty = false
o = s2:option(ListValue, "interface", translate("Interface"))
@@ -885,6 +885,62 @@ o.template = "openclash/other_stream_option"
o.value = "OpenAI"
o:depends("stream_auto_select_openai", "1")
-- Claude
o = s:taboption("stream_enhance", Flag, "stream_auto_select_claude", font_red..translate("Claude")..font_off)
o.default = 0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_claude", translate("Group Filter"))
o.placeholder = "Claude|AI"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_claude", "1")
o.rmempty = true
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_claude", translate("Unlock Region Filter"))
o.placeholder = "US"
o.description = translate("It Will Be Selected Region(Country Shortcode) According To The Regex")
o:depends("stream_auto_select_claude", "1")
o.rmempty = true
o = s:taboption("stream_enhance", Value, "stream_auto_select_node_key_claude", translate("Unlock Nodes Filter"))
o.description = translate("It Will Be Selected Nodes According To The Regex")
o:depends("stream_auto_select_claude", "1")
o.rmempty = true
o = s:taboption("stream_enhance", DummyValue, "Claude", translate("Manual Test"))
o.rawhtml = true
o.template = "openclash/other_stream_option"
o.value = "Claude"
o:depends("stream_auto_select_claude", "1")
-- Gemini
o = s:taboption("stream_enhance", Flag, "stream_auto_select_gemini", font_red..translate("Gemini")..font_off)
o.default = 0
o:depends("stream_auto_select", "1")
o = s:taboption("stream_enhance", Value, "stream_auto_select_group_key_gemini", translate("Group Filter"))
o.placeholder = "Gemini|AI"
o.description = translate("It Will Be Searched According To The Regex When Auto Search Group Fails")
o:depends("stream_auto_select_gemini", "1")
o.rmempty = true
o = s:taboption("stream_enhance", Value, "stream_auto_select_region_key_gemini", translate("Unlock Region Filter"))
o.placeholder = "US"
o.description = translate("It Will Be Selected Region(Country Shortcode) According To The Regex")
o:depends("stream_auto_select_gemini", "1")
o.rmempty = true
o = s:taboption("stream_enhance", Value, "stream_auto_select_node_key_gemini", translate("Unlock Nodes Filter"))
o.description = translate("It Will Be Selected Nodes According To The Regex")
o:depends("stream_auto_select_gemini", "1")
o.rmempty = true
o = s:taboption("stream_enhance", DummyValue, "Gemini", translate("Manual Test"))
o.rawhtml = true
o.template = "openclash/other_stream_option"
o.value = "Gemini"
o:depends("stream_auto_select_gemini", "1")
---- update Settings
o = s:taboption("geo_update", Flag, "geo_auto_update", font_red..bold_on..translate("Auto Update GeoIP MMDB")..bold_off..font_off)
o.default = 0
@@ -912,9 +968,10 @@ o = s:taboption("geo_update", Value, "geo_custom_url")
o.title = translate("Custom GeoIP MMDB URL")
o.rmempty = true
o.description = translate("Custom GeoIP MMDB URL, Click Button Below To Refresh After Edit")
o:value("https://testingcf.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/lite/Country.mmdb", translate("Alecthw-lite-Version")..translate("(Default mmdb)"))
o:value("https://testingcf.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/Country.mmdb", translate("Alecthw-Version")..translate("(All Info mmdb)"))
o:value("https://testingcf.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/Country.mmdb", translate("Hackl0us-Version")..translate("(Only CN)"))
o:value("https://testingcf.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/lite/Country.mmdb", translate("Alecthw-lite-testingcf-Version")..translate("(Default mmdb)"))
o:value("https://testingcf.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/Country.mmdb", translate("Alecthw-testingcf-Version")..translate("(All Info mmdb)"))
o:value("https://testingcf.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/Country.mmdb", translate("Hackl0us-testingcf-Version")..translate("(Only CN)"))
o:value("https://github.com/alecthw/mmdb_china_ip_list/releases/latest/download/Country-lite.mmdb", translate("Alecthw-lite-github-Version"))
o.default = "https://testingcf.jsdelivr.net/gh/alecthw/mmdb_china_ip_list@release/lite/Country.mmdb"
o:depends("geo_auto_update", "1")
@@ -926,7 +983,7 @@ o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_ipdb.sh >/dev/null 2>&1 &")
SYS.call("/usr/share/openclash/openclash_geo.sh ipdb >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
@@ -958,6 +1015,7 @@ o.rmempty = true
o.description = translate("Custom GeoIP Dat URL, Click Button Below To Refresh After Edit")
o:value("https://testingcf.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat", translate("Loyalsoldier-testingcf-jsdelivr-Version")..translate("(Default)"))
o:value("https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat", translate("Loyalsoldier-fastly-jsdelivr-Version"))
o:value("https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", translate("Loyalsoldier-github-Version"))
o.default = "https://testingcf.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
o:depends("geoip_auto_update", "1")
@@ -969,7 +1027,7 @@ o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_geoip.sh >/dev/null 2>&1 &")
SYS.call("/usr/share/openclash/openclash_geo.sh geoip >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
@@ -1001,6 +1059,7 @@ o.rmempty = true
o.description = translate("Custom GeoSite Data URL, Click Button Below To Refresh After Edit")
o:value("https://testingcf.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat", translate("Loyalsoldier-testingcf-jsdelivr-Version")..translate("(Default)"))
o:value("https://fastly.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat", translate("Loyalsoldier-fastly-jsdelivr-Version"))
o:value("https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", translate("Loyalsoldier-github-Version"))
o.default = "https://testingcf.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
o:depends("geosite_auto_update", "1")
@@ -1012,7 +1071,7 @@ o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_geosite.sh >/dev/null 2>&1 &")
SYS.call("/usr/share/openclash/openclash_geo.sh geosite >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
@@ -1044,6 +1103,7 @@ o.rmempty = true
o.description = translate("Custom Geo ASN Data URL, Click Button Below To Refresh After Edit")
o:value("https://testingcf.jsdelivr.net/gh/xishang0128/geoip@release/GeoLite2-ASN.mmdb", translate("xishang0128-testingcf-jsdelivr-Version")..translate("(Default)"))
o:value("https://fastly.jsdelivr.net/gh/xishang0128/geoip@release/GeoLite2-ASN.mmdb", translate("xishang0128-fastly-jsdelivr-Version"))
o:value("https://github.com/xishang0128/geoip/releases/latest/download/GeoLite2-ASN.mmdb", translate("xishang0128-github-Version"))
o.default = "https://testingcf.jsdelivr.net/gh/xishang0128/geoip@release/GeoLite2-ASN.mmdb"
o:depends("geoasn_auto_update", "1")
@@ -1055,7 +1115,7 @@ o.inputstyle = "reload"
o.write = function()
m.uci:set("openclash", "config", "enable", 1)
m.uci:commit("openclash")
SYS.call("/usr/share/openclash/openclash_geoasn.sh >/dev/null 2>&1 &")
SYS.call("/usr/share/openclash/openclash_geo.sh geoasn >/dev/null 2>&1 &")
HTTP.redirect(DISP.build_url("admin", "services", "openclash"))
end
@@ -1088,6 +1148,7 @@ o:value("https://ispip.clang.cn/all_cn.txt", translate("Clang-CN")..translate("(
o:value("https://ispip.clang.cn/all_cn_cidr.txt", translate("Clang-CN-CIDR"))
o:value("https://fastly.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/CN-ip-cidr.txt", translate("Hackl0us-CN-CIDR-fastly-jsdelivr"))
o:value("https://testingcf.jsdelivr.net/gh/Hackl0us/GeoIP2-CN@release/CN-ip-cidr.txt", translate("Hackl0us-CN-CIDR-testingcf-jsdelivr"))
o:value("https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/direct-list.txt", translate("Loyalsoldier-github-Version"))
o.default = "https://ispip.clang.cn/all_cn.txt"
o = s:taboption("chnr_update", Value, "chnr6_custom_url")
+21
View File
@@ -31,6 +31,7 @@ local fs = require "nixio.fs"
local nutil = require "nixio.util"
local uci = require "luci.model.uci".cursor()
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local type = type
local string = string
@@ -335,4 +336,24 @@ function uci_get_config(section, key)
val = uci:get("openclash", section, key)
end
return val
end
function get_file_path_from_request()
local file_path
local referer = HTTP.getenv("HTTP_REFERER")
if referer then
local _, _, file_value = referer:find("file=([^&]*)$")
if file_value and file_value ~= "" then
file_path = HTTP.urldecode(file_value)
end
end
if not file_path or file_path == "/" then
file_path = HTTP.formvalue("file")
if not file_path then
file_path = HTTP.urldecode(file_path)
end
end
return file_path
end
@@ -51,7 +51,7 @@
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 10000;
z-index: 5;
display: none;
align-items: center;
justify-content: center;
@@ -110,6 +110,12 @@
white-space: nowrap;
flex: 1 1 auto;
}
.oc .config-editor-title #editTitle,
.oc .config-editor-title #config-file-name {
user-select: text;
-webkit-user-select: text;
cursor: text;
}
.oc .config-editor-title .config-file-name {
color: var(--primary-color);
font-weight: 700;
@@ -362,6 +368,149 @@
min-width: 0;
box-sizing: border-box;
}
.oc .overwrite-config-dropdown {
position: relative;
width: 100%;
}
.oc .overwrite-config-dropdown-btn {
width: 100%;
min-height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
cursor: pointer;
font-size: 13px;
line-height: 1.4;
}
.oc .overwrite-config-dropdown.form-select-wrapper {
width: 100%;
}
.oc .overwrite-config-dropdown-btn.form-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.oc .overwrite-config-dropdown-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100% - 20px);
text-align: left;
}
.oc .overwrite-config-dropdown-arrow {
margin-left: 8px;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid var(--text-secondary);
flex-shrink: 0;
}
.oc .overwrite-config-dropdown-panel {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 220px;
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
box-shadow: var(--shadow-md);
z-index: 10003;
}
.oc .overwrite-config-dropdown.open .overwrite-config-dropdown-panel {
display: block;
}
.oc .overwrite-config-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-white);
color: var(--text-primary);
cursor: pointer;
}
.oc .overwrite-config-option:last-child {
border-bottom: none;
}
.oc .overwrite-config-option:hover {
background: var(--primary-color);
color: var(--select-hover);
}
.oc[data-darkmode="true"] .overwrite-config-option {
background: var(--bg-gray);
border-bottom-color: var(--border-light);
}
.oc[data-darkmode="true"] .overwrite-config-option:hover {
background: var(--primary-color);
color: var(--select-hover);
}
.oc .overwrite-config-option:hover .overwrite-config-option-state {
border-color: var(--select-hover);
}
.oc[data-darkmode="true"] .overwrite-config-option-state {
background: var(--bg-white);
border-color: var(--border-color);
}
.oc .overwrite-config-option.disabled-by-all {
cursor: not-allowed;
opacity: 0.55;
background: var(--bg-gray);
color: var(--text-secondary);
}
.oc .overwrite-config-option.disabled-by-all:hover {
background: var(--bg-gray);
color: var(--text-secondary);
}
.oc .overwrite-config-option.disabled-by-all .overwrite-config-option-state {
border-color: var(--border-light);
color: transparent;
}
.oc .overwrite-config-option-left {
display: flex;
align-items: center;
min-width: 0;
flex: 1;
}
.oc .overwrite-config-option-left input[type="checkbox"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.oc .overwrite-config-option-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.oc .overwrite-config-option-state {
flex-shrink: 0;
width: 16px;
height: 16px;
border: 1px solid var(--border-color);
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
color: transparent;
background: var(--bg-white);
font-size: 12px;
line-height: 1;
}
.oc .overwrite-config-option.selected .overwrite-config-option-state {
border-color: var(--primary-color);
background: var(--primary-color);
color: var(--select-hover);
}
.oc .overwrite-config-dropdown.disabled .overwrite-config-dropdown-btn {
opacity: 0.6;
cursor: not-allowed;
}
.oc .overwrite-card-row {
display: flex;
flex-wrap: nowrap;
@@ -1407,6 +1556,11 @@ var ConfigEditor = {
.then(function(data) {
if (data.status === 'success') {
self.overwriteSubInfo = (data && data.data) ? data.data : {};
Object.keys(self.overwriteSubInfo).forEach(function(key) {
var subInfo = self.overwriteSubInfo[key] || {};
subInfo.config = self.parseOverwriteConfigValue(subInfo.config);
self.overwriteSubInfo[key] = subInfo;
});
self.overwriteFiles.sort(function(a, b) {
var an = a.name || (a.path ? a.path.split('/').pop() : '');
var bn = b.name || (b.path ? b.path.split('/').pop() : '');
@@ -1530,6 +1684,7 @@ var ConfigEditor = {
formData.append('update_hour', sub.update_hour || '');
formData.append('order', (typeof sub.order !== 'undefined' && sub.order !== null && sub.order !== '') ? sub.order : idx);
formData.append('param', sub.param || '');
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
formData.append('enable', newEnable);
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
method: 'POST',
@@ -1590,6 +1745,7 @@ var ConfigEditor = {
formData.append('update_hour', sub.update_hour || '');
formData.append('order', (typeof sub.order !== 'undefined' && sub.order !== null && sub.order !== '') ? sub.order : idx);
formData.append('param', sub.param || '');
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
formData.append('enable', typeof sub.enable !== 'undefined' ? sub.enable : 1);
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
method: 'POST',
@@ -2116,6 +2272,7 @@ var ConfigEditor = {
formData.append('update_days', sub.update_days || '');
formData.append('update_hour', sub.update_hour || '');
formData.append('param', sub.param || '');
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
formData.append('order', idx);
reqs.push(fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
method: 'POST',
@@ -2127,12 +2284,225 @@ var ConfigEditor = {
});
},
getOverwriteConfigFiles: function() {
var rawList = [];
if (window.configFiles && Array.isArray(window.configFiles)) {
rawList = window.configFiles;
} else if (window.ConfigFileManager && Array.isArray(window.ConfigFileManager.configList)) {
rawList = window.ConfigFileManager.configList;
}
return rawList.map(function(file) {
if (typeof file === 'string') {
return {
name: file,
path: file
};
}
return {
name: file.name || file.filename || file.path || '',
path: file.path || file.filepath || file.name || ''
};
}).filter(function(file) {
return !!file.path;
});
},
parseOverwriteConfigValue: function(value) {
if (!value) return [];
if (Array.isArray(value)) {
return value.filter(function(item) { return !!item; });
}
return String(value).split(/[\r\n,;]+/).map(function(item) {
return item.trim();
}).filter(function(item) {
return !!item;
});
},
renderOverwriteConfigDropdown: function(configValue, dropdownId) {
var files = this.getOverwriteConfigFiles();
var selected = this.parseOverwriteConfigValue(configValue);
var selectedMap = {};
selected.forEach(function(item) {
selectedMap[item] = true;
});
if (!selected.length) {
selectedMap['all'] = true;
}
var allChecked = !!selectedMap['all'];
var allOptionHtml = `
<label class="overwrite-config-option${allChecked ? ' selected' : ''}" data-path="all" data-all-option="1">
<span class="overwrite-config-option-left">
<input type="checkbox" value="all"${allChecked ? ' checked' : ''}>
<span class="overwrite-config-option-name" title="<%:Use For All Config File%>"><%:Use For All Config File%></span>
</span>
<span class="overwrite-config-option-state">${allChecked ? '✓' : ''}</span>
</label>
`;
var fileOptionsHtml = files.length ? files.map(function(file) {
var checked = (!allChecked && (selectedMap[file.path] || selectedMap[file.name])) ? ' checked' : '';
var selectedClass = checked ? ' selected' : '';
var disabledClass = allChecked ? ' disabled-by-all' : '';
var stateText = checked ? '✓' : '';
var disabled = allChecked;
return `
<label class="overwrite-config-option${selectedClass}${disabledClass}" data-path="${file.path}">
<span class="overwrite-config-option-left">
<input type="checkbox" value="${file.path}"${checked}${disabled ? ' disabled' : ''}>
<span class="overwrite-config-option-name" title="${file.name}">${file.name}</span>
</span>
<span class="overwrite-config-option-state">${stateText}</span>
</label>
`;
}).join('') : '<div class="overwrite-config-option"><span class="overwrite-config-option-name"><%:No config files found%></span></div>';
var optionsHtml = allOptionHtml + fileOptionsHtml;
return `
<div class="overwrite-config-dropdown form-select-wrapper" id="${dropdownId}">
<button type="button" class="overwrite-config-dropdown-btn form-select">
<span class="overwrite-config-dropdown-text"><%:Use For All Config File%></span>
<span class="overwrite-config-dropdown-arrow"></span>
</button>
<div class="overwrite-config-dropdown-panel">
${optionsHtml}
</div>
</div>
`;
},
updateOverwriteConfigDropdownLabel: function(dropdown) {
if (!dropdown) return;
var textEl = dropdown.querySelector('.overwrite-config-dropdown-text');
if (!textEl) return;
var checked = dropdown.querySelectorAll('.overwrite-config-option input[type="checkbox"]:checked');
if (!checked.length) {
textEl.textContent = '<%:Use For All Config File%>';
return;
}
if (checked.length === 1) {
var oneName = checked[0].closest('.overwrite-config-option').querySelector('.overwrite-config-option-name');
textEl.textContent = oneName ? oneName.textContent : '<%:1 file selected%>';
return;
}
textEl.textContent = checked.length + ' <%:files selected%>';
},
bindOverwriteConfigDropdown: function(container) {
if (!container || container.dataset.inited === '1') return;
container.dataset.inited = '1';
var self = this;
var btn = container.querySelector('.overwrite-config-dropdown-btn');
var panel = container.querySelector('.overwrite-config-dropdown-panel');
function syncAllExclusiveState() {
var allInput = container.querySelector('.overwrite-config-option input[type="checkbox"][value="all"]');
var allOption = allInput ? allInput.closest('.overwrite-config-option') : null;
var allState = allOption ? allOption.querySelector('.overwrite-config-option-state') : null;
var allSelected = !!(allInput && allInput.checked);
if (allOption) {
allOption.classList.toggle('selected', allSelected);
}
if (allState) {
allState.textContent = allSelected ? '✓' : '';
}
container.querySelectorAll('.overwrite-config-option input[type="checkbox"]').forEach(function(input) {
if (input.value === 'all') return;
var option = input.closest('.overwrite-config-option');
var state = option ? option.querySelector('.overwrite-config-option-state') : null;
if (allSelected) {
input.checked = false;
}
input.disabled = allSelected;
if (option) {
option.classList.toggle('selected', input.checked);
option.classList.toggle('disabled-by-all', allSelected);
option.setAttribute('aria-disabled', allSelected ? 'true' : 'false');
}
if (state) {
state.textContent = input.checked ? '✓' : '';
}
});
}
this.updateOverwriteConfigDropdownLabel(container);
syncAllExclusiveState();
this.updateOverwriteConfigDropdownLabel(container);
if (btn && !btn.disabled) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
container.classList.toggle('open');
});
}
container.querySelectorAll('.overwrite-config-option input[type="checkbox"]').forEach(function(input) {
input.addEventListener('change', function() {
if (input.value === 'all') {
syncAllExclusiveState();
} else if (input.checked) {
var allInput = container.querySelector('.overwrite-config-option input[type="checkbox"][value="all"]');
if (allInput) {
allInput.checked = false;
}
syncAllExclusiveState();
}
var option = input.closest('.overwrite-config-option');
var state = option ? option.querySelector('.overwrite-config-option-state') : null;
if (option) {
option.classList.toggle('selected', input.checked);
}
if (state) {
state.textContent = input.checked ? '✓' : '';
}
self.updateOverwriteConfigDropdownLabel(container);
});
});
if (panel) {
panel.addEventListener('click', function(e) {
e.stopPropagation();
});
}
document.addEventListener('click', function(e) {
if (!container.contains(e.target)) {
container.classList.remove('open');
}
});
},
getOverwriteConfigSelection: function(root, dropdownId) {
var dropdown = root.querySelector('#' + dropdownId);
if (!dropdown) return [];
var selected = Array.from(dropdown.querySelectorAll('.overwrite-config-option input[type="checkbox"]:checked')).map(function(input) {
return input.value;
}).filter(function(value) {
return !!value;
});
if (selected.indexOf('all') !== -1) {
return ['all'];
}
return selected;
},
renderOverwriteSubForm: function(options) {
var name = options.name || '';
var sub = options.sub || {};
var readonly = !!options.readonly;
var showTabs = !!options.showTabs;
var activeTab = options.activeTab || 'file';
var fileConfigDropdown = this.renderOverwriteConfigDropdown(sub.config || '', 'overwrite-upload-config-dropdown');
var subscribeConfigDropdown = this.renderOverwriteConfigDropdown(sub.config || '', 'overwrite-subscribe-config-dropdown');
var tabsHtml = showTabs ? `
<div class="upload-mode-selector">
@@ -2160,6 +2530,10 @@ var ConfigEditor = {
var fileContent = `
<form id="overwrite-upload-form-file" style="display:${activeTab==='file'?'block':'none'};">
<div class="form-group" style="margin-bottom:20px;">
<label for="overwrite-upload-config"><%:Config File%>:</label>
${fileConfigDropdown}
</div>
<div class="upload-zone" id="overwrite-upload-zone">
<div class="upload-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
@@ -2187,6 +2561,10 @@ var ConfigEditor = {
<label><%:File Name%>:</label>
<input type="text" class="form-input" name="filename" id="overwrite-subscribe-filename" value="${name}" ${readonly ? 'readonly' : ''} placeholder="<%:Please enter a filename%>">
</div>
<div class="form-group">
<label for="overwrite-subscribe-config"><%:Config File%>:</label>
${subscribeConfigDropdown}
</div>
<div class="form-group">
<label><%:Type%>:</label>
<div class="form-select-wrapper">
@@ -2299,6 +2677,9 @@ var ConfigEditor = {
ocDiv.appendChild(overlay);
document.body.appendChild(ocDiv);
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-upload-config-dropdown'));
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-subscribe-config-dropdown'));
var tabFile = model.querySelector('#overwrite-upload-mode-file');
var tabSub = model.querySelector('#overwrite-upload-mode-subscribe');
var contentFile = model.querySelector('#overwrite-upload-content-file');
@@ -2357,11 +2738,7 @@ var ConfigEditor = {
model.querySelector('#overwrite-add-close').onclick = function() {
document.body.removeChild(ocDiv);
};
overlay.onclick = function(e) {
if (e.target === overlay) {
document.body.removeChild(ocDiv);
}
};
model.querySelector('#overwrite-upload-cancel').onclick = function() {
document.body.removeChild(ocDiv);
};
@@ -2439,11 +2816,13 @@ var ConfigEditor = {
var reader = new FileReader();
reader.onload = function(e) {
var fileContent = e.target.result;
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-upload-config-dropdown');
var formData = new FormData();
formData.append('filename', filename);
formData.append('config_file', fileContent);
formData.append('order', self.overwriteFileCardCount + 1);
formData.append('enable', '0');
formData.append('config', selectedConfigPaths.join('\n'));
fetch('/cgi-bin/luci/admin/services/openclash/upload_overwrite', {
method: 'POST',
@@ -2479,6 +2858,7 @@ var ConfigEditor = {
var update_hour = form.querySelector('#overwrite-subscribe-update-hour').value;
var type = form.querySelector('#overwrite-subscribe-type').value;
var param = form.querySelector('#overwrite-subscribe-param').value.trim();
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-subscribe-config-dropdown');
if (!filename) {
alert('<%:Please enter a filename%>');
return;
@@ -2501,6 +2881,7 @@ var ConfigEditor = {
formData.append('param', param);
formData.append('order', self.overwriteFileCardCount + 1);
formData.append('enable', '0');
formData.append('config', selectedConfigPaths.join('\n'));
if (type === 'http') {
formData.append('url', url);
formData.append('update_days', update_days);
@@ -2603,6 +2984,8 @@ var ConfigEditor = {
ocDiv.appendChild(overlay);
document.body.appendChild(ocDiv);
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-subscribe-config-dropdown'));
var typeSelect = model.querySelector('#overwrite-subscribe-type');
var urlGroup = model.querySelector('#overwrite-subscribe-url-group');
var updateGroup = model.querySelector('#overwrite-subscribe-update-group');
@@ -2663,6 +3046,7 @@ var ConfigEditor = {
var type = form.querySelector('#overwrite-subscribe-type').value;
var url = form.querySelector('#overwrite-subscribe-url').value.trim();
var param = form.querySelector('#overwrite-subscribe-param').value.trim();
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-subscribe-config-dropdown');
if (!newName) {
alert('<%:Please enter a filename%>');
@@ -2689,6 +3073,8 @@ var ConfigEditor = {
formData.delete('update_days');
formData.delete('update_hour');
}
formData.delete('config');
formData.append('config', selectedConfigPaths.join('\n'));
if (newName !== name) {
formData.append('old_filename', name);
}
@@ -2761,6 +3147,13 @@ var ConfigEditor = {
}
this.hideMergeView();
this.hide();
if (window.OverwriteSubscribeManager && typeof window.OverwriteSubscribeManager.load === 'function') {
window.OverwriteSubscribeManager.load(true);
}
try {
window.dispatchEvent(new Event('oc-overwrite-updated'));
} catch (e) {}
},
updateZoom: function(newZoom) {
@@ -2800,6 +3193,29 @@ var ConfigEditor = {
this.updateZoom(100);
},
isPointOnTextContent: function(el, clientX, clientY) {
if (!el) return false;
var textWalker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
var textNode;
while ((textNode = textWalker.nextNode())) {
if (!textNode.nodeValue || !textNode.nodeValue.trim()) {
continue;
}
var range = document.createRange();
range.selectNodeContents(textNode);
var rects = range.getClientRects();
for (var i = 0; i < rects.length; i++) {
var rect = rects[i];
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
return true;
}
}
}
return false;
},
makeDraggable: function() {
var self = this;
var header = this.model.querySelector('.config-editor-header');
@@ -2807,7 +3223,12 @@ var ConfigEditor = {
var isDragging = false;
header.addEventListener('mousedown', function(e) {
if (e.target.closest('.config-editor-actions')) {
var target = e.target && e.target.nodeType === 1 ? e.target : e.target.parentElement;
if (target && target.closest('.config-editor-actions')) {
return;
}
var textEl = target ? target.closest('#editTitle, #config-file-name') : null;
if (textEl && self.isPointOnTextContent(textEl, e.clientX, e.clientY)) {
return;
}
@@ -2864,7 +3285,13 @@ var ConfigEditor = {
}
header.addEventListener('touchstart', function(e) {
if (e.target.closest('.config-editor-actions')) {
var target = e.target && e.target.nodeType === 1 ? e.target : e.target.parentElement;
if (target && target.closest('.config-editor-actions')) {
return;
}
var touch = e.touches && e.touches[0] ? e.touches[0] : null;
var textEl = target ? target.closest('#editTitle, #config-file-name') : null;
if (touch && textEl && self.isPointOnTextContent(textEl, touch.clientX, touch.clientY)) {
return;
}
if (e.touches.length !== 1) return;
@@ -214,7 +214,7 @@
padding: 10px 12px;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
background: var(--bg-white) !important;
color: var(--text-primary);
font-size: 14px;
transition: all var(--transition-fast);
@@ -258,6 +258,10 @@
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.oc .form-textarea::placeholder {
color: var(--text-secondary);
}
.oc .form-row {
display: flex;
gap: 16px;
@@ -553,6 +557,10 @@
box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.1);
}
.oc[data-darkmode="true"] .form-textarea::placeholder {
color: var(--text-secondary);
}
.oc[data-darkmode="true"] .checkmark {
border-color: var(--border-light);
background: var(--bg-gray);
@@ -752,8 +760,8 @@
<label for="subscribe-ua-input"><%:User-Agent%> (<%:Optional%>):</label>
<div class="form-select-wrapper">
<select id="subscribe-ua-input" class="form-select">
<option value="clash.meta">clash.meta</option>
<option value="clash-verge/v1.5.1">clash-verge/v1.5.1</option>
<option value="clash-verge/v2.4.5">clash-verge/v2.4.5</option>
<option value="clash.meta/1.19.20">clash.meta/1.19.20</option>
<option value="Clash">clash</option>
<option value="custom"><%:Custom%></option>
</select>
@@ -785,8 +793,8 @@
<label for="convert-address-input"><%:Convert Address%>:</label>
<div class="form-select-wrapper">
<select id="convert-address-input" class="form-select">
<option value="https://api.wcc.best/sub">api.wcc.best</option>
<option value="https://api.asailor.org/sub">api.asailor.org</option>
<option value="https://api.wcc.best/sub">api.wcc.best</option>
<option value="custom"><%:Custom%></option>
</select>
</div>
@@ -1073,6 +1081,7 @@ var ConfigUploader = {
} else {
advancedOptionsContainer.style.display = 'none';
self.resetAdvancedOptions();
self.updateSubmitButton();
}
});
@@ -1082,6 +1091,7 @@ var ConfigUploader = {
} else {
subConvertOptions.style.display = 'none';
}
self.updateSubmitButton();
});
convertAddressSelect.addEventListener('change', function() {
@@ -1120,12 +1130,6 @@ var ConfigUploader = {
}
});
this.overlay.addEventListener('click', function(e) {
if (e.target === self.overlay && !self.isProcessing) {
self.hide();
}
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && !self.isProcessing && self.overlay.classList.contains('show')) {
self.hide();
@@ -1197,7 +1201,7 @@ var ConfigUploader = {
this.switchMode('file');
document.getElementById('config-filename-input').value = '';
document.getElementById('subscribe-url-input').value = '';
document.getElementById('subscribe-ua-input').value = 'clash.meta';
document.getElementById('subscribe-ua-input').value = 'clash-verge/v2.4.5';
document.getElementById('subscribe-ua-custom').style.display = 'none';
document.getElementById('advanced-options-enable').checked = false;
@@ -1226,7 +1230,7 @@ var ConfigUploader = {
document.getElementById('subscribe-url-input').value = data.address;
}
if (data.sub_ua) {
if (data.sub_ua === 'clash.meta' || data.sub_ua === 'clash-verge/v1.5.1' || data.sub_ua === 'Clash' || data.sub_ua === 'custom') {
if (data.sub_ua === 'clash.meta/1.19.20' || data.sub_ua === 'clash-verge/v2.4.5' || data.sub_ua === 'Clash' || data.sub_ua === 'custom') {
document.getElementById('subscribe-ua-input').value = data.sub_ua;
if (data.sub_ua === 'custom') {
document.getElementById('subscribe-ua-custom').style.display = 'block';
@@ -1390,10 +1394,10 @@ var ConfigUploader = {
updateSubmitButton: function() {
var filename = document.getElementById('config-filename-input').value.trim();
var submitBtn = document.getElementById('config-upload-submit');
var isValid = false;
var isValidFormat = false;
if (this.currentMode === 'file') {
isValid = this.selectedFile && filename;
isValidFormat = this.selectedFile && filename;
} else if (this.currentMode === 'subscribe') {
var url = document.getElementById('subscribe-url-input').value.trim();
var advancedEnabled = document.getElementById('advanced-options-enable').checked;
@@ -1401,25 +1405,29 @@ var ConfigUploader = {
if (url && filename) {
if (subConvert) {
var linkPattern = /^(?:https?:\/\/|[a-zA-Z]+:\/\/|[^,\s]+,https?:\/\/.+|[^%\s]+%2Chttps?%3A%2F%2F.+)$/i;
if (url.indexOf('\n') !== -1 || url.indexOf('|') !== -1) {
var links = url.indexOf('\n') !== -1 ? url.split('\n') : url.split('|');
var links = url.split(/[\n|]/);
for (var i = 0; i < links.length; i++) {
var link = links[i].trim();
if (link && (/^https?:\/\//.test(link) || /^[a-zA-Z]+:\/\//.test(link))) {
isValid = true;
if (link && linkPattern.test(link)) {
isValidFormat = true;
break;
}
}
} else {
isValid = /^https?:\/\//.test(url) || /^[a-zA-Z]+:\/\//.test(url);
isValidFormat = linkPattern.test(url);
}
} else {
isValid = /^https?:\/\//.test(url) && url.indexOf('\n') === -1 && url.indexOf('|') === -1;
if (/^https?:\/\//.test(url) && url.indexOf('\n') === -1 && url.indexOf('|') === -1) {
isValidFormat = true;
}
}
}
}
submitBtn.disabled = !isValid || this.isProcessing;
submitBtn.disabled = !isValidFormat || this.isProcessing;
},
uploadFile: function() {
@@ -1562,7 +1570,7 @@ var ConfigUploader = {
}
if (userAgent === 'custom') {
userAgent = subscribeUaCustom.value.trim() || 'clash.meta';
userAgent = subscribeUaCustom.value.trim();
}
if (!url || !filename) {
@@ -1573,25 +1581,19 @@ var ConfigUploader = {
var isValidFormat = false;
if (subConvert) {
if (url.indexOf('\n') !== -1 || url.indexOf('|') !== -1) {
var links = [];
if (url.indexOf('\n') !== -1) {
links = url.split('\n');
} else {
links = url.split('|');
}
var linkPattern = /^(?:https?:\/\/|[a-zA-Z]+:\/\/|[^,\s]+,https?:\/\/.+|[^%\s]+%2Chttps?%3A%2F%2F.+)$/i;
if (url.indexOf('\n') !== -1 || url.indexOf('|') !== -1) {
var links = url.split(/[\n|]/);
for (var i = 0; i < links.length; i++) {
var link = links[i].trim();
if (link && (/^https?:\/\//.test(link) || /^[a-zA-Z]+:\/\//.test(link))) {
if (link && linkPattern.test(link)) {
isValidFormat = true;
break;
}
}
} else {
if (/^https?:\/\//.test(url) || /^[a-zA-Z]+:\/\//.test(url)) {
isValidFormat = true;
}
isValidFormat = linkPattern.test(url);
}
} else {
if (/^https?:\/\//.test(url) && url.indexOf('\n') === -1 && url.indexOf('|') === -1) {
@@ -824,6 +824,8 @@ function poll_log(){
var currentContent = "";
var bufferToProcess = [];
clearTimeout(r);
for(var i=0; i<titles.length; i++){
if(titles[i].className === 'cbi-tab') {
activeTabId = i;
@@ -933,7 +933,7 @@
}
else {
if (!refresh_http) {
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
}
if (!refresh_ip) {
if (use_router_mode) {
@@ -1074,7 +1074,7 @@
get_router_ip_info();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("get_router_ip_info()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -1097,7 +1097,7 @@
myip_Load();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("myip_Load()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -1249,7 +1249,7 @@
if (use_router_mode) {
get_router_ip_info();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("get_router_ip_info()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -1268,7 +1268,7 @@
myip_Load();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("myip_Load()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -25,7 +25,7 @@ function act_manual_unlock_test_<%=stream_id_name%>(btn, type)
{
manual_<%=stream_id_name%>_type_legend.style.display = 'none';
manual_<%=stream_id_name%>_type_output.innerHTML = '<textarea id="manual_<%=stream_id_name%>_type_output" class="cbi-input-textarea" style="width: 100%;display:inline" data-update="change" rows="10" cols="50" readonly="readonly" >'+x.responseText+'</textarea>';
log_area(document.getElementById("manual_<%=stream_id_name%>_type_output"), 'manual_stream_unlock_test', 'true', manual_<%=stream_id_name%>_type_output.offsetWidth, '250px');
log_editor(document.getElementById("manual_<%=stream_id_name%>_type_output"), 'manual_stream_unlock_test', 'true', manual_<%=stream_id_name%>_type_output.offsetWidth, '250px');
}
else
{
@@ -6,6 +6,7 @@
--text-primary: #374151;
--text-secondary: #64748b;
--text-title: #4d4d4d;
--select-hover: #ebebeb;
--border-color: #b1b1b1;
--border-light: #e2e8f0;
--hover-bg: #f8fafc;
@@ -27,6 +28,7 @@
--control-height: 36px;
--card-padding: 6px 8px;
--version-right: 8px;
--gap-size: 16px;
}
@@ -37,6 +39,7 @@
--text-primary: #ebebeb;
--text-secondary: #d0cfcf;
--text-title: #e5e7eb;
--select-hover: #ebebeb;
--border-color: #939393;
--border-light: #6b7280;
--hover-bg: #374151;
@@ -62,7 +65,7 @@
border-radius: var(--radius-lg);
}
.select-popup {
.oc .select-popup {
position: fixed;
top: 50%;
left: 50%;
@@ -77,9 +80,11 @@
width: 60%;
box-shadow: var(--shadow-md);
transition: all var(--transition-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.close-btn {
.oc .close-btn {
background: none;
border: 1px solid var(--border-light);
color: var(--text-secondary);
@@ -97,7 +102,7 @@
margin-left: 8px;
}
.close-btn:hover {
.oc .close-btn:hover {
background: var(--hover-bg) !important;
border-color: var(--primary-color) !important;
color: var(--primary-color) !important;
@@ -105,23 +110,23 @@
box-shadow: var(--shadow-sm) !important;
}
.close-btn:focus {
.oc .close-btn:focus {
outline: none;
box-shadow: var(--shadow-sm) !important;
border-color: var(--primary-color) !important;
}
.close-btn svg {
.oc .close-btn svg {
width: 14px !important;
height: 14px !important;
flex-shrink: 0 !important;
}
.select-popup.hidden {
.oc .select-popup.hidden {
display: none;
}
.select-popup-header {
.oc .select-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -135,7 +140,7 @@
box-shadow: var(--shadow-sm);
}
.select-config-section {
.oc .select-config-section {
background: var(--bg-gray);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
@@ -144,14 +149,14 @@
box-shadow: var(--shadow-sm);
}
.config-grid {
.oc .config-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--gap-size);
align-items: center;
}
.config-item {
.oc .config-item {
display: flex;
flex-direction: column;
gap: 8px;
@@ -160,7 +165,7 @@
position: relative;
}
.config-item::after {
.oc .config-item::after {
content: '';
position: absolute;
right: 12px;
@@ -175,7 +180,7 @@
z-index: 1;
}
.config-editor-title {
.oc .config-editor-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
@@ -187,7 +192,7 @@
flex: 1;
}
.config-label {
.oc .config-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
@@ -195,7 +200,7 @@
position: relative;
}
.config-label::after {
.oc .config-label::after {
content: '';
position: absolute;
bottom: -4px;
@@ -206,7 +211,7 @@
background-color: var(--border-color);
}
.select-class {
.oc .select-class {
width: 100% !important;
height: var(--control-height);
padding: 8px 32px 8px 12px;
@@ -222,17 +227,17 @@
min-width: 0;
}
.select-class:hover {
.oc .select-class:hover {
border-color: var(--primary-color);
}
.select-class:focus {
.oc .select-class:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.help-link {
.oc .help-link {
display: inline-flex;
align-items: center;
color: var(--primary-color);
@@ -243,7 +248,7 @@
border-radius: var(--radius-sm);
}
.select-class option {
.oc .select-class option {
width: 100%;
padding: 8px 12px;
font-size: 13px;
@@ -255,12 +260,12 @@
color: var(--text-primary);
}
.help-link:hover {
.oc .help-link:hover {
opacity: 1;
background: rgba(59, 130, 246, 0.1);
}
.select-popup-body {
.oc .select-popup-body {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-light);
@@ -268,7 +273,7 @@
background: var(--bg-gray);
}
.select-option {
.oc .select-option {
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid var(--border-light);
@@ -281,31 +286,31 @@
flex-direction: row;
}
.select-option span {
.oc .select-option span {
white-space: normal;
word-break: break-all;
}
.select-option:last-child {
.oc .select-option:last-child {
border-bottom: none;
}
.select-option:hover {
.oc .select-option:hover {
background-color: var(--hover-bg);
color: var(--primary-color);
}
.select-option[data-value="custom"] {
.oc .select-option[data-value="custom"] {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(99, 102, 241, 0.1));
border-color: rgba(59, 130, 246, 0.2);
font-weight: 500;
}
.select-option[data-value="custom"]:hover {
.oc .select-option[data-value="custom"]:hover {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.2));
}
.custom-option-input {
.oc .custom-option-input {
margin: 12px auto;
padding: 12px;
width: calc(100% - 24px) !important;
@@ -317,18 +322,18 @@
transition: all var(--transition-fast);
}
.custom-option-input:focus {
.oc .custom-option-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.custom-option-input::placeholder {
.oc .custom-option-input::placeholder {
color: var(--text-secondary);
opacity: 0.7;
}
#addCustomOption {
.oc #addCustomOption {
background: var(--primary-color);
color: white;
font-weight: 500;
@@ -338,12 +343,12 @@
text-align: center;
}
#addCustomOption:hover {
.oc #addCustomOption:hover {
background: #2563eb;
color: white;
}
.cdn-status {
.oc .cdn-status {
font-size: 12px;
font-weight: 500;
padding: 4px 8px;
@@ -352,45 +357,45 @@
white-space: nowrap !important;
}
.cdn-status.fast {
.oc .cdn-status.fast {
background: rgba(5, 150, 105, 0.1);
color: var(--success-color);
}
.cdn-status.medium {
.oc .cdn-status.medium {
background: rgba(245, 158, 11, 0.1);
color: var(--warning-color);
}
.cdn-status.slow {
.oc .cdn-status.slow {
background: rgba(220, 38, 38, 0.1);
color: var(--error-color);
}
.cdn-status.error {
.oc .cdn-status.error {
background: rgba(220, 38, 38, 0.1);
color: var(--error-color);
}
@media screen and (max-width: 1200px) {
.config-grid {
.oc .config-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.config-item {
.oc .config-item {
width: 100%;
max-width: 100%;
}
.select-class {
.oc .select-class {
max-width: 100%;
font-size: 13px;
}
}
@media screen and (max-width: 768px) {
.select-popup {
.oc .select-popup {
min-width: 80%;
width: 80%;
padding: 15px;
@@ -398,76 +403,76 @@
overflow-y: auto;
}
.config-grid {
.oc .config-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.config-item {
.oc .config-item {
text-align: center;
}
.select-popup-header {
.oc .select-popup-header {
font-size: 16px;
margin-bottom: 16px;
padding-bottom: 12px;
}
.select-config-section {
.oc .select-config-section {
padding: 12px;
margin-bottom: 16px;
}
.select-popup-body {
.oc .select-popup-body {
max-height: 200px;
}
.select-option {
.oc .select-option {
padding: 10px 12px;
font-size: 13px;
align-items: flex-start;
gap: 4px;
}
.custom-option-input {
.oc .custom-option-input {
font-size: 13px;
padding: 10px;
}
}
@media screen and (max-width: 575px) {
.select-popup {
.oc .select-popup {
min-width: 80%;
width: 80%;
padding: 12px;
}
.select-popup-header {
.oc .select-popup-header {
font-size: 15px;
margin-bottom: 12px;
}
.select-config-section {
.oc .select-config-section {
padding: 10px;
}
.config-label {
.oc .config-label {
font-size: 12px;
}
.select-class {
.oc .select-class {
font-size: 12px;
padding: 6px 28px 6px 10px;
}
.config-item::after {
.oc .config-item::after {
right: 10px;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 3px solid var(--text-secondary);
}
.select-option {
.oc .select-option {
padding: 8px 10px;
font-size: 12px;
}
@@ -546,97 +551,99 @@
</style>
<body>
<div id="selectPopup" class="oc select-popup hidden">
<div class="select-popup-header">
<div class="config-editor-title">
<%:Check Update%>
</div>
<div class="config-editor-actions">
<button type="button" class="icon-btn close-btn" onclick="closeSelectPopup()" title="<%:Close%>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="select-config-section">
<div class="config-grid">
<div class="config-item">
<div class="config-label"><%:Compiled Version%></div>
<select class="select-class" id="CORE_VERSION_CDN">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64-v1"><%:linux-amd64-v1(x86-64)%></option>
<option value="linux-amd64-v2"><%:linux-amd64-v2(x86-64)%></option>
<option value="linux-amd64-v3"><%:linux-amd64-v3(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-arm64"><%:linux-arm64(armv8)%></option>
<option value="linux-loong64-abi1"><%:linux-loong64-abi1%></option>
<option value="linux-loong64-abi2"><%:linux-loong64-abi2%></option>
<option value="linux-riscv64"><%:linux-riscv64%></option>
<option value="linux-s390x"><%:linux-s390x%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select>
<div class="oc">
<div id="selectPopup" class="select-popup hidden">
<div class="select-popup-header">
<div class="config-editor-title">
<%:Check Update%>
</div>
<div class="config-item">
<div class="config-label"><%:Release Branch%></div>
<select class="select-class" id="RELEASE_BRANCH_CDN">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select>
<div class="config-editor-actions">
<button type="button" class="icon-btn close-btn" onclick="closeSelectPopup()" title="<%:Close%>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="select-config-section">
<div class="config-grid">
<div class="config-item">
<div class="config-label"><%:Compiled Version%></div>
<select class="select-class" id="CORE_VERSION_CDN">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64-v1"><%:linux-amd64-v1(x86-64)%></option>
<option value="linux-amd64-v2"><%:linux-amd64-v2(x86-64)%></option>
<option value="linux-amd64-v3"><%:linux-amd64-v3(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-arm64"><%:linux-arm64(armv8)%></option>
<option value="linux-loong64-abi1"><%:linux-loong64-abi1%></option>
<option value="linux-loong64-abi2"><%:linux-loong64-abi2%></option>
<option value="linux-riscv64"><%:linux-riscv64%></option>
<option value="linux-s390x"><%:linux-s390x%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select>
</div>
<div class="config-item">
<div class="config-label">
<%:Smart Core%>
<a href="javascript:void(0);" onclick="window.open('https://github.com/vernesong/mihomo/releases', '_blank');" class="help-link" title="<%:View core infos that support smart group%>">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</a>
<div class="config-item">
<div class="config-label"><%:Release Branch%></div>
<select class="select-class" id="RELEASE_BRANCH_CDN">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select>
</div>
<div class="config-item">
<div class="config-label">
<%:Smart Core%>
<a href="javascript:void(0);" onclick="window.open('https://github.com/vernesong/mihomo/releases', '_blank');" class="help-link" title="<%:View core infos that support smart group%>">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</a>
</div>
<select class="select-class" id="SMART_ENABLE_CDN">
<option value="0"><%:Disabled%></option>
<option value="1"><%:Enable%></option>
</select>
</div>
<select class="select-class" id="SMART_ENABLE_CDN">
<option value="0"><%:Disabled%></option>
<option value="1"><%:Enable%></option>
</select>
</div>
</div>
</div>
<div class="select-popup-body">
<div class="select-option" data-value="https://raw.githubusercontent.com/">
<span>https://raw.githubusercontent.com/ (<%:RAW address%>)</span>
<div class="select-popup-body">
<div class="select-option" data-value="https://raw.githubusercontent.com/">
<span>https://raw.githubusercontent.com/ (<%:RAW address%>)</span>
</div>
<div class="select-option" data-value="https://fastly.jsdelivr.net/">
<span>https://fastly.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://testingcf.jsdelivr.net/">
<span>https://testingcf.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://cdn.jsdelivr.net/">
<span>https://cdn.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="custom">
<span><%:Custom Your CDN URL%></span>
<span style="display: flex; align-items: center; height: 100%;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display: block;">
<polygon points="6,9 18,9 12,15"/>
</svg>
</span>
</div>
<input type="text" id="customOptionInput" class="custom-option-input" value="https://ghfast.top/" placeholder="<%:Type CDN URL, Format Like%> https://ghfast.top/" style="display: none;">
<div class="select-option" id="addCustomOption" style="display: none;"><%:Add%></div>
</div>
<div class="select-option" data-value="https://fastly.jsdelivr.net/">
<span>https://fastly.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://testingcf.jsdelivr.net/">
<span>https://testingcf.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://cdn.jsdelivr.net/">
<span>https://cdn.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="custom">
<span><%:Custom Your CDN URL%></span>
<span style="display: flex; align-items: center; height: 100%;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display: block;">
<polygon points="6,9 18,9 12,15"/>
</svg>
</span>
</div>
<input type="text" id="customOptionInput" class="custom-option-input" value="https://ghfast.top/" placeholder="<%:Type CDN URL, Format Like%> https://ghfast.top/" style="display: none;">
<div class="select-option" id="addCustomOption" style="display: none;"><%:Add%></div>
</div>
</div>
</body>
File diff suppressed because it is too large. Load diff
@@ -8,6 +8,7 @@
--text-primary: #374151;
--text-secondary: #64748b;
--text-title: #4d4d4d;
--select-hover: #ebebeb;
--border-color: #b1b1b1;
--border-light: #e2e8f0;
--hover-bg: #f8fafc;
@@ -26,9 +27,13 @@
--radius-lg: 8px;
--transition-fast: 0.15s ease;
--transition-normal: 0.3s ease;
--stripe-color: rgba(255,255,255,0.24);
--stripe-opacity: 0.5;
--stripe-size: 40px;
--control-height: 32px;
--card-padding: 6px 8px;
--version-right: 8px;
--gap-size: 10px;
--row-1-height: 160px;
@@ -44,6 +49,7 @@
--text-primary: #ebebeb;
--text-secondary: #d0cfcf;
--text-title: #e5e7eb;
--select-hover: #ebebeb;
--border-color: #939393;
--border-light: #6b7280;
--hover-bg: #374151;
@@ -55,6 +61,8 @@
--warning-color: #fa41c8;
--watchdog-color: #c147f5;
--info-color: #3b82f6;
--stripe-color: rgba(0, 0, 0, 0.255);
--stripe-opacity: 0.8;
}
.oc[data-darkmode="true"] .plugin-toggle-slider {
@@ -294,6 +302,13 @@
box-sizing: border-box;
}
.oc .sub-card:hover {
background: var(--hover-bg, #f3f4f6);
border-color: var(--primary-color, #3b82f6);
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
}
.oc .sub-card.stat-item {
display: flex;
flex-direction: column;
@@ -332,7 +347,7 @@
.oc .version-display-container {
position: absolute;
right: 10px;
right: var(--version-right);
display: flex;
gap: 8px;
z-index: 2;
@@ -906,12 +921,13 @@
min-height: 24px;
min-width: 0;
gap: 8px;
padding: 0 20px;
}
.oc .subscription-info-details {
display: flex;
flex-direction: column;
gap: 8px;
gap: 5px;
flex: 1;
min-height: 80px;
padding-left: 20px;
@@ -937,6 +953,30 @@
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
position: relative;
overflow: hidden;
}
.oc .subscription-progress-fill::before {
content: "";
position: absolute;
inset: 0;
background-image: linear-gradient(45deg, var(--stripe-color) 25%, transparent 25%, transparent 50%, var(--stripe-color) 50%, var(--stripe-color) 75%, transparent 75%, transparent);
background-size: var(--stripe-size) var(--stripe-size);
animation: oc-stripes 2s linear infinite;
opacity: var(--stripe-opacity);
pointer-events: none;
mix-blend-mode: overlay;
border-radius: 4px;
}
@keyframes oc-stripes {
to { background-position: 40px 0; }
}
.oc[data-darkmode="true"] .subscription-progress-fill::before {
background-image: linear-gradient(45deg, var(--stripe-color) 25%, transparent 25%, transparent 50%, var(--stripe-color) 50%, var(--stripe-color) 75%, transparent 75%, transparent);
opacity: var(--stripe-opacity);
}
.oc .subscription-progress-fill.high {
@@ -962,20 +1002,115 @@
display: block;
}
.oc .subscription-actions-row {
width: 100%;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
.oc .subscription-overwrite-tags {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
margin-right: auto;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
padding-bottom: 2px;
}
.oc .subscription-overwrite-tags::-webkit-scrollbar {
display: none;
}
.oc .subscription-overwrite-tags.drag-scroll-enabled {
cursor: grab;
user-select: none;
}
.oc .subscription-overwrite-tags.dragging {
cursor: grabbing;
}
.oc .subscription-overwrite-tag {
height: 28px;
width: fit-content;
padding: 0 8px;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
white-space: nowrap;
word-break: keep-all;
line-height: 1;
box-sizing: border-box;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm);
}
.oc .subscription-overwrite-tag.type-http {
color: var(--primary-color);
border-color: var(--primary-color);
}
.oc .subscription-overwrite-tag.type-file {
color: var(--success-color);
border-color: var(--success-color);
}
.oc .subscription-overwrite-tag.type-unknown {
color: var(--warning-color);
border-color: var(--warning-color);
}
.oc .subscription-action-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.oc .subscription-info-text {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.4;
text-overflow: ellipsis;
overflow: hidden;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
display: block;
scrollbar-width: none;
-ms-overflow-style: none;
cursor: default;
user-select: text;
-webkit-overflow-scrolling: touch;
}
.oc .subscription-info-text::-webkit-scrollbar {
display: none;
}
.oc .subscription-info-text.is-draggable {
cursor: grab;
user-select: none;
}
.oc .subscription-info-text.dragging {
cursor: grabbing;
}
/* Multiple Providers Grid Layout */
.oc .subscription-providers-grid {
display: flex;
gap: 12px;
gap: 10px;
padding: 8px 0;
max-width: 100%;
width: 100%;
@@ -1106,10 +1241,22 @@
text-overflow: ellipsis;
min-width: 0;
max-width: 100%;
padding-left: 20px;
overflow: hidden;
}
.oc .config-file-name .selected-file-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
vertical-align: middle;
margin-right: 0.2em;
margin-bottom: 0.2em;
background: var(--primary-color);
flex-shrink: 0;
animation: gentlePulse 4s cubic-bezier(.17,.67,.83,.67) infinite;
}
.oc .config-file-bottom {
display: flex;
align-items: center;
@@ -1178,13 +1325,20 @@
min-width: 0;
max-width: 30vw;
overflow: hidden;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
}
.oc .file-info:hover {
background: var(--hover-bg);
color: var(--text-primary);
}
.oc .file-info-item {
display: block;
align-items: center;
white-space: nowrap;
padding-right: 20px;
padding: 2px 6px;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
@@ -1528,6 +1682,13 @@
min-width: 24px;
}
.oc .subscription-overwrite-tag {
height: 24px;
width: fit-content;
font-size: 10px;
padding: 0 6px;
}
.oc .icon-btn svg {
width: 12px !important;
height: 12px !important;
@@ -1906,21 +2067,24 @@
</div>
<div id="subscription-info-text" class="subscription-info-text"><%:Collecting data...%></div>
</div>
<div class="card-actions">
<button type="button" class="icon-btn action-btn" id="refresh-subscription" title="<%:Refresh%>" onclick="return refreshSubscriptionInfo()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button type="button" class="icon-btn action-btn" id="config-subscription-url" title="<%:Specify URL%>" onclick="return setSubscriptionUrl()">
<svg width="14" height="14" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="4">
<path d="M4 6H44V36H29L24 41L19 36H4V6Z"/>
<path d="M23 21H25.0025" />
<path d="M33.001 21H34.9999"/>
<path d="M13.001 21H14.9999"/>
</svg>
</button>
<div class="card-actions subscription-actions-row">
<div id="subscription-overwrite-tags" class="subscription-overwrite-tags" style="display: none;"></div>
<div class="subscription-action-buttons">
<button type="button" class="icon-btn action-btn" id="refresh-subscription" title="<%:Refresh%>" onclick="return refreshSubscriptionInfo()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button type="button" class="icon-btn action-btn" id="config-subscription-url" title="<%:Specify URL%>" onclick="return setSubscriptionUrl()">
<svg width="14" height="14" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="4">
<path d="M4 6H44V36H29L24 41L19 36H4V6Z"/>
<path d="M23 21H25.0025" />
<path d="M33.001 21H34.9999"/>
<path d="M13.001 21H14.9999"/>
</svg>
</button>
</div>
</div>
</div>
</div>
@@ -2528,6 +2692,7 @@
this.rawCurrentConfig = currentConfigFile;
this.configList = configFiles;
window.configFiles = configFiles;
this.currentConfig = currentConfigFile;
this.currentConfigIndex = -1;
@@ -2716,9 +2881,7 @@
var displayName = this.formatDisplayName(configFile);
if (configFile === this.rawCurrentConfig) {
configNameElement.innerHTML =
'<svg width="0.6em" height="0.6em" viewBox="0 0 24 24" fill="var(--primary-color)" style="vertical-align:middle;margin-right:0.2em;margin-bottom:0.2em;">' +
'<circle cx="12" cy="12" r="8" fill="var(--primary-color)"/>' +
'</svg>' + displayName;
'<span class="selected-file-dot"></span>' + displayName;
} else {
configNameElement.textContent = displayName;
}
@@ -2829,12 +2992,14 @@
}
SubscriptionManager.getSubscriptionInfo();
}
OverwriteSubscribeManager.render(OverwriteSubscribeManager.data);
} else {
this.currentConfig = '';
this.currentConfigIndex = -1;
SubscriptionManager.currentConfigFile = '';
this.hideSubscriptionDisplay();
this.updateNavigationArrows();
OverwriteSubscribeManager.render(OverwriteSubscribeManager.data);
}
},
@@ -2913,6 +3078,8 @@
SubscriptionManager.getSubscriptionInfo();
}
OverwriteSubscribeManager.render(OverwriteSubscribeManager.data);
return true;
},
@@ -2936,14 +3103,112 @@
updateTimer: null,
isInitialized: false,
enableOverflowDrag: function(element) {
if (!element) return;
function updateDragState() {
var isOverflowing = element.scrollWidth > element.clientWidth + 1;
if (isOverflowing) {
element.classList.add('is-draggable');
} else {
element.classList.remove('is-draggable');
element.classList.remove('dragging');
element.scrollLeft = 0;
}
}
if (!element._dragBound) {
var isDragging = false;
var startX = 0;
var startScrollLeft = 0;
var touchIdentifier = null;
function endDrag() {
if (!isDragging) return;
isDragging = false;
touchIdentifier = null;
element.classList.remove('dragging');
}
element.addEventListener('mousedown', function(e) {
if (!element.classList.contains('is-draggable')) return;
isDragging = true;
startX = e.pageX - element.getBoundingClientRect().left;
startScrollLeft = element.scrollLeft;
element.classList.add('dragging');
e.preventDefault();
});
element.addEventListener('mousemove', function(e) {
if (!isDragging) return;
var x = e.pageX - element.getBoundingClientRect().left;
var walk = (x - startX) * 1.2;
element.scrollLeft = startScrollLeft - walk;
e.preventDefault();
});
element.addEventListener('mouseleave', function() {
endDrag();
});
element.addEventListener('mouseup', function() {
endDrag();
});
element.addEventListener('touchstart', function(e) {
if (!element.classList.contains('is-draggable')) return;
if (!e.touches || e.touches.length !== 1) return;
var touch = e.touches[0];
isDragging = true;
touchIdentifier = touch.identifier;
startX = touch.pageX - element.getBoundingClientRect().left;
startScrollLeft = element.scrollLeft;
element.classList.add('dragging');
}, { passive: true });
element.addEventListener('touchmove', function(e) {
if (!isDragging || !e.touches || e.touches.length === 0) return;
var touch = null;
for (var i = 0; i < e.touches.length; i++) {
if (e.touches[i].identifier === touchIdentifier) {
touch = e.touches[i];
break;
}
}
if (!touch) return;
var x = touch.pageX - element.getBoundingClientRect().left;
var walk = (x - startX) * 1.2;
element.scrollLeft = startScrollLeft - walk;
e.preventDefault();
}, { passive: false });
element.addEventListener('touchend', function() {
endDrag();
});
element.addEventListener('touchcancel', function() {
endDrag();
});
element.addEventListener('dragstart', function(e) {
e.preventDefault();
});
element._dragBound = true;
}
updateDragState();
},
init: function() {
if (this.isInitialized) return;
this.isInitialized = true;
setTimeout(function() {
SubscriptionManager.loadSubscriptionInfo();
SubscriptionManager.startAutoUpdate();
}, 500);
SubscriptionManager.loadSubscriptionInfo();
SubscriptionManager.startAutoUpdate();
},
loadSubscriptionInfo: function() {
@@ -3010,8 +3275,8 @@
}
var needsErrorHandling = false;
if (x && x.status == 200 && status.providers) {
var newProvidersLength = (status.providers) ? status.providers.length : 0;
if (x && x.status == 200 && status.providers.length > 0) {
var newProvidersLength = status.providers.length;
var cachedProvidersLength = (parsedData && parsedData.providers) ? parsedData.providers.length : 0;
if (newProvidersLength !== cachedProvidersLength && cachedProvidersLength > newProvidersLength) {
@@ -3023,17 +3288,18 @@
localStorage.setItem('sub_info_' + filename, JSON.stringify(status));
if (status.providers && status.providers.length === 0) {
SubscriptionManager.showNoInfo();
return;
}
SubscriptionManager.displaySubscriptionInfo(status);
return;
}
} else {
needsErrorHandling = true;
if (!cachedData) {
SubscriptionManager.showNoInfo();
}
}
if (needsErrorHandling) {
SubscriptionManager.handleError(cachedData);
SubscriptionManager.handleError(status);
}
}, true);
}
@@ -3112,6 +3378,7 @@
infoText.textContent = infoString;
infoText.title = tooltipString;
this.enableOverflowDrag(infoText);
},
displayMultipleProviders: function(providers, progressSection) {
@@ -3169,6 +3436,7 @@
infoText.textContent = infoString;
infoText.title = tooltipString;
SubscriptionManager.enableOverflowDrag(infoText);
card.appendChild(infoText);
progressSection.appendChild(card);
@@ -3298,21 +3566,23 @@
}
},
handleError: function(cachedData) {
if (!cachedData) {
this.showNoInfo();
}
handleError: function(status) {
if (this.retryCount >= this.maxRetries) {
this.retryCount = 0;
if (this.currentConfigFile && cachedData) {
localStorage.removeItem('sub_info_' + this.extractFilename(this.currentConfigFile));
if (this.currentConfigFile && status && status.providers) {
localStorage.setItem('sub_info_' + filename, JSON.stringify(status));
}
if (!status.providers || status.providers.length === 0) {
this.showNoInfo();
}
if (status.providers && status.providers.length > 0) {
SubscriptionManager.displaySubscriptionInfo(status);
}
} else {
this.retryCount++;
setTimeout(function() {
SubscriptionManager.getSubscriptionInfo();
}, 15000);
}, 5000);
}
},
@@ -3375,26 +3645,18 @@
bindEvents: function() {
var textarea = document.getElementById('subscription-url-textarea');
var overlay = document.getElementById('subscription-url-overlay');
if (textarea) {
textarea.addEventListener('keydown', this.handleKeyDown.bind(this));
}
if (overlay) {
overlay.addEventListener('click', this.handleOverlayClick.bind(this));
}
},
unbindEvents: function() {
var textarea = document.getElementById('subscription-url-textarea');
var overlay = document.getElementById('subscription-url-overlay');
if (textarea) {
textarea.removeEventListener('keydown', this.handleKeyDown.bind(this));
}
if (overlay) {
overlay.removeEventListener('click', this.handleOverlayClick.bind(this));
}
},
handleKeyDown: function(event) {
@@ -3405,12 +3667,6 @@
}
},
handleOverlayClick: function(event) {
if (event.target.id === 'subscription-url-overlay') {
this.hide();
}
},
submit: function() {
var textarea = document.getElementById('subscription-url-textarea');
if (!textarea) return;
@@ -3478,6 +3734,251 @@
}
};
var OverwriteSubscribeManager = {
container: null,
data: null,
isLoading: false,
_dragBound: false,
_isDragging: false,
_dragStartX: 0,
_dragStartScrollLeft: 0,
_touchIdentifier: null,
init: function() {
this.container = document.getElementById('subscription-overwrite-tags');
if (!this.container) return;
this.bindDragScroll();
this.load(false);
},
hasOverflow: function() {
if (!this.container) return false;
return this.container.scrollWidth > this.container.clientWidth + 1;
},
updateDragScrollState: function() {
if (!this.container) return;
if (this.hasOverflow()) {
this.container.classList.add('drag-scroll-enabled');
} else {
this.container.classList.remove('drag-scroll-enabled');
this.container.classList.remove('dragging');
}
},
bindDragScroll: function() {
if (!this.container || this._dragBound) {
return;
}
var self = this;
function endDrag() {
if (!self._isDragging) return;
self._isDragging = false;
self._touchIdentifier = null;
self.container.classList.remove('dragging');
}
this.container.addEventListener('mousedown', function(e) {
if (!self.hasOverflow()) {
return;
}
self._isDragging = true;
self._dragStartX = e.pageX - self.container.offsetLeft;
self._dragStartScrollLeft = self.container.scrollLeft;
self.container.classList.add('dragging');
e.preventDefault();
});
this.container.addEventListener('mousemove', function(e) {
if (!self._isDragging) {
return;
}
var x = e.pageX - self.container.offsetLeft;
var walk = (x - self._dragStartX) * 1.5;
self.container.scrollLeft = self._dragStartScrollLeft - walk;
e.preventDefault();
});
this.container.addEventListener('mouseleave', function() {
endDrag();
});
this.container.addEventListener('mouseup', function() {
endDrag();
});
this.container.addEventListener('touchstart', function(e) {
if (!self.hasOverflow() || !e.touches || e.touches.length !== 1) {
return;
}
var touch = e.touches[0];
self._isDragging = true;
self._touchIdentifier = touch.identifier;
self._dragStartX = touch.pageX - self.container.offsetLeft;
self._dragStartScrollLeft = self.container.scrollLeft;
self.container.classList.add('dragging');
}, { passive: true });
this.container.addEventListener('touchmove', function(e) {
if (!self._isDragging || !e.touches || e.touches.length === 0) {
return;
}
var touch = null;
for (var i = 0; i < e.touches.length; i++) {
if (e.touches[i].identifier === self._touchIdentifier) {
touch = e.touches[i];
break;
}
}
if (!touch) {
return;
}
var x = touch.pageX - self.container.offsetLeft;
var walk = (x - self._dragStartX) * 1.5;
self.container.scrollLeft = self._dragStartScrollLeft - walk;
e.preventDefault();
}, { passive: false });
this.container.addEventListener('touchend', function() {
endDrag();
});
this.container.addEventListener('touchcancel', function() {
endDrag();
});
this.container.addEventListener('dragstart', function(e) {
e.preventDefault();
});
this._dragBound = true;
},
load: function(force) {
if (!this.container || this.isLoading) {
return;
}
this.isLoading = true;
var self = this;
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "overwrite_subscribe_info")%>', function(x, status) {
self.isLoading = false;
if (x && x.status == 200 && status && status.status === 'success' && status.data) {
self.data = status.data;
self.render(status.data);
} else {
self.render(null);
}
}, !!force);
},
getTypeClass: function(type) {
var normalizedType = String(type || '').toLowerCase();
if (normalizedType === 'http') return 'type-http';
if (normalizedType === 'file') return 'type-file';
return 'type-unknown';
},
parseModuleConfigList: function(configValue) {
if (!Array.isArray(configValue)) {
return [];
}
return configValue.map(function(item) {
return String(item || '').trim();
}).filter(function(item) {
return !!item;
});
},
isModuleForCurrentConfig: function(moduleInfo, currentConfigPath) {
var configList = this.parseModuleConfigList(moduleInfo ? moduleInfo.config : null);
if (!configList.length) {
return true;
}
if (configList.indexOf('all') !== -1) {
return true;
}
var fullPath = String(currentConfigPath || '').trim();
if (!fullPath) {
return false;
}
return configList.indexOf(fullPath) !== -1;
},
getEnabledModules: function(data) {
if (!data || typeof data !== 'object') {
return [];
}
var currentConfigPath = ConfigFileManager.getCurrentConfig() || ConfigFileManager.getSelectedConfig();
var modules = [];
for (var moduleName in data) {
if (!Object.prototype.hasOwnProperty.call(data, moduleName)) {
continue;
}
var moduleInfo = data[moduleName] || {};
if (parseInt(moduleInfo.enable, 10) === 1 && this.isModuleForCurrentConfig(moduleInfo, currentConfigPath)) {
modules.push({
name: moduleName,
type: moduleInfo.type || 'unknown',
order: parseInt(moduleInfo.order, 10) || 0
});
}
}
modules.sort(function(a, b) {
if (a.order === b.order) {
return String(a.name).localeCompare(String(b.name));
}
return a.order - b.order;
});
return modules;
},
render: function(data) {
if (!this.container) return;
var enabledModules = this.getEnabledModules(data);
this.container.innerHTML = '';
if (enabledModules.length === 0) {
this.container.style.display = 'none';
this.updateDragScrollState();
return;
}
for (var i = 0; i < enabledModules.length; i++) {
var moduleItem = enabledModules[i];
var tag = document.createElement('span');
tag.className = 'subscription-overwrite-tag ' + this.getTypeClass(moduleItem.type);
tag.title = moduleItem.name;
tag.textContent = moduleItem.name;
if (moduleItem.type == 'http') {
tag.title += ' [<%:HTTP Module%>]';
} else if (moduleItem.type == 'file') {
tag.title += ' [<%:File Module%>]';
} else {
tag.title += ' [<%:Unknown%>]';
}
this.container.appendChild(tag);
}
this.container.style.display = 'flex';
this.updateDragScrollState();
}
};
var LogManager = {
isPolling: false,
pollTimer: null,
@@ -4029,7 +4530,12 @@
DarkModeDetector.init();
ConfigFileManager.init();
window.addEventListener('oc-overwrite-updated', function() {
OverwriteSubscribeManager.load(true);
});
setTimeout(function() {
OverwriteSubscribeManager.init();
SubscriptionManager.init();
}, 300);
@@ -4745,7 +5251,7 @@
}
function telegrampage() {
url6 = 'https://t.me/ctcgfw_openwrt_discuss';
url6 = 'https://t.me/openclash_group';
winOpen(url6);
}
@@ -5326,6 +5832,7 @@
var filename = SubscriptionManager.extractFilename(currentConfig);
localStorage.removeItem('sub_info_' + filename);
SubscriptionManager.getSubscriptionInfo();
OverwriteSubscribeManager.load(true);
return false;
}
@@ -5506,4 +6013,4 @@
DarkModeDetector.init();
}
//]]></script>
</html>
</html>
@@ -789,9 +789,9 @@ function sub_info_refresh_<%=idname%>(force) {
var save_info = (localStorage.getItem("sub_info_<%=filename%>")) ? JSON.parse(localStorage.getItem("sub_info_<%=filename%>")) : null;
var shouldFetchNew = true;
if (save_info && !force) {
if (!force) {
dispaly_progressbar('<%=idname%>', save_info);
if (save_info.get_time) {
if (save_info && save_info.get_time) {
var currentTime = Math.floor(Date.now() / 1000);
var cacheTime = parseInt(save_info.get_time);
var timeDiff = currentTime - cacheTime;
@@ -803,12 +803,6 @@ function sub_info_refresh_<%=idname%>(force) {
}
}
if (retry_<%=idname%> >= 3) {
localStorage.removeItem("sub_info_<%=filename%>");
retry_<%=idname%> = 0;
shouldFetchNew = false;
}
if (!shouldFetchNew) {
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 60000*15);
return;
@@ -821,9 +815,9 @@ function sub_info_refresh_<%=idname%>(force) {
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "sub_info_get")%>', {filename: "<%=filename%>"}, function(x, status) {
var needsErrorHandling = false;
if (x && x.status == 200 && status.providers) {
if (x && x.status == 200 && status.providers.length > 0) {
var save_info_length = (save_info && save_info.providers) ? save_info.providers.length : 0;
var newProvidersLength = (status && status.providers) ? status.providers.length : 0;
var newProvidersLength = status.providers.length;
if (save_info_length !== newProvidersLength && save_info_length > newProvidersLength) {
needsErrorHandling = true;
}
@@ -839,8 +833,13 @@ function sub_info_refresh_<%=idname%>(force) {
if (needsErrorHandling && retry_<%=idname%> <= 2) {
retry_<%=idname%>++;
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 15000);
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 5000);
} else {
retry_<%=idname%> = 0;
dispaly_progressbar('<%=idname%>', status);
if (status && status.providers) {
localStorage.setItem("sub_info_<%=filename%>", JSON.stringify(status));
}
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 60000*15);
};
});
@@ -1,148 +1,110 @@
<%+cbi/valueheader%>
<div class="cbi-value-field" id="switch_dashboard_<%=self.option%>">
<%:Collecting data...%>
</div>
<div class="cbi-value-field" id="delete_dashboard_<%=self.option%>">
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, '<%=self.option%>')"/>
</div>
<div class="cbi-value-field" id="default_dashboard_<%=self.option%>">
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Set to Default%>" onclick="return default_dashboard(this, '<%=self.option%>')"/>
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 6px;">
<div class="cbi-value-field" id="switch_dashboard_<%=self.option%>"><%:Collecting data...%></div>
<div class="cbi-value-field" id="delete_dashboard_<%=self.option%>">
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, '<%=self.option%>')"/>
</div>
<div class="cbi-value-field" id="default_dashboard_<%=self.option%>">
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Set to Default%>" onclick="return default_dashboard(this, '<%=self.option%>')"/>
</div>
</div>
<script type="text/javascript">//<![CDATA[
var btn_type_<%=self.option%> = "<%=self.option%>";
var switch_dashboard_<%=self.option%> = document.getElementById('switch_dashboard_<%=self.option%>');
var default_dashboard_<%=self.option%> = document.getElementById('default_dashboard_<%=self.option%>');
var delete_dashboard_<%=self.option%> = document.getElementById('delete_dashboard_<%=self.option%>');
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "dashboard_type")%>', null, function(x, status) {
if ( x && x.status == 200 ) {
if ( btn_type_<%=self.option%> == "Dashboard" ) {
if ( status.dashboard_type == "Meta" ) {
switch_dashboard_<%=self.option%>.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch To Official Version%>" onclick="return switch_dashboard(this, btn_type_<%=self.option%>, \'Official\')"/>';
}
else {
switch_dashboard_<%=self.option%>.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch To Meta Version%>" onclick="return switch_dashboard(this, btn_type_<%=self.option%>, \'Meta\')"/>';
}
}
if ( btn_type_<%=self.option%> == "Yacd" ) {
if ( status.yacd_type == "Meta" ) {
switch_dashboard_<%=self.option%>.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch To Official Version%>" onclick="return switch_dashboard(this, btn_type_<%=self.option%>, \'Official\')"/>';
}
else {
switch_dashboard_<%=self.option%>.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch To Meta Version%>" onclick="return switch_dashboard(this, btn_type_<%=self.option%>, \'Meta\')"/>';
}
}
if ( btn_type_<%=self.option%> == "Metacubexd" ) {
switch_dashboard_<%=self.option%>.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Update Metacubexd Version%>" onclick="return switch_dashboard(this, btn_type_<%=self.option%>, \'Official\')"/>';
}
if ( btn_type_<%=self.option%> == "Zashboard" ) {
switch_dashboard_<%=self.option%>.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Update Zashboard Version%>" onclick="return switch_dashboard(this, btn_type_<%=self.option%>, \'Official\')"/>';
}
(function() {
var name = "<%=self.option%>";
if ( status.default_dashboard == btn_type_<%=self.option%>.toLowerCase() ) {
default_dashboard_<%=self.option%>.innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Default%>" disabled="disabled" onclick="return default_dashboard(this, btn_type_<%=self.option%>)"/>';
}
function makeBtn(label, fn, args) {
var argStr = args.map(function(a){ return "'" + a + "'"; }).join(', ');
return '<input type="button" class="btn cbi-button cbi-button-apply" value="' + label + '" onclick="return ' + fn + '(this, ' + argStr + ')"/>';
}
if ( !status[btn_type_<%=self.option%>.toLowerCase()] ) {
default_dashboard_<%=self.option%>.firstElementChild.disabled = true;
delete_dashboard_<%=self.option%>.firstElementChild.disabled = true;
}
var switchEl = document.getElementById('switch_dashboard_' + name);
var defaultEl = document.getElementById('default_dashboard_' + name);
var deleteEl = document.getElementById('delete_dashboard_' + name);
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "dashboard_type")%>', null, function(x, status) {
if (!x || x.status != 200) return;
var switchLabel;
if (name == "Metacubexd") {
switchLabel = ['<%:Update Metacubexd Version%>', 'Official'];
} else if (name == "Zashboard") {
switchLabel = ['<%:Update Zashboard Version%>', 'Official'];
} else {
var isMeta = (name == "Dashboard" ? status.dashboard_type : status.yacd_type) == "Meta";
switchLabel = isMeta
? ['<%:Switch To Official Version%>', 'Official']
: ['<%:Switch To Meta Version%>', 'Meta'];
}
});
switchEl.innerHTML = makeBtn(switchLabel[0], 'switch_dashboard', [name, switchLabel[1]]);
function switch_dashboard(btn, name, type)
{
btn.disabled = true;
btn.value = '<%:Downloading File...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_dashboard")%>', {name: name, type : type}, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.download_state == "0" ) {
if ( type == "Meta" ) {
if ( name == "Dashboard" ) {
document.getElementById("switch_dashboard_"+name).innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch Successful%> - <%:Switch To Official Version%>" onclick="return switch_dashboard(this, \'Dashboard\', \'Official\')"/>';
}
else
{
document.getElementById("switch_dashboard_"+name).innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch Successful%> - <%:Switch To Official Version%>" onclick="return switch_dashboard(this, \'Yacd\', \'Official\')"/>';
}
}
else {
if ( name == "Dashboard" ) {
document.getElementById("switch_dashboard_"+name).innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch Successful%> - <%:Switch To Meta Version%>" onclick="return switch_dashboard(this, \'Dashboard\', \'Meta\')"/>';
}
else if ( name == "Yacd" )
{
document.getElementById("switch_dashboard_"+name).innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Switch Successful%> - <%:Switch To Meta Version%>" onclick="return switch_dashboard(this, \'Yacd\', \'Meta\')"/>';
}
else if ( name == "Metacubexd" ) {
document.getElementById("switch_dashboard_"+name).innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Update Successful%> - <%:Update Metacubexd Version%>" onclick="return switch_dashboard(this, \'Metacubexd\', \'Official\')"/>';
} else {
document.getElementById("switch_dashboard_"+name).innerHTML = '<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Update Successful%> - <%:Update Zashboard Version%>" onclick="return switch_dashboard(this, \'Zashboard\', \'Official\')"/>';
}
}
document.getElementById("default_dashboard_"+name).firstElementChild.disabled = false;
document.getElementById("delete_dashboard_"+name).firstElementChild.disabled = false;
}
else if ( status.download_state == "2" ) {
btn.value = '<%:Unzip Error%>';
}
else {
if ( name == "Metacubexd" || name == "Zashboard" ) {
btn.value = '<%:Update Failed%>';
}
else {
btn.value = '<%:Switch Failed%>';
}
}
}
});
btn.disabled = false;
return false;
}
if (status.default_dashboard == name.toLowerCase()) {
defaultEl.innerHTML = makeBtn('<%:Default%>', 'default_dashboard', [name]);
defaultEl.firstElementChild.disabled = true;
}
if (!status[name.toLowerCase()]) {
defaultEl.firstElementChild.disabled = true;
deleteEl.firstElementChild.disabled = true;
}
});
function delete_dashboard(btn, name)
{
if ( confirm("<%:Are you sure you want to delete this panel?%>") ) {
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "delete_dashboard")%>', {name: name}, function(x, status) {
if ( x && x.status == 200 ) {
if ( status.delete_state == "1" ) {
if ( document.getElementById('default_dashboard_' + name).firstElementChild.disabled ) {
document.getElementById('default_dashboard_' + name).firstElementChild.value = '<%:Set to Default%>';
}
document.getElementById('default_dashboard_' + name).firstElementChild.disabled = true;
}
else {
btn.disabled = false;
}
}
});
}
return false;
}
window.switch_dashboard = window.switch_dashboard || function(btn, n, type) {
btn.disabled = true;
btn.value = '<%:Downloading File...%>';
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "switch_dashboard")%>', {name: n, type: type}, function(x, status) {
if (!x || x.status != 200) return;
if (status.download_state == "0") {
var isUpdate = (n == "Metacubexd" || n == "Zashboard");
var nextLabel, nextType;
if (isUpdate) {
nextLabel = '<%:Update Successful%> - ' + (n == "Metacubexd" ? '<%:Update Metacubexd Version%>' : '<%:Update Zashboard Version%>');
nextType = 'Official';
} else {
nextLabel = '<%:Switch Successful%> - ' + (type == "Meta" ? '<%:Switch To Official Version%>' : '<%:Switch To Meta Version%>');
nextType = type == "Meta" ? 'Official' : 'Meta';
}
document.getElementById('switch_dashboard_' + n).innerHTML = makeBtn(nextLabel, 'switch_dashboard', [n, nextType]);
document.getElementById('default_dashboard_' + n).firstElementChild.disabled = false;
document.getElementById('delete_dashboard_' + n).firstElementChild.disabled = false;
} else {
btn.value = status.download_state == "2" ? '<%:Unzip Error%>'
: (n == "Metacubexd" || n == "Zashboard") ? '<%:Update Failed%>' : '<%:Switch Failed%>';
}
btn.disabled = false;
});
return false;
};
function default_dashboard(btn, name)
{
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "default_dashboard")%>', {name: name}, function(x, status) {
if ( x && x.status == 200 ) {
btn.value = '<%:Default%>';
btn.disabled = true;
var allBtns = document.querySelectorAll('[id^="default_dashboard_"]');
for (var i = 0; i < allBtns.length; i++) {
var btnEl = allBtns[i].firstElementChild;
if (btnEl && btnEl !== btn && btnEl.value === '<%:Default%>') {
btnEl.disabled = false;
btnEl.value = '<%:Set to Default%>';
}
}
} else {
btn.disabled = false;
}
});
return false;
}
window.delete_dashboard = window.delete_dashboard || function(btn, n) {
if (!confirm("<%:Are you sure you want to delete this panel?%>")) return false;
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "delete_dashboard")%>', {name: n}, function(x, status) {
if (!x || x.status != 200) { btn.disabled = false; return; }
if (status.delete_state == "1") {
var defBtn = document.getElementById('default_dashboard_' + n).firstElementChild;
if (defBtn.disabled) defBtn.value = '<%:Set to Default%>';
defBtn.disabled = true;
} else btn.disabled = false;
});
return false;
};
window.default_dashboard = window.default_dashboard || function(btn, n) {
btn.disabled = true;
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "default_dashboard")%>', {name: n}, function(x, status) {
if (!x || x.status != 200) { btn.disabled = false; return; }
btn.value = '<%:Default%>';
document.querySelectorAll('[id^="default_dashboard_"]').forEach(function(el) {
var b = el.firstElementChild;
if (b && b !== btn && b.value === '<%:Default%>') {
b.disabled = false;
b.value = '<%:Set to Default%>';
}
});
});
return false;
};
})();
//]]></script>
<%+cbi/valuefooter%>
<%+cbi/valuefooter%>
@@ -51,11 +51,9 @@ else
end
end
local cfg_name
if os.getenv("PATH_INFO") then
local cfg_path = string.match(os.getenv("PATH_INFO"), "/config/(.+)$")
cfg_name = cfg_path and string.match(cfg_path, "([^/]+)$") or nil
end
local file_path = fs.get_file_path_from_request()
local cfg_name = (file_path and fs.basename(file_path)) or nil
if not cfg_name and uci:get("openclash", "config", "config_path") then
cfg_name = fs.basename(uci:get("openclash", "config", "config_path"))
end
@@ -392,7 +390,13 @@ local sectiontype = "_"..self.config.."_"..string.match(self.sectiontype, "[%w_]
<%- end
%> alt="<%:Edit%>" title="<%:Edit%>" />
<%- end; if self.addremove then -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>" onclick="this.form.cbi_state='del-section'; return switch_to_tab<%=sectiontype%>()" name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- if file_path then -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>"
onclick="this.form.cbi_state='del-section'; var redirectUrl = 'servers?file=<%=file_path%>'; var originalSubmit = this.form.onsubmit; this.form.onsubmit = function(e) { var result = true; if (originalSubmit) { result = originalSubmit.call(this, e); } if (result !== false) { setTimeout(function() { window.location.href = redirectUrl; }, 200); } return result; }; return switch_to_tab<%=sectiontype%>()"
name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- else -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>" onclick="this.form.cbi_state='del-section'; return switch_to_tab<%=sectiontype%>()" name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- end -%>
<%- end -%>
</td>
<%- end -%>
@@ -503,7 +507,6 @@ local sectiontype = "_"..self.config.."_"..string.match(self.sectiontype, "[%w_]
}
localStorage.setItem("id<%=sectiontype%>",'cbi-<%=self.config%>-<%=self.sectiontype%>');
return true;
};
function onload<%=sectiontype%>() {
+56 -149
View File
@@ -1592,77 +1592,29 @@ msgstr "Error al descargar la lista CIDR de China (IPv4); inténtalo más tarde.
msgid "Chnroute6 Cidr List Update Error, Please Try Again Later..."
msgstr "Error al descargar la lista CIDR de China (IPv6); inténtalo más tarde."
msgid "Start Downloading GeoSite Database..."
msgstr "Iniciando descarga de la base de datos GeoSite..."
msgid "Download Failed: HTML Response Detected, Abort Update..."
msgstr "Descarga fallida: respuesta HTML detectada, aborto de la actualización..."
msgid "GeoSite Database Download Success, Check Updated..."
msgstr "GeoSite descargado correctamente, comprobando actualizaciones..."
msgid "Download Failed: File Size Too Small, Abort Update..."
msgstr "Descarga fallida: el archivo es demasiado pequeño, aborto de la actualización..."
msgid "GeoSite Database Has Been Updated, Starting To Replace The Old Version..."
msgstr "GeoSite tiene una nueva versión; iniciando reemplazo..."
msgid "Download Success, Check Updated..."
msgstr "Descarga correcta, comprobando actualizaciones..."
msgid "GeoSite Database Update Successful!"
msgstr "Base de datos GeoSite actualizada correctamente."
msgid "Has Been Updated, Starting To Replace The Old Version..."
msgstr "Tiene nueva versión; iniciando reemplazo..."
msgid "Updated GeoSite Database No Change, Do Nothing..."
msgstr "GeoSite no presenta cambios; operación detenida."
msgid "Update Successful!"
msgstr "Actualizado correctamente."
msgid "GeoSite Database Update Error, Please Try Again Later..."
msgstr "Error al descargar GeoSite; inténtalo más tarde."
msgid "No Change, Do Nothing..."
msgstr "Sin cambios, no hacer nada..."
msgid "Start Downloading GeoIP Dat..."
msgstr "Iniciando descarga de GeoIP Dat..."
msgid "Update Error, Please Try Again Later..."
msgstr "Error al actualizar; inténtalo más tarde..."
msgid "GeoIP Dat Download Success, Check Updated..."
msgstr "GeoIP Dat descargado correctamente, comprobando actualizaciones..."
msgid "GeoIP Dat Has Been Updated, Starting To Replace The Old Version..."
msgstr "GeoIP Dat tiene una nueva versión; iniciando reemplazo..."
msgid "GeoIP Dat Update Successful!"
msgstr "GeoIP Dat actualizado correctamente."
msgid "Updated GeoIP Dat No Change, Do Nothing..."
msgstr "GeoIP Dat no presenta cambios; operación detenida."
msgid "GeoIP Dat Update Error, Please Try Again Later..."
msgstr "Error al descargar GeoIP Dat; inténtalo más tarde."
msgid "Start Downloading Geoip Database..."
msgstr "Iniciando descarga de la base de datos GEOIP..."
msgid "Geoip Database Download Success, Check Updated..."
msgstr "GEOIP descargado correctamente, comprobando actualizaciones..."
msgid "Geoip Database Has Been Updated, Starting To Replace The Old Version..."
msgstr "GEOIP tiene una nueva versión; iniciando reemplazo..."
msgid "Geoip Database Update Successful!"
msgstr "Base de datos GEOIP actualizada correctamente."
msgid "Updated Geoip Database No Change, Do Nothing..."
msgstr "GEOIP no presenta cambios; operación detenida."
msgid "Geoip Database Update Error, Please Try Again Later..."
msgstr "Error al descargar GEOIP; inténtalo más tarde."
msgid "Start Downloading Geo ASN Database..."
msgstr "Iniciando descarga de la base de datos GEO ASN..."
msgid "Geo ASN Database Download Success, Check Updated..."
msgstr "GEO ASN descargado correctamente, comprobando actualizaciones..."
msgid "Geo ASN Database Has Been Updated, Starting To Replace The Old Version..."
msgstr "GEO ASN tiene una nueva versión; iniciando reemplazo..."
msgid "Geo ASN Database Update Successful!"
msgstr "Base de datos GEO ASN actualizada correctamente."
msgid "Updated Geo ASN Database No Change, Do Nothing..."
msgstr "GEO ASN no presenta cambios; operación detenida."
msgid "Geo ASN Database Update Error, Please Try Again Later..."
msgstr "Error al descargar GEO ASN; inténtalo más tarde."
msgid "Start Downloading Database..."
msgstr "Iniciando descarga de la base de datos..."
msgid "Start Getting"
msgstr "Iniciando obtención de"
@@ -1955,6 +1907,9 @@ msgstr "No se pudo analizar el archivo de configuración,"
msgid "Edit Provider Path Failed,"
msgstr "Fallo al modificar la ruta del proveedor,"
msgid "Edit Provider CDN Failed,"
msgstr "Fallo al modificar la CDN del proveedor,"
msgid "Edit URL-Test Group Tolerance Option Failed,"
msgstr "Fallo al modificar la tolerancia del grupo URL-Test,"
@@ -2111,77 +2066,11 @@ msgstr "Filtro de regiones"
msgid "Unlock Nodes Filter"
msgstr "Filtro de nodos desbloqueados"
msgid "Start Auto Select Proxy For Netflix Unlock..."
msgstr "Iniciando selección automática de nodo para Netflix..."
msgid "Start Auto Select Unlock Proxy For"
msgstr "Iniciando selección automática de nodo"
msgid "Start Auto Select Proxy For Disney Plus Unlock..."
msgstr "Iniciando selección automática de nodo para Disney Plus..."
msgid "Start Auto Select Proxy For HBO Max Unlock..."
msgstr "Iniciando selección automática de nodo para HBO Max..."
msgid "Start Auto Select Proxy For YouTube Premium Unlock..."
msgstr "Iniciando selección automática de nodo para YouTube Premium..."
msgid "Start Auto Select Proxy For TVB Anywhere+ Unlock..."
msgstr "Iniciando selección automática de nodo para TVB Anywhere+..."
msgid "Start Auto Select Proxy For Amazon Prime Video Unlock..."
msgstr "Iniciando selección automática de nodo para Amazon Prime Video..."
msgid "Start Auto Select Proxy For DAZN Unlock..."
msgstr "Iniciando selección automática de nodo para DAZN..."
msgid "Start Auto Select Proxy For Paramount Plus Unlock..."
msgstr "Iniciando selección automática de nodo para Paramount Plus..."
msgid "Start Auto Select Proxy For Discovery Plus Unlock..."
msgstr "Iniciando selección automática de nodo para Discovery Plus..."
msgid "Start Auto Select Proxy For Bilibili Unlock..."
msgstr "Iniciando selección automática de nodo para Bilibili..."
msgid "Start Auto Select Proxy For Google Not CN Unlock..."
msgstr "Iniciando selección automática de nodo para Google fuera de China..."
msgid "Start Auto Select Proxy For OpenAI Unlock..."
msgstr "Iniciando selección automática de nodo para OpenAI..."
msgid "Netflix Group:"
msgstr "Grupo de Netflix:"
msgid "Disney Plus Group:"
msgstr "Grupo de Disney Plus:"
msgid "HBO Max Group:"
msgstr "Grupo de HBO Max:"
msgid "TVB Anywhere+ Group:"
msgstr "Grupo de TVB Anywhere+:"
msgid "Amazon Prime Video Group:"
msgstr "Grupo de Amazon Prime Video:"
msgid "YouTube Premium Group:"
msgstr "Grupo de YouTube Premium:"
msgid "DAZN Group:"
msgstr "Grupo de DAZN:"
msgid "Paramount Plus Group:"
msgstr "Grupo de Paramount Plus:"
msgid "Discovery Plus Group:"
msgstr "Grupo de Discovery Plus:"
msgid "Bilibili Group:"
msgstr "Grupo de Bilibili:"
msgid "Google Group:"
msgstr "Grupo de Google:"
msgid "OpenAI Group:"
msgstr "Grupo de OpenAI:"
msgid "Group:"
msgstr "Grupo:"
msgid "full support"
msgstr "desbloqueo completo"
@@ -2666,8 +2555,8 @@ msgstr "Inicio de TUN fallido; intentando reiniciar..."
msgid "Core Start Failed, Please Check The Log Infos!"
msgstr "Fallo al iniciar el núcleo. Revisa el registro de núcleo."
msgid "Core Status Abnormal, Please Check The Log Infos!"
msgstr "Estado del núcleo anómalo. Consulta el registro de núcleo."
msgid "Core Initial Configuration Timeout, Please Check The Log Infos!"
msgstr "Tiempo de espera agotado en la configuración inicial del núcleo, ¡consulta la información del registro!"
msgid "Forced Sniff Pure IP Connections"
msgstr "Forzar sniffing de conexiones IP puras"
@@ -2720,18 +2609,6 @@ msgstr "Habilita reglas por proceso (solo para procesos del router). Si no está
msgid "Enable Process Rule"
msgstr "Activar reglas por proceso"
msgid "Because there is a port range"
msgstr "Debido a que existe un rango de puertos"
msgid "in the firewall rule settings"
msgstr "en la configuración del firewall"
msgid "auto bypassing may cause the normal connection of the client not to reach the core, if necessary, please add your own in the access control!"
msgstr "el bypass automático puede impedir que conexiones legítimas lleguen al núcleo. Añade reglas manuales si es necesario."
msgid "Start Add Port Bypassing Rules For Firewall Redirect and Firewall Rules..."
msgstr "Añadiendo reglas de bypass de puertos para redirecciones de firewall..."
msgid "DNS Hijacking Mode is Dnsmasq Redirect..."
msgstr "Modo de secuestro DNS: redirección Dnsmasq..."
@@ -3773,6 +3650,12 @@ msgstr "Editar información de módulo"
msgid "Subscription"
msgstr "Suscripción"
msgid "HTTP Module"
msgstr "Módulo HTTP"
msgid "File Module"
msgstr "Módulo de archivo"
msgid "Overwrite Module"
msgstr "Módulo de sobrescritura"
@@ -3834,4 +3717,28 @@ msgid "Serial Number"
msgstr "Número de serie"
msgid "Set Custom Overwrite Script Failed,"
msgstr "Fallo al ejecutar el script de sobrescritura personalizado,"
msgstr "Fallo al ejecutar el script de sobrescritura personalizado,"
msgid "Load YAML Override Block"
msgstr "Cargar bloque de sobrescritura YAML"
msgid "Invalid YAML Override format, skipped..."
msgstr "Formato de sobrescritura YAML no válido, omitido..."
msgid "Parse YAML Override failed:"
msgstr "Error al analizar bloque de sobrescritura YAML:"
msgid "YAML overwrite failed:"
msgstr "Error en la sobrescritura YAML:"
msgid "files selected"
msgstr "archivos seleccionados"
msgid "Global User-Agent"
msgstr "Agente de usuario global"
msgid "depended package reinstalling..."
msgstr "reinstalando paquete dependiente..."
msgid "failed to install, please try to install it manually..."
msgstr "no se pudo instalar; por favor, intenta instalarlo manualmente."
+57 -150
View File
@@ -1590,77 +1590,29 @@ msgstr "大陆 IP 白名单下载失败,请检查网络或稍后再试..."
msgid "Chnroute6 Cidr List Update Error, Please Try Again Later..."
msgstr "大陆 IPv6 白名单下载失败,请检查网络或稍后再试..."
msgid "Start Downloading GeoSite Database..."
msgstr "开始下载 GeoSite 数据库..."
msgid "Download Failed: HTML Response Detected, Abort Update..."
msgstr "下载失败:检测到 HTML 响应(可能为 CDN 错误页面),终止更新..."
msgid "GeoSite Database Download Success, Check Updated..."
msgstr "GeoSite 数据库下载成功,检查数据库版本是否更新..."
msgid "Download Failed: File Size Too Small, Abort Update..."
msgstr "下载失败:文件大小过小(可能被截断),终止更新..."
msgid "GeoSite Database Has Been Updated, Starting To Replace The Old Version..."
msgstr "GeoSite 数据库版本有更新,开始替换数据库版本..."
msgid "Download Success, Check Updated..."
msgstr "下载成功,检查数据库是否已更新..."
msgid "GeoSite Database Update Successful!"
msgstr "GeoSite 数据库更新成功!"
msgid "Has Been Updated, Starting To Replace The Old Version..."
msgstr "有新版本,开始替换旧版本..."
msgid "Updated GeoSite Database No Change, Do Nothing..."
msgstr "GeoSite 数据库版本没有更新,停止继续操作..."
msgid "Update Successful!"
msgstr "更新成功!"
msgid "GeoSite Database Update Error, Please Try Again Later..."
msgstr "GeoSite 数据库下载失败,请检查网络或稍后再试..."
msgid "No Change, Do Nothing..."
msgstr "无变化,停止操作..."
msgid "Start Downloading GeoIP Dat..."
msgstr "开始下载 GeoIP Dat 数据库..."
msgid "Update Error, Please Try Again Later..."
msgstr "更新失败,请稍后再试..."
msgid "GeoIP Dat Download Success, Check Updated..."
msgstr "GeoIP Dat 数据库下载成功,检查数据库版本是否更新..."
msgid "GeoIP Dat Has Been Updated, Starting To Replace The Old Version..."
msgstr "GeoIP Dat 数据库版本有更新,开始替换数据库版本..."
msgid "GeoIP Dat Update Successful!"
msgstr "GeoIP Dat 数据库更新成功!"
msgid "Updated GeoIP Dat No Change, Do Nothing..."
msgstr "GeoIP Dat 数据库版本没有更新,停止继续操作..."
msgid "GeoIP Dat Update Error, Please Try Again Later..."
msgstr "GeoIP Dat 数据库下载失败,请检查网络或稍后再试..."
msgid "Start Downloading Geoip Database..."
msgstr "开始下载 GEOIP 数据库..."
msgid "Geoip Database Download Success, Check Updated..."
msgstr "GEOIP 数据库下载成功,检查数据库版本是否更新..."
msgid "Geoip Database Has Been Updated, Starting To Replace The Old Version..."
msgstr "GEOIP 数据库版本有更新,开始替换数据库版本..."
msgid "Geoip Database Update Successful!"
msgstr "GEOIP 数据库更新成功!"
msgid "Updated Geoip Database No Change, Do Nothing..."
msgstr "GEOIP 数据库版本没有更新,停止继续操作..."
msgid "Geoip Database Update Error, Please Try Again Later..."
msgstr "GEOIP 数据库下载失败,请检查网络或稍后再试..."
msgid "Start Downloading Geo ASN Database..."
msgstr "开始下载 GEO ASN 数据库..."
msgid "Geo ASN Database Download Success, Check Updated..."
msgstr "GEO ASN 数据库下载成功,检查数据库版本是否更新..."
msgid "Geo ASN Database Has Been Updated, Starting To Replace The Old Version..."
msgstr "GEO ASN 数据库版本有更新,开始替换数据库版本..."
msgid "Geo ASN Database Update Successful!"
msgstr "GEO ASN 数据库更新成功!"
msgid "Updated Geo ASN Database No Change, Do Nothing..."
msgstr "GEO ASN 数据库版本没有更新,停止继续操作..."
msgid "Geo ASN Database Update Error, Please Try Again Later..."
msgstr "GEO ASN 数据库下载失败,请检查网络或稍后再试..."
msgid "Start Downloading Database..."
msgstr "开始下载数据库..."
msgid "Start Getting"
msgstr "开始获取"
@@ -1953,6 +1905,9 @@ msgstr "配置文件解析失败,"
msgid "Edit Provider Path Failed,"
msgstr "修改集路径失败,"
msgid "Edit Provider CDN Failed,"
msgstr "修改集 CDN 地址失败,"
msgid "Edit URL-Test Group Tolerance Option Failed,"
msgstr "修改 URL-Test 策略组容忍值失败,"
@@ -2109,77 +2064,11 @@ msgstr "解锁区域筛选"
msgid "Unlock Nodes Filter"
msgstr "解锁节点筛选"
msgid "Start Auto Select Proxy For Netflix Unlock..."
msgstr "开始自动选择(检测)Netflix 解锁节点..."
msgid "Start Auto Select Unlock Proxy For"
msgstr "开始自动选择(检测)解锁节点"
msgid "Start Auto Select Proxy For Disney Plus Unlock..."
msgstr "开始自动选择(检测)Disney Plus 解锁节点..."
msgid "Start Auto Select Proxy For HBO Max Unlock..."
msgstr "开始自动选择(检测)HBO Max 解锁节点..."
msgid "Start Auto Select Proxy For YouTube Premium Unlock..."
msgstr "开始自动选择(检测)YouTube Premium 解锁节点..."
msgid "Start Auto Select Proxy For TVB Anywhere+ Unlock..."
msgstr "开始自动选择(检测)TVB Anywhere+ 解锁节点..."
msgid "Start Auto Select Proxy For Amazon Prime Video Unlock..."
msgstr "开始自动选择(检测)Amazon Prime Video 解锁节点..."
msgid "Start Auto Select Proxy For DAZN Unlock..."
msgstr "开始自动选择(检测)DAZN 解锁节点..."
msgid "Start Auto Select Proxy For Paramount Plus Unlock..."
msgstr "开始自动选择(检测)Paramount Plus 解锁节点..."
msgid "Start Auto Select Proxy For Discovery Plus Unlock..."
msgstr "开始自动选择(检测)Discovery Plus 解锁节点..."
msgid "Start Auto Select Proxy For Bilibili Unlock..."
msgstr "开始自动选择(检测)Bilibili 解锁节点..."
msgid "Start Auto Select Proxy For Google Not CN Unlock..."
msgstr "开始自动选择(检测)Google 非送中节点..."
msgid "Start Auto Select Proxy For OpenAI Unlock..."
msgstr "开始自动选择(检测)OpenAI 解锁节点..."
msgid "Netflix Group:"
msgstr "Netflix 策略组:"
msgid "Disney Plus Group:"
msgstr "Disney Plus 策略组:"
msgid "HBO Max Group:"
msgstr "HBO Max 策略组:"
msgid "TVB Anywhere+ Group:"
msgstr "TVB Anywhere+ 策略组:"
msgid "Amazon Prime Video Group:"
msgstr "Amazon Prime Video 策略组:"
msgid "YouTube Premium Group:"
msgstr "YouTube Premium 策略组:"
msgid "DAZN Group:"
msgstr "DAZN 策略组:"
msgid "Paramount Plus Group:"
msgstr "Paramount Plus 策略组:"
msgid "Discovery Plus Group:"
msgstr "Discovery Plus 策略组:"
msgid "Bilibili Group:"
msgstr "Bilibili 策略组:"
msgid "Google Group:"
msgstr "Google 策略组:"
msgid "OpenAI Group:"
msgstr "OpenAI 策略组:"
msgid "Group:"
msgstr "策略组:"
msgid "full support"
msgstr "完整解锁"
@@ -2437,7 +2326,7 @@ msgid "Geodata Loader Mode"
msgstr "Geodata 数据加载方式"
msgid "Memconservative"
msgstr "小内存模式"
msgstr "低内存模式"
msgid "Standard"
msgstr "标准模式"
@@ -2664,8 +2553,8 @@ msgstr "TUN 接口启动失败,尝试重启内核..."
msgid "Core Start Failed, Please Check The Log Infos!"
msgstr "内核启动失败,请查看《内核日志》排查失败原因!"
msgid "Core Status Abnormal, Please Check The Log Infos!"
msgstr "内核状态异常,请查看《内核日志》排查异常原因!"
msgid "Core Initial Configuration Timeout, Please Check The Log Infos!"
msgstr "内核加载配置超时,请查看《内核日志》排查原因!"
msgid "Forced Sniff Pure IP Connections"
msgstr "强制探测(嗅探)所有纯 IP 的连接"
@@ -2718,18 +2607,6 @@ msgstr "是否启用进程规则,仅能匹配路由器自身进程,在路由
msgid "Enable Process Rule"
msgstr "启用进程规则"
msgid "Because there is a port range"
msgstr "因为端口范围"
msgid "in the firewall rule settings"
msgstr "存在于防火墙规则设置"
msgid "auto bypassing may cause the normal connection of the client not to reach the core, if necessary, please add your own in the access control!"
msgstr "自动绕过后可能造成客户端的正常连接无法到达内核,如有需要,请在插件设置-来源流量访问控制中自行添加!"
msgid "Start Add Port Bypassing Rules For Firewall Redirect and Firewall Rules..."
msgstr "正在根据防火墙端口转发和防火墙通信规则添加端口绕过规则..."
msgid "DNS Hijacking Mode is Dnsmasq Redirect..."
msgstr "DNS 劫持模式为 Dnsmasq 转发..."
@@ -3780,6 +3657,12 @@ msgstr "编辑模块信息"
msgid "Subscription"
msgstr "订阅"
msgid "HTTP Module"
msgstr "远程模块"
msgid "File Module"
msgstr "本地模块"
msgid "Overwrite Module"
msgstr "覆写模块"
@@ -3841,4 +3724,28 @@ msgid "Serial Number"
msgstr "序号"
msgid "Set Custom Overwrite Script Failed,"
msgstr "自定义覆写命令执行失败,"
msgstr "自定义覆写命令执行失败,"
msgid "Load YAML Override Block"
msgstr "加载 YAML 覆写模块"
msgid "Invalid YAML Override format, skipped..."
msgstr "无效的 YAML 覆写格式,跳过..."
msgid "Parse YAML Override failed:"
msgstr "YAML 覆写模块解析失败:"
msgid "YAML overwrite failed:"
msgstr "YAML 覆写模块执行失败:"
msgid "files selected"
msgstr "配置已选择"
msgid "Global User-Agent"
msgstr "全局 User-Agent"
msgid "depended package reinstalling..."
msgstr "依赖正在重新安装..."
msgid "failed to install, please try to install it manually..."
msgstr "依赖安装失败,请重试手动安装..."
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -1,3 +1,21 @@
rule-providers:
## google:
## type: http
## path: ./rule1.yaml
## url: "https://raw.githubusercontent.com/../Google.yaml"
## interval: 600
## proxy: DIRECT
## behavior: classical
## format: yaml
## size-limit: 0
## header:
## User-Agent:
## - "mihomo/1.18.3"
## Authorization:
## - 'token 1231231'
## payload:
## - 'DOMAIN-SUFFIX,google.com'
rules:
##- PROCESS-NAME,curl,DIRECT #匹配路由自身进程(curl直连)
##- DOMAIN-SUFFIX,google.com,Proxy #匹配域名后缀(交由Proxy代理服务器组)
@@ -1,3 +1,20 @@
rule-providers:
## google:
## type: http
## path: ./rule1.yaml
## url: "https://raw.githubusercontent.com/../Google.yaml"
## interval: 600
## proxy: DIRECT
## behavior: classical
## format: yaml
## size-limit: 0
## header:
## User-Agent:
## - "mihomo/1.18.3"
## Authorization:
## - 'token 1231231'
## payload:
## - 'DOMAIN-SUFFIX,google.com'
rules:
##- DOMAIN-SUFFIX,google.com,Proxy #匹配域名后缀(交由Proxy代理服务器组)
##- DOMAIN-KEYWORD,google,Proxy #匹配域名关键字(交由Proxy代理服务器组)
@@ -73,7 +73,6 @@
# ENABLE_TCP_CONCURRENT (启用 TCP 并发, 0: 插件不覆写, 1: 启用)
# FIND_PROCESS_MODE (进程查找模式, 0: 插件不覆写, off: 关闭, always, strict)
# GLOBAL_CLIENT_FINGERPRINT (全局客户端指纹, none/random/chrome/firefox/safari/ios/android/edge/360/qq/0: 插件不覆写)
# ENABLE_UNIFIED_DELAY (启用统一延迟计算, 0: 插件不覆写, 1: 启用)
# ENABLE_RULE_PROXY (仅代理命中规则流量, 0: 禁用, 1: 启用)
@@ -117,6 +116,7 @@
# GEOASN_CUSTOM_URL (GeoASN 数据自定义更新地址, URL or default)
# --- 其它功能 ---
# GLOBAL_UA (全局 User-Agent, 0:不覆写,或具体 UA)
# SMALL_FLASH_MEMORY (小闪存模式, 0: 禁用, 1: 启用)
# DISABLE_QUIC_GO_GSO (禁用 QUIC GO GSO, 0: 插件不覆写, 1: 启用)
# DELAY_START (延时启动, 0: 禁用 or 秒数)
@@ -209,6 +209,298 @@
#指定展示订阅信息的 URL 地址
#SUB_INFO_URL = https://default-demo.yaml
# ==========================================================
# ==== 以下为 YAML 块覆写示例 ====
# ==========================================================
# YAML 覆写块识别标记
# [YAML]
# ----------------------------------------------------------
# 操作符速查表:
# key → 默认合并(Hash 递归合并,其他直接覆盖)
# key! → 强制覆盖整个值
# key+ → 数组后置追加
# +key → 数组前置插入
# key- → 数组差集删除 / 非数组则删除该键
# key* → 批量条件更新(需配合 where/set)
# <key>后缀 → 同上(+, !, *, -),适用于键名含特殊字符时
# +<key> → 数组前置插入,适用于键名含特殊字符时
# ----------------------------------------------------------
# 1. 默认合并 - 合并哈希键值,键不存在则添加,Hash 值递归合并
# 示例:修改 dns.enable 并添加新键
# dns:
# enable: true
# cache-algorithm: lru
# 示例:直接替换标量值
# mixed-port: 10802
# allow-lan: false
# 示例:合并 tun 配置(仅修改指定字段,其余保留)
# tun:
# enable: true
# stack: gvisor
# ----------------------------------------------------------
# 2. 强制覆盖 (key!) - 强制替换整个值,不做递归合并
# 示例:替换整个 dns.fake-ip-filter 数组
# dns:
# fake-ip-filter!:
# - '*.lan'
# - 'new.domain.com'
# 示例:强制覆盖整个 rules 数组
# rules!:
# - DOMAIN-SUFFIX,example.com,DIRECT
# - MATCH,PROXY
# 示例:使用 <> 强制覆盖整个 dns 配置(键名含特殊字符时)
# <dns>!:
# enable: false
# nameserver:
# - '114.114.114.114'
# ----------------------------------------------------------
# 3. 数组后置 (key+) - 将新元素追加到数组末尾
# 示例:向 dns.nameserver 末尾追加 DNS
# dns:
# nameserver+:
# - '1.1.1.1'
# - '8.8.8.8'
# 示例:向 rules 末尾追加规则
# rules+:
# - DOMAIN-SUFFIX,example.com,REJECT
# 示例:使用 <> 后置
# dns:
# <nameserver>+:
# - '8.8.8.8'
# ----------------------------------------------------------
# 4. 数组前置 (+key / +<key>) - 将新元素插入到数组开头
# 示例:向 dns.nameserver 开头插入 DNS
# dns:
# +nameserver:
# - '223.5.5.5'
# - '119.29.29.29'
# 示例:向 rules 开头插入规则(优先匹配)
# +rules:
# - DOMAIN-SUFFIX,priority.com,DIRECT
# 示例:使用 +<> 前置插入(键名含特殊字符时)
# dns:
# +<nameserver>:
# - '119.29.29.29'
# ----------------------------------------------------------
# 5. 数组删除 (key-) - 从数组中移除指定元素,非数组则删除整个键
# 示例:从 dns.nameserver 中移除指定 DNS
# dns:
# nameserver-:
# - '8.8.8.8'
# - '8.8.4.4'
# 示例:从 rules 中移除指定规则
# rules-:
# - DOMAIN-SUFFIX,old.com,REJECT
# 示例:删除整个键(值为 null/~ 或空)
# dns:
# cache-algorithm-:
# ----------------------------------------------------------
# 6. 批量条件更新 (key*) - 按 where 条件匹配集合元素,用 set 子句更新
#
# 支持的集合类型:
# - 数组(元素为 Hash 或 String)
# - Hash(值为 Hash)
#
# where 条件支持:
# - 普通值相等: type: select
# - 正则匹配: name: '/^HK/'
# - 数组包含: proxies: ['proxy1'] (检查数组是否包含所有指定元素)
# - Hash 键匹配: key: 'some-key' (仅用于 Hash 类型集合)
# - 字符串匹配: value: 'some-string' (仅用于字符串数组)
#
# set 子句支持的操作符(同顶层操作符):
# field → 直接覆盖该字段
# field! → 强制覆盖该字段
# field+ → 数组后置追加
# +field → 数组前置插入
# field- → 数组差集删除 / 非数组则删除该键
# value → 替换字符串数组中的匹配项
# value: ~ → 删除字符串数组中的匹配项
# key: ~ → 删除 Hash 中的匹配键(仅 Hash 集合)
# key-: → 同上
# 示例:按 type 匹配,替换 proxy-groups 的 proxies
# proxy-groups*:
# where:
# type: select
# set:
# proxies:
# - 'new-proxy1'
# - 'new-proxy2'
# 示例:按 type 匹配,向 proxies 末尾追加节点
# proxy-groups*:
# where:
# type: select
# set:
# proxies+:
# - 'new-proxy3'
# 示例:按 type 匹配,向 proxies 开头插入节点
# proxy-groups*:
# where:
# type: url-test
# set:
# +proxies:
# - 'new-proxy1'
# 示例:按 type 匹配,从 proxies 中移除指定节点
# proxy-groups*:
# where:
# type: select
# set:
# proxies-:
# - 'old-proxy1'
# - 'old-proxy2'
# 示例:使用数组包含条件(proxies 中必须包含指定元素才匹配)
# proxy-groups*:
# where:
# type: select
# proxies:
# - 'old-proxy1'
# set:
# proxies:
# - 'new-proxy1'
# - 'new-proxy2'
# 示例:使用正则匹配 name 字段
# proxy-groups*:
# where:
# name: '/^HK/'
# set:
# +proxies:
# - 'hk-new-proxy'
# 示例:删除 proxy-groups 中某个字段(set value 为 null/~)
# proxy-groups*:
# where:
# name: Proxy
# set:
# icon-:
# 示例:按 type 匹配修改 proxies 中 socks5 节点端口
# proxies*:
# where:
# type: socks5
# set:
# port: 1080
# 示例:更新 hosts Hash 中的指定键
# hosts*:
# where:
# key: '*.mihomo.dev'
# set:
# '*.mihomo.dev': '::1'
# 示例:删除 hosts Hash 中的匹配键
# hosts*:
# where:
# key: '*.old.dev'
# set:
# key-:
# 示例:替换字符串数组中的匹配项
# rules*:
# where:
# value: 'DOMAIN-SUFFIX,old.com,REJECT'
# set:
# value: 'DOMAIN-SUFFIX,new.com,DIRECT'
# 示例:使用正则匹配并删除字符串数组中的规则
# rules*:
# where:
# value: '/,REJECT$/'
# set:
# value:
# 示例:删除 fake-ip-filter 中的指定项
# dns:
# fake-ip-filter*:
# where:
# value: '+.localhost.ptlogin2.qq.com'
# set:
# value:
# 示例:使用 <> 批量更新
# <proxy-groups>*:
# where:
# type: url-test
# set:
# interval: 300
# ----------------------------------------------------------
# 7. 组合操作 - 在同一块内同时使用多个操作符
# 示例:同时前置和后置追加 DNS(必须写在同一 dns 块内)
# dns:
# nameserver+:
# - '1.0.0.1'
# +nameserver:
# - '119.29.29.29'
# 示例:同时删除旧 DNS 并前置插入新 DNS
# dns:
# nameserver-:
# - '8.8.8.8'
# +nameserver:
# - '223.5.5.5'
# ----------------------------------------------------------
# 8. <key> 语法补充 - 适用于键名含特殊字符时
# 后缀支持:+(后置)、-(删除)、!(强制覆盖)、*(批量更新),空串为默认合并
# 前置插入请使用 +<key> 写法(前缀形式)
# 示例:使用 <> 后置追加
# dns:
# <nameserver>+:
# - '8.8.8.8'
# 示例:使用 <> 删除数组元素
# dns:
# <nameserver>-:
# - '8.8.8.8'
# 示例:使用 <> 强制覆盖
# dns:
# <nameserver>!:
# - '114.114.114.114'
# 示例:使用 <> 批量更新(等同于 key*)
# <proxy-groups>*:
# where:
# type: url-test
# set:
# interval: 300
# 示例:使用 +<> 前置插入(键名含特殊字符时的前置写法)
# dns:
# +<nameserver>:
# - '119.29.29.29'
# ==========================================================
# ==== 以下为仅修改插件设置示例 ====
# ==========================================================
@@ -174,8 +174,10 @@ if [ -f "/tmp/openclash.bak" ]; then
cp -rf "/tmp/openclash/." "/etc/openclash/" >/dev/null 2>&1
#ui
if [ -d "/tmp/openclash_ui/" ]; then
rm -rf "/usr/share/openclash/ui/" >/dev/null 2>&1
cp -rf "/tmp/openclash_ui/." "/usr/share/openclash/ui/" >/dev/null 2>&1
if [ -d "/tmp/openclash_ui/metacubexd/" ] || [ -d "/tmp/openclash_ui/zashboard/" ] || [ -d "/tmp/openclash_ui/yacd/" ] || [ -d "/tmp/openclash_ui/dashboard/" ]; then
rm -rf "/usr/share/openclash/ui/" >/dev/null 2>&1
cp -rf "/tmp/openclash_ui/." "/usr/share/openclash/ui/" >/dev/null 2>&1
fi
rm -rf "/tmp/openclash_ui/" >/dev/null 2>&1
fi
#pac
@@ -264,6 +266,6 @@ sed -i '/.*kB maximum content size*/c\export let HTTP_MAX_CONTENT = 1024*10240;
/etc/init.d/uhttpd restart >/dev/null 2>&1
rm -f /tmp/luci-indexcache >/dev/null 2>&1
rm -f /tmp/luci-indexcache.* >/dev/null 2>&1
rm -rf /tmp/luci-indexcache /tmp/luci-indexcache.* /var/luci-indexcache /var/luci-indexcache.* >/dev/null 2>&1
rm -rf /tmp/luci-modulecache /var/luci-modulecache >/dev/null 2>&1
exit 0
@@ -1,119 +1,457 @@
module YAML
class << self
alias_method :load, :unsafe_load if YAML.respond_to? :unsafe_load
alias_method :original_dump, :dump
alias_method :original_load_file, :load_file
end
class << self
alias_method :load, :unsafe_load if YAML.respond_to? :unsafe_load
alias_method :original_dump, :dump
alias_method :original_load_file, :load_file
end
def self.LOG(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Info] " + "#{info}"
end
def self.LOG(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Info] " + "#{info}"
end
def self.LOG_ERROR(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Error] " + "#{info}"
end
def self.LOG_ERROR(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Error] " + "#{info}"
end
def self.LOG_WARN(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Warning] " + "#{info}"
end
def self.LOG_WARN(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Warning] " + "#{info}"
end
def self.LOG_TIP(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Tip] " + "#{info}"
end
def self.LOG_TIP(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Tip] " + "#{info}"
end
# Keep `short-id` as string before YAML parsing so leading zeros are preserved.
# This is required for REALITY short-id values like `00000000`.
def self.load_file(filename, *args, **kwargs)
yaml_content = File.read(filename)
processed_content = fix_short_id_quotes(yaml_content)
def self.load_file(filename, *args, **kwargs)
yaml_content = File.read(filename)
processed_content = fix_short_id_quotes(yaml_content)
if kwargs.empty?
load(processed_content, *args)
else
load(processed_content, *args, **kwargs)
end
end
load(processed_content, *args, **kwargs)
end
def self.dump(obj, io = nil, **options)
begin
if io.nil?
yaml_content = original_dump(obj, **options)
fix_short_id_quotes(yaml_content)
elsif io.respond_to?(:write)
require 'stringio'
temp_io = StringIO.new
original_dump(obj, temp_io, **options)
yaml_content = temp_io.string
processed_content = fix_short_id_quotes(yaml_content)
io.write(processed_content)
io
else
yaml_content = original_dump(obj, io, **options)
fix_short_id_quotes(yaml_content)
end
rescue => e
LOG_ERROR("Write file failed:【%s】" % [e.message])
nil
end
end
def self.dump(obj, io = nil, **options)
begin
yaml_content = original_dump(obj, **options)
processed = fix_short_id_quotes(yaml_content)
if io.nil?
processed
elsif io.respond_to?(:write)
io.write(processed)
io
else
processed
end
rescue => e
LOG_ERROR("Write file failed:【%s】" % [e.message])
nil
end
end
private
private
SHORT_ID_REGEX = /^(\s*)short-id:\s*(.*)$/
LIST_ITEM_REGEX = /^(\s*)-\s*(.*)$/
KEY_REGEX = /^(\s*)([a-zA-Z0-9_-]+):\s*(.*)$/
QUOTED_VALUE_REGEX = /^["'].*["']$/
# fix_short_id_quotes:
# Purpose: ensure YAML `short-id` values are emitted with the intended
# representation when dumping or re-writing YAML files.
# Behavior:
# - Non-null, non-empty scalar `short-id` values (and items inside
# `short-id` sequences) are written as double-quoted strings.
# - Empty strings are preserved as empty quoted strings ("" remains "").
# - Null values are preserved and may be emitted explicitly (e.g. `! "").
# Lastly, use gsub to clean up any `! ""` tags
# Examples:
# Input: short-id: 00000000 -> Output: short-id: "00000000"
# Input: short-id: "" -> Output: short-id: ""
# Input: short-id: "abc123" -> Output: short-id: "abc123"
# Input: short-id: "1600e237" -> Output: short-id: "1600e237"
# Input: short-id: null -> Output: short-id: ""
# Inline map support, e.g. reality-opts: { ..., short-id: 00000000 }
INLINE_SHORT_ID_REGEX = /(short-id:\s*)(?!["'\[])([^\s,"'{}\[\]\n\r]+)(?=\s*(?:[,}\]\n\r]|$))/m.freeze
def self.fix_short_id_quotes(yaml_content)
return yaml_content unless yaml_content.include?('short-id:')
def self.fix_short_id_quotes(yaml_content)
return yaml_content unless yaml_content.include?('short-id:')
begin
stream = Psych.parse_stream(yaml_content)
begin
# First, normalize inline-map style unquoted short-id.
processed = yaml_content.gsub(INLINE_SHORT_ID_REGEX) do
"#{$1}\"#{$2}\""
end
traverse = lambda do |node|
case node
when Psych::Nodes::Mapping
children = node.children || []
i = 0
while i < children.length
key = children[i]
val = children[i + 1]
if key.is_a?(Psych::Nodes::Scalar) && key.value == 'short-id'
if val.is_a?(Psych::Nodes::Scalar)
is_null_scalar = (val.tag == 'tag:yaml.org,2002:null') || (val.tag == '!!null') || (val.value =~ /^\s*(~|null|NULL|Null)\s*$/)
unless is_null_scalar
val.tag = nil
val.style = defined?(Psych::Nodes::Scalar::DOUBLE_QUOTED) ? Psych::Nodes::Scalar::DOUBLE_QUOTED : 2
end
elsif val.is_a?(Psych::Nodes::Sequence)
val.children.each do |child|
if child.is_a?(Psych::Nodes::Scalar)
is_null_child = (child.tag == 'tag:yaml.org,2002:null') || (child.tag == '!!null') || (child.value =~ /^\s*(~|null|NULL|Null)\s*$/)
unless is_null_child
child.tag = nil
child.style = defined?(Psych::Nodes::Scalar::DOUBLE_QUOTED) ? Psych::Nodes::Scalar::DOUBLE_QUOTED : 2
end
end
end
end
else
traverse.call(key) if key.respond_to?(:children)
traverse.call(val) if val.respond_to?(:children)
end
i += 2
end
when Psych::Nodes::Sequence
(node.children || []).each { |c| traverse.call(c) }
when Psych::Nodes::Document, Psych::Nodes::Stream
(node.children || []).each { |c| traverse.call(c) }
else
if node.respond_to?(:children)
(node.children || []).each { |c| traverse.call(c) }
end
end
end
lines = processed.lines
short_id_indices = lines.each_index.select { |i| lines[i] =~ SHORT_ID_REGEX }
short_id_indices.each do |short_id_index|
line = lines[short_id_index]
if line =~ SHORT_ID_REGEX
indent = $1
value = $2.strip
if value.empty?
(short_id_index + 1...lines.size).each do |i|
line = lines[i]
next if line.strip.empty?
if line[/^\s*/].length <= short_id_indent_len
break
end
if line =~ LIST_ITEM_REGEX
indent = $1
value = $2.strip
if value =~ KEY_REGEX
break
end
if value !~ QUOTED_VALUE_REGEX
lines[i] = "#{indent}- \"#{value}\"\n"
end
elsif line =~ KEY_REGEX
break
end
end
else
if value !~ QUOTED_VALUE_REGEX
lines[short_id_index] = "#{indent}short-id: \"#{value}\"\n"
end
end
end
end
lines.join
rescue => e
LOG_ERROR("Fix short-id values type failed:【%s】" % [e.message])
yaml_content
end
end
end
stream.children.each do |doc_node|
if doc_node.is_a?(Psych::Nodes::Document)
traverse.call(doc_node.root) if doc_node.root
end
end
if stream.respond_to?(:to_yaml)
processed_yaml = stream.to_yaml
processed_yaml = processed_yaml.gsub(/^([ \t]*short-id:\s*)!\s*/, "\\1")
processed_yaml
else
yaml_content
end
rescue => e
LOG_ERROR("Fix short-id values type failed:【%s】" % [e.message])
yaml_content
end
end
def self.overwrite(base, override)
return override if base.nil?
return base if override.nil?
current_key = nil
current_operation = nil
begin
case override
when Hash
result = base.is_a?(Hash) ? base.dup : {}
override.each do |key, value|
current_key = key
processed_key, operation = parse_key(key)
current_operation = operation
applied = apply_operation(result[processed_key], value, operation)
if applied.equal?(DELETED_SENTINEL)
result.delete(processed_key)
else
result[processed_key] = applied
end
end
result
else
override
end
rescue => e
LOG_ERROR("YAML overwrite failed:【key: %s, operation: %s, error: %s】" % [current_key, current_operation, e.message])
base
end
end
private
def self.parse_key(key)
key_str = key.to_s
# +<key>
if key_str.start_with?('+<') && key_str.include?('>')
close_idx = key_str.index('>')
inner_key = key_str[2...close_idx]
return inner_key, :prepend_array
end
# <key>suffix
if key_str.start_with?('<') && key_str.include?('>')
close_idx = key_str.index('>')
inner_key = key_str[1...close_idx]
suffix = key_str[(close_idx + 1)..-1]
return inner_key, determine_operation(suffix)
end
# 前缀 +key
if key_str.start_with?('+')
return key_str[1..-1], :prepend_array
end
# 尾部(支持 +, !, *, -)
if key_str =~ /^(.*?)([+!*\-])$/
return Regexp.last_match(1), determine_operation(Regexp.last_match(2))
end
[key_str, :merge]
end
def self.determine_operation(suffix)
case suffix
when '+'
:append_array
when '-'
:delete
when '!'
:force_overwrite
when '*'
:batch_update
else
:merge
end
end
def self.match_value(target, condition)
return false if target.nil? || condition.nil?
begin
if condition.is_a?(String) && condition.start_with?('/') && condition.end_with?('/')
pattern = condition[1...-1]
regexp = Regexp.new(pattern)
if target.is_a?(Array)
target.any? { |item| item.to_s =~ regexp }
else
target.to_s =~ regexp
end
elsif condition.is_a?(Array) && target.is_a?(Array)
condition.all? { |c| target.include?(c) }
else
target == condition
end
rescue => e
LOG_ERROR("YAML overwrite failed:【(match value) => target: %s, condition: %s, error: %s】" % [target, condition, e.message])
false
end
end
def self.deep_dup(obj)
case obj
when Array
obj.map { |x| deep_dup(x) }
when Hash
obj.transform_values { |v| deep_dup(v) }
else
obj.dup rescue obj
end
end
def self.merge_hash(base, value, prepend: false)
if prepend
result = {}
value.each do |k, v|
if base.key?(k)
result[k] = apply_operation(base[k], v, :merge)
else
result[k] = deep_dup(v)
end
end
base.each do |k, v|
result[k] = deep_dup(v) unless result.key?(k)
end
result
else
result = deep_dup(base)
value.each do |k, v|
if result.key?(k)
result[k] = apply_operation(result[k], v, :merge)
else
result[k] = deep_dup(v)
end
end
result
end
end
def self.delete_from_hash(base, value)
result = deep_dup(base)
case value
when Array
value.each { |k| result.delete(k) }
when Hash
value.each do |k, v|
if v.nil? || v == true
result.delete(k)
elsif result[k].is_a?(Hash) && v.is_a?(Hash)
nested = apply_operation(result[k], v, :delete)
if nested.equal?(DELETED_SENTINEL)
result.delete(k)
else
result[k] = nested
end
else
result.delete(k)
end
end
else
result.delete(value)
end
result
end
DELETED_SENTINEL = Object.new.freeze
def self.apply_operation(base, value, operation)
case operation
when :delete
if base.is_a?(Array) && value.is_a?(Array)
base - value
elsif base.is_a?(Array) && !value.nil?
base - [value]
elsif base.is_a?(Hash)
delete_from_hash(base, value)
else
DELETED_SENTINEL
end
when :force_overwrite
deep_dup(value)
when :prepend_array
if base.is_a?(Array) && value.is_a?(Array)
(deep_dup(value) + base).uniq
elsif base.is_a?(Hash) && value.is_a?(Hash)
merge_hash(base, value, prepend: true)
else
deep_dup(value)
end
when :append_array
if base.is_a?(Array) && value.is_a?(Array)
base_dup = base.dup
deep_dup(value).each { |v| base_dup.delete(v) }
base_dup + deep_dup(value)
elsif base.is_a?(Hash) && value.is_a?(Hash)
merge_hash(base, value, prepend: false)
else
deep_dup(value)
end
when :batch_update
batch_update_items(base, value)
when :merge
if base.is_a?(Hash) && value.is_a?(Hash)
overwrite(base, value)
elsif value.nil?
base
else
deep_dup(value)
end
else
deep_dup(value)
end
end
def self.apply_set_fields(item, set_values)
keys_to_delete = []
set_values.each do |k, v|
processed_key, operation = parse_key(k)
result = apply_operation(item[processed_key], v, operation)
if result.equal?(DELETED_SENTINEL)
keys_to_delete << processed_key
else
item[processed_key] = result
end
end
keys_to_delete.each { |k| item.delete(k) }
end
def self.match_item(item, where_conditions, key = nil)
where_conditions.all? do |k, v|
if k == 'key' && !key.nil?
match_value(key, v)
elsif item.is_a?(Hash)
match_value(item[k] || item[k.to_s], v)
elsif item.is_a?(String) && k == 'value'
match_value(item, v)
else
false
end
end
end
def self.batch_update_items(collection, update_spec)
return collection unless update_spec.is_a?(Hash)
begin
where_conditions = update_spec['where'] || {}
set_values = update_spec['set'] || {}
if collection.is_a?(Array)
result = collection.dup
delete_indices = []
result.each_with_index do |item, index|
match = match_item(item, where_conditions)
if match
if item.is_a?(Hash)
apply_set_fields(item, set_values)
elsif item.is_a?(String) && set_values.key?('value')
new_value = set_values['value']
if new_value.nil?
delete_indices << index
else
result[index] = deep_dup(new_value)
end
end
end
end
delete_indices.reverse_each { |i| result.delete_at(i) }
result
elsif collection.is_a?(Hash)
if where_conditions.any? { |k, _| k != 'key' } &&
match_item(collection, where_conditions)
result = collection.dup
apply_set_fields(result, set_values)
result
else
result = collection.dup
keys_to_delete = []
result.each do |key, value|
next unless value.is_a?(Hash)
match = match_item(value, where_conditions, key)
if match
if set_values.key?('key-') || (set_values.key?('key') && set_values['key'].nil?)
keys_to_delete << key
else
apply_set_fields(value, set_values)
end
end
end
keys_to_delete.each { |k| result.delete(k) }
result
end
elsif collection.nil?
nil
else
collection
end
rescue => e
LOG_ERROR("YAML overwrite failed:【(batch update) => update_spec: %s, error: %s】" % [update_spec, e.message])
collection
end
end
end
@@ -86,17 +86,6 @@ DOWNLOAD_RESULT=$?
config_cus_up()
{
if [ -z "$CONFIG_PATH" ]; then
for file_name in /etc/openclash/config/*
do
if [ -f "$file_name" ]; then
CONFIG_PATH=$file_name
break
fi
done
uci -q set openclash.config.config_path="$CONFIG_PATH"
uci commit openclash
fi
if [ -z "$subscribe_url_param" ]; then
if [ -n "$key_match_param" ] || [ -n "$key_ex_match_param" ]; then
LOG_OUT "Config File【$name】Start Picking Nodes..."
@@ -192,6 +181,10 @@ config_su_check()
mv "$CFG_FILE" "$CONFIG_FILE" 2>/dev/null
LOG_OUT "Config File【$name】Update Successful!"
fi
if [ -z "$CONFIG_PATH" ]; then
uci -q set openclash.config.config_path="$CONFIG_FILE"
uci commit openclash
fi
if [ "$CONFIG_FILE" == "$CONFIG_PATH" ]; then
restart=1
fi
@@ -335,13 +328,13 @@ sub_info_get()
config_get "udp" "$section" "udp" ""
config_get "skip_cert_verify" "$section" "skip_cert_verify" ""
config_get "sort" "$section" "sort" ""
config_get "convert_address" "$section" "convert_address" ""
config_get "convert_address" "$section" "convert_address" "https://api.asailor.org/sub"
config_get "template" "$section" "template" ""
config_get "node_type" "$section" "node_type" ""
config_get "rule_provider" "$section" "rule_provider" ""
config_get "custom_template_url" "$section" "custom_template_url" ""
config_get "de_ex_keyword" "$section" "de_ex_keyword" ""
config_get "sub_ua" "$section" "sub_ua" "clash.meta"
config_get "sub_ua" "$section" "sub_ua" "clash-verge/v2.4.5"
CONFIG_FILE="/etc/openclash/config/$name.yaml"
CFG_FILE="/tmp/$name.yaml"
@@ -74,7 +74,7 @@ DOWNLOAD_FILE_CURL() {
fi
else
CURL_OUTPUT=$(curl -w "\n%{http_code}" -SsL --connect-timeout 30 -m 60 --speed-time 30 --speed-limit 1 --retry 2 -H "User-Agent: ${DOWNLOAD_UA}" "$DOWNLOAD_URL" -o "$DOWNLOAD_PATH" 2>&1)
EXIR_CODE=${PIPESTATUS[0]}
EXIR_CODE=$?
HTTP_CODE=$(echo "$CURL_OUTPUT" | tail -n1)
if [ "$EXIR_CODE" -ne 0 ] || [ "$HTTP_CODE" -ne 200 ]; then
@@ -0,0 +1,179 @@
#!/bin/bash
. /usr/share/openclash/openclash_ps.sh
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_curl.sh
. /usr/share/openclash/uci.sh
set_lock() {
exec 888>"/tmp/lock/openclash_update_databases.lock" 2>/dev/null
flock -x 888 2>/dev/null
}
del_lock() {
flock -u 888 2>/dev/null
rm -rf "/tmp/lock/openclash_update_databases.lock" 2>/dev/null
}
set_lock
inc_job_counter
small_flash_memory=$(uci_get_config "small_flash_memory")
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)
restart=0
update_one() {
local type="$1"
local custom_key="$2"
local tmpfile="$3"
local path="$4"
local name="$5"
local min_size=10240
local download_url=""
local custom_url=$(uci_get_config "$custom_key")
if [ "$small_flash_memory" != "1" ]; then
mkdir -p /etc/openclash
else
mkdir -p /tmp/etc/openclash
fi
case "$type" in
geoip)
if [ -z "$custom_url" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
download_url="${github_address_mod}gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
else
download_url="${github_address_mod}https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
fi
else
download_url="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
fi
else
download_url="$custom_url"
fi
;;
geosite)
if [ -z "$custom_url" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
download_url="${github_address_mod}gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
else
download_url="${github_address_mod}https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
fi
else
download_url="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
fi
else
download_url="$custom_url"
fi
;;
geoasn)
if [ -z "$custom_url" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
download_url="${github_address_mod}gh/xishang0128/geoip@release/GeoLite2-ASN.mmdb"
else
download_url="${github_address_mod}https://github.com/xishang0128/geoip/releases/latest/download/GeoLite2-ASN.mmdb"
fi
else
download_url="https://github.com/xishang0128/geoip/releases/latest/download/GeoLite2-ASN.mmdb"
fi
else
download_url="$custom_url"
fi
;;
ipdb)
if [ -z "$custom_url" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
download_url="${github_address_mod}gh/alecthw/mmdb_china_ip_list@release/lite/Country.mmdb"
else
download_url="${github_address_mod}https://raw.githubusercontent.com/alecthw/mmdb_china_ip_list/release/lite/Country.mmdb"
fi
else
download_url="https://raw.githubusercontent.com/alecthw/mmdb_china_ip_list/release/lite/Country.mmdb"
fi
else
download_url="$custom_url"
fi
;;
*)
return 1
;;
esac
LOG_OUT "【$name】Start Downloading Database..."
DOWNLOAD_FILE_CURL "$download_url" "/tmp/$tmpfile" "$path"
DOWNLOAD_RESULT=$?
if [ "$DOWNLOAD_RESULT" -eq 0 ] && [ -s "/tmp/$tmpfile" ]; then
if head -c 512 "/tmp/$tmpfile" | grep -qiE "<!doctype|<html|<head|<body"; then
LOG_OUT "【$name】Download Failed: HTML Response Detected, Abort Update..."
rm -rf "/tmp/$tmpfile"
elif [ $(du -b "/tmp/$tmpfile" 2>/dev/null | awk '{print $1}' || echo "$min_size") -lt "$min_size" ]; then
LOG_OUT "【$name】Download Failed: File Size Too Small, Abort Update..."
rm -rf "/tmp/$tmpfile"
else
LOG_OUT "【$name】Download Success, Check Updated..."
cmp -s "/tmp/$tmpfile" "$path"
if [ "$?" -ne 0 ]; then
LOG_OUT "【$name】Has Been Updated, Starting To Replace The Old Version..."
case "$type" in
geoip)
rm -rf "/etc/openclash/geoip.dat"
;;
geosite)
rm -rf "/etc/openclash/geosite.dat"
;;
geoasn)
rm -rf "/etc/openclash/GeoLite2-ASN.mmdb"
;;
esac
mv "/tmp/$tmpfile" "$path" >/dev/null 2>&1
LOG_OUT "【$name】Update Successful!"
restart=1
else
LOG_OUT "【$name】No Change, Do Nothing..."
fi
fi
elif [ "$DOWNLOAD_RESULT" -eq 2 ]; then
LOG_OUT "【$name】No Change, Do Nothing..."
else
LOG_OUT "【$name】Update Error, Please Try Again Later..."
fi
rm -rf "/tmp/$tmpfile" >/dev/null 2>&1
}
case "$1" in
all)
update_one geoip "geoip_custom_url" "GeoIP.dat" "/etc/openclash/GeoIP.dat" "GeoIP.dat"
update_one geosite "geosite_custom_url" "GeoSite.dat" "/etc/openclash/GeoSite.dat" "GeoSite.dat"
update_one geoasn "geoasn_custom_url" "GeoLite2-ASN.mmdb" "/etc/openclash/ASN.mmdb" "ASN.mmdb"
update_one ipdb "geo_custom_url" "Country.mmdb" "/etc/openclash/Country.mmdb" "Country.mmdb"
;;
geoip)
update_one geoip "geoip_custom_url" "GeoIP.dat" "/etc/openclash/GeoIP.dat" "GeoIP.dat"
;;
geosite)
update_one geosite "geosite_custom_url" "GeoSite.dat" "/etc/openclash/GeoSite.dat" "GeoSite.dat"
;;
geoasn)
update_one geoasn "geoasn_custom_url" "GeoLite2-ASN.mmdb" "/etc/openclash/ASN.mmdb" "ASN.mmdb"
;;
ipdb)
update_one ipdb "geo_custom_url" "Country.mmdb" "/etc/openclash/Country.mmdb" "Country.mmdb"
;;
*)
echo "Usage: $0 {all|geoip|geosite|geoasn|ipdb}"
del_lock
dec_job_counter_and_restart "$restart"
exit 1
;;
esac
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
del_lock
@@ -1,70 +0,0 @@
#!/bin/bash
. /usr/share/openclash/openclash_ps.sh
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_curl.sh
. /usr/share/openclash/uci.sh
set_lock() {
exec 874>"/tmp/lock/openclash_geoasn.lock" 2>/dev/null
flock -x 874 2>/dev/null
}
del_lock() {
flock -u 874 2>/dev/null
rm -rf "/tmp/lock/openclash_geoasn.lock" 2>/dev/null
}
set_lock
inc_job_counter
small_flash_memory=$(uci_get_config "small_flash_memory")
GEOASN_CUSTOM_URL=$(uci_get_config "geoasn_custom_url")
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)
restart=0
if [ "$small_flash_memory" != "1" ]; then
geoasn_path="/etc/openclash/ASN.mmdb"
mkdir -p /etc/openclash
else
geoasn_path="/tmp/etc/openclash/ASN.mmdb"
mkdir -p /tmp/etc/openclash
fi
LOG_OUT "Start Downloading Geo ASN Database..."
if [ -z "$GEOASN_CUSTOM_URL" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/xishang0128/geoip@release/GeoLite2-ASN.mmdb"
else
DOWNLOAD_URL="${github_address_mod}https://github.com/xishang0128/geoip/releases/latest/download/GeoLite2-ASN.mmdb"
fi
else
DOWNLOAD_URL="https://github.com/xishang0128/geoip/releases/latest/download/GeoLite2-ASN.mmdb"
fi
else
DOWNLOAD_URL=$GEOASN_CUSTOM_URL
fi
DOWNLOAD_FILE_CURL "$DOWNLOAD_URL" "/tmp/GeoLite2-ASN.mmdb" "$geoasn_path"
DOWNLOAD_RESULT=$?
if [ "$DOWNLOAD_RESULT" -eq 0 ] && [ -s "/tmp/GeoLite2-ASN.mmdb" ]; then
LOG_OUT "Geo ASN Database Download Success, Check Updated..."
cmp -s /tmp/GeoLite2-ASN.mmdb "$geoasn_path"
if [ "$?" -ne "0" ]; then
LOG_OUT "Geo ASN Database Has Been Updated, Starting To Replace The Old Version..."
rm -rf "/etc/openclash/GeoLite2-ASN.mmdb"
mv /tmp/GeoLite2-ASN.mmdb "$geoasn_path" >/dev/null 2>&1
LOG_OUT "Geo ASN Database Update Successful!"
restart=1
else
LOG_OUT "Updated Geo ASN Database No Change, Do Nothing..."
fi
elif [ "$DOWNLOAD_RESULT" -eq 2 ]; then
LOG_OUT "Updated Geo ASN Database No Change, Do Nothing..."
else
LOG_OUT "Geo ASN Database Update Error, Please Try Again Later..."
fi
rm -rf /tmp/GeoLite2-ASN.mmdb >/dev/null 2>&1
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
del_lock
@@ -1,70 +0,0 @@
#!/bin/bash
. /usr/share/openclash/openclash_ps.sh
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_curl.sh
. /usr/share/openclash/uci.sh
set_lock() {
exec 873>"/tmp/lock/openclash_geoip.lock" 2>/dev/null
flock -x 873 2>/dev/null
}
del_lock() {
flock -u 873 2>/dev/null
rm -rf "/tmp/lock/openclash_geoip.lock" 2>/dev/null
}
set_lock
inc_job_counter
small_flash_memory=$(uci_get_config "small_flash_memory")
GEOIP_CUSTOM_URL=$(uci_get_config "geoip_custom_url")
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)
restart=0
if [ "$small_flash_memory" != "1" ]; then
geoip_path="/etc/openclash/GeoIP.dat"
mkdir -p /etc/openclash
else
geoip_path="/tmp/etc/openclash/GeoIP.dat"
mkdir -p /tmp/etc/openclash
fi
LOG_OUT "Start Downloading GeoIP Dat..."
if [ -z "$GEOIP_CUSTOM_URL" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
else
DOWNLOAD_URL="${github_address_mod}https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
fi
else
DOWNLOAD_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
fi
else
DOWNLOAD_URL=$GEOIP_CUSTOM_URL
fi
DOWNLOAD_FILE_CURL "$DOWNLOAD_URL" "/tmp/GeoIP.dat" "$geoip_path"
DOWNLOAD_RESULT=$?
if [ "$DOWNLOAD_RESULT" -eq 0 ] && [ -s "/tmp/GeoIP.dat" ]; then
LOG_OUT "GeoIP Dat Download Success, Check Updated..."
cmp -s /tmp/GeoIP.dat "$geoip_path"
if [ "$?" -ne "0" ]; then
LOG_OUT "GeoIP Dat Has Been Updated, Starting To Replace The Old Version..."
rm -rf "/etc/openclash/geoip.dat"
mv /tmp/GeoIP.dat "$geoip_path" >/dev/null 2>&1
LOG_OUT "GeoIP Dat Update Successful!"
restart=1
else
LOG_OUT "Updated GeoIP Dat No Change, Do Nothing..."
fi
elif [ "$DOWNLOAD_RESULT" -eq 2 ]; then
LOG_OUT "Updated GeoIP Dat No Change, Do Nothing..."
else
LOG_OUT "GeoIP Dat Update Error, Please Try Again Later..."
fi
rm -rf /tmp/GeoIP.dat >/dev/null 2>&1
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
del_lock
@@ -1,70 +0,0 @@
#!/bin/bash
. /usr/share/openclash/openclash_ps.sh
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_curl.sh
. /usr/share/openclash/uci.sh
set_lock() {
exec 874>"/tmp/lock/openclash_geosite.lock" 2>/dev/null
flock -x 874 2>/dev/null
}
del_lock() {
flock -u 874 2>/dev/null
rm -rf "/tmp/lock/openclash_geosite.lock" 2>/dev/null
}
set_lock
inc_job_counter
small_flash_memory=$(uci_get_config "small_flash_memory")
GEOSITE_CUSTOM_URL=$(uci_get_config "geosite_custom_url")
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)
restart=0
if [ "$small_flash_memory" != "1" ]; then
geosite_path="/etc/openclash/GeoSite.dat"
mkdir -p /etc/openclash
else
geosite_path="/tmp/etc/openclash/GeoSite.dat"
mkdir -p /tmp/etc/openclash
fi
LOG_OUT "Start Downloading GeoSite Database..."
if [ -z "$GEOSITE_CUSTOM_URL" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
else
DOWNLOAD_URL="${github_address_mod}https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
fi
else
DOWNLOAD_URL="https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
fi
else
DOWNLOAD_URL=$GEOSITE_CUSTOM_URL
fi
DOWNLOAD_FILE_CURL "$DOWNLOAD_URL" "/tmp/GeoSite.dat" "$geosite_path"
DOWNLOAD_RESULT=$?
if [ "$DOWNLOAD_RESULT" -eq 0 ] && [ -s "/tmp/GeoSite.dat" ]; then
LOG_OUT "GeoSite Database Download Success, Check Updated..."
cmp -s /tmp/GeoSite.dat "$geosite_path"
if [ "$?" -ne "0" ]; then
LOG_OUT "GeoSite Database Has Been Updated, Starting To Replace The Old Version..."
rm -rf "/etc/openclash/geosite.dat"
mv /tmp/GeoSite.dat "$geosite_path" >/dev/null 2>&1
LOG_OUT "GeoSite Database Update Successful!"
restart=1
else
LOG_OUT "Updated GeoSite Database No Change, Do Nothing..."
fi
elif [ "$DOWNLOAD_RESULT" -eq 2 ]; then
LOG_OUT "Updated GeoSite Database No Change, Do Nothing..."
else
LOG_OUT "GeoSite Database Update Error, Please Try Again Later..."
fi
rm -rf /tmp/GeoSite.dat >/dev/null 2>&1
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
del_lock
@@ -1,69 +0,0 @@
#!/bin/bash
. /usr/share/openclash/openclash_ps.sh
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_curl.sh
. /usr/share/openclash/uci.sh
set_lock() {
exec 880>"/tmp/lock/openclash_ipdb.lock" 2>/dev/null
flock -x 880 2>/dev/null
}
del_lock() {
flock -u 880 2>/dev/null
rm -rf "/tmp/lock/openclash_ipdb.lock" 2>/dev/null
}
set_lock
inc_job_counter
small_flash_memory=$(uci_get_config "small_flash_memory")
GEOIP_CUSTOM_URL=$(uci_get_config "geo_custom_url")
github_address_mod=$(uci_get_config "github_address_mod" || echo 0)
restart=0
if [ "$small_flash_memory" != "1" ]; then
geoip_path="/etc/openclash/Country.mmdb"
mkdir -p /etc/openclash
else
geoip_path="/tmp/etc/openclash/Country.mmdb"
mkdir -p /tmp/etc/openclash
fi
LOG_OUT "Start Downloading Geoip Database..."
if [ -z "$GEOIP_CUSTOM_URL" ]; then
if [ "$github_address_mod" != "0" ]; then
if [ "$github_address_mod" == "https://cdn.jsdelivr.net/" ] || [ "$github_address_mod" == "https://fastly.jsdelivr.net/" ] || [ "$github_address_mod" == "https://testingcf.jsdelivr.net/" ]; then
DOWNLOAD_URL="${github_address_mod}gh/alecthw/mmdb_china_ip_list@release/lite/Country.mmdb"
else
DOWNLOAD_URL="${github_address_mod}https://raw.githubusercontent.com/alecthw/mmdb_china_ip_list/release/lite/Country.mmdb"
fi
else
DOWNLOAD_URL="https://raw.githubusercontent.com/alecthw/mmdb_china_ip_list/release/lite/Country.mmdb"
fi
else
DOWNLOAD_URL=$GEOIP_CUSTOM_URL
fi
DOWNLOAD_FILE_CURL "$DOWNLOAD_URL" "/tmp/Country.mmdb" "$geoip_path"
DOWNLOAD_RESULT=$?
if [ "$DOWNLOAD_RESULT" -eq 0 ] && [ -s "/tmp/Country.mmdb" ]; then
LOG_OUT "Geoip Database Download Success, Check Updated..."
cmp -s /tmp/Country.mmdb "$geoip_path"
if [ "$?" -ne 0 ]; then
LOG_OUT "Geoip Database Has Been Updated, Starting To Replace The Old Version..."
mv /tmp/Country.mmdb "$geoip_path" >/dev/null 2>&1
LOG_OUT "Geoip Database Update Successful!"
restart=1
else
LOG_OUT "Updated Geoip Database No Change, Do Nothing..."
fi
elif [ "$DOWNLOAD_RESULT" -eq 2 ]; then
LOG_OUT "Updated Geoip Database No Change, Do Nothing..."
else
LOG_OUT "Geoip Database Update Error, Please Try Again Later..."
fi
rm -rf /tmp/Country.mmdb >/dev/null 2>&1
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
del_lock
@@ -46,7 +46,7 @@ function unlock_auto_select()
local no_old_region_unlock = {}
local full_support_list = {}
local fallback_select = {}
local gorup_i18 = "Group:"
local gorup_i18 = "【"..type.."】Group:"
local no_group_find = "failed to search based on keywords and automatically obtain the group, please confirm the validity of the regex!"
local full_support_no_area = "full support"
local full_support = "full support, area:"
@@ -119,6 +119,10 @@ function unlock_auto_select()
key_group = FS.uci_get_config("config", "stream_auto_select_group_key_google_not_cn") or "google|谷歌"
elseif type == "OpenAI" then
key_group = FS.uci_get_config("config", "stream_auto_select_group_key_openai") or "OpenAI|ChatGPT"
elseif type == "Claude" then
key_group = FS.uci_get_config("config", "stream_auto_select_group_key_claude") or "Claude"
elseif type == "Gemini" then
key_group = FS.uci_get_config("config", "stream_auto_select_group_key_gemini") or "Gemini"
end
if not key_group then key_group = type end
else
@@ -148,9 +152,9 @@ function unlock_auto_select()
--test now proxy
region = proxy_unlock_test()
if table_include(groups, now_name) then
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..group_show.."】"
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..group_show.."】"
else
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..group_show.." ➟ "..now_name.."】"
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..group_show.." ➟ "..now_name.."】"
end
if status ~= 2 and status ~= 4 then
region = proxy_unlock_test()
@@ -244,7 +248,7 @@ function unlock_auto_select()
end
end
if #(value.all) == 0 then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..no_nodes_filter)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..no_nodes_filter)
break
end
--loop proxy test
@@ -274,16 +278,16 @@ function unlock_auto_select()
end
end
if #(proxies) == 0 then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..group_show.."】"..no_nodes_filter)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..group_show.."】"..no_nodes_filter)
break
end
for p = 1, #(proxies) do
proxy = proxies[p]
if table_include(groups, proxy) then
group_now = get_group_now(info, proxy)
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..group_show.." ➟ "..group_now.."】"
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..group_show.." ➟ "..group_now.."】"
else
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..group_show.." ➟ "..proxy.."】"
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..group_show.." ➟ "..proxy.."】"
end
--skip tested proxy
while true do
@@ -303,7 +307,7 @@ function unlock_auto_select()
table.insert(full_support_list, {value.all[i], group_name, proxy, region})
if not all_test then
print(now..full_support.."【"..region.."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_success.."【"..proxy.."】"..area_i18.."【"..region.."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_success.."【"..proxy.."】"..area_i18.."【"..region.."】")
else
print(now..full_support.."【"..region.."】")
end
@@ -312,7 +316,7 @@ function unlock_auto_select()
table.insert(full_support_list, {value.all[i], group_name, proxy})
if not all_test then
print(now..full_support_no_area)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_success.."【"..proxy.."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_success.."【"..proxy.."】")
else
print(now..full_support_no_area)
end
@@ -372,16 +376,16 @@ function unlock_auto_select()
end
region = proxy_unlock_test()
if table_include(groups, now_name) then
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..group_show.."】"
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..group_show.."】"
else
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..group_show.." ➟ "..now_name.."】"
now = os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..group_show.." ➟ "..now_name.."】"
end
if status == 2 then
if region ~= "" then
table.insert(full_support_list, {value.all[i], group_name, value.all[i], region})
if not all_test then
print(now..full_support.."【"..region.."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, now_name).."】"..area_i18.."【"..region.."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, now_name).."】"..area_i18.."【"..region.."】")
else
print(now..full_support.."【"..region.."】")
end
@@ -390,7 +394,7 @@ function unlock_auto_select()
table.insert(full_support_list, {value.all[i], group_name, value.all[i]})
if not all_test then
print(now..full_support_no_area)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, now_name).."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, now_name).."】")
else
print(now..full_support_no_area)
end
@@ -469,24 +473,24 @@ function unlock_auto_select()
group_now = group_now .. area_i18 .. "【"..v[4].."】"
end
if #full_support_list > 0 then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_all_full_support..group_now)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_all_full_support..group_now)
elseif #no_old_region_unlock > 0 then
if not all_test then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_success_no_old_region..group_now)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_success_no_old_region..group_now)
else
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_all_no_old_region..group_now)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_all_no_old_region..group_now)
end
elseif #other_region_unlock > 0 then
if not all_test then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_faild_other_region..group_now)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_faild_other_region..group_now)
else
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_all_other_region..group_now)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_all_other_region..group_now)
end
else
if not all_test then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_faild..group_now)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_faild..group_now)
else
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_all_faild..group_now)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_all_faild..group_now)
end
end
close_connections()
@@ -499,7 +503,7 @@ function unlock_auto_select()
else
group_now = value.name.." ➟ "..proxy_default
end
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_faild.."【"..group_now.."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_faild.."【"..group_now.."】")
end
end
elseif #nodes_filter(get_group_now(info, value.name), info) ~= 0 then
@@ -509,12 +513,12 @@ function unlock_auto_select()
if not all_test then
print(now..full_support.."【"..region.."】")
end
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, value.name).."】"..area_i18.."【"..region.."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, value.name).."】"..area_i18.."【"..region.."】")
else
if not all_test then
print(now..full_support_no_area)
end
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, value.name).."】")
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..value.name.."】"..select_success.."【"..get_group_now(info, value.name).."】")
end
if not all_test then
break
@@ -548,7 +552,7 @@ function unlock_auto_select()
if status == 2 and not all_test then break end
end
if not group_match and not auto_get_group then
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..type.." "..gorup_i18.."【"..key_group.."】"..no_group_find)
print(os.date("%Y-%m-%d %H:%M:%S").." [Info] "..gorup_i18.."【"..key_group.."】"..no_group_find)
end
--write cache
FS.writefile(unlock_cache_file, JSON.stringify(unlock_cache_info))
@@ -817,6 +821,10 @@ function nodes_filter(t, info)
regex = FS.uci_get_config("config", "stream_auto_select_node_key_google_not_cn") or ""
elseif type == "OpenAI" then
regex = FS.uci_get_config("config", "stream_auto_select_node_key_openai") or ""
elseif type == "Claude" then
regex = FS.uci_get_config("config", "stream_auto_select_node_key_claude") or ""
elseif type == "Gemini" then
regex = FS.uci_get_config("config", "stream_auto_select_node_key_gemini") or ""
end
if class_type(t) == "table" then
@@ -877,6 +885,10 @@ function proxy_unlock_test()
region = google_not_cn_test()
elseif type == "OpenAI" then
region = openai_unlock_test()
elseif type == "Claude" then
region = claude_unlock_test()
elseif type == "Gemini" then
region = gemini_unlock_test()
end
return region
end
@@ -908,6 +920,10 @@ function auto_get_policy_group(passwd, ip, port)
SYS.call('curl -sL -m 5 --limit-rate 1k -o /dev/null https://timeline.google.com &')
elseif type == "OpenAI" then
SYS.call('curl -sL -m 5 --limit-rate 1k -o /dev/null https://chatgpt.com/ &')
elseif type == "Claude" then
SYS.call('curl -sL -m 5 --limit-rate 1k -o /dev/null https://claude.ai/ &')
elseif type == "Gemini" then
SYS.call('curl -sL -m 5 --limit-rate 1k -o /dev/null https://gemini.google.com/ &')
end
os.execute("sleep 1")
con = SYS.exec(string.format('curl -sL -m 5 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://%s:%s/connections', passwd, ip, port))
@@ -976,6 +992,16 @@ function auto_get_policy_group(passwd, ip, port)
auto_get_group = con.connections[i].chains[#(con.connections[i].chains)]
break
end
elseif type == "Claude" then
if string.match(con.connections[i].metadata.host, "claude%.ai") then
auto_get_group = con.connections[i].chains[#(con.connections[i].chains)]
break
end
elseif type == "Gemini" then
if string.match(con.connections[i].metadata.host, "gemini%.google%.com") then
auto_get_group = con.connections[i].chains[#(con.connections[i].chains)]
break
end
end
end
end
@@ -1571,6 +1597,82 @@ function openai_unlock_test()
return region
end
function claude_unlock_test()
status = 0
local url = "https://claude.ai/"
local region_url = "https://claude.ai/cdn-cgi/trace"
local regex = FS.uci_get_config("config", "stream_auto_select_region_key_claude") or ""
local region = ""
local old_region = get_old_region()
local old_regex = get_old_regex()
local data = SYS.exec(string.format("curl -sIL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, url))
if data then
if string.find(data, "App unavailable in region") then
status = 1
else
status = 2
local region_data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, region_url))
if region_data and string.match(region_data, "loc=%a+") then
region = string.upper(string.sub(string.match(region_data, "loc=%a+"), 5, -1))
if not datamatch(region, regex) then
status = 3
elseif old_regex ~= regex and not all_test then
status = 2
elseif old_region ~= "" and region ~= old_region and not all_test then
status = 4
end
if status == 2 and not all_test and region ~= old_region then
write_cache(type, "old_region", region)
end
if status == 2 and not all_test and regex ~= old_regex then
write_cache(type, "old_regex", regex)
end
end
end
end
return region
end
function gemini_unlock_test()
status = 0
local url = "https://gemini.google.com/"
local regex = FS.uci_get_config("config", "stream_auto_select_region_key_gemini") or ""
local region = ""
local old_region = get_old_region()
local old_regex = get_old_regex()
local data = SYS.exec(string.format("curl -sL --connect-timeout 5 -m 5 --speed-time 5 --speed-limit 1 --retry 2 -H 'Accept-Language: en' -H 'Content-Type: application/json' -H 'User-Agent: %s' '%s'", UA, url))
if not data or string.sub(data, 1, 4) == "curl" then
status = 1
return region
end
if string.find(data, "45631641,null,true", 1, true) then
local cc = string.match(data, ',2,1,200,"([A-Z][A-Z][A-Z])"')
if cc and cc ~= "" then
region = cc
end
status = 2
if region ~= "" then
if not datamatch(region, regex) then
status = 3
elseif old_regex ~= regex and not all_test then
status = 2
elseif old_region ~= "" and region ~= old_region and not all_test then
status = 4
end
if status == 2 and not all_test and region ~= old_region then
write_cache(type, "old_region", region)
end
if status == 2 and not all_test and regex ~= old_regex then
write_cache(type, "old_regex", regex)
end
end
else
status = 1
end
return region
end
function network_test()
local test_url1 = "https://www.gstatic.com/generate_204"
local test_url2 = "https://cp.cloudflare.com/generate_204"
@@ -16,7 +16,6 @@ del_lock() {
set_lock
inc_job_counter
restart=0
if [ -n "$1" ] && [ "$1" != "one_key_update" ]; then
/usr/share/openclash/openclash_version.sh "$1" 2>/dev/null
@@ -29,7 +28,7 @@ fi
if [ ! -f "/tmp/openclash_last_version" ]; then
LOG_ERROR "Failed to get version information, please try again later..."
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
dec_job_counter_and_restart "0"
del_lock
exit 0
fi
@@ -57,7 +56,7 @@ LAST_VER=$(sed -n 1p "$LAST_OPVER" 2>/dev/null |sed "s/^v//g" |tr -d "\n")
if [ -x "/bin/opkg" ]; then
OP_CV=$(rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
OP_CV=$(apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
OP_CV=$(apk list luci-app-openclash 2>/dev/null|grep "installed" | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
fi
OP_LV=$(sed -n 1p "$LAST_OPVER" 2>/dev/null |sed "s/^v//g" |tr -d "\n")
RELEASE_BRANCH=$(uci_get_config "release_branch" || echo "master")
@@ -69,14 +68,12 @@ if [ "$1" = "one_key_update" ]; then
LOG_TIP "If the download fails, try setting the CDN in Overwrite Settings - General Settings - Github Address Modify Options"
fi
if [ -n "$2" ]; then
/usr/share/openclash/openclash_core.sh "Meta" "$1" "$2" >/dev/null 2>&1 &
/usr/share/openclash/openclash_core.sh "Meta" "$1" "$2" >/dev/null 2>&1
github_address_mod="$2"
else
/usr/share/openclash/openclash_core.sh "Meta" "$1" >/dev/null 2>&1 &
/usr/share/openclash/openclash_core.sh "Meta" "$1" >/dev/null 2>&1
github_address_mod=0
fi
wait
else
if [ "$github_address_mod" = "0" ]; then
LOG_TIP "If the download fails, try setting the CDN in Overwrite Settings - General Settings - Github Address Modify Options"
@@ -175,9 +172,8 @@ if [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" && [
elif [ -x "/usr/bin/apk" ]; then
LOG_ERROR "【OpenClash - v$LAST_VER】Pre update test failed after 3 attempts, the file is saved in /tmp/openclash.apk, please try to update manually with【apk add -q --force-overwrite --clean-protected --allow-untrusted /tmp/openclash.apk】"
fi
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
dec_job_counter_and_restart "0"
del_lock
exit 0
fi
@@ -191,8 +187,8 @@ if [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" && [
LOG_ERROR "【OpenClash - v$LAST_VER】Download Failed after 3 attempts, please check the network or try again later!"
rm -rf /tmp/openclash.ipk >/dev/null 2>&1
rm -rf /tmp/openclash.apk >/dev/null 2>&1
dec_job_counter_and_restart "0"
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
del_lock
exit 0
fi
@@ -200,9 +196,8 @@ if [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" && [
done
cat > /tmp/openclash_update.sh <<"EOF"
#!/bin/sh
START_LOG="/tmp/openclash_start.log"
LOG_FILE="/tmp/openclash.log"
LOGTIME=$(date "+%Y-%m-%d %H:%M:%S")
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_ps.sh
UPDATE_LOCK="/tmp/lock/openclash_update_install.lock"
mkdir -p /tmp/lock
@@ -224,27 +219,6 @@ fi
trap 'del_update_lock; exit' INT TERM EXIT
LOG_ERROR()
{
if [ -n "${1}" ]; then
echo -e "${1}" > $START_LOG
echo -e "${LOGTIME} [Error] ${1}" >> $LOG_FILE
fi
}
LOG_TIP()
{
if [ -n "${1}" ]; then
echo -e "${1}" > $START_LOG
echo -e "${LOGTIME} [Tip] ${1}" >> $LOG_FILE
fi
}
SLOG_CLEAN()
{
echo "" > $START_LOG
}
check_install_success()
{
local target_version="$1"
@@ -253,7 +227,7 @@ check_install_success()
if [ -x "/bin/opkg" ]; then
current_version=$(rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' 2>/dev/null)
elif [ -x "/usr/bin/apk" ]; then
current_version=$(apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
current_version=$(apk list luci-app-openclash 2>/dev/null |grep "installed" | grep -oE '[0-9]+(\.[0-9]+)*' | head -1 2>/dev/null)
fi
if [ -n "$current_version" ] && [ "$current_version" = "$target_version" ]; then
@@ -266,41 +240,39 @@ check_install_success()
install_missing_packages() {
local installed_before="$1"
if [ -x "/bin/opkg" ]; then
if [ -n "$installed_before" ]; then
for pkg in $installed_before; do
if ! opkg status "$pkg" >/dev/null 2>&1; then
local retry_count=0
local max_retries=3
while [ $retry_count -lt $max_retries ]; do
retry_count=$((retry_count + 1))
local retry_count=0
local max_retries=3
if [ -x "/bin/opkg" ]; then
if opkg status "$pkg" >/dev/null 2>&1; then
continue
fi
elif [ -x "/usr/bin/apk" ]; then
if apk list "$pkg" |grep "installed" >/dev/null 2>&1; then
continue
fi
fi
LOG_TIP "【$pkg】depended package reinstalling..."
while [ $retry_count -lt $max_retries ]; do
retry_count=$((retry_count + 1))
if [ -x "/bin/opkg" ]; then
opkg install "$pkg"
if [ $? -eq 0 ]; then
break
else
if [ $retry_count -lt $max_retries ]; then
sleep 2
fi
fi
done
fi
done
elif [ -x "/usr/bin/apk" ]; then
for pkg in $installed_before; do
if ! apk info "$pkg" >/dev/null 2>&1; then
local retry_count=0
local max_retries=3
while [ $retry_count -lt $max_retries ]; do
retry_count=$((retry_count + 1))
elif [ -x "/usr/bin/apk" ]; then
apk add "$pkg"
if [ $? -eq 0 ]; then
break
fi
if [ $? -eq 0 ]; then
break
else
if [ $retry_count -lt $max_retries ]; then
sleep 2
else
if [ $retry_count -lt $max_retries ]; then
sleep 2
fi
LOG_ERROR "【$pkg】failed to install, please try to install it manually..."
fi
done
fi
fi
done
done
fi
}
@@ -324,7 +296,7 @@ while [ $install_retry_count -lt $max_install_retries ]; do
opkg install /tmp/openclash.ipk
elif [ -x "/usr/bin/apk" ]; then
for pkg in $packages_to_check; do
if apk info "$pkg" >/dev/null 2>&1; then
if apk list "$pkg" |grep "installed" >/dev/null 2>&1; then
installed_before="$installed_before $pkg"
fi
done
@@ -357,9 +329,9 @@ else
elif [ -x "/usr/bin/apk" ]; then
LOG_ERROR "OpenClash update failed after 3 attempts, the file is saved in /tmp/openclash.apk, please try to update manually with【apk add -q --force-overwrite --clean-protected --allow-untrusted /tmp/openclash.apk】"
fi
SLOG_CLEAN
fi
dec_job_counter_and_restart "0"
SLOG_CLEAN
del_update_lock
EOF
chmod 4755 /tmp/openclash_update.sh
@@ -367,6 +339,8 @@ EOF
if [ ! -f "/tmp/openclash_update.sh" ] || [ ! -s "/tmp/openclash_update.sh" ] || [ ! -x "/tmp/openclash_update.sh" ]; then
LOG_ERROR "Failed to create update script!"
rm -rf /tmp/openclash_update.sh
dec_job_counter_and_restart "0"
SLOG_CLEAN
del_lock
exit 1
fi
@@ -405,8 +379,8 @@ else
else
LOG_TIP "OpenClash has not been updated, stop continuing!"
fi
dec_job_counter_and_restart "0"
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
fi
del_lock
@@ -5,20 +5,10 @@
. /usr/share/openclash/uci.sh
LOG_FILE="/tmp/openclash.log"
CONFIG_FILE="/etc/openclash/$(uci_get_config "config_path" |awk -F '/' '{print $5}' 2>/dev/null)"
ipv6_enable=$(uci_get_config "ipv6_enable" || echo 0)
enable_redirect_dns=$(uci_get_config "enable_redirect_dns")
dns_port=$(uci_get_config "dns_port")
disable_masq_cache=$(uci_get_config "disable_masq_cache")
cfg_update_interval=$(uci_get_config "config_update_interval" || echo 60)
log_size=$(uci_get_config "log_size" || echo 1024)
router_self_proxy=$(uci_get_config "router_self_proxy" || echo 1)
stream_auto_select_interval=$(uci_get_config "stream_auto_select_interval" || echo 30)
skip_proxy_address=$(uci_get_config "skip_proxy_address" || echo 0)
CFG_UPDATE_INT=1
CFG_UPDATE_INT=0
SKIP_PROXY_ADDRESS=1
SKIP_PROXY_ADDRESS_INTERVAL=30
STREAM_AUTO_SELECT=1
STREAM_AUTO_SELECT=0
FIREWALL_RELOAD=0
MAX_FIREWALL_RELOAD=3
FW4=$(command -v fw4)
@@ -155,11 +145,20 @@ end" 2>/dev/null >> $LOG_FILE
while :;
do
CONFIG_FILE="/etc/openclash/$(uci_get_config "config_path" |awk -F '/' '{print $5}' 2>/dev/null)"
ipv6_enable=$(uci_get_config "ipv6_enable" || echo 0)
enable_redirect_dns=$(uci_get_config "enable_redirect_dns")
dns_port=$(uci_get_config "dns_port")
disable_masq_cache=$(uci_get_config "disable_masq_cache")
log_size=$(uci_get_config "log_size" || echo 1024)
router_self_proxy=$(uci_get_config "router_self_proxy" || echo 1)
skip_proxy_address=$(uci_get_config "skip_proxy_address" || echo 0)
cfg_update=$(uci_get_config "auto_update")
cfg_update_mode=$(uci_get_config "config_auto_update_mode")
cfg_update_interval_now=$(uci_get_config "config_update_interval" || echo 60)
cfg_update_interval=$(uci_get_config "config_update_interval" || echo 60)
stream_auto_select=$(uci_get_config "stream_auto_select" || echo 0)
stream_auto_select_interval_now=$(uci_get_config "stream_auto_select_interval" || echo 30)
stream_auto_select_interval=$(uci_get_config "stream_auto_select_interval" || echo 30)
stream_auto_select_netflix=$(uci_get_config "stream_auto_select_netflix" || echo 0)
stream_auto_select_disney=$(uci_get_config "stream_auto_select_disney" || echo 0)
stream_auto_select_hbo_max=$(uci_get_config "stream_auto_select_hbo_max" || echo 0)
@@ -172,6 +171,8 @@ do
stream_auto_select_bilibili=$(uci_get_config "stream_auto_select_bilibili" || echo 0)
stream_auto_select_google_not_cn=$(uci_get_config "stream_auto_select_google_not_cn" || echo 0)
stream_auto_select_openai=$(uci_get_config "stream_auto_select_openai" || echo 0)
stream_auto_select_claude=$(uci_get_config "stream_auto_select_claude" || echo 0)
stream_auto_select_gemini=$(uci_get_config "stream_auto_select_gemini" || echo 0)
upnp_lease_file=$(uci -q get upnpd.config.upnp_lease_file)
#wait for core start complete
@@ -362,66 +363,72 @@ fi
## 配置文件循环更新
if [ "$cfg_update" -eq 1 ] && [ "$cfg_update_mode" -eq 1 ]; then
[ "$cfg_update_interval" -ne "$cfg_update_interval_now" ] && CFG_UPDATE_INT=0 && cfg_update_interval="$cfg_update_interval_now"
if [ "$CFG_UPDATE_INT" -ne 0 ]; then
[ "$(expr "$CFG_UPDATE_INT" % "$cfg_update_interval_now")" -eq 0 ] && /usr/share/openclash/openclash.sh
[ "$(expr "$CFG_UPDATE_INT" % "$cfg_update_interval")" -eq 0 ] && /usr/share/openclash/openclash.sh
fi
CFG_UPDATE_INT=$(expr "$CFG_UPDATE_INT" + 1)
fi
##STREAMING_UNLOCK_CHECK
if [ "$stream_auto_select" -eq 1 ] && [ "$router_self_proxy" -eq 1 ]; then
[ "$stream_auto_select_interval" -ne "$stream_auto_select_interval_now" ] && STREAM_AUTO_SELECT=1 && stream_auto_select_interval="$stream_auto_select_interval_now"
if [ "$STREAM_AUTO_SELECT" -ne 0 ]; then
if [ "$(expr "$STREAM_AUTO_SELECT" % "$stream_auto_select_interval_now")" -eq 0 ] || [ "$STREAM_AUTO_SELECT" -eq 1 ]; then
if [ "$(expr "$STREAM_AUTO_SELECT" % "$stream_auto_select_interval")" -eq 0 ] || [ "$STREAM_AUTO_SELECT" -eq 1 ]; then
if [ "$stream_auto_select_netflix" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Netflix Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【Netflix】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Netflix" >> $LOG_FILE
fi
if [ "$stream_auto_select_disney" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Disney Plus Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【Disney Plus】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Disney Plus" >> $LOG_FILE
fi
if [ "$stream_auto_select_google_not_cn" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Google Not CN Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【Google Not CN】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Google" >> $LOG_FILE
fi
if [ "$stream_auto_select_ytb" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For YouTube Premium Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【YouTube Premium】..."
/usr/share/openclash/openclash_streaming_unlock.lua "YouTube Premium" >> $LOG_FILE
fi
if [ "$stream_auto_select_prime_video" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Amazon Prime Video Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【Amazon Prime Video】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Amazon Prime Video" >> $LOG_FILE
fi
if [ "$stream_auto_select_hbo_max" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For HBO Max Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【HBO Max】..."
/usr/share/openclash/openclash_streaming_unlock.lua "HBO Max" >> $LOG_FILE
fi
if [ "$stream_auto_select_tvb_anywhere" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For TVB Anywhere+ Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【TVB Anywhere+】..."
/usr/share/openclash/openclash_streaming_unlock.lua "TVB Anywhere+" >> $LOG_FILE
fi
if [ "$stream_auto_select_dazn" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For DAZN Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【DAZN】..."
/usr/share/openclash/openclash_streaming_unlock.lua "DAZN" >> $LOG_FILE
fi
if [ "$stream_auto_select_paramount_plus" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Paramount Plus Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【Paramount Plus】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Paramount Plus" >> $LOG_FILE
fi
if [ "$stream_auto_select_discovery_plus" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Discovery Plus Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【Discovery Plus】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Discovery Plus" >> $LOG_FILE
fi
if [ "$stream_auto_select_bilibili" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Bilibili Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【Bilibili】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Bilibili" >> $LOG_FILE
fi
if [ "$stream_auto_select_openai" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For OpenAI Unlock..."
LOG_TIP "Start Auto Select Unlock Proxy For【OpenAI】..."
/usr/share/openclash/openclash_streaming_unlock.lua "OpenAI" >> $LOG_FILE
fi
if [ "$stream_auto_select_claude" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Claude】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Claude" >> $LOG_FILE
fi
if [ "$stream_auto_select_gemini" -eq 1 ]; then
LOG_TIP "Start Auto Select Unlock Proxy For【Gemini】..."
/usr/share/openclash/openclash_streaming_unlock.lua "Gemini" >> $LOG_FILE
fi
fi
fi
STREAM_AUTO_SELECT=$(expr "$STREAM_AUTO_SELECT" + 1)
@@ -672,6 +672,7 @@ proxies: # socks5
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
# ip-version: ipv4
# vless
@@ -762,6 +763,7 @@ proxies: # socks5
grpc-opts:
grpc-service-name: "grpc"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
@@ -790,6 +792,69 @@ proxies: # socks5
# v2ray-http-upgrade: false
# v2ray-http-upgrade-fast-open: false
- name: "vless-xhttp"
type: vless
server: server
port: 443
uuid: uuid
udp: true
tls: true
network: xhttp
alpn:
- h2
# ech-opts: ...
# reality-opts: ...
# skip-cert-verify: false
# fingerprint: ...
# certificate: ...
# private-key: ...
servername: xxx.com
client-fingerprint: chrome
encryption: ""
xhttp-opts:
path: "/"
host: xxx.com
# mode: "stream-one" # Available: "stream-one", "stream-up" or "packet-up"
# headers:
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# reuse-settings: # aka XMUX
# max-connections: "16-32"
# max-concurrency: "0"
# c-max-reuse-times: "0"
# h-max-request-times: "600-900"
# h-max-reusable-secs: "1800-3000"
# download-settings:
# ## xhttp part
# path: "/"
# host: xxx.com
# headers:
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# reuse-settings: # aka XMUX
# max-connections: "16-32"
# max-concurrency: "0"
# c-max-reuse-times: "0"
# h-max-request-times: "600-900"
# h-max-reusable-secs: "1800-3000"
# ## proxy part
# server: server
# port: 443
# tls: true
# alpn:
# - h2
# ech-opts: ...
# reality-opts: ...
# skip-cert-verify: false
# fingerprint: ...
# certificate: ...
# private-key: ...
# servername: xxx.com
# client-fingerprint: chrome
# Trojan
- name: "trojan"
type: trojan
@@ -833,6 +898,7 @@ proxies: # socks5
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
- name: trojan-ws
server: server
@@ -1080,6 +1146,8 @@ proxies: # socks5
# multiplexing: MULTIPLEXING_LOW
# 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT,否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD
# handshake-mode: HANDSHAKE_STANDARD
# 一个 base64 字符串用于微调网络行为
# traffic-pattern: ""
# sudoku
- name: sudoku
@@ -1090,15 +1158,17 @@ proxies: # socks5
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;且 enable-pure-downlink=false 时不可用)
padding-min: 2 # 最小填充率(0-100)
padding-max: 7 # 最大填充率(0-100,必须 >= padding-min)
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
http-mask: true # 是否启用http掩码
# http-mask-mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll);stream/poll/auto 支持走 CDN/反代
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 http(不会根据端口自动推断)
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
# http-mask-multiplex: off # 可选:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 http-mask-mode=stream/poll/auto 生效)
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),非空时覆盖 custom-table
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
httpmask:
disable: false # true 禁用所有 HTTP 伪装/隧道
mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
# tls: true # 可选:按需开启 HTTPS/WSS
# host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 mode 为 stream/poll/auto/ws 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
# multiplex: "off" # 可选字符串:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 mode=stream/poll/auto 生效;ws 强制 off)
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
# anytls
@@ -1118,6 +1188,23 @@ proxies: # socks5
# - http/1.1
# skip-cert-verify: true
# trusttunnel
- name: trusttunnel
type: trusttunnel
server: 1.2.3.4
port: 443
username: username
password: password
# client-fingerprint: chrome
health-check: true
udp: true
# sni: "example.com"
# alpn:
# - h2
# skip-cert-verify: true
# quic: true # 默认为false
# congestion-controller: bbr
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out"
type: dns
@@ -1555,6 +1642,12 @@ listeners:
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层
# xhttp-config: # 如果不为空则开启 xhttp 传输层
# path: "/"
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# no-sse-header: false
# sc-stream-up-server-secs: "20-80"
# -------------------------
# vless encryption服务端配置:
# (原生外观 / 只 XOR 公钥 / 全随机数。1-RTT 每次下发随机 300 到 600 秒的 ticket 以便 0-RTT 复用 / 只允许 1-RTT)
@@ -1631,6 +1724,8 @@ listeners:
users:
username1: password1
username2: password2
# 一个 base64 字符串用于微调网络行为
# traffic-pattern: ""
- name: sudoku-in-1
type: sudoku
@@ -1640,14 +1735,18 @@ listeners:
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;且 enable-pure-downlink=false 时不可用)
padding-min: 1 # 最小填充率(0-100)
padding-max: 15 # 最大填充率(0-100,必须 >= padding-min)
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于多表轮换;非空时覆盖 custom-table
handshake-timeout: 5 # 可选(秒)
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false)
# http-mask-mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll);stream/poll/auto 支持走 CDN/反代
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
httpmask:
disable: false # true 禁用所有 HTTP 伪装/隧道
mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
#
# fallback: "127.0.0.1:80" # 可选:用于可连接请求的回落转发,可与其他服务共端口
@@ -1734,6 +1833,30 @@ listeners:
# masquerade: http://127.0.0.1:8080 #作为反向代理
# masquerade: https://127.0.0.1:8080 #作为反向代理
- name: trusttunnel-in-1
type: trusttunnel
port: 10821 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
listen: 0.0.0.0
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
users:
- username: 1
password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径
private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径
network: ["tcp", "udp"] # http2+http3
congestion-controller: bbr
# 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空
# client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify"
# client-auth-cert: string # 证书 PEM 格式,或者 证书的路径
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 注意,listeners中的tun仅提供给高级用户使用,普通用户应使用顶层配置中的tun
- name: tun-in-1
type: tun
@@ -41,7 +41,7 @@ if [ "$1" = "fake-ip" ] && [ "$enable_redirect_dns" != "2" ]; then
awk -v mode="$fake_ip_filter_mode" '
!/^$/ && !/^#/ {
# 跳过IPv4和IPv6地址
if ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ || $0 ~ /:/) {
if ($0 ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(\/[0-9]+)?$/ || $0 ~ /:/) {
next
}
if (mode == "blacklist") {
@@ -185,7 +185,7 @@ set_disable_qtype()
yml_dns_get()
{
local section="$1" regex='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$'
local enabled port type ip group dns_type dns_address interface specific_group node_resolve http3 ecs_subnet ecs_override disable_qtype_param
local enabled port type ip group dns_type dns_address interface specific_group node_resolve direct_nameserver http3 skip_cert_verify ecs_subnet ecs_override disable_qtype_param disable_qtype disable_ipv4 disable_ipv6 disable_reuse
config_get_bool "enabled" "$section" "enabled" "1"
[ "$enabled" = "0" ] && return
@@ -378,6 +378,8 @@ begin
smart_collect = '${44}' == '1'
smart_collect_size = '${45}'
fake_ip_range6 = '${46}'
fake_ip_range6_enable = '${47}' == '1'
global_ua = '${48}'
default_dashboard = '$default_dashboard'
yacd_type = '$yacd_type'
dashboard_type = '$dashboard_type'
@@ -408,6 +410,7 @@ begin
Value['external-controller'] = '0.0.0.0:' + controller_port
Value['secret'] = secret
Value['bind-address'] = '*'
Value['global-ua'] = global_ua if global_ua != '0'
Value['external-ui'] = '/usr/share/openclash/ui'
Value['external-ui-name'] = default_dashboard
case default_dashboard
@@ -480,7 +483,7 @@ begin
else
Value['dns']['enhanced-mode'] = 'fake-ip'
Value['dns']['fake-ip-range'] = fake_ip_range
if Value['dns']['ipv6']
if Value['dns']['ipv6'] and fake_ip_range6_enable
Value['dns']['fake-ip-range6'] = fake_ip_range6
end
end
@@ -657,14 +660,13 @@ begin
end
if fake_ip_mode == 'fake-ip' && (china_ip_route || china_ip6_route)
filter_mode = Value.dig('dns', 'fake-ip-filter-mode')
filters = Value.dig('dns', 'fake-ip-filter') || []
deleted_filters = filters.select { |f| f =~ /(geosite:?|rule-set:?).*(@cn|:cn|,cn|:china)/i }
if filter_mode == 'blacklist' || filter_mode.nil?
if !deleted_filters.any?
(Value['dns']['fake-ip-filter'] ||= []) << 'geosite:cn'
YAML.LOG_TIP('Because Need Ensure Bypassing IP Option Work, Added The Fake-IP-Filter Rule【geosite:cn】...')
end
else
filter_rule = 'rule-set:oc-cn-domain'
(Value['dns']['fake-ip-filter'] ||= []) << filter_rule
end
if filter_mode == 'whitelist'
filters = Value.dig('dns', 'fake-ip-filter') || []
deleted_filters = filters.select { |f| f =~ /(geosite:?|rule-set:?).*(@cn|:cn|,cn|:china)/i }
if deleted_filters.any?
Value['dns']['fake-ip-filter'] -= deleted_filters
deleted_filters.each do |f|
@@ -673,8 +675,14 @@ begin
end
end
if filter_mode == 'rule'
(Value['dns']['fake-ip-filter'] ||= []).unshift('GEOSITE,cn,real-ip')
YAML.LOG_TIP('Because Need Ensure Bypassing IP Option Work, Added The Fake-IP-Filter Rule【GEOSITE,cn,real-ip】...')
filter_rule = 'RULE-SET,oc-cn-domain,real-ip'
(Value['dns']['fake-ip-filter'] ||= []).unshift(filter_rule)
end
Value['dns']['fake-ip-filter'].uniq!
if filter_mode != 'whitelist'
rule_set_hash = {'rule-providers+'=>{'oc-cn-domain'=>{'type'=>'http', 'interval'=>43200, 'behavior'=>'domain', 'format'=>'mrs', 'url'=>'https://raw.githubusercontent.com/MetaCubeX/meta-rules-dat/meta/geo/geosite/cn.mrs', 'path'=> './rule_provider/oc-cn-domain.mrs'}}}
Value = YAML.overwrite(Value, rule_set_hash)
YAML.LOG_TIP('Because Need Ensure Bypassing IP Option Work, Added The Fake-IP-Filter Rule【%s】...' % [filter_rule])
end
end
rescue Exception => e
@@ -592,7 +592,7 @@ ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
};
end;
#Mieru
#Mieru
if x['type'] == 'mieru' then
threads << Thread.new{
#port-range
@@ -1138,6 +1138,19 @@ ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
uci_commands << uci_set + 'reality_short_id=\"' + x['reality-opts']['short-id'].to_s + '\"'
end
end
if x.key?('encryption') then
uci_commands << uci_set + 'vless_encryption=\"' + x['encryption'].to_s + '\"'
end
elsif x['network'].to_s == 'xhttp'
uci_commands << uci_set + 'obfs_vless=xhttp'
if x.key?('xhttp-opts') then
if x['xhttp-opts'].key?('path') then
uci_commands << uci_set + 'xhttp_opts_path=\"' + x['xhttp-opts']['path'].to_s + '\"'
end
if x['xhttp-opts'].key?('host') then
uci_commands << uci_set + 'xhttp_opts_host=\"' + x['xhttp-opts']['host'].to_s + '\"'
end
end
end
end
};
@@ -1424,6 +1437,132 @@ ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
};
end;
if x['type'] == 'masque' then
threads << Thread.new{
#private-key
if x.key?('private-key') then
uci_commands << uci_set + 'masque_private_key=\"' + x['private-key'].to_s + '\"'
end
};
threads << Thread.new{
#public-key
if x.key?('public-key') then
uci_commands << uci_set + 'masque_public_key=\"' + x['public-key'].to_s + '\"'
end
};
threads << Thread.new{
#ip
if x.key?('ip') then
uci_commands << uci_set + 'masque_ip=\"' + x['ip'].to_s + '\"'
end
};
threads << Thread.new{
#ipv6
if x.key?('ipv6') then
uci_commands << uci_set + 'masque_ipv6=\"' + x['ipv6'].to_s + '\"'
end
};
threads << Thread.new{
#mtu
if x.key?('mtu') then
uci_commands << uci_set + 'masque_mtu=\"' + x['mtu'].to_s + '\"'
end
};
threads << Thread.new{
#remote-dns-resolve
if x.key?('remote-dns-resolve') then
uci_commands << uci_set + 'masque_remote_dns_resolve=\"' + x['remote-dns-resolve'].to_s + '\"'
end
};
threads << Thread.new{
#dns
if x.key?('dns') then
dns = uci_del + 'dns >/dev/null 2>&1'
system(dns)
x['dns'].each{
|x|
uci_commands << uci_add + 'masque_dns=\"' + x.to_s + '\"'
}
end
};
end;
if x['type'] == 'trusttunnel' then
threads << Thread.new{
#username
if x.key?('username') then
uci_commands << uci_set + 'trusttunnel_username=\"' + x['username'].to_s + '\"'
end
};
threads << Thread.new{
#password
if x.key?('password') then
uci_commands << uci_set + 'trusttunnel_password=\"' + x['password'].to_s + '\"'
end
};
threads << Thread.new{
#health-check
if x.key?('health-check') then
uci_commands << uci_set + 'trusttunnel_health_check=\"' + x['health-check'].to_s + '\"'
end
};
threads << Thread.new{
#quic
if x.key?('quic') then
uci_commands << uci_set + 'trusttunnel_quic=\"' + x['quic'].to_s + '\"'
end
};
threads << Thread.new{
#congestion-controller
if x.key?('congestion-controller') then
uci_commands << uci_set + 'trusttunnel_congestion_controller=\"' + x['congestion-controller'].to_s + '\"'
end
};
#alpn
threads << Thread.new{
if x.key?('alpn') then
alpn = uci_del + 'alpn >/dev/null 2>&1'
system(alpn)
x['alpn'].each{
|x|
uci_commands << uci_add + 'alpn=\"' + x.to_s + '\"'
}
end
};
#sni
threads << Thread.new{
if x.key?('sni') then
uci_commands << uci_set + 'sni=\"' + x['sni'].to_s + '\"'
end
};
#skip-cert-verify
threads << Thread.new{
if x.key?('skip-cert-verify') then
uci_commands << uci_set + 'skip_cert_verify=\"' + x['skip-cert-verify'].to_s + '\"'
end
};
#client_fingerprint
threads << Thread.new{
if x.key?('client-fingerprint') then
uci_commands << uci_set + 'client_fingerprint=\"' + x['client-fingerprint'].to_s + '\"'
end
};
end;
#加入策略组
threads << Thread.new{
#加入策略组
@@ -186,135 +186,6 @@ yml_servers_set()
config_get "name" "$section" "name" ""
config_get "server" "$section" "server" ""
config_get "port" "$section" "port" ""
config_get "dialer_proxy" "$section" "dialer_proxy" ""
config_get "cipher" "$section" "cipher" ""
config_get "cipher_ssr" "$section" "cipher_ssr" ""
config_get "password" "$section" "password" ""
config_get "securitys" "$section" "securitys" ""
config_get "udp" "$section" "udp" ""
config_get "obfs" "$section" "obfs" ""
config_get "obfs_ssr" "$section" "obfs_ssr" ""
config_get "obfs_param" "$section" "obfs_param" ""
config_get "obfs_vmess" "$section" "obfs_vmess" ""
config_get "obfs_trojan" "$section" "obfs_trojan" ""
config_get "protocol" "$section" "protocol" ""
config_get "protocol_param" "$section" "protocol_param" ""
config_get "host" "$section" "host" ""
config_get "mux" "$section" "mux" ""
config_get "custom" "$section" "custom" ""
config_get "tls" "$section" "tls" ""
config_get "skip_cert_verify" "$section" "skip_cert_verify" ""
config_get "path" "$section" "path" ""
config_get "alterId" "$section" "alterId" ""
config_get "uuid" "$section" "uuid" ""
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
config_get "psk" "$section" "psk" ""
config_get "obfs_snell" "$section" "obfs_snell" ""
config_get "snell_version" "$section" "snell_version" ""
config_get "sni" "$section" "sni" ""
config_get "alpn" "$section" "alpn" ""
config_get "http_path" "$section" "http_path" ""
config_get "keep_alive" "$section" "keep_alive" ""
config_get "servername" "$section" "servername" ""
config_get "h2_path" "$section" "h2_path" ""
config_get "h2_host" "$section" "h2_host" ""
config_get "grpc_service_name" "$section" "grpc_service_name" ""
config_get "ws_opts_path" "$section" "ws_opts_path" ""
config_get "ws_opts_headers" "$section" "ws_opts_headers" ""
config_get "max_early_data" "$section" "max_early_data" ""
config_get "early_data_header_name" "$section" "early_data_header_name" ""
config_get "trojan_ws_path" "$section" "trojan_ws_path" ""
config_get "trojan_ws_headers" "$section" "trojan_ws_headers" ""
config_get "interface_name" "$section" "interface_name" ""
config_get "routing_mark" "$section" "routing_mark" ""
config_get "obfs_vless" "$section" "obfs_vless" ""
config_get "vless_flow" "$section" "vless_flow" ""
config_get "http_headers" "$section" "http_headers" ""
config_get "hysteria_protocol" "$section" "hysteria_protocol" ""
config_get "hysteria2_protocol" "$section" "hysteria2_protocol" ""
config_get "hysteria_up" "$section" "hysteria_up" ""
config_get "hysteria_down" "$section" "hysteria_down" ""
config_get "hysteria_alpn" "$section" "hysteria_alpn" ""
config_get "hysteria_obfs" "$section" "hysteria_obfs" ""
config_get "hysteria_auth" "$section" "hysteria_auth" ""
config_get "hysteria_auth_str" "$section" "hysteria_auth_str" ""
config_get "hysteria_ca" "$section" "hysteria_ca" ""
config_get "hysteria_ca_str" "$section" "hysteria_ca_str" ""
config_get "recv_window_conn" "$section" "recv_window_conn" ""
config_get "recv_window" "$section" "recv_window" ""
config_get "disable_mtu_discovery" "$section" "disable_mtu_discovery" ""
config_get "initial_stream_receive_window" "$section" "initial_stream_receive_window" ""
config_get "max_stream_receive_window" "$section" "max_stream_receive_window" ""
config_get "initial_connection_receive_window" "$section" "initial_connection_receive_window" ""
config_get "max_connection_receive_window" "$section" "max_connection_receive_window" ""
config_get "xudp" "$section" "xudp" ""
config_get "packet_encoding" "$section" "packet_encoding" ""
config_get "global_padding" "$section" "global_padding" ""
config_get "authenticated_length" "$section" "authenticated_length" ""
config_get "wg_ip" "$section" "wg_ip" ""
config_get "wg_ipv6" "$section" "wg_ipv6" ""
config_get "private_key" "$section" "private_key" ""
config_get "public_key" "$section" "public_key" ""
config_get "preshared_key" "$section" "preshared_key" ""
config_get "wg_dns" "$section" "wg_dns" ""
config_get "public_key" "$section" "public_key" ""
config_get "preshared_key" "$section" "preshared_key" ""
config_get "wg_mtu" "$section" "wg_mtu" ""
config_get "tc_ip" "$section" "tc_ip" ""
config_get "tc_token" "$section" "tc_token" ""
config_get "tc_uuid" "$section" "tc_uuid" ""
config_get "tc_password" "$section" "tc_password" ""
config_get "udp_relay_mode" "$section" "udp_relay_mode" ""
config_get "congestion_controller" "$section" "congestion_controller" ""
config_get "tc_alpn" "$section" "tc_alpn" ""
config_get "disable_sni" "$section" "disable_sni" ""
config_get "reduce_rtt" "$section" "reduce_rtt" ""
config_get "heartbeat_interval" "$section" "heartbeat_interval" ""
config_get "request_timeout" "$section" "request_timeout" ""
config_get "max_udp_relay_packet_size" "$section" "max_udp_relay_packet_size" ""
config_get "fast_open" "$section" "fast_open" ""
config_get "fingerprint" "$section" "fingerprint" ""
config_get "ports" "$section" "ports" ""
config_get "hop_interval" "$section" "hop_interval" ""
config_get "max_open_streams" "$section" "max_open_streams" ""
config_get "obfs_password" "$section" "obfs_password" ""
config_get "packet_addr" "$section" "packet_addr" ""
config_get "client_fingerprint" "$section" "client_fingerprint" ""
config_get "ip_version" "$section" "ip_version" ""
config_get "tfo" "$section" "tfo" ""
config_get "udp_over_tcp" "$section" "udp_over_tcp" ""
config_get "reality_public_key" "$section" "reality_public_key" ""
config_get "reality_short_id" "$section" "reality_short_id" ""
config_get "obfs_version_hint" "$section" "obfs_version_hint" ""
config_get "obfs_restls_script" "$section" "obfs_restls_script" ""
config_get "multiplex" "$section" "multiplex" ""
config_get "multiplex_protocol" "$section" "multiplex_protocol" ""
config_get "multiplex_max_connections" "$section" "multiplex_max_connections" ""
config_get "multiplex_min_streams" "$section" "multiplex_min_streams" ""
config_get "multiplex_max_streams" "$section" "multiplex_max_streams" ""
config_get "multiplex_padding" "$section" "multiplex_padding" ""
config_get "multiplex_statistic" "$section" "multiplex_statistic" ""
config_get "multiplex_only_tcp" "$section" "multiplex_only_tcp" ""
config_get "other_parameters" "$section" "other_parameters" ""
config_get "hysteria_obfs_password" "$section" "hysteria_obfs_password" ""
config_get "port_range" "$section" "port_range" ""
config_get "username" "$section" "username" ""
config_get "transport" "$section" "transport" "TCP"
config_get "multiplexing" "$section" "multiplexing" "MULTIPLEXING_LOW"
config_get "private_key" "$section" "private_key" ""
config_get "private_key_passphrase" "$section" "private_key_passphrase" ""
config_get "host_key" "$section" "host_key" ""
config_get "host_key_algorithms" "$section" "host_key_algorithms" ""
config_get "idle_session_check_interval" "$section" "idle_session_check_interval" ""
config_get "idle_session_timeout" "$section" "idle_session_timeout" ""
config_get "min_idle_session" "$section" "min_idle_session" ""
config_get "sudoku_key" "$section" "sudoku_key" ""
config_get "aead_method" "$section" "aead_method" "none"
config_get "padding_min" "$section" "padding_min" ""
config_get "padding_max" "$section" "padding_max" ""
config_get "table_type" "$section" "table_type" "prefer_ascii"
config_get "http_mask" "$section" "http_mask" "true"
if [ "$enabled" = "0" ]; then
return
@@ -336,11 +207,12 @@ yml_servers_set()
return
fi
if [ -z "$password" ]; then
if [ "$type" = "ss" ] || [ "$type" = "trojan" ] || [ "$type" = "ssr" ]; then
return
fi
fi
if [ "$type" = "ss" ] || [ "$type" = "trojan" ] || [ "$type" = "ssr" ]; then
config_get "password" "$section" "password" ""
if [ -z "$password" ]; then
return
fi
fi
if [ ! -z "$config" ] && [ "$config" != "$CONFIG_NAME" ] && [ "$config" != "all" ]; then
return
@@ -364,59 +236,27 @@ yml_servers_set()
fi
LOG_OUT "Start Writing【$CONFIG_NAME - $type - $name】Proxy To Config File..."
if [ "$obfs" != "none" ] && [ -n "$obfs" ]; then
if [ "$obfs" = "websocket" ]; then
obfss="plugin: v2ray-plugin"
elif [ "$obfs" = "shadow-tls" ]; then
obfss="plugin: shadow-tls"
elif [ "$obfs" = "restls" ]; then
obfss="plugin: restls"
else
obfss="plugin: obfs"
fi
else
obfss=""
fi
if [ "$obfs_vless" = "ws" ]; then
obfs_vless="network: ws"
fi
if [ "$obfs_vless" = "grpc" ]; then
obfs_vless="network: grpc"
fi
if [ "$obfs_vless" = "tcp" ]; then
obfs_vless="network: tcp"
fi
if [ "$obfs_vmess" = "websocket" ]; then
obfs_vmess="network: ws"
fi
if [ "$obfs_vmess" = "http" ]; then
obfs_vmess="network: http"
fi
if [ "$obfs_vmess" = "h2" ]; then
obfs_vmess="network: h2"
fi
if [ "$obfs_vmess" = "grpc" ]; then
obfs_vmess="network: grpc"
fi
if [ ! -z "$custom" ] && [ "$type" = "vmess" ]; then
custom="Host: \"$custom\""
fi
if [ ! -z "$path" ]; then
if [ "$type" != "vmess" ]; then
path="path: \"$path\""
elif [ "$obfs_vmess" = "network: ws" ]; then
path="ws-path: \"$path\""
fi
fi
config_get "dialer_proxy" "$section" "dialer_proxy" ""
config_get "udp" "$section" "udp" ""
config_get "skip_cert_verify" "$section" "skip_cert_verify" ""
config_get "tls" "$section" "tls" ""
config_get "sni" "$section" "sni" ""
config_get "alpn" "$section" "alpn" ""
config_get "fingerprint" "$section" "fingerprint" ""
config_get "client_fingerprint" "$section" "client_fingerprint" ""
config_get "ip_version" "$section" "ip_version" ""
config_get "tfo" "$section" "tfo" ""
config_get "multiplex" "$section" "multiplex" ""
config_get "multiplex_protocol" "$section" "multiplex_protocol" ""
config_get "multiplex_max_connections" "$section" "multiplex_max_connections" ""
config_get "multiplex_min_streams" "$section" "multiplex_min_streams" ""
config_get "multiplex_max_streams" "$section" "multiplex_max_streams" ""
config_get "multiplex_padding" "$section" "multiplex_padding" ""
config_get "multiplex_statistic" "$section" "multiplex_statistic" ""
config_get "multiplex_only_tcp" "$section" "multiplex_only_tcp" ""
config_get "interface_name" "$section" "interface_name" ""
config_get "routing_mark" "$section" "routing_mark" ""
config_get "other_parameters" "$section" "other_parameters" ""
if [ "$client_fingerprint" = "none" ]; then
client_fingerprint=""
@@ -428,6 +268,35 @@ yml_servers_set()
#ss
if [ "$type" = "ss" ]; then
config_get "cipher" "$section" "cipher" ""
config_get "obfs" "$section" "obfs" ""
config_get "host" "$section" "host" ""
config_get "mux" "$section" "mux" ""
config_get "custom" "$section" "custom" ""
config_get "path" "$section" "path" ""
config_get "obfs_password" "$section" "obfs_password" ""
config_get "obfs_version_hint" "$section" "obfs_version_hint" ""
config_get "obfs_restls_script" "$section" "obfs_restls_script" ""
config_get "udp_over_tcp" "$section" "udp_over_tcp" ""
if [ "$obfs" != "none" ] && [ -n "$obfs" ]; then
if [ "$obfs" = "websocket" ]; then
obfss="plugin: v2ray-plugin"
elif [ "$obfs" = "shadow-tls" ]; then
obfss="plugin: shadow-tls"
elif [ "$obfs" = "restls" ]; then
obfss="plugin: restls"
else
obfss="plugin: obfs"
fi
else
obfss=""
fi
if [ ! -z "$path" ]; then
path="path: \"$path\""
fi
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -528,6 +397,12 @@ fi
#ssr
if [ "$type" = "ssr" ]; then
config_get "cipher_ssr" "$section" "cipher_ssr" ""
config_get "obfs_ssr" "$section" "obfs_ssr" ""
config_get "protocol" "$section" "protocol" ""
config_get "obfs_param" "$section" "obfs_param" ""
config_get "protocol_param" "$section" "protocol_param" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -557,6 +432,48 @@ fi
#vmess
if [ "$type" = "vmess" ]; then
config_get "uuid" "$section" "uuid" ""
config_get "alterId" "$section" "alterId" ""
config_get "securitys" "$section" "securitys" ""
config_get "xudp" "$section" "xudp" ""
config_get "packet_encoding" "$section" "packet_encoding" ""
config_get "global_padding" "$section" "global_padding" ""
config_get "authenticated_length" "$section" "authenticated_length" ""
config_get "servername" "$section" "servername" ""
config_get "obfs_vmess" "$section" "obfs_vmess" ""
config_get "custom" "$section" "custom" ""
config_get "path" "$section" "path" ""
config_get "ws_opts_path" "$section" "ws_opts_path" ""
config_get "ws_opts_headers" "$section" "ws_opts_headers" ""
config_get "max_early_data" "$section" "max_early_data" ""
config_get "early_data_header_name" "$section" "early_data_header_name" ""
config_get "http_path" "$section" "http_path" ""
config_get "keep_alive" "$section" "keep_alive" ""
config_get "h2_path" "$section" "h2_path" ""
config_get "h2_host" "$section" "h2_host" ""
config_get "grpc_service_name" "$section" "grpc_service_name" ""
if [ "$obfs_vmess" = "websocket" ]; then
obfs_vmess="network: ws"
fi
if [ "$obfs_vmess" = "http" ]; then
obfs_vmess="network: http"
fi
if [ "$obfs_vmess" = "h2" ]; then
obfs_vmess="network: h2"
fi
if [ "$obfs_vmess" = "grpc" ]; then
obfs_vmess="network: grpc"
fi
if [ ! -z "$custom" ]; then
custom="Host: \"$custom\""
fi
if [ ! -z "$path" ] && [ "$obfs_vmess" = "network: ws" ]; then
path="ws-path: \"$path\""
fi
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -702,6 +619,11 @@ fi
#anytls
if [ "$type" = "anytls" ]; then
config_get "password" "$section" "password" ""
config_get "idle_session_check_interval" "$section" "idle_session_check_interval" ""
config_get "idle_session_timeout" "$section" "idle_session_timeout" ""
config_get "min_idle_session" "$section" "min_idle_session" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -758,6 +680,11 @@ fi
#Mieru
if [ "$type" = "mieru" ]; then
config_get "port_range" "$section" "port_range" ""
config_get "username" "$section" "username" ""
config_get "transport" "$section" "transport" "TCP"
config_get "multiplexing" "$section" "multiplexing" "MULTIPLEXING_LOW"
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -788,6 +715,21 @@ fi
#Tuic
if [ "$type" = "tuic" ]; then
config_get "tc_ip" "$section" "tc_ip" ""
config_get "tc_token" "$section" "tc_token" ""
config_get "tc_uuid" "$section" "tc_uuid" ""
config_get "tc_password" "$section" "tc_password" ""
config_get "udp_relay_mode" "$section" "udp_relay_mode" ""
config_get "congestion_controller" "$section" "congestion_controller" ""
config_get "tc_alpn" "$section" "tc_alpn" ""
config_get "disable_sni" "$section" "disable_sni" ""
config_get "reduce_rtt" "$section" "reduce_rtt" ""
config_get "fast_open" "$section" "fast_open" ""
config_get "heartbeat_interval" "$section" "heartbeat_interval" ""
config_get "request_timeout" "$section" "request_timeout" ""
config_get "max_udp_relay_packet_size" "$section" "max_udp_relay_packet_size" ""
config_get "max_open_streams" "$section" "max_open_streams" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -874,6 +816,14 @@ fi
#WireGuard
if [ "$type" = "wireguard" ]; then
config_get "wg_ip" "$section" "wg_ip" ""
config_get "wg_ipv6" "$section" "wg_ipv6" ""
config_get "private_key" "$section" "private_key" ""
config_get "public_key" "$section" "public_key" ""
config_get "preshared_key" "$section" "preshared_key" ""
config_get "wg_dns" "$section" "wg_dns" ""
config_get "wg_mtu" "$section" "wg_mtu" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -905,7 +855,7 @@ cat >> "$SERVER_FILE" <<-EOF
preshared-key: "$preshared_key"
EOF
fi
if [ -n "$preshared_key" ]; then
if [ -n "$wg_dns" ]; then
cat >> "$SERVER_FILE" <<-EOF
dns:
EOF
@@ -925,6 +875,22 @@ fi
#hysteria
if [ "$type" = "hysteria" ]; then
config_get "hysteria_protocol" "$section" "hysteria_protocol" ""
config_get "hysteria_up" "$section" "hysteria_up" ""
config_get "hysteria_down" "$section" "hysteria_down" ""
config_get "hysteria_alpn" "$section" "hysteria_alpn" ""
config_get "hysteria_obfs" "$section" "hysteria_obfs" ""
config_get "hysteria_auth" "$section" "hysteria_auth" ""
config_get "hysteria_auth_str" "$section" "hysteria_auth_str" ""
config_get "hysteria_ca" "$section" "hysteria_ca" ""
config_get "hysteria_ca_str" "$section" "hysteria_ca_str" ""
config_get "recv_window_conn" "$section" "recv_window_conn" ""
config_get "recv_window" "$section" "recv_window" ""
config_get "disable_mtu_discovery" "$section" "disable_mtu_discovery" ""
config_get "fast_open" "$section" "fast_open" ""
config_get "ports" "$section" "ports" ""
config_get "hop_interval" "$section" "hop_interval" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1029,6 +995,22 @@ fi
#hysteria2
if [ "$type" = "hysteria2" ]; then
config_get "password" "$section" "password" ""
config_get "hysteria_up" "$section" "hysteria_up" ""
config_get "hysteria_down" "$section" "hysteria_down" ""
config_get "hysteria_alpn" "$section" "hysteria_alpn" ""
config_get "hysteria_obfs" "$section" "hysteria_obfs" ""
config_get "hysteria_obfs_password" "$section" "hysteria_obfs_password" ""
config_get "hysteria_ca" "$section" "hysteria_ca" ""
config_get "hysteria_ca_str" "$section" "hysteria_ca_str" ""
config_get "initial_stream_receive_window" "$section" "initial_stream_receive_window" ""
config_get "max_stream_receive_window" "$section" "max_stream_receive_window" ""
config_get "initial_connection_receive_window" "$section" "initial_connection_receive_window" ""
config_get "max_connection_receive_window" "$section" "max_connection_receive_window" ""
config_get "ports" "$section" "ports" ""
config_get "hysteria2_protocol" "$section" "hysteria2_protocol" ""
config_get "hop_interval" "$section" "hop_interval" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1096,7 +1078,7 @@ EOF
fi
if [ -n "$max_stream_receive_window" ]; then
cat >> "$SERVER_FILE" <<-EOF
max_stream_receive_window: "$max_stream_receive_window"
max-stream-receive-window: "$max_stream_receive_window"
EOF
fi
if [ -n "$initial_connection_receive_window" ]; then
@@ -1133,6 +1115,35 @@ fi
#vless
if [ "$type" = "vless" ]; then
config_get "uuid" "$section" "uuid" ""
config_get "xudp" "$section" "xudp" ""
config_get "packet_addr" "$section" "packet_addr" ""
config_get "packet_encoding" "$section" "packet_encoding" ""
config_get "servername" "$section" "servername" ""
config_get "obfs_vless" "$section" "obfs_vless" ""
config_get "ws_opts_path" "$section" "ws_opts_path" ""
config_get "ws_opts_headers" "$section" "ws_opts_headers" ""
config_get "grpc_service_name" "$section" "grpc_service_name" ""
config_get "reality_public_key" "$section" "reality_public_key" ""
config_get "reality_short_id" "$section" "reality_short_id" ""
config_get "vless_flow" "$section" "vless_flow" ""
config_get "xhttp_opts_path" "$section" "xhttp_opts_path" ""
config_get "xhttp_opts_host" "$section" "xhttp_opts_host" ""
config_get "vless_encryption" "$section" "vless_encryption" ""
if [ "$obfs_vless" = "ws" ]; then
obfs_vless="network: ws"
fi
if [ "$obfs_vless" = "grpc" ]; then
obfs_vless="network: grpc"
fi
if [ "$obfs_vless" = "tcp" ]; then
obfs_vless="network: tcp"
fi
if [ "$obfs_vless" = "xhttp" ]; then
obfs_vless="network: xhttp"
fi
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1232,6 +1243,11 @@ EOF
if [ ! -z "$vless_flow" ]; then
cat >> "$SERVER_FILE" <<-EOF
flow: "$vless_flow"
EOF
fi
if [ -n "$vless_encryption" ]; then
cat >> "$SERVER_FILE" <<-EOF
encryption: "$vless_encryption"
EOF
fi
if [ -n "$reality_public_key" ] || [ -n "$reality_short_id" ]; then
@@ -1247,6 +1263,21 @@ EOF
if [ -n "$reality_short_id" ]; then
cat >> "$SERVER_FILE" <<-EOF
short-id: "$reality_short_id"
EOF
fi
fi
if [ "$obfs_vless" = "network: xhttp" ]; then
cat >> "$SERVER_FILE" <<-EOF
xhttp-opts:
EOF
if [ -n "$xhttp_opts_path" ]; then
cat >> "$SERVER_FILE" <<-EOF
path: "$xhttp_opts_path"
EOF
fi
if [ -n "$xhttp_opts_host" ]; then
cat >> "$SERVER_FILE" <<-EOF
host: "$xhttp_opts_host"
EOF
fi
fi
@@ -1276,6 +1307,13 @@ fi
#ssh
if [ "$type" = "ssh" ]; then
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
config_get "private_key" "$section" "private_key" ""
config_get "private_key_passphrase" "$section" "private_key_passphrase" ""
config_get "host_key" "$section" "host_key" ""
config_get "host_key_algorithms" "$section" "host_key_algorithms" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1318,6 +1356,9 @@ fi
#socks5
if [ "$type" = "socks5" ]; then
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1358,6 +1399,10 @@ fi
#http
if [ "$type" = "http" ]; then
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
config_get "http_headers" "$section" "http_headers" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1399,6 +1444,11 @@ fi
#trojan
if [ "$type" = "trojan" ]; then
config_get "grpc_service_name" "$section" "grpc_service_name" ""
config_get "obfs_trojan" "$section" "obfs_trojan" ""
config_get "trojan_ws_path" "$section" "trojan_ws_path" ""
config_get "trojan_ws_headers" "$section" "trojan_ws_headers" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1467,6 +1517,11 @@ fi
#snell
if [ "$type" = "snell" ]; then
config_get "psk" "$section" "psk" ""
config_get "snell_version" "$section" "snell_version" ""
config_get "obfs_snell" "$section" "obfs_snell" ""
config_get "host" "$section" "host" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1490,6 +1545,13 @@ fi
#Sudoku
if [ "$type" = "sudoku" ]; then
config_get "sudoku_key" "$section" "sudoku_key" ""
config_get "aead_method" "$section" "aead_method" "none"
config_get "padding_min" "$section" "padding_min" ""
config_get "padding_max" "$section" "padding_max" ""
config_get "table_type" "$section" "table_type" "prefer_ascii"
config_get "http_mask" "$section" "http_mask" "true"
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1528,6 +1590,132 @@ EOF
fi
fi
#MASQUE
if [ "$type" = "masque" ]; then
config_get "masque_private_key" "$section" "masque_private_key" ""
config_get "masque_public_key" "$section" "masque_public_key" ""
config_get "masque_ip" "$section" "masque_ip" ""
config_get "masque_ipv6" "$section" "masque_ipv6" ""
config_get "masque_mtu" "$section" "masque_mtu" ""
config_get "masque_remote_dns_resolve" "$section" "masque_remote_dns_resolve" ""
config_get "masque_dns" "$section" "masque_dns" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
server: "$server"
port: $port
EOF
if [ -n "$masque_private_key" ]; then
cat >> "$SERVER_FILE" <<-EOF
private-key: "$masque_private_key"
EOF
fi
if [ -n "$masque_public_key" ]; then
cat >> "$SERVER_FILE" <<-EOF
public-key: "$masque_public_key"
EOF
fi
if [ -n "$masque_ip" ]; then
cat >> "$SERVER_FILE" <<-EOF
ip: "$masque_ip"
EOF
fi
if [ -n "$masque_ipv6" ]; then
cat >> "$SERVER_FILE" <<-EOF
ipv6: "$masque_ipv6"
EOF
fi
if [ -n "$masque_mtu" ]; then
cat >> "$SERVER_FILE" <<-EOF
mtu: $masque_mtu
EOF
fi
if [ -n "$masque_remote_dns_resolve" ]; then
cat >> "$SERVER_FILE" <<-EOF
remote-dns-resolve: $masque_remote_dns_resolve
EOF
fi
if [ ! -z "$udp" ]; then
cat >> "$SERVER_FILE" <<-EOF
udp: $udp
EOF
fi
if [ ! -z "$masque_dns" ]; then
cat >> "$SERVER_FILE" <<-EOF
dns:
EOF
config_list_foreach "$section" "masque_dns" set_alpn
fi
fi
#TrustTunnel
if [ "$type" = "trusttunnel" ]; then
config_get "trusttunnel_username" "$section" "trusttunnel_username" ""
config_get "trusttunnel_password" "$section" "trusttunnel_password" ""
config_get "trusttunnel_health_check" "$section" "trusttunnel_health_check" ""
config_get "trusttunnel_quic" "$section" "trusttunnel_quic" ""
config_get "trusttunnel_congestion_controller" "$section" "trusttunnel_congestion_controller" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
server: "$server"
port: $port
EOF
if [ -n "$trusttunnel_username" ]; then
cat >> "$SERVER_FILE" <<-EOF
username: "$trusttunnel_username"
EOF
fi
if [ -n "$trusttunnel_password" ]; then
cat >> "$SERVER_FILE" <<-EOF
password: "$trusttunnel_password"
EOF
fi
if [ -n "$trusttunnel_health_check" ]; then
cat >> "$SERVER_FILE" <<-EOF
health-check: $trusttunnel_health_check
EOF
fi
if [ -n "$trusttunnel_quic" ]; then
cat >> "$SERVER_FILE" <<-EOF
quic: $trusttunnel_quic
EOF
fi
if [ -n "$trusttunnel_congestion_controller" ]; then
cat >> "$SERVER_FILE" <<-EOF
congestion-controller: "$trusttunnel_congestion_controller"
EOF
fi
if [ -n "$client_fingerprint" ]; then
cat >> "$SERVER_FILE" <<-EOF
client-fingerprint: "$client_fingerprint"
EOF
fi
if [ -n "$udp" ]; then
cat >> "$SERVER_FILE" <<-EOF
udp: $udp
EOF
fi
if [ -n "$sni" ]; then
cat >> "$SERVER_FILE" <<-EOF
sni: "$sni"
EOF
fi
if [ ! -z "$alpn" ]; then
cat >> "$SERVER_FILE" <<-EOF
alpn:
EOF
config_list_foreach "$section" "alpn" set_alpn
fi
if [ ! -z "$skip_cert_verify" ]; then
cat >> "$SERVER_FILE" <<-EOF
skip-cert-verify: $skip_cert_verify
EOF
fi
fi
#ip_version
if [ ! -z "$ip_version" ]; then
cat >> "$SERVER_FILE" <<-EOF
@@ -21,6 +21,9 @@ yml_other_set()
begin
thread_pool = [];
# GEOIP replace
geoip_pattern = /GEOIP,([A-Za-z]{2}),([^,]+)(,.*)?/;
match_pattern = /(^MATCH.*|^FINAL.*)/;
thread_pool << Thread.new{
#BT/P2P DIRECT Rules
begin
@@ -102,10 +105,6 @@ yml_other_set()
end;
end;
# GEOIP replace
geoip_pattern = /GEOIP,([A-Za-z]{2}),([^,]+)(,.*)?/;
match_pattern = /(^MATCH.*|^FINAL.*)/;
Value['rules'].to_a.collect!{|x|
x.to_s.gsub(geoip_pattern, 'GEOIP,\1,DIRECT\3').gsub(match_pattern, 'MATCH,DIRECT')
};
@@ -136,6 +135,18 @@ yml_other_set()
{ file: '/etc/openclash/custom/openclash_custom_rules_2.list', position: 'bottom' }
];
# 在开始对 custom_files 进行插入之前,记录当前 rules 中 GEOIP、MATCH、DST-PORT,80 的原始规则文本(锚点)
anchors_orig = {}
if Value.has_key?('rules') and not Value['rules'].to_a.empty? then
anchors_orig[:dst80_rule] = Value['rules'].grep(/DST-PORT,80/).last
anchors_orig[:match_rule] = Value['rules'].grep(match_pattern).first
anchors_orig[:geo_rule] = Value['rules'].grep(geoip_pattern).first
else
anchors_orig[:dst80_rule] = nil
anchors_orig[:match_rule] = nil
anchors_orig[:geo_rule] = nil
end;
custom_files.each{|file_info|
if File::exist?(file_info[:file]) then
custom_data = YAML.load_file(file_info[:file]);
@@ -150,6 +161,13 @@ yml_other_set()
[]
end;
rule_providers_array = case custom_data.class.to_s
when 'Hash'
custom_data['rule-providers'].to_a if custom_data['rule-providers'].class.to_s == 'Hash'
else
[]
end;
next unless rules_array;
ipv4_regex = /^(\d{1,3}\.){3}\d{1,3}$/;
@@ -157,9 +175,10 @@ yml_other_set()
cidr_regex = /\/\d+$/;
rule_suffix_regex = /^no-resolve$|^src$/;
ip_rule_types = ['IP-CIDR', 'IP-CIDR6', 'SRC-IP-CIDR', 'SRC-IP-CIDR6', 'IP-SUFFIX', 'SRC-IP-SUFFIX'];
transformed_rules = rules_array.map{|x|
parts = x.split(',');
if parts.length >= 2 then
if parts.length >= 2 && ip_rule_types.include?(parts[0].strip.upcase) then
ip_part = parts[1].strip;
if ip_part !~ cidr_regex then
# IPv4
@@ -194,11 +213,15 @@ yml_other_set()
if file_info[:position] == 'top' then
valid_rules.reverse.each{|x| Value['rules'].insert(0,x)};
else
if Value['rules'].grep(/GEOIP/)[0].nil? or Value['rules'].grep(/GEOIP/)[0].empty? then
ruby_add_index = Value['rules'].index(Value['rules'].grep(/DST-PORT,80/).last);
ruby_add_index ||= Value['rules'].index(Value['rules'].grep(/(MATCH|FINAL)/).first);
else
ruby_add_index = Value['rules'].index(Value['rules'].grep(/GEOIP/).first);
ruby_add_index = nil;
if anchors_orig[:dst80_rule]
ruby_add_index = Value['rules'].rindex(anchors_orig[:dst80_rule])
end;
if ruby_add_index.nil? and anchors_orig[:match_rule]
ruby_add_index = Value['rules'].rindex(anchors_orig[:match_rule])
end;
if ruby_add_index.nil? and anchors_orig[:geo_rule]
ruby_add_index = Value['rules'].rindex(anchors_orig[:geo_rule])
end;
ruby_add_index ||= -1;
@@ -209,6 +232,11 @@ yml_other_set()
else
Value['rules'] = valid_rules.uniq;
end;
if rule_providers_array and not rule_providers_array.empty? then
Value['rule-providers'] ||= {};
Value['rule-providers'] = Value['rule-providers'].merge!(custom_data['rule-providers']);
end;
end;
};
@@ -256,19 +284,13 @@ yml_other_set()
thread_pool << Thread.new{
threads = [];
#provider path
#provider CDN
begin
provider_configs = {'proxy-providers' => 'proxy_provider', 'rule-providers' => 'rule_provider'};
provider_configs.each do |provider_type, path_prefix|
if Value.key?(provider_type) then
if Value.key?(provider_type) && Value[provider_type].is_a?(Hash) then
Value[provider_type].each{|name, config|
threads << Thread.new {
if config['path'] and not config['path'] =~ /.\/#{path_prefix}\/*/ then
config['path'] = './'+path_prefix+'/'+File.basename(config['path']);
elsif not config['path'] and config['type'] == 'http' then
config['path'] = './'+path_prefix+'/'+name;
end;
# CDN
if '$github_address_mod' != '0' and config['url'] then
if config['url'] =~ /^https:\/\/raw.githubusercontent.com/ then
@@ -293,12 +315,12 @@ yml_other_set()
end;
end;
rescue Exception => e
YAML.LOG_ERROR('Edit Provider Path Failed,【' + e.message + '】');
YAML.LOG_ERROR('Edit Provider CDN Failed,【' + e.message + '】');
end;
# tolerance
begin
if '$tolerance' != '0' and Value.key?('proxy-groups') then
if '$tolerance' != '0' and Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
if group['type'] == 'url-test' then
@@ -314,7 +336,7 @@ yml_other_set()
# URL-Test interval
begin
if '$urltest_interval_mod' != '0' then
if Value.key?('proxy-groups') then
if Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
if ['url-test', 'fallback', 'load-balance', 'smart'].include?(group['type']) then
@@ -349,7 +371,7 @@ yml_other_set()
};
};
end;
if Value.key?('proxy-groups') then
if Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
if ['url-test', 'fallback', 'load-balance', 'smart'].include?(group['type']) then
@@ -365,7 +387,7 @@ yml_other_set()
# smart auto switch
begin
if ('${8}' == '1' or '${9}' == '1' or '${11}' != '0' or '${12}' != '0' or '${12}' == '1' or '${13}' == '1') and Value.key?('proxy-groups') then
if ('${8}' == '1' or '${9}' == '1' or '${11}' != '0' or '${12}' != '0' or '${12}' == '1' or '${13}' == '1') and Value.key?('proxy-groups') and Value['proxy-groups'].is_a?(Array) then
Value['proxy-groups'].each{|group|
threads << Thread.new {
if '${8}' == '1' and ['url-test', 'load-balance'].include?(group['type']) then
@@ -1,2 +1,2 @@
/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).jsyaml={})}(this,(function(e){"use strict";function t(e){return null==e}var n={isNothing:t,isObject:function(e){return"object"==typeof e&&null!==e},toArray:function(e){return Array.isArray(e)?e:t(e)?[]:[e]},repeat:function(e,t){var n,i="";for(n=0;n<t;n+=1)i+=e;return i},isNegativeZero:function(e){return 0===e&&Number.NEGATIVE_INFINITY===1/e},extend:function(e,t){var n,i,r,o;if(t)for(n=0,i=(o=Object.keys(t)).length;n<i;n+=1)e[r=o[n]]=t[r];return e}};function i(e,t){var n="",i=e.reason||"(unknown reason)";return e.mark?(e.mark.name&&(n+='in "'+e.mark.name+'" '),n+="("+(e.mark.line+1)+":"+(e.mark.column+1)+")",!t&&e.mark.snippet&&(n+="\n\n"+e.mark.snippet),i+" "+n):i}function r(e,t){Error.call(this),this.name="YAMLException",this.reason=e,this.mark=t,this.message=i(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||""}r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r.prototype.toString=function(e){return this.name+": "+i(this,e)};var o=r;function a(e,t,n,i,r){var o="",a="",l=Math.floor(r/2)-1;return i-t>l&&(t=i-l+(o=" ... ").length),n-i>l&&(n=i+l-(a=" ...").length),{str:o+e.slice(t,n).replace(/\t/g,"→")+a,pos:i-t+o.length}}function l(e,t){return n.repeat(" ",t-e.length)+e}var c=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var i,r=/\r?\n|\r|\0/g,o=[0],c=[],s=-1;i=r.exec(e.buffer);)c.push(i.index),o.push(i.index+i[0].length),e.position<=i.index&&s<0&&(s=o.length-2);s<0&&(s=o.length-1);var u,p,f="",d=Math.min(e.line+t.linesAfter,c.length).toString().length,h=t.maxLength-(t.indent+d+3);for(u=1;u<=t.linesBefore&&!(s-u<0);u++)p=a(e.buffer,o[s-u],c[s-u],e.position-(o[s]-o[s-u]),h),f=n.repeat(" ",t.indent)+l((e.line-u+1).toString(),d)+" | "+p.str+"\n"+f;for(p=a(e.buffer,o[s],c[s],e.position,h),f+=n.repeat(" ",t.indent)+l((e.line+1).toString(),d)+" | "+p.str+"\n",f+=n.repeat("-",t.indent+d+3+p.pos)+"^\n",u=1;u<=t.linesAfter&&!(s+u>=c.length);u++)p=a(e.buffer,o[s+u],c[s+u],e.position-(o[s]-o[s+u]),h),f+=n.repeat(" ",t.indent)+l((e.line+u+1).toString(),d)+" | "+p.str+"\n";return f.replace(/\n$/,"")},s=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],u=["scalar","sequence","mapping"];var p=function(e,t){if(t=t||{},Object.keys(t).forEach((function(t){if(-1===s.indexOf(t))throw new o('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=function(e){var t={};return null!==e&&Object.keys(e).forEach((function(n){e[n].forEach((function(e){t[String(e)]=n}))})),t}(t.styleAliases||null),-1===u.indexOf(this.kind))throw new o('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')};function f(e,t){var n=[];return e[t].forEach((function(e){var t=n.length;n.forEach((function(n,i){n.tag===e.tag&&n.kind===e.kind&&n.multi===e.multi&&(t=i)})),n[t]=e})),n}function d(e){return this.extend(e)}d.prototype.extend=function(e){var t=[],n=[];if(e instanceof p)n.push(e);else if(Array.isArray(e))n=n.concat(e);else{if(!e||!Array.isArray(e.implicit)&&!Array.isArray(e.explicit))throw new o("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(n=n.concat(e.explicit))}t.forEach((function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(e.loadKind&&"scalar"!==e.loadKind)throw new o("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(e.multi)throw new o("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")})),n.forEach((function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.")}));var i=Object.create(d.prototype);return i.implicit=(this.implicit||[]).concat(t),i.explicit=(this.explicit||[]).concat(n),i.compiledImplicit=f(i,"implicit"),i.compiledExplicit=f(i,"explicit"),i.compiledTypeMap=function(){var e,t,n={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function i(e){e.multi?(n.multi[e.kind].push(e),n.multi.fallback.Line truncated
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).jsyaml={})}(this,function(e){"use strict";function t(e){return null==e}var n={isNothing:t,isObject:function(e){return"object"==typeof e&&null!==e},toArray:function(e){return Array.isArray(e)?e:t(e)?[]:[e]},repeat:function(e,t){var n,i="";for(n=0;n<t;n+=1)i+=e;return i},isNegativeZero:function(e){return 0===e&&Number.NEGATIVE_INFINITY===1/e},extend:function(e,t){var n,i,r,o;if(t)for(n=0,i=(o=Object.keys(t)).length;n<i;n+=1)e[r=o[n]]=t[r];return e}};function i(e,t){var n="",i=e.reason||"(unknown reason)";return e.mark?(e.mark.name&&(n+='in "'+e.mark.name+'" '),n+="("+(e.mark.line+1)+":"+(e.mark.column+1)+")",!t&&e.mark.snippet&&(n+="\n\n"+e.mark.snippet),i+" "+n):i}function r(e,t){Error.call(this),this.name="YAMLException",this.reason=e,this.mark=t,this.message=i(this,!1),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=(new Error).stack||""}r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r.prototype.toString=function(e){return this.name+": "+i(this,e)};var o=r;function a(e,t,n,i,r){var o="",a="",l=Math.floor(r/2)-1;return i-t>l&&(t=i-l+(o=" ... ").length),n-i>l&&(n=i+l-(a=" ...").length),{str:o+e.slice(t,n).replace(/\t/g,"→")+a,pos:i-t+o.length}}function l(e,t){return n.repeat(" ",t-e.length)+e}var c=function(e,t){if(t=Object.create(t||null),!e.buffer)return null;t.maxLength||(t.maxLength=79),"number"!=typeof t.indent&&(t.indent=1),"number"!=typeof t.linesBefore&&(t.linesBefore=3),"number"!=typeof t.linesAfter&&(t.linesAfter=2);for(var i,r=/\r?\n|\r|\0/g,o=[0],c=[],s=-1;i=r.exec(e.buffer);)c.push(i.index),o.push(i.index+i[0].length),e.position<=i.index&&s<0&&(s=o.length-2);s<0&&(s=o.length-1);var u,p,f="",d=Math.min(e.line+t.linesAfter,c.length).toString().length,h=t.maxLength-(t.indent+d+3);for(u=1;u<=t.linesBefore&&!(s-u<0);u++)p=a(e.buffer,o[s-u],c[s-u],e.position-(o[s]-o[s-u]),h),f=n.repeat(" ",t.indent)+l((e.line-u+1).toString(),d)+" | "+p.str+"\n"+f;for(p=a(e.buffer,o[s],c[s],e.position,h),f+=n.repeat(" ",t.indent)+l((e.line+1).toString(),d)+" | "+p.str+"\n",f+=n.repeat("-",t.indent+d+3+p.pos)+"^\n",u=1;u<=t.linesAfter&&!(s+u>=c.length);u++)p=a(e.buffer,o[s+u],c[s+u],e.position-(o[s]-o[s+u]),h),f+=n.repeat(" ",t.indent)+l((e.line+u+1).toString(),d)+" | "+p.str+"\n";return f.replace(/\n$/,"")},s=["kind","multi","resolve","construct","instanceOf","predicate","represent","representName","defaultStyle","styleAliases"],u=["scalar","sequence","mapping"];var p=function(e,t){if(t=t||{},Object.keys(t).forEach(function(t){if(-1===s.indexOf(t))throw new o('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')}),this.options=t,this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.representName=t.representName||null,this.defaultStyle=t.defaultStyle||null,this.multi=t.multi||!1,this.styleAliases=function(e){var t={};return null!==e&&Object.keys(e).forEach(function(n){e[n].forEach(function(e){t[String(e)]=n})}),t}(t.styleAliases||null),-1===u.indexOf(this.kind))throw new o('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')};function f(e,t){var n=[];return e[t].forEach(function(e){var t=n.length;n.forEach(function(n,i){n.tag===e.tag&&n.kind===e.kind&&n.multi===e.multi&&(t=i)}),n[t]=e}),n}function d(e){return this.extend(e)}d.prototype.extend=function(e){var t=[],n=[];if(e instanceof p)n.push(e);else if(Array.isArray(e))n=n.concat(e);else{if(!e||!Array.isArray(e.implicit)&&!Array.isArray(e.explicit))throw new o("Schema.extend argument should be a Type, [ Type ], or a schema definition ({ implicit: [...], explicit: [...] })");e.implicit&&(t=t.concat(e.implicit)),e.explicit&&(n=n.concat(e.explicit))}t.forEach(function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.");if(e.loadKind&&"scalar"!==e.loadKind)throw new o("There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.");if(e.multi)throw new o("There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.")}),n.forEach(function(e){if(!(e instanceof p))throw new o("Specified list of YAML types (or a single Type object) contains a non-Type object.")});var i=Object.create(d.prototype);return i.implicit=(this.implicit||[]).concat(t),i.explicit=(this.explicit||[]).concat(n),i.compiledImplicit=f(i,"implicit"),i.compiledExplicit=f(i,"explicit"),i.compiledTypeMap=function(){var e,t,n={scalar:{},sequence:{},mapping:{},fallback:{},multi:{scalar:[],sequence:[],mapping:[],fallback:[]}};function i(e){e.multi?(n.multi[e.kind].push(e),n.multi.fallback.push(e)):n[e.kiLine truncated
@@ -1,49 +1,55 @@
(function(){function diff_match_patch(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=0.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=0.5;this.Patch_Margin=4;this.Match_MaxBits=32}
diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[[0,a]]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);var f=this.diff_commonSuffix(a,b),g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,b.length-f);a=this.diff_compute_(a,
b,e,d);c&&a.unshift([0,c]);g&&a.push([0,g]);this.diff_cleanupMerge(a);return a};
diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[[1,b]];if(!b)return[[-1,a]];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[[1,e.substring(0,g)],[0,f],[1,e.substring(g+f.length)]],a.length>b.length&&(c[0][0]=c[2][0]=-1),c):1==f.length?[[-1,a],[1,b]]:(e=this.diff_halfMatch_(a,b))?(f=e[0],a=e[1],g=e[2],b=e[3],e=e[4],f=this.diff_main(f,g,c,d),c=this.diff_main(a,b,c,d),f.concat([[0,e]],c)):c&&100<a.length&&100<b.length?this.diff_lineMode_(a,b,
d):this.diff_bisect_(a,b,d)};
diff_match_patch.prototype.diff_lineMode_=function(a,b,c){var d=this.diff_linesToChars_(a,b);a=d.chars1;b=d.chars2;d=d.lineArray;a=this.diff_main(a,b,!1,c);this.diff_charsToLines_(a,d);this.diff_cleanupSemantic(a);a.push([0,""]);for(var e=d=b=0,f="",g="";b<a.length;){switch(a[b][0]){case 1:e++;g+=a[b][1];break;case -1:d++;f+=a[b][1];break;case 0:if(1<=d&&1<=e){a.splice(b-d-e,d+e);b=b-d-e;d=this.diff_main(f,g,!1,c);for(e=d.length-1;0<=e;e--)a.splice(b,0,d[e]);b+=d.length}d=e=0;g=f=""}b++}a.pop();return a};
diff_match_patch.prototype.diff_bisect_=function(a,b,c){for(var d=a.length,e=b.length,f=Math.ceil((d+e)/2),g=f,h=2*f,j=Array(h),i=Array(h),k=0;k<h;k++)j[k]=-1,i[k]=-1;j[g+1]=0;i[g+1]=0;for(var k=d-e,q=0!=k%2,r=0,t=0,p=0,w=0,v=0;v<f&&!((new Date).getTime()>c);v++){for(var n=-v+r;n<=v-t;n+=2){var l=g+n,m;m=n==-v||n!=v&&j[l-1]<j[l+1]?j[l+1]:j[l-1]+1;for(var s=m-n;m<d&&s<e&&a.charAt(m)==b.charAt(s);)m++,s++;j[l]=m;if(m>d)t+=2;else if(s>e)r+=2;else if(q&&(l=g+k-n,0<=l&&l<h&&-1!=i[l])){var u=d-i[l];if(m>=
u)return this.diff_bisectSplit_(a,b,m,s,c)}}for(n=-v+p;n<=v-w;n+=2){l=g+n;u=n==-v||n!=v&&i[l-1]<i[l+1]?i[l+1]:i[l-1]+1;for(m=u-n;u<d&&m<e&&a.charAt(d-u-1)==b.charAt(e-m-1);)u++,m++;i[l]=u;if(u>d)w+=2;else if(m>e)p+=2;else if(!q&&(l=g+k-n,0<=l&&(l<h&&-1!=j[l])&&(m=j[l],s=g+m-l,u=d-u,m>=u)))return this.diff_bisectSplit_(a,b,m,s,c)}}return[[-1,a],[1,b]]};
var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]};
diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,
b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a};
diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[new diff_match_patch.Diff(DIFF_INSERT,b)];if(!b)return[new diff_match_patch.Diff(DIFF_DELETE,a)];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[new diff_match_patch.Diff(DIFF_INSERT,e.substring(0,g)),new diff_match_patch.Diff(DIFF_EQUAL,f),new diff_match_patch.Diff(DIFF_INSERT,e.substring(g+f.length))],a.length>b.length&&(c[0][0]=c[2][0]=DIFF_DELETE),c):1==f.length?[new diff_match_patch.Diff(DIFF_DELETE,
a),new diff_match_patch.Diff(DIFF_INSERT,b)]:(e=this.diff_halfMatch_(a,b))?(b=e[1],f=e[3],a=e[4],e=this.diff_main(e[0],e[2],c,d),c=this.diff_main(b,f,c,d),e.concat([new diff_match_patch.Diff(DIFF_EQUAL,a)],c)):c&&100<a.length&&100<b.length?this.diff_lineMode_(a,b,d):this.diff_bisect_(a,b,d)};
diff_match_patch.prototype.diff_lineMode_=function(a,b,c){var d=this.diff_linesToChars_(a,b);a=d.chars1;b=d.chars2;d=d.lineArray;a=this.diff_main(a,b,!1,c);this.diff_charsToLines_(a,d);this.diff_cleanupSemantic(a);a.push(new diff_match_patch.Diff(DIFF_EQUAL,""));for(var e=d=b=0,f="",g="";b<a.length;){switch(a[b][0]){case DIFF_INSERT:e++;g+=a[b][1];break;case DIFF_DELETE:d++;f+=a[b][1];break;case DIFF_EQUAL:if(1<=d&&1<=e){a.splice(b-d-e,d+e);b=b-d-e;d=this.diff_main(f,g,!1,c);for(e=d.length-1;0<=e;e--)a.splice(b,
0,d[e]);b+=d.length}d=e=0;g=f=""}b++}a.pop();return a};
diff_match_patch.prototype.diff_bisect_=function(a,b,c){for(var d=a.length,e=b.length,f=Math.ceil((d+e)/2),g=2*f,h=Array(g),l=Array(g),k=0;k<g;k++)h[k]=-1,l[k]=-1;h[f+1]=0;l[f+1]=0;k=d-e;for(var m=0!=k%2,p=0,x=0,w=0,q=0,t=0;t<f&&!((new Date).getTime()>c);t++){for(var v=-t+p;v<=t-x;v+=2){var n=f+v;var r=v==-t||v!=t&&h[n-1]<h[n+1]?h[n+1]:h[n-1]+1;for(var y=r-v;r<d&&y<e&&a.charAt(r)==b.charAt(y);)r++,y++;h[n]=r;if(r>d)x+=2;else if(y>e)p+=2;else if(m&&(n=f+k-v,0<=n&&n<g&&-1!=l[n])){var u=d-l[n];if(r>=
u)return this.diff_bisectSplit_(a,b,r,y,c)}}for(v=-t+w;v<=t-q;v+=2){n=f+v;u=v==-t||v!=t&&l[n-1]<l[n+1]?l[n+1]:l[n-1]+1;for(r=u-v;u<d&&r<e&&a.charAt(d-u-1)==b.charAt(e-r-1);)u++,r++;l[n]=u;if(u>d)q+=2;else if(r>e)w+=2;else if(!m&&(n=f+k-v,0<=n&&n<g&&-1!=h[n]&&(r=h[n],y=f+r-n,u=d-u,r>=u)))return this.diff_bisectSplit_(a,b,r,y,c)}}return[new diff_match_patch.Diff(DIFF_DELETE,a),new diff_match_patch.Diff(DIFF_INSERT,b)]};
diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)};
diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,f=-1,g=d.length;f<a.length-1;){f=a.indexOf("\n",c);-1==f&&(f=a.length-1);var r=a.substring(c,f+1),c=f+1;(e.hasOwnProperty?e.hasOwnProperty(r):void 0!==e[r])?b+=String.fromCharCode(e[r]):(b+=String.fromCharCode(g),e[r]=g,d[g++]=r)}return b}var d=[],e={};d[0]="";var f=c(a),g=c(b);return{chars1:f,chars2:g,lineArray:d}};
diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,g=-1,h=d.length;g<a.length-1;){g=a.indexOf("\n",c);-1==g&&(g=a.length-1);var l=a.substring(c,g+1);(e.hasOwnProperty?e.hasOwnProperty(l):void 0!==e[l])?b+=String.fromCharCode(e[l]):(h==f&&(l=a.substring(c),g=a.length),b+=String.fromCharCode(h),e[l]=h,d[h++]=l);c=g+1}return b}var d=[],e={};d[0]="";var f=4E4,g=c(a);f=65535;var h=c(b);return{chars1:g,chars2:h,lineArray:d}};
diff_match_patch.prototype.diff_charsToLines_=function(a,b){for(var c=0;c<a.length;c++){for(var d=a[c][1],e=[],f=0;f<d.length;f++)e[f]=b[d.charCodeAt(f)];a[c][1]=e.join("")}};diff_match_patch.prototype.diff_commonPrefix=function(a,b){if(!a||!b||a.charAt(0)!=b.charAt(0))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;)a.substring(f,e)==b.substring(f,e)?f=c=e:d=e,e=Math.floor((d-c)/2+c);return e};
diff_match_patch.prototype.diff_commonSuffix=function(a,b){if(!a||!b||a.charAt(a.length-1)!=b.charAt(b.length-1))return 0;for(var c=0,d=Math.min(a.length,b.length),e=d,f=0;c<e;)a.substring(a.length-e,a.length-f)==b.substring(b.length-e,b.length-f)?f=c=e:d=e,e=Math.floor((d-c)/2+c);return e};
diff_match_patch.prototype.diff_commonOverlap_=function(a,b){var c=a.length,d=b.length;if(0==c||0==d)return 0;c>d?a=a.substring(c-d):c<d&&(b=b.substring(0,c));c=Math.min(c,d);if(a==b)return c;for(var d=0,e=1;;){var f=a.substring(c-e),f=b.indexOf(f);if(-1==f)return d;e+=f;if(0==f||a.substring(c-e)==b.substring(0,e))d=e,e++}};
diff_match_patch.prototype.diff_halfMatch_=function(a,b){function c(a,b,c){for(var d=a.substring(c,c+Math.floor(a.length/4)),e=-1,g="",h,j,n,l;-1!=(e=b.indexOf(d,e+1));){var m=f.diff_commonPrefix(a.substring(c),b.substring(e)),s=f.diff_commonSuffix(a.substring(0,c),b.substring(0,e));g.length<s+m&&(g=b.substring(e-s,e)+b.substring(e,e+m),h=a.substring(0,c-s),j=a.substring(c+m),n=b.substring(0,e-s),l=b.substring(e+m))}return 2*g.length>=a.length?[h,j,n,l,g]:null}if(0>=this.Diff_Timeout)return null;
var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.length<d.length)return null;var f=this,g=c(d,e,Math.ceil(d.length/4)),d=c(d,e,Math.ceil(d.length/2)),h;if(!g&&!d)return null;h=d?g?g[4].length>d[4].length?g:d:d:g;var j;a.length>b.length?(g=h[0],d=h[1],e=h[2],j=h[3]):(e=h[0],j=h[1],g=h[2],d=h[3]);h=h[4];return[g,d,e,j,h]};
diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,j=0,i=0;f<a.length;)0==a[f][0]?(c[d++]=f,g=j,h=i,i=j=0,e=a[f][1]):(1==a[f][0]?j+=a[f][1].length:i+=a[f][1].length,e&&(e.length<=Math.max(g,h)&&e.length<=Math.max(j,i))&&(a.splice(c[d-1],0,[-1,e]),a[c[d-1]+1][0]=1,d--,d--,f=0<d?c[d-1]:-1,i=j=h=g=0,e=null,b=!0)),f++;b&&this.diff_cleanupMerge(a);this.diff_cleanupSemanticLossless(a);for(f=1;f<a.length;){if(-1==a[f-1][0]&&1==a[f][0]){b=a[f-1][1];c=a[f][1];
d=this.diff_commonOverlap_(b,c);e=this.diff_commonOverlap_(c,b);if(d>=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,[0,c.substring(0,d)]),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,[0,b.substring(0,e)]),a[f-1][0]=1,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=-1,a[f+1][1]=b.substring(e),f++;f++}f++}};
diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_),c=g&&c.match(diff_match_patch.linebreakRegex_),d=h&&d.match(diff_match_patch.linebreakRegex_),i=c&&a.match(diff_match_patch.blanklineEndRegex_),j=d&&b.match(diff_match_patch.blanklineStartRegex_);
return i||j?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c<a.length-1;){if(0==a[c-1][0]&&0==a[c+1][0]){var d=a[c-1][1],e=a[c][1],f=a[c+1][1],g=this.diff_commonSuffix(d,e);if(g)var h=e.substring(e.length-g),d=d.substring(0,d.length-g),e=h+e.substring(0,e.length-g),f=h+f;for(var g=d,h=e,j=f,i=b(d,e)+b(e,f);e.charAt(0)===f.charAt(0);){var d=d+e.charAt(0),e=e.substring(1)+f.charAt(0),f=f.substring(1),k=b(d,e)+b(e,f);k>=i&&(i=k,g=d,h=e,j=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-1,1),c--),a[c][1]=
h,j?a[c+1][1]=j:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/;
diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,j=!1,i=!1;f<a.length;){if(0==a[f][0])a[f][1].length<this.Diff_EditCost&&(j||i)?(c[d++]=f,g=j,h=i,e=a[f][1]):(d=0,e=null),j=i=!1;else if(-1==a[f][0]?i=!0:j=!0,e&&(g&&h&&j&&i||e.length<this.Diff_EditCost/2&&3==g+h+j+i))a.splice(c[d-1],0,[-1,e]),a[c[d-1]+1][0]=1,d--,e=null,g&&h?(j=i=!0,d=0):(d--,f=0<d?c[d-1]:-1,j=i=!1),b=!0;f++}b&&this.diff_cleanupMerge(a)};
diff_match_patch.prototype.diff_cleanupMerge=function(a){a.push([0,""]);for(var b=0,c=0,d=0,e="",f="",g;b<a.length;)switch(a[b][0]){case 1:d++;f+=a[b][1];b++;break;case -1:c++;e+=a[b][1];b++;break;case 0:1<c+d?(0!==c&&0!==d&&(g=this.diff_commonPrefix(f,e),0!==g&&(0<b-c-d&&0==a[b-c-d-1][0]?a[b-c-d-1][1]+=f.substring(0,g):(a.splice(0,0,[0,f.substring(0,g)]),b++),f=f.substring(g),e=e.substring(g)),g=this.diff_commonSuffix(f,e),0!==g&&(a[b][1]=f.substring(f.length-g)+a[b][1],f=f.substring(0,f.length-
g),e=e.substring(0,e.length-g))),0===c?a.splice(b-d,c+d,[1,f]):0===d?a.splice(b-c,c+d,[-1,e]):a.splice(b-c-d,c+d,[-1,e],[1,f]),b=b-c-d+(c?1:0)+(d?1:0)+1):0!==b&&0==a[b-1][0]?(a[b-1][1]+=a[b][1],a.splice(b,1)):b++,c=d=0,f=e=""}""===a[a.length-1][1]&&a.pop();c=!1;for(b=1;b<a.length-1;)0==a[b-1][0]&&0==a[b+1][0]&&(a[b][1].substring(a[b][1].length-a[b-1][1].length)==a[b-1][1]?(a[b][1]=a[b-1][1]+a[b][1].substring(0,a[b][1].length-a[b-1][1].length),a[b+1][1]=a[b-1][1]+a[b+1][1],a.splice(b-1,1),c=!0):a[b][1].substring(0,
a[b+1][1].length)==a[b+1][1]&&(a[b-1][1]+=a[b+1][1],a[b][1]=a[b][1].substring(a[b+1][1].length)+a[b+1][1],a.splice(b+1,1),c=!0)),b++;c&&this.diff_cleanupMerge(a)};diff_match_patch.prototype.diff_xIndex=function(a,b){var c=0,d=0,e=0,f=0,g;for(g=0;g<a.length;g++){1!==a[g][0]&&(c+=a[g][1].length);-1!==a[g][0]&&(d+=a[g][1].length);if(c>b)break;e=c;f=d}return a.length!=g&&-1===a[g][0]?f:f+(b-e)};
diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=/</g,e=/>/g,f=/\n/g,g=0;g<a.length;g++){var h=a[g][0],j=a[g][1],j=j.replace(c,"&amp;").replace(d,"&lt;").replace(e,"&gt;").replace(f,"&para;<br>");switch(h){case 1:b[g]='<ins style="background:#e6ffe6;">'+j+"</ins>";break;case -1:b[g]='<del style="background:#ffe6e6;">'+j+"</del>";break;case 0:b[g]="<span>"+j+"</span>"}}return b.join("")};
diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;c<a.length;c++)1!==a[c][0]&&(b[c]=a[c][1]);return b.join("")};diff_match_patch.prototype.diff_text2=function(a){for(var b=[],c=0;c<a.length;c++)-1!==a[c][0]&&(b[c]=a[c][1]);return b.join("")};diff_match_patch.prototype.diff_levenshtein=function(a){for(var b=0,c=0,d=0,e=0;e<a.length;e++){var f=a[e][0],g=a[e][1];switch(f){case 1:c+=g.length;break;case -1:d+=g.length;break;case 0:b+=Math.max(c,d),d=c=0}}return b+=Math.max(c,d)};
diff_match_patch.prototype.diff_toDelta=function(a){for(var b=[],c=0;c<a.length;c++)switch(a[c][0]){case 1:b[c]="+"+encodeURI(a[c][1]);break;case -1:b[c]="-"+a[c][1].length;break;case 0:b[c]="="+a[c][1].length}return b.join("\t").replace(/%20/g," ")};
diff_match_patch.prototype.diff_fromDelta=function(a,b){for(var c=[],d=0,e=0,f=b.split(/\t/g),g=0;g<f.length;g++){var h=f[g].substring(1);switch(f[g].charAt(0)){case "+":try{c[d++]=[1,decodeURI(h)]}catch(j){throw Error("Illegal escape in diff_fromDelta: "+h);}break;case "-":case "=":var i=parseInt(h,10);if(isNaN(i)||0>i)throw Error("Invalid number in diff_fromDelta: "+h);h=a.substring(e,e+=i);"="==f[g].charAt(0)?c[d++]=[0,h]:c[d++]=[-1,h];break;default:if(f[g])throw Error("Invalid diff operation in diff_fromDelta: "+
f[g]);}}if(e!=a.length)throw Error("Delta length ("+e+") does not equal source text length ("+a.length+").");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(null==a||null==b||null==c)throw Error("Null input. (match_main)");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};
diff_match_patch.prototype.match_bitap_=function(a,b,c){function d(a,d){var e=a/b.length,g=Math.abs(c-d);return!f.Match_Distance?g?1:e:e+g/f.Match_Distance}if(b.length>this.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));for(var j=1<<b.length-1,h=-1,i,k,q=b.length+a.length,r,t=0;t<b.length;t++){i=0;for(k=q;i<k;)d(t,c+
k)<=g?i=k:q=k,k=Math.floor((q-i)/2+i);q=k;i=Math.max(1,c-k+1);var p=Math.min(c+k,a.length)+b.length;k=Array(p+2);for(k[p+1]=(1<<t)-1;p>=i;p--){var w=e[a.charAt(p-1)];k[p]=0===t?(k[p+1]<<1|1)&w:(k[p+1]<<1|1)&w|((r[p+1]|r[p])<<1|1)|r[p+1];if(k[p]&j&&(w=d(t,p-1),w<=g))if(g=w,h=p-1,h>c)i=Math.max(1,2*c-h);else break}if(d(t+1,c)>g)break;r=k}return h};
diff_match_patch.prototype.diff_commonOverlap_=function(a,b){var c=a.length,d=b.length;if(0==c||0==d)return 0;c>d?a=a.substring(c-d):c<d&&(b=b.substring(0,c));c=Math.min(c,d);if(a==b)return c;d=0;for(var e=1;;){var f=a.substring(c-e);f=b.indexOf(f);if(-1==f)return d;e+=f;if(0==f||a.substring(c-e)==b.substring(0,e))d=e,e++}};
diff_match_patch.prototype.diff_halfMatch_=function(a,b){function c(a,b,c){for(var d=a.substring(c,c+Math.floor(a.length/4)),e=-1,g="",h,k,l,m;-1!=(e=b.indexOf(d,e+1));){var p=f.diff_commonPrefix(a.substring(c),b.substring(e)),u=f.diff_commonSuffix(a.substring(0,c),b.substring(0,e));g.length<u+p&&(g=b.substring(e-u,e)+b.substring(e,e+p),h=a.substring(0,c-u),k=a.substring(c+p),l=b.substring(0,e-u),m=b.substring(e+p))}return 2*g.length>=a.length?[h,k,l,m,g]:null}if(0>=this.Diff_Timeout)return null;
var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.length<d.length)return null;var f=this,g=c(d,e,Math.ceil(d.length/4));d=c(d,e,Math.ceil(d.length/2));if(g||d)g=d?g?g[4].length>d[4].length?g:d:d:g;else return null;if(a.length>b.length){d=g[0];e=g[1];var h=g[2];var l=g[3]}else h=g[0],l=g[1],d=g[2],e=g[3];return[d,e,h,l,g[4]]};
diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,l=0,k=0;f<a.length;)a[f][0]==DIFF_EQUAL?(c[d++]=f,g=l,h=k,k=l=0,e=a[f][1]):(a[f][0]==DIFF_INSERT?l+=a[f][1].length:k+=a[f][1].length,e&&e.length<=Math.max(g,h)&&e.length<=Math.max(l,k)&&(a.splice(c[d-1],0,new diff_match_patch.Diff(DIFF_DELETE,e)),a[c[d-1]+1][0]=DIFF_INSERT,d--,d--,f=0<d?c[d-1]:-1,k=l=h=g=0,e=null,b=!0)),f++;b&&this.diff_cleanupMerge(a);this.diff_cleanupSemanticLossless(a);for(f=1;f<
a.length;){if(a[f-1][0]==DIFF_DELETE&&a[f][0]==DIFF_INSERT){b=a[f-1][1];c=a[f][1];d=this.diff_commonOverlap_(b,c);e=this.diff_commonOverlap_(c,b);if(d>=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,c.substring(0,d))),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,b.substring(0,e))),a[f-1][0]=DIFF_INSERT,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=DIFF_DELETE,
a[f+1][1]=b.substring(e),f++;f++}f++}};
diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_);c=g&&c.match(diff_match_patch.linebreakRegex_);d=h&&d.match(diff_match_patch.linebreakRegex_);var k=c&&a.match(diff_match_patch.blanklineEndRegex_),l=d&&b.match(diff_match_patch.blanklineStartRegex_);
return k||l?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c<a.length-1;){if(a[c-1][0]==DIFF_EQUAL&&a[c+1][0]==DIFF_EQUAL){var d=a[c-1][1],e=a[c][1],f=a[c+1][1],g=this.diff_commonSuffix(d,e);if(g){var h=e.substring(e.length-g);d=d.substring(0,d.length-g);e=h+e.substring(0,e.length-g);f=h+f}g=d;h=e;for(var l=f,k=b(d,e)+b(e,f);e.charAt(0)===f.charAt(0);){d+=e.charAt(0);e=e.substring(1)+f.charAt(0);f=f.substring(1);var m=b(d,e)+b(e,f);m>=k&&(k=m,g=d,h=e,l=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-
1,1),c--),a[c][1]=h,l?a[c+1][1]=l:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/;
diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,l=!1,k=!1;f<a.length;)a[f][0]==DIFF_EQUAL?(a[f][1].length<this.Diff_EditCost&&(l||k)?(c[d++]=f,g=l,h=k,e=a[f][1]):(d=0,e=null),l=k=!1):(a[f][0]==DIFF_DELETE?k=!0:l=!0,e&&(g&&h&&l&&k||e.length<this.Diff_EditCost/2&&3==g+h+l+k)&&(a.splice(c[d-1],0,new diff_match_patch.Diff(DIFF_DELETE,e)),a[c[d-1]+1][0]=DIFF_INSERT,d--,e=null,g&&h?(l=k=!0,d=0):(d--,f=0<d?c[d-1]:-1,l=k=!1),b=!0)),f++;b&&this.diff_cleanupMerge(a)};
diff_match_patch.prototype.diff_cleanupMerge=function(a){a.push(new diff_match_patch.Diff(DIFF_EQUAL,""));for(var b=0,c=0,d=0,e="",f="",g;b<a.length;)switch(a[b][0]){case DIFF_INSERT:d++;f+=a[b][1];b++;break;case DIFF_DELETE:c++;e+=a[b][1];b++;break;case DIFF_EQUAL:1<c+d?(0!==c&&0!==d&&(g=this.diff_commonPrefix(f,e),0!==g&&(0<b-c-d&&a[b-c-d-1][0]==DIFF_EQUAL?a[b-c-d-1][1]+=f.substring(0,g):(a.splice(0,0,new diff_match_patch.Diff(DIFF_EQUAL,f.substring(0,g))),b++),f=f.substring(g),e=e.substring(g)),
g=this.diff_commonSuffix(f,e),0!==g&&(a[b][1]=f.substring(f.length-g)+a[b][1],f=f.substring(0,f.length-g),e=e.substring(0,e.length-g))),b-=c+d,a.splice(b,c+d),e.length&&(a.splice(b,0,new diff_match_patch.Diff(DIFF_DELETE,e)),b++),f.length&&(a.splice(b,0,new diff_match_patch.Diff(DIFF_INSERT,f)),b++),b++):0!==b&&a[b-1][0]==DIFF_EQUAL?(a[b-1][1]+=a[b][1],a.splice(b,1)):b++,c=d=0,f=e=""}""===a[a.length-1][1]&&a.pop();c=!1;for(b=1;b<a.length-1;)a[b-1][0]==DIFF_EQUAL&&a[b+1][0]==DIFF_EQUAL&&(a[b][1].substring(a[b][1].length-
a[b-1][1].length)==a[b-1][1]?(a[b][1]=a[b-1][1]+a[b][1].substring(0,a[b][1].length-a[b-1][1].length),a[b+1][1]=a[b-1][1]+a[b+1][1],a.splice(b-1,1),c=!0):a[b][1].substring(0,a[b+1][1].length)==a[b+1][1]&&(a[b-1][1]+=a[b+1][1],a[b][1]=a[b][1].substring(a[b+1][1].length)+a[b+1][1],a.splice(b+1,1),c=!0)),b++;c&&this.diff_cleanupMerge(a)};
diff_match_patch.prototype.diff_xIndex=function(a,b){var c=0,d=0,e=0,f=0,g;for(g=0;g<a.length;g++){a[g][0]!==DIFF_INSERT&&(c+=a[g][1].length);a[g][0]!==DIFF_DELETE&&(d+=a[g][1].length);if(c>b)break;e=c;f=d}return a.length!=g&&a[g][0]===DIFF_DELETE?f:f+(b-e)};
diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=/</g,e=/>/g,f=/\n/g,g=0;g<a.length;g++){var h=a[g][0],l=a[g][1].replace(c,"&amp;").replace(d,"&lt;").replace(e,"&gt;").replace(f,"&para;<br>");switch(h){case DIFF_INSERT:b[g]='<ins style="background:#e6ffe6;">'+l+"</ins>";break;case DIFF_DELETE:b[g]='<del style="background:#ffe6e6;">'+l+"</del>";break;case DIFF_EQUAL:b[g]="<span>"+l+"</span>"}}return b.join("")};
diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;c<a.length;c++)a[c][0]!==DIFF_INSERT&&(b[c]=a[c][1]);return b.join("")};diff_match_patch.prototype.diff_text2=function(a){for(var b=[],c=0;c<a.length;c++)a[c][0]!==DIFF_DELETE&&(b[c]=a[c][1]);return b.join("")};
diff_match_patch.prototype.diff_levenshtein=function(a){for(var b=0,c=0,d=0,e=0;e<a.length;e++){var f=a[e][1];switch(a[e][0]){case DIFF_INSERT:c+=f.length;break;case DIFF_DELETE:d+=f.length;break;case DIFF_EQUAL:b+=Math.max(c,d),d=c=0}}return b+=Math.max(c,d)};
diff_match_patch.prototype.diff_toDelta=function(a){for(var b=[],c=0;c<a.length;c++)switch(a[c][0]){case DIFF_INSERT:b[c]="+"+encodeURI(a[c][1]);break;case DIFF_DELETE:b[c]="-"+a[c][1].length;break;case DIFF_EQUAL:b[c]="="+a[c][1].length}return b.join("\t").replace(/%20/g," ")};
diff_match_patch.prototype.diff_fromDelta=function(a,b){for(var c=[],d=0,e=0,f=b.split(/\t/g),g=0;g<f.length;g++){var h=f[g].substring(1);switch(f[g].charAt(0)){case "+":try{c[d++]=new diff_match_patch.Diff(DIFF_INSERT,decodeURI(h))}catch(k){throw Error("Illegal escape in diff_fromDelta: "+h);}break;case "-":case "=":var l=parseInt(h,10);if(isNaN(l)||0>l)throw Error("Invalid number in diff_fromDelta: "+h);h=a.substring(e,e+=l);"="==f[g].charAt(0)?c[d++]=new diff_match_patch.Diff(DIFF_EQUAL,h):c[d++]=
new diff_match_patch.Diff(DIFF_DELETE,h);break;default:if(f[g])throw Error("Invalid diff operation in diff_fromDelta: "+f[g]);}}if(e!=a.length)throw Error("Delta length ("+e+") does not equal source text length ("+a.length+").");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(null==a||null==b||null==c)throw Error("Null input. (match_main)");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};
diff_match_patch.prototype.match_bitap_=function(a,b,c){function d(a,d){var e=a/b.length,g=Math.abs(c-d);return f.Match_Distance?e+g/f.Match_Distance:g?1:e}if(b.length>this.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));var l=1<<b.length-1;h=-1;for(var k,m,p=b.length+a.length,x,w=0;w<b.length;w++){k=0;for(m=p;k<m;)d(w,
c+m)<=g?k=m:p=m,m=Math.floor((p-k)/2+k);p=m;k=Math.max(1,c-m+1);var q=Math.min(c+m,a.length)+b.length;m=Array(q+2);for(m[q+1]=(1<<w)-1;q>=k;q--){var t=e[a.charAt(q-1)];m[q]=0===w?(m[q+1]<<1|1)&t:(m[q+1]<<1|1)&t|(x[q+1]|x[q])<<1|1|x[q+1];if(m[q]&l&&(t=d(w,q-1),t<=g))if(g=t,h=q-1,h>c)k=Math.max(1,2*c-h);else break}if(d(w+1,c)>g)break;x=m}return h};
diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c<a.length;c++)b[a.charAt(c)]=0;for(c=0;c<a.length;c++)b[a.charAt(c)]|=1<<a.length-c-1;return b};
diff_match_patch.prototype.patch_addContext_=function(a,b){if(0!=b.length){for(var c=b.substring(a.start2,a.start2+a.length1),d=0;b.indexOf(c)!=b.lastIndexOf(c)&&c.length<this.Match_MaxBits-this.Patch_Margin-this.Patch_Margin;)d+=this.Patch_Margin,c=b.substring(a.start2-d,a.start2+a.length1+d);d+=this.Patch_Margin;(c=b.substring(a.start2-d,a.start2))&&a.diffs.unshift([0,c]);(d=b.substring(a.start2+a.length1,a.start2+a.length1+d))&&a.diffs.push([0,d]);a.start1-=c.length;a.start2-=c.length;a.length1+=
c.length+d.length;a.length2+=c.length+d.length}};
diff_match_patch.prototype.patch_make=function(a,b,c){var d;if("string"==typeof a&&"string"==typeof b&&"undefined"==typeof c)d=a,b=this.diff_main(d,b,!0),2<b.length&&(this.diff_cleanupSemantic(b),this.diff_cleanupEfficiency(b));else if(a&&"object"==typeof a&&"undefined"==typeof b&&"undefined"==typeof c)b=a,d=this.diff_text1(b);else if("string"==typeof a&&b&&"object"==typeof b&&"undefined"==typeof c)d=a;else if("string"==typeof a&&"string"==typeof b&&c&&"object"==typeof c)d=a,b=c;else throw Error("Unknown call format to patch_make.");
if(0===b.length)return[];c=[];a=new diff_match_patch.patch_obj;for(var e=0,f=0,g=0,h=d,j=0;j<b.length;j++){var i=b[j][0],k=b[j][1];!e&&0!==i&&(a.start1=f,a.start2=g);switch(i){case 1:a.diffs[e++]=b[j];a.length2+=k.length;d=d.substring(0,g)+k+d.substring(g);break;case -1:a.length1+=k.length;a.diffs[e++]=b[j];d=d.substring(0,g)+d.substring(g+k.length);break;case 0:k.length<=2*this.Patch_Margin&&e&&b.length!=j+1?(a.diffs[e++]=b[j],a.length1+=k.length,a.length2+=k.length):k.length>=2*this.Patch_Margin&&
e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}1!==i&&(f+=k.length);-1!==i&&(g+=k.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;c<a.length;c++){var d=a[c],e=new diff_match_patch.patch_obj;e.diffs=[];for(var f=0;f<d.diffs.length;f++)e.diffs[f]=d.diffs[f].slice();e.start1=d.start1;e.start2=d.start2;e.length1=d.length1;e.length2=d.length2;b[c]=e}return b};
diff_match_patch.prototype.patch_apply=function(a,b){if(0==a.length)return[b,[]];a=this.patch_deepCopy(a);var c=this.patch_addPadding(a);b=c+b+c;this.patch_splitMax(a);for(var d=0,e=[],f=0;f<a.length;f++){var g=a[f].start2+d,h=this.diff_text1(a[f].diffs),j,i=-1;if(h.length>this.Match_MaxBits){if(j=this.match_main(b,h.substring(0,this.Match_MaxBits),g),-1!=j&&(i=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==i||j>=i))j=-1}else j=this.match_main(b,h,g);
if(-1==j)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=j-g,g=-1==i?b.substring(j,j+h.length):b.substring(j,i+this.Match_MaxBits),h==g)b=b.substring(0,j)+this.diff_text2(a[f].diffs)+b.substring(j+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);for(var h=0,k,i=0;i<a[f].diffs.length;i++){var q=a[f].diffs[i];0!==q[0]&&(k=this.diff_xIndex(g,h));1===q[0]?b=b.substring(0,
j+k)+q[1]+b.substring(j+k):-1===q[0]&&(b=b.substring(0,j+k)+b.substring(j+this.diff_xIndex(g,h+q[1].length)));-1!==q[0]&&(h+=q[1].length)}}}b=b.substring(c.length,b.length-c.length);return[b,e]};
diff_match_patch.prototype.patch_addPadding=function(a){for(var b=this.Patch_Margin,c="",d=1;d<=b;d++)c+=String.fromCharCode(d);for(d=0;d<a.length;d++)a[d].start1+=b,a[d].start2+=b;var d=a[0],e=d.diffs;if(0==e.length||0!=e[0][0])e.unshift([0,c]),d.start1-=b,d.start2-=b,d.length1+=b,d.length2+=b;else if(b>e[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;0==e.length||0!=e[e.length-1][0]?(e.push([0,
c]),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c};
diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c<a.length;c++)if(!(a[c].length1<=b)){var d=a[c];a.splice(c--,1);for(var e=d.start1,f=d.start2,g="";0!==d.diffs.length;){var h=new diff_match_patch.patch_obj,j=!0;h.start1=e-g.length;h.start2=f-g.length;""!==g&&(h.length1=h.length2=g.length,h.diffs.push([0,g]));for(;0!==d.diffs.length&&h.length1<b-this.Patch_Margin;){var g=d.diffs[0][0],i=d.diffs[0][1];1===g?(h.length2+=i.length,f+=i.length,h.diffs.push(d.diffs.shift()),
j=!1):-1===g&&1==h.diffs.length&&0==h.diffs[0][0]&&i.length>2*b?(h.length1+=i.length,e+=i.length,j=!1,h.diffs.push([g,i]),d.diffs.shift()):(i=i.substring(0,b-h.length1-this.Patch_Margin),h.length1+=i.length,e+=i.length,0===g?(h.length2+=i.length,f+=i.length):j=!1,h.diffs.push([g,i]),i==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(i.length))}g=this.diff_text2(h.diffs);g=g.substring(g.length-this.Patch_Margin);i=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==i&&
(h.length1+=i.length,h.length2+=i.length,0!==h.diffs.length&&0===h.diffs[h.diffs.length-1][0]?h.diffs[h.diffs.length-1][1]+=i:h.diffs.push([0,i]));j||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c<a.length;c++)b[c]=a[c];return b.join("")};
diff_match_patch.prototype.patch_addContext_=function(a,b){if(0!=b.length){if(null===a.start2)throw Error("patch not initialized");for(var c=b.substring(a.start2,a.start2+a.length1),d=0;b.indexOf(c)!=b.lastIndexOf(c)&&c.length<this.Match_MaxBits-this.Patch_Margin-this.Patch_Margin;)d+=this.Patch_Margin,c=b.substring(a.start2-d,a.start2+a.length1+d);d+=this.Patch_Margin;(c=b.substring(a.start2-d,a.start2))&&a.diffs.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));(d=b.substring(a.start2+a.length1,
a.start2+a.length1+d))&&a.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,d));a.start1-=c.length;a.start2-=c.length;a.length1+=c.length+d.length;a.length2+=c.length+d.length}};
diff_match_patch.prototype.patch_make=function(a,b,c){if("string"==typeof a&&"string"==typeof b&&"undefined"==typeof c){var d=a;b=this.diff_main(d,b,!0);2<b.length&&(this.diff_cleanupSemantic(b),this.diff_cleanupEfficiency(b))}else if(a&&"object"==typeof a&&"undefined"==typeof b&&"undefined"==typeof c)b=a,d=this.diff_text1(b);else if("string"==typeof a&&b&&"object"==typeof b&&"undefined"==typeof c)d=a;else if("string"==typeof a&&"string"==typeof b&&c&&"object"==typeof c)d=a,b=c;else throw Error("Unknown call format to patch_make.");
if(0===b.length)return[];c=[];a=new diff_match_patch.patch_obj;for(var e=0,f=0,g=0,h=d,l=0;l<b.length;l++){var k=b[l][0],m=b[l][1];e||k===DIFF_EQUAL||(a.start1=f,a.start2=g);switch(k){case DIFF_INSERT:a.diffs[e++]=b[l];a.length2+=m.length;d=d.substring(0,g)+m+d.substring(g);break;case DIFF_DELETE:a.length1+=m.length;a.diffs[e++]=b[l];d=d.substring(0,g)+d.substring(g+m.length);break;case DIFF_EQUAL:m.length<=2*this.Patch_Margin&&e&&b.length!=l+1?(a.diffs[e++]=b[l],a.length1+=m.length,a.length2+=m.length):
m.length>=2*this.Patch_Margin&&e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}k!==DIFF_INSERT&&(f+=m.length);k!==DIFF_DELETE&&(g+=m.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};
diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;c<a.length;c++){var d=a[c],e=new diff_match_patch.patch_obj;e.diffs=[];for(var f=0;f<d.diffs.length;f++)e.diffs[f]=new diff_match_patch.Diff(d.diffs[f][0],d.diffs[f][1]);e.start1=d.start1;e.start2=d.start2;e.length1=d.length1;e.length2=d.length2;b[c]=e}return b};
diff_match_patch.prototype.patch_apply=function(a,b){if(0==a.length)return[b,[]];a=this.patch_deepCopy(a);var c=this.patch_addPadding(a);b=c+b+c;this.patch_splitMax(a);for(var d=0,e=[],f=0;f<a.length;f++){var g=a[f].start2+d,h=this.diff_text1(a[f].diffs),l=-1;if(h.length>this.Match_MaxBits){var k=this.match_main(b,h.substring(0,this.Match_MaxBits),g);-1!=k&&(l=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==l||k>=l)&&(k=-1)}else k=this.match_main(b,h,
g);if(-1==k)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=k-g,g=-1==l?b.substring(k,k+h.length):b.substring(k,l+this.Match_MaxBits),h==g)b=b.substring(0,k)+this.diff_text2(a[f].diffs)+b.substring(k+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);h=0;var m;for(l=0;l<a[f].diffs.length;l++){var p=a[f].diffs[l];p[0]!==DIFF_EQUAL&&(m=this.diff_xIndex(g,h));p[0]===
DIFF_INSERT?b=b.substring(0,k+m)+p[1]+b.substring(k+m):p[0]===DIFF_DELETE&&(b=b.substring(0,k+m)+b.substring(k+this.diff_xIndex(g,h+p[1].length)));p[0]!==DIFF_DELETE&&(h+=p[1].length)}}}b=b.substring(c.length,b.length-c.length);return[b,e]};
diff_match_patch.prototype.patch_addPadding=function(a){for(var b=this.Patch_Margin,c="",d=1;d<=b;d++)c+=String.fromCharCode(d);for(d=0;d<a.length;d++)a[d].start1+=b,a[d].start2+=b;d=a[0];var e=d.diffs;if(0==e.length||e[0][0]!=DIFF_EQUAL)e.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c)),d.start1-=b,d.start2-=b,d.length1+=b,d.length2+=b;else if(b>e[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;
0==e.length||e[e.length-1][0]!=DIFF_EQUAL?(e.push(new diff_match_patch.Diff(DIFF_EQUAL,c)),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c};
diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c<a.length;c++)if(!(a[c].length1<=b)){var d=a[c];a.splice(c--,1);for(var e=d.start1,f=d.start2,g="";0!==d.diffs.length;){var h=new diff_match_patch.patch_obj,l=!0;h.start1=e-g.length;h.start2=f-g.length;""!==g&&(h.length1=h.length2=g.length,h.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,g)));for(;0!==d.diffs.length&&h.length1<b-this.Patch_Margin;){g=d.diffs[0][0];var k=d.diffs[0][1];g===DIFF_INSERT?(h.length2+=
k.length,f+=k.length,h.diffs.push(d.diffs.shift()),l=!1):g===DIFF_DELETE&&1==h.diffs.length&&h.diffs[0][0]==DIFF_EQUAL&&k.length>2*b?(h.length1+=k.length,e+=k.length,l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),d.diffs.shift()):(k=k.substring(0,b-h.length1-this.Patch_Margin),h.length1+=k.length,e+=k.length,g===DIFF_EQUAL?(h.length2+=k.length,f+=k.length):l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),k==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(k.length))}g=this.diff_text2(h.diffs);
g=g.substring(g.length-this.Patch_Margin);k=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==k&&(h.length1+=k.length,h.length2+=k.length,0!==h.diffs.length&&h.diffs[h.diffs.length-1][0]===DIFF_EQUAL?h.diffs[h.diffs.length-1][1]+=k:h.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,k)));l||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c<a.length;c++)b[c]=a[c];return b.join("")};
diff_match_patch.prototype.patch_fromText=function(a){var b=[];if(!a)return b;a=a.split("\n");for(var c=0,d=/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/;c<a.length;){var e=a[c].match(d);if(!e)throw Error("Invalid patch string: "+a[c]);var f=new diff_match_patch.patch_obj;b.push(f);f.start1=parseInt(e[1],10);""===e[2]?(f.start1--,f.length1=1):"0"==e[2]?f.length1=0:(f.start1--,f.length1=parseInt(e[2],10));f.start2=parseInt(e[3],10);""===e[4]?(f.start2--,f.length2=1):"0"==e[4]?f.length2=0:(f.start2--,f.length2=
parseInt(e[4],10));for(c++;c<a.length;){e=a[c].charAt(0);try{var g=decodeURI(a[c].substring(1))}catch(h){throw Error("Illegal escape in patch_fromText: "+g);}if("-"==e)f.diffs.push([-1,g]);else if("+"==e)f.diffs.push([1,g]);else if(" "==e)f.diffs.push([0,g]);else if("@"==e)break;else if(""!==e)throw Error('Invalid patch mode "'+e+'" in: '+g);c++}}return b};diff_match_patch.patch_obj=function(){this.diffs=[];this.start2=this.start1=null;this.length2=this.length1=0};
diff_match_patch.patch_obj.prototype.toString=function(){var a,b;a=0===this.length1?this.start1+",0":1==this.length1?this.start1+1:this.start1+1+","+this.length1;b=0===this.length2?this.start2+",0":1==this.length2?this.start2+1:this.start2+1+","+this.length2;a=["@@ -"+a+" +"+b+" @@\n"];var c;for(b=0;b<this.diffs.length;b++){switch(this.diffs[b][0]){case 1:c="+";break;case -1:c="-";break;case 0:c=" "}a[b+1]=c+encodeURI(this.diffs[b][1])+"\n"}return a.join("").replace(/%20/g," ")};
this.diff_match_patch=diff_match_patch;this.DIFF_DELETE=-1;this.DIFF_INSERT=1;this.DIFF_EQUAL=0;})()
parseInt(e[4],10));for(c++;c<a.length;){e=a[c].charAt(0);try{var g=decodeURI(a[c].substring(1))}catch(h){throw Error("Illegal escape in patch_fromText: "+g);}if("-"==e)f.diffs.push(new diff_match_patch.Diff(DIFF_DELETE,g));else if("+"==e)f.diffs.push(new diff_match_patch.Diff(DIFF_INSERT,g));else if(" "==e)f.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,g));else if("@"==e)break;else if(""!==e)throw Error('Invalid patch mode "'+e+'" in: '+g);c++}}return b};
diff_match_patch.patch_obj=function(){this.diffs=[];this.start2=this.start1=null;this.length2=this.length1=0};
diff_match_patch.patch_obj.prototype.toString=function(){for(var a=["@@ -"+(0===this.length1?this.start1+",0":1==this.length1?this.start1+1:this.start1+1+","+this.length1)+" +"+(0===this.length2?this.start2+",0":1==this.length2?this.start2+1:this.start2+1+","+this.length2)+" @@\n"],b,c=0;c<this.diffs.length;c++){switch(this.diffs[c][0]){case DIFF_INSERT:b="+";break;case DIFF_DELETE:b="-";break;case DIFF_EQUAL:b=" "}a[c+1]=b+encodeURI(this.diffs[c][1])+"\n"}return a.join("").replace(/%20/g," ")};
this.diff_match_patch=diff_match_patch;this.DIFF_DELETE=DIFF_DELETE;this.DIFF_INSERT=DIFF_INSERT;this.DIFF_EQUAL=DIFF_EQUAL;
@@ -166,7 +166,7 @@
function copyObj(obj, target, overwrite) {
if (!target) { target = {}; }
for (var prop in obj)
{ if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop)))
{ if (Object.prototype.hasOwnProperty.call(obj, prop) && (overwrite !== false || !Object.prototype.hasOwnProperty.call(target, prop)))
{ target[prop] = obj[prop]; } }
return target
}
@@ -4426,7 +4426,7 @@
// The element in which the editor lives.
d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror");
// See #6982. FIXME remove when this has been fixed for a while in Chrome
if (chrome && chrome_version >= 105) { d.wrapper.style.clipPath = "inset(0px)"; }
if (chrome && chrome_version === 105) { d.wrapper.style.clipPath = "inset(0px)"; }
// This attribute is respected by automatic translation systems such as Google Translate,
// and may also be respected by tools used by human translators.
@@ -9877,7 +9877,7 @@
addLegacyProps(CodeMirror);
CodeMirror.version = "5.65.18";
CodeMirror.version = "5.65.21";
return CodeMirror;
+12
View File
@@ -0,0 +1,12 @@
INSTALL = install
PREFIX = /usr/bin
po2lmo: src/po2lmo.o src/template_lmo.o
$(CC) $(LDFLAGS) -o src/po2lmo src/po2lmo.o src/template_lmo.o
install:
$(INSTALL) -m 755 src/po2lmo $(PREFIX)
clean:
$(RM) src/po2lmo src/*.o
Binary file not shown.
@@ -0,0 +1,247 @@
/*
* lmo - Lua Machine Objects - PO to LMO conversion tool
*
* Copyright (C) 2009-2012 Jo-Philipp Wich <xm@subsignal.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "template_lmo.h"
static void die(const char *msg)
{
fprintf(stderr, "Error: %s\n", msg);
exit(1);
}
static void usage(const char *name)
{
fprintf(stderr, "Usage: %s input.po output.lmo\n", name);
exit(1);
}
static void print(const void *ptr, size_t size, size_t nmemb, FILE *stream)
{
if( fwrite(ptr, size, nmemb, stream) == 0 )
die("Failed to write stdout");
}
static int extract_string(const char *src, char *dest, int len)
{
int pos = 0;
int esc = 0;
int off = -1;
for( pos = 0; (pos < strlen(src)) && (pos < len); pos++ )
{
if( (off == -1) && (src[pos] == '"') )
{
off = pos + 1;
}
else if( off >= 0 )
{
if( esc == 1 )
{
switch (src[pos])
{
case '"':
case '\\':
off++;
break;
}
dest[pos-off] = src[pos];
esc = 0;
}
else if( src[pos] == '\\' )
{
dest[pos-off] = src[pos];
esc = 1;
}
else if( src[pos] != '"' )
{
dest[pos-off] = src[pos];
}
else
{
dest[pos-off] = '\0';
break;
}
}
}
return (off > -1) ? strlen(dest) : -1;
}
static int cmp_index(const void *a, const void *b)
{
uint32_t x = ((const lmo_entry_t *)a)->key_id;
uint32_t y = ((const lmo_entry_t *)b)->key_id;
if (x < y)
return -1;
else if (x > y)
return 1;
return 0;
}
static void print_uint32(uint32_t x, FILE *out)
{
uint32_t y = htonl(x);
print(&y, sizeof(uint32_t), 1, out);
}
static void print_index(void *array, int n, FILE *out)
{
lmo_entry_t *e;
qsort(array, n, sizeof(*e), cmp_index);
for (e = array; n > 0; n--, e++)
{
print_uint32(e->key_id, out);
print_uint32(e->val_id, out);
print_uint32(e->offset, out);
print_uint32(e->length, out);
}
}
int main(int argc, char *argv[])
{
char line[4096];
char key[4096];
char val[4096];
char tmp[4096];
int state = 0;
int offset = 0;
int length = 0;
int n_entries = 0;
void *array = NULL;
lmo_entry_t *entry = NULL;
uint32_t key_id, val_id;
FILE *in;
FILE *out;
if( (argc != 3) || ((in = fopen(argv[1], "r")) == NULL) || ((out = fopen(argv[2], "w")) == NULL) )
usage(argv[0]);
memset(line, 0, sizeof(key));
memset(key, 0, sizeof(val));
memset(val, 0, sizeof(val));
while( (NULL != fgets(line, sizeof(line), in)) || (state >= 2 && feof(in)) )
{
if( state == 0 && strstr(line, "msgid \"") == line )
{
switch(extract_string(line, key, sizeof(key)))
{
case -1:
die("Syntax error in msgid");
case 0:
state = 1;
break;
default:
state = 2;
}
}
else if( state == 1 || state == 2 )
{
if( strstr(line, "msgstr \"") == line || state == 2 )
{
switch(extract_string(line, val, sizeof(val)))
{
case -1:
state = 4;
break;
default:
state = 3;
}
}
else
{
switch(extract_string(line, tmp, sizeof(tmp)))
{
case -1:
state = 2;
break;
default:
strcat(key, tmp);
}
}
}
else if( state == 3 )
{
switch(extract_string(line, tmp, sizeof(tmp)))
{
case -1:
state = 4;
break;
default:
strcat(val, tmp);
}
}
if( state == 4 )
{
if( strlen(key) > 0 && strlen(val) > 0 )
{
key_id = sfh_hash(key, strlen(key));
val_id = sfh_hash(val, strlen(val));
if( key_id != val_id )
{
n_entries++;
array = realloc(array, n_entries * sizeof(lmo_entry_t));
entry = (lmo_entry_t *)array + n_entries - 1;
if (!array)
die("Out of memory");
entry->key_id = key_id;
entry->val_id = val_id;
entry->offset = offset;
entry->length = strlen(val);
length = strlen(val) + ((4 - (strlen(val) % 4)) % 4);
print(val, length, 1, out);
offset += length;
}
}
state = 0;
memset(key, 0, sizeof(key));
memset(val, 0, sizeof(val));
}
memset(line, 0, sizeof(line));
}
print_index(array, n_entries, out);
if( offset > 0 )
{
print_uint32(offset, out);
fsync(fileno(out));
fclose(out);
}
else
{
fclose(out);
unlink(argv[2]);
}
fclose(in);
return(0);
}
@@ -0,0 +1,328 @@
/*
* lmo - Lua Machine Objects - Base functions
*
* Copyright (C) 2009-2010 Jo-Philipp Wich <xm@subsignal.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "template_lmo.h"
/*
* Hash function from http://www.azillionmonkeys.com/qed/hash.html
* Copyright (C) 2004-2008 by Paul Hsieh
*/
uint32_t sfh_hash(const char *data, int len)
{
uint32_t hash = len, tmp;
int rem;
if (len <= 0 || data == NULL) return 0;
rem = len & 3;
len >>= 2;
/* Main loop */
for (;len > 0; len--) {
hash += sfh_get16(data);
tmp = (sfh_get16(data+2) << 11) ^ hash;
hash = (hash << 16) ^ tmp;
data += 2*sizeof(uint16_t);
hash += hash >> 11;
}
/* Handle end cases */
switch (rem) {
case 3: hash += sfh_get16(data);
hash ^= hash << 16;
hash ^= data[sizeof(uint16_t)] << 18;
hash += hash >> 11;
break;
case 2: hash += sfh_get16(data);
hash ^= hash << 11;
hash += hash >> 17;
break;
case 1: hash += *data;
hash ^= hash << 10;
hash += hash >> 1;
}
/* Force "avalanching" of final 127 bits */
hash ^= hash << 3;
hash += hash >> 5;
hash ^= hash << 4;
hash += hash >> 17;
hash ^= hash << 25;
hash += hash >> 6;
return hash;
}
uint32_t lmo_canon_hash(const char *str, int len)
{
char res[4096];
char *ptr, prev;
int off;
if (!str || len >= sizeof(res))
return 0;
for (prev = ' ', ptr = res, off = 0; off < len; prev = *str, off++, str++)
{
if (isspace(*str))
{
if (!isspace(prev))
*ptr++ = ' ';
}
else
{
*ptr++ = *str;
}
}
if ((ptr > res) && isspace(*(ptr-1)))
ptr--;
return sfh_hash(res, ptr - res);
}
lmo_archive_t * lmo_open(const char *file)
{
int in = -1;
uint32_t idx_offset = 0;
struct stat s;
lmo_archive_t *ar = NULL;
if (stat(file, &s) == -1)
goto err;
if ((in = open(file, O_RDONLY)) == -1)
goto err;
if ((ar = (lmo_archive_t *)malloc(sizeof(*ar))) != NULL)
{
memset(ar, 0, sizeof(*ar));
ar->fd = in;
ar->size = s.st_size;
fcntl(ar->fd, F_SETFD, fcntl(ar->fd, F_GETFD) | FD_CLOEXEC);
if ((ar->mmap = mmap(NULL, ar->size, PROT_READ, MAP_SHARED, ar->fd, 0)) == MAP_FAILED)
goto err;
idx_offset = ntohl(*((const uint32_t *)
(ar->mmap + ar->size - sizeof(uint32_t))));
if (idx_offset >= ar->size)
goto err;
ar->index = (lmo_entry_t *)(ar->mmap + idx_offset);
ar->length = (ar->size - idx_offset - sizeof(uint32_t)) / sizeof(lmo_entry_t);
ar->end = ar->mmap + ar->size;
return ar;
}
err:
if (in > -1)
close(in);
if (ar != NULL)
{
if ((ar->mmap != NULL) && (ar->mmap != MAP_FAILED))
munmap(ar->mmap, ar->size);
free(ar);
}
return NULL;
}
void lmo_close(lmo_archive_t *ar)
{
if (ar != NULL)
{
if ((ar->mmap != NULL) && (ar->mmap != MAP_FAILED))
munmap(ar->mmap, ar->size);
close(ar->fd);
free(ar);
ar = NULL;
}
}
lmo_catalog_t *_lmo_catalogs = NULL;
lmo_catalog_t *_lmo_active_catalog = NULL;
int lmo_load_catalog(const char *lang, const char *dir)
{
DIR *dh = NULL;
char pattern[16];
char path[PATH_MAX];
struct dirent *de = NULL;
lmo_archive_t *ar = NULL;
lmo_catalog_t *cat = NULL;
if (!lmo_change_catalog(lang))
return 0;
if (!dir || !(dh = opendir(dir)))
goto err;
if (!(cat = malloc(sizeof(*cat))))
goto err;
memset(cat, 0, sizeof(*cat));
snprintf(cat->lang, sizeof(cat->lang), "%s", lang);
snprintf(pattern, sizeof(pattern), "*.%s.lmo", lang);
while ((de = readdir(dh)) != NULL)
{
if (!fnmatch(pattern, de->d_name, 0))
{
snprintf(path, sizeof(path), "%s/%s", dir, de->d_name);
ar = lmo_open(path);
if (ar)
{
ar->next = cat->archives;
cat->archives = ar;
}
}
}
closedir(dh);
cat->next = _lmo_catalogs;
_lmo_catalogs = cat;
if (!_lmo_active_catalog)
_lmo_active_catalog = cat;
return 0;
err:
if (dh) closedir(dh);
if (cat) free(cat);
return -1;
}
int lmo_change_catalog(const char *lang)
{
lmo_catalog_t *cat;
for (cat = _lmo_catalogs; cat; cat = cat->next)
{
if (!strncmp(cat->lang, lang, sizeof(cat->lang)))
{
_lmo_active_catalog = cat;
return 0;
}
}
return -1;
}
static lmo_entry_t * lmo_find_entry(lmo_archive_t *ar, uint32_t hash)
{
unsigned int m, l, r;
uint32_t k;
l = 0;
r = ar->length - 1;
while (1)
{
m = l + ((r - l) / 2);
if (r < l)
break;
k = ntohl(ar->index[m].key_id);
if (k == hash)
return &ar->index[m];
if (k > hash)
{
if (!m)
break;
r = m - 1;
}
else
{
l = m + 1;
}
}
return NULL;
}
int lmo_translate(const char *key, int keylen, char **out, int *outlen)
{
uint32_t hash;
lmo_entry_t *e;
lmo_archive_t *ar;
if (!key || !_lmo_active_catalog)
return -2;
hash = lmo_canon_hash(key, keylen);
for (ar = _lmo_active_catalog->archives; ar; ar = ar->next)
{
if ((e = lmo_find_entry(ar, hash)) != NULL)
{
*out = ar->mmap + ntohl(e->offset);
*outlen = ntohl(e->length);
return 0;
}
}
return -1;
}
void lmo_close_catalog(const char *lang)
{
lmo_archive_t *ar, *next;
lmo_catalog_t *cat, *prev;
for (prev = NULL, cat = _lmo_catalogs; cat; prev = cat, cat = cat->next)
{
if (!strncmp(cat->lang, lang, sizeof(cat->lang)))
{
if (prev)
prev->next = cat->next;
else
_lmo_catalogs = cat->next;
for (ar = cat->archives; ar; ar = next)
{
next = ar->next;
lmo_close(ar);
}
free(cat);
break;
}
}
}
@@ -0,0 +1,92 @@
/*
* lmo - Lua Machine Objects - General header
*
* Copyright (C) 2009-2012 Jo-Philipp Wich <xm@subsignal.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _TEMPLATE_LMO_H_
#define _TEMPLATE_LMO_H_
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <fnmatch.h>
#include <dirent.h>
#include <ctype.h>
#include <limits.h>
#if (defined(__GNUC__) && defined(__i386__))
#define sfh_get16(d) (*((const uint16_t *) (d)))
#else
#define sfh_get16(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8)\
+(uint32_t)(((const uint8_t *)(d))[0]) )
#endif
struct lmo_entry {
uint32_t key_id;
uint32_t val_id;
uint32_t offset;
uint32_t length;
} __attribute__((packed));
typedef struct lmo_entry lmo_entry_t;
struct lmo_archive {
int fd;
int length;
uint32_t size;
lmo_entry_t *index;
char *mmap;
char *end;
struct lmo_archive *next;
};
typedef struct lmo_archive lmo_archive_t;
struct lmo_catalog {
char lang[6];
struct lmo_archive *archives;
struct lmo_catalog *next;
};
typedef struct lmo_catalog lmo_catalog_t;
uint32_t sfh_hash(const char *data, int len);
uint32_t lmo_canon_hash(const char *data, int len);
lmo_archive_t * lmo_open(const char *file);
void lmo_close(lmo_archive_t *ar);
extern lmo_catalog_t *_lmo_catalogs;
extern lmo_catalog_t *_lmo_active_catalog;
int lmo_load_catalog(const char *lang, const char *dir);
int lmo_change_catalog(const char *lang);
int lmo_translate(const char *key, int keylen, char **out, int *outlen);
void lmo_close_catalog(const char *lang);
#endif