add groupRules

This commit is contained in:
nite 2025-04-20 13:03:06 +10:00
parent 87e06ffc67
commit ed3d2e9e64
9 changed files with 116 additions and 74 deletions

View File

@ -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{

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en" class="mdui-theme-auto">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -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>* {
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 @@
}
</style>
</head>
<body>
<div class="mdui-container">
<div style="display: flex; align-items: center; justify-content: space-between;">
@ -90,23 +98,19 @@
<h3 data-i18n="nodes">Nodes</h3>
<div>
<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" name="subscription"
placeholder="One per line">
data-i18n-placeholder="one-per-line" label="Subscription Link" type="text" id="subscription"
name="subscription" placeholder="One per line">
</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"
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 data-i18n="delete-node" data-i18n-placeholder="supports-regex" label="Delete Node"
type="text" id="delete" name="delete" placeholder="Supports regex">
</mdui-text-field>
</div>
<div>
@ -123,8 +127,11 @@
<div class="section">
<h3 data-i18n="policy-group">Policy Group</h3>
<div>
<mdui-text-field data-i18n="type" label="Type" type="text" id="group-type" name="group-type"
value="selector">
<mdui-checkbox data-i18n="group" label="Group" name="group" id="group"
value="true">placeholder</mdui-checkbox>
</div>
<div>
<mdui-text-field data-i18n="type" label="Type" type="text" id="group-type" name="group-type" value="selector">
</mdui-text-field>
</div>
<div>
@ -139,6 +146,11 @@
<mdui-menu-item value="desc" data-i18n="descending">Descending</mdui-menu-item>
</mdui-select>
</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>
<!-- Result Section -->
<div class="section">
@ -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,7 +240,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": "Включить группу политик"
}
};
@ -259,6 +280,8 @@
if (element.tagName.toLowerCase() === 'mdui-text-field' ||
element.tagName.toLowerCase() === 'mdui-select') {
element.setAttribute('label', translation);
} else if (element.tagName.toLowerCase() === 'mdui-checkbox') {
element.innerHTML = translation;
} else {
element.textContent = translation;
}
@ -344,6 +367,8 @@
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++) {
@ -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 @@
}
</script>
</body>
</html>

View File

@ -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)

View File

@ -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)

View File

@ -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"`
}

View File

@ -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"`

View File

@ -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"
},
{

View File

@ -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"
},
{

View File

@ -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"
},
{