Update On Wed Dec 10 19:41:24 CET 2025

This commit is contained in:
github-action[bot]
2025-12-10 19:41:25 +01:00
parent 970b3e529a
commit 616e2d9bd9
93 changed files with 1117 additions and 779 deletions

View File

@@ -7,7 +7,7 @@ jobs:
stale:
runs-on: ubuntu-22.04
steps:
- uses: actions/stale@v7.0.0
- uses: actions/stale@v10
with:
stale-issue-message: "Stale Issue"
stale-pr-message: "Stale PR"

View File

@@ -206,9 +206,9 @@ function gen_outbound(flag, node, tag, proxy_table)
local first = node.tcp_guise_http_path[1]
return (first == "" or not first) and "/" or first
end)() or "/",
headers = {
["User-Agent"] = node.tcp_guise_http_user_agent or nil
},
headers = node.tcp_guise_http_user_agent and {
["User-Agent"] = node.tcp_guise_http_user_agent
} or nil,
idle_timeout = (node.http_h2_health_check == "1") and node.http_h2_read_idle_timeout or nil,
ping_timeout = (node.http_h2_health_check == "1") and node.http_h2_health_check_timeout or nil,
}
@@ -220,9 +220,9 @@ function gen_outbound(flag, node, tag, proxy_table)
type = "http",
host = node.http_host or {},
path = node.http_path or "/",
headers = {
["User-Agent"] = node.http_user_agent or nil
},
headers = node.http_user_agent and {
["User-Agent"] = node.http_user_agent
} or nil,
idle_timeout = (node.http_h2_health_check == "1") and node.http_h2_read_idle_timeout or nil,
ping_timeout = (node.http_h2_health_check == "1") and node.http_h2_health_check_timeout or nil,
}
@@ -233,10 +233,10 @@ function gen_outbound(flag, node, tag, proxy_table)
v2ray_transport = {
type = "ws",
path = node.ws_path or "/",
headers = {
Host = node.ws_host or nil,
["User-Agent"] = node.ws_user_agent or nil
},
headers = (node.ws_host or node.ws_user_agent) and {
Host = node.ws_host,
["User-Agent"] = node.ws_user_agent
} or nil,
max_early_data = tonumber(node.ws_maxEarlyData) or nil,
early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil --要与 Xray-core 兼容,请将其设置为 Sec-WebSocket-Protocol。它需要与服务器保持一致。
}
@@ -247,9 +247,9 @@ function gen_outbound(flag, node, tag, proxy_table)
type = "httpupgrade",
host = node.httpupgrade_host,
path = node.httpupgrade_path or "/",
headers = {
["User-Agent"] = node.httpupgrade_user_agent or nil
}
headers = node.httpupgrade_user_agent and {
["User-Agent"] = node.httpupgrade_user_agent
} or nil
}
end

View File

@@ -178,10 +178,10 @@ function gen_outbound(flag, node, tag, proxy_table)
end
return r
end)() or {"/"},
headers = {
Host = node.tcp_guise_http_host or {},
headers = (node.tcp_guise_http_host or node.tcp_guise_http_user_agent) and {
Host = node.tcp_guise_http_host,
["User-Agent"] = node.tcp_guise_http_user_agent and {node.tcp_guise_http_user_agent} or nil
}
} or nil
} or nil
}
} or nil,
@@ -201,10 +201,10 @@ function gen_outbound(flag, node, tag, proxy_table)
} or nil,
wsSettings = (node.transport == "ws") and {
path = node.ws_path or "/",
headers = {
Host = node.ws_host or nil,
["User-Agent"] = node.ws_user_agent or nil
},
headers = (node.ws_host or node.ws_user_agent) and {
Host = node.ws_host,
["User-Agent"] = node.ws_user_agent
} or nil,
maxEarlyData = tonumber(node.ws_maxEarlyData) or nil,
earlyDataHeaderName = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil,
heartbeatPeriod = tonumber(node.ws_heartbeatPeriod) or nil
@@ -220,9 +220,9 @@ function gen_outbound(flag, node, tag, proxy_table)
httpupgradeSettings = (node.transport == "httpupgrade") and {
path = node.httpupgrade_path or "/",
host = node.httpupgrade_host,
headers = {
["User-Agent"] = node.httpupgrade_user_agent or nil
}
headers = node.httpupgrade_user_agent and {
["User-Agent"] = node.httpupgrade_user_agent
} or nil
} or nil,
xhttpSettings = (node.transport == "xhttp") and {
mode = node.xhttp_mode or "auto",

View File

@@ -1,5 +1,10 @@
<%+cbi/valueheader%>
<%
-- Template Developers:
-- - lwb1978
-- Copyright: copyright(c)20252027
-- Description: Passwall(2) UI template
local cbid = "cbid." .. self.config .. "." .. section .. "." .. self.option
-- 读取 MultiValue
@@ -41,9 +46,62 @@ for _, item in ipairs(values) do
end
%>
<div id="<%=cbid%>" class="cbi-input-select" style="display:inline-block;">
<!-- 搜索 -->
<input type="text" id="<%=cbid%>.search" class="node_search_input cbi-input-text" placeholder="<%:Search nodes...%>"
style="width:100%;padding:6px;margin-bottom:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;max-height:36px;" />
<!-- 主容器 -->
<div style="max-height:300px;overflow:auto;margin-bottom:8px;white-space:nowrap;">
<ul class="cbi-multi" id="<%=cbid%>.node_list" style="padding:0 !important;margin:0 !important;width:100%;box-sizing:border-box;">
<% for _, gname in ipairs(group_order) do %>
<% local items = groups[gname] %>
<li class="group-block" data-group="<%=gname%>" style="list-style:none;padding:0;margin:0 0 8px 0;">
<!-- 组标题 -->
<div class="group-title" style="cursor:pointer;padding:6px;background:#f0f0f0;border-radius:4px;margin-bottom:4px;display:flex;align-items:center;white-space:nowrap;">
<span id="arrow-<%=self.option%>-<%=gname%>" class="mv-arrow-down-small"></span>
<b style="margin-left:8px;"><%=gname%></b>
<span id="group-count-<%=self.option%>-<%=gname%>" style="margin-left:8px;color:#007bff;">
(0/<%=#items%>)
</span>
</div>
<!-- 组内容 -->
<ul id="group-<%=self.option%>-<%=gname%>" style="margin:0 0 8px 16px;padding:0;list-style:none;">
<% for _, item in ipairs(items) do %>
<li data-node-name="<%=pcdata(item.label):lower()%>" title="<%=pcdata(item.label)%>" style="list-style:none;padding:0;margin:0;white-space:nowrap;text-align:left;">
<div style="display:inline-flex;align-items:center;vertical-align:middle;">
<input type="checkbox" class="cbi-input-checkbox" style="vertical-align:middle;margin:0;margin-right:6px;"
<%= attr("id", cbid .. "." .. item.key) ..
attr("name", cbid) ..
attr("value", item.key) ..
ifattr(selected[item.key], "checked", "checked")
%> />
<label for="<%=cbid .. "." .. item.key%>" style="vertical-align:middle;margin:0;padding:0;"><%=pcdata(item.label)%></label>
</div>
</li>
<% end %>
</ul>
</li>
<% end %>
</ul>
</div>
<!-- 控制栏 -->
<div style="margin-top:4px;display:flex;gap:4px;align-items:center;">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="mv_selectAll('<%=cbid%>','<%=self.option%>',true)" value="<%:Select all%>">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="mv_selectAll('<%=cbid%>','<%=self.option%>',false)" value="<%:DeSelect all%>">
<span id="count-<%=self.option%>" style="color:#666;"></span>
</div>
</div>
<%+cbi/valuefooter%>
<%
-- 公共部分(只加载一次)
if not _G.__NODES_MULTIVALUE_CSS_JS__ then
_G.__NODES_MULTIVALUE_CSS_JS__ = true
%>
<style>
/* 组标题的右箭头(折叠) */
.lv-arrow-right {
/* 组标题的右箭头 */
.mv-arrow-right {
width: 0;
height: 0;
border-top: 4px solid transparent;
@@ -52,9 +110,8 @@ end
display: inline-block;
vertical-align: middle;
}
/* 组标题的下箭头(展开) */
.lv-arrow-down-small {
/* 组标题的下箭头 */
.mv-arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
@@ -65,120 +122,67 @@ end
}
</style>
<div id="<%=cbid%>" class="cbi-input-select" style="display: inline-block;">
<!-- 搜索 -->
<input type="text"
id="<%=cbid%>.search"
class="node-search-input cbi-input-text"
placeholder="<%:Search nodes...%>"
oninput="filterGroups_<%=self.option%>(this.value)"
style="width:100%;padding:6px;margin-bottom:8px;border:1px solid #ccc;border-radius:4px;box-sizing:border-box;max-height:36px;" />
<!-- 主容器 -->
<div style="max-height:300px; overflow:auto; margin-bottom:8px; white-space:nowrap;">
<ul class="cbi-multi" id="<%=cbid%>.node_list" style="padding:0 !important;margin:0 !important;width:100%;box-sizing:border-box;">
<% for _, gname in ipairs(group_order) do %>
<% local items = groups[gname] %>
<li class="group-block" data-group="<%=gname%>" style="list-style:none; padding:0; margin:0 0 8px 0;">
<!-- 组标题 -->
<div class="group-title"
onclick="toggleGroup_<%=self.option%>('<%=gname%>')"
style="cursor:pointer;padding:6px;background:#f0f0f0;border-radius:4px;margin-bottom:4px;display:flex;align-items:center;white-space:nowrap;">
<span id="arrow-<%=self.option%>-<%=gname%>" class="lv-arrow-down-small"></span>
<b style="margin-left:8px;"><%=gname%></b>
<span id="group-count-<%=self.option%>-<%=gname%>" style="margin-left:8px;color:#007bff;">
(0/<%=#items%>)
</span>
</div>
<!-- 组内容(可折叠)-->
<ul id="group-<%=self.option%>-<%=gname%>" style="margin:0 0 8px 16px; padding:0; list-style:none;">
<% for _, item in ipairs(items) do %>
<li data-node-name="<%=pcdata(item.label):lower()%>" style="list-style:none;padding:0;margin:0;white-space:nowrap;" title="<%=pcdata(item.label)%>">
<input
type="checkbox"
class="cbi-input-checkbox"
style="vertical-align: middle; margin-right:6px;"
<%= attr("id", cbid .. "." .. item.key) ..
attr("name", cbid) ..
attr("value", item.key) ..
ifattr(selected[item.key], "checked", "checked")
%> />
<label for="<%=cbid .. "." .. item.key%>"><%=pcdata(item.label)%></label>
</li>
<% end %>
</ul>
</li>
<% end %>
</ul>
</div>
<!-- 控制栏 -->
<div style="margin-top:4px;display:flex;gap:4px;align-items:center;">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="selectAll_<%=self.option%>(true)" value="<%:Select all%>">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="selectAll_<%=self.option%>(false)" value="<%:DeSelect all%>">
<span id="count-<%=self.option%>" style="color:#666;"></span>
</div>
</div>
<%+cbi/valuefooter%>
<script type="text/javascript">
//<![CDATA[
(function(){
const cbid = "<%=cbid%>";
const opt = "<%=self.option%>";
const listId = cbid + ".node_list";
// 折叠组
window["toggleGroup_" + opt] = function(g){
const ul = document.getElementById("group-" + opt + "-" + g);
const arrow = document.getElementById("arrow-" + opt + "-" + g);
function mv_toggleGroup(opt, nodeList, searchInput, g) {
const ul = nodeList.querySelector("#group-" + opt + "-" + g);
const arrow = nodeList.querySelector("#arrow-" + opt + "-" + g);
if (!ul) return;
// 判断是否在搜索状态
const keyword = document.getElementById(cbid + ".search").value.trim().toLowerCase();
const keyword = searchInput.value.trim().toLowerCase();
const isSearching = keyword.length > 0;
// 搜索状态下,仅切换当前组,不处理其他组
if (isSearching) {
if (ul.style.display === "none") {
ul.style.display = "";
if (arrow) arrow.className = "lv-arrow-down-small";
} else {
ul.style.display = "none";
if (arrow) arrow.className = "lv-arrow-right";
}
if (isSearching){
ul.style.display = ul.style.display === "none" ? "block" : "none";
if (arrow) arrow.className = ul.style.display === "none" ? "mv-arrow-right" : "mv-arrow-down-small";
return;
}
// 非搜索模式:先折叠其他组
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
groups.forEach(group=>{
nodeList.querySelectorAll(".group-block").forEach(group=>{
const gname = group.getAttribute("data-group");
const gul = document.getElementById("group-" + opt + "-" + gname);
const garrow = document.getElementById("arrow-" + opt + "-" + gname);
if (gname !== g) {
if (gul) gul.style.display = "none";
if (garrow) arrow.className = "lv-arrow-right";
if (garrow) garrow.className = "mv-arrow-right";
}
});
document.getElementById(listId).parentNode.scrollTop = 0;
nodeList.parentNode.scrollTop = 0;
// 切换当前组
if (ul.style.display === "none") {
ul.style.display = "";
if (arrow) arrow.className = "lv-arrow-down-small";
} else {
ul.style.display = "none";
if (arrow) arrow.className = "lv-arrow-right";
}
ul.style.display = ul.style.display === "none" ? "block" : "none";
if (arrow) arrow.className = ul.style.display === "none" ? "mv-arrow-right" : "mv-arrow-down-small";
};
// 计数
function mv_updateCount(opt, nodeList) {
// 当前实例下的所有 checkbox
const cbs = nodeList.querySelectorAll("input[type=checkbox]");
let checked = 0;
cbs.forEach(cb => { if(cb.checked) checked++; });
// 更新总计
const totalSpan = document.getElementById("count-" + opt);
if (totalSpan) {
totalSpan.innerHTML = "<%:Selected:%> <span style='color:red;'>" + checked + " / " + cbs.length + "</span>";
}
// 更新每个组计数
nodeList.querySelectorAll(".group-block").forEach(group => {
const gname = group.getAttribute("data-group");
const groupCbs = group.querySelectorAll("li[data-node-name] input[type=checkbox]");
let groupChecked = 0;
groupCbs.forEach(cb => { if(cb.checked) groupChecked++; });
const span = document.getElementById("group-count-" + opt + "-" + gname);
if(span) span.textContent = "(" + groupChecked + "/" + groupCbs.length + ")";
});
}
// 搜索
window["filterGroups_" + opt] = function(keyword){
keyword = keyword.toLowerCase().trim();
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
groups.forEach(group=>{
function mv_filterGroups(keyword, opt, nodeList) {
keyword = (keyword || "").toLowerCase().trim();
nodeList.querySelectorAll(".group-block").forEach(group => {
const items = group.querySelectorAll("li[data-node-name]");
let matchCount = 0;
items.forEach(li=>{
items.forEach(li => {
const name = li.getAttribute("data-node-name");
if (!keyword || name.indexOf(keyword) !== -1) {
li.style.display = "";
@@ -187,7 +191,7 @@ end
li.style.display = "none";
}
});
// 搜索时自动展开所有
// 搜索时自动展开组
const gname = group.getAttribute("data-group");
const ul = document.getElementById("group-" + opt + "-" + gname);
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
@@ -196,88 +200,71 @@ end
group.style.display = "none";
} else {
group.style.display = "";
if (keyword) {
if (keyword && ul && arrow) {
ul.style.display = "";
arrow.className = "lv-arrow-down-small";
arrow.className = "mv-arrow-down-small";
}
}
});
updateCount();
mv_updateCount(opt, nodeList);
// 清空搜索后恢复全部折叠
if (!keyword) {
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
groups.forEach(group=>{
const gname = group.getAttribute("data-group");
const ul = document.getElementById("group-" + opt + "-" + gname);
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
if (ul) ul.style.display = "none";
if (arrow) arrow.className = "lv-arrow-right";
});
mv_collapseAllGroups(opt, nodeList);
}
};
}
// 全选 / 全不选
window["selectAll_" + opt] = function(flag){
const cbs = document.querySelectorAll("[id='" + listId + "'] input[type=checkbox]");
function mv_selectAll(cbid, opt, flag) {
const nodeList = document.getElementById(cbid + ".node_list");
const cbs = nodeList.querySelectorAll("input[type=checkbox]");
cbs.forEach(cb=>{
if (cb.offsetParent !== null) cb.checked = flag;
});
updateCount();
mv_updateCount(opt, nodeList);
};
// 计数
function updateCount(){
const cbs = document.querySelectorAll("[id='" + listId + "'] input[type=checkbox]");
let checked = 0;
cbs.forEach(cb=>{ if (cb.checked) checked++; });
// 更新总计
document.getElementById("count-" + opt).innerHTML =
"<%:Selected:%> <span style='color:red;'>" + checked + " / " + cbs.length + "</span>";
// 更新每个组
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
groups.forEach(group=>{
const gname = group.getAttribute("data-group");
const groupCbs = group.querySelectorAll("li[data-node-name] input[type=checkbox]");
let groupChecked = 0;
groupCbs.forEach(cb=>{ if(cb.checked) groupChecked++; });
const span = document.getElementById("group-count-" + opt + "-" + gname);
if(span) span.textContent = "(" + groupChecked + "/" + groupCbs.length + ")";
});
}
document.getElementById(listId)?.addEventListener("change", updateCount);
// 初始化折叠所有组和计数
const initObserver = new MutationObserver(() => {
const list = document.getElementById(listId);
if (!list) return;
if (list.offsetParent === null) return;
if (list.dataset.initDone === "1") return;
list.dataset.initDone = "1";
const groups = document.querySelectorAll("[id='" + listId + "'] .group-block");
groups.forEach(group => {
// 折叠所有组
function mv_collapseAllGroups(opt, nodeList) {
nodeList.querySelectorAll(".group-block").forEach(group => {
const gname = group.getAttribute("data-group");
const ul = document.getElementById("group-" + opt + "-" + gname);
const arrow = document.getElementById("arrow-" + opt + "-" + gname);
if (ul) ul.style.display = "none";
if (arrow) arrow.className = "lv-arrow-right";
if (arrow) arrow.className = "mv-arrow-right";
});
}
//]]>
</script>
<% end %>
updateCount();
<script type="text/javascript">
//<![CDATA[
(function(){
const cbid = "<%=cbid%>";
const opt = "<%=self.option%>";
const searchInput = document.getElementById(cbid + ".search");
const nodeList = document.getElementById(cbid + ".node_list");
nodeList.querySelectorAll(".group-title").forEach(title => {
title.addEventListener("click", function() {
const g = this.closest(".group-block")?.getAttribute("data-group");
if (g) mv_toggleGroup(opt, nodeList, searchInput, g);
});
});
initObserver.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ["style", "class"]
searchInput.addEventListener("input", function() {
mv_filterGroups(this.value, opt, nodeList);
})
// checkbox 改变时更新计数
nodeList.addEventListener("change", () => {
mv_updateCount(opt, nodeList);
});
// 初始化折叠所有组和计数
mv_collapseAllGroups(opt, nodeList)
mv_updateCount(opt, nodeList);
})();
//]]>
</script>