Files
sub-store/internal/template/builtin.go
T

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
}