feat: unified 'To' field for Proxy/Rewrite, support KV map configs, and auto-detect HTTP protocol for simple ports (by AI)

This commit is contained in:
AI Engineer 2026-05-13 12:03:00 +08:00
parent 1bf819281a
commit 8a2f76ffc9
6 changed files with 99 additions and 36 deletions

View File

@ -1,6 +1,7 @@
package service package service
import ( import (
"apigo.cc/go/cast"
"path/filepath" "path/filepath"
) )
@ -66,8 +67,8 @@ type ServiceConfig struct {
StopTimeout int // 停止服务的超时时间 (ms) StopTimeout int // 停止服务的超时时间 (ms)
// 从配置文件中加载的静态路由策略 (按 Host 分组,全局配置用 "" 或 "*") // 从配置文件中加载的静态路由策略 (按 Host 分组,全局配置用 "" 或 "*")
Proxies map[string][]ProxyRule Proxies map[string]map[string]any
Rewrites map[string][]RewriteRule Rewrites map[string]map[string]any
Statics map[string]map[string]string Statics map[string]map[string]string
} }
@ -78,31 +79,53 @@ func ApplyConfig() {
hostPoliciesLock.Lock() hostPoliciesLock.Lock()
defer hostPoliciesLock.Unlock() defer hostPoliciesLock.Unlock()
// 清理旧的 file 策略 // 1. Proxies KV 解析
fileProxies = make(map[string][]*proxyType) fileProxies = make(map[string][]*proxyType)
fileRewrites = make(map[string][]*rewriteType) for host, kv := range Config.Proxies {
for host, rules := range Config.Proxies {
if host == "*" { if host == "*" {
host = "" host = ""
} }
newProxies := make([]*proxyType, 0, len(rules)) rules := make([]*proxyType, 0, len(kv))
for _, r := range rules { for path, val := range kv {
newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath)) if to, ok := val.(string); ok {
// 极简 KV 模式: "/api/*": "user-svc/v1/*"
rules = append(rules, parseProxyRule(0, path, "", "", to))
} else {
// 对象模式: "/api/*": {"To": "...", "Auth": 1}
m, _ := cast.ToMap[string, any](val)
rules = append(rules, parseProxyRule(
cast.Int(m["Auth"]),
path,
cast.String(m["ToApp"]),
cast.String(m["ToPath"]),
cast.String(m["To"]),
))
}
} }
fileProxies[host] = newProxies fileProxies[host] = rules
rebuildProxiesUnderLock(host) rebuildProxiesUnderLock(host)
} }
for host, rules := range Config.Rewrites { // 2. Rewrites KV 解析
fileRewrites = make(map[string][]*rewriteType)
for host, kv := range Config.Rewrites {
if host == "*" { if host == "*" {
host = "" host = ""
} }
newRewrites := make([]*rewriteType, 0, len(rules)) rules := make([]*rewriteType, 0, len(kv))
for _, r := range rules { for path, val := range kv {
newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath)) if to, ok := val.(string); ok {
rules = append(rules, parseRewriteRule(path, "", to))
} else {
m, _ := cast.ToMap[string, any](val)
rules = append(rules, parseRewriteRule(
path,
cast.String(m["ToPath"]),
cast.String(m["To"]),
))
}
} }
fileRewrites[host] = newRewrites fileRewrites[host] = rules
rebuildRewritesUnderLock(host) rebuildRewritesUnderLock(host)
} }

View File

@ -22,7 +22,40 @@ type proxyType struct {
toPrefix string toPrefix string
} }
func parseProxyRule(authLevel int, path, toApp, toPath string) *proxyType { func parseTo(to string) (app, path string) {
if to == "" {
return "", "/"
}
// 查找协议
if strings.Contains(to, "://") {
protocolPart, rest, _ := strings.Cut(to, "://")
// 协议后的第一个 /
if firstSlash := strings.Index(rest, "/"); firstSlash != -1 {
app = protocolPart + "://" + rest[:firstSlash]
path = rest[firstSlash:]
} else {
app = to
path = "/"
}
} else {
// 无协议,第一个 / 之前是 app
if firstSlash := strings.Index(to, "/"); firstSlash != -1 {
app = to[:firstSlash]
path = to[firstSlash:]
} else {
app = to
path = "/"
}
}
return
}
func parseProxyRule(authLevel int, path, toApp, toPath string, to string) *proxyType {
if to != "" {
toApp, toPath = parseTo(to)
}
p := &proxyType{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath} p := &proxyType{authLevel: authLevel, fromPath: path, toApp: toApp, toPath: toPath}
if strings.ContainsRune(path, '(') { if strings.ContainsRune(path, '(') {
matcher, err := regexp.Compile("^" + path + "$") matcher, err := regexp.Compile("^" + path + "$")
@ -41,8 +74,8 @@ func parseProxyRule(authLevel int, path, toApp, toPath string) *proxyType {
return p return p
} }
func (hc *HostContext) Proxy(authLevel int, path string, toApp, toPath string) *HostContext { func (hc *HostContext) Proxy(authLevel int, path string, to string) *HostContext {
p := parseProxyRule(authLevel, path, toApp, toPath) p := parseProxyRule(authLevel, path, "", "", to)
hostPoliciesLock.Lock() hostPoliciesLock.Lock()
defer hostPoliciesLock.Unlock() defer hostPoliciesLock.Unlock()
@ -199,15 +232,16 @@ func copyResponse(res *gohttp.Result, response *Response, logger *log.Logger) {
type ProxyRule struct { type ProxyRule struct {
Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$ Path string // 匹配路径或正则,支持变量捕获如 ^/api/(.*)$
AuthLevel int // 所需鉴权级别 AuthLevel int // 所需鉴权级别
ToApp string // 目标 AppName 或完整 URL (可含 $1 变量替换) To string // 目标地址,格式为 "app/path" 或 "http://url/path" (支持后缀 /* 映射)
ToPath string // 目标路径 (可含 $1 变量替换) ToApp string // [Deprecated] 目标 AppName 或完整 URL (可含 $1 变量替换)
ToPath string // [Deprecated] 目标路径 (可含 $1 变量替换)
} }
// ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的动态代理规则,不影响通过代码或文件写入的固定规则 // ReplaceProxies 使用全量指针替换的方式 (Copy-on-Write) 无缝更新指定 host 的动态代理规则
func ReplaceProxies(host string, rules []ProxyRule) { func ReplaceProxies(host string, rules []ProxyRule) {
newProxies := make([]*proxyType, 0, len(rules)) newProxies := make([]*proxyType, 0, len(rules))
for _, r := range rules { for _, r := range rules {
newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath)) newProxies = append(newProxies, parseProxyRule(r.AuthLevel, r.Path, r.ToApp, r.ToPath, r.To))
} }
hostPoliciesLock.Lock() hostPoliciesLock.Lock()

View File

@ -43,7 +43,7 @@ func TestProxyDirect(t *testing.T) {
defer backend.Close() defer backend.Close()
// 注册代理规则 // 注册代理规则
Host("*").Proxy(0, "/proxy", backend.URL, "/hello") Host("*").Proxy(0, "/proxy", backend.URL+"/hello")
rh := &RouteHandler{} rh := &RouteHandler{}
req := httptest.NewRequest("GET", "/proxy", nil) req := httptest.NewRequest("GET", "/proxy", nil)

View File

@ -17,7 +17,10 @@ type rewriteType struct {
toPrefix string toPrefix string
} }
func parseRewriteRule(fromPath, toPath string) *rewriteType { func parseRewriteRule(fromPath, toPath, to string) *rewriteType {
if to != "" {
toPath = to
}
s := &rewriteType{fromPath: fromPath, toPath: toPath} s := &rewriteType{fromPath: fromPath, toPath: toPath}
if strings.ContainsRune(fromPath, '(') { if strings.ContainsRune(fromPath, '(') {
matcher, err := regexp.Compile("^" + fromPath + "$") matcher, err := regexp.Compile("^" + fromPath + "$")
@ -36,8 +39,8 @@ func parseRewriteRule(fromPath, toPath string) *rewriteType {
return s return s
} }
func (hc *HostContext) Rewrite(path string, toPath string) *HostContext { func (hc *HostContext) Rewrite(path string, to string) *HostContext {
s := parseRewriteRule(path, toPath) s := parseRewriteRule(path, "", to)
hostPoliciesLock.Lock() hostPoliciesLock.Lock()
defer hostPoliciesLock.Unlock() defer hostPoliciesLock.Unlock()
@ -135,14 +138,15 @@ func processRewrite(request *Request, response *Response, logger *log.Logger) bo
// RewriteRule 定义了外部传递的 URL 重写规则 // RewriteRule 定义了外部传递的 URL 重写规则
type RewriteRule struct { type RewriteRule struct {
Path string // 原始路径或匹配正则,例如 ^/old/(.*)$ Path string // 原始路径或匹配正则,例如 ^/old/(.*)$
ToPath string // 重写后的路径,例如 /new/$1 To string // 目标路径或完整 URL例如 /new/$1
ToPath string // [Deprecated] 重写后的路径
} }
// ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的动态重写规则。 // ReplaceRewrites 使用 Copy-on-Write 机制原子地替换指定 host 下的动态重写规则。
func ReplaceRewrites(host string, rules []RewriteRule) { func ReplaceRewrites(host string, rules []RewriteRule) {
newRewrites := make([]*rewriteType, 0, len(rules)) newRewrites := make([]*rewriteType, 0, len(rules))
for _, r := range rules { for _, r := range rules {
newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath)) newRewrites = append(newRewrites, parseRewriteRule(r.Path, r.ToPath, r.To))
} }
hostPoliciesLock.Lock() hostPoliciesLock.Lock()

View File

@ -55,14 +55,18 @@ func (ws *WebServer) Start(ctx context.Context, logger *log.Logger) error {
part := strings.Split(listenStr, "|")[0] part := strings.Split(listenStr, "|")[0]
addr, opts, _ := strings.Cut(part, ",") addr, opts, _ := strings.Cut(part, ",")
protocol := "http" protocol := ""
for _, opt := range strings.Split(opts, ",") { for _, opt := range strings.Split(opts, ",") {
opt = strings.ToLower(strings.TrimSpace(opt)) opt = strings.ToLower(strings.TrimSpace(opt))
if opt == "h2c" || opt == "h2" { if opt == "h2c" || opt == "h2" || opt == "http" || opt == "https" {
protocol = opt protocol = opt
} }
} }
if protocol == "" {
protocol = "http" // Default to http
}
if !strings.Contains(addr, ":") { if !strings.Contains(addr, ":") {
addr = ":" + addr addr = ":" + addr
} }

View File

@ -260,16 +260,14 @@ func (gc *GroupContext) WebSocket(path string, serviceFunc any) *websocketServic
return gc.hc.WebSocket(gc.prefix+path, serviceFunc) return gc.hc.WebSocket(gc.prefix+path, serviceFunc)
} }
func (gc *GroupContext) Rewrite(path string, toPath string) *GroupContext { func (gc *GroupContext) Rewrite(path string, to string) *GroupContext {
gc.hc.Rewrite(gc.prefix+path, toPath) gc.hc.Rewrite(gc.prefix+path, to)
return gc return gc
} }
func (gc *GroupContext) Proxy(authLevel int, path string, to string) *GroupContext {
func (gc *GroupContext) Proxy(authLevel int, path string, toApp, toPath string) *GroupContext { gc.hc.Proxy(authLevel, gc.prefix+path, to)
gc.hc.Proxy(authLevel, gc.prefix+path, toApp, toPath)
return gc return gc
} }
func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType { func (hc *HostContext) WebSocket(path string, serviceFunc any) *websocketServiceType {
funcType := reflect.TypeOf(serviceFunc) funcType := reflect.TypeOf(serviceFunc)
if funcType.Kind() != reflect.Func { if funcType.Kind() != reflect.Func {