update 2026-07-16 18:02:58

This commit is contained in:
action 2026-07-16 18:02:58 +08:00
parent ba1b0b5cf2
commit adc73c928c
37 changed files with 234 additions and 107 deletions

BIN
doc/lucky1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
doc/lucky2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

BIN
doc/lucky3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 547 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 650 KiB

View File

@ -0,0 +1,38 @@
function is_json(str) {
try {
const result = JSON.parse(str);
return typeof result === 'object' && result !== null;
} catch (e) {
return false;
}
}
function is_timehhmm(str) {
const match = String(str).match(/^(\d?\d):(\d\d)$/);
if (match) {
const hour = parseInt(match[1], 10);
const minute = parseInt(match[2], 10);
if (hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59) {
return true;
}
}
return false;
}
if (typeof cbi_validators !== 'undefined' && cbi_validators !== null) {
cbi_validators['json'] = function() {
return is_json(this);
}
cbi_validators['timehhmm'] = function() {
return is_timehhmm(this);
}
} else {
L.require('validation').then(function(validation) {
validation.types['json'] = function() {
return this.assert(is_json(this.value), _('Must be JSON text!'));
}
validation.types['timehhmm'] = function() {
return this.assert(is_timehhmm(this.value), _('valid time (hh:mm)'));
}
});
}

View File

@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local appname = "passwall"
local sys = api.sys
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -101,4 +103,4 @@ o:value("1:65535", translate("All"))
o:value("53", "53")
]]--
return m
return api.return_map(m)

View File

@ -1,6 +1,8 @@
local api = require "luci.passwall.api"
local appname = "passwall"
api.set_default_cbi()
m = Map(appname)
m.redirect = api.url("acl")
api.set_apply_on_parse(m)
@ -601,4 +603,4 @@ for k, v in pairs(nodes_table) do
end
end
return m
return api.return_map(m)

View File

@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local com = require "luci.passwall.com"
local appname = "passwall"
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -34,4 +36,4 @@ o.cfgvalue = function(t, n)
return string.format('<font color="red">%s</font>', translate("if you want to run from memory, change the path, /tmp beginning then save the application and update it manually."))
end
return m
return api.return_map(m)

View File

@ -8,6 +8,8 @@ local has_gfwlist = fs.access("/usr/share/passwall/rules/gfwlist")
local has_chnlist = fs.access("/usr/share/passwall/rules/chnlist")
local has_chnroute = fs.access("/usr/share/passwall/rules/chnroute")
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -839,4 +841,4 @@ footer.global_cfgid = global_cfgid
footer.shunt_list = api.jsonc.stringify(shunt_list)
m:append(footer)
return m
return api.return_map(m)

View File

@ -15,6 +15,8 @@ for k, e in ipairs(api.get_valid_nodes()) do
end
end
api.set_default_cbi()
m = Map(appname, "HAPROXY " .. translate("Load Balancing"))
api.set_apply_on_parse(m)
@ -179,4 +181,4 @@ o.rmempty = false
m:append(Template(appname .. "/haproxy/js"))
return m
return api.return_map(m)

View File

@ -1,6 +1,8 @@
api = require "luci.passwall.api"
appname = "passwall"
api.set_default_cbi()
m = Map(appname, translate("Node Config"))
m.redirect = api.url("node_list")
api.set_apply_on_parse(m)
@ -120,7 +122,6 @@ local footer = Template(appname .. "/node_config/footer")
footer.api = api
footer.config = m.config
footer.section = arg[1]
m:append(footer)
return m
return api.return_map(m)

View File

@ -3,6 +3,8 @@ local appname = "passwall"
local sys = api.sys
local datatypes = api.datatypes
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -33,4 +35,4 @@ s:append(Template(appname .. "/node_list/link_add_node"))
m:append(Template(appname .. "/node_list/node_list"))
return m
return api.return_map(m)

View File

@ -38,6 +38,8 @@ if has_hysteria2 then
table.insert(hysteria2_type, s)
end
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -225,4 +227,4 @@ end
m:append(Template(appname .. "/node_subscribe/js"))
return m
return api.return_map(m)

View File

@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local uci = api.uci
local appname = "passwall"
api.set_default_cbi()
m = Map(appname)
m.redirect = api.url("node_subscribe")
api.set_apply_on_parse(m)
@ -244,13 +246,7 @@ o:value(0, translate("Every Sunday"))
o = s:option(Value, "update_time_mode", translate("Update Time"))
for t = 0, 23 do o:value(t .. ":00") end
o.default = "0:00"
o.validate = function(self, value)
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o.datatype = "timehhmm"
o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1")
o:depends("update_week_mode", "2")
@ -321,4 +317,4 @@ for k, v in pairs(nodes_table) do
end
end
return m
return api.return_map(m)

View File

@ -8,6 +8,8 @@ local port_validate = function(self, value, t)
return value:gsub("-", ":")
end
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -43,13 +45,7 @@ for index, value in ipairs({"stop", "start", "restart"}) do
o = s:option(Value, value .. "_time_mode", translate(value .. " Time"))
for t = 0, 23 do o:value(t .. ":00") end
o.default = "0:00"
o.validate = function(self, value)
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o.datatype = "timehhmm"
o:depends(value .. "_week_mode", "0")
o:depends(value .. "_week_mode", "1")
o:depends(value .. "_week_mode", "2")
@ -271,4 +267,4 @@ if has_singbox then
o.default = 0
end
return m
return api.return_map(m)

View File

@ -3,6 +3,8 @@ local appname = "passwall"
local has_xray = api.finded_com("xray")
local has_singbox = api.finded_com("sing-box")
api.set_default_cbi()
m = Map(appname)
api.set_apply_on_parse(m)
@ -112,13 +114,7 @@ o:value(0, translate("Every Sunday"))
o = s:option(Value, "update_time_mode", translate("Update Time"))
for t = 0, 23 do o:value(t .. ":00") end
o.default = "0:00"
o.validate = function(self, value)
local b = api.is_timehhmm(value)
if b then
return value
end
return nil
end
o.datatype = "timehhmm"
o:depends("update_week_mode", "0")
o:depends("update_week_mode", "1")
o:depends("update_week_mode", "2")
@ -169,4 +165,4 @@ if has_xray or has_singbox then
o = s:option(DummyValue, "remarks", translate("Remarks"))
end
return m
return api.return_map(m)

View File

@ -1,5 +1,5 @@
local api = require "luci.passwall.api"
local appname = "passwall"
local appname = api.appname
local fs = api.fs
local sys = api.sys
local uci = api.uci

View File

@ -2,6 +2,8 @@ local api = require "luci.passwall.api"
local appname = "passwall"
local datatypes = api.datatypes
api.set_default_cbi()
m = Map(appname, "Sing-Box/Xray " .. translate("Shunt Rule"))
m.redirect = api.url("rule")
api.set_apply_on_parse(m)
@ -245,4 +247,4 @@ ip_list.description = "<br /><ul>"
o = s:option(Flag, "invert", "Invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
return m
return api.return_map(m)

View File

@ -1,6 +1,8 @@
local api = require "luci.passwall.api"
local appname = "passwall"
api.set_default_cbi()
m = Map(appname)
m.redirect = api.url()
api.set_apply_on_parse(m)
@ -166,4 +168,4 @@ o = s:option(DummyValue, "btn")
o.template = appname .. "/socks_auto_switch/btn"
o:depends("backup_node_add_mode", "manual")
return m
return api.return_map(m)

View File

@ -667,6 +667,14 @@ o = s:option(TextValue, _n("xhttp_extra"), " ", translate("An XHttpObject in J
o:depends({ [_n("use_xhttp_extra")] = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "xhttp_extra")
if raw then
@ -689,14 +697,6 @@ o.custom_write = function(self, section, value)
m:del(section, "download_address")
end
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "XHTTP Extra " .. translate("Must be JSON text!")
end
end
o.custom_remove = function(self, section, value)
m:del(section, "xhttp_extra")
m:del(section, "download_address")
@ -752,6 +752,14 @@ o.rows = 10
o.wrap = "off"
o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "<br>" ..
translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.")
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
local v = o_validate(self, value)
if v then return v end
return nil, "FinalMask " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
@ -761,14 +769,6 @@ end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "FinalMask " .. translate("Must be JSON text!")
end
end
--[[Fast Open]]
o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"), translate("Need node support required"))

View File

@ -1,4 +1,7 @@
local api = require "luci.passwall.api"
local appname = api.appname
api.set_default_cbi()
m = Map("passwall_server", translate("Server-Side"))
api.set_apply_on_parse(m)
@ -85,7 +88,8 @@ e = t:option(Flag, "log", translate("Log"))
e.default = "1"
e.rmempty = false
m:append(Template("passwall/server/log"))
m:append(Template(appname .. "/server/log"))
m:append(Template("passwall/server/users_list_status"))
return m
m:append(Template(appname .. "/server/users_list_status"))
return api.return_map(m)

View File

@ -131,12 +131,12 @@ o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -406,6 +406,7 @@ o = s:option(TextValue, _n("finalmask"), "FinalMask JSON")
o:depends({ [_n("use_finalmask")] = true })
o.rows = 10
o.wrap = "off"
o.datatype = "json"
o.custom_cfgvalue = function(self, section, value)
local raw = m:get(section, "finalmask")
if raw then
@ -415,14 +416,6 @@ end
o.custom_write = function(self, section, value)
m:set(section, "finalmask", api.base64Encode(value) or "")
end
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
if api.jsonc.parse(value) then
return value
else
return nil, "FinalMask " .. translate("Must be JSON text!")
end
end
--[[acceptProxyProtocol]]
o = s:option(Flag, _n("acceptProxyProtocol"), translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
@ -517,16 +510,16 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -489,16 +489,16 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -49,16 +49,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -76,16 +76,16 @@ o = s:option(Flag, _n("tcp_fast_open"), "TCP " .. translate("Fast Open"))
o.default = "0"
o:depends({ [_n("custom")] = false })
o = s:option(TextValue, _n("custom_config"), translate("Custom Config"))
o = s:option(TextValue, _n("custom_config"), translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ [_n("custom")] = true })
o.validate = function(self, value, t)
if value and api.jsonc.parse(value) then
return value
else
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")

View File

@ -2,6 +2,8 @@ api = require "luci.passwall.api"
appname = api.appname
fs = api.fs
api.set_default_cbi()
m = Map("passwall_server", translate("Server Config"))
m.redirect = api.url("server")
api.set_apply_on_parse(m)
@ -56,4 +58,4 @@ footer.config = m.config
footer.section = arg[1]
m:append(footer)
return m
return api.return_map(m)

View File

@ -348,8 +348,16 @@ function strToTable(str)
return loadstring("return " .. str)()
end
function is_timehhmm(timeStr)
local hour, minute = string.match(timeStr, "^(%d?%d):(%d%d)$")
function is_json(str)
if str and jsonc.parse(str) then
return true
end
return false
end
datatypes.json = is_json
function is_timehhmm(str)
local hour, minute = string.match(str, "^(%d?%d):(%d%d)$")
if hour and minute then
hour = tonumber(hour)
minute = tonumber(minute)
@ -359,6 +367,7 @@ function is_timehhmm(timeStr)
end
return false
end
datatypes.timehhmm = is_timehhmm
function is_normal_node(e)
if e and e.type and e.protocol and (e.protocol == "_balancing" or e.protocol == "_shunt" or e.protocol == "_iface" or e.protocol == "_urltest") then
@ -1327,6 +1336,40 @@ function to_check_self()
}
end
function set_default_cbi()
local cbi = require "luci.cbi"
if true then
--TextValue
local TextValue = cbi.TextValue
local original_init = TextValue.__init__
function TextValue.__init__(self, ...)
original_init(self, ...)
self.template = appname .. "/cbi/tvalue"
end
end
end
function return_map(map)
local cbi = require "luci.cbi"
local api = require "luci.passwall.api"
if true then
-- header
local header = cbi.Template(appname .. "/cbi/header")
header.api = api
header.config = map.config
table.insert(map.children, 1, header)
end
if true then
-- footer
local footer = cbi.Template(appname .. "/cbi/footer")
footer.api = api
footer.config = map.config
map:append(footer)
end
return map
end
function luci_types(id, m, s, type_name, option_prefix)
local fv_type
local field_type = s.fields["type"]

View File

@ -0,0 +1 @@
<!-- map footer -->

View File

@ -0,0 +1,2 @@
<!-- map header -->
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>

View File

@ -0,0 +1,13 @@
<%+cbi/valueheader%>
<textarea class="cbi-input-textarea" <% if not self.size then %> style="width: 100%"<% else %> cols="<%=self.size%>"<% end %>
data-update="change"<%=
attr("name", cbid) ..
attr("id", cbid) ..
ifattr(self.rows, "rows") ..
ifattr(self.wrap, "wrap") ..
ifattr(self.readonly, "readonly") ..
ifattr(self.datatype, "data-type", self.datatype)
%>>
<%-=pcdata(self:cfgvalue(section) or self.default)-%>
</textarea>
<%+cbi/valuefooter%>

View File

@ -635,11 +635,15 @@ table td, .table .td {
if (cacheData && cacheData.savetime && (now - cacheData.timestamp) < cacheData.savetime) {
const a = ping_value[i].firstElementChild;
let color = "red";
if (cacheData.value < 100)
color = "green";
else if (cacheData.value < 200)
color = "#fb9a05";
a.innerText = cacheData.value + " ms";
if (cacheData.value >= 0) {
a.innerText = cacheData.value + " ms";
if (cacheData.value < 100)
color = "green";
else if (cacheData.value < 200)
color = "#fb9a05";
} else {
a.innerText = "<%:Timeout%>";
}
a.style.color = color;
a.onclick = function () {
ping_node(cbi_id, this, auto_detection_time);
@ -679,15 +683,23 @@ table td, .table .td {
const cache_data = {
dom_id: strs[i],
timestamp: Date.now(),
savetime: 60 * 1000,
savetime: 150 * 1000,
value: ping
};
text = result.ping + " ms";
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
text = result.ping + " ms";
if (ping < 100)
color = "green";
else if (ping < 200)
color = "#fb9a05";
} else {
const cache_data = {
dom_id: strs[i],
timestamp: Date.now(),
savetime: 60 * 1000,
value: -1
};
localStorage.setItem(auto_detection_time + ":" + dom.address + ":" + dom.port, JSON.stringify(cache_data));
}
a.innerText = text;
a.style.color = color;
@ -1101,6 +1113,15 @@ table td, .table .td {
onChange("auto_detection_time", auto_detection_time);
});
});
const links = document.querySelectorAll('a');
links.forEach(link => {
link.addEventListener('click', (e) => {
if (link.getAttribute('target') !== '_blank') {
ajaxAbortAll();
}
});
});
});
//]]>
</script>

View File

@ -2144,3 +2144,6 @@ msgstr "Gecko 包大小(最小)"
msgid "Gecko Packet Size (max)"
msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效时间hh:mm"