Compare commits

..

No commits in common. "ee01f7f61c514333ae381a2f65ff3b193976253c" and "87e06ffc673f8e25f26e229de2da7f84b4723dc6" have entirely different histories.

9 changed files with 77 additions and 119 deletions

View File

@ -39,13 +39,6 @@ func Convert(c *gin.Context) {
}) })
return return
} }
groupRules := make(map[string][]string)
err = json.Unmarshal([]byte(data.GroupRules), &groupRules)
if err != nil {
c.JSON(400, gin.H{
"error": err.Error(),
})
}
result, err := common.Convert( result, err := common.Convert(
data.Subscriptions, data.Subscriptions,
data.Proxies, data.Proxies,
@ -56,7 +49,6 @@ func Convert(c *gin.Context) {
data.GroupType, data.GroupType,
data.SortKey, data.SortKey,
data.SortType, data.SortType,
groupRules,
) )
if err != nil { if err != nil {
c.JSON(400, gin.H{ c.JSON(400, gin.H{

View File

@ -1,6 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" class="mdui-theme-auto"> <html lang="en" 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" />
@ -17,11 +16,9 @@
body { body {
font-family: "Roboto", "Noto Sans SC", sans-serif; 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;
@ -29,23 +26,19 @@
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 { .header-controls {
display: flex; display: flex;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
} }
/* Rename fields: now stacked in a wide layout */ /* Rename fields: now stacked in a wide layout */
.rename-group { .rename-group {
display: flex; display: flex;
@ -54,12 +47,11 @@
} }
</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/bestnite/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"> <div class="header-controls">
<!-- Language switcher --> <!-- Language switcher -->
@ -86,10 +78,10 @@
<!-- Template Section --> <!-- Template Section -->
<div class="section"> <div class="section">
<h3> <h3>
<a href="https://github.com/bestnite/sub2sing-box/tree/master/templates" data-i18n="template">Template</a> <a href="https://github.com/nitezs/sub2sing-box/tree/master/templates" data-i18n="template">Template</a>
</h3> </h3>
<mdui-text-field data-i18n="template" label="Template" type="text" id="template" name="template" <mdui-text-field data-i18n="template" label="Template" type="text" id="template" name="template"
value="https://raw.githubusercontent.com/bestnite/sub2sing-box/refs/heads/master/templates/example-windows.json"> value="https://raw.githubusercontent.com/nitezs/sub2sing-box/refs/heads/master/templates/example-windows.json">
</mdui-text-field> </mdui-text-field>
</div> </div>
<div id="form"> <div id="form">
@ -97,20 +89,24 @@
<div class="section"> <div class="section">
<h3 data-i18n="nodes">Nodes</h3> <h3 data-i18n="nodes">Nodes</h3>
<div> <div>
<mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="subscription-link" <mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="subscription-link"
data-i18n-placeholder="one-per-line" label="Subscription Link" type="text" id="subscription" data-i18n-placeholder="one-per-line"
name="subscription" placeholder="One per line"> label="Subscription Link" type="text" id="subscription" name="subscription"
</mdui-text-field>
</div>
<div>
<mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="proxy-link"
data-i18n-placeholder="one-per-line" label="Node Share Link" type="text" id="proxy" name="proxy"
placeholder="One per line"> placeholder="One per line">
</mdui-text-field> </mdui-text-field>
</div> </div>
<div> <div>
<mdui-text-field data-i18n="delete-node" data-i18n-placeholder="supports-regex" label="Delete Node" <mdui-text-field autosize min-rows="3" max-rows="5" data-i18n="proxy-link"
type="text" id="delete" name="delete" placeholder="Supports regex"> data-i18n-placeholder="one-per-line"
label="Node Share Link" type="text" id="proxy" name="proxy"
placeholder="One per line">
</mdui-text-field>
</div>
<div>
<mdui-text-field data-i18n="delete-node"
data-i18n-placeholder="supports-regex"
label="Delete Node" type="text" id="delete" name="delete"
placeholder="Supports regex">
</mdui-text-field> </mdui-text-field>
</div> </div>
<div> <div>
@ -127,11 +123,8 @@
<div class="section"> <div class="section">
<h3 data-i18n="policy-group">Policy Group</h3> <h3 data-i18n="policy-group">Policy Group</h3>
<div> <div>
<mdui-checkbox data-i18n="group" label="Group" name="group" id="group" <mdui-text-field data-i18n="type" label="Type" type="text" id="group-type" name="group-type"
value="true">placeholder</mdui-checkbox> value="selector">
</div>
<div>
<mdui-text-field data-i18n="type" label="Type" type="text" id="group-type" name="group-type" value="selector">
</mdui-text-field> </mdui-text-field>
</div> </div>
<div> <div>
@ -146,11 +139,6 @@
<mdui-menu-item value="desc" data-i18n="descending">Descending</mdui-menu-item> <mdui-menu-item value="desc" data-i18n="descending">Descending</mdui-menu-item>
</mdui-select> </mdui-select>
</div> </div>
<div>
<mdui-text-field autosize data-i18n="group-rules" label="Group Rules" type="text" id="group-rules"
name="group-rules" min-rows="3" max-rows="5" data-i18n-placeholder="group-rules-placeholder">
</mdui-text-field>
</div>
</div> </div>
<!-- Result Section --> <!-- Result Section -->
<div class="section"> <div class="section">
@ -188,10 +176,7 @@
"theme-auto": "跟随系统", "theme-auto": "跟随系统",
"import-profile": "导入配置", "import-profile": "导入配置",
"one-per-line": "一行一个", "one-per-line": "一行一个",
"supports-regex": "支持正则表达式", "supports-regex": "支持正则表达式"
"group-rules": "策略组规则",
"group-rules-placeholder": "Json格式: {\"US\": [\"US-CA\", \"US-TX\"], \"CN\": [\"CN-BJ\", \"CN-SH\"]}, 支持正则表达式",
"group": "启用策略组"
}, },
"en": { "en": {
"template": "Template", "template": "Template",
@ -214,10 +199,7 @@
"theme-auto": "Auto", "theme-auto": "Auto",
"import-profile": "Import Profile", "import-profile": "Import Profile",
"one-per-line": "One per line", "one-per-line": "One per line",
"supports-regex": "Supports regex", "supports-regex": "Supports regex"
"group-rules": "Group Rules",
"group-rules-placeholder": "Json format: {\"US\": [\"US-CA\", \"US-TX\"], \"CN\": [\"CN-BJ\", \"CN-SH\"]}, support regex",
"group": "Enable Policy Group"
}, },
"ru": { "ru": {
"template": "Шаблон", "template": "Шаблон",
@ -240,14 +222,11 @@
"theme-auto": "Системная", "theme-auto": "Системная",
"import-profile": "Импортировать профиль", "import-profile": "Импортировать профиль",
"one-per-line": "Одна на строку", "one-per-line": "Одна на строку",
"supports-regex": "Поддерживает рег. выражения", "supports-regex": "Поддерживает рег. выражения"
"group-rules": "Группа правил",
"group-rules-placeholder": "Формат JSON: {\"US\": [\"US-CA\", \"US-TX\"], \"CN\": [\"CN-BJ\", \"CN-SH\"]}, поддержка регулярных выражений",
"group": "Включить группу политик"
} }
}; };
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function() {
setupLanguage(); setupLanguage();
setupTheme(); setupTheme();
listenInput(); listenInput();
@ -263,7 +242,7 @@
languageMenu.value = savedLang; languageMenu.value = savedLang;
changeLanguage(savedLang); changeLanguage(savedLang);
languageMenu.addEventListener('change', function () { languageMenu.addEventListener('change', function() {
changeLanguage(languageMenu.value); changeLanguage(languageMenu.value);
}); });
} }
@ -278,10 +257,8 @@
const translation = translations[lang][key]; const translation = translations[lang][key];
if (translation) { if (translation) {
if (element.tagName.toLowerCase() === 'mdui-text-field' || if (element.tagName.toLowerCase() === 'mdui-text-field' ||
element.tagName.toLowerCase() === 'mdui-select') { element.tagName.toLowerCase() === 'mdui-select') {
element.setAttribute('label', translation); element.setAttribute('label', translation);
} else if (element.tagName.toLowerCase() === 'mdui-checkbox') {
element.innerHTML = translation;
} else { } else {
element.textContent = translation; element.textContent = translation;
} }
@ -302,7 +279,7 @@
document.documentElement.className = "mdui-theme-" + savedTheme; document.documentElement.className = "mdui-theme-" + savedTheme;
themeMenu.value = savedTheme; themeMenu.value = savedTheme;
themeMenu.addEventListener("change", function () { themeMenu.addEventListener("change", function() {
const newTheme = themeMenu.value; const newTheme = themeMenu.value;
document.documentElement.className = "mdui-theme-" + newTheme; document.documentElement.className = "mdui-theme-" + newTheme;
localStorage.setItem("theme", newTheme); localStorage.setItem("theme", newTheme);
@ -367,16 +344,14 @@
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;
const sortType = document.getElementById("sort-type").value; const sortType = document.getElementById("sort-type").value;
const groupRules = document.getElementById("group-rules").value;
const group = document.getElementById("group").value;
let rename = {}; let rename = {};
for (let i = 0; i < renameFrom.length; i++) { for (let i = 0; i < renameFrom.length; i++) {
if (renameFrom[i] && renameTo[i]) { if (renameFrom[i] && renameTo[i]) {
rename[renameFrom[i]] = renameTo[i]; rename[renameFrom[i]] = renameTo[i];
} }
} }
// Only generate link if there is some user input (subscription, proxy, delete, or rename) // Only generate link if there is some user input (subscription, proxy, delete, or rename)
if ( if (
subscription.length === 0 && subscription.length === 0 &&
@ -387,15 +362,15 @@
output.value = ""; output.value = "";
return; return;
} }
// Determine base URL using window.location.origin and window.location.pathname // Determine base URL using window.location.origin and window.location.pathname
let basePath = window.location.pathname; let basePath = window.location.pathname;
// Remove trailing slash if exists // Remove trailing slash if exists
if (basePath.endsWith('/')) { if(basePath.endsWith('/')){
basePath = basePath.slice(0, -1); basePath = basePath.slice(0, -1);
} }
const baseURL = window.location.origin + basePath; const baseURL = window.location.origin + basePath;
const data = { const data = {
subscription, subscription,
proxy, proxy,
@ -404,11 +379,17 @@
rename, rename,
"group-type": groupType, "group-type": groupType,
sort, sort,
"sort-type": sortType, "sort-type": sortType
"group-rules": groupRules,
group: group === "true"
}; };
output.value = baseURL + "/convert?data=" + btoa(unescape(encodeURIComponent(JSON.stringify(data)))).replace(/\+/g, "-").replace(/\//g, "_"); output.value = baseURL + "/convert?data=" + encodeBase64(JSON.stringify(data));
}
function encodeBase64(str) {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode("0x" + p1);
})
).replace(/\+/g, "-").replace(/\//g, "_");
} }
function openProfileLink() { function openProfileLink() {
@ -437,5 +418,4 @@
} }
</script> </script>
</body> </body>
</html>
</html>

View File

@ -24,7 +24,6 @@ var (
sortKey string sortKey string
sortType string sortType string
config string config string
groupRules string
) )
func init() { func init() {
@ -39,7 +38,6 @@ func init() {
convertCmd.Flags().StringVarP(&sortKey, "sort", "S", "", "sort key, tag or num") convertCmd.Flags().StringVarP(&sortKey, "sort", "S", "", "sort key, tag or num")
convertCmd.Flags().StringVarP(&sortType, "sort-type", "T", "", "sort type, asc or desc") convertCmd.Flags().StringVarP(&sortType, "sort-type", "T", "", "sort type, asc or desc")
convertCmd.Flags().StringVarP(&config, "config", "c", "", "configuration file path") convertCmd.Flags().StringVarP(&config, "config", "c", "", "configuration file path")
convertCmd.Flags().StringVarP(&groupRules, "group-rules", "R", "", "group rules")
RootCmd.AddCommand(convertCmd) RootCmd.AddCommand(convertCmd)
} }
@ -52,14 +50,6 @@ var convertCmd = &cobra.Command{
func convertRun(cmd *cobra.Command, args []string) { func convertRun(cmd *cobra.Command, args []string) {
loadConfig() loadConfig()
groupRulesMap := make(map[string][]string)
if groupRules != "" {
err := json.Unmarshal([]byte(groupRules), &groupRulesMap)
if err != nil {
fmt.Println("Error parsing group rules:", err)
return
}
}
result, err := common.Convert( result, err := common.Convert(
subscriptions, subscriptions,
proxies, proxies,
@ -70,7 +60,6 @@ func convertRun(cmd *cobra.Command, args []string) {
groupType, groupType,
sortKey, sortKey,
sortType, sortType,
groupRulesMap,
) )
if err != nil { if err != nil {
fmt.Println("Conversion error:", err) fmt.Println("Conversion error:", err)

View File

@ -8,7 +8,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices"
"sort" "sort"
"strings" "strings"
@ -35,7 +34,6 @@ func Convert(
groupType string, groupType string,
sortKey string, sortKey string,
sortType string, sortType string,
groupRules map[string][]string,
) (string, error) { ) (string, error) {
result := "" result := ""
var err error var err error
@ -101,7 +99,7 @@ func Convert(
} }
if enableGroup { if enableGroup {
outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType, groupRules) outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType)
} }
if templatePath != "" { if templatePath != "" {
templateData, err := ReadTemplate(templatePath) templateData, err := ReadTemplate(templatePath)
@ -116,7 +114,7 @@ func Convert(
} }
} }
if !enableGroup && (reg.MatchString(templateData) || strings.Contains(templateData, constant.AllCountryTags) || group) { if !enableGroup && (reg.MatchString(templateData) || strings.Contains(templateData, constant.AllCountryTags) || group) {
outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType, groupRules) outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType)
} }
var template model.Options var template model.Options
if template, err = J.UnmarshalExtendedContext[model.Options](globalCtx, []byte(templateData)); err != nil { if template, err = J.UnmarshalExtendedContext[model.Options](globalCtx, []byte(templateData)); err != nil {
@ -131,7 +129,7 @@ func Convert(
for _, v := range template.Options.Endpoints { for _, v := range template.Options.Endpoints {
template.Endpoints = append(template.Endpoints, (model.Endpoint)(v)) template.Endpoints = append(template.Endpoints, (model.Endpoint)(v))
} }
result, err = MergeTemplate(outbounds, &template, groupRules) result, err = MergeTemplate(outbounds, &template)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -153,25 +151,11 @@ func Convert(
return string(result), nil return string(result), nil
} }
func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string, groupRules map[string][]string) []model.Outbound { func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string) []model.Outbound {
newGroup := make(map[string]model.Outbound) newGroup := make(map[string]model.Outbound)
groupRulesRegexps := make(map[string][]*regexp.Regexp)
for k, v := range groupRules {
for _, rule := range v {
groupRulesRegexps[k] = append(groupRulesRegexps[k], regexp.MustCompile(rule))
}
}
for _, p := range proxies { for _, p := range proxies {
if p.Type != C.TypeSelector && p.Type != C.TypeURLTest { if p.Type != C.TypeSelector && p.Type != C.TypeURLTest {
country := model.GetContryName(p.Tag) country := model.GetContryName(p.Tag)
for k, rules := range groupRulesRegexps {
for _, rule := range rules {
if rule.MatchString(p.Tag) {
country = k
break
}
}
}
if group, ok := newGroup[country]; ok { if group, ok := newGroup[country]; ok {
AppendOutbound(&group, p.Tag) AppendOutbound(&group, p.Tag)
newGroup[country] = group newGroup[country] = group
@ -251,17 +235,13 @@ func ReadTemplate(template string) (string, error) {
} }
} }
func MergeTemplate(outbounds []model.Outbound, template *model.Options, groupRules map[string][]string) (string, error) { func MergeTemplate(outbounds []model.Outbound, template *model.Options) (string, error) {
var err error var err error
proxyTags := make([]string, 0) proxyTags := make([]string, 0)
groupTags := make([]string, 0) groupTags := make([]string, 0)
groups := make(map[string]model.Outbound) groups := make(map[string]model.Outbound)
rulesKeys := make([]string, 0)
for k := range groupRules {
rulesKeys = append(rulesKeys, k)
}
for _, p := range outbounds { for _, p := range outbounds {
if slices.Contains(rulesKeys, p.Tag) || model.IsCountryGroup(p.Tag) { if model.IsCountryGroup(p.Tag) {
groupTags = append(groupTags, p.Tag) groupTags = append(groupTags, p.Tag)
reg := regexp.MustCompile("[A-Za-z]{2}") reg := regexp.MustCompile("[A-Za-z]{2}")
country := reg.FindString(p.Tag) country := reg.FindString(p.Tag)

View File

@ -11,5 +11,4 @@ type ConvertRequest struct {
SortKey string `form:"sort" json:"sort"` SortKey string `form:"sort" json:"sort"`
SortType string `form:"sort-type" json:"sort-type"` SortType string `form:"sort-type" json:"sort-type"`
Output string `json:"output"` Output string `json:"output"`
GroupRules string `form:"group-rules" json:"group-rules"`
} }

View File

@ -1,7 +1,7 @@
package model package model
type VmessJson struct { type VmessJson struct {
V any `json:"v"` V string `json:"v"`
Ps string `json:"ps"` Ps string `json:"ps"`
Add string `json:"add"` Add string `json:"add"`
Port any `json:"port"` Port any `json:"port"`

View File

@ -68,7 +68,10 @@
"inbounds": [ "inbounds": [
{ {
"type": "tun", "type": "tun",
"address": ["172.16.0.1/30", "fdfe:dcba:9876::1/126"], "address": [
"172.16.0.1/30",
"fdfe:dcba:9876::1/126"
],
"auto_route": true, "auto_route": true,
"strict_route": true, "strict_route": true,
"exclude_interface": "tailscale0", "exclude_interface": "tailscale0",
@ -81,7 +84,6 @@
"tag": "default", "tag": "default",
"outbounds": [ "outbounds": [
"\u003call-proxy-tags\u003e", "\u003call-proxy-tags\u003e",
"\u003call-country-tags\u003e",
"direct" "direct"
] ]
}, },
@ -105,7 +107,11 @@
"action": "hijack-dns" "action": "hijack-dns"
}, },
{ {
"port": [3478, 5348, 5349], "port": [
3478,
5348,
5349
],
"outbound": "direct" "outbound": "direct"
}, },
{ {

View File

@ -69,7 +69,10 @@
{ {
"type": "tun", "type": "tun",
"tag": "tun-in", "tag": "tun-in",
"address": ["192.168.135.1/30", "fdfe:dcba:9876::1/126"], "address": [
"192.168.135.1/30",
"fdfe:dcba:9876::1/126"
],
"auto_route": true, "auto_route": true,
"auto_redirect": true, "auto_redirect": true,
"strict_route": true, "strict_route": true,
@ -82,7 +85,6 @@
"tag": "default", "tag": "default",
"outbounds": [ "outbounds": [
"\u003call-proxy-tags\u003e", "\u003call-proxy-tags\u003e",
"\u003call-country-tags\u003e",
"direct" "direct"
] ]
}, },
@ -106,7 +108,11 @@
"action": "hijack-dns" "action": "hijack-dns"
}, },
{ {
"port": [3478, 5348, 5349], "port": [
3478,
5348,
5349
],
"outbound": "direct" "outbound": "direct"
}, },
{ {

View File

@ -68,7 +68,10 @@
"inbounds": [ "inbounds": [
{ {
"type": "tun", "type": "tun",
"address": ["172.16.0.1/30", "fdfe:dcba:9876::1/126"], "address": [
"172.16.0.1/30",
"fdfe:dcba:9876::1/126"
],
"auto_route": true, "auto_route": true,
"strict_route": true, "strict_route": true,
"exclude_interface": "tailscale0", "exclude_interface": "tailscale0",
@ -81,7 +84,6 @@
"tag": "default", "tag": "default",
"outbounds": [ "outbounds": [
"\u003call-proxy-tags\u003e", "\u003call-proxy-tags\u003e",
"\u003call-country-tags\u003e",
"direct" "direct"
] ]
}, },
@ -105,7 +107,11 @@
"action": "hijack-dns" "action": "hijack-dns"
}, },
{ {
"port": [3478, 5348, 5349], "port": [
3478,
5348,
5349
],
"outbound": "direct" "outbound": "direct"
}, },
{ {