feat: GeoIP-based geo detection + configurable rename template

- Add MaxMind GeoLite2-City mmdb auto-download and IP-based geo lookup
  (covers all 249 countries globally, replacing limited 8-country regex)
- Add RenameOptions with 5 toggleable fields (flag/alias/country/city/index)
  stored as rename_options_json on collections
- Collection rename pipeline uses DetectGeoWithServer(name, server) which
  tries GeoIP lookup first, falls back to name-based regex matching
- Chinese country/city names + emoji flag prefix
- Country-city dedup: skip city when it equals country (e.g. 香港/香港)
- URI fragment encoding: url.QueryEscape -> url.PathEscape (%20 not +)
- Egress cache key excludes name field so renamed nodes still match cache
- Frontend: 5 toggle buttons in collection edit form, default all on
- Migrations: 0004 (source alias + collection rename_enabled),
  0005 (collection rename_options_json)
This commit is contained in:
2026-07-28 14:44:54 +08:00
parent 389c0d172a
commit de9ac92ede
29 changed files with 1256 additions and 225 deletions
+9 -9
View File
@@ -69,7 +69,7 @@ func ToProxyUri(proxy model.ProxyNode) string {
url.PathEscape(getString(proxy, "uuid")),
server, port,
params.Encode(),
url.QueryEscape(name))
url.PathEscape(name))
case "hysteria2":
params := url.Values{}
@@ -89,7 +89,7 @@ func ToProxyUri(proxy model.ProxyNode) string {
url.PathEscape(getString(proxy, "password")),
server, port,
params.Encode(),
url.QueryEscape(name))
url.PathEscape(name))
case "hysteria":
params := url.Values{}
@@ -118,7 +118,7 @@ func ToProxyUri(proxy model.ProxyNode) string {
url.PathEscape(getString(proxy, "auth_str")),
server, port,
params.Encode(),
url.QueryEscape(name))
url.PathEscape(name))
case "anytls":
params := url.Values{}
@@ -135,7 +135,7 @@ func ToProxyUri(proxy model.ProxyNode) string {
url.PathEscape(getString(proxy, "password")),
server, port,
params.Encode(),
url.QueryEscape(name))
url.PathEscape(name))
case "tuic":
params := url.Values{}
@@ -162,7 +162,7 @@ func ToProxyUri(proxy model.ProxyNode) string {
url.PathEscape(getString(proxy, "password")),
server, port,
params.Encode(),
url.QueryEscape(name))
url.PathEscape(name))
case "trojan":
params := url.Values{}
@@ -176,7 +176,7 @@ func ToProxyUri(proxy model.ProxyNode) string {
url.PathEscape(getString(proxy, "password")),
server, port,
params.Encode(),
url.QueryEscape(name))
url.PathEscape(name))
case "socks5":
params := url.Values{}
@@ -208,14 +208,14 @@ func ToProxyUri(proxy model.ProxyNode) string {
scheme = "https"
}
return fmt.Sprintf("%s://%s%s:%d#%s",
scheme, auth, server, port, url.QueryEscape(name))
scheme, auth, server, port, url.PathEscape(name))
case "ss":
userInfo := util.Base64Utf8(fmt.Sprintf("%s:%s@%s:%d",
getString(proxy, "cipher"),
getString(proxy, "password"),
server, port))
return fmt.Sprintf("ss://%s#%s", userInfo, url.QueryEscape(name))
return fmt.Sprintf("ss://%s#%s", userInfo, url.PathEscape(name))
case "ssr":
main := strings.Join([]string{
@@ -257,7 +257,7 @@ func ToProxyUri(proxy model.ProxyNode) string {
url.PathEscape(getString(proxy, "private-key")),
server, port,
params.Encode(),
url.QueryEscape(name))
url.PathEscape(name))
case "vmess":
wsOpts, _ := proxy["ws-opts"].(map[string]any)