c26b06f1cc
Go 控制面(SQLite + REST API)嵌入 Mihomo 数据面,Vue 3 多语言 Web 管理台。 含订阅/节点/规则/出站/监控/日志、OpenAPI、API Key、Gitea Actions CI 与开源文档; CI 使用 ubuntu-latest runner,Go/Docker 步骤走国内镜像与 shell,避免 GitHub 下载超时。 Co-authored-by: Cursor <cursoragent@cursor.com>
167 lines
3.6 KiB
Go
167 lines
3.6 KiB
Go
package country
|
||
|
||
import (
|
||
"sort"
|
||
"strings"
|
||
)
|
||
|
||
type entry struct {
|
||
Code string
|
||
Name string
|
||
Keywords []string
|
||
}
|
||
|
||
var (
|
||
sortedKeywords []keywordMatch
|
||
nameByCode map[string]string
|
||
)
|
||
|
||
type keywordMatch struct {
|
||
keyword string
|
||
code string
|
||
}
|
||
|
||
func init() {
|
||
nameByCode = make(map[string]string, len(catalog))
|
||
for _, e := range catalog {
|
||
nameByCode[e.Code] = e.Name
|
||
for _, kw := range e.Keywords {
|
||
sortedKeywords = append(sortedKeywords, keywordMatch{keyword: strings.ToLower(kw), code: e.Code})
|
||
}
|
||
}
|
||
sort.Slice(sortedKeywords, func(i, j int) bool {
|
||
return len(sortedKeywords[i].keyword) > len(sortedKeywords[j].keyword)
|
||
})
|
||
}
|
||
|
||
// Detect returns ISO-like country code from node name, or empty string.
|
||
func Detect(name string) string {
|
||
lower := strings.ToLower(strings.TrimSpace(name))
|
||
if lower == "" {
|
||
return ""
|
||
}
|
||
normalized := strings.NewReplacer(
|
||
"_", " ", "-", " ", "|", " ", "/", " ", "[", " ", "]", " ",
|
||
"(", " ", ")", " ", "【", " ", "】", " ", "(", " ", ")", " ",
|
||
).Replace(lower)
|
||
padded := " " + normalized + " "
|
||
for _, m := range sortedKeywords {
|
||
kw := m.keyword
|
||
if len(kw) <= 2 {
|
||
continue
|
||
}
|
||
if keywordMatches(padded, kw) {
|
||
return m.code
|
||
}
|
||
}
|
||
for _, tok := range strings.Fields(normalized) {
|
||
tok = strings.TrimSpace(tok)
|
||
if tok == "" {
|
||
continue
|
||
}
|
||
if len(tok) == 2 {
|
||
if _, ok := nameByCode[strings.ToUpper(tok)]; ok {
|
||
return strings.ToUpper(tok)
|
||
}
|
||
continue
|
||
}
|
||
if len(tok) > 2 {
|
||
prefix := strings.ToUpper(tok[:2])
|
||
if _, ok := nameByCode[prefix]; !ok {
|
||
continue
|
||
}
|
||
suffix := tok[2:]
|
||
if suffix == "" {
|
||
continue
|
||
}
|
||
allDigits := true
|
||
for _, r := range suffix {
|
||
if r < '0' || r > '9' {
|
||
allDigits = false
|
||
break
|
||
}
|
||
}
|
||
if allDigits {
|
||
return prefix
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// Name returns Chinese display name for a country code.
|
||
func Name(code string) string {
|
||
if n, ok := nameByCode[strings.ToUpper(code)]; ok {
|
||
return n
|
||
}
|
||
return code
|
||
}
|
||
|
||
// GroupRuntime returns Mihomo proxy-group name for a country pool.
|
||
func GroupRuntime(code string) string {
|
||
return "country_" + strings.ToUpper(strings.TrimSpace(code))
|
||
}
|
||
|
||
// TargetAlias returns rule target alias stored in DB, e.g. country:HK.
|
||
func TargetAlias(code string) string {
|
||
return "country:" + strings.ToUpper(strings.TrimSpace(code))
|
||
}
|
||
|
||
// ParseTargetAlias parses country:XX to code, ok false if not a country target.
|
||
func ParseTargetAlias(target string) (string, bool) {
|
||
if !strings.HasPrefix(strings.ToLower(target), "country:") {
|
||
return "", false
|
||
}
|
||
code := strings.TrimSpace(target[len("country:"):])
|
||
if code == "" {
|
||
return "", false
|
||
}
|
||
return strings.ToUpper(code), true
|
||
}
|
||
|
||
// All returns catalog entries for UI.
|
||
func All() []struct{ Code, Name string } {
|
||
out := make([]struct{ Code, Name string }, 0, len(catalog))
|
||
for _, e := range catalog {
|
||
out = append(out, struct{ Code, Name string }{Code: e.Code, Name: e.Name})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func keywordMatches(text, kw string) bool {
|
||
if !strings.Contains(text, kw) {
|
||
return false
|
||
}
|
||
if containsHan(kw) {
|
||
return true
|
||
}
|
||
start := 0
|
||
for {
|
||
idx := strings.Index(text[start:], kw)
|
||
if idx < 0 {
|
||
return false
|
||
}
|
||
pos := start + idx
|
||
before := pos == 0 || !isASCIIAlphaNum(text[pos-1])
|
||
afterEnd := pos + len(kw)
|
||
after := afterEnd >= len(text) || !isASCIIAlphaNum(text[afterEnd])
|
||
if before && after {
|
||
return true
|
||
}
|
||
start = pos + 1
|
||
}
|
||
}
|
||
|
||
func isASCIIAlphaNum(b byte) bool {
|
||
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9')
|
||
}
|
||
|
||
func containsHan(s string) bool {
|
||
for _, r := range s {
|
||
if r >= 0x4e00 && r <= 0x9fff {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|