1
0
mirror of https://github.com/nitezs/sub2clash.git synced 2024-12-23 14:24:42 -05:00

feat: 增加输出NodeList选项

This commit is contained in:
Nite07 2023-09-25 15:43:21 +08:00
parent ad7d2b98f6
commit 6a5cfd35c9
13 changed files with 741 additions and 712 deletions

View File

@ -19,7 +19,7 @@
# `/short`
获取短链Content-Type 为 `application/json`
具体参考使用可以参考 [api\templates\index.html](./api/templates/index.html)
具体参考使用可以参考 [api\templates\index.html](api/static/index.html)
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|----------|--------|------|-----|------------------|

View File

@ -43,7 +43,7 @@
### 模板
可以通过变量自定义模板中的策略组代理节点
可以通过变量自定义模板中的策略组代理节点
解释的不太清楚,可以参考下方默认模板
- `<all>` 为添加所有节点

View File

@ -22,6 +22,17 @@ func SubmodHandler(c *gin.Context) {
return
}
// 输出
if query.NodeListMode {
nodelist := model.NodeList{}
nodelist.Proxies = sub.Proxies
marshal, err := yaml.Marshal(nodelist)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
return
}
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())

View File

@ -23,6 +23,17 @@ func SubHandler(c *gin.Context) {
return
}
// 输出
if query.NodeListMode {
nodelist := model.NodeList{}
nodelist.Proxies = sub.Proxies
marshal, err := yaml.Marshal(nodelist)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
return
}
c.String(http.StatusOK, string(marshal))
return
}
marshal, err := yaml.Marshal(sub)
if err != nil {
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())

View File

@ -4,23 +4,40 @@ import (
"embed"
"github.com/gin-gonic/gin"
"html/template"
"log"
"net/http"
"sub2clash/api/controller"
"sub2clash/config"
"sub2clash/middleware"
)
//go:embed templates/*
var templates embed.FS
//go:embed static
var staticFiles embed.FS
func SetRoute(r *gin.Engine) {
r.Use(middleware.ZapLogger())
// 使用内嵌的模板文件
r.SetHTMLTemplate(template.Must(template.New("").ParseFS(templates, "templates/*")))
tpl, err := template.ParseFS(staticFiles, "static/*")
if err != nil {
log.Fatalf("Error parsing templates: %v", err)
}
r.SetHTMLTemplate(tpl)
r.GET(
"/static/*filepath", func(c *gin.Context) {
c.FileFromFS("static/"+c.Param("filepath"), http.FS(staticFiles))
},
)
r.GET(
"/", func(c *gin.Context) {
version := config.Version
if len(config.Version) > 7 {
version = config.Version[:7]
}
c.HTML(
200, "index.html", gin.H{
"Version": config.Version,
"Version": version,
},
)
},

258
api/static/index.html Normal file
View File

@ -0,0 +1,258 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>sub2clash</title>
<!-- Bootstrap CSS -->
<link
crossorigin="anonymous"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
rel="stylesheet"
/>
<!-- Bootstrap JS -->
<script
crossorigin="anonymous"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@latest/dist/axios.min.js"></script>
<script src="./static/index.js"></script>
<style>
.container {
max-width: 800px;
}
.btn-xs {
padding: 2px 2px; /* 调整内边距以减小按钮大小 */
font-size: 10px; /* 设置字体大小 */
line-height: 1.2; /* 调整行高 */
border-radius: 3px; /* 可选的边框半径调整 */
height: 25px;
width: 25px;
}
</style>
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="mb-4">
<h2>sub2clash</h2>
<span class="text-muted fst-italic"
>通用订阅链接转 Clash(Meta) 配置工具
<a
href="https://github.com/nitezs/sub2clash#clash-meta"
target="_blank"
>使用文档</a
></span
>
</div>
<!-- Input URL -->
<div class="form-group mb-5">
<label for="apiLink">解析链接:</label>
<div class="input-group mb-2">
<input
class="form-control"
id="urlInput"
type="text"
placeholder="通过生成的链接重新填写下方设置"
/>
<button
class="btn btn-primary"
onclick="parseInputURL()"
type="button"
>
解析
</button>
</div>
</div>
<!-- API Endpoint -->
<div class="form-group mb-3">
<label for="endpoint">客户端类型:</label>
<select class="form-control" id="endpoint" name="endpoint">
<option value="clash">Clash</option>
<option value="meta">Clash.Meta</option>
</select>
</div>
<!-- Template -->
<div class="form-group mb-3">
<label for="template">模板链接或名称:</label>
<input
class="form-control"
id="template"
name="template"
placeholder="输入外部模板链接或内部模板名称(可选)"
type="text"
/>
</div>
<!-- Subscription Link -->
<div class="form-group mb-3">
<label for="sub">订阅链接:</label>
<textarea
class="form-control"
id="sub"
name="sub"
placeholder="每行输入一个订阅链接"
rows="5"
></textarea>
</div>
<!-- Proxy Link -->
<div class="form-group mb-3">
<label for="proxy">节点分享链接:</label>
<textarea
class="form-control"
id="proxy"
name="proxy"
placeholder="每行输入一个节点分享链接"
rows="5"
></textarea>
</div>
<!-- Refresh -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="refresh"
name="refresh"
type="checkbox"
/>
<label class="form-check-label" for="refresh">强制重新获取订阅</label>
</div>
<!-- Node List -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="nodeList"
name="nodeList"
type="checkbox"
/>
<label class="form-check-label" for="nodeList">输出为 Node List</label>
</div>
<!-- Auto Test -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="autoTest"
name="autoTest"
type="checkbox"
/>
<label class="form-check-label" for="autoTest"
>国家策略组自动测速</label
>
</div>
<!-- Lazy -->
<div class="form-check mb-3">
<input class="form-check-input" id="lazy" name="lazy" type="checkbox" />
<label class="form-check-label" for="lazy"
>自动测速启用 lazy 模式</label
>
</div>
<!-- Rule Provider -->
<div class="form-group mb-3" id="ruleProviderGroup">
<label>Rule Provider:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addRuleProvider()"
type="button"
>
+
</button>
</div>
<!-- Rule -->
<div class="form-group mb-3" id="ruleGroup">
<label>规则:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addRule()"
type="button"
>
+
</button>
</div>
<!-- Sort -->
<div class="form-group mb-3">
<label for="sort">国家策略组排序规则:</label>
<select class="form-control" id="sort" name="sort">
<option value="nameasc">名称(升序)</option>
<option value="namedesc">名称(降序)</option>
<option value="sizeasc">节点数量(升序)</option>
<option value="sizedesc">节点数量(降序)</option>
</select>
</div>
<!-- Remove -->
<div class="form-group mb-3">
<label for="remove">排除节点:</label>
<input
class="form-control"
type="text"
name="remove"
id="remove"
placeholder="正则表达式"
/>
</div>
<!-- Rename -->
<div class="form-group mb-3" id="replaceGroup">
<label>节点名称替换:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addReplace()"
type="button"
>
+
</button>
</div>
<!-- Display the API Link -->
<div class="form-group mb-5">
<label for="apiLink">配置链接:</label>
<div class="input-group mb-2">
<input class="form-control" id="apiLink" readonly type="text" />
<button class="btn btn-primary" onclick="generateURL()" type="button">
生成链接
</button>
<button
class="btn btn-primary"
onclick="copyToClipboard('apiLink',this)"
type="button"
>
复制链接
</button>
</div>
<div class="input-group">
<input class="form-control" id="apiShortLink" readonly type="text" />
<input
class="form-control"
id="password"
type="text"
placeholder="密码"
/>
<button
class="btn btn-primary"
onclick="generateShortLink()"
type="button"
>
生成短链
</button>
<button
class="btn btn-primary"
onclick="copyToClipboard('apiShortLink',this)"
type="button"
>
复制短链
</button>
</div>
</div>
<!-- footer-->
<footer>
<p class="text-center">
Powered by
<a class="link-primary" href="https://github.com/nitezs/sub2clash"
>sub2clash</a
>
</p>
<p class="text-center">Version {{.Version}}</p>
</footer>
</div>
</body>
</html>

415
api/static/index.js Normal file
View File

@ -0,0 +1,415 @@
function clearExistingValues() {
// 清除简单输入框和复选框的值
document.getElementById("endpoint").value = "clash";
document.getElementById("sub").value = "";
document.getElementById("proxy").value = "";
document.getElementById("refresh").checked = false;
document.getElementById("autoTest").checked = false;
document.getElementById("lazy").checked = false;
document.getElementById("template").value = "";
document.getElementById("sort").value = "nameasc";
document.getElementById("remove").value = "";
document.getElementById("apiLink").value = "";
document.getElementById("apiShortLink").value = "";
document.getElementById("password").value = "";
document.getElementById("nodeList").checked = false;
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
clearInputGroup("ruleProviderGroup");
clearInputGroup("replaceGroup");
clearInputGroup("ruleGroup");
}
function generateURI() {
const queryParams = [];
// 获取 API Endpoint
const endpoint = document.getElementById("endpoint").value;
// 获取并组合订阅链接
let subLines = document
.getElementById("sub")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noSub = false;
// 去除 subLines 中空元素
subLines = subLines.map((item) => {
if (item !== "") {
return item;
}
});
if (subLines.length > 0) {
queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`);
} else {
noSub = true;
}
// 获取并组合节点分享链接
let proxyLines = document
.getElementById("proxy")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noProxy = false;
// 去除 proxyLines 中空元素
proxyLines = proxyLines.map((item) => {
if (item !== "") {
return item;
}
});
if (proxyLines.length > 0) {
queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`);
} else {
noProxy = true;
}
if (noSub && noProxy) {
alert("订阅链接和节点分享链接不能同时为空!");
return "";
}
// 获取复选框的值
const refresh = document.getElementById("refresh").checked;
queryParams.push(`refresh=${refresh ? "true" : "false"}`);
const autoTest = document.getElementById("autoTest").checked;
queryParams.push(`autoTest=${autoTest ? "true" : "false"}`);
const lazy = document.getElementById("lazy").checked;
queryParams.push(`lazy=${lazy ? "true" : "false"}`);
const nodeList = document.getElementById("nodeList").checked;
queryParams.push(`nodeList=${nodeList ? "true" : "false"}`);
// 获取模板链接或名称(如果存在)
const template = document.getElementById("template").value;
if (template.trim() !== "") {
queryParams.push(`template=${encodeURIComponent(template)}`);
}
// 获取Rule Provider和规则
const ruleProviders = document.getElementsByName("ruleProvider");
const rules = document.getElementsByName("rule");
let providers = [];
for (let i = 0; i < ruleProviders.length / 5; i++) {
let baseIndex = i * 5;
let behavior = ruleProviders[baseIndex].value;
let url = ruleProviders[baseIndex + 1].value;
let group = ruleProviders[baseIndex + 2].value;
let prepend = ruleProviders[baseIndex + 3].value;
let name = ruleProviders[baseIndex + 4].value;
// 是否存在空值
if (
behavior.trim() === "" ||
url.trim() === "" ||
group.trim() === "" ||
prepend.trim() === "" ||
name.trim() === ""
) {
alert("Rule Provider 中存在空值,请检查后重试!");
return "";
}
providers.push(`[${behavior},${url},${group},${prepend},${name}]`);
}
queryParams.push(`ruleProvider=${encodeURIComponent(providers.join(","))}`);
let ruleList = [];
for (let i = 0; i < rules.length / 3; i++) {
if (rules[i * 3].value.trim() !== "") {
let rule = rules[i * 3].value;
let prepend = rules[i * 3 + 1].value;
let group = rules[i * 3 + 2].value;
// 是否存在空值
if (rule.trim() === "" || prepend.trim() === "" || group.trim() === "") {
alert("Rule 中存在空值,请检查后重试!");
return "";
}
ruleList.push(`[${rule},${prepend},${group}]`);
}
}
queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`);
// 获取排序策略
const sort = document.getElementById("sort").value;
queryParams.push(`sort=${sort}`);
// 获取删除节点的正则表达式
const remove = document.getElementById("remove").value;
if (remove.trim() !== "") {
queryParams.push(`remove=${encodeURIComponent(remove)}`);
}
// 获取替换节点名称的正则表达式
let replaceList = [];
const replaces = document.getElementsByName("replace");
for (let i = 0; i < replaces.length / 2; i++) {
let replaceStr = `<${replaces[i * 2].value}>`;
let replaceTo = `<${replaces[i * 2 + 1].value}>`;
if (replaceStr.trim() === "") {
alert("重命名设置中存在空值,请检查后重试!");
return "";
}
replaceList.push(`[${replaceStr},${replaceTo}]`);
}
queryParams.push(`replace=${encodeURIComponent(replaceList.join(","))}`);
return `${endpoint}?${queryParams.join("&")}`;
}
// 将输入框中的 URL 解析为参数
function parseInputURL() {
// 获取输入框中的 URL
const inputURL = document.getElementById("urlInput").value;
if (!inputURL) {
alert("请输入有效的链接!");
return;
}
let url;
try {
url = new URL(inputURL);
} catch (_) {
alert("无效的链接!");
return;
}
// 清除现有的输入框值
clearExistingValues();
// 获取查询参数
const params = new URLSearchParams(url.search);
// 分配值到对应的输入框
const pathSections = url.pathname.split("/");
const lastSection = pathSections[pathSections.length - 1];
const clientTypeSelect = document.getElementById("endpoint");
switch (lastSection.toLowerCase()) {
case "meta":
clientTypeSelect.value = "meta";
break;
case "clash":
default:
clientTypeSelect.value = "clash";
break;
}
if (params.has("sub")) {
document.getElementById("sub").value = decodeURIComponent(params.get("sub"))
.split(",")
.join("\n");
}
if (params.has("proxy")) {
document.getElementById("proxy").value = decodeURIComponent(
params.get("proxy"),
)
.split(",")
.join("\n");
}
if (params.has("refresh")) {
document.getElementById("refresh").checked =
params.get("refresh") === "true";
}
if (params.has("autoTest")) {
document.getElementById("autoTest").checked =
params.get("autoTest") === "true";
}
if (params.has("lazy")) {
document.getElementById("lazy").checked = params.get("lazy") === "true";
}
if (params.has("template")) {
document.getElementById("template").value = decodeURIComponent(
params.get("template"),
);
}
if (params.has("sort")) {
document.getElementById("sort").value = params.get("sort");
}
if (params.has("remove")) {
document.getElementById("remove").value = decodeURIComponent(
params.get("remove"),
);
}
if (params.has("replace")) {
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
}
if (params.has("ruleProvider")) {
parseAndFillRuleProviderParams(
decodeURIComponent(params.get("ruleProvider")),
);
}
if (params.has("rule")) {
parseAndFillRuleParams(decodeURIComponent(params.get("rule")));
}
if (params.has("nodeList")) {
document.getElementById("nodeList").checked =
params.get("nodeList") === "true";
}
}
function clearInputGroup(groupId) {
// 清空第二个之后的child
const group = document.getElementById(groupId);
while (group.children.length > 2) {
group.removeChild(group.lastChild);
}
}
function parseAndFillReplaceParams(replaceParams) {
const replaceGroup = document.getElementById("replaceGroup");
let matches;
const regex = /\[(<.*?>),(<.*?>)\]/g;
const str = decodeURIComponent(replaceParams);
while ((matches = regex.exec(str)) !== null) {
const div = createReplace();
const original = matches[1].slice(1, -1); // Remove < and >
const replacement = matches[2].slice(1, -1); // Remove < and >
div.children[0].value = original;
div.children[1].value = replacement;
replaceGroup.appendChild(div);
}
}
function parseAndFillRuleProviderParams(ruleProviderParams) {
const ruleProviderGroup = document.getElementById("ruleProviderGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleProviderParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRuleProvider();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
div.children[3].value = matches[4];
div.children[4].value = matches[5];
ruleProviderGroup.appendChild(div);
}
}
function parseAndFillRuleParams(ruleParams) {
const ruleGroup = document.getElementById("ruleGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRule();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
ruleGroup.appendChild(div);
}
}
async function copyToClipboard(elem, e) {
const apiLinkInput = document.querySelector(`#${elem}`).value;
try {
await navigator.clipboard.writeText(apiLinkInput);
let text = e.textContent;
e.addEventListener("mouseout", function () {
e.textContent = text;
});
e.textContent = "复制成功";
} catch (err) {
console.error("复制到剪贴板失败:", err);
}
}
function createRuleProvider() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="ruleProvider" placeholder="Behavior">
<input type="text" class="form-control" name="ruleProvider" placeholder="Url">
<input type="text" class="form-control" name="ruleProvider" placeholder="Group">
<input type="text" class="form-control" name="ruleProvider" placeholder="Prepend">
<input type="text" class="form-control" name="ruleProvider" placeholder="Name">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
}
function createReplace() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
<input type="text" class="form-control" name="replace" placeholder="替换为(可为空)">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
}
function createRule() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="rule" placeholder="Rule">
<input type="text" class="form-control" name="rule" placeholder="Prepend">
<input type="text" class="form-control" name="rule" placeholder="Group">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
}
function addRuleProvider() {
const div = createRuleProvider();
document.getElementById("ruleProviderGroup").appendChild(div);
}
function addRule() {
const div = createRule();
document.getElementById("ruleGroup").appendChild(div);
}
function addReplace() {
const div = createReplace();
document.getElementById("replaceGroup").appendChild(div);
}
function removeElement(button) {
button.parentElement.remove();
}
function generateURL() {
const apiLink = document.getElementById("apiLink");
let uri = generateURI();
if (uri === "") {
return;
}
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
}
function generateShortLink() {
const apiShortLink = document.getElementById("apiShortLink");
const password = document.getElementById("password");
let uri = generateURI();
if (uri === "") {
return;
}
axios
.post(
"./short",
{
url: uri,
password: password.value.trim(),
},
{
headers: {
"Content-Type": "application/json",
},
},
)
.then((response) => {
apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
})
.catch((error) => {
console.log(error);
alert("生成短链失败,请重试!");
});
}

View File

@ -1,688 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>sub2clash</title>
<!-- Bootstrap CSS -->
<link
crossorigin="anonymous"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
rel="stylesheet"
/>
<!-- Bootstrap JS -->
<script
crossorigin="anonymous"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<style>
.container {
max-width: 800px;
}
.btn-xs {
padding: 2px 2px; /* 调整内边距以减小按钮大小 */
font-size: 10px; /* 设置字体大小 */
line-height: 1.2; /* 调整行高 */
border-radius: 3px; /* 可选的边框半径调整 */
height: 25px;
width: 25px;
}
</style>
</head>
<body class="bg-light">
<div class="container mt-5">
<div class="mb-4">
<h2>sub2clash</h2>
<span class="text-muted fst-italic"
>通用订阅链接转 Clash(Meta) 配置工具
<a
href="https://github.com/nitezs/sub2clash#clash-meta"
target="_blank"
>使用文档</a
></span
>
</div>
<form id="apiForm">
<!-- Input URL -->
<div class="form-group mb-5">
<label for="apiLink">解析链接:</label>
<div class="input-group mb-2">
<input class="form-control" id="urlInput" type="text" />
<button
class="btn btn-primary"
onclick="parseInputURL()"
type="button"
>
解析
</button>
</div>
</div>
<!-- API Endpoint -->
<div class="form-group mb-3">
<label for="endpoint">客户端类型:</label>
<select class="form-control" id="endpoint" name="endpoint">
<option value="clash">Clash</option>
<option value="meta">Clash.Meta</option>
</select>
</div>
<!-- Subscription Link -->
<div class="form-group mb-3">
<label for="sub">订阅链接:</label>
<textarea
class="form-control"
id="sub"
name="sub"
placeholder="每行输入一个订阅链接"
rows="5"
></textarea>
</div>
<!-- Proxy Link -->
<div class="form-group mb-3">
<label for="proxy">节点分享链接:</label>
<textarea
class="form-control"
id="proxy"
name="proxy"
placeholder="每行输入一个节点分享链接"
rows="5"
></textarea>
</div>
<!-- Refresh -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="refresh"
name="refresh"
type="checkbox"
/>
<label class="form-check-label" for="refresh">强制刷新配置</label>
</div>
<!-- Template -->
<div class="form-group mb-3">
<label for="template">模板链接或名称(可选):</label>
<input
class="form-control"
id="template"
name="template"
placeholder="输入外部模板链接或内部模板名称"
type="text"
/>
</div>
<!-- Rule Provider -->
<div class="form-group mb-3" id="ruleProviderGroup">
<label>Rule Provider:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addRuleProvider()"
type="button"
>
+
</button>
</div>
<!-- Rule -->
<div class="form-group mb-3" id="ruleGroup">
<label>规则:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addRule()"
type="button"
>
+
</button>
</div>
<!-- Auto Test -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="autoTest"
name="autoTest"
type="checkbox"
/>
<label class="form-check-label" for="autoTest"
>国家策略组自动测速</label
>
</div>
<!-- Lazy -->
<div class="form-check mb-3">
<input
class="form-check-input"
id="lazy"
name="lazy"
type="checkbox"
/>
<label class="form-check-label" for="lazy">自动测速启用 lazy</label>
</div>
<!-- Sort -->
<div class="form-group mb-3">
<label for="sort">国家策略组排序策略:</label>
<select class="form-control" id="sort" name="sort">
<option value="nameasc">名称(升序)</option>
<option value="namedesc">名称(降序)</option>
<option value="sizeasc">节点数量(升序)</option>
<option value="sizedesc">节点数量(降序)</option>
</select>
</div>
<!-- Remove -->
<div class="form-group mb-3">
<label for="remove">删除节点:</label>
<input
class="form-control"
type="text"
name="remove"
id="remove"
placeholder="正则表达式"
/>
</div>
<!-- Rename -->
<div class="form-group mb-3" id="replaceGroup">
<label>节点名称替换:</label>
<button
class="btn btn-primary mb-1 btn-xs"
onclick="addReplace()"
type="button"
>
+
</button>
</div>
</form>
<!-- Display the API Link -->
<div class="form-group mb-5">
<label for="apiLink">配置链接:</label>
<div class="input-group mb-2">
<input class="form-control" id="apiLink" readonly type="text" />
<button class="btn btn-primary" onclick="generateURL()" type="button">
生成链接
</button>
<button
class="btn btn-primary"
onclick="copyToClipboard('apiLink',this)"
type="button"
>
复制链接
</button>
</div>
<div class="input-group">
<input class="form-control" id="apiShortLink" readonly type="text" />
<input
class="form-control"
id="password"
type="text"
placeholder="密码"
/>
<button
class="btn btn-primary"
onclick="generateShortLink()"
type="button"
>
生成短链
</button>
<button
class="btn btn-primary"
onclick="copyToClipboard('apiShortLink',this)"
type="button"
>
复制短链
</button>
</div>
</div>
<!-- footer-->
<footer>
<p class="text-center">
Powered by
<a class="link-primary" href="https://github.com/nitezs/sub2clash"
>sub2clash</a
>
</p>
<p class="text-center">Version {{.Version}}</p>
</footer>
</div>
<script>
function clearExistingValues() {
// 清除简单输入框和复选框的值
document.getElementById("endpoint").value = "clash";
document.getElementById("sub").value = "";
document.getElementById("proxy").value = "";
document.getElementById("refresh").checked = false;
document.getElementById("autoTest").checked = false;
document.getElementById("lazy").checked = false;
document.getElementById("template").value = "";
document.getElementById("sort").value = "nameasc";
document.getElementById("remove").value = "";
document.getElementById("apiLink").value = "";
document.getElementById("apiShortLink").value = "";
document.getElementById("password").value = "";
// 清除由 createRuleProvider, createReplace, 和 createRule 创建的所有额外输入组
clearInputGroup("ruleProviderGroup");
clearInputGroup("replaceGroup");
clearInputGroup("ruleGroup");
}
function clearInputGroup(groupId) {
// 清空第二个之后的child
const group = document.getElementById(groupId);
while (group.children.length > 2) {
group.removeChild(group.lastChild);
}
}
function parseInputURL() {
// 获取输入框中的 URL
const inputURL = document.getElementById("urlInput").value;
if (!inputURL) {
alert("请输入有效的链接!");
return;
}
let url;
try {
url = new URL(inputURL);
} catch (_) {
alert("无效的链接!");
return;
}
// 清除现有的输入框值
clearExistingValues();
// 获取查询参数
const params = new URLSearchParams(url.search);
// 分配值到对应的输入框
const pathSections = url.pathname.split("/");
const lastSection = pathSections[pathSections.length - 1];
const clientTypeSelect = document.getElementById("endpoint");
switch (lastSection.toLowerCase()) {
case "meta":
clientTypeSelect.value = "meta";
break;
case "clash":
default:
clientTypeSelect.value = "clash";
break;
}
if (params.has("sub")) {
document.getElementById("sub").value = decodeURIComponent(
params.get("sub"),
)
.split(",")
.join("\n");
}
if (params.has("proxy")) {
document.getElementById("proxy").value = decodeURIComponent(
params.get("proxy"),
)
.split(",")
.join("\n");
}
if (params.has("refresh")) {
document.getElementById("refresh").checked =
params.get("refresh") === "true";
}
if (params.has("autoTest")) {
document.getElementById("autoTest").checked =
params.get("autoTest") === "true";
}
if (params.has("lazy")) {
document.getElementById("lazy").checked =
params.get("lazy") === "true";
}
if (params.has("template")) {
document.getElementById("template").value = decodeURIComponent(
params.get("template"),
);
}
if (params.has("sort")) {
document.getElementById("sort").value = params.get("sort");
}
if (params.has("remove")) {
document.getElementById("remove").value = decodeURIComponent(
params.get("remove"),
);
}
if (params.has("replace")) {
// 这里您需要特别解析 'replace' 参数,并逐一填充到相关的输入框
parseAndFillReplaceParams(decodeURIComponent(params.get("replace")));
}
if (params.has("ruleProvider")) {
// 这里您需要特别解析 'ruleProvider' 参数,并逐一填充到相关的输入框
parseAndFillRuleProviderParams(
decodeURIComponent(params.get("ruleProvider")),
);
}
if (params.has("rule")) {
// 这里您需要特别解析 'rule' 参数,并逐一填充到相关的输入框
parseAndFillRuleParams(decodeURIComponent(params.get("rule")));
}
}
function parseAndFillReplaceParams(replaceParams) {
const replaceGroup = document.getElementById("replaceGroup");
let matches;
const regex = /\[(<.*?>),(<.*?>)\]/g;
const str = decodeURIComponent(replaceParams);
while ((matches = regex.exec(str)) !== null) {
const div = createReplace();
const original = matches[1].slice(1, -1); // Remove < and >
const replacement = matches[2].slice(1, -1); // Remove < and >
div.children[0].value = original;
div.children[1].value = replacement;
replaceGroup.appendChild(div);
}
}
function parseAndFillRuleProviderParams(ruleProviderParams) {
const ruleProviderGroup = document.getElementById("ruleProviderGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleProviderParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRuleProvider();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
div.children[3].value = matches[4];
div.children[4].value = matches[5];
ruleProviderGroup.appendChild(div);
}
}
function parseAndFillRuleParams(ruleParams) {
const ruleGroup = document.getElementById("ruleGroup");
let matches;
const regex = /\[(.*?),(.*?),(.*?)\]/g;
const str = decodeURIComponent(ruleParams);
while ((matches = regex.exec(str)) !== null) {
const div = createRule();
div.children[0].value = matches[1];
div.children[1].value = matches[2];
div.children[2].value = matches[3];
ruleGroup.appendChild(div);
}
}
async function copyToClipboard(elem, e) {
const apiLinkInput = document.querySelector(`#${elem}`).value;
try {
await navigator.clipboard.writeText(apiLinkInput);
let text = e.textContent;
e.addEventListener("mouseout", function () {
e.textContent = text;
});
e.textContent = "复制成功";
} catch (err) {
console.error("复制到剪贴板失败:", err);
}
}
function createRuleProvider() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="ruleProvider" placeholder="Behavior">
<input type="text" class="form-control" name="ruleProvider" placeholder="Url">
<input type="text" class="form-control" name="ruleProvider" placeholder="Group">
<input type="text" class="form-control" name="ruleProvider" placeholder="Prepend">
<input type="text" class="form-control" name="ruleProvider" placeholder="Name">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
}
function createReplace() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
<input type="text" class="form-control" name="replace" placeholder="替换为(可为空)">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
}
function createRule() {
const div = document.createElement("div");
div.classList.add("input-group", "mb-2");
div.innerHTML = `
<input type="text" class="form-control" name="rule" placeholder="Rule">
<input type="text" class="form-control" name="rule" placeholder="Prepend">
<input type="text" class="form-control" name="rule" placeholder="Group">
<button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
`;
return div;
}
function addRuleProvider() {
const div = createRuleProvider();
document.getElementById("ruleProviderGroup").appendChild(div);
}
function addRule() {
const div = createRule();
document.getElementById("ruleGroup").appendChild(div);
}
function addReplace() {
const div = createReplace();
document.getElementById("replaceGroup").appendChild(div);
}
function removeElement(button) {
button.parentElement.remove();
}
function generateURI() {
const queryParams = [];
// 获取 API Endpoint
const endpoint = document.getElementById("endpoint").value;
// 获取并组合订阅链接
let subLines = document
.getElementById("sub")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noSub = false;
// 去除 subLines 中空元素
subLines = subLines.map((item) => {
if (item !== "") {
return item;
}
});
if (subLines.length > 0) {
queryParams.push(`sub=${encodeURIComponent(subLines.join(","))}`);
} else {
noSub = true;
}
// 获取并组合节点分享链接
let proxyLines = document
.getElementById("proxy")
.value.split("\n")
.filter((line) => line.trim() !== "");
let noProxy = false;
// 去除 proxyLines 中空元素
proxyLines = proxyLines.map((item) => {
if (item !== "") {
return item;
}
});
if (proxyLines.length > 0) {
queryParams.push(`proxy=${encodeURIComponent(proxyLines.join(","))}`);
} else {
noProxy = true;
}
if (noSub && noProxy) {
alert("订阅链接和节点分享链接不能同时为空!");
return "";
}
// 获取复选框的值
const refresh = document.getElementById("refresh").checked;
queryParams.push(`refresh=${refresh ? "true" : "false"}`);
const autoTest = document.getElementById("autoTest").checked;
queryParams.push(`autoTest=${autoTest ? "true" : "false"}`);
const lazy = document.getElementById("lazy").checked;
queryParams.push(`lazy=${lazy ? "true" : "false"}`);
// 获取模板链接或名称(如果存在)
const template = document.getElementById("template").value;
if (template.trim() !== "") {
queryParams.push(`template=${encodeURIComponent(template)}`);
}
// 获取Rule Provider和规则
const ruleProviders = document.getElementsByName("ruleProvider");
const rules = document.getElementsByName("rule");
let providers = [];
for (let i = 0; i < ruleProviders.length / 5; i++) {
let baseIndex = i * 5;
let behavior = ruleProviders[baseIndex].value;
let url = ruleProviders[baseIndex + 1].value;
let group = ruleProviders[baseIndex + 2].value;
let prepend = ruleProviders[baseIndex + 3].value;
let name = ruleProviders[baseIndex + 4].value;
// 是否存在空值
if (
behavior.trim() === "" ||
url.trim() === "" ||
group.trim() === "" ||
prepend.trim() === "" ||
name.trim() === ""
) {
alert("Rule Provider 中存在空值,请检查后重试!");
return "";
}
providers.push(`[${behavior},${url},${group},${prepend},${name}]`);
}
queryParams.push(
`ruleProvider=${encodeURIComponent(providers.join(","))}`,
);
let ruleList = [];
for (let i = 0; i < rules.length / 3; i++) {
if (rules[i * 3].value.trim() !== "") {
let rule = rules[i * 3].value;
let prepend = rules[i * 3 + 1].value;
let group = rules[i * 3 + 2].value;
// 是否存在空值
if (
rule.trim() === "" ||
prepend.trim() === "" ||
group.trim() === ""
) {
alert("Rule 中存在空值,请检查后重试!");
return "";
}
ruleList.push(`[${rule},${prepend},${group}]`);
}
}
queryParams.push(`rule=${encodeURIComponent(ruleList.join(","))}`);
// 获取排序策略
const sort = document.getElementById("sort").value;
queryParams.push(`sort=${sort}`);
// 获取删除节点的正则表达式
const remove = document.getElementById("remove").value;
if (remove.trim() !== "") {
queryParams.push(`remove=${encodeURIComponent(remove)}`);
}
// 获取替换节点名称的正则表达式
let replaceList = [];
const replaces = document.getElementsByName("replace");
for (let i = 0; i < replaces.length / 2; i++) {
let replaceStr = `<${replaces[i * 2].value}>`;
let replaceTo = `<${replaces[i * 2 + 1].value}>`;
if (replaceStr.trim() === "") {
alert("重命名设置中存在空值,请检查后重试!");
return "";
}
replaceList.push(`[${replaceStr},${replaceTo}]`);
}
queryParams.push(
`replace=${encodeURIComponent(replaceList.join(","))}`,
);
return `${endpoint}?${queryParams.join("&")}`;
}
function generateURL() {
const apiLink = document.getElementById("apiLink");
let uri = generateURI();
if (uri === "") {
return;
}
apiLink.value = `${window.location.origin}${window.location.pathname}${uri}`;
}
function generateShortLink() {
const apiShortLink = document.getElementById("apiShortLink");
const password = document.getElementById("password");
let uri = generateURI();
if (uri === "") {
return;
}
axios
.post(
"./short",
{
url: uri,
password: password.value.trim(),
},
{
headers: {
"Content-Type": "application/json",
},
},
)
.then((response) => {
apiShortLink.value = `${window.location.origin}${window.location.pathname}s/${response.data}`;
})
.catch((error) => {
console.log(error);
alert("生成短链失败,请重试!");
});
}
</script>
</body>
</html>

View File

@ -4,24 +4,6 @@ type SmuxStruct struct {
Enabled bool `yaml:"enable"`
}
type VmessJson struct {
V string `json:"v"`
Ps string `json:"ps"`
Add string `json:"add"`
Port interface{} `json:"port"`
Id string `json:"id"`
Aid interface{} `json:"aid"`
Scy string `json:"scy"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
Tls string `json:"tls"`
Sni string `json:"sni"`
Alpn string `json:"alpn"`
Fp string `json:"fp"`
}
type Proxy struct {
Name string `yaml:"name,omitempty"`
Server string `yaml:"server,omitempty"`

View File

@ -27,6 +27,24 @@ type WSOptions struct {
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
}
type VmessJson struct {
V string `json:"v"`
Ps string `json:"ps"`
Add string `json:"add"`
Port interface{} `json:"port"`
Id string `json:"id"`
Aid interface{} `json:"aid"`
Scy string `json:"scy"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
Tls string `json:"tls"`
Sni string `json:"sni"`
Alpn string `json:"alpn"`
Fp string `json:"fp"`
}
type Vmess struct {
Type string `yaml:"type"`
Name string `yaml:"name"`

View File

@ -12,3 +12,7 @@ type Subscription struct {
Rules []string `yaml:"rules,omitempty"`
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
}
type NodeList struct {
Proxies []Proxy `yaml:"proxies,omitempty"`
}

View File

@ -29,6 +29,7 @@ type SubValidator struct {
Replace string `form:"replace" binding:""`
ReplaceKeys []string `form:"-" binding:""`
ReplaceTo []string `form:"-" binding:""`
NodeListMode bool `form:"nodeList,default=false" binding:""`
}
type RuleProviderStruct struct {