Compare commits

..

No commits in common. "master" and "v0.0.9-beta.1" have entirely different histories.

20 changed files with 526 additions and 485 deletions

View File

@ -15,7 +15,7 @@ import (
//go:embed static //go:embed static
var staticFiles embed.FS var staticFiles embed.FS
func RunServer(bind string, port uint16) { func RunServer(port uint16) {
tpl, err := template.ParseFS(staticFiles, "static/*") tpl, err := template.ParseFS(staticFiles, "static/*")
if err != nil { if err != nil {
println(err.Error()) println(err.Error())
@ -41,9 +41,8 @@ func RunServer(bind string, port uint16) {
r.GET("/convert", handler.Convert) r.GET("/convert", handler.Convert)
address := bind + ":" + strconv.Itoa(int(port)) fmt.Println("Server is running on port", port)
fmt.Println("Server is running on", address) err = r.Run(":" + strconv.Itoa(int(port)))
err = r.Run(address)
if err != nil { if err != nil {
fmt.Println("Run server failed: ", err) fmt.Println("Run server failed: ", err)
} }

View File

@ -1,24 +1,19 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="mdui-theme-auto"> <html lang="zh-CN" class="mdui-theme-auto">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>sub2sing-box</title> <title>sub2sing-box</title>
<!-- Include Material Design UI CSS -->
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" /> <link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css" />
<!-- Include Google Fonts with Chinese support -->
<link href="https://fonts.googleapis.com/css?family=Roboto|Noto+Sans+SC&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" /> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet" /> <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet" />
<script src="https://unpkg.com/mdui@2/mdui.global.js"></script> <script src="https://unpkg.com/mdui@2/mdui.global.js"></script>
<style> <style>
/* Apply a uniform font across Chinese, Russian, and English */
body {
font-family: "Roboto", "Noto Sans SC", sans-serif;
}
h3 { h3 {
margin-top: 0; margin-top: 0;
} }
.section { .section {
margin: 20px 0; margin: 20px 0;
padding: 15px; padding: 15px;
@ -26,320 +21,221 @@
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
background-color: rgb(var(--mdui-color-surface-container)); background-color: rgb(var(--mdui-color-surface-container));
} }
.section>* { .section>* {
margin-bottom: 10px; margin-bottom: 10px;
} }
.mdui-container { .mdui-container {
border-radius: 10px; border-radius: 10px;
max-width: 1200px; max-width: 1200px;
margin: 30px auto; margin: 30px auto;
} }
.header-controls {
display: flex;
gap: 10px;
align-items: center;
}
/* Rename fields: now stacked in a wide layout */
.rename-group {
display: flex;
flex-direction: column;
gap: 10px;
}
</style> </style>
</head> </head>
<body> <body>
<div class="mdui-container"> <div class="mdui-container">
<div style="display: flex; align-items: center; justify-content: space-between;"> <div style="
display: flex;
align-items: center;
justify-content: space-between;
">
<h2> <h2>
<a href="https://github.com/nitezs/sub2sing-box" target="_blank">sub2sing-box</a> <a href="https://github.com/nitezs/sub2sing-box" target="_blank">sub2sing-box</a>
</h2> </h2>
<div class="header-controls">
<!-- Language switcher -->
<mdui-dropdown trigger="hover">
<mdui-icon slot="trigger" name="language"></mdui-icon>
<mdui-menu id="language" selects="single">
<mdui-menu-item value="zh-CN">中文</mdui-menu-item>
<mdui-menu-item value="en">English</mdui-menu-item>
<mdui-menu-item value="ru">Русский</mdui-menu-item>
</mdui-menu>
</mdui-dropdown>
<!-- Theme switcher -->
<mdui-dropdown trigger="hover"> <mdui-dropdown trigger="hover">
<mdui-icon slot="trigger" name="dark_mode"></mdui-icon> <mdui-icon slot="trigger" name="dark_mode"></mdui-icon>
<mdui-menu id="theme" selects="single" value="dark"> <mdui-menu id="theme" selects="single" value="dark">
<mdui-menu-item value="light" data-i18n="theme-light">Light</mdui-menu-item> <mdui-menu-item value="light">亮色</mdui-menu-item>
<mdui-menu-item value="dark" data-i18n="theme-dark">Dark</mdui-menu-item> <mdui-menu-item value="dark">暗色</mdui-menu-item>
<mdui-divider></mdui-divider> <mdui-divider></mdui-divider>
<mdui-menu-item value="auto" data-i18n="theme-auto">Auto</mdui-menu-item> <mdui-menu-item value="auto">跟随系统</mdui-menu-item>
</mdui-menu> </mdui-menu>
</mdui-dropdown> </mdui-dropdown>
</div> </div>
</div>
<!-- Template Section --> <!-- Template Section -->
<div class=" section"> <div class=" section">
<h3> <h3><a href="https://github.com/nitezs/sub2sing-box/tree/master/templates">模板</a></h3>
<a href="https://github.com/nitezs/sub2sing-box/tree/master/templates" data-i18n="template">Template</a> <mdui-text-field label="模板" type="text" id="template" name="template"
</h3> value="https://raw.githubusercontent.com/nitezs/sub2sing-box/refs/heads/master/templates/example-windows.json" />
<mdui-text-field data-i18n="template" label="Template" type="text" id="template" name="template"
value="https://raw.githubusercontent.com/nitezs/sub2sing-box/refs/heads/master/templates/example-windows.json">
</mdui-text-field>
</div> </div>
<div id="form"> <div id="form">
<!-- Nodes Section --> <!-- Nodes Section -->
<div class="section"> <div class="section">
<h3 data-i18n="nodes">Nodes</h3> <h3>节点</h3>
<div> <div>
<mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="subscription-link" <mdui-text-field label="订阅链接" autosize min-rows="3" max-rows="5" id="subscription" name="subscription"
data-i18n-placeholder="one-per-line" placeholder="一行一个"></mdui-text-field>
label="Subscription Link" type="text" id="subscription" name="subscription"
placeholder="One per line">
</mdui-text-field>
</div> </div>
<div> <div>
<mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="proxy-link" <mdui-text-field label="节点分享链接" autosize min-rows="3" max-rows="5" id="proxy" name="proxy"
data-i18n-placeholder="one-per-line" placeholder="一行一个"></mdui-text-field>
label="Node Share Link" type="text" id="proxy" name="proxy"
placeholder="One per line">
</mdui-text-field>
</div> </div>
<div> <div>
<mdui-text-field data-i18n="delete-node" <mdui-text-field label="删除节点" type="text" id="delete" name="delete" placeholder="支持正则表达式" />
data-i18n-placeholder="supports-regex"
label="Delete Node" type="text" id="delete" name="delete"
placeholder="Supports regex">
</mdui-text-field>
</div> </div>
<div> <div>
<mdui-list id="list"> <mdui-list id="list">
<mdui-list-item> <mdui-list-item>
<span data-i18n="rename-node">Rename Node</span> 重命名节点
<mdui-button slot="end-icon" type="button" onclick="addRenameField()">+</mdui-button> <mdui-button slot="end-icon" type="button" onclick="addRenameField()">+
</mdui-button>
</mdui-list-item> </mdui-list-item>
</mdui-list> </mdui-list>
<div id="renameContainer"></div> <div id="renameContainer"></div>
</div> </div>
</div> </div>
<!-- Policy Group Section --> <!-- Policy Group Section -->
<div class="section"> <div class="section">
<h3 data-i18n="policy-group">Policy Group</h3> <h3>策略组</h3>
<div> <div>
<mdui-text-field data-i18n="type" label="Type" type="text" id="group-type" name="group-type" <mdui-text-field label="类型" type="text" id="group-type" name="group-type" value="selector" />
value="selector">
</mdui-text-field>
</div> </div>
<div> <div>
<mdui-select data-i18n="sort-by" label="Sort By" name="sort" id="sort" value="tag"> <mdui-select label="排序依据" name="sort" id="sort" value="tag">
<mdui-menu-item value="tag" selected data-i18n="node-name">Node Name</mdui-menu-item> <mdui-menu-item value="tag" selected>节点名</mdui-menu-item>
<mdui-menu-item value="num" data-i18n="node-count">Node Count</mdui-menu-item> <mdui-menu-item value="num">节点数量</mdui-menu-item>
</mdui-select> </mdui-select>
</div> </div>
<div> <div>
<mdui-select data-i18n="sort-order" label="Sort Order" name="sort-type" id="sort-type" value="asc"> <mdui-select label="排序方式" name="sort-type" id="sort-type" value="asc">
<mdui-menu-item value="asc" selected data-i18n="ascending">Ascending</mdui-menu-item> <mdui-menu-item value="asc" selected>升序</mdui-menu-item>
<mdui-menu-item value="desc" data-i18n="descending">Descending</mdui-menu-item> <mdui-menu-item value="desc">降序</mdui-menu-item>
</mdui-select> </mdui-select>
</div> </div>
</div> </div>
<!-- Result Section --> <!-- Result Section -->
<div class="section"> <div class="section">
<mdui-text-field data-i18n="generate-link" label="Generate Link" type="text" id="output" name="output"> <mdui-text-field label="生成链接" autosize min-rows="2" max-rows="5" id="output" name="output"></mdui-text-field>
<!-- Integrated copy icon inside the Generate Link text field -->
<mdui-icon slot="end-icon" onclick="copyLink()" style="cursor:pointer;">content_copy</mdui-icon>
</mdui-text-field>
<mdui-button color="primary" onclick="openProfileLink()">
<span data-i18n="import-profile">Import Profile</span>
</mdui-button>
</div> </div>
</div> </div>
</div> </div>
<script> <script>
// Translation object (v11 parameters) init();
const translations = {
"zh-CN": {
"template": "模板",
"nodes": "节点",
"subscription-link": "订阅链接",
"proxy-link": "节点分享链接",
"delete-node": "删除节点",
"rename-node": "重命名节点",
"policy-group": "策略组",
"type": "类型",
"sort-by": "排序依据",
"node-name": "节点名",
"node-count": "节点数量",
"sort-order": "排序方式",
"ascending": "升序",
"descending": "降序",
"generate-link": "生成链接",
"theme-light": "亮色",
"theme-dark": "暗色",
"theme-auto": "跟随系统",
"import-profile": "导入配置",
"one-per-line": "一行一个",
"supports-regex": "支持正则表达式"
},
"en": {
"template": "Template",
"nodes": "Nodes",
"subscription-link": "Subscription Link",
"proxy-link": "Node Share Link",
"delete-node": "Delete Node",
"rename-node": "Rename Node",
"policy-group": "Policy Group",
"type": "Type",
"sort-by": "Sort By",
"node-name": "Node Name",
"node-count": "Node Count",
"sort-order": "Sort Order",
"ascending": "Ascending",
"descending": "Descending",
"generate-link": "Generate Link",
"theme-light": "Light",
"theme-dark": "Dark",
"theme-auto": "Auto",
"import-profile": "Import Profile",
"one-per-line": "One per line",
"supports-regex": "Supports regex"
},
"ru": {
"template": "Шаблон",
"nodes": "Узлы",
"subscription-link": "Ссылка подписки",
"proxy-link": "Ссылка на узел",
"delete-node": "Удалить узел",
"rename-node": "Переименовать узел",
"policy-group": "Группа политик",
"type": "Тип",
"sort-by": "Сортировать по",
"node-name": "Имя узла",
"node-count": "Количество узлов",
"sort-order": "Порядок сортировки",
"ascending": "По возрастанию",
"descending": "По убыванию",
"generate-link": "Создать ссылку",
"theme-light": "Светлая",
"theme-dark": "Темная",
"theme-auto": "Системная",
"import-profile": "Импортировать профиль",
"one-per-line": "Одна на строку",
"supports-regex": "Поддерживает рег. выражения"
}
};
document.addEventListener('DOMContentLoaded', function() { function init() {
setupLanguage(); theme();
setupTheme();
listenInput(); listenInput();
// Do not auto-generate link on page load to keep Generate Link empty
});
function setupLanguage() {
const languageMenu = document.getElementById('language');
if (languageMenu) {
const userLang = navigator.language || navigator.userLanguage;
const savedLang = localStorage.getItem('language') ||
(userLang.includes('zh') ? 'zh-CN' : userLang.includes('ru') ? 'ru' : 'en');
languageMenu.value = savedLang;
changeLanguage(savedLang);
languageMenu.addEventListener('change', function() {
changeLanguage(languageMenu.value);
});
}
} }
function changeLanguage(lang) { function theme() {
localStorage.setItem('language', lang); const theme = window.localStorage.getItem("theme");
document.documentElement.setAttribute('lang', lang); if (theme) {
document.querySelector("html").className = "mdui-theme-" + theme;
document.querySelectorAll('[data-i18n]').forEach(element => { document.getElementById("theme").value = theme;
const key = element.getAttribute('data-i18n');
const translation = translations[lang][key];
if (translation) {
if (element.tagName.toLowerCase() === 'mdui-text-field' ||
element.tagName.toLowerCase() === 'mdui-select') {
element.setAttribute('label', translation);
} else { } else {
element.textContent = translation; document.querySelector("html").className = "mdui-theme-auto";
document.getElementById("theme").value = "auto";
window.localStorage.setItem("theme", "auto");
} }
const icon = document.getElementById("theme");
const html = document.querySelector("html");
icon.addEventListener("change", function (e) {
switch (icon.value) {
case "light":
html.className = "mdui-theme-light";
window.localStorage.setItem("theme", "light");
break;
case "dark":
html.className = "mdui-theme-dark";
window.localStorage.setItem("theme", "dark");
break;
case "auto":
html.className = "mdui-theme-auto";
window.localStorage.setItem("theme", "auto");
break;
} }
});
document.querySelectorAll('mdui-text-field').forEach(element => {
const phKey = element.getAttribute('data-i18n-placeholder');
if (phKey && translations[lang][phKey]) {
element.setAttribute('placeholder', translations[lang][phKey]);
}
}); });
} }
function setupTheme() { function encodeBase64(str) {
const themeMenu = document.getElementById('theme'); return btoa(
const savedTheme = localStorage.getItem("theme") || "auto"; encodeURIComponent(str).replace(
document.documentElement.className = "mdui-theme-" + savedTheme; /%([0-9A-F]{2})/g,
themeMenu.value = savedTheme; function (match, p1) {
return String.fromCharCode("0x" + p1);
}
)
)
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
themeMenu.addEventListener("change", function() { function decodeBase64(str) {
const newTheme = themeMenu.value; return decodeURIComponent(
document.documentElement.className = "mdui-theme-" + newTheme; Array.prototype.map
localStorage.setItem("theme", newTheme); .call(atob(str), function (c) {
}); return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
})
.join("")
);
} }
function listenInput() { function listenInput() {
const inputs = document.querySelectorAll("mdui-text-field, mdui-select, mdui-checkbox"); const inputs = document.querySelectorAll(
inputs.forEach(input => { "mdui-checkbox, mdui-text-field"
);
for (let input of inputs) {
input.addEventListener("input", generateLink); input.addEventListener("input", generateLink);
input.addEventListener("change", generateLink); }
}); const selects = document.querySelectorAll("mdui-select");
for (let select of selects) {
select.addEventListener("change", generateLink);
}
} }
function cleanListeners() { function cleanLisnter() {
const inputs = document.querySelectorAll("#form input, #form textarea"); const inputs = document.querySelectorAll("#form input, #form textarea");
inputs.forEach(input => { for (let input of inputs) {
input.removeEventListener("input", generateLink); input.removeEventListener("input", generateLink);
}); }
const selects = document.querySelectorAll("#form select"); const selects = document.querySelectorAll("#form select");
selects.forEach(select => { for (let select of selects) {
select.removeEventListener("change", generateLink); select.removeEventListener("change", generateLink);
}); }
} }
// Rename field parameters from v11 - fields are now wide (stacked)
function addRenameField() { function addRenameField() {
cleanListeners(); cleanLisnter();
const container = document.getElementById("list"); const container = document.getElementById("list");
const currentLang = localStorage.getItem('language') || 'en'; const fieldHTML = `<mdui-list-item class="rename-group">
const fieldHTML = ` <mdui-text-field slot="icon" type="text" name="rename_from[]" placeholder="支持正则表达式" label="原字符"></mdui-text-field>
<mdui-list-item class="rename-group"> <mdui-text-field type="text" name="rename_to[]" label="替换字符"></mdui-text-field>
<mdui-text-field type="text" name="rename_from[]" data-i18n-placeholder="delete-node" <mdui-button slot="end-icon" type="button" class="btn btn-danger" onclick="removeThisField(this)">-</mdui-button>
label="${translations[currentLang]['delete-node']}" data-i18n="rename-node">
</mdui-text-field>
<mdui-text-field type="text" name="rename_to[]"
label="${translations[currentLang]['rename-node']}" data-i18n="rename-node">
</mdui-text-field>
<mdui-button slot="end-icon" type="button" onclick="removeThisField(this)">-</mdui-button>
</mdui-list-item>`; </mdui-list-item>`;
container.insertAdjacentHTML("beforeend", fieldHTML); container.insertAdjacentHTML("beforeend", fieldHTML);
listenInput(); listenInput();
} }
function removeThisField(button) { function removeThisField(button) {
cleanListeners(); cleanLisnter();
button.parentElement.remove(); button.parentElement.remove();
generateLink(); generateLink();
listenInput(); listenInput();
} }
function generateLink() { function generateLink() {
const subscription = document.getElementById("subscription").value const subscription = document
.split("\n").filter(i => i.trim()); .getElementById("subscription")
const proxy = document.getElementById("proxy").value .value.split("\n")
.split("\n").filter(i => i.trim()); .filter((i) => i);
const proxy = document
.getElementById("proxy")
.value.split("\n")
.filter((i) => i);
const deleteRule = document.getElementById("delete").value; const deleteRule = document.getElementById("delete").value;
const template = document.getElementById("template").value; const template = document.getElementById("template").value;
const renameFrom = Array.from(document.getElementsByName("rename_from[]")).map(input => input.value); const renameFrom = Array.from(
const renameTo = Array.from(document.getElementsByName("rename_to[]")).map(input => input.value); document.getElementsByName("rename_from[]")
).map((input) => input.value);
const renameTo = Array.from(
document.getElementsByName("rename_to[]")
).map((input) => input.value);
const output = document.getElementById("output"); const output = document.getElementById("output");
const groupType = document.getElementById("group-type").value; const groupType = document.getElementById("group-type").value;
const sort = document.getElementById("sort").value; const sort = document.getElementById("sort").value;
@ -351,26 +247,6 @@
rename[renameFrom[i]] = renameTo[i]; rename[renameFrom[i]] = renameTo[i];
} }
} }
// Only generate link if there is some user input (subscription, proxy, delete, or rename)
if (
subscription.length === 0 &&
proxy.length === 0 &&
deleteRule.trim() === "" &&
Object.keys(rename).length === 0
) {
output.value = "";
return;
}
// Determine base URL using window.location.origin and window.location.pathname
let basePath = window.location.pathname;
// Remove trailing slash if exists
if(basePath.endsWith('/')){
basePath = basePath.slice(0, -1);
}
const baseURL = window.location.origin + basePath;
const data = { const data = {
subscription, subscription,
proxy, proxy,
@ -379,43 +255,14 @@
rename, rename,
"group-type": groupType, "group-type": groupType,
sort, sort,
"sort-type": sortType "sort-type": sortType,
}; };
output.value = baseURL + "/convert?data=" + encodeBase64(JSON.stringify(data));
}
function encodeBase64(str) { output.value = `${window.location.origin}/convert?data=${encodeBase64(
return btoa( JSON.stringify(data)
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) { )}`;
return String.fromCharCode("0x" + p1);
})
).replace(/\+/g, "-").replace(/\//g, "_");
}
function openProfileLink() {
const output = document.getElementById("output").value;
if (output) {
window.location.href = "sing-box://import-remote-profile?url=" + encodeURIComponent(output);
} else {
alert("Output link is empty");
}
}
// Integrated copy function for the Generate Link field
function copyLink() {
const output = document.getElementById("output").value;
if (output) {
navigator.clipboard.writeText(output)
.then(() => {
mdui.snackbar({ message: "Link copied!", position: "top" });
})
.catch(err => {
mdui.snackbar({ message: "Failed to copy link!", position: "top" });
});
} else {
mdui.snackbar({ message: "Output link is empty", position: "top" });
}
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -6,14 +6,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var port uint16
port uint16
bind string
)
func init() { func init() {
runCmd.Flags().Uint16VarP(&port, "port", "p", 8080, "server port") runCmd.Flags().Uint16VarP(&port, "port", "p", 8080, "server port")
runCmd.Flags().StringVarP(&bind, "bind", "b", "0.0.0.0", "bind address (e.g., 0.0.0.0, 127.0.0.1, localhost)")
RootCmd.AddCommand(runCmd) RootCmd.AddCommand(runCmd)
} }
@ -22,6 +18,6 @@ var runCmd = &cobra.Command{
Long: "Run the server", Long: "Run the server",
Short: "Run the server", Short: "Run the server",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
api.RunServer(bind, port) api.RunServer(port)
}, },
} }

View File

@ -38,10 +38,6 @@ func Convert(
result := "" result := ""
var err error var err error
if groupType == "" {
groupType = C.TypeSelector
}
outbounds, err := ConvertSubscriptionsToSProxy(subscriptions) outbounds, err := ConvertSubscriptionsToSProxy(subscriptions)
if err != nil { if err != nil {
return "", err return "", err
@ -120,15 +116,6 @@ func Convert(
if template, err = J.UnmarshalExtendedContext[model.Options](globalCtx, []byte(templateData)); err != nil { if template, err = J.UnmarshalExtendedContext[model.Options](globalCtx, []byte(templateData)); err != nil {
return "", err return "", err
} }
for _, v := range template.Options.Outbounds {
template.Outbounds = append(template.Outbounds, (model.Outbound)(v))
}
for _, v := range template.Options.Inbounds {
template.Inbounds = append(template.Inbounds, (model.Inbound)(v))
}
for _, v := range template.Options.Endpoints {
template.Endpoints = append(template.Endpoints, (model.Endpoint)(v))
}
result, err = MergeTemplate(outbounds, &template) result, err = MergeTemplate(outbounds, &template)
if err != nil { if err != nil {
return "", err return "", err
@ -160,7 +147,7 @@ func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string,
AppendOutbound(&group, p.Tag) AppendOutbound(&group, p.Tag)
newGroup[country] = group newGroup[country] = group
} else { } else {
if groupType == C.TypeSelector { if groupType == C.TypeSelector || groupType == "" {
newGroup[country] = model.Outbound{ newGroup[country] = model.Outbound{
Tag: country, Tag: country,
Type: groupType, Type: groupType,
@ -186,7 +173,6 @@ func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string,
for _, p := range newGroup { for _, p := range newGroup {
groups = append(groups, p) groups = append(groups, p)
} }
if sortType != "" {
if sortType == "asc" { if sortType == "asc" {
switch sortKey { switch sortKey {
case "tag": case "tag":
@ -206,7 +192,6 @@ func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string,
sort.Sort(sort.Reverse(model.SortByTag(groups))) sort.Sort(sort.Reverse(model.SortByTag(groups)))
} }
} }
}
return append(proxies, groups...) return append(proxies, groups...)
} }
@ -251,8 +236,7 @@ func MergeTemplate(outbounds []model.Outbound, template *model.Options) (string,
} }
} }
reg := regexp.MustCompile("<[A-Za-z]{2}>") reg := regexp.MustCompile("<[A-Za-z]{2}>")
for i, o := range template.Outbounds { for i, outbound := range template.Outbounds {
outbound := (model.Outbound)(o)
if outbound.Type == C.TypeSelector || outbound.Type == C.TypeURLTest { if outbound.Type == C.TypeSelector || outbound.Type == C.TypeURLTest {
var parsedOutbound []string = make([]string, 0) var parsedOutbound []string = make([]string, 0)
for _, o := range GetOutbounds(&outbound) { for _, o := range GetOutbounds(&outbound) {
@ -365,12 +349,6 @@ func SetOutbounds(outbound *model.Outbound, outbounds []string) {
case option.URLTestOutboundOptions: case option.URLTestOutboundOptions:
v.Outbounds = outbounds v.Outbounds = outbounds
outbound.Options = v outbound.Options = v
case *option.SelectorOutboundOptions:
v.Outbounds = outbounds
outbound.Options = v
case *option.URLTestOutboundOptions:
v.Outbounds = outbounds
outbound.Options = v
} }
} }
@ -382,12 +360,6 @@ func AppendOutbound(outbound *model.Outbound, outboundTag string) {
case option.URLTestOutboundOptions: case option.URLTestOutboundOptions:
v.Outbounds = append(v.Outbounds, outboundTag) v.Outbounds = append(v.Outbounds, outboundTag)
outbound.Options = v outbound.Options = v
case *option.SelectorOutboundOptions:
v.Outbounds = append(v.Outbounds, outboundTag)
outbound.Options = v
case *option.URLTestOutboundOptions:
v.Outbounds = append(v.Outbounds, outboundTag)
outbound.Options = v
} }
} }
@ -397,10 +369,6 @@ func GetOutbounds(outbound *model.Outbound) []string {
return v.Outbounds return v.Outbounds
case option.URLTestOutboundOptions: case option.URLTestOutboundOptions:
return v.Outbounds return v.Outbounds
case *option.SelectorOutboundOptions:
return v.Outbounds
case *option.URLTestOutboundOptions:
return v.Outbounds
} }
return nil return nil
} }

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/bin/bash
sing-box merge -C ./templates/windows ./templates/example-windows.json sing-box merge -C ./templates/windows ./templates/example-windows.json
sing-box merge -C ./templates/linux ./templates/example-linux.json sing-box merge -C ./templates/linux ./templates/example-linux.json
sing-box merge -C ./templates/windows ./templates/example-android.json sing-box merge -C ./templates/android ./templates/example-android.json

60
templates/android/dns.json Executable file
View File

@ -0,0 +1,60 @@
{
"dns": {
"servers": [
{
"tag": "google",
"address": "https://8.8.8.8/dns-query",
"detour": "default"
},
{
"tag": "local",
"address": "h3://223.5.5.5/dns-query",
"detour": "direct"
},
{
"tag": "refused",
"address": "rcode://refused"
}
],
"rules": [
{
"rule_set": "geosite-category-ads-all",
"server": "refused"
},
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"rule_set": "geosite-geolocation-cn",
"server": "local"
},
{
"type": "logical",
"mode": "and",
"rules": [
{
"rule_set": "geosite-geolocation-!cn",
"invert": true
},
{
"rule_set": "geoip-cn"
}
],
"server": "google",
"client_subnet": "114.114.114.114/24"
}
],
"final": "google",
"disable_cache": false,
"disable_expire": false
}
}

View File

@ -0,0 +1,13 @@
{
"experimental": {
"cache_file": {
"enabled": true
},
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "./ui",
"external_ui_download_detour": "default",
"default_mode": "Enhanced"
}
}
}

12
templates/android/inbounds.json Executable file
View File

@ -0,0 +1,12 @@
{
"inbounds": [
{
"type": "tun",
"auto_route": true,
"strict_route": true,
"sniff": true,
"address": ["172.16.0.1/30", "fdfe:dcba:9876::1/126"],
"exclude_interface": ["tailscale0"]
}
]
}

6
templates/android/log.json Executable file
View File

@ -0,0 +1,6 @@
{
"log": {
"level": "info",
"timestamp": true
}
}

8
templates/android/ntp.json Executable file
View File

@ -0,0 +1,8 @@
{
"ntp": {
"enabled": true,
"server": "ntp.aliyun.com",
"server_port": 123,
"interval": "30m"
}
}

View File

@ -0,0 +1,21 @@
{
"outbounds": [
{
"outbounds": ["<all-proxy-tags>", "direct"],
"tag": "default",
"type": "selector"
},
{
"tag": "direct",
"type": "direct"
},
{
"tag": "block",
"type": "block"
},
{
"tag": "dns",
"type": "dns"
}
]
}

53
templates/android/route.json Executable file
View File

@ -0,0 +1,53 @@
{
"route": {
"rules": [
{
"type": "logical",
"mode": "or",
"rules": [
{
"protocol": "dns"
},
{
"port": 53
}
],
"outbound": "dns"
},
{
"ip_is_private": true,
"outbound": "direct"
},
{
"clash_mode": "Direct",
"outbound": "direct"
},
{
"clash_mode": "Global",
"outbound": "default"
},
{
"rule_set": "geosite-google",
"outbound": "default"
},
{
"type": "logical",
"mode": "or",
"rules": [
{
"rule_set": [
"geoip-cn",
"geosite-geolocation-cn",
"geosite-cn",
"geosite-category-games@cn",
"geosite-steam@cn"
]
}
],
"outbound": "direct"
}
],
"final": "default",
"auto_detect_interface": true
}
}

View File

@ -0,0 +1,62 @@
{
"route": {
"rule_set": [
{
"type": "remote",
"tag": "geosite-category-ads-all",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-ads-all.srs",
"download_detour": "default"
},
{
"type": "remote",
"tag": "geosite-geolocation-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs",
"download_detour": "default"
},
{
"type": "remote",
"tag": "geosite-geolocation-!cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs",
"download_detour": "default"
},
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs",
"download_detour": "default"
},
{
"type": "remote",
"tag": "geosite-category-games@cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-category-games@cn.srs",
"download_detour": "default"
},
{
"type": "remote",
"tag": "geosite-steam@cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-steam@cn.srs",
"download_detour": "default"
},
{
"type": "remote",
"tag": "geosite-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs",
"download_detour": "default"
},
{
"type": "remote",
"tag": "geosite-google",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-google.srs",
"download_detour": "default"
}
]
}
}

View File

@ -90,29 +90,30 @@
{ {
"type": "direct", "type": "direct",
"tag": "direct" "tag": "direct"
},
{
"type": "block",
"tag": "block"
},
{
"type": "dns",
"tag": "dns"
} }
], ],
"route": { "route": {
"rules": [ "rules": [
{ {
"action": "resolve", "type": "logical",
"strategy": "prefer_ipv4" "mode": "or",
"rules": [
{
"protocol": "dns"
}, },
{ {
"action": "sniff", "port": 53
"timeout": "1s" }
},
{
"protocol": "dns",
"action": "hijack-dns"
},
{
"port": [
3478,
5348,
5349
], ],
"outbound": "direct" "outbound": "dns"
}, },
{ {
"ip_is_private": true, "ip_is_private": true,
@ -131,12 +132,9 @@
"outbound": "default" "outbound": "default"
}, },
{ {
"process_path_regex": [ "type": "logical",
"^.+/steam/compatibilitytools.d/.+", "mode": "or",
"^.+/steamapps/common/.*" "rules": [
],
"outbound": "direct"
},
{ {
"rule_set": [ "rule_set": [
"geoip-cn", "geoip-cn",
@ -144,6 +142,8 @@
"geosite-cn", "geosite-cn",
"geosite-category-games@cn", "geosite-category-games@cn",
"geosite-steam@cn" "geosite-steam@cn"
]
}
], ],
"outbound": "direct" "outbound": "direct"
} }
@ -216,7 +216,6 @@
"clash_api": { "clash_api": {
"external_controller": "127.0.0.1:9090", "external_controller": "127.0.0.1:9090",
"external_ui": "./ui", "external_ui": "./ui",
"external_ui_download_url": "https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip",
"external_ui_download_detour": "default", "external_ui_download_detour": "default",
"default_mode": "Enhanced" "default_mode": "Enhanced"
} }

View File

@ -68,15 +68,15 @@
"inbounds": [ "inbounds": [
{ {
"type": "tun", "type": "tun",
"tag": "tun-in",
"address": [ "address": [
"192.168.135.1/30", "172.16.0.1/30",
"fdfe:dcba:9876::1/126" "fdfe:dcba:9876::1/126"
], ],
"auto_route": true, "auto_route": true,
"auto_redirect": true, "auto_redirect": true,
"strict_route": true, "strict_route": true,
"route_exclude_address": "100.64.0.0/10" "exclude_interface": "tailscale0",
"sniff": true
} }
], ],
"outbounds": [ "outbounds": [
@ -91,29 +91,30 @@
{ {
"type": "direct", "type": "direct",
"tag": "direct" "tag": "direct"
},
{
"type": "block",
"tag": "block"
},
{
"type": "dns",
"tag": "dns"
} }
], ],
"route": { "route": {
"rules": [ "rules": [
{ {
"action": "resolve", "type": "logical",
"strategy": "prefer_ipv4" "mode": "or",
"rules": [
{
"protocol": "dns"
}, },
{ {
"action": "sniff", "port": 53
"timeout": "1s" }
},
{
"protocol": "dns",
"action": "hijack-dns"
},
{
"port": [
3478,
5348,
5349
], ],
"outbound": "direct" "outbound": "dns"
}, },
{ {
"ip_is_private": true, "ip_is_private": true,
@ -132,16 +133,9 @@
"outbound": "default" "outbound": "default"
}, },
{ {
"rule_set": "geosite-bahamut", "type": "logical",
"outbound": "台湾(TW)" "mode": "or",
}, "rules": [
{
"process_path_regex": [
"^.+/steam/compatibilitytools.d/.+",
"^.+/steamapps/common/.*"
],
"outbound": "direct"
},
{ {
"rule_set": [ "rule_set": [
"geoip-cn", "geoip-cn",
@ -149,6 +143,8 @@
"geosite-cn", "geosite-cn",
"geosite-category-games@cn", "geosite-category-games@cn",
"geosite-steam@cn" "geosite-steam@cn"
]
}
], ],
"outbound": "direct" "outbound": "direct"
} }
@ -221,7 +217,6 @@
"clash_api": { "clash_api": {
"external_controller": "127.0.0.1:9090", "external_controller": "127.0.0.1:9090",
"external_ui": "./ui", "external_ui": "./ui",
"external_ui_download_url": "https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip",
"external_ui_download_detour": "default", "external_ui_download_detour": "default",
"default_mode": "Enhanced" "default_mode": "Enhanced"
} }

View File

@ -90,29 +90,30 @@
{ {
"type": "direct", "type": "direct",
"tag": "direct" "tag": "direct"
},
{
"type": "block",
"tag": "block"
},
{
"type": "dns",
"tag": "dns"
} }
], ],
"route": { "route": {
"rules": [ "rules": [
{ {
"action": "resolve", "type": "logical",
"strategy": "prefer_ipv4" "mode": "or",
"rules": [
{
"protocol": "dns"
}, },
{ {
"action": "sniff", "port": 53
"timeout": "1s" }
},
{
"protocol": "dns",
"action": "hijack-dns"
},
{
"port": [
3478,
5348,
5349
], ],
"outbound": "direct" "outbound": "dns"
}, },
{ {
"ip_is_private": true, "ip_is_private": true,
@ -131,12 +132,9 @@
"outbound": "default" "outbound": "default"
}, },
{ {
"process_path_regex": [ "type": "logical",
"^.+/steam/compatibilitytools.d/.+", "mode": "or",
"^.+/steamapps/common/.*" "rules": [
],
"outbound": "direct"
},
{ {
"rule_set": [ "rule_set": [
"geoip-cn", "geoip-cn",
@ -144,6 +142,8 @@
"geosite-cn", "geosite-cn",
"geosite-category-games@cn", "geosite-category-games@cn",
"geosite-steam@cn" "geosite-steam@cn"
]
}
], ],
"outbound": "direct" "outbound": "direct"
} }
@ -216,7 +216,6 @@
"clash_api": { "clash_api": {
"external_controller": "127.0.0.1:9090", "external_controller": "127.0.0.1:9090",
"external_ui": "./ui", "external_ui": "./ui",
"external_ui_download_url": "https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip",
"external_ui_download_detour": "default", "external_ui_download_detour": "default",
"default_mode": "Enhanced" "default_mode": "Enhanced"
} }

View File

@ -37,6 +37,10 @@
"rule_set": "geosite-bahamut", "rule_set": "geosite-bahamut",
"outbound": "台湾(TW)" "outbound": "台湾(TW)"
}, },
{
"domain_suffix": "online-fix.me",
"outbound": "香港(HK)"
},
{ {
"process_path_regex": [ "process_path_regex": [
"^.+/steam/compatibilitytools.d/.+", "^.+/steam/compatibilitytools.d/.+",

View File

@ -6,7 +6,6 @@
"clash_api": { "clash_api": {
"external_controller": "127.0.0.1:9090", "external_controller": "127.0.0.1:9090",
"external_ui": "./ui", "external_ui": "./ui",
"external_ui_download_url": "https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip",
"external_ui_download_detour": "default", "external_ui_download_detour": "default",
"default_mode": "Enhanced" "default_mode": "Enhanced"
} }

View File

@ -8,6 +8,14 @@
{ {
"tag": "direct", "tag": "direct",
"type": "direct" "type": "direct"
},
{
"tag": "block",
"type": "block"
},
{
"tag": "dns",
"type": "dns"
} }
] ]
} }

View File

@ -2,20 +2,17 @@
"route": { "route": {
"rules": [ "rules": [
{ {
"action": "resolve", "type": "logical",
"strategy": "prefer_ipv4" "mode": "or",
"rules": [
{
"protocol": "dns"
}, },
{ {
"action": "sniff", "port": 53
"timeout": "1s" }
}, ],
{ "outbound": "dns"
"protocol": "dns",
"action": "hijack-dns"
},
{
"port": [3478, 5348, 5349],
"outbound": "direct"
}, },
{ {
"ip_is_private": true, "ip_is_private": true,
@ -34,16 +31,9 @@
"outbound": "default" "outbound": "default"
}, },
{ {
"rule_set": "geosite-bahamut", "type": "logical",
"outbound": "台湾(TW)" "mode": "or",
}, "rules": [
{
"process_path_regex": [
"^.+/steam/compatibilitytools.d/.+",
"^.+/steamapps/common/.*"
],
"outbound": "direct"
},
{ {
"rule_set": [ "rule_set": [
"geoip-cn", "geoip-cn",
@ -51,6 +41,8 @@
"geosite-cn", "geosite-cn",
"geosite-category-games@cn", "geosite-category-games@cn",
"geosite-steam@cn" "geosite-steam@cn"
]
}
], ],
"outbound": "direct" "outbound": "direct"
} }