diff --git a/api/handler/convert.go b/api/handler/convert.go index 79a2fe7..4923655 100755 --- a/api/handler/convert.go +++ b/api/handler/convert.go @@ -39,6 +39,13 @@ func Convert(c *gin.Context) { }) 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( data.Subscriptions, data.Proxies, @@ -49,6 +56,7 @@ func Convert(c *gin.Context) { data.GroupType, data.SortKey, data.SortType, + groupRules, ) if err != nil { c.JSON(400, gin.H{ diff --git a/api/static/index.html b/api/static/index.html index 19bbd44..29f6147 100755 --- a/api/static/index.html +++ b/api/static/index.html @@ -1,5 +1,6 @@ + @@ -16,9 +17,11 @@ body { font-family: "Roboto", "Noto Sans SC", sans-serif; } + h3 { margin-top: 0; } + .section { margin: 20px 0; padding: 15px; @@ -26,19 +29,23 @@ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); background-color: rgb(var(--mdui-color-surface-container)); } - .section > * { + + .section>* { margin-bottom: 10px; } + .mdui-container { border-radius: 10px; max-width: 1200px; margin: 30px auto; } + .header-controls { display: flex; gap: 10px; align-items: center; } + /* Rename fields: now stacked in a wide layout */ .rename-group { display: flex; @@ -47,6 +54,7 @@ } +
@@ -89,24 +97,20 @@

Nodes

- + +
+
+
- - -
-
- +
@@ -123,8 +127,11 @@

Policy Group

- + placeholder +
+
+
@@ -139,6 +146,11 @@ Descending
+
+ + +
@@ -176,7 +188,10 @@ "theme-auto": "跟随系统", "import-profile": "导入配置", "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": { "template": "Template", @@ -199,7 +214,10 @@ "theme-auto": "Auto", "import-profile": "Import Profile", "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": { "template": "Шаблон", @@ -222,11 +240,14 @@ "theme-auto": "Системная", "import-profile": "Импортировать профиль", "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(); setupTheme(); listenInput(); @@ -242,7 +263,7 @@ languageMenu.value = savedLang; changeLanguage(savedLang); - languageMenu.addEventListener('change', function() { + languageMenu.addEventListener('change', function () { changeLanguage(languageMenu.value); }); } @@ -257,8 +278,10 @@ const translation = translations[lang][key]; if (translation) { if (element.tagName.toLowerCase() === 'mdui-text-field' || - element.tagName.toLowerCase() === 'mdui-select') { + element.tagName.toLowerCase() === 'mdui-select') { element.setAttribute('label', translation); + } else if (element.tagName.toLowerCase() === 'mdui-checkbox') { + element.innerHTML = translation; } else { element.textContent = translation; } @@ -279,7 +302,7 @@ document.documentElement.className = "mdui-theme-" + savedTheme; themeMenu.value = savedTheme; - themeMenu.addEventListener("change", function() { + themeMenu.addEventListener("change", function () { const newTheme = themeMenu.value; document.documentElement.className = "mdui-theme-" + newTheme; localStorage.setItem("theme", newTheme); @@ -344,14 +367,16 @@ const groupType = document.getElementById("group-type").value; const sort = document.getElementById("sort").value; const sortType = document.getElementById("sort-type").value; - + const groupRules = document.getElementById("group-rules").value; + const group = document.getElementById("group").value; + let rename = {}; for (let i = 0; i < renameFrom.length; i++) { if (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 && @@ -362,15 +387,15 @@ 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('/')){ + if (basePath.endsWith('/')) { basePath = basePath.slice(0, -1); } const baseURL = window.location.origin + basePath; - + const data = { subscription, proxy, @@ -379,17 +404,11 @@ rename, "group-type": groupType, sort, - "sort-type": sortType + "sort-type": sortType, + "group-rules": groupRules, + group: group === "true" }; - 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, "_"); + output.value = baseURL + "/convert?data=" + btoa(unescape(encodeURIComponent(JSON.stringify(data)))).replace(/\+/g, "-").replace(/\//g, "_"); } function openProfileLink() { @@ -418,4 +437,5 @@ } - + + \ No newline at end of file diff --git a/cmd/convert.go b/cmd/convert.go index e80a238..5460623 100755 --- a/cmd/convert.go +++ b/cmd/convert.go @@ -24,6 +24,7 @@ var ( sortKey string sortType string config string + groupRules string ) func init() { @@ -38,6 +39,7 @@ func init() { 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(&config, "config", "c", "", "configuration file path") + convertCmd.Flags().StringVarP(&groupRules, "group-rules", "R", "", "group rules") RootCmd.AddCommand(convertCmd) } @@ -50,6 +52,14 @@ var convertCmd = &cobra.Command{ func convertRun(cmd *cobra.Command, args []string) { 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( subscriptions, proxies, @@ -60,6 +70,7 @@ func convertRun(cmd *cobra.Command, args []string) { groupType, sortKey, sortType, + groupRulesMap, ) if err != nil { fmt.Println("Conversion error:", err) diff --git a/common/convert.go b/common/convert.go index e9713d1..7d9f768 100755 --- a/common/convert.go +++ b/common/convert.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "sort" "strings" @@ -34,6 +35,7 @@ func Convert( groupType string, sortKey string, sortType string, + groupRules map[string][]string, ) (string, error) { result := "" var err error @@ -99,7 +101,7 @@ func Convert( } if enableGroup { - outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType) + outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType, groupRules) } if templatePath != "" { templateData, err := ReadTemplate(templatePath) @@ -114,7 +116,7 @@ func Convert( } } if !enableGroup && (reg.MatchString(templateData) || strings.Contains(templateData, constant.AllCountryTags) || group) { - outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType) + outbounds = AddCountryGroup(outbounds, groupType, sortKey, sortType, groupRules) } var template model.Options if template, err = J.UnmarshalExtendedContext[model.Options](globalCtx, []byte(templateData)); err != nil { @@ -129,7 +131,7 @@ func Convert( for _, v := range template.Options.Endpoints { template.Endpoints = append(template.Endpoints, (model.Endpoint)(v)) } - result, err = MergeTemplate(outbounds, &template) + result, err = MergeTemplate(outbounds, &template, groupRules) if err != nil { return "", err } @@ -151,11 +153,25 @@ func Convert( return string(result), nil } -func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string) []model.Outbound { +func AddCountryGroup(proxies []model.Outbound, groupType string, sortKey string, sortType string, groupRules map[string][]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 { if p.Type != C.TypeSelector && p.Type != C.TypeURLTest { 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 { AppendOutbound(&group, p.Tag) newGroup[country] = group @@ -235,13 +251,17 @@ func ReadTemplate(template string) (string, error) { } } -func MergeTemplate(outbounds []model.Outbound, template *model.Options) (string, error) { +func MergeTemplate(outbounds []model.Outbound, template *model.Options, groupRules map[string][]string) (string, error) { var err error proxyTags := make([]string, 0) groupTags := make([]string, 0) groups := make(map[string]model.Outbound) + rulesKeys := make([]string, 0) + for k := range groupRules { + rulesKeys = append(rulesKeys, k) + } for _, p := range outbounds { - if model.IsCountryGroup(p.Tag) { + if slices.Contains(rulesKeys, p.Tag) || model.IsCountryGroup(p.Tag) { groupTags = append(groupTags, p.Tag) reg := regexp.MustCompile("[A-Za-z]{2}") country := reg.FindString(p.Tag) diff --git a/model/convert.go b/model/convert.go index 081ccce..4b88c20 100755 --- a/model/convert.go +++ b/model/convert.go @@ -11,4 +11,5 @@ type ConvertRequest struct { SortKey string `form:"sort" json:"sort"` SortType string `form:"sort-type" json:"sort-type"` Output string `json:"output"` + GroupRules string `form:"group-rules" json:"group-rules"` } diff --git a/model/vmess.go b/model/vmess.go index 0ee8cf2..2c8d624 100755 --- a/model/vmess.go +++ b/model/vmess.go @@ -1,7 +1,7 @@ package model type VmessJson struct { - V string `json:"v"` + V any `json:"v"` Ps string `json:"ps"` Add string `json:"add"` Port any `json:"port"` diff --git a/templates/example-android.json b/templates/example-android.json index 076dc0a..a8527a4 100755 --- a/templates/example-android.json +++ b/templates/example-android.json @@ -68,10 +68,7 @@ "inbounds": [ { "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, "strict_route": true, "exclude_interface": "tailscale0", @@ -84,6 +81,7 @@ "tag": "default", "outbounds": [ "\u003call-proxy-tags\u003e", + "\u003c-all-country-tags\u003e", "direct" ] }, @@ -107,11 +105,7 @@ "action": "hijack-dns" }, { - "port": [ - 3478, - 5348, - 5349 - ], + "port": [3478, 5348, 5349], "outbound": "direct" }, { diff --git a/templates/example-linux.json b/templates/example-linux.json index 5daaf5f..a0382b4 100755 --- a/templates/example-linux.json +++ b/templates/example-linux.json @@ -69,10 +69,7 @@ { "type": "tun", "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_redirect": true, "strict_route": true, @@ -85,6 +82,7 @@ "tag": "default", "outbounds": [ "\u003call-proxy-tags\u003e", + "\u003c-all-country-tags\u003e", "direct" ] }, @@ -108,11 +106,7 @@ "action": "hijack-dns" }, { - "port": [ - 3478, - 5348, - 5349 - ], + "port": [3478, 5348, 5349], "outbound": "direct" }, { diff --git a/templates/example-windows.json b/templates/example-windows.json index 076dc0a..a8527a4 100755 --- a/templates/example-windows.json +++ b/templates/example-windows.json @@ -68,10 +68,7 @@ "inbounds": [ { "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, "strict_route": true, "exclude_interface": "tailscale0", @@ -84,6 +81,7 @@ "tag": "default", "outbounds": [ "\u003call-proxy-tags\u003e", + "\u003c-all-country-tags\u003e", "direct" ] }, @@ -107,11 +105,7 @@ "action": "hijack-dns" }, { - "port": [ - 3478, - 5348, - 5349 - ], + "port": [3478, 5348, 5349], "outbound": "direct" }, {