371 lines
12 KiB
Go
371 lines
12 KiB
Go
package template
|
|
|
|
// This file defines the 6 built-in routing templates.
|
|
// Per review-resolution #41: DefaultProxyGroups returns 3 groups when no template configured.
|
|
|
|
const testURL = "https://www.gstatic.com/generate_204"
|
|
|
|
// BuiltinTemplateIDs is the set of IDs that cannot be modified or deleted.
|
|
var BuiltinTemplateIDs = map[string]bool{
|
|
"mihomo-basic": true,
|
|
"acl4ssr-mihomo": true,
|
|
"acl4ssr-mihomo-no-emoji": true,
|
|
"loyalsoldier-whitelist": true,
|
|
"loyalsoldier-blacklist": true,
|
|
"ai-streaming-mihomo": true,
|
|
}
|
|
|
|
const DefaultTemplateID = "acl4ssr-mihomo"
|
|
|
|
// BuiltinTemplates returns all 6 built-in templates as config maps.
|
|
var BuiltinTemplates = []BuiltinTemplate{
|
|
{ID: "mihomo-basic", Name: "Mihomo Basic", Target: "mihomo", Config: mihomoBasicConfig()},
|
|
{ID: "acl4ssr-mihomo", Name: "ACL4SSR Mihomo", Target: "mihomo", Config: acl4ssrConfig()},
|
|
{ID: "acl4ssr-mihomo-no-emoji", Name: "ACL4SSR Mihomo 无 Emoji", Target: "mihomo", Config: acl4ssrNoEmojiConfig()},
|
|
{ID: "loyalsoldier-whitelist", Name: "Loyalsoldier 白名单", Target: "mihomo", Config: loyalsoldierWhitelistConfig()},
|
|
{ID: "loyalsoldier-blacklist", Name: "Loyalsoldier 黑名单", Target: "mihomo", Config: loyalsoldierBlacklistConfig()},
|
|
{ID: "ai-streaming-mihomo", Name: "AI + Streaming", Target: "mihomo", Config: aiStreamingConfig()},
|
|
}
|
|
|
|
type BuiltinTemplate struct {
|
|
ID string
|
|
Name string
|
|
Target string
|
|
Config map[string]any
|
|
}
|
|
|
|
// DefaultProxyGroups returns 3 default groups per review-resolution #41.
|
|
func DefaultProxyGroups() []map[string]any {
|
|
return []map[string]any{
|
|
{"name": "🚀 节点选择", "type": "select", "proxies": []any{"♻️ 自动选择", "🚀 手动切换", "DIRECT"}},
|
|
{"name": "♻️ 自动选择", "type": "url-test", "proxies": []any{"$all"}, "url": testURL, "interval": 300, "tolerance": 50},
|
|
{"name": "🚀 手动切换", "type": "select", "proxies": []any{"$all"}},
|
|
}
|
|
}
|
|
|
|
// NormalizeMihomoTemplateConfig copies kebab-case aliases to camelCase keys.
|
|
func NormalizeMihomoTemplateConfig(input map[string]any) map[string]any {
|
|
output := make(map[string]any, len(input))
|
|
for k, v := range input {
|
|
output[k] = v
|
|
}
|
|
copyAlias(output, "mixed-port", "mixedPort")
|
|
copyAlias(output, "allow-lan", "allowLan")
|
|
copyAlias(output, "log-level", "logLevel")
|
|
copyAlias(output, "proxy-groups", "proxyGroups")
|
|
copyAlias(output, "rule-providers", "ruleProviders")
|
|
return output
|
|
}
|
|
|
|
func copyAlias(input map[string]any, from, to string) {
|
|
if input[to] == nil && input[from] != nil {
|
|
input[to] = input[from]
|
|
}
|
|
delete(input, from)
|
|
}
|
|
|
|
func provider(url, behavior string) map[string]any {
|
|
if behavior == "" {
|
|
behavior = "classical"
|
|
}
|
|
return map[string]any{
|
|
"type": "http",
|
|
"behavior": behavior,
|
|
"url": url,
|
|
"path": "./ruleset/" + lastPathSegment(url),
|
|
"interval": 86400,
|
|
}
|
|
}
|
|
|
|
func lastPathSegment(url string) string {
|
|
for i := len(url) - 1; i >= 0; i-- {
|
|
if url[i] == '/' {
|
|
return url[i+1:]
|
|
}
|
|
}
|
|
return "ruleset"
|
|
}
|
|
|
|
func acl4ssrRaw(name string) string {
|
|
return "https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/" + name + ".list"
|
|
}
|
|
|
|
func loyalSoldier(name string) string {
|
|
return "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release/" + name + ".txt"
|
|
}
|
|
|
|
func blackmatrix(name string) string {
|
|
return "https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Clash/" + name + "/" + name + ".yaml"
|
|
}
|
|
|
|
func baseGroups() []map[string]any {
|
|
return []map[string]any{
|
|
{"name": "🚀 节点选择", "type": "select", "proxies": []any{"♻️ 自动选择", "🚀 手动切换", "DIRECT"}},
|
|
{"name": "♻️ 自动选择", "type": "url-test", "proxies": []any{"$all"}, "url": testURL, "interval": 300, "tolerance": 50},
|
|
{"name": "🚀 手动切换", "type": "select", "proxies": []any{"$all"}},
|
|
{"name": "🌏 国外媒体", "type": "select", "proxies": []any{"🚀 节点选择", "♻️ 自动选择", "🚀 手动切换", "DIRECT"}},
|
|
{"name": "💬 AI 服务", "type": "select", "proxies": []any{"🚀 节点选择", "♻️ 自动选择", "🚀 手动切换", "DIRECT"}},
|
|
{"name": "Ⓜ️ 微软服务", "type": "select", "proxies": []any{"DIRECT", "🚀 节点选择", "♻️ 自动选择"}},
|
|
{"name": "🍎 苹果服务", "type": "select", "proxies": []any{"DIRECT", "🚀 节点选择", "♻️ 自动选择"}},
|
|
{"name": "🎯 全球直连", "type": "select", "proxies": []any{"DIRECT", "🚀 节点选择"}},
|
|
{"name": "🛑 全球拦截", "type": "select", "proxies": []any{"REJECT", "DIRECT"}},
|
|
{"name": "🐟 漏网之鱼", "type": "select", "proxies": []any{"🚀 节点选择", "DIRECT"}},
|
|
}
|
|
}
|
|
|
|
func defaultDNS() map[string]any {
|
|
return map[string]any{
|
|
"enable": true,
|
|
"ipv6": false,
|
|
"enhanced-mode": "fake-ip",
|
|
"nameserver": []any{"https://doh.pub/dns-query", "https://dns.alidns.com/dns-query"},
|
|
}
|
|
}
|
|
|
|
func mihomoBase() map[string]any {
|
|
return map[string]any{
|
|
"mixedPort": 7890,
|
|
"allowLan": false,
|
|
"mode": "rule",
|
|
"logLevel": "info",
|
|
"dns": defaultDNS(),
|
|
"proxyGroups": baseGroups(),
|
|
}
|
|
}
|
|
|
|
func mihomoBasicConfig() map[string]any {
|
|
c := mihomoBase()
|
|
c["ruleProviders"] = map[string]any{}
|
|
c["rules"] = []any{
|
|
"DOMAIN-SUFFIX,openai.com,💬 AI 服务",
|
|
"DOMAIN-SUFFIX,chatgpt.com,💬 AI 服务",
|
|
"DOMAIN-SUFFIX,anthropic.com,💬 AI 服务",
|
|
"DOMAIN-SUFFIX,claude.ai,💬 AI 服务",
|
|
"DOMAIN-SUFFIX,netflix.com,🌏 国外媒体",
|
|
"DOMAIN-SUFFIX,youtube.com,🌏 国外媒体",
|
|
"DOMAIN-SUFFIX,googlevideo.com,🌏 国外媒体",
|
|
"GEOIP,CN,🎯 全球直连",
|
|
"MATCH,🐟 漏网之鱼",
|
|
}
|
|
return c
|
|
}
|
|
|
|
func acl4ssrConfig() map[string]any {
|
|
c := mihomoBase()
|
|
c["ruleProviders"] = map[string]any{
|
|
"LocalAreaNetwork": provider(acl4ssrRaw("LocalAreaNetwork"), ""),
|
|
"UnBan": provider(acl4ssrRaw("UnBan"), ""),
|
|
"BanAD": provider(acl4ssrRaw("BanAD"), ""),
|
|
"BanProgramAD": provider(acl4ssrRaw("BanProgramAD"), ""),
|
|
"GoogleCN": provider(acl4ssrRaw("GoogleCN"), ""),
|
|
"SteamCN": provider(acl4ssrRaw("SteamCN"), ""),
|
|
"Microsoft": provider(acl4ssrRaw("Microsoft"), ""),
|
|
"Apple": provider(acl4ssrRaw("Apple"), ""),
|
|
"Telegram": provider(acl4ssrRaw("Telegram"), ""),
|
|
"OpenAI": provider(blackmatrix("OpenAI"), ""),
|
|
"YouTube": provider(acl4ssrRaw("Ruleset/YouTube"), ""),
|
|
"Netflix": provider(acl4ssrRaw("Ruleset/Netflix"), ""),
|
|
"DisneyPlus": provider(acl4ssrRaw("Ruleset/DisneyPlus"), ""),
|
|
"ProxyGFWlist": provider(acl4ssrRaw("ProxyGFWlist"), ""),
|
|
"ChinaDomain": provider(acl4ssrRaw("ChinaDomain"), ""),
|
|
"ChinaCompanyIp": provider(acl4ssrRaw("ChinaCompanyIp"), ""),
|
|
"Download": provider(acl4ssrRaw("Download"), ""),
|
|
}
|
|
c["rules"] = []any{
|
|
"RULE-SET,LocalAreaNetwork,DIRECT",
|
|
"RULE-SET,UnBan,DIRECT",
|
|
"RULE-SET,BanAD,🛑 全球拦截",
|
|
"RULE-SET,BanProgramAD,🛑 全球拦截",
|
|
"RULE-SET,GoogleCN,DIRECT",
|
|
"RULE-SET,SteamCN,DIRECT",
|
|
"RULE-SET,Microsoft,Ⓜ️ 微软服务",
|
|
"RULE-SET,Apple,🍎 苹果服务",
|
|
"RULE-SET,Telegram,🚀 节点选择",
|
|
"RULE-SET,OpenAI,💬 AI 服务",
|
|
"RULE-SET,YouTube,🌏 国外媒体",
|
|
"RULE-SET,Netflix,🌏 国外媒体",
|
|
"RULE-SET,DisneyPlus,🌏 国外媒体",
|
|
"RULE-SET,ProxyGFWlist,🚀 节点选择",
|
|
"RULE-SET,ChinaDomain,DIRECT",
|
|
"RULE-SET,ChinaCompanyIp,DIRECT",
|
|
"RULE-SET,Download,DIRECT",
|
|
"GEOIP,CN,🎯 全球直连",
|
|
"MATCH,🐟 漏网之鱼",
|
|
}
|
|
return c
|
|
}
|
|
|
|
func loyalsoldierProviders() map[string]any {
|
|
return map[string]any{
|
|
"reject": provider(loyalSoldier("reject"), "domain"),
|
|
"icloud": provider(loyalSoldier("icloud"), "domain"),
|
|
"apple": provider(loyalSoldier("apple"), "domain"),
|
|
"google": provider(loyalSoldier("google"), "domain"),
|
|
"proxy": provider(loyalSoldier("proxy"), "domain"),
|
|
"direct": provider(loyalSoldier("direct"), "domain"),
|
|
"private": provider(loyalSoldier("private"), "domain"),
|
|
"gfw": provider(loyalSoldier("gfw"), "domain"),
|
|
"greatfire": provider(loyalSoldier("greatfire"), "domain"),
|
|
"tld-not-cn": provider(loyalSoldier("tld-not-cn"), "domain"),
|
|
"telegramcidr": provider(loyalSoldier("telegramcidr"), "ipcidr"),
|
|
"cncidr": provider(loyalSoldier("cncidr"), "ipcidr"),
|
|
"lancidr": provider(loyalSoldier("lancidr"), "ipcidr"),
|
|
"applications": provider(loyalSoldier("applications"), "classical"),
|
|
}
|
|
}
|
|
|
|
func loyalsoldierWhitelistConfig() map[string]any {
|
|
c := mihomoBase()
|
|
c["ruleProviders"] = loyalsoldierProviders()
|
|
c["rules"] = []any{
|
|
"RULE-SET,reject,🛑 全球拦截",
|
|
"RULE-SET,icloud,DIRECT",
|
|
"RULE-SET,apple,DIRECT",
|
|
"RULE-SET,google,🚀 节点选择",
|
|
"RULE-SET,proxy,🚀 节点选择",
|
|
"RULE-SET,direct,DIRECT",
|
|
"RULE-SET,private,DIRECT",
|
|
"RULE-SET,gfw,🚀 节点选择",
|
|
"RULE-SET,greatfire,🚀 节点选择",
|
|
"RULE-SET,tld-not-cn,🚀 节点选择",
|
|
"RULE-SET,telegramcidr,🚀 节点选择",
|
|
"RULE-SET,cncidr,DIRECT",
|
|
"RULE-SET,lancidr,DIRECT",
|
|
"RULE-SET,applications,DIRECT",
|
|
"GEOIP,CN,DIRECT",
|
|
"MATCH,🚀 节点选择",
|
|
}
|
|
return c
|
|
}
|
|
|
|
func loyalsoldierBlacklistConfig() map[string]any {
|
|
c := mihomoBase()
|
|
c["ruleProviders"] = loyalsoldierProviders()
|
|
c["rules"] = []any{
|
|
"RULE-SET,reject,🛑 全球拦截",
|
|
"RULE-SET,private,DIRECT",
|
|
"RULE-SET,lancidr,DIRECT",
|
|
"RULE-SET,cncidr,DIRECT",
|
|
"RULE-SET,direct,DIRECT",
|
|
"RULE-SET,applications,DIRECT",
|
|
"RULE-SET,icloud,DIRECT",
|
|
"RULE-SET,apple,DIRECT",
|
|
"RULE-SET,google,🚀 节点选择",
|
|
"RULE-SET,proxy,🚀 节点选择",
|
|
"RULE-SET,gfw,🚀 节点选择",
|
|
"RULE-SET,greatfire,🚀 节点选择",
|
|
"RULE-SET,tld-not-cn,🚀 节点选择",
|
|
"RULE-SET,telegramcidr,🚀 节点选择",
|
|
"GEOIP,CN,DIRECT",
|
|
"MATCH,DIRECT",
|
|
}
|
|
return c
|
|
}
|
|
|
|
func aiStreamingConfig() map[string]any {
|
|
c := mihomoBase()
|
|
c["ruleProviders"] = map[string]any{
|
|
"OpenAI": provider(blackmatrix("OpenAI"), ""),
|
|
"Claude": provider(blackmatrix("Claude"), ""),
|
|
"Gemini": provider(blackmatrix("Gemini"), ""),
|
|
"YouTube": provider(blackmatrix("YouTube"), ""),
|
|
"Netflix": provider(blackmatrix("Netflix"), ""),
|
|
"Disney": provider(blackmatrix("Disney"), ""),
|
|
"Spotify": provider(blackmatrix("Spotify"), ""),
|
|
"Telegram": provider(blackmatrix("Telegram"), ""),
|
|
"GitHub": provider(blackmatrix("GitHub"), ""),
|
|
"China": provider(blackmatrix("China"), ""),
|
|
}
|
|
c["rules"] = []any{
|
|
"RULE-SET,OpenAI,💬 AI 服务",
|
|
"RULE-SET,Claude,💬 AI 服务",
|
|
"RULE-SET,Gemini,💬 AI 服务",
|
|
"RULE-SET,YouTube,🌏 国外媒体",
|
|
"RULE-SET,Netflix,🌏 国外媒体",
|
|
"RULE-SET,Disney,🌏 国外媒体",
|
|
"RULE-SET,Spotify,🌏 国外媒体",
|
|
"RULE-SET,Telegram,🚀 节点选择",
|
|
"RULE-SET,GitHub,🚀 节点选择",
|
|
"RULE-SET,China,DIRECT",
|
|
"GEOIP,CN,🎯 全球直连",
|
|
"MATCH,🐟 漏网之鱼",
|
|
}
|
|
return c
|
|
}
|
|
|
|
// acl4ssrNoEmojiConfig is the ACL4SSR config with emoji labels stripped.
|
|
func acl4ssrNoEmojiConfig() map[string]any {
|
|
c := acl4ssrConfig()
|
|
if stripped, ok := stripEmoji(c).(map[string]any); ok {
|
|
return stripped
|
|
}
|
|
return c
|
|
}
|
|
|
|
var emojiLabelMap = map[string]string{
|
|
"🚀 节点选择": "节点选择",
|
|
"♻️ 自动选择": "自动选择",
|
|
"🚀 手动切换": "手动切换",
|
|
"🌏 国外媒体": "国外媒体",
|
|
"💬 AI 服务": "AI 服务",
|
|
"Ⓜ️ 微软服务": "微软服务",
|
|
"🍎 苹果服务": "苹果服务",
|
|
"🎯 全球直连": "全球直连",
|
|
"🛑 全球拦截": "全球拦截",
|
|
"🐟 漏网之鱼": "漏网之鱼",
|
|
}
|
|
|
|
func stripEmoji(v any) any {
|
|
switch val := v.(type) {
|
|
case string:
|
|
result := val
|
|
for from, to := range emojiLabelMap {
|
|
result = replaceAll(result, from, to)
|
|
}
|
|
return result
|
|
case map[string]any:
|
|
result := make(map[string]any, len(val))
|
|
for k, vv := range val {
|
|
result[k] = stripEmoji(vv)
|
|
}
|
|
return result
|
|
case []any:
|
|
result := make([]any, len(val))
|
|
for i, vv := range val {
|
|
result[i] = stripEmoji(vv)
|
|
}
|
|
return result
|
|
case []map[string]any:
|
|
result := make([]map[string]any, len(val))
|
|
for i, vv := range val {
|
|
result[i] = stripEmoji(vv).(map[string]any)
|
|
}
|
|
return result
|
|
default:
|
|
return v
|
|
}
|
|
}
|
|
|
|
func replaceAll(s, old, new string) string {
|
|
result := ""
|
|
for {
|
|
idx := indexOf(s, old)
|
|
if idx < 0 {
|
|
result += s
|
|
break
|
|
}
|
|
result += s[:idx] + new
|
|
s = s[idx+len(old):]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func indexOf(s, sub string) int {
|
|
for i := 0; i <= len(s)-len(sub); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|