🤞 Sync 2026-09-27 22:16:29

This commit is contained in:
github-actions[bot] committed 2026-09-27 22:16:29 +08:00
1 parent 8ede4a8320
commit 618f51e9dc
459 files changed
+44817 -2191

No files matched your search

+9
View File
@@ -2,6 +2,15 @@
All notable changes to this project will be documented in this file.
## [2.2.0-r1] - 节点页批量选择与操作
- 「节点」页关键词搜索框缩短为原长度的 3/5(`size` 默认 20 → 12)
- 节点表格最左新增复选框列:表头复选框全选 / 取消全选,行复选框单独勾选
- 「筛选」后新增「刷新」按钮(重载页面,筛选参数随 GET URL 自动保留)与「删除」按钮(勾选批量删除,confirm 确认,返回保留筛选参数)
- action_node_delete 的 idx 参数支持逗号分隔多值,倒序 table.remove 避免下标偏移;行内单删路径不变
- po/zh-cn 新增 Refresh / Delete selected nodes? / Please select nodes first 三条
- 版本号 2.1.3-r6 → 2.2.0-r1;README.md / README.en.md / docs/INSTALL.md 版本与功能描述同步
## [2.1.3-r6] - 节点分组与单节点编辑
- 表单导入节点行新增「分组」输入框(`data-k="group"`,parse_local 透传入模型)
+2 -2
View File
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-substore
# 版本约定:每次提交 PKG_RELEASE +1(2.1.2-r1 ~ r9);
# 达到 r10 时 PKG_VERSION 末位 +1(2.1.2 -> 2.1.3),PKG_RELEASE 重置为 1。
PKG_VERSION:=2.1.3
PKG_RELEASE:=1
PKG_VERSION:=2.2.0
PKG_RELEASE:=2
LUCI_DEPENDS:=+luci-lua-runtime +luci-compat
+17 -6
View File
@@ -21,6 +21,9 @@ group them, then re-emit them in a format your client can consume.
(optionally with keyword include / exclude and dedup rules) into one combination that
has its own name, token and subscription link; combinations are recomputed automatically
when a source updates
- **Local subscription**: no URL needed — paste node text (mixed YAML / URI / JSON) or
enter nodes one by one via a form whose fields adapt to the selected protocol
(vmess / vless / ss / ssr / trojan / hysteria2 / tuic / wireguard / socks)
**Input parsing**
- Subscription formats: URI lists, Base64, JSON, Clash YAML, sing-box JSON,
@@ -30,7 +33,12 @@ group them, then re-emit them in a format your client can consume.
`tuic` / `socks` (and more)
**Node processing**
- Browse nodes, filter by protocol, keyword search, sort
- Browse nodes, filter by group / protocol, keyword search, sort
- Node grouping: set a node's group inline in the Group column (saved via XHR without
reload), filter via the Group dropdown
- Per-node edit / delete (Actions column); header checkbox selects all, then the Delete
button batch-deletes the selection; the Refresh button reloads the list keeping the
current filters
- Per-subscription rules applied on every update:
- keyword include / exclude (comma-separated, multi-keyword)
- deduplication
@@ -59,18 +67,18 @@ group them, then re-emit them in a format your client can consume.
## Installation
> The version in the package name must match `PKG_VERSION` / `PKG_RELEASE` in the
> [Makefile](Makefile) (currently `2.1.3-r6`).
> [Makefile](Makefile) (currently `2.2.0-r1`).
opkg (OpenWrt / ImmortalWrt 24.10 and earlier):
```bash
opkg install luci-app-substore-2.1.3-r6.ipk
opkg install luci-app-substore-2.2.0-r1.ipk
```
apk (OpenWrt / ImmortalWrt 25.12+):
```bash
apk add --allow-untrusted luci-app-substore-2.1.3-r6.apk
apk add --allow-untrusted luci-app-substore-2.2.0-r1.apk
```
Then open LuCI: **Services → Subscriptions**.
@@ -78,9 +86,12 @@ Then open LuCI: **Services → Subscriptions**.
## Usage
1. **Add a subscription** — paste the subscription URL; optionally set up a
per-subscription cron schedule, rules, or a download proxy.
per-subscription cron schedule, rules, or a download proxy. No remote source?
Use **Add local subscription**: paste node text or enter nodes via the form.
2. **Update** — fetch, parse and filter the nodes.
3. **Browse nodes** — filter, sort, and probe latency.
3. **Browse nodes** — filter (group / protocol / keyword), sort, probe latency; tick
checkboxes and hit Delete for batch deletion, edit / delete / regroup in-row, and
Refresh reloads the list.
4. **Export** — pick one of the 13 output formats, or copy the subscription link
to feed a downstream client (Passwall / OpenClash / …).
+12 -5
View File
@@ -19,6 +19,8 @@
用于订阅源直连失败时
- **组合订阅**:勾选任意子集的现有订阅(可叠加关键词包含 / 排除、去重规则)合并成一个组合,
拥有独立名称、token 与订阅链接;源订阅更新后组合自动重算
- **本地订阅**:不填 URL,直接粘贴节点文本(YAML / URI / JSON 混合)导入,或用「表单导入」
按协议动态字段逐条录入节点(vmess / vless / ss / ssr / trojan / hysteria2 / tuic / wireguard / socks)
**输入解析**
- 订阅格式:URI 列表、Base64、JSON、Clash YAML、sing-box JSON、V2Ray / Xray JSON、
@@ -26,7 +28,10 @@
- 节点协议:`vmess` / `vless` / `trojan` / `shadowsocks` / `ssr` / `hysteria2` / `tuic` / `socks`(及更多)
**节点处理**
- 浏览节点,按协议筛选、关键词搜索、排序
- 浏览节点,按分组 / 协议筛选、关键词搜索、排序
- 节点分组:「分组」列单元格内直接修改单节点分组(XHR 无刷新保存),配合「分组:」下拉筛选
- 单节点编辑 / 删除(行尾「操作」列);表头复选框全选、行复选框勾选后点「删除」批量删除;
「刷新」按钮重载列表(保留当前筛选条件)
- 每次更新时生效的按订阅规则:
- 关键词包含 / 排除(逗号分隔,支持多关键词)
- 去重
@@ -54,18 +59,18 @@
## 安装
> 包名中的版本号必须与 [Makefile](Makefile) 的 `PKG_VERSION` / `PKG_RELEASE` 保持一致
> (当前 `2.1.3-r6`)。
> (当前 `2.2.0-r1`)。
opkg(OpenWrt / ImmortalWrt 24.10 及更早):
```bash
opkg install luci-app-substore-2.1.3-r6.ipk
opkg install luci-app-substore-2.2.0-r1.ipk
```
apk(OpenWrt / ImmortalWrt 25.12+):
```bash
apk add --allow-untrusted luci-app-substore-2.1.3-r6.apk
apk add --allow-untrusted luci-app-substore-2.2.0-r1.apk
```
然后在 LuCI 菜单打开:**服务 → 订阅**。
@@ -73,8 +78,10 @@ apk add --allow-untrusted luci-app-substore-2.1.3-r6.apk
## 使用方法
1. **添加订阅** —— 粘贴订阅 URL;可选的按订阅 cron 定时、规则或下载代理。
无订阅源时可「添加本地订阅」:粘贴节点文本或表单逐条录入。
2. **更新** —— 下载、解析并过滤节点。
3. **浏览节点** —— 筛选、排序、探测延迟。
3. **浏览节点** —— 筛选(分组 / 协议 / 关键词)、排序、探测延迟;勾选复选框后「删除」
可批量删除,行内可编辑 / 删除 / 改分组,「刷新」重载列表。
4. **导出** —— 任选 13 种格式之一,或复制订阅链接供下游客户端(Passwall / OpenClash / …)使用。
## 目录结构
+3 -3
View File
@@ -3,18 +3,18 @@
> NOTE: stage-0 skeleton has not yet been installed/verified on a target device.
> This document will be updated once installation is validated.
>
> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.1.3-r6)。
> 包名中的版本号需与 Makefile 的 `PKG_VERSION` / `PKG_RELEASE` 保持同步(当前 2.2.0-r1)。
## Install (opkg — OpenWrt / ImmortalWrt 24.10 及更早)
```bash
opkg install luci-app-substore-2.1.3-r6.ipk
opkg install luci-app-substore-2.2.0-r1.ipk
```
## Install (apk — OpenWrt / ImmortalWrt 25.12+)
```bash
apk add --allow-untrusted luci-app-substore-2.1.3-r6.apk
apk add --allow-untrusted luci-app-substore-2.2.0-r1.apk
```
Refresh LuCI:
+11 -1
View File
@@ -265,4 +265,14 @@ msgid "Failed to set group"
msgstr "分组设置失败"
msgid "Note: edits to this node are overwritten the next time the subscription is updated or re-saved."
msgstr "注意:对该节点的修改会在订阅下次更新或重新保存后被覆盖。"
msgstr "注意:对该节点的修改会在订阅下次更新或重新保存后被覆盖。"
# 节点页:刷新 / 勾选批量删除
msgid "Refresh"
msgstr "刷新"
msgid "Delete selected nodes?"
msgstr "确定删除选中的节点?"
msgid "Please select nodes first"
msgstr "请先勾选节点"
@@ -218,21 +218,32 @@ function action_node_save()
back_to_nodes(http)
end
-- 单节点删除
-- 节点删除:idx 支持单个(行内删除按钮)或逗号分隔多个(勾选批量删除)
function action_node_delete()
local http = require("luci.http")
local core = require("substore.core")
if post_ok() then
local id = http.formvalue("id") or ""
local idx = tonumber(http.formvalue("idx") or "")
local idx_param = http.formvalue("idx") or ""
if type(idx_param) == "table" then idx_param = table.concat(idx_param, ",") end
local nodes = core.read_nodes(id)
if idx and nodes[idx] then
table.remove(nodes, idx)
if core.write_nodes(id, nodes) then
core.save_meta(id, { node_count = #nodes })
core.refresh_combos(id)
local idxs = {}
for s in tostring(idx_param):gmatch("%d+") do
idxs[#idxs + 1] = tonumber(s)
end
-- 倒序删除,避免 table.remove 后下标偏移
table.sort(idxs, function(a, b) return a > b end)
local removed = false
for _, i in ipairs(idxs) do
if nodes[i] then
table.remove(nodes, i)
removed = true
end
end
if removed and core.write_nodes(id, nodes) then
core.save_meta(id, { node_count = #nodes })
core.refresh_combos(id)
end
end
back_to_nodes(http)
end
@@ -125,7 +125,7 @@ var NODEFORM_I18N = {
"defaultOpt": "<%:Default%>"
};
</script>
<script src="/luci-static/resources/substore/nodeform.js?v=2.1.3"></script>
<script src="/luci-static/resources/substore/nodeform.js?v=2.2.0"></script>
<script>
// 切换导入模式:仅切换显示/禁用,不提交表单
function substore_local_switch(mode) {
@@ -73,7 +73,7 @@ var NODEFORM_I18N = {
};
var NODE_EDIT_DATA = <%=js_node%>;
</script>
<script src="/luci-static/resources/substore/nodeform.js?v=2.1.3"></script>
<script src="/luci-static/resources/substore/nodeform.js?v=2.2.0"></script>
<script>
function node_edit_prepare() {
var arr = collectNodes();
@@ -57,7 +57,7 @@
<% end %>
</select>
</label>
<label><%:Keyword%>: <input type="text" name="keyword" value="<%=keyword%>"/></label>
<label><%:Keyword%>: <input type="text" name="keyword" size="12" value="<%=keyword%>"/></label>
<label><%:Sort by%>:
<select name="sort">
<option value="name" <%=sort_by=="name" and "selected" or ""%>>name</option>
@@ -69,14 +69,29 @@
</label>
<label style="margin-left:0.5em"><input type="checkbox" name="desc" value="1" <%=sort_desc and "checked" or ""%>/> <%:Desc%></label>
<input type="submit" class="cbi-button cbi-button-apply" value="<%:Filter%>"/>
<button type="button" class="cbi-button" onclick="location.reload()"><%:Refresh%></button>
<button type="button" class="cbi-button cbi-button-reset" onclick="substore_delete_selected()"><%:Delete%></button>
<button type="button" class="cbi-button" onclick="runProbe('ping')">Ping</button>
<button type="button" class="cbi-button" onclick="runProbe('tcping')">TCPing</button>
<button type="button" class="cbi-button" onclick="runProbe('url')"><%:URL Test%></button>
</div>
</form>
<!-- 勾选批量删除用的隐藏表单:JS 收集选中下标填入 idx(逗号分隔)后提交 -->
<form id="batch_delete_form" method="post" action="<%=luci.dispatcher.build_url('admin/services/substore/node_delete')%>" style="display:none">
<input type="hidden" name="token" value="<%=token%>"/>
<input type="hidden" name="id" value="<%=id%>"/>
<input type="hidden" name="idx" value=""/>
<input type="hidden" name="proto" value="<%=proto%>"/>
<input type="hidden" name="keyword" value="<%=keyword%>"/>
<input type="hidden" name="group" value="<%=group%>"/>
<input type="hidden" name="sort" value="<%=sort_by%>"/>
<% if sort_desc then %><input type="hidden" name="desc" value="1"/><% end %>
</form>
<table class="cbi-section-table">
<tr class="cbi-section-table-titles">
<th style="width:1em"><input type="checkbox" onclick="substore_toggle_all(this)"/></th>
<th><%:Name%></th>
<th><%:Group%></th>
<th><%:Protocol%></th>
@@ -87,6 +102,7 @@
</tr>
<% for _, n in ipairs(nodes) do %>
<tr class="cbi-section-table-row" data-server="<%=n.server or ""%>" data-port="<%=n.port or ""%>">
<td><input type="checkbox" class="node-check" value="<%=n.__idx%>"/></td>
<td><%=n.name or ""%></td>
<td>
<input type="text" class="group-input" style="width:8em" value="<%=luci.util.pcdata(n.group or "")%>"
@@ -114,7 +130,7 @@
</tr>
<% end %>
<% if #nodes == 0 then %>
<tr class="cbi-section-table-row"><td colspan="7" style="text-align:center;color:#808080"><%:No nodes found.%></td></tr>
<tr class="cbi-section-table-row"><td colspan="8" style="text-align:center;color:#808080"><%:No nodes found.%></td></tr>
<% end %>
</table>
<div id="probe_area" style="display:none;margin:0.5em 0">
@@ -149,6 +165,20 @@ function substore_set_group(el) {
+ '&group=' + encodeURIComponent(el.value)
+ '&token=' + encodeURIComponent('<%=token%>'));
}
function substore_toggle_all(el) {
var boxes = document.querySelectorAll('.node-check');
for (var i = 0; i < boxes.length; i++) boxes[i].checked = el.checked;
}
function substore_delete_selected() {
var boxes = document.querySelectorAll('.node-check:checked');
if (boxes.length === 0) { alert('<%:Please select nodes first%>'); return; }
if (!confirm('<%=luci.util.pcdata(luci.i18n.translate('Delete selected nodes?'))%>')) return;
var idxs = [];
for (var i = 0; i < boxes.length; i++) idxs.push(boxes[i].value);
var form = document.getElementById('batch_delete_form');
form.elements['idx'].value = idxs.join(',');
form.submit();
}
function runProbe(mode) {
var area = document.getElementById('probe_area');
var sum = document.getElementById('probe_summary');