mirror of
https://github.com/nitezs/sub2clash.git
synced 2024-12-24 10:54:43 -05:00
feat: 增加输出NodeList选项
This commit is contained in:
parent
ad7d2b98f6
commit
6a5cfd35c9
@ -19,7 +19,7 @@
|
|||||||
# `/short`
|
# `/short`
|
||||||
|
|
||||||
获取短链,Content-Type 为 `application/json`
|
获取短链,Content-Type 为 `application/json`
|
||||||
具体参考使用可以参考 [api\templates\index.html](./api/templates/index.html)
|
具体参考使用可以参考 [api\templates\index.html](api/static/index.html)
|
||||||
|
|
||||||
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
| Body 参数 | 类型 | 是否必须 | 默认值 | 说明 |
|
||||||
|----------|--------|------|-----|------------------|
|
|----------|--------|------|-----|------------------|
|
||||||
|
@ -22,6 +22,17 @@ func SubmodHandler(c *gin.Context) {
|
|||||||
return
|
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)
|
marshal, err := yaml.Marshal(sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||||
|
@ -23,6 +23,17 @@ func SubHandler(c *gin.Context) {
|
|||||||
return
|
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)
|
marshal, err := yaml.Marshal(sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error())
|
||||||
|
25
api/route.go
25
api/route.go
@ -4,23 +4,40 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"sub2clash/api/controller"
|
"sub2clash/api/controller"
|
||||||
"sub2clash/config"
|
"sub2clash/config"
|
||||||
"sub2clash/middleware"
|
"sub2clash/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed templates/*
|
//go:embed static
|
||||||
var templates embed.FS
|
var staticFiles embed.FS
|
||||||
|
|
||||||
func SetRoute(r *gin.Engine) {
|
func SetRoute(r *gin.Engine) {
|
||||||
r.Use(middleware.ZapLogger())
|
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(
|
r.GET(
|
||||||
"/", func(c *gin.Context) {
|
"/", func(c *gin.Context) {
|
||||||
|
version := config.Version
|
||||||
|
if len(config.Version) > 7 {
|
||||||
|
version = config.Version[:7]
|
||||||
|
}
|
||||||
c.HTML(
|
c.HTML(
|
||||||
200, "index.html", gin.H{
|
200, "index.html", gin.H{
|
||||||
"Version": config.Version,
|
"Version": version,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
258
api/static/index.html
Normal file
258
api/static/index.html
Normal 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
415
api/static/index.js
Normal 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("生成短链失败,请重试!");
|
||||||
|
});
|
||||||
|
}
|
@ -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>
|
|
@ -4,24 +4,6 @@ type SmuxStruct struct {
|
|||||||
Enabled bool `yaml:"enable"`
|
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 {
|
type Proxy struct {
|
||||||
Name string `yaml:"name,omitempty"`
|
Name string `yaml:"name,omitempty"`
|
||||||
Server string `yaml:"server,omitempty"`
|
Server string `yaml:"server,omitempty"`
|
||||||
|
@ -27,6 +27,24 @@ type WSOptions struct {
|
|||||||
EarlyDataHeaderName string `proxy:"early-data-header-name,omitempty"`
|
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 Vmess struct {
|
||||||
Type string `yaml:"type"`
|
Type string `yaml:"type"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
|
@ -12,3 +12,7 @@ type Subscription struct {
|
|||||||
Rules []string `yaml:"rules,omitempty"`
|
Rules []string `yaml:"rules,omitempty"`
|
||||||
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
|
RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NodeList struct {
|
||||||
|
Proxies []Proxy `yaml:"proxies,omitempty"`
|
||||||
|
}
|
||||||
|
@ -29,6 +29,7 @@ type SubValidator struct {
|
|||||||
Replace string `form:"replace" binding:""`
|
Replace string `form:"replace" binding:""`
|
||||||
ReplaceKeys []string `form:"-" binding:""`
|
ReplaceKeys []string `form:"-" binding:""`
|
||||||
ReplaceTo []string `form:"-" binding:""`
|
ReplaceTo []string `form:"-" binding:""`
|
||||||
|
NodeListMode bool `form:"nodeList,default=false" binding:""`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleProviderStruct struct {
|
type RuleProviderStruct struct {
|
||||||
|
Loading…
Reference in New Issue
Block a user