mirror of
				https://github.com/bestnite/sub2clash.git
				synced 2025-11-04 04:40:36 +00:00 
			
		
		
		
	v0.0.5
feat: 增加节点去重 feat: 增加节点重命名 feat: 增加节点过滤 feat: 增加短链密码 modify: 修改模板解析逻辑,现在需要添加 <all>,<countries> 来让程序解析模板 modify: 修改短链请求逻辑,不再跳转链接,而是服务器内部请求 modify: 完善 Meta 默认模板 如果你从旧版升级,请务必修改或删除程序目录下的模板
This commit is contained in:
		
							
								
								
									
										36
									
								
								API_README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								API_README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					# `/clash`, `/meta`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					获取 Clash/Clash.Meta 配置链接
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Query 参数     | 类型     | 是否必须              | 默认值       | 说明                                                                                                                                                                          |
 | 
				
			||||||
 | 
					|--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | 
				
			||||||
 | 
					| sub          | string | sub/proxy 至少有一项存在 | -         | 订阅链接(可以输入多个,用 `,` 分隔)                                                                                                                                                       |
 | 
				
			||||||
 | 
					| proxy        | string | sub/proxy 至少有一项存在 | -         | 节点分享链接(可以输入多个,用 `,` 分隔)                                                                                                                                                     |
 | 
				
			||||||
 | 
					| refresh      | bool   | 否                 | `false`   | 强制刷新配置(默认缓存 5 分钟)                                                                                                                                                           |
 | 
				
			||||||
 | 
					| template     | string | 否                 | -         | 外部模板链接或内部模板名称                                                                                                                                                               |
 | 
				
			||||||
 | 
					| ruleProvider | string | 否                 | -         | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集使用的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) | 
 | 
				
			||||||
 | 
					| rule         | string | 否                 | -         | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前)                                                            | 
 | 
				
			||||||
 | 
					| autoTest     | bool   | 否                 | `false`   | 国家策略组是否自动测速                                                                                                                                                                 |
 | 
				
			||||||
 | 
					| lazy         | bool   | 否                 | `false`   | 自动测速是否启用 lazy                                                                                                                                                               |
 | 
				
			||||||
 | 
					| sort         | string | 否                 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc`                                                                                                                     |
 | 
				
			||||||
 | 
					| replace      | string | 否                 | -         | 通过正则表达式重命名节点,格式 `[<ReplaceKey>,<ReplaceTo>],[<ReplaceKey>,<ReplaceTo>]...`                                                                                                  |
 | 
				
			||||||
 | 
					| remove       | string | 否                 | -         | 通过正则表达式删除节点                                                                                                                                                                 |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# `/short`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					获取短链,Content-Type 为 `application/json`
 | 
				
			||||||
 | 
					具体参考使用可以参考 [api\templates\index.html](./api/templates/index.html)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Body 参数  | 类型     | 是否必须 | 默认值 | 说明               |
 | 
				
			||||||
 | 
					|----------|--------|------|-----|------------------|
 | 
				
			||||||
 | 
					| url      | string | 是    | -   | 需要转换的 Query 参数部分 |
 | 
				
			||||||
 | 
					| password | string | 否    | -   | 短链密码             |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# `/s/:hash`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					短链跳转
 | 
				
			||||||
 | 
					`hash` 为动态路由参数,可以通过 `/short` 接口获取
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Query 参数 | 类型     | 是否必须 | 默认值 | 说明   |
 | 
				
			||||||
 | 
					|----------|--------|------|-----|------|
 | 
				
			||||||
 | 
					| password | string | 否    | -   | 短链密码 |
 | 
				
			||||||
							
								
								
									
										24
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								README.md
									
									
									
									
									
								
							@@ -39,23 +39,17 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
### API
 | 
					### API
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### `/clash`, `/meta`
 | 
					[API文档](./API_README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
获取 Clash/Clash.Meta 配置链接
 | 
					### 模板
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Query 参数     | 类型     | 是否必须              | 默认值       | 说明                                                                                                                                                                          |
 | 
					可以通过变量自定义模板中的策略组代理节点
 | 
				
			||||||
|--------------|--------|-------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | 
					解释的不太清楚,可以参考下方默认模板
 | 
				
			||||||
| sub          | string | sub/proxy 至少有一项存在 | -         | 订阅链接(可以输入多个,用 `,` 分隔)                                                                                                                                                       |
 | 
					 | 
				
			||||||
| proxy        | string | sub/proxy 至少有一项存在 | -         | 节点分享链接(可以输入多个,用 `,` 分隔)                                                                                                                                                     |
 | 
					 | 
				
			||||||
| refresh      | bool   | 否                 | `false`   | 强制刷新配置(默认缓存 5 分钟)                                                                                                                                                           |
 | 
					 | 
				
			||||||
| template     | string | 否                 | -         | 外部模板链接或内部模板名称                                                                                                                                                               |
 | 
					 | 
				
			||||||
| ruleProvider | string | 否                 | -         | 格式 `[Behavior,Url,Group,Prepend,Name],[Behavior,Url,Group,Prepend,Name]...`,其中 `Group` 是该规则集所走的策略组名,`Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前) | 
 | 
					 | 
				
			||||||
| rule         | string | 否                 | -         | 格式 `[Rule,Prepend],[Rule,Prepend]...`,其中 `Prepend` 为 bool 类型,如果为 `true` 规则将被添加到规则列表顶部,否则添加到规则列表底部(会调整到MATCH规则之前)                                                            | 
 | 
					 | 
				
			||||||
| autoTest     | bool   | 否                 | `false`   | 国家策略组是否自动测速                                                                                                                                                                 |
 | 
					 | 
				
			||||||
| lazy         | bool   | 否                 | `false`   | 自动测速是否启用 lazy                                                                                                                                                               |
 | 
					 | 
				
			||||||
| sort         | string | 否                 | `nameasc` | 国家策略组排序策略,可选值 `nameasc`、`namedesc`、`sizeasc`、`sizedesc`                                                                                                                     |
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
## 默认模板
 | 
					- `<all>` 为添加所有节点
 | 
				
			||||||
 | 
					- `<countries>` 为添加所有国家策略组
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 默认模板
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- [Clash](./templates/template_clash.yaml)
 | 
					- [Clash](./templates/template_clash.yaml)
 | 
				
			||||||
- [Clash.Meta](./templates/template_meta.yaml)
 | 
					- [Clash.Meta](./templates/template_meta.yaml)
 | 
				
			||||||
@@ -63,5 +57,3 @@
 | 
				
			|||||||
## 已知问题
 | 
					## 已知问题
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue
 | 
					[代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue
 | 
				
			||||||
 | 
					 | 
				
			||||||
## TODO
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,11 +4,14 @@ import (
 | 
				
			|||||||
	"crypto/sha256"
 | 
						"crypto/sha256"
 | 
				
			||||||
	"encoding/hex"
 | 
						"encoding/hex"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						"go.uber.org/zap"
 | 
				
			||||||
	"gopkg.in/yaml.v3"
 | 
						"gopkg.in/yaml.v3"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"sort"
 | 
						"sort"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"sub2clash/logger"
 | 
				
			||||||
	"sub2clash/model"
 | 
						"sub2clash/model"
 | 
				
			||||||
	"sub2clash/parser"
 | 
						"sub2clash/parser"
 | 
				
			||||||
	"sub2clash/utils"
 | 
						"sub2clash/utils"
 | 
				
			||||||
@@ -31,17 +34,24 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		templateBytes, err = utils.LoadTemplate(template)
 | 
							templateBytes, err = utils.LoadTemplate(template)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logger.Logger.Debug(
 | 
				
			||||||
 | 
									"load template failed", zap.String("template", template), zap.Error(err),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			return nil, errors.New("加载模板失败: " + err.Error())
 | 
								return nil, errors.New("加载模板失败: " + err.Error())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		templateBytes, err = utils.LoadSubscription(template, query.Refresh)
 | 
							templateBytes, err = utils.LoadSubscription(template, query.Refresh)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logger.Logger.Debug(
 | 
				
			||||||
 | 
									"load template failed", zap.String("template", template), zap.Error(err),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			return nil, errors.New("加载模板失败: " + err.Error())
 | 
								return nil, errors.New("加载模板失败: " + err.Error())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// 解析模板
 | 
						// 解析模板
 | 
				
			||||||
	err = yaml.Unmarshal(templateBytes, &temp)
 | 
						err = yaml.Unmarshal(templateBytes, &temp)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Logger.Debug("parse template failed", zap.Error(err))
 | 
				
			||||||
		return nil, errors.New("解析模板失败: " + err.Error())
 | 
							return nil, errors.New("解析模板失败: " + err.Error())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var proxyList []model.Proxy
 | 
						var proxyList []model.Proxy
 | 
				
			||||||
@@ -49,13 +59,15 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
 | 
				
			|||||||
	for i := range query.Subs {
 | 
						for i := range query.Subs {
 | 
				
			||||||
		data, err := utils.LoadSubscription(query.Subs[i], query.Refresh)
 | 
							data, err := utils.LoadSubscription(query.Subs[i], query.Refresh)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 | 
								logger.Logger.Debug(
 | 
				
			||||||
 | 
									"load subscription failed", zap.String("url", query.Subs[i]), zap.Error(err),
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
			return nil, errors.New("加载订阅失败: " + err.Error())
 | 
								return nil, errors.New("加载订阅失败: " + err.Error())
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// 解析订阅
 | 
							// 解析订阅
 | 
				
			||||||
 | 
					 | 
				
			||||||
		err = yaml.Unmarshal(data, &sub)
 | 
							err = yaml.Unmarshal(data, &sub)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|http|https)://")
 | 
								reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|vless)://")
 | 
				
			||||||
			if reg.Match(data) {
 | 
								if reg.Match(data) {
 | 
				
			||||||
				p := utils.ParseProxy(strings.Split(string(data), "\n")...)
 | 
									p := utils.ParseProxy(strings.Split(string(data), "\n")...)
 | 
				
			||||||
				proxyList = append(proxyList, p...)
 | 
									proxyList = append(proxyList, p...)
 | 
				
			||||||
@@ -63,6 +75,11 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
 | 
				
			|||||||
				// 如果无法直接解析,尝试Base64解码
 | 
									// 如果无法直接解析,尝试Base64解码
 | 
				
			||||||
				base64, err := parser.DecodeBase64(string(data))
 | 
									base64, err := parser.DecodeBase64(string(data))
 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
 | 
										logger.Logger.Debug(
 | 
				
			||||||
 | 
											"parse subscription failed", zap.String("url", query.Subs[i]),
 | 
				
			||||||
 | 
											zap.String("data", string(data)),
 | 
				
			||||||
 | 
											zap.Error(err),
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
					return nil, errors.New("加载订阅失败: " + err.Error())
 | 
										return nil, errors.New("加载订阅失败: " + err.Error())
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				p := utils.ParseProxy(strings.Split(base64, "\n")...)
 | 
									p := utils.ParseProxy(strings.Split(base64, "\n")...)
 | 
				
			||||||
@@ -72,14 +89,80 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template
 | 
				
			|||||||
			proxyList = append(proxyList, sub.Proxies...)
 | 
								proxyList = append(proxyList, sub.Proxies...)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						// 添加自定义节点
 | 
				
			||||||
 | 
						if len(query.Proxies) != 0 {
 | 
				
			||||||
 | 
							proxyList = append(proxyList, utils.ParseProxy(query.Proxies...)...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 去掉配置相同的节点
 | 
				
			||||||
 | 
						proxies := make(map[string]*model.Proxy)
 | 
				
			||||||
 | 
						newProxies := make([]model.Proxy, 0, len(proxyList))
 | 
				
			||||||
 | 
						for i := range proxyList {
 | 
				
			||||||
 | 
							key := proxyList[i].Server + ":" + strconv.Itoa(proxyList[i].Port) + ":" + proxyList[i].Type
 | 
				
			||||||
 | 
							if _, exist := proxies[key]; !exist {
 | 
				
			||||||
 | 
								proxies[key] = &proxyList[i]
 | 
				
			||||||
 | 
								newProxies = append(newProxies, proxyList[i])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						proxyList = newProxies
 | 
				
			||||||
 | 
						// 删除节点
 | 
				
			||||||
 | 
						if strings.TrimSpace(query.Remove) != "" {
 | 
				
			||||||
 | 
							newProxyList := make([]model.Proxy, 0, len(proxyList))
 | 
				
			||||||
 | 
							for i := range proxyList {
 | 
				
			||||||
 | 
								removeReg, err := regexp.Compile(query.Remove)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logger.Logger.Debug("remove regexp compile failed", zap.Error(err))
 | 
				
			||||||
 | 
									return nil, errors.New("remove 参数非法: " + err.Error())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								// 删除匹配到的节点
 | 
				
			||||||
 | 
								if removeReg.MatchString(proxyList[i].Name) {
 | 
				
			||||||
 | 
									continue // 如果匹配到要删除的元素,跳过该元素,不添加到新切片中
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								newProxyList = append(newProxyList, proxyList[i]) // 将要保留的元素添加到新切片中
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							proxyList = newProxyList
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 重命名
 | 
				
			||||||
 | 
						if len(query.ReplaceKeys) != 0 {
 | 
				
			||||||
 | 
							// 创建重命名正则表达式
 | 
				
			||||||
 | 
							replaceRegs := make([]*regexp.Regexp, 0, len(query.ReplaceKeys))
 | 
				
			||||||
 | 
							for _, v := range query.ReplaceKeys {
 | 
				
			||||||
 | 
								replaceReg, err := regexp.Compile(v)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
 | 
				
			||||||
 | 
									return nil, errors.New("replace 参数非法: " + err.Error())
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								replaceRegs = append(replaceRegs, replaceReg)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for i := range proxyList {
 | 
				
			||||||
 | 
								// 重命名匹配到的节点
 | 
				
			||||||
 | 
								for j, v := range replaceRegs {
 | 
				
			||||||
 | 
									if err != nil {
 | 
				
			||||||
 | 
										logger.Logger.Debug("replace regexp compile failed", zap.Error(err))
 | 
				
			||||||
 | 
										return nil, errors.New("replaceName 参数非法: " + err.Error())
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if v.MatchString(proxyList[i].Name) {
 | 
				
			||||||
 | 
										proxyList[i].Name = v.ReplaceAllString(
 | 
				
			||||||
 | 
											proxyList[i].Name, query.ReplaceTo[j],
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// 重名检测
 | 
				
			||||||
 | 
						names := make(map[string]int)
 | 
				
			||||||
 | 
						for i := range proxyList {
 | 
				
			||||||
 | 
							if _, exist := names[proxyList[i].Name]; exist {
 | 
				
			||||||
 | 
								proxyList[i].Name = proxyList[i].Name + " " + strconv.Itoa(names[proxyList[i].Name])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							names[proxyList[i].Name] = names[proxyList[i].Name] + 1
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// trim
 | 
				
			||||||
 | 
						for i := range proxyList {
 | 
				
			||||||
 | 
							proxyList[i].Name = strings.TrimSpace(proxyList[i].Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
 | 
						// 将新增节点都添加到临时变量 t 中,防止策略组排序错乱
 | 
				
			||||||
	var t = &model.Subscription{}
 | 
						var t = &model.Subscription{}
 | 
				
			||||||
	utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
 | 
						utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...)
 | 
				
			||||||
	// 处理自定义代理
 | 
					 | 
				
			||||||
	utils.AddProxy(
 | 
					 | 
				
			||||||
		t, query.AutoTest, query.Lazy, clashType,
 | 
					 | 
				
			||||||
		utils.ParseProxy(query.Proxies...)...,
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
	// 排序策略组
 | 
						// 排序策略组
 | 
				
			||||||
	switch query.Sort {
 | 
						switch query.Sort {
 | 
				
			||||||
	case "sizeasc":
 | 
						case "sizeasc":
 | 
				
			||||||
@@ -138,28 +221,28 @@ func MergeSubAndTemplate(temp *model.Subscription, sub *model.Subscription) {
 | 
				
			|||||||
			)
 | 
								)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						var proxyNames []string
 | 
				
			||||||
 | 
						for _, proxy := range sub.Proxies {
 | 
				
			||||||
 | 
							proxyNames = append(proxyNames, proxy.Name)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	// 将订阅中的节点添加到模板中
 | 
						// 将订阅中的节点添加到模板中
 | 
				
			||||||
	temp.Proxies = append(temp.Proxies, sub.Proxies...)
 | 
						temp.Proxies = append(temp.Proxies, sub.Proxies...)
 | 
				
			||||||
	// 将订阅中的策略组添加到模板中
 | 
						// 将订阅中的策略组添加到模板中
 | 
				
			||||||
	skipGroups := []string{"全球直连", "广告拦截", "手动切换"}
 | 
					 | 
				
			||||||
	for i := range temp.ProxyGroups {
 | 
						for i := range temp.ProxyGroups {
 | 
				
			||||||
		skip := false
 | 
							if temp.ProxyGroups[i].IsCountryGrop {
 | 
				
			||||||
		for _, v := range skipGroups {
 | 
								continue
 | 
				
			||||||
			if strings.Contains(temp.ProxyGroups[i].Name, v) {
 | 
							}
 | 
				
			||||||
				if v == "手动切换" {
 | 
							newProxies := make([]string, 0, len(temp.ProxyGroups[i].Proxies))
 | 
				
			||||||
					proxies := make([]string, 0, len(sub.Proxies))
 | 
							for j := range temp.ProxyGroups[i].Proxies {
 | 
				
			||||||
					for _, p := range sub.Proxies {
 | 
								if temp.ProxyGroups[i].Proxies[j] == "<all>" {
 | 
				
			||||||
						proxies = append(proxies, p.Name)
 | 
									newProxies = append(newProxies, proxyNames...)
 | 
				
			||||||
					}
 | 
								} else if temp.ProxyGroups[i].Proxies[j] == "<countries>" {
 | 
				
			||||||
					temp.ProxyGroups[i].Proxies = proxies
 | 
									newProxies = append(newProxies, countryGroupNames...)
 | 
				
			||||||
				}
 | 
								} else {
 | 
				
			||||||
				skip = true
 | 
									newProxies = append(newProxies, temp.ProxyGroups[i].Proxies[j])
 | 
				
			||||||
				continue
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if !skip {
 | 
							temp.ProxyGroups[i].Proxies = newProxies
 | 
				
			||||||
			temp.ProxyGroups[i].Proxies = append(temp.ProxyGroups[i].Proxies, countryGroupNames...)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
 | 
						temp.ProxyGroups = append(temp.ProxyGroups, sub.ProxyGroups...)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,10 +3,14 @@ package controller
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"go.uber.org/zap"
 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sub2clash/config"
 | 
						"sub2clash/config"
 | 
				
			||||||
 | 
						"sub2clash/logger"
 | 
				
			||||||
	"sub2clash/model"
 | 
						"sub2clash/model"
 | 
				
			||||||
	"sub2clash/utils"
 | 
						"sub2clash/utils"
 | 
				
			||||||
	"sub2clash/utils/database"
 | 
						"sub2clash/utils/database"
 | 
				
			||||||
@@ -26,11 +30,16 @@ func ShortLinkGenHandler(c *gin.Context) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	// 生成hash
 | 
						// 生成hash
 | 
				
			||||||
	hash := utils.RandomString(config.Default.ShortLinkLength)
 | 
						hash := utils.RandomString(config.Default.ShortLinkLength)
 | 
				
			||||||
	// 存入数据库
 | 
					 | 
				
			||||||
	var item model.ShortLink
 | 
						var item model.ShortLink
 | 
				
			||||||
	result := database.FindShortLinkByUrl(params.Url, &item)
 | 
						result := database.FindShortLinkByUrl(params.Url, &item)
 | 
				
			||||||
	if result.Error == nil {
 | 
						if result.Error == nil {
 | 
				
			||||||
		c.String(200, item.Hash)
 | 
							if item.Password != params.Password {
 | 
				
			||||||
 | 
								item.Password = params.Password
 | 
				
			||||||
 | 
								database.SaveShortLink(&item)
 | 
				
			||||||
 | 
								c.String(200, item.Hash+"?password="+params.Password)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								c.String(200, item.Hash)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
 | 
							if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
 | 
				
			||||||
@@ -50,15 +59,20 @@ func ShortLinkGenHandler(c *gin.Context) {
 | 
				
			|||||||
			Hash:            hash,
 | 
								Hash:            hash,
 | 
				
			||||||
			Url:             params.Url,
 | 
								Url:             params.Url,
 | 
				
			||||||
			LastRequestTime: -1,
 | 
								LastRequestTime: -1,
 | 
				
			||||||
 | 
								Password:        params.Password,
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
	// 返回短链接
 | 
						// 返回短链接
 | 
				
			||||||
 | 
						if params.Password != "" {
 | 
				
			||||||
 | 
							hash += "?password=" + params.Password
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	c.String(200, hash)
 | 
						c.String(200, hash)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func ShortLinkGetHandler(c *gin.Context) {
 | 
					func ShortLinkGetHandler(c *gin.Context) {
 | 
				
			||||||
	// 获取动态路由
 | 
						// 获取动态路由
 | 
				
			||||||
	hash := c.Param("hash")
 | 
						hash := c.Param("hash")
 | 
				
			||||||
 | 
						password := c.Query("password")
 | 
				
			||||||
	if strings.TrimSpace(hash) == "" {
 | 
						if strings.TrimSpace(hash) == "" {
 | 
				
			||||||
		c.String(400, "参数错误")
 | 
							c.String(400, "参数错误")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
@@ -68,12 +82,27 @@ func ShortLinkGetHandler(c *gin.Context) {
 | 
				
			|||||||
	result := database.FindShortLinkByHash(hash, &shortLink)
 | 
						result := database.FindShortLinkByHash(hash, &shortLink)
 | 
				
			||||||
	// 重定向
 | 
						// 重定向
 | 
				
			||||||
	if result.Error != nil {
 | 
						if result.Error != nil {
 | 
				
			||||||
		c.String(404, "未找到短链接")
 | 
							c.String(404, "未找到短链接或密码错误")
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if shortLink.Password != "" && shortLink.Password != password {
 | 
				
			||||||
 | 
							c.String(404, "未找到短链接或密码错误")
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// 更新最后访问时间
 | 
						// 更新最后访问时间
 | 
				
			||||||
	shortLink.LastRequestTime = time.Now().Unix()
 | 
						shortLink.LastRequestTime = time.Now().Unix()
 | 
				
			||||||
	database.SaveShortLink(&shortLink)
 | 
						database.SaveShortLink(&shortLink)
 | 
				
			||||||
	uri := config.Default.BasePath + shortLink.Url
 | 
						get, err := utils.Get("http://localhost:" + strconv.Itoa(config.Default.Port) + "/" + shortLink.Url)
 | 
				
			||||||
	c.Redirect(http.StatusTemporaryRedirect, uri)
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Logger.Debug("get short link data failed", zap.Error(err))
 | 
				
			||||||
 | 
							c.String(500, "请求错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						all, err := io.ReadAll(get.Body)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							logger.Logger.Debug("read short link data failed", zap.Error(err))
 | 
				
			||||||
 | 
							c.String(500, "读取错误: "+err.Error())
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.String(http.StatusOK, string(all))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -168,6 +168,30 @@
 | 
				
			|||||||
            <option value="sizedesc">节点数量(降序)</option>
 | 
					            <option value="sizedesc">节点数量(降序)</option>
 | 
				
			||||||
          </select>
 | 
					          </select>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- Remove -->
 | 
				
			||||||
 | 
					        <div class="form-group mb-3">
 | 
				
			||||||
 | 
					          <label for="remove">删除节点:</label>
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            class="form-control"
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            name="remove"
 | 
				
			||||||
 | 
					            id="remove"
 | 
				
			||||||
 | 
					            placeholder="正则表达式"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- Rename  -->
 | 
				
			||||||
 | 
					        <div class="form-group mb-3" id="replaceGroup">
 | 
				
			||||||
 | 
					          <label>节点名称替换:</label>
 | 
				
			||||||
 | 
					          <button
 | 
				
			||||||
 | 
					            class="btn btn-primary mb-1 btn-xs"
 | 
				
			||||||
 | 
					            onclick="addReplace()"
 | 
				
			||||||
 | 
					            type="button"
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            +
 | 
				
			||||||
 | 
					          </button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
      </form>
 | 
					      </form>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- Display the API Link -->
 | 
					      <!-- Display the API Link -->
 | 
				
			||||||
@@ -188,6 +212,12 @@
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="input-group">
 | 
					        <div class="input-group">
 | 
				
			||||||
          <input class="form-control" id="apiShortLink" readonly type="text" />
 | 
					          <input class="form-control" id="apiShortLink" readonly type="text" />
 | 
				
			||||||
 | 
					          <input
 | 
				
			||||||
 | 
					            class="form-control"
 | 
				
			||||||
 | 
					            id="password"
 | 
				
			||||||
 | 
					            type="text"
 | 
				
			||||||
 | 
					            placeholder="密码"
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
          <button
 | 
					          <button
 | 
				
			||||||
            class="btn btn-primary"
 | 
					            class="btn btn-primary"
 | 
				
			||||||
            onclick="generateShortLink()"
 | 
					            onclick="generateShortLink()"
 | 
				
			||||||
@@ -246,6 +276,17 @@
 | 
				
			|||||||
        return div;
 | 
					        return div;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function createReplace() {
 | 
				
			||||||
 | 
					        const div = document.createElement("div");
 | 
				
			||||||
 | 
					        div.classList.add("input-group", "mb-2");
 | 
				
			||||||
 | 
					        div.innerHTML = `
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" name="replace" placeholder="原字符串(正则表达式)">
 | 
				
			||||||
 | 
					            <input type="text" class="form-control" name="replace" placeholder="替换为(可为空)">
 | 
				
			||||||
 | 
					            <button type="button" class="btn btn-danger" onclick="removeElement(this)">删除</button>
 | 
				
			||||||
 | 
					        `;
 | 
				
			||||||
 | 
					        return div;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      function createRule() {
 | 
					      function createRule() {
 | 
				
			||||||
        const div = document.createElement("div");
 | 
					        const div = document.createElement("div");
 | 
				
			||||||
        div.classList.add("input-group", "mb-2");
 | 
					        div.classList.add("input-group", "mb-2");
 | 
				
			||||||
@@ -268,6 +309,11 @@
 | 
				
			|||||||
        document.getElementById("ruleGroup").appendChild(div);
 | 
					        document.getElementById("ruleGroup").appendChild(div);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      function addReplace() {
 | 
				
			||||||
 | 
					        const div = createReplace();
 | 
				
			||||||
 | 
					        document.getElementById("replaceGroup").appendChild(div);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      function removeElement(button) {
 | 
					      function removeElement(button) {
 | 
				
			||||||
        button.parentElement.remove();
 | 
					        button.parentElement.remove();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -382,6 +428,29 @@
 | 
				
			|||||||
        // 获取排序策略
 | 
					        // 获取排序策略
 | 
				
			||||||
        const sort = document.getElementById("sort").value;
 | 
					        const sort = document.getElementById("sort").value;
 | 
				
			||||||
        queryParams.push(`sort=${sort}`);
 | 
					        queryParams.push(`sort=${sort}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取删除节点的正则表达式
 | 
				
			||||||
 | 
					        const remove = document.getElementById("remove").value;
 | 
				
			||||||
 | 
					        if (remove.trim() !== "") {
 | 
				
			||||||
 | 
					          queryParams.push(`remove=${encodeURIComponent(remove)}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 获取替换节点名称的正则表达式
 | 
				
			||||||
 | 
					        let replaceList = [];
 | 
				
			||||||
 | 
					        const replaces = document.getElementsByName("replace");
 | 
				
			||||||
 | 
					        for (let i = 0; i < replaces.length / 2; i++) {
 | 
				
			||||||
 | 
					          let replaceStr = `<${replaces[i * 2].value}>`;
 | 
				
			||||||
 | 
					          let replaceTo = `<${replaces[i * 2 + 1].value}>`;
 | 
				
			||||||
 | 
					          if (replaceStr.trim() === "") {
 | 
				
			||||||
 | 
					            alert("重命名设置中存在空值,请检查后重试!");
 | 
				
			||||||
 | 
					            return "";
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          replaceList.push(`[${replaceStr},${replaceTo}]`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        queryParams.push(
 | 
				
			||||||
 | 
					          `replace=${encodeURIComponent(replaceList.join(","))}`,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return `${endpoint}?${queryParams.join("&")}`;
 | 
					        return `${endpoint}?${queryParams.join("&")}`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -396,6 +465,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      function generateShortLink() {
 | 
					      function generateShortLink() {
 | 
				
			||||||
        const apiShortLink = document.getElementById("apiShortLink");
 | 
					        const apiShortLink = document.getElementById("apiShortLink");
 | 
				
			||||||
 | 
					        const password = document.getElementById("password");
 | 
				
			||||||
        let uri = generateURI();
 | 
					        let uri = generateURI();
 | 
				
			||||||
        if (uri === "") {
 | 
					        if (uri === "") {
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
@@ -405,6 +475,7 @@
 | 
				
			|||||||
            "./short",
 | 
					            "./short",
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              url: uri,
 | 
					              url: uri,
 | 
				
			||||||
 | 
					              password: password.value.trim(),
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              headers: {
 | 
					              headers: {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,5 +3,6 @@ package model
 | 
				
			|||||||
type ShortLink struct {
 | 
					type ShortLink struct {
 | 
				
			||||||
	Hash            string `gorm:"primary_key"`
 | 
						Hash            string `gorm:"primary_key"`
 | 
				
			||||||
	Url             string
 | 
						Url             string
 | 
				
			||||||
 | 
						Password        string
 | 
				
			||||||
	LastRequestTime int64
 | 
						LastRequestTime int64
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,88 +8,104 @@ proxy-groups:
 | 
				
			|||||||
  - name: 节点选择
 | 
					  - name: 节点选择
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 手动切换
 | 
					  - name: 手动切换
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <all>
 | 
				
			||||||
  - name: 电报消息
 | 
					  - name: 电报消息
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: OpenAi
 | 
					  - name: OpenAi
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 油管视频
 | 
					  - name: 油管视频
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 巴哈姆特
 | 
					  - name: 巴哈姆特
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 哔哩哔哩
 | 
					  - name: 哔哩哔哩
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 全球直连
 | 
					      - 全球直连
 | 
				
			||||||
  - name: 国外媒体
 | 
					  - name: 国外媒体
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 国内媒体
 | 
					  - name: 国内媒体
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
  - name: 谷歌FCM
 | 
					  - name: 谷歌FCM
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
  - name: 微软云盘
 | 
					  - name: 微软云盘
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
  - name: 微软服务
 | 
					  - name: 微软服务
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
  - name: 苹果服务
 | 
					  - name: 苹果服务
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
  - name: 游戏平台
 | 
					  - name: 游戏平台
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
  - name: 网易音乐
 | 
					  - name: 网易音乐
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
  - name: 全球直连
 | 
					  - name: 全球直连
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
  - name: 广告拦截
 | 
					  - name: 广告拦截
 | 
				
			||||||
@@ -105,6 +121,7 @@ proxy-groups:
 | 
				
			|||||||
  - name: 漏网之鱼
 | 
					  - name: 漏网之鱼
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
@@ -9579,4 +9596,4 @@ rules:
 | 
				
			|||||||
  - PROCESS-NAME,Weiyun.exe,全球直连
 | 
					  - PROCESS-NAME,Weiyun.exe,全球直连
 | 
				
			||||||
  - PROCESS-NAME,baidunetdisk.exe,全球直连
 | 
					  - PROCESS-NAME,baidunetdisk.exe,全球直连
 | 
				
			||||||
  - GEOIP,CN,全球直连
 | 
					  - GEOIP,CN,全球直连
 | 
				
			||||||
  - MATCH,漏网之鱼
 | 
					  - MATCH,漏网之鱼
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,40 +8,90 @@ proxy-groups:
 | 
				
			|||||||
  - name: 节点选择
 | 
					  - name: 节点选择
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 手动切换
 | 
					  - name: 手动切换
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
  - name: 微软服务
 | 
					      - <all>
 | 
				
			||||||
 | 
					  - name: 游戏平台(中国)
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 游戏平台
 | 
					  - name: 游戏平台(全球)
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 巴哈姆特
 | 
					  - name: 巴哈姆特
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 哔哩哔哩
 | 
					  - name: 哔哩哔哩
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 全球直连
 | 
					  - name: Telegram
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
      - DIRECT
 | 
					 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
 | 
					      - 手动切换
 | 
				
			||||||
 | 
					      - DIRECT
 | 
				
			||||||
 | 
					  - name: OpenAI
 | 
				
			||||||
 | 
					    type: select
 | 
				
			||||||
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
 | 
					      - 手动切换
 | 
				
			||||||
 | 
					      - DIRECT
 | 
				
			||||||
 | 
					  - name: Youtube
 | 
				
			||||||
 | 
					    type: select
 | 
				
			||||||
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
 | 
					      - 手动切换
 | 
				
			||||||
 | 
					      - DIRECT
 | 
				
			||||||
 | 
					  - name: Microsoft
 | 
				
			||||||
 | 
					    type: select
 | 
				
			||||||
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
 | 
					      - 手动切换
 | 
				
			||||||
 | 
					      - DIRECT
 | 
				
			||||||
 | 
					  - name: Onedrive
 | 
				
			||||||
 | 
					    type: select
 | 
				
			||||||
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
 | 
					      - 手动切换
 | 
				
			||||||
 | 
					      - DIRECT
 | 
				
			||||||
 | 
					  - name: Apple
 | 
				
			||||||
 | 
					    type: select
 | 
				
			||||||
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
 | 
					      - 手动切换
 | 
				
			||||||
 | 
					      - DIRECT
 | 
				
			||||||
 | 
					  - name: Netflix
 | 
				
			||||||
 | 
					    type: select
 | 
				
			||||||
 | 
					    proxies:
 | 
				
			||||||
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
 | 
					      - 手动切换
 | 
				
			||||||
 | 
					      - DIRECT
 | 
				
			||||||
  - name: 广告拦截
 | 
					  - name: 广告拦截
 | 
				
			||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
@@ -51,17 +101,25 @@ proxy-groups:
 | 
				
			|||||||
    type: select
 | 
					    type: select
 | 
				
			||||||
    proxies:
 | 
					    proxies:
 | 
				
			||||||
      - 节点选择
 | 
					      - 节点选择
 | 
				
			||||||
 | 
					      - <countries>
 | 
				
			||||||
      - 手动切换
 | 
					      - 手动切换
 | 
				
			||||||
      - DIRECT
 | 
					      - DIRECT
 | 
				
			||||||
rules:
 | 
					rules:
 | 
				
			||||||
  - GEOSITE,private,全球直连,no-resolve
 | 
					  - GEOSITE,private,DIRECT,no-resolve
 | 
				
			||||||
  - GEOIP,private,全球直连
 | 
					  - GEOIP,private,DIRECT
 | 
				
			||||||
  - GEOSITE,category-ads-all,广告拦截
 | 
					  - GEOSITE,category-ads-all,广告拦截
 | 
				
			||||||
  - GEOSITE,microsoft,微软服务
 | 
					  - GEOSITE,microsoft,Microsoft
 | 
				
			||||||
 | 
					  - GEOSITE,apple,Apple
 | 
				
			||||||
 | 
					  - GEOSITE,netflix,Netflix
 | 
				
			||||||
 | 
					  - GEOSITE,onedrive,Onedrive
 | 
				
			||||||
 | 
					  - GEOSITE,youtube,Youtube
 | 
				
			||||||
 | 
					  - GEOSITE,telegram,Telegram
 | 
				
			||||||
 | 
					  - GEOSITE,openai,OpenAI
 | 
				
			||||||
  - GEOSITE,bilibili,哔哩哔哩
 | 
					  - GEOSITE,bilibili,哔哩哔哩
 | 
				
			||||||
  - GEOSITE,bahamut,巴哈姆特
 | 
					  - GEOSITE,bahamut,巴哈姆特
 | 
				
			||||||
  - GEOSITE,category-games,游戏平台
 | 
					  - GEOSITE,category-games@cn,游戏平台(中国)
 | 
				
			||||||
 | 
					  - GEOSITE,category-games,游戏平台(全球)
 | 
				
			||||||
  - GEOSITE,geolocation-!cn,节点选择
 | 
					  - GEOSITE,geolocation-!cn,节点选择
 | 
				
			||||||
  - GEOSITE,CN,全球直连
 | 
					  - GEOSITE,CN,DIRECT
 | 
				
			||||||
  - GEOIP,CN,全球直连
 | 
					  - GEOIP,CN,DIRECT
 | 
				
			||||||
  - MATCH,漏网之鱼
 | 
					  - MATCH,漏网之鱼
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								utils/get.go
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								utils/get.go
									
									
									
									
									
								
							@@ -12,7 +12,15 @@ func Get(url string) (resp *http.Response, err error) {
 | 
				
			|||||||
	haveTried := 0
 | 
						haveTried := 0
 | 
				
			||||||
	retryDelay := time.Second // 延迟1秒再重试
 | 
						retryDelay := time.Second // 延迟1秒再重试
 | 
				
			||||||
	for haveTried < retryTimes {
 | 
						for haveTried < retryTimes {
 | 
				
			||||||
		get, err := http.Get(url)
 | 
							client := &http.Client{}
 | 
				
			||||||
 | 
							//client.Timeout = time.Second * 10
 | 
				
			||||||
 | 
							req, err := http.NewRequest("GET", url, nil)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								haveTried++
 | 
				
			||||||
 | 
								time.Sleep(retryDelay)
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							get, err := client.Do(req)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			haveTried++
 | 
								haveTried++
 | 
				
			||||||
			time.Sleep(retryDelay)
 | 
								time.Sleep(retryDelay)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,7 +31,6 @@ func AddProxy(
 | 
				
			|||||||
	sub *model.Subscription, autotest bool,
 | 
						sub *model.Subscription, autotest bool,
 | 
				
			||||||
	lazy bool, clashType model.ClashType, proxies ...model.Proxy,
 | 
						lazy bool, clashType model.ClashType, proxies ...model.Proxy,
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
	newCountryGroupNames := make([]string, 0)
 | 
					 | 
				
			||||||
	proxyTypes := model.GetSupportProxyTypes(clashType)
 | 
						proxyTypes := model.GetSupportProxyTypes(clashType)
 | 
				
			||||||
	// 添加节点
 | 
						// 添加节点
 | 
				
			||||||
	for _, proxy := range proxies {
 | 
						for _, proxy := range proxies {
 | 
				
			||||||
@@ -79,7 +78,6 @@ func AddProxy(
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
 | 
								sub.ProxyGroups = append(sub.ProxyGroups, newGroup)
 | 
				
			||||||
			newCountryGroupNames = append(newCountryGroupNames, countryName)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								utils/sub.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								utils/sub.go
									
									
									
									
									
								
							@@ -35,7 +35,9 @@ func LoadSubscription(url string, refresh bool) ([]byte, error) {
 | 
				
			|||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		defer func(file *os.File) {
 | 
							defer func(file *os.File) {
 | 
				
			||||||
			_ = file.Close()
 | 
								if file != nil {
 | 
				
			||||||
 | 
									_ = file.Close()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}(file)
 | 
							}(file)
 | 
				
			||||||
		fileLock.RLock()
 | 
							fileLock.RLock()
 | 
				
			||||||
		defer fileLock.RUnlock()
 | 
							defer fileLock.RUnlock()
 | 
				
			||||||
@@ -56,7 +58,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer func(Body io.ReadCloser) {
 | 
						defer func(Body io.ReadCloser) {
 | 
				
			||||||
		_ = Body.Close()
 | 
							if Body != nil {
 | 
				
			||||||
 | 
								_ = Body.Close()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}(resp.Body)
 | 
						}(resp.Body)
 | 
				
			||||||
	data, err := io.ReadAll(resp.Body)
 | 
						data, err := io.ReadAll(resp.Body)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -67,7 +71,9 @@ func FetchSubscriptionFromAPI(url string) ([]byte, error) {
 | 
				
			|||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer func(file *os.File) {
 | 
						defer func(file *os.File) {
 | 
				
			||||||
		_ = file.Close()
 | 
							if file != nil {
 | 
				
			||||||
 | 
								_ = file.Close()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}(file)
 | 
						}(file)
 | 
				
			||||||
	fileLock.Lock()
 | 
						fileLock.Lock()
 | 
				
			||||||
	defer fileLock.Unlock()
 | 
						defer fileLock.Unlock()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,9 @@ func LoadTemplate(template string) ([]byte, error) {
 | 
				
			|||||||
			return nil, err
 | 
								return nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		defer func(file *os.File) {
 | 
							defer func(file *os.File) {
 | 
				
			||||||
			_ = file.Close()
 | 
								if file != nil {
 | 
				
			||||||
 | 
									_ = file.Close()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}(file)
 | 
							}(file)
 | 
				
			||||||
		result, err := io.ReadAll(file)
 | 
							result, err := io.ReadAll(file)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,9 @@ func writeTemplate(path string, template string) error {
 | 
				
			|||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		defer func(file *os.File) {
 | 
							defer func(file *os.File) {
 | 
				
			||||||
			_ = file.Close()
 | 
								if file != nil {
 | 
				
			||||||
 | 
									_ = file.Close()
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
		}(file)
 | 
							}(file)
 | 
				
			||||||
		_, err = file.WriteString(template)
 | 
							_, err = file.WriteString(template)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,11 @@
 | 
				
			|||||||
package validator
 | 
					package validator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ShortLinkGenValidator struct {
 | 
					type ShortLinkGenValidator struct {
 | 
				
			||||||
	Url string `form:"url" binding:"required"`
 | 
						Url      string `form:"url" binding:"required"`
 | 
				
			||||||
 | 
						Password string `form:"password"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ShortLinkGetValidator struct {
 | 
					type ShortLinkGetValidator struct {
 | 
				
			||||||
	Hash string `form:"hash" binding:"required"`
 | 
						Hash     string `form:"hash" binding:"required"`
 | 
				
			||||||
 | 
						Password string `form:"password"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,10 @@ type SubValidator struct {
 | 
				
			|||||||
	AutoTest      bool                 `form:"autoTest,default=false" binding:""`
 | 
						AutoTest      bool                 `form:"autoTest,default=false" binding:""`
 | 
				
			||||||
	Lazy          bool                 `form:"lazy,default=false" binding:""`
 | 
						Lazy          bool                 `form:"lazy,default=false" binding:""`
 | 
				
			||||||
	Sort          string               `form:"sort" binding:""`
 | 
						Sort          string               `form:"sort" binding:""`
 | 
				
			||||||
 | 
						Remove        string               `form:"remove" binding:""`
 | 
				
			||||||
 | 
						Replace       string               `form:"replace" binding:""`
 | 
				
			||||||
 | 
						ReplaceKeys   []string             `form:"-" binding:""`
 | 
				
			||||||
 | 
						ReplaceTo     []string             `form:"-" binding:""`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RuleProviderStruct struct {
 | 
					type RuleProviderStruct struct {
 | 
				
			||||||
@@ -135,5 +139,17 @@ func ParseQuery(c *gin.Context) (SubValidator, error) {
 | 
				
			|||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		query.Rules = nil
 | 
							query.Rules = nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						if strings.TrimSpace(query.Replace) != "" {
 | 
				
			||||||
 | 
							reg := regexp.MustCompile(`\[<(.*?)>,<(.*?)>\]`)
 | 
				
			||||||
 | 
							replaces := reg.FindAllStringSubmatch(query.Replace, -1)
 | 
				
			||||||
 | 
							for i := range replaces {
 | 
				
			||||||
 | 
								length := len(replaces[i])
 | 
				
			||||||
 | 
								if length != 3 {
 | 
				
			||||||
 | 
									return SubValidator{}, errors.New("参数错误: replace 格式错误")
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								query.ReplaceKeys = append(query.ReplaceKeys, replaces[i][1])
 | 
				
			||||||
 | 
								query.ReplaceTo = append(query.ReplaceTo, replaces[i][2])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return query, nil
 | 
						return query, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user