mirror of
https://github.com/bestnite/sub2sing-box.git
synced 2025-05-03 01:22:02 +08:00
add groupRules
This commit is contained in:
parent
87e06ffc67
commit
ed3d2e9e64
@ -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{
|
||||
|
@ -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 > * {
|
||||
|
||||
.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;">
|
||||
@ -89,24 +97,20 @@
|
||||
<div class="section">
|
||||
<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"
|
||||
<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">
|
||||
</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">
|
||||
</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">
|
||||
</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,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 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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"`
|
||||
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user